1use oxicuda_ptx::prelude::*;
15
16use crate::error::{SparseError, SparseResult};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum StoragePrecision {
25 Fp16,
27 Bf16,
29}
30
31impl StoragePrecision {
32 #[must_use]
34 pub const fn ptx_type(self) -> PtxType {
35 match self {
36 Self::Fp16 => PtxType::F16,
37 Self::Bf16 => PtxType::BF16,
38 }
39 }
40
41 #[must_use]
43 pub const fn packed_ptx_type(self) -> PtxType {
44 match self {
45 Self::Fp16 => PtxType::F16x2,
46 Self::Bf16 => PtxType::BF16x2,
47 }
48 }
49
50 #[must_use]
52 pub const fn element_bytes(self) -> u32 {
53 2
54 }
55
56 #[must_use]
58 pub const fn mantissa_bits(self) -> u32 {
59 match self {
60 Self::Fp16 => 10,
61 Self::Bf16 => 7,
62 }
63 }
64
65 #[must_use]
67 pub const fn suffix(self) -> &'static str {
68 match self {
69 Self::Fp16 => "f16",
70 Self::Bf16 => "bf16",
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum ComputePrecision {
78 Fp32,
80 Fp64,
82}
83
84impl ComputePrecision {
85 #[must_use]
87 pub const fn ptx_type(self) -> PtxType {
88 match self {
89 Self::Fp32 => PtxType::F32,
90 Self::Fp64 => PtxType::F64,
91 }
92 }
93
94 #[must_use]
96 pub const fn element_bytes(self) -> u32 {
97 match self {
98 Self::Fp32 => 4,
99 Self::Fp64 => 8,
100 }
101 }
102
103 #[must_use]
105 pub const fn suffix(self) -> &'static str {
106 match self {
107 Self::Fp32 => "f32",
108 Self::Fp64 => "f64",
109 }
110 }
111
112 #[must_use]
114 pub const fn bit_suffix(self) -> &'static str {
115 match self {
116 Self::Fp32 => "b32",
117 Self::Fp64 => "b64",
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub enum MixedSpMVAlgo {
125 Scalar,
127 Vector,
129 VectorPacked,
132 Auto,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub struct MixedPrecisionConfig {
139 pub storage_precision: StoragePrecision,
141 pub compute_precision: ComputePrecision,
143 pub algorithm: MixedSpMVAlgo,
145 pub sm_version: SmVersion,
147}
148
149impl MixedPrecisionConfig {
150 #[must_use]
152 pub const fn fp16_fp32(algo: MixedSpMVAlgo, sm: SmVersion) -> Self {
153 Self {
154 storage_precision: StoragePrecision::Fp16,
155 compute_precision: ComputePrecision::Fp32,
156 algorithm: algo,
157 sm_version: sm,
158 }
159 }
160
161 #[must_use]
163 pub const fn bf16_fp32(algo: MixedSpMVAlgo, sm: SmVersion) -> Self {
164 Self {
165 storage_precision: StoragePrecision::Bf16,
166 compute_precision: ComputePrecision::Fp32,
167 algorithm: algo,
168 sm_version: sm,
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
182pub struct MixedPrecisionPlan {
183 pub config: MixedPrecisionConfig,
185 pub rows: u32,
187 pub cols: u32,
189 pub nnz: u64,
191 pub values_bytes: u64,
193 pub values_bytes_full: u64,
195}
196
197impl MixedPrecisionPlan {
198 #[must_use]
203 pub fn bandwidth_savings_ratio(&self) -> f64 {
204 if self.values_bytes == 0 {
205 return 1.0;
206 }
207 self.values_bytes_full as f64 / self.values_bytes as f64
208 }
209
210 #[must_use]
216 pub fn estimated_gflops(&self, peak_bandwidth_gb_s: f64) -> f64 {
217 if self.nnz == 0 {
218 return 0.0;
219 }
220 let bytes_per_nnz = self.config.storage_precision.element_bytes() as f64
222 + 4.0
223 + self.config.compute_precision.element_bytes() as f64;
224 let total_bytes = bytes_per_nnz * self.nnz as f64;
225 let total_flops = 2.0 * self.nnz as f64;
227 let bandwidth_bytes_s = peak_bandwidth_gb_s * 1e9;
229 let time_s = total_bytes / bandwidth_bytes_s;
231 total_flops / time_s / 1e9
233 }
234
235 #[must_use]
237 pub fn avg_nnz_per_row(&self) -> f64 {
238 if self.rows == 0 {
239 return 0.0;
240 }
241 self.nnz as f64 / self.rows as f64
242 }
243}
244
245#[derive(Debug, Clone)]
247pub struct MixedPrecisionStats {
248 pub elapsed_us: f64,
250 pub gflops: f64,
252 pub bandwidth_gb_s: f64,
254 pub precision_loss_bound: f64,
256}
257
258const VECTOR_THRESHOLD: f64 = 4.0;
264
265const PACKED_THRESHOLD: f64 = 32.0;
267
268const SCALAR_BLOCK_SIZE: u32 = 256;
270
271const VECTOR_BLOCK_SIZE: u32 = 256;
273
274pub fn plan_mixed_precision_spmv(
288 config: &MixedPrecisionConfig,
289 rows: u32,
290 cols: u32,
291 nnz: u64,
292) -> SparseResult<MixedPrecisionPlan> {
293 validate_mixed_precision_config(config)?;
294
295 let avg_nnz = if rows > 0 {
296 nnz as f64 / rows as f64
297 } else {
298 0.0
299 };
300
301 let resolved_algo = match config.algorithm {
303 MixedSpMVAlgo::Auto => {
304 if avg_nnz >= PACKED_THRESHOLD {
305 MixedSpMVAlgo::VectorPacked
306 } else if avg_nnz >= VECTOR_THRESHOLD {
307 MixedSpMVAlgo::Vector
308 } else {
309 MixedSpMVAlgo::Scalar
310 }
311 }
312 other => other,
313 };
314
315 let resolved_config = MixedPrecisionConfig {
316 algorithm: resolved_algo,
317 ..*config
318 };
319
320 let values_bytes = nnz * config.storage_precision.element_bytes() as u64;
321 let values_bytes_full = nnz * config.compute_precision.element_bytes() as u64;
322
323 Ok(MixedPrecisionPlan {
324 config: resolved_config,
325 rows,
326 cols,
327 nnz,
328 values_bytes,
329 values_bytes_full,
330 })
331}
332
333pub fn validate_mixed_precision_config(config: &MixedPrecisionConfig) -> SparseResult<()> {
346 if config.storage_precision == StoragePrecision::Bf16 {
348 let (major, _minor) = config.sm_version.ptx_isa_version();
349 if major < 7 {
350 return Err(SparseError::InvalidArgument(
351 "BF16 storage requires sm_80 (Ampere) or newer; \
352 the selected SM version does not support BF16 instructions"
353 .to_string(),
354 ));
355 }
356 }
357
358 if config.compute_precision == ComputePrecision::Fp64 {
361 }
364
365 Ok(())
366}
367
368#[must_use]
384pub fn estimate_precision_loss(nnz_per_row: f64, storage: StoragePrecision) -> f64 {
385 let eps_storage = match storage {
386 StoragePrecision::Fp16 => f64::powi(2.0, -11), StoragePrecision::Bf16 => f64::powi(2.0, -8), };
389 let eps_compute: f64 = f64::powi(2.0, -24); nnz_per_row * eps_storage + nnz_per_row * eps_compute
393}
394
395pub fn generate_mixed_scalar_spmv_ptx(
422 config: &MixedPrecisionConfig,
423) -> Result<String, PtxGenError> {
424 let storage = config.storage_precision;
425 let compute = config.compute_precision;
426 let sm = config.sm_version;
427 let storage_suffix = storage.suffix();
428 let compute_suffix = compute.suffix();
429 let compute_bit = compute.bit_suffix();
430 let elem_bytes = storage.element_bytes();
431
432 KernelBuilder::new("mixed_spmv_scalar")
433 .target(sm)
434 .param("row_ptr", PtxType::U64)
435 .param("col_idx", PtxType::U64)
436 .param("values", PtxType::U64)
437 .param("x_ptr", PtxType::U64)
438 .param("y_ptr", PtxType::U64)
439 .param("alpha_bits", PtxType::U32)
440 .param("beta_bits", PtxType::U32)
441 .param("num_rows", PtxType::U32)
442 .body(move |b| {
443 let gid = b.global_thread_id_x();
444 let num_rows = b.load_param_u32("num_rows");
445
446 let gid_inner = gid.clone();
447 b.if_lt_u32(gid, num_rows, move |b| {
448 let row = gid_inner;
449 let row_ptr_base = b.load_param_u64("row_ptr");
450 let col_idx_base = b.load_param_u64("col_idx");
451 let values_base = b.load_param_u64("values");
452 let x_ptr = b.load_param_u64("x_ptr");
453 let y_ptr = b.load_param_u64("y_ptr");
454
455 let alpha_bits = b.load_param_u32("alpha_bits");
457 let alpha = b.alloc_reg(compute.ptx_type());
458 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
459
460 let beta_bits = b.load_param_u32("beta_bits");
461 let beta = b.alloc_reg(compute.ptx_type());
462 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
463
464 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
466 let row_start = b.load_global_i32(rp_addr);
467
468 let row_plus_1 = b.alloc_reg(PtxType::U32);
469 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
470 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
471 let row_end = b.load_global_i32(rp_addr_next);
472
473 let acc = b.alloc_reg(compute.ptx_type());
475 let zero_bits: u32 = 0u32;
476 b.raw_ptx(&format!("mov.{compute_bit} {acc}, 0F{zero_bits:08X};"));
477
478 let loop_label = b.fresh_label("mpspmv_loop");
480 let done_label = b.fresh_label("mpspmv_done");
481
482 let k = b.alloc_reg(PtxType::U32);
483 let rs_u32 = b.alloc_reg(PtxType::U32);
484 b.raw_ptx(&format!("mov.b32 {rs_u32}, {row_start};"));
485 b.raw_ptx(&format!("mov.u32 {k}, {rs_u32};"));
486
487 let re_u32 = b.alloc_reg(PtxType::U32);
488 b.raw_ptx(&format!("mov.b32 {re_u32}, {row_end};"));
489
490 b.label(&loop_label);
491 let pred = b.alloc_reg(PtxType::Pred);
492 b.raw_ptx(&format!("setp.lo.u32 {pred}, {k}, {re_u32};"));
493 b.raw_ptx(&format!("@!{pred} bra {done_label};"));
494
495 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
497 let col = b.load_global_i32(ci_addr);
498 let col_u32 = b.alloc_reg(PtxType::U32);
499 b.raw_ptx(&format!("mov.b32 {col_u32}, {col};"));
500
501 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
503 let val_half = b.alloc_reg(storage.ptx_type());
504 b.raw_ptx(&format!(
505 "ld.global.{storage_suffix} {val_half}, [{v_addr}];"
506 ));
507 let val_fp32 = b.alloc_reg(compute.ptx_type());
508 b.raw_ptx(&format!(
509 "cvt.{compute_suffix}.{storage_suffix} {val_fp32}, {val_half};"
510 ));
511
512 let x_addr = b.byte_offset_addr(x_ptr.clone(), col_u32, compute.element_bytes());
514 let x_val = b.load_global_f32(x_addr);
515
516 let new_acc = b.fma_f32(val_fp32, x_val, acc.clone());
518 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {new_acc};"));
519
520 b.raw_ptx(&format!("add.u32 {k}, {k}, 1;"));
522 b.branch(&loop_label);
523 b.label(&done_label);
524
525 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
527 let y_old = b.load_global_f32(y_addr.clone());
528
529 let alpha_acc = b.alloc_reg(compute.ptx_type());
530 b.raw_ptx(&format!(
531 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {acc};"
532 ));
533
534 let beta_y = b.alloc_reg(compute.ptx_type());
535 b.raw_ptx(&format!(
536 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
537 ));
538
539 let result = b.alloc_reg(compute.ptx_type());
540 b.raw_ptx(&format!(
541 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
542 ));
543
544 b.store_global_f32(y_addr, result);
545 });
546
547 b.ret();
548 })
549 .build()
550}
551
552pub fn generate_mixed_vector_spmv_ptx(
566 config: &MixedPrecisionConfig,
567) -> Result<String, PtxGenError> {
568 let storage = config.storage_precision;
569 let compute = config.compute_precision;
570 let sm = config.sm_version;
571 let storage_suffix = storage.suffix();
572 let compute_suffix = compute.suffix();
573 let compute_bit = compute.bit_suffix();
574 let elem_bytes = storage.element_bytes();
575
576 KernelBuilder::new("mixed_spmv_vector")
577 .target(sm)
578 .param("row_ptr", PtxType::U64)
579 .param("col_idx", PtxType::U64)
580 .param("values", PtxType::U64)
581 .param("x_ptr", PtxType::U64)
582 .param("y_ptr", PtxType::U64)
583 .param("alpha_bits", PtxType::U32)
584 .param("beta_bits", PtxType::U32)
585 .param("num_rows", PtxType::U32)
586 .body(move |b| {
587 let tid_global = b.global_thread_id_x();
588 let num_rows = b.load_param_u32("num_rows");
589
590 let lane = b.alloc_reg(PtxType::U32);
592 b.raw_ptx(&format!("and.b32 {lane}, {tid_global}, 31;"));
593
594 let warp_id = b.alloc_reg(PtxType::U32);
596 b.raw_ptx(&format!("shr.u32 {warp_id}, {tid_global}, 5;"));
597
598 let warp_id_inner = warp_id.clone();
599 let lane_inner = lane.clone();
600 b.if_lt_u32(warp_id, num_rows, move |b| {
601 let row = warp_id_inner;
602 let lane = lane_inner;
603
604 let row_ptr_base = b.load_param_u64("row_ptr");
605 let col_idx_base = b.load_param_u64("col_idx");
606 let values_base = b.load_param_u64("values");
607 let x_ptr = b.load_param_u64("x_ptr");
608 let y_ptr = b.load_param_u64("y_ptr");
609
610 let alpha_bits = b.load_param_u32("alpha_bits");
611 let alpha = b.alloc_reg(compute.ptx_type());
612 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
613
614 let beta_bits = b.load_param_u32("beta_bits");
615 let beta = b.alloc_reg(compute.ptx_type());
616 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
617
618 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
620 let row_start_i32 = b.load_global_i32(rp_addr);
621 let row_start = b.alloc_reg(PtxType::U32);
622 b.raw_ptx(&format!("mov.b32 {row_start}, {row_start_i32};"));
623
624 let row_plus_1 = b.alloc_reg(PtxType::U32);
625 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
626 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
627 let row_end_i32 = b.load_global_i32(rp_addr_next);
628 let row_end = b.alloc_reg(PtxType::U32);
629 b.raw_ptx(&format!("mov.b32 {row_end}, {row_end_i32};"));
630
631 let acc = b.alloc_reg(compute.ptx_type());
633 let zero_bits: u32 = 0u32;
634 b.raw_ptx(&format!("mov.{compute_bit} {acc}, 0F{zero_bits:08X};"));
635
636 let k = b.alloc_reg(PtxType::U32);
637 b.raw_ptx(&format!("add.u32 {k}, {row_start}, {lane};"));
638
639 let loop_label = b.fresh_label("mpspmv_vloop");
640 let done_label = b.fresh_label("mpspmv_vdone");
641
642 b.label(&loop_label);
643 let pred = b.alloc_reg(PtxType::Pred);
644 b.raw_ptx(&format!("setp.lo.u32 {pred}, {k}, {row_end};"));
645 b.raw_ptx(&format!("@!{pred} bra {done_label};"));
646
647 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
649 let col_i32 = b.load_global_i32(ci_addr);
650 let col_u32 = b.alloc_reg(PtxType::U32);
651 b.raw_ptx(&format!("mov.b32 {col_u32}, {col_i32};"));
652
653 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
654 let val_half = b.alloc_reg(storage.ptx_type());
655 b.raw_ptx(&format!("ld.global.{storage_suffix} {val_half}, [{v_addr}];"));
656 let val_fp32 = b.alloc_reg(compute.ptx_type());
657 b.raw_ptx(&format!(
658 "cvt.{compute_suffix}.{storage_suffix} {val_fp32}, {val_half};"
659 ));
660
661 let x_addr = b.byte_offset_addr(
662 x_ptr.clone(),
663 col_u32,
664 compute.element_bytes(),
665 );
666 let x_val = b.load_global_f32(x_addr);
667
668 let new_acc = b.fma_f32(val_fp32, x_val, acc.clone());
670 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {new_acc};"));
671
672 b.raw_ptx(&format!("add.u32 {k}, {k}, 32;"));
674 b.branch(&loop_label);
675 b.label(&done_label);
676
677 let mut current = acc;
679 for offset in [16u32, 8, 4, 2, 1] {
680 let shuffled = b.alloc_reg(compute.ptx_type());
681 b.raw_ptx(&format!(
682 "shfl.sync.down.{compute_bit} {shuffled}, {current}, {offset}, 31, 0xFFFFFFFF;"
683 ));
684 let sum = b.alloc_reg(compute.ptx_type());
685 b.raw_ptx(&format!(
686 "add.{compute_suffix} {sum}, {current}, {shuffled};"
687 ));
688 current = sum;
689 }
690
691 let is_lane_0 = b.alloc_reg(PtxType::Pred);
693 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0}, {lane}, 0;"));
694
695 let skip_label = b.fresh_label("mpspmv_skip");
696 b.raw_ptx(&format!("@!{is_lane_0} bra {skip_label};"));
697
698 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
699 let y_old = b.load_global_f32(y_addr.clone());
700
701 let alpha_acc = b.alloc_reg(compute.ptx_type());
702 b.raw_ptx(&format!(
703 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {current};"
704 ));
705 let beta_y = b.alloc_reg(compute.ptx_type());
706 b.raw_ptx(&format!(
707 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
708 ));
709 let result = b.alloc_reg(compute.ptx_type());
710 b.raw_ptx(&format!(
711 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
712 ));
713
714 b.store_global_f32(y_addr, result);
715
716 b.label(&skip_label);
717 });
718
719 b.ret();
720 })
721 .build()
722}
723
724pub fn generate_packed_vector_spmv_ptx(
742 config: &MixedPrecisionConfig,
743) -> Result<String, PtxGenError> {
744 let storage = config.storage_precision;
745 let compute = config.compute_precision;
746 let sm = config.sm_version;
747 let storage_suffix = storage.suffix();
748 let compute_suffix = compute.suffix();
749 let compute_bit = compute.bit_suffix();
750 let elem_bytes = storage.element_bytes();
751
752 KernelBuilder::new("mixed_spmv_packed")
753 .target(sm)
754 .param("row_ptr", PtxType::U64)
755 .param("col_idx", PtxType::U64)
756 .param("values", PtxType::U64)
757 .param("x_ptr", PtxType::U64)
758 .param("y_ptr", PtxType::U64)
759 .param("alpha_bits", PtxType::U32)
760 .param("beta_bits", PtxType::U32)
761 .param("num_rows", PtxType::U32)
762 .body(move |b| {
763 let tid_global = b.global_thread_id_x();
764 let num_rows = b.load_param_u32("num_rows");
765
766 let lane = b.alloc_reg(PtxType::U32);
767 b.raw_ptx(&format!("and.b32 {lane}, {tid_global}, 31;"));
768
769 let warp_id = b.alloc_reg(PtxType::U32);
770 b.raw_ptx(&format!("shr.u32 {warp_id}, {tid_global}, 5;"));
771
772 let warp_id_inner = warp_id.clone();
773 let lane_inner = lane.clone();
774 b.if_lt_u32(warp_id, num_rows, move |b| {
775 let row = warp_id_inner;
776 let lane = lane_inner;
777
778 let row_ptr_base = b.load_param_u64("row_ptr");
779 let col_idx_base = b.load_param_u64("col_idx");
780 let values_base = b.load_param_u64("values");
781 let x_ptr = b.load_param_u64("x_ptr");
782 let y_ptr = b.load_param_u64("y_ptr");
783
784 let alpha_bits = b.load_param_u32("alpha_bits");
785 let alpha = b.alloc_reg(compute.ptx_type());
786 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
787
788 let beta_bits = b.load_param_u32("beta_bits");
789 let beta = b.alloc_reg(compute.ptx_type());
790 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
791
792 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
794 let row_start_i32 = b.load_global_i32(rp_addr);
795 let row_start = b.alloc_reg(PtxType::U32);
796 b.raw_ptx(&format!("mov.b32 {row_start}, {row_start_i32};"));
797
798 let row_plus_1 = b.alloc_reg(PtxType::U32);
799 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
800 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
801 let row_end_i32 = b.load_global_i32(rp_addr_next);
802 let row_end = b.alloc_reg(PtxType::U32);
803 b.raw_ptx(&format!("mov.b32 {row_end}, {row_end_i32};"));
804
805 let nnz_row = b.alloc_reg(PtxType::U32);
807 b.raw_ptx(&format!("sub.u32 {nnz_row}, {row_end}, {row_start};"));
808 let nnz_even = b.alloc_reg(PtxType::U32);
809 b.raw_ptx(&format!("and.b32 {nnz_even}, {nnz_row}, 0xFFFFFFFE;"));
810 let row_end_even = b.alloc_reg(PtxType::U32);
811 b.raw_ptx(&format!("add.u32 {row_end_even}, {row_start}, {nnz_even};"));
812
813 let acc = b.alloc_reg(compute.ptx_type());
815 let zero_bits: u32 = 0u32;
816 b.raw_ptx(&format!("mov.{compute_bit} {acc}, 0F{zero_bits:08X};"));
817
818 let k = b.alloc_reg(PtxType::U32);
821 let lane_x2 = b.alloc_reg(PtxType::U32);
822 b.raw_ptx(&format!("shl.b32 {lane_x2}, {lane}, 1;"));
823 b.raw_ptx(&format!("add.u32 {k}, {row_start}, {lane_x2};"));
824
825 let packed_loop = b.fresh_label("mpspmv_packed_loop");
826 let packed_done = b.fresh_label("mpspmv_packed_done");
827
828 b.label(&packed_loop);
829 let pred_pair = b.alloc_reg(PtxType::Pred);
830 let k_plus_1 = b.alloc_reg(PtxType::U32);
832 b.raw_ptx(&format!("add.u32 {k_plus_1}, {k}, 1;"));
833 b.raw_ptx(&format!("setp.ls.u32 {pred_pair}, {k_plus_1}, {row_end_even};"));
834 b.raw_ptx(&format!("@!{pred_pair} bra {packed_done};"));
835
836 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
839 let packed_val = b.alloc_reg(PtxType::B32);
840 b.raw_ptx(&format!("ld.global.b32 {packed_val}, [{v_addr}];"));
841
842 let val_lo = b.alloc_reg(storage.ptx_type());
844 b.raw_ptx(&format!("mov.b16 {val_lo}, {{0}}; // placeholder"));
845 let lo_bits = b.alloc_reg(PtxType::B16);
847 b.raw_ptx("{ .reg .b16 __hi;");
848 b.raw_ptx(&format!("mov.b32 {{{lo_bits}, __hi}}, {packed_val}; }}"));
849 let val_lo_h = b.alloc_reg(storage.ptx_type());
850 b.raw_ptx(&format!("mov.b16 {val_lo_h}, {lo_bits};"));
851 let val_lo_f32 = b.alloc_reg(compute.ptx_type());
852 b.raw_ptx(&format!(
853 "cvt.{compute_suffix}.{storage_suffix} {val_lo_f32}, {val_lo_h};"
854 ));
855
856 let hi_bits = b.alloc_reg(PtxType::B16);
858 b.raw_ptx("{ .reg .b16 __lo;");
859 b.raw_ptx(&format!("mov.b32 {{__lo, {hi_bits}}}, {packed_val}; }}"));
860 let val_hi_h = b.alloc_reg(storage.ptx_type());
861 b.raw_ptx(&format!("mov.b16 {val_hi_h}, {hi_bits};"));
862 let val_hi_f32 = b.alloc_reg(compute.ptx_type());
863 b.raw_ptx(&format!(
864 "cvt.{compute_suffix}.{storage_suffix} {val_hi_f32}, {val_hi_h};"
865 ));
866
867 let ci_addr_0 = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
869 let col_0_i32 = b.load_global_i32(ci_addr_0);
870 let col_0 = b.alloc_reg(PtxType::U32);
871 b.raw_ptx(&format!("mov.b32 {col_0}, {col_0_i32};"));
872
873 let ci_addr_1 = b.byte_offset_addr(col_idx_base.clone(), k_plus_1.clone(), 4);
874 let col_1_i32 = b.load_global_i32(ci_addr_1);
875 let col_1 = b.alloc_reg(PtxType::U32);
876 b.raw_ptx(&format!("mov.b32 {col_1}, {col_1_i32};"));
877
878 let x_addr_0 = b.byte_offset_addr(x_ptr.clone(), col_0, compute.element_bytes());
879 let x_val_0 = b.load_global_f32(x_addr_0);
880
881 let x_addr_1 = b.byte_offset_addr(x_ptr.clone(), col_1, compute.element_bytes());
882 let x_val_1 = b.load_global_f32(x_addr_1);
883
884 let acc1 = b.fma_f32(val_lo_f32, x_val_0, acc.clone());
886 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc1};"));
887 let acc2 = b.fma_f32(val_hi_f32, x_val_1, acc.clone());
888 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc2};"));
889
890 b.raw_ptx(&format!("add.u32 {k}, {k}, 64;"));
892 b.branch(&packed_loop);
893 b.label(&packed_done);
894
895 let has_remainder = b.alloc_reg(PtxType::Pred);
897 let nnz_odd = b.alloc_reg(PtxType::U32);
898 b.raw_ptx(&format!("and.b32 {nnz_odd}, {nnz_row}, 1;"));
899 b.raw_ptx(&format!("setp.ne.u32 {has_remainder}, {nnz_odd}, 0;"));
900
901 let remainder_done = b.fresh_label("mpspmv_rem_done");
902 b.raw_ptx(&format!("@!{has_remainder} bra {remainder_done};"));
903
904 let is_lane_0_rem = b.alloc_reg(PtxType::Pred);
906 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0_rem}, {lane}, 0;"));
907 b.raw_ptx(&format!("@!{is_lane_0_rem} bra {remainder_done};"));
908
909 let last_idx = b.alloc_reg(PtxType::U32);
911 b.raw_ptx(&format!("sub.u32 {last_idx}, {row_end}, 1;"));
912
913 let last_v_addr =
914 b.byte_offset_addr(values_base.clone(), last_idx.clone(), elem_bytes);
915 let last_val_h = b.alloc_reg(storage.ptx_type());
916 b.raw_ptx(&format!(
917 "ld.global.{storage_suffix} {last_val_h}, [{last_v_addr}];"
918 ));
919 let last_val_f32 = b.alloc_reg(compute.ptx_type());
920 b.raw_ptx(&format!(
921 "cvt.{compute_suffix}.{storage_suffix} {last_val_f32}, {last_val_h};"
922 ));
923
924 let last_ci_addr = b.byte_offset_addr(col_idx_base, last_idx, 4);
925 let last_col_i32 = b.load_global_i32(last_ci_addr);
926 let last_col = b.alloc_reg(PtxType::U32);
927 b.raw_ptx(&format!("mov.b32 {last_col}, {last_col_i32};"));
928
929 let last_x_addr =
930 b.byte_offset_addr(x_ptr, last_col, compute.element_bytes());
931 let last_x_val = b.load_global_f32(last_x_addr);
932
933 let acc_rem = b.fma_f32(last_val_f32, last_x_val, acc.clone());
934 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc_rem};"));
935
936 b.label(&remainder_done);
937
938 let mut current = acc;
940 for offset in [16u32, 8, 4, 2, 1] {
941 let shuffled = b.alloc_reg(compute.ptx_type());
942 b.raw_ptx(&format!(
943 "shfl.sync.down.{compute_bit} {shuffled}, {current}, {offset}, 31, 0xFFFFFFFF;"
944 ));
945 let sum = b.alloc_reg(compute.ptx_type());
946 b.raw_ptx(&format!(
947 "add.{compute_suffix} {sum}, {current}, {shuffled};"
948 ));
949 current = sum;
950 }
951
952 let is_lane_0 = b.alloc_reg(PtxType::Pred);
954 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0}, {lane}, 0;"));
955
956 let skip_label = b.fresh_label("mpspmv_pskip");
957 b.raw_ptx(&format!("@!{is_lane_0} bra {skip_label};"));
958
959 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
960 let y_old = b.load_global_f32(y_addr.clone());
961
962 let alpha_acc = b.alloc_reg(compute.ptx_type());
963 b.raw_ptx(&format!(
964 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {current};"
965 ));
966 let beta_y = b.alloc_reg(compute.ptx_type());
967 b.raw_ptx(&format!(
968 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
969 ));
970 let result = b.alloc_reg(compute.ptx_type());
971 b.raw_ptx(&format!(
972 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
973 ));
974
975 b.store_global_f32(y_addr, result);
976
977 b.label(&skip_label);
978 });
979
980 b.ret();
981 })
982 .build()
983}
984
985#[must_use]
991pub fn scalar_launch_params(num_rows: u32) -> (u32, u32) {
992 let block = SCALAR_BLOCK_SIZE;
993 let grid = num_rows.div_ceil(block);
994 (grid, block)
995}
996
997#[must_use]
999pub fn vector_launch_params(num_rows: u32) -> (u32, u32) {
1000 let block = VECTOR_BLOCK_SIZE;
1001 let warps_per_block = block / 32;
1002 let grid = num_rows.div_ceil(warps_per_block);
1003 (grid, block)
1004}
1005
1006#[cfg(test)]
1011mod tests {
1012 use super::*;
1013
1014 fn default_fp16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1015 MixedPrecisionConfig::fp16_fp32(algo, SmVersion::Sm80)
1016 }
1017
1018 fn default_bf16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1019 MixedPrecisionConfig::bf16_fp32(algo, SmVersion::Sm80)
1020 }
1021
1022 #[test]
1025 fn scalar_ptx_fp16_generates() {
1026 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1027 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1028 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1029 let ptx = ptx.expect("test");
1030 assert!(ptx.contains(".entry mixed_spmv_scalar"));
1031 assert!(ptx.contains(".target sm_80"));
1032 assert!(ptx.contains("cvt.f32.f16"));
1034 }
1035
1036 #[test]
1037 fn scalar_ptx_bf16_generates() {
1038 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1039 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1040 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1041 let ptx = ptx.expect("test");
1042 assert!(ptx.contains("cvt.f32.bf16"));
1043 }
1044
1045 #[test]
1046 fn vector_ptx_fp16_generates() {
1047 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1048 let ptx = generate_mixed_vector_spmv_ptx(&config);
1049 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1050 let ptx = ptx.expect("test");
1051 assert!(ptx.contains(".entry mixed_spmv_vector"));
1052 assert!(ptx.contains("shfl.sync.down"));
1053 assert!(ptx.contains("cvt.f32.f16"));
1054 }
1055
1056 #[test]
1057 fn vector_ptx_bf16_generates() {
1058 let config = default_bf16_config(MixedSpMVAlgo::Vector);
1059 let ptx = generate_mixed_vector_spmv_ptx(&config);
1060 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1061 let ptx = ptx.expect("test");
1062 assert!(ptx.contains("cvt.f32.bf16"));
1063 assert!(ptx.contains("shfl.sync.down"));
1064 }
1065
1066 #[test]
1067 fn packed_ptx_fp16_generates() {
1068 let config = default_fp16_config(MixedSpMVAlgo::VectorPacked);
1069 let ptx = generate_packed_vector_spmv_ptx(&config);
1070 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1071 let ptx = ptx.expect("test");
1072 assert!(ptx.contains(".entry mixed_spmv_packed"));
1073 assert!(ptx.contains("ld.global.b32"));
1075 assert!(ptx.contains("shfl.sync.down"));
1076 }
1077
1078 #[test]
1079 fn packed_ptx_bf16_generates() {
1080 let config = default_bf16_config(MixedSpMVAlgo::VectorPacked);
1081 let ptx = generate_packed_vector_spmv_ptx(&config);
1082 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1083 let ptx = ptx.expect("test");
1084 assert!(ptx.contains("cvt.f32.bf16"));
1085 }
1086
1087 #[test]
1090 fn validate_fp16_on_turing() {
1091 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1092 assert!(validate_mixed_precision_config(&config).is_ok());
1093 }
1094
1095 #[test]
1096 fn validate_bf16_on_turing_fails() {
1097 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1098 let result = validate_mixed_precision_config(&config);
1099 assert!(result.is_err());
1100 let err_msg = format!("{}", result.expect_err("test"));
1101 assert!(err_msg.contains("BF16"));
1102 }
1103
1104 #[test]
1105 fn validate_bf16_on_ampere_ok() {
1106 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1107 assert!(validate_mixed_precision_config(&config).is_ok());
1108 }
1109
1110 #[test]
1113 fn plan_auto_selects_scalar_for_sparse() {
1114 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1115 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 2000);
1117 assert!(plan.is_ok());
1118 let plan = plan.expect("test");
1119 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Scalar);
1120 }
1121
1122 #[test]
1123 fn plan_auto_selects_vector_for_moderate() {
1124 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1125 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 10000);
1127 assert!(plan.is_ok());
1128 let plan = plan.expect("test");
1129 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Vector);
1130 }
1131
1132 #[test]
1133 fn plan_auto_selects_packed_for_dense() {
1134 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1135 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 50000);
1137 assert!(plan.is_ok());
1138 let plan = plan.expect("test");
1139 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::VectorPacked);
1140 }
1141
1142 #[test]
1145 fn bandwidth_savings_fp16_vs_fp32() {
1146 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1147 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1148 let ratio = plan.bandwidth_savings_ratio();
1150 assert!((ratio - 2.0).abs() < 1e-10, "Expected ~2.0, got {ratio}");
1151 }
1152
1153 #[test]
1154 fn estimated_gflops_positive() {
1155 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1156 let plan = plan_mixed_precision_spmv(&config, 10000, 10000, 100000).expect("test");
1157 let gflops = plan.estimated_gflops(1000.0);
1159 assert!(gflops > 0.0, "Expected positive GFLOPS, got {gflops}");
1160 assert!(gflops < 1000.0, "GFLOPS suspiciously high: {gflops}");
1162 }
1163
1164 #[test]
1167 fn precision_loss_fp16_bounded() {
1168 let loss = estimate_precision_loss(100.0, StoragePrecision::Fp16);
1169 assert!(loss > 0.0);
1172 assert!(loss < 0.1, "Precision loss bound too large: {loss}");
1173 }
1174
1175 #[test]
1176 fn precision_loss_bf16_larger_than_fp16() {
1177 let loss_fp16 = estimate_precision_loss(50.0, StoragePrecision::Fp16);
1178 let loss_bf16 = estimate_precision_loss(50.0, StoragePrecision::Bf16);
1179 assert!(
1181 loss_bf16 > loss_fp16,
1182 "BF16 loss ({loss_bf16}) should exceed FP16 loss ({loss_fp16})"
1183 );
1184 }
1185
1186 #[test]
1189 fn scalar_launch_params_correct() {
1190 let (grid, block) = scalar_launch_params(1000);
1191 assert_eq!(block, 256);
1192 assert_eq!(grid, 4); }
1194
1195 #[test]
1196 fn vector_launch_params_correct() {
1197 let (grid, block) = vector_launch_params(1000);
1198 assert_eq!(block, 256);
1199 assert_eq!(grid, 125);
1201 }
1202
1203 #[test]
1210 fn mixed_precision_scalar_ptx_contains_fp16_to_fp32_conversion() {
1211 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1212 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1213 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1214 let ptx = ptx.expect("test");
1215
1216 assert!(
1218 ptx.contains("cvt.f32.f16"),
1219 "scalar kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1220 );
1221 }
1222
1223 #[test]
1227 fn mixed_precision_scalar_ptx_contains_fma_rn_f32_accumulation() {
1228 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1229 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1230 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1231 let ptx = ptx.expect("test");
1232
1233 assert!(
1235 ptx.contains("fma.rn.f32"),
1236 "scalar kernel must contain fma.rn.f32 for FP32 accumulation"
1237 );
1238 }
1239
1240 #[test]
1242 fn mixed_precision_vector_ptx_contains_conversion_and_fma() {
1243 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1244 let ptx = generate_mixed_vector_spmv_ptx(&config);
1245 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1246 let ptx = ptx.expect("test");
1247
1248 assert!(
1249 ptx.contains("cvt.f32.f16"),
1250 "vector kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1251 );
1252 assert!(
1253 ptx.contains("fma.rn.f32"),
1254 "vector kernel must contain fma.rn.f32 for FP32 accumulation"
1255 );
1256 }
1257
1258 #[test]
1260 fn mixed_precision_bf16_ptx_uses_bf16_conversion() {
1261 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1262 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1263 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1264 let ptx = ptx.expect("test");
1265
1266 assert!(
1267 ptx.contains("cvt.f32.bf16"),
1268 "BF16 kernel must contain cvt.f32.bf16 for BF16→FP32 widening"
1269 );
1270 assert!(
1272 !ptx.contains("cvt.f32.f16"),
1273 "BF16 kernel must NOT contain cvt.f32.f16"
1274 );
1275 }
1276
1277 #[test]
1279 fn mixed_precision_scalar_ptx_uses_rn_mode_for_scaling() {
1280 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1281 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1282 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1283 let ptx = ptx.expect("test");
1284
1285 assert!(
1287 ptx.contains("mul.rn.f32"),
1288 "scalar kernel must contain mul.rn.f32 for alpha/beta scaling"
1289 );
1290 }
1291
1292 #[test]
1294 fn precision_loss_monotone_in_nnz_per_row() {
1295 let nnz_values = [1.0_f64, 10.0, 100.0, 1000.0];
1296 let mut prev_loss = 0.0_f64;
1297 for &nnz in &nnz_values {
1298 let loss = estimate_precision_loss(nnz, StoragePrecision::Fp16);
1299 assert!(
1300 loss >= prev_loss,
1301 "precision loss should increase with nnz/row: nnz={nnz}, loss={loss}, prev={prev_loss}"
1302 );
1303 prev_loss = loss;
1304 }
1305 }
1306
1307 #[test]
1312 fn precision_loss_linear_in_nnz_per_row() {
1313 let loss_10 = estimate_precision_loss(10.0, StoragePrecision::Fp16);
1314 let loss_20 = estimate_precision_loss(20.0, StoragePrecision::Fp16);
1315
1316 assert!(
1318 (loss_20 - 2.0 * loss_10).abs() < 1e-15,
1319 "Precision loss should be linear: loss(20)={loss_20} should be 2*loss(10)={loss_10}"
1320 );
1321 }
1322
1323 #[test]
1325 fn bandwidth_savings_bf16_vs_fp32() {
1326 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1327 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1328 let ratio = plan.bandwidth_savings_ratio();
1330 assert!(
1331 (ratio - 2.0).abs() < 1e-10,
1332 "BF16 bandwidth savings ratio should be ~2.0, got {ratio}"
1333 );
1334 }
1335
1336 #[test]
1338 fn mixed_plan_avg_nnz_per_row_calculation() {
1339 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1340 let plan = plan_mixed_precision_spmv(&config, 100, 1000, 500).expect("test");
1342 let avg = plan.avg_nnz_per_row();
1343 assert!(
1344 (avg - 5.0).abs() < 1e-10,
1345 "avg_nnz_per_row should be 5.0, got {avg}"
1346 );
1347 }
1348}