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 lo_bits = b.alloc_reg(PtxType::B16);
845 b.raw_ptx("{ .reg .b16 __hi;");
846 b.raw_ptx(&format!("mov.b32 {{{lo_bits}, __hi}}, {packed_val}; }}"));
847 let val_lo_h = b.alloc_reg(storage.ptx_type());
848 b.raw_ptx(&format!("mov.b16 {val_lo_h}, {lo_bits};"));
849 let val_lo_f32 = b.alloc_reg(compute.ptx_type());
850 b.raw_ptx(&format!(
851 "cvt.{compute_suffix}.{storage_suffix} {val_lo_f32}, {val_lo_h};"
852 ));
853
854 let hi_bits = b.alloc_reg(PtxType::B16);
856 b.raw_ptx("{ .reg .b16 __lo;");
857 b.raw_ptx(&format!("mov.b32 {{__lo, {hi_bits}}}, {packed_val}; }}"));
858 let val_hi_h = b.alloc_reg(storage.ptx_type());
859 b.raw_ptx(&format!("mov.b16 {val_hi_h}, {hi_bits};"));
860 let val_hi_f32 = b.alloc_reg(compute.ptx_type());
861 b.raw_ptx(&format!(
862 "cvt.{compute_suffix}.{storage_suffix} {val_hi_f32}, {val_hi_h};"
863 ));
864
865 let ci_addr_0 = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
867 let col_0_i32 = b.load_global_i32(ci_addr_0);
868 let col_0 = b.alloc_reg(PtxType::U32);
869 b.raw_ptx(&format!("mov.b32 {col_0}, {col_0_i32};"));
870
871 let ci_addr_1 = b.byte_offset_addr(col_idx_base.clone(), k_plus_1.clone(), 4);
872 let col_1_i32 = b.load_global_i32(ci_addr_1);
873 let col_1 = b.alloc_reg(PtxType::U32);
874 b.raw_ptx(&format!("mov.b32 {col_1}, {col_1_i32};"));
875
876 let x_addr_0 = b.byte_offset_addr(x_ptr.clone(), col_0, compute.element_bytes());
877 let x_val_0 = b.load_global_f32(x_addr_0);
878
879 let x_addr_1 = b.byte_offset_addr(x_ptr.clone(), col_1, compute.element_bytes());
880 let x_val_1 = b.load_global_f32(x_addr_1);
881
882 let acc1 = b.fma_f32(val_lo_f32, x_val_0, acc.clone());
884 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc1};"));
885 let acc2 = b.fma_f32(val_hi_f32, x_val_1, acc.clone());
886 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc2};"));
887
888 b.raw_ptx(&format!("add.u32 {k}, {k}, 64;"));
890 b.branch(&packed_loop);
891 b.label(&packed_done);
892
893 let has_remainder = b.alloc_reg(PtxType::Pred);
895 let nnz_odd = b.alloc_reg(PtxType::U32);
896 b.raw_ptx(&format!("and.b32 {nnz_odd}, {nnz_row}, 1;"));
897 b.raw_ptx(&format!("setp.ne.u32 {has_remainder}, {nnz_odd}, 0;"));
898
899 let remainder_done = b.fresh_label("mpspmv_rem_done");
900 b.raw_ptx(&format!("@!{has_remainder} bra {remainder_done};"));
901
902 let is_lane_0_rem = b.alloc_reg(PtxType::Pred);
904 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0_rem}, {lane}, 0;"));
905 b.raw_ptx(&format!("@!{is_lane_0_rem} bra {remainder_done};"));
906
907 let last_idx = b.alloc_reg(PtxType::U32);
909 b.raw_ptx(&format!("sub.u32 {last_idx}, {row_end}, 1;"));
910
911 let last_v_addr =
912 b.byte_offset_addr(values_base.clone(), last_idx.clone(), elem_bytes);
913 let last_val_h = b.alloc_reg(storage.ptx_type());
914 b.raw_ptx(&format!(
915 "ld.global.{storage_suffix} {last_val_h}, [{last_v_addr}];"
916 ));
917 let last_val_f32 = b.alloc_reg(compute.ptx_type());
918 b.raw_ptx(&format!(
919 "cvt.{compute_suffix}.{storage_suffix} {last_val_f32}, {last_val_h};"
920 ));
921
922 let last_ci_addr = b.byte_offset_addr(col_idx_base, last_idx, 4);
923 let last_col_i32 = b.load_global_i32(last_ci_addr);
924 let last_col = b.alloc_reg(PtxType::U32);
925 b.raw_ptx(&format!("mov.b32 {last_col}, {last_col_i32};"));
926
927 let last_x_addr =
928 b.byte_offset_addr(x_ptr, last_col, compute.element_bytes());
929 let last_x_val = b.load_global_f32(last_x_addr);
930
931 let acc_rem = b.fma_f32(last_val_f32, last_x_val, acc.clone());
932 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc_rem};"));
933
934 b.label(&remainder_done);
935
936 let mut current = acc;
938 for offset in [16u32, 8, 4, 2, 1] {
939 let shuffled = b.alloc_reg(compute.ptx_type());
940 b.raw_ptx(&format!(
941 "shfl.sync.down.{compute_bit} {shuffled}, {current}, {offset}, 31, 0xFFFFFFFF;"
942 ));
943 let sum = b.alloc_reg(compute.ptx_type());
944 b.raw_ptx(&format!(
945 "add.{compute_suffix} {sum}, {current}, {shuffled};"
946 ));
947 current = sum;
948 }
949
950 let is_lane_0 = b.alloc_reg(PtxType::Pred);
952 b.raw_ptx(&format!("setp.eq.u32 {is_lane_0}, {lane}, 0;"));
953
954 let skip_label = b.fresh_label("mpspmv_pskip");
955 b.raw_ptx(&format!("@!{is_lane_0} bra {skip_label};"));
956
957 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
958 let y_old = b.load_global_f32(y_addr.clone());
959
960 let alpha_acc = b.alloc_reg(compute.ptx_type());
961 b.raw_ptx(&format!(
962 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {current};"
963 ));
964 let beta_y = b.alloc_reg(compute.ptx_type());
965 b.raw_ptx(&format!(
966 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
967 ));
968 let result = b.alloc_reg(compute.ptx_type());
969 b.raw_ptx(&format!(
970 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
971 ));
972
973 b.store_global_f32(y_addr, result);
974
975 b.label(&skip_label);
976 });
977
978 b.ret();
979 })
980 .build()
981}
982
983#[must_use]
989pub fn scalar_launch_params(num_rows: u32) -> (u32, u32) {
990 let block = SCALAR_BLOCK_SIZE;
991 let grid = num_rows.div_ceil(block);
992 (grid, block)
993}
994
995#[must_use]
997pub fn vector_launch_params(num_rows: u32) -> (u32, u32) {
998 let block = VECTOR_BLOCK_SIZE;
999 let warps_per_block = block / 32;
1000 let grid = num_rows.div_ceil(warps_per_block);
1001 (grid, block)
1002}
1003
1004#[cfg(test)]
1009mod tests {
1010 use super::*;
1011
1012 fn default_fp16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1013 MixedPrecisionConfig::fp16_fp32(algo, SmVersion::Sm80)
1014 }
1015
1016 fn default_bf16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1017 MixedPrecisionConfig::bf16_fp32(algo, SmVersion::Sm80)
1018 }
1019
1020 #[test]
1023 fn scalar_ptx_fp16_generates() {
1024 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1025 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1026 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1027 let ptx = ptx.expect("test");
1028 assert!(ptx.contains(".entry mixed_spmv_scalar"));
1029 assert!(ptx.contains(".target sm_80"));
1030 assert!(ptx.contains("cvt.f32.f16"));
1032 }
1033
1034 #[test]
1035 fn scalar_ptx_bf16_generates() {
1036 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1037 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1038 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1039 let ptx = ptx.expect("test");
1040 assert!(ptx.contains("cvt.f32.bf16"));
1041 }
1042
1043 #[test]
1044 fn vector_ptx_fp16_generates() {
1045 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1046 let ptx = generate_mixed_vector_spmv_ptx(&config);
1047 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1048 let ptx = ptx.expect("test");
1049 assert!(ptx.contains(".entry mixed_spmv_vector"));
1050 assert!(ptx.contains("shfl.sync.down"));
1051 assert!(ptx.contains("cvt.f32.f16"));
1052 }
1053
1054 #[test]
1055 fn vector_ptx_bf16_generates() {
1056 let config = default_bf16_config(MixedSpMVAlgo::Vector);
1057 let ptx = generate_mixed_vector_spmv_ptx(&config);
1058 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1059 let ptx = ptx.expect("test");
1060 assert!(ptx.contains("cvt.f32.bf16"));
1061 assert!(ptx.contains("shfl.sync.down"));
1062 }
1063
1064 #[test]
1065 fn packed_ptx_fp16_generates() {
1066 let config = default_fp16_config(MixedSpMVAlgo::VectorPacked);
1067 let ptx = generate_packed_vector_spmv_ptx(&config);
1068 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1069 let ptx = ptx.expect("test");
1070 assert!(ptx.contains(".entry mixed_spmv_packed"));
1071 assert!(ptx.contains("ld.global.b32"));
1073 assert!(ptx.contains("shfl.sync.down"));
1074 }
1075
1076 #[test]
1077 fn packed_ptx_bf16_generates() {
1078 let config = default_bf16_config(MixedSpMVAlgo::VectorPacked);
1079 let ptx = generate_packed_vector_spmv_ptx(&config);
1080 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1081 let ptx = ptx.expect("test");
1082 assert!(ptx.contains("cvt.f32.bf16"));
1083 }
1084
1085 #[test]
1088 fn validate_fp16_on_turing() {
1089 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1090 assert!(validate_mixed_precision_config(&config).is_ok());
1091 }
1092
1093 #[test]
1094 fn validate_bf16_on_turing_fails() {
1095 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1096 let result = validate_mixed_precision_config(&config);
1097 assert!(result.is_err());
1098 let err_msg = format!("{}", result.expect_err("test"));
1099 assert!(err_msg.contains("BF16"));
1100 }
1101
1102 #[test]
1103 fn validate_bf16_on_ampere_ok() {
1104 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1105 assert!(validate_mixed_precision_config(&config).is_ok());
1106 }
1107
1108 #[test]
1111 fn plan_auto_selects_scalar_for_sparse() {
1112 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1113 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 2000);
1115 assert!(plan.is_ok());
1116 let plan = plan.expect("test");
1117 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Scalar);
1118 }
1119
1120 #[test]
1121 fn plan_auto_selects_vector_for_moderate() {
1122 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1123 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 10000);
1125 assert!(plan.is_ok());
1126 let plan = plan.expect("test");
1127 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Vector);
1128 }
1129
1130 #[test]
1131 fn plan_auto_selects_packed_for_dense() {
1132 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1133 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 50000);
1135 assert!(plan.is_ok());
1136 let plan = plan.expect("test");
1137 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::VectorPacked);
1138 }
1139
1140 #[test]
1143 fn bandwidth_savings_fp16_vs_fp32() {
1144 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1145 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1146 let ratio = plan.bandwidth_savings_ratio();
1148 assert!((ratio - 2.0).abs() < 1e-10, "Expected ~2.0, got {ratio}");
1149 }
1150
1151 #[test]
1152 fn estimated_gflops_positive() {
1153 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1154 let plan = plan_mixed_precision_spmv(&config, 10000, 10000, 100000).expect("test");
1155 let gflops = plan.estimated_gflops(1000.0);
1157 assert!(gflops > 0.0, "Expected positive GFLOPS, got {gflops}");
1158 assert!(gflops < 1000.0, "GFLOPS suspiciously high: {gflops}");
1160 }
1161
1162 #[test]
1165 fn precision_loss_fp16_bounded() {
1166 let loss = estimate_precision_loss(100.0, StoragePrecision::Fp16);
1167 assert!(loss > 0.0);
1170 assert!(loss < 0.1, "Precision loss bound too large: {loss}");
1171 }
1172
1173 #[test]
1174 fn precision_loss_bf16_larger_than_fp16() {
1175 let loss_fp16 = estimate_precision_loss(50.0, StoragePrecision::Fp16);
1176 let loss_bf16 = estimate_precision_loss(50.0, StoragePrecision::Bf16);
1177 assert!(
1179 loss_bf16 > loss_fp16,
1180 "BF16 loss ({loss_bf16}) should exceed FP16 loss ({loss_fp16})"
1181 );
1182 }
1183
1184 #[test]
1187 fn scalar_launch_params_correct() {
1188 let (grid, block) = scalar_launch_params(1000);
1189 assert_eq!(block, 256);
1190 assert_eq!(grid, 4); }
1192
1193 #[test]
1194 fn vector_launch_params_correct() {
1195 let (grid, block) = vector_launch_params(1000);
1196 assert_eq!(block, 256);
1197 assert_eq!(grid, 125);
1199 }
1200
1201 #[test]
1208 fn mixed_precision_scalar_ptx_contains_fp16_to_fp32_conversion() {
1209 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1210 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1211 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1212 let ptx = ptx.expect("test");
1213
1214 assert!(
1216 ptx.contains("cvt.f32.f16"),
1217 "scalar kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1218 );
1219 }
1220
1221 #[test]
1225 fn mixed_precision_scalar_ptx_contains_fma_rn_f32_accumulation() {
1226 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1227 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1228 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1229 let ptx = ptx.expect("test");
1230
1231 assert!(
1233 ptx.contains("fma.rn.f32"),
1234 "scalar kernel must contain fma.rn.f32 for FP32 accumulation"
1235 );
1236 }
1237
1238 #[test]
1240 fn mixed_precision_vector_ptx_contains_conversion_and_fma() {
1241 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1242 let ptx = generate_mixed_vector_spmv_ptx(&config);
1243 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1244 let ptx = ptx.expect("test");
1245
1246 assert!(
1247 ptx.contains("cvt.f32.f16"),
1248 "vector kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1249 );
1250 assert!(
1251 ptx.contains("fma.rn.f32"),
1252 "vector kernel must contain fma.rn.f32 for FP32 accumulation"
1253 );
1254 }
1255
1256 #[test]
1258 fn mixed_precision_bf16_ptx_uses_bf16_conversion() {
1259 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1260 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1261 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1262 let ptx = ptx.expect("test");
1263
1264 assert!(
1265 ptx.contains("cvt.f32.bf16"),
1266 "BF16 kernel must contain cvt.f32.bf16 for BF16→FP32 widening"
1267 );
1268 assert!(
1270 !ptx.contains("cvt.f32.f16"),
1271 "BF16 kernel must NOT contain cvt.f32.f16"
1272 );
1273 }
1274
1275 #[test]
1277 fn mixed_precision_scalar_ptx_uses_rn_mode_for_scaling() {
1278 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1279 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1280 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1281 let ptx = ptx.expect("test");
1282
1283 assert!(
1285 ptx.contains("mul.rn.f32"),
1286 "scalar kernel must contain mul.rn.f32 for alpha/beta scaling"
1287 );
1288 }
1289
1290 #[test]
1292 fn precision_loss_monotone_in_nnz_per_row() {
1293 let nnz_values = [1.0_f64, 10.0, 100.0, 1000.0];
1294 let mut prev_loss = 0.0_f64;
1295 for &nnz in &nnz_values {
1296 let loss = estimate_precision_loss(nnz, StoragePrecision::Fp16);
1297 assert!(
1298 loss >= prev_loss,
1299 "precision loss should increase with nnz/row: nnz={nnz}, loss={loss}, prev={prev_loss}"
1300 );
1301 prev_loss = loss;
1302 }
1303 }
1304
1305 #[test]
1310 fn precision_loss_linear_in_nnz_per_row() {
1311 let loss_10 = estimate_precision_loss(10.0, StoragePrecision::Fp16);
1312 let loss_20 = estimate_precision_loss(20.0, StoragePrecision::Fp16);
1313
1314 assert!(
1316 (loss_20 - 2.0 * loss_10).abs() < 1e-15,
1317 "Precision loss should be linear: loss(20)={loss_20} should be 2*loss(10)={loss_10}"
1318 );
1319 }
1320
1321 #[test]
1323 fn bandwidth_savings_bf16_vs_fp32() {
1324 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1325 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1326 let ratio = plan.bandwidth_savings_ratio();
1328 assert!(
1329 (ratio - 2.0).abs() < 1e-10,
1330 "BF16 bandwidth savings ratio should be ~2.0, got {ratio}"
1331 );
1332 }
1333
1334 #[test]
1336 fn mixed_plan_avg_nnz_per_row_calculation() {
1337 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1338 let plan = plan_mixed_precision_spmv(&config, 100, 1000, 500).expect("test");
1340 let avg = plan.avg_nnz_per_row();
1341 assert!(
1342 (avg - 5.0).abs() < 1e-10,
1343 "avg_nnz_per_row should be 5.0, got {avg}"
1344 );
1345 }
1346}