1use oxicuda_ptx::prelude::*;
18
19use crate::error::{SparseError, SparseResult};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum StoragePrecision {
28 Fp16,
30 Bf16,
32}
33
34impl StoragePrecision {
35 #[must_use]
37 pub const fn ptx_type(self) -> PtxType {
38 match self {
39 Self::Fp16 => PtxType::F16,
40 Self::Bf16 => PtxType::BF16,
41 }
42 }
43
44 #[must_use]
46 pub const fn packed_ptx_type(self) -> PtxType {
47 match self {
48 Self::Fp16 => PtxType::F16x2,
49 Self::Bf16 => PtxType::BF16x2,
50 }
51 }
52
53 #[must_use]
55 pub const fn element_bytes(self) -> u32 {
56 2
57 }
58
59 #[must_use]
61 pub const fn mantissa_bits(self) -> u32 {
62 match self {
63 Self::Fp16 => 10,
64 Self::Bf16 => 7,
65 }
66 }
67
68 #[must_use]
70 pub const fn suffix(self) -> &'static str {
71 match self {
72 Self::Fp16 => "f16",
73 Self::Bf16 => "bf16",
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub enum ComputePrecision {
81 Fp32,
83 Fp64,
85}
86
87impl ComputePrecision {
88 #[must_use]
90 pub const fn ptx_type(self) -> PtxType {
91 match self {
92 Self::Fp32 => PtxType::F32,
93 Self::Fp64 => PtxType::F64,
94 }
95 }
96
97 #[must_use]
99 pub const fn element_bytes(self) -> u32 {
100 match self {
101 Self::Fp32 => 4,
102 Self::Fp64 => 8,
103 }
104 }
105
106 #[must_use]
108 pub const fn suffix(self) -> &'static str {
109 match self {
110 Self::Fp32 => "f32",
111 Self::Fp64 => "f64",
112 }
113 }
114
115 #[must_use]
117 pub const fn bit_suffix(self) -> &'static str {
118 match self {
119 Self::Fp32 => "b32",
120 Self::Fp64 => "b64",
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
127pub enum MixedSpMVAlgo {
128 Scalar,
130 Vector,
132 VectorPacked,
135 Auto,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub struct MixedPrecisionConfig {
142 pub storage_precision: StoragePrecision,
144 pub compute_precision: ComputePrecision,
146 pub algorithm: MixedSpMVAlgo,
148 pub sm_version: SmVersion,
150}
151
152impl MixedPrecisionConfig {
153 #[must_use]
155 pub const fn fp16_fp32(algo: MixedSpMVAlgo, sm: SmVersion) -> Self {
156 Self {
157 storage_precision: StoragePrecision::Fp16,
158 compute_precision: ComputePrecision::Fp32,
159 algorithm: algo,
160 sm_version: sm,
161 }
162 }
163
164 #[must_use]
166 pub const fn bf16_fp32(algo: MixedSpMVAlgo, sm: SmVersion) -> Self {
167 Self {
168 storage_precision: StoragePrecision::Bf16,
169 compute_precision: ComputePrecision::Fp32,
170 algorithm: algo,
171 sm_version: sm,
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
185pub struct MixedPrecisionPlan {
186 pub config: MixedPrecisionConfig,
188 pub rows: u32,
190 pub cols: u32,
192 pub nnz: u64,
194 pub values_bytes: u64,
196 pub values_bytes_full: u64,
198}
199
200impl MixedPrecisionPlan {
201 #[must_use]
206 pub fn bandwidth_savings_ratio(&self) -> f64 {
207 if self.values_bytes == 0 {
208 return 1.0;
209 }
210 self.values_bytes_full as f64 / self.values_bytes as f64
211 }
212
213 #[must_use]
219 pub fn estimated_gflops(&self, peak_bandwidth_gb_s: f64) -> f64 {
220 if self.nnz == 0 {
221 return 0.0;
222 }
223 let bytes_per_nnz = self.config.storage_precision.element_bytes() as f64
225 + 4.0
226 + self.config.compute_precision.element_bytes() as f64;
227 let total_bytes = bytes_per_nnz * self.nnz as f64;
228 let total_flops = 2.0 * self.nnz as f64;
230 let bandwidth_bytes_s = peak_bandwidth_gb_s * 1e9;
232 let time_s = total_bytes / bandwidth_bytes_s;
234 total_flops / time_s / 1e9
236 }
237
238 #[must_use]
240 pub fn avg_nnz_per_row(&self) -> f64 {
241 if self.rows == 0 {
242 return 0.0;
243 }
244 self.nnz as f64 / self.rows as f64
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct MixedPrecisionStats {
251 pub elapsed_us: f64,
253 pub gflops: f64,
255 pub bandwidth_gb_s: f64,
257 pub precision_loss_bound: f64,
259}
260
261const VECTOR_THRESHOLD: f64 = 4.0;
267
268const PACKED_THRESHOLD: f64 = 32.0;
270
271const SCALAR_BLOCK_SIZE: u32 = 256;
273
274const VECTOR_BLOCK_SIZE: u32 = 256;
276
277pub fn plan_mixed_precision_spmv(
291 config: &MixedPrecisionConfig,
292 rows: u32,
293 cols: u32,
294 nnz: u64,
295) -> SparseResult<MixedPrecisionPlan> {
296 validate_mixed_precision_config(config)?;
297
298 let avg_nnz = if rows > 0 {
299 nnz as f64 / rows as f64
300 } else {
301 0.0
302 };
303
304 let resolved_algo = match config.algorithm {
306 MixedSpMVAlgo::Auto => {
307 if avg_nnz >= PACKED_THRESHOLD {
308 MixedSpMVAlgo::VectorPacked
309 } else if avg_nnz >= VECTOR_THRESHOLD {
310 MixedSpMVAlgo::Vector
311 } else {
312 MixedSpMVAlgo::Scalar
313 }
314 }
315 other => other,
316 };
317
318 let resolved_config = MixedPrecisionConfig {
319 algorithm: resolved_algo,
320 ..*config
321 };
322
323 let values_bytes = nnz * config.storage_precision.element_bytes() as u64;
324 let values_bytes_full = nnz * config.compute_precision.element_bytes() as u64;
325
326 Ok(MixedPrecisionPlan {
327 config: resolved_config,
328 rows,
329 cols,
330 nnz,
331 values_bytes,
332 values_bytes_full,
333 })
334}
335
336pub fn validate_mixed_precision_config(config: &MixedPrecisionConfig) -> SparseResult<()> {
349 if config.storage_precision == StoragePrecision::Bf16 {
351 let (major, _minor) = config.sm_version.ptx_isa_version();
352 if major < 7 {
353 return Err(SparseError::InvalidArgument(
354 "BF16 storage requires sm_80 (Ampere) or newer; \
355 the selected SM version does not support BF16 instructions"
356 .to_string(),
357 ));
358 }
359 }
360
361 Ok(())
369}
370
371#[must_use]
387pub fn estimate_precision_loss(nnz_per_row: f64, storage: StoragePrecision) -> f64 {
388 let eps_storage = match storage {
389 StoragePrecision::Fp16 => f64::powi(2.0, -11), StoragePrecision::Bf16 => f64::powi(2.0, -8), };
392 let eps_compute: f64 = f64::powi(2.0, -24); nnz_per_row * eps_storage + nnz_per_row * eps_compute
396}
397
398const fn mp_bits_param_ty(compute: ComputePrecision) -> PtxType {
413 match compute {
414 ComputePrecision::Fp32 => PtxType::U32,
415 ComputePrecision::Fp64 => PtxType::U64,
416 }
417}
418
419fn mp_load_bits_param(b: &mut BodyBuilder<'_>, compute: ComputePrecision, name: &str) -> Register {
421 match compute {
422 ComputePrecision::Fp32 => b.load_param_u32(name),
423 ComputePrecision::Fp64 => b.load_param_u64(name),
424 }
425}
426
427const fn mp_zero_literal(compute: ComputePrecision) -> &'static str {
429 match compute {
430 ComputePrecision::Fp32 => "0F00000000",
431 ComputePrecision::Fp64 => "0D0000000000000000",
432 }
433}
434
435fn mp_load_compute(b: &mut BodyBuilder<'_>, compute: ComputePrecision, addr: Register) -> Register {
437 match compute {
438 ComputePrecision::Fp32 => b.load_global_f32(addr),
439 ComputePrecision::Fp64 => b.load_global_f64(addr),
440 }
441}
442
443fn mp_store_compute(
445 b: &mut BodyBuilder<'_>,
446 compute: ComputePrecision,
447 addr: Register,
448 val: Register,
449) {
450 match compute {
451 ComputePrecision::Fp32 => b.store_global_f32(addr, val),
452 ComputePrecision::Fp64 => b.store_global_f64(addr, val),
453 }
454}
455
456fn mp_fma_compute(
458 b: &mut BodyBuilder<'_>,
459 compute: ComputePrecision,
460 a: Register,
461 x: Register,
462 c: Register,
463) -> Register {
464 match compute {
465 ComputePrecision::Fp32 => b.fma_f32(a, x, c),
466 ComputePrecision::Fp64 => b.fma_f64(a, x, c),
467 }
468}
469
470fn mp_shfl_down_compute(
476 b: &mut BodyBuilder<'_>,
477 compute: ComputePrecision,
478 val: Register,
479 offset: u32,
480) -> Register {
481 let off = offset.to_string();
482 match compute {
483 ComputePrecision::Fp32 => {
484 crate::ptx_helpers::emit_shfl_float::<f32>(b, "down", val, &off, "31")
485 }
486 ComputePrecision::Fp64 => {
487 crate::ptx_helpers::emit_shfl_float::<f64>(b, "down", val, &off, "31")
488 }
489 }
490}
491
492fn mp_half_cvt_fragment(
500 storage: StoragePrecision,
501 compute: ComputePrecision,
502 src: &str,
503 dst: &Register,
504 widen: &str,
505) -> String {
506 let ss = storage.suffix();
507 match (storage, compute) {
508 (_, ComputePrecision::Fp32) => format!("cvt.f32.{ss} {dst}, {src};"),
509 (StoragePrecision::Fp16, ComputePrecision::Fp64) => {
510 format!("cvt.f64.f16 {dst}, {src};")
511 }
512 (StoragePrecision::Bf16, ComputePrecision::Fp64) => {
513 format!(".reg .f32 {widen}; cvt.f32.bf16 {widen}, {src}; cvt.f64.f32 {dst}, {widen};")
514 }
515 }
516}
517
518fn mp_load_half(
529 b: &mut BodyBuilder<'_>,
530 storage: StoragePrecision,
531 compute: ComputePrecision,
532 addr: &Register,
533) -> Register {
534 let dst = b.alloc_reg(compute.ptx_type());
535 let frag = mp_half_cvt_fragment(storage, compute, "%mphalf", &dst, "%mpw");
536 b.raw_ptx(&format!(
537 "{{ .reg .b16 %mphalf; ld.global.b16 %mphalf, [{addr}]; {frag} }}"
538 ));
539 dst
540}
541
542fn mp_unpack_pair(
550 b: &mut BodyBuilder<'_>,
551 storage: StoragePrecision,
552 compute: ComputePrecision,
553 packed: &Register,
554) -> (Register, Register) {
555 let lo = b.alloc_reg(compute.ptx_type());
556 let hi = b.alloc_reg(compute.ptx_type());
557 let frag_lo = mp_half_cvt_fragment(storage, compute, "%mplo", &lo, "%mpwlo");
558 let frag_hi = mp_half_cvt_fragment(storage, compute, "%mphi", &hi, "%mpwhi");
559 b.raw_ptx(&format!(
560 "{{ .reg .b16 %mplo, %mphi; mov.b32 {{%mplo, %mphi}}, {packed}; {frag_lo} {frag_hi} }}"
561 ));
562 (lo, hi)
563}
564
565pub fn generate_mixed_scalar_spmv_ptx(
593 config: &MixedPrecisionConfig,
594) -> Result<String, PtxGenError> {
595 let storage = config.storage_precision;
596 let compute = config.compute_precision;
597 let sm = config.sm_version;
598 let compute_suffix = compute.suffix();
599 let compute_bit = compute.bit_suffix();
600 let elem_bytes = storage.element_bytes();
601
602 KernelBuilder::new("mixed_spmv_scalar")
603 .target(sm)
604 .param("row_ptr", PtxType::U64)
605 .param("col_idx", PtxType::U64)
606 .param("values", PtxType::U64)
607 .param("x_ptr", PtxType::U64)
608 .param("y_ptr", PtxType::U64)
609 .param("alpha_bits", mp_bits_param_ty(compute))
610 .param("beta_bits", mp_bits_param_ty(compute))
611 .param("num_rows", PtxType::U32)
612 .body(move |b| {
613 let gid = b.global_thread_id_x();
614 let num_rows = b.load_param_u32("num_rows");
615
616 let gid_inner = gid.clone();
617 b.if_lt_u32(gid, num_rows, move |b| {
618 let row = gid_inner;
619 let row_ptr_base = b.load_param_u64("row_ptr");
620 let col_idx_base = b.load_param_u64("col_idx");
621 let values_base = b.load_param_u64("values");
622 let x_ptr = b.load_param_u64("x_ptr");
623 let y_ptr = b.load_param_u64("y_ptr");
624
625 let alpha_bits = mp_load_bits_param(b, compute, "alpha_bits");
627 let alpha = b.alloc_reg(compute.ptx_type());
628 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
629
630 let beta_bits = mp_load_bits_param(b, compute, "beta_bits");
631 let beta = b.alloc_reg(compute.ptx_type());
632 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
633
634 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
636 let row_start = b.load_global_i32(rp_addr);
637
638 let row_plus_1 = b.alloc_reg(PtxType::U32);
639 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
640 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
641 let row_end = b.load_global_i32(rp_addr_next);
642
643 let acc = b.alloc_reg(compute.ptx_type());
645 let zero_lit = mp_zero_literal(compute);
648 b.raw_ptx(&format!("mov.{compute_bit} {acc}, {zero_lit};"));
649
650 let loop_label = b.fresh_label("mpspmv_loop");
652 let done_label = b.fresh_label("mpspmv_done");
653
654 let k = b.alloc_reg(PtxType::U32);
655 let rs_u32 = b.alloc_reg(PtxType::U32);
656 b.raw_ptx(&format!("mov.b32 {rs_u32}, {row_start};"));
657 b.raw_ptx(&format!("mov.u32 {k}, {rs_u32};"));
658
659 let re_u32 = b.alloc_reg(PtxType::U32);
660 b.raw_ptx(&format!("mov.b32 {re_u32}, {row_end};"));
661
662 b.label(&loop_label);
663 let pred = b.alloc_reg(PtxType::Pred);
666 b.raw_ptx(&format!("setp.hs.u32 {pred}, {k}, {re_u32};"));
667 b.branch_if(pred, &done_label);
668
669 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
671 let col = b.load_global_i32(ci_addr);
672 let col_u32 = b.alloc_reg(PtxType::U32);
673 b.raw_ptx(&format!("mov.b32 {col_u32}, {col};"));
674
675 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
677 let val_fp32 = mp_load_half(b, storage, compute, &v_addr);
678
679 let x_addr = b.byte_offset_addr(x_ptr.clone(), col_u32, compute.element_bytes());
681 let x_val = mp_load_compute(b, compute, x_addr);
682
683 let new_acc = mp_fma_compute(b, compute, val_fp32, x_val, acc.clone());
685 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {new_acc};"));
686
687 b.raw_ptx(&format!("add.u32 {k}, {k}, 1;"));
689 b.branch(&loop_label);
690 b.label(&done_label);
691
692 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
694 let y_old = mp_load_compute(b, compute, y_addr.clone());
695
696 let alpha_acc = b.alloc_reg(compute.ptx_type());
697 b.raw_ptx(&format!(
698 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {acc};"
699 ));
700
701 let beta_y = b.alloc_reg(compute.ptx_type());
702 b.raw_ptx(&format!(
703 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
704 ));
705
706 let result = b.alloc_reg(compute.ptx_type());
707 b.raw_ptx(&format!(
708 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
709 ));
710
711 mp_store_compute(b, compute, y_addr, result);
712 });
713
714 b.ret();
715 })
716 .build()
717}
718
719pub fn generate_mixed_vector_spmv_ptx(
733 config: &MixedPrecisionConfig,
734) -> Result<String, PtxGenError> {
735 let storage = config.storage_precision;
736 let compute = config.compute_precision;
737 let sm = config.sm_version;
738 let compute_suffix = compute.suffix();
739 let compute_bit = compute.bit_suffix();
740 let elem_bytes = storage.element_bytes();
741
742 KernelBuilder::new("mixed_spmv_vector")
743 .target(sm)
744 .param("row_ptr", PtxType::U64)
745 .param("col_idx", PtxType::U64)
746 .param("values", PtxType::U64)
747 .param("x_ptr", PtxType::U64)
748 .param("y_ptr", PtxType::U64)
749 .param("alpha_bits", mp_bits_param_ty(compute))
750 .param("beta_bits", mp_bits_param_ty(compute))
751 .param("num_rows", PtxType::U32)
752 .body(move |b| {
753 let tid_global = b.global_thread_id_x();
754 let num_rows = b.load_param_u32("num_rows");
755
756 let lane = b.alloc_reg(PtxType::U32);
758 b.raw_ptx(&format!("and.b32 {lane}, {tid_global}, 31;"));
759
760 let warp_id = b.alloc_reg(PtxType::U32);
762 b.raw_ptx(&format!("shr.u32 {warp_id}, {tid_global}, 5;"));
763
764 let warp_id_inner = warp_id.clone();
765 let lane_inner = lane.clone();
766 b.if_lt_u32(warp_id, num_rows, move |b| {
767 let row = warp_id_inner;
768 let lane = lane_inner;
769
770 let row_ptr_base = b.load_param_u64("row_ptr");
771 let col_idx_base = b.load_param_u64("col_idx");
772 let values_base = b.load_param_u64("values");
773 let x_ptr = b.load_param_u64("x_ptr");
774 let y_ptr = b.load_param_u64("y_ptr");
775
776 let alpha_bits = mp_load_bits_param(b, compute, "alpha_bits");
777 let alpha = b.alloc_reg(compute.ptx_type());
778 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
779
780 let beta_bits = mp_load_bits_param(b, compute, "beta_bits");
781 let beta = b.alloc_reg(compute.ptx_type());
782 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
783
784 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
786 let row_start_i32 = b.load_global_i32(rp_addr);
787 let row_start = b.alloc_reg(PtxType::U32);
788 b.raw_ptx(&format!("mov.b32 {row_start}, {row_start_i32};"));
789
790 let row_plus_1 = b.alloc_reg(PtxType::U32);
791 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
792 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
793 let row_end_i32 = b.load_global_i32(rp_addr_next);
794 let row_end = b.alloc_reg(PtxType::U32);
795 b.raw_ptx(&format!("mov.b32 {row_end}, {row_end_i32};"));
796
797 let acc = b.alloc_reg(compute.ptx_type());
799 let zero_lit = mp_zero_literal(compute);
802 b.raw_ptx(&format!("mov.{compute_bit} {acc}, {zero_lit};"));
803
804 let k = b.alloc_reg(PtxType::U32);
805 b.raw_ptx(&format!("add.u32 {k}, {row_start}, {lane};"));
806
807 let loop_label = b.fresh_label("mpspmv_vloop");
808 let done_label = b.fresh_label("mpspmv_vdone");
809
810 b.label(&loop_label);
811 let pred = b.alloc_reg(PtxType::Pred);
813 b.raw_ptx(&format!("setp.hs.u32 {pred}, {k}, {row_end};"));
814 b.branch_if(pred, &done_label);
815
816 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
818 let col_i32 = b.load_global_i32(ci_addr);
819 let col_u32 = b.alloc_reg(PtxType::U32);
820 b.raw_ptx(&format!("mov.b32 {col_u32}, {col_i32};"));
821
822 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
823 let val_fp32 = mp_load_half(b, storage, compute, &v_addr);
824
825 let x_addr = b.byte_offset_addr(x_ptr.clone(), col_u32, compute.element_bytes());
826 let x_val = mp_load_compute(b, compute, x_addr);
827
828 let new_acc = mp_fma_compute(b, compute, val_fp32, x_val, acc.clone());
830 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {new_acc};"));
831
832 b.raw_ptx(&format!("add.u32 {k}, {k}, 32;"));
834 b.branch(&loop_label);
835 b.label(&done_label);
836
837 let mut current = acc;
841 for offset in [16u32, 8, 4, 2, 1] {
842 let shuffled = mp_shfl_down_compute(b, compute, current.clone(), offset);
843 let sum = b.alloc_reg(compute.ptx_type());
844 b.raw_ptx(&format!(
845 "add.{compute_suffix} {sum}, {current}, {shuffled};"
846 ));
847 current = sum;
848 }
849
850 let not_lane_0 = b.alloc_reg(PtxType::Pred);
853 b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
854 let skip_label = b.fresh_label("mpspmv_skip");
855 b.branch_if(not_lane_0, &skip_label);
856
857 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
858 let y_old = mp_load_compute(b, compute, y_addr.clone());
859
860 let alpha_acc = b.alloc_reg(compute.ptx_type());
861 b.raw_ptx(&format!(
862 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {current};"
863 ));
864 let beta_y = b.alloc_reg(compute.ptx_type());
865 b.raw_ptx(&format!(
866 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
867 ));
868 let result = b.alloc_reg(compute.ptx_type());
869 b.raw_ptx(&format!(
870 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
871 ));
872
873 mp_store_compute(b, compute, y_addr, result);
874
875 b.label(&skip_label);
876 });
877
878 b.ret();
879 })
880 .build()
881}
882
883pub fn generate_packed_vector_spmv_ptx(
901 config: &MixedPrecisionConfig,
902) -> Result<String, PtxGenError> {
903 let storage = config.storage_precision;
904 let compute = config.compute_precision;
905 let sm = config.sm_version;
906 let compute_suffix = compute.suffix();
907 let compute_bit = compute.bit_suffix();
908 let elem_bytes = storage.element_bytes();
909
910 KernelBuilder::new("mixed_spmv_packed")
911 .target(sm)
912 .param("row_ptr", PtxType::U64)
913 .param("col_idx", PtxType::U64)
914 .param("values", PtxType::U64)
915 .param("x_ptr", PtxType::U64)
916 .param("y_ptr", PtxType::U64)
917 .param("alpha_bits", mp_bits_param_ty(compute))
918 .param("beta_bits", mp_bits_param_ty(compute))
919 .param("num_rows", PtxType::U32)
920 .body(move |b| {
921 let tid_global = b.global_thread_id_x();
922 let num_rows = b.load_param_u32("num_rows");
923
924 let lane = b.alloc_reg(PtxType::U32);
925 b.raw_ptx(&format!("and.b32 {lane}, {tid_global}, 31;"));
926
927 let warp_id = b.alloc_reg(PtxType::U32);
928 b.raw_ptx(&format!("shr.u32 {warp_id}, {tid_global}, 5;"));
929
930 let warp_id_inner = warp_id.clone();
931 let lane_inner = lane.clone();
932 b.if_lt_u32(warp_id, num_rows, move |b| {
933 let row = warp_id_inner;
934 let lane = lane_inner;
935
936 let row_ptr_base = b.load_param_u64("row_ptr");
937 let col_idx_base = b.load_param_u64("col_idx");
938 let values_base = b.load_param_u64("values");
939 let x_ptr = b.load_param_u64("x_ptr");
940 let y_ptr = b.load_param_u64("y_ptr");
941
942 let alpha_bits = mp_load_bits_param(b, compute, "alpha_bits");
943 let alpha = b.alloc_reg(compute.ptx_type());
944 b.raw_ptx(&format!("mov.{compute_bit} {alpha}, {alpha_bits};"));
945
946 let beta_bits = mp_load_bits_param(b, compute, "beta_bits");
947 let beta = b.alloc_reg(compute.ptx_type());
948 b.raw_ptx(&format!("mov.{compute_bit} {beta}, {beta_bits};"));
949
950 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
952 let row_start_i32 = b.load_global_i32(rp_addr);
953 let row_start = b.alloc_reg(PtxType::U32);
954 b.raw_ptx(&format!("mov.b32 {row_start}, {row_start_i32};"));
955
956 let row_plus_1 = b.alloc_reg(PtxType::U32);
957 b.raw_ptx(&format!("add.u32 {row_plus_1}, {row}, 1;"));
958 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_plus_1, 4);
959 let row_end_i32 = b.load_global_i32(rp_addr_next);
960 let row_end = b.alloc_reg(PtxType::U32);
961 b.raw_ptx(&format!("mov.b32 {row_end}, {row_end_i32};"));
962
963 let nnz_row = b.alloc_reg(PtxType::U32);
965 b.raw_ptx(&format!("sub.u32 {nnz_row}, {row_end}, {row_start};"));
966 let nnz_even = b.alloc_reg(PtxType::U32);
967 b.raw_ptx(&format!("and.b32 {nnz_even}, {nnz_row}, 0xFFFFFFFE;"));
968 let row_end_even = b.alloc_reg(PtxType::U32);
969 b.raw_ptx(&format!("add.u32 {row_end_even}, {row_start}, {nnz_even};"));
970
971 let acc = b.alloc_reg(compute.ptx_type());
973 let zero_lit = mp_zero_literal(compute);
976 b.raw_ptx(&format!("mov.{compute_bit} {acc}, {zero_lit};"));
977
978 let k = b.alloc_reg(PtxType::U32);
981 let lane_x2 = b.alloc_reg(PtxType::U32);
982 b.raw_ptx(&format!("shl.b32 {lane_x2}, {lane}, 1;"));
983 b.raw_ptx(&format!("add.u32 {k}, {row_start}, {lane_x2};"));
984
985 let packed_loop = b.fresh_label("mpspmv_packed_loop");
986 let packed_done = b.fresh_label("mpspmv_packed_done");
987
988 b.label(&packed_loop);
989 let pred_pair = b.alloc_reg(PtxType::Pred);
990 let k_plus_1 = b.alloc_reg(PtxType::U32);
992 b.raw_ptx(&format!("add.u32 {k_plus_1}, {k}, 1;"));
993 b.raw_ptx(&format!(
996 "setp.hi.u32 {pred_pair}, {k_plus_1}, {row_end_even};"
997 ));
998 b.branch_if(pred_pair, &packed_done);
999
1000 let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
1003 let packed_val = b.alloc_reg(PtxType::B32);
1004 b.raw_ptx(&format!("ld.global.b32 {packed_val}, [{v_addr}];"));
1005
1006 let (val_lo_f32, val_hi_f32) = mp_unpack_pair(b, storage, compute, &packed_val);
1009
1010 let ci_addr_0 = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
1012 let col_0_i32 = b.load_global_i32(ci_addr_0);
1013 let col_0 = b.alloc_reg(PtxType::U32);
1014 b.raw_ptx(&format!("mov.b32 {col_0}, {col_0_i32};"));
1015
1016 let ci_addr_1 = b.byte_offset_addr(col_idx_base.clone(), k_plus_1.clone(), 4);
1017 let col_1_i32 = b.load_global_i32(ci_addr_1);
1018 let col_1 = b.alloc_reg(PtxType::U32);
1019 b.raw_ptx(&format!("mov.b32 {col_1}, {col_1_i32};"));
1020
1021 let x_addr_0 = b.byte_offset_addr(x_ptr.clone(), col_0, compute.element_bytes());
1022 let x_val_0 = mp_load_compute(b, compute, x_addr_0);
1023
1024 let x_addr_1 = b.byte_offset_addr(x_ptr.clone(), col_1, compute.element_bytes());
1025 let x_val_1 = mp_load_compute(b, compute, x_addr_1);
1026
1027 let acc1 = mp_fma_compute(b, compute, val_lo_f32, x_val_0, acc.clone());
1029 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc1};"));
1030 let acc2 = mp_fma_compute(b, compute, val_hi_f32, x_val_1, acc.clone());
1031 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc2};"));
1032
1033 b.raw_ptx(&format!("add.u32 {k}, {k}, 64;"));
1035 b.branch(&packed_loop);
1036 b.label(&packed_done);
1037
1038 let has_remainder = b.alloc_reg(PtxType::Pred);
1040 let nnz_odd = b.alloc_reg(PtxType::U32);
1041 b.raw_ptx(&format!("and.b32 {nnz_odd}, {nnz_row}, 1;"));
1042 b.raw_ptx(&format!("setp.eq.u32 {has_remainder}, {nnz_odd}, 0;"));
1045
1046 let remainder_done = b.fresh_label("mpspmv_rem_done");
1047 b.branch_if(has_remainder, &remainder_done);
1048
1049 let is_lane_0_rem = b.alloc_reg(PtxType::Pred);
1053 b.raw_ptx(&format!("setp.ne.u32 {is_lane_0_rem}, {lane}, 0;"));
1054 b.branch_if(is_lane_0_rem, &remainder_done);
1055
1056 let last_idx = b.alloc_reg(PtxType::U32);
1058 b.raw_ptx(&format!("sub.u32 {last_idx}, {row_end}, 1;"));
1059
1060 let last_v_addr =
1061 b.byte_offset_addr(values_base.clone(), last_idx.clone(), elem_bytes);
1062 let last_val_f32 = mp_load_half(b, storage, compute, &last_v_addr);
1063
1064 let last_ci_addr = b.byte_offset_addr(col_idx_base, last_idx, 4);
1065 let last_col_i32 = b.load_global_i32(last_ci_addr);
1066 let last_col = b.alloc_reg(PtxType::U32);
1067 b.raw_ptx(&format!("mov.b32 {last_col}, {last_col_i32};"));
1068
1069 let last_x_addr = b.byte_offset_addr(x_ptr, last_col, compute.element_bytes());
1070 let last_x_val = mp_load_compute(b, compute, last_x_addr);
1071
1072 let acc_rem = mp_fma_compute(b, compute, last_val_f32, last_x_val, acc.clone());
1073 b.raw_ptx(&format!("mov.{compute_suffix} {acc}, {acc_rem};"));
1074
1075 b.label(&remainder_done);
1076
1077 let mut current = acc;
1081 for offset in [16u32, 8, 4, 2, 1] {
1082 let shuffled = mp_shfl_down_compute(b, compute, current.clone(), offset);
1083 let sum = b.alloc_reg(compute.ptx_type());
1084 b.raw_ptx(&format!(
1085 "add.{compute_suffix} {sum}, {current}, {shuffled};"
1086 ));
1087 current = sum;
1088 }
1089
1090 let not_lane_0 = b.alloc_reg(PtxType::Pred);
1093 b.raw_ptx(&format!("setp.ne.u32 {not_lane_0}, {lane}, 0;"));
1094 let skip_label = b.fresh_label("mpspmv_pskip");
1095 b.branch_if(not_lane_0, &skip_label);
1096
1097 let y_addr = b.byte_offset_addr(y_ptr, row, compute.element_bytes());
1098 let y_old = mp_load_compute(b, compute, y_addr.clone());
1099
1100 let alpha_acc = b.alloc_reg(compute.ptx_type());
1101 b.raw_ptx(&format!(
1102 "mul.rn.{compute_suffix} {alpha_acc}, {alpha}, {current};"
1103 ));
1104 let beta_y = b.alloc_reg(compute.ptx_type());
1105 b.raw_ptx(&format!(
1106 "mul.rn.{compute_suffix} {beta_y}, {beta}, {y_old};"
1107 ));
1108 let result = b.alloc_reg(compute.ptx_type());
1109 b.raw_ptx(&format!(
1110 "add.{compute_suffix} {result}, {alpha_acc}, {beta_y};"
1111 ));
1112
1113 mp_store_compute(b, compute, y_addr, result);
1114
1115 b.label(&skip_label);
1116 });
1117
1118 b.ret();
1119 })
1120 .build()
1121}
1122
1123#[must_use]
1129pub fn scalar_launch_params(num_rows: u32) -> (u32, u32) {
1130 let block = SCALAR_BLOCK_SIZE;
1131 let grid = num_rows.div_ceil(block);
1132 (grid, block)
1133}
1134
1135#[must_use]
1137pub fn vector_launch_params(num_rows: u32) -> (u32, u32) {
1138 let block = VECTOR_BLOCK_SIZE;
1139 let warps_per_block = block / 32;
1140 let grid = num_rows.div_ceil(warps_per_block);
1141 (grid, block)
1142}
1143
1144#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 use crate::ptx_helpers::test_support::assert_assembles_and_clean;
1153
1154 fn default_fp16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1155 MixedPrecisionConfig::fp16_fp32(algo, SmVersion::Sm80)
1156 }
1157
1158 #[test]
1164 fn mixed_precision_all_kernels_assemble_sm86() {
1165 for storage in [StoragePrecision::Fp16, StoragePrecision::Bf16] {
1166 for compute in [ComputePrecision::Fp32, ComputePrecision::Fp64] {
1167 for algo in [
1168 MixedSpMVAlgo::Scalar,
1169 MixedSpMVAlgo::Vector,
1170 MixedSpMVAlgo::VectorPacked,
1171 ] {
1172 let config = MixedPrecisionConfig {
1173 storage_precision: storage,
1174 compute_precision: compute,
1175 algorithm: algo,
1176 sm_version: SmVersion::Sm86,
1177 };
1178 let (ptx, kernel) = match algo {
1179 MixedSpMVAlgo::Scalar => (
1180 generate_mixed_scalar_spmv_ptx(&config).expect("scalar PTX"),
1181 "mixed_scalar",
1182 ),
1183 MixedSpMVAlgo::Vector => (
1184 generate_mixed_vector_spmv_ptx(&config).expect("vector PTX"),
1185 "mixed_vector",
1186 ),
1187 MixedSpMVAlgo::VectorPacked => (
1188 generate_packed_vector_spmv_ptx(&config).expect("packed PTX"),
1189 "mixed_packed",
1190 ),
1191 MixedSpMVAlgo::Auto => unreachable!("Auto is resolved before codegen"),
1192 };
1193 let tag = format!("{kernel}_{}_{}", storage.suffix(), compute.suffix());
1194 assert_assembles_and_clean(&tag, &ptx);
1195 if compute == ComputePrecision::Fp64 {
1196 assert!(
1197 !ptx.contains("0F00000000"),
1198 "{tag}: FP64 compute must not materialize an f32 0.0 immediate:\n{ptx}"
1199 );
1200 assert!(
1201 ptx.contains("fma.rn.f64"),
1202 "{tag}: FP64 compute must use f64 FMA:\n{ptx}"
1203 );
1204 }
1205 }
1206 }
1207 }
1208 }
1209
1210 fn default_bf16_config(algo: MixedSpMVAlgo) -> MixedPrecisionConfig {
1211 MixedPrecisionConfig::bf16_fp32(algo, SmVersion::Sm80)
1212 }
1213
1214 #[test]
1217 fn scalar_ptx_fp16_generates() {
1218 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1219 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1220 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1221 let ptx = ptx.expect("test");
1222 assert!(ptx.contains(".entry mixed_spmv_scalar"));
1223 assert!(ptx.contains(".target sm_80"));
1224 assert!(ptx.contains("cvt.f32.f16"));
1226 }
1227
1228 #[test]
1229 fn scalar_ptx_bf16_generates() {
1230 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1231 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1232 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1233 let ptx = ptx.expect("test");
1234 assert!(ptx.contains("cvt.f32.bf16"));
1235 }
1236
1237 #[test]
1238 fn vector_ptx_fp16_generates() {
1239 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1240 let ptx = generate_mixed_vector_spmv_ptx(&config);
1241 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1242 let ptx = ptx.expect("test");
1243 assert!(ptx.contains(".entry mixed_spmv_vector"));
1244 assert!(ptx.contains("shfl.sync.down"));
1245 assert!(ptx.contains("cvt.f32.f16"));
1246 }
1247
1248 #[test]
1249 fn vector_ptx_bf16_generates() {
1250 let config = default_bf16_config(MixedSpMVAlgo::Vector);
1251 let ptx = generate_mixed_vector_spmv_ptx(&config);
1252 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1253 let ptx = ptx.expect("test");
1254 assert!(ptx.contains("cvt.f32.bf16"));
1255 assert!(ptx.contains("shfl.sync.down"));
1256 }
1257
1258 #[test]
1259 fn packed_ptx_fp16_generates() {
1260 let config = default_fp16_config(MixedSpMVAlgo::VectorPacked);
1261 let ptx = generate_packed_vector_spmv_ptx(&config);
1262 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1263 let ptx = ptx.expect("test");
1264 assert!(ptx.contains(".entry mixed_spmv_packed"));
1265 assert!(ptx.contains("ld.global.b32"));
1267 assert!(ptx.contains("shfl.sync.down"));
1268 }
1269
1270 #[test]
1271 fn packed_ptx_bf16_generates() {
1272 let config = default_bf16_config(MixedSpMVAlgo::VectorPacked);
1273 let ptx = generate_packed_vector_spmv_ptx(&config);
1274 assert!(ptx.is_ok(), "PTX generation failed: {ptx:?}");
1275 let ptx = ptx.expect("test");
1276 assert!(ptx.contains("cvt.f32.bf16"));
1277 }
1278
1279 #[test]
1282 fn validate_fp16_on_turing() {
1283 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1284 assert!(validate_mixed_precision_config(&config).is_ok());
1285 }
1286
1287 #[test]
1288 fn validate_bf16_on_turing_fails() {
1289 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm75);
1290 let result = validate_mixed_precision_config(&config);
1291 assert!(result.is_err());
1292 let err_msg = format!("{}", result.expect_err("test"));
1293 assert!(err_msg.contains("BF16"));
1294 }
1295
1296 #[test]
1297 fn validate_bf16_on_ampere_ok() {
1298 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1299 assert!(validate_mixed_precision_config(&config).is_ok());
1300 }
1301
1302 #[test]
1305 fn plan_auto_selects_scalar_for_sparse() {
1306 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1307 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 2000);
1309 assert!(plan.is_ok());
1310 let plan = plan.expect("test");
1311 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Scalar);
1312 }
1313
1314 #[test]
1315 fn plan_auto_selects_vector_for_moderate() {
1316 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1317 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 10000);
1319 assert!(plan.is_ok());
1320 let plan = plan.expect("test");
1321 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::Vector);
1322 }
1323
1324 #[test]
1325 fn plan_auto_selects_packed_for_dense() {
1326 let config = default_fp16_config(MixedSpMVAlgo::Auto);
1327 let plan = plan_mixed_precision_spmv(&config, 1000, 5000, 50000);
1329 assert!(plan.is_ok());
1330 let plan = plan.expect("test");
1331 assert_eq!(plan.config.algorithm, MixedSpMVAlgo::VectorPacked);
1332 }
1333
1334 #[test]
1337 fn bandwidth_savings_fp16_vs_fp32() {
1338 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1339 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1340 let ratio = plan.bandwidth_savings_ratio();
1342 assert!((ratio - 2.0).abs() < 1e-10, "Expected ~2.0, got {ratio}");
1343 }
1344
1345 #[test]
1346 fn estimated_gflops_positive() {
1347 let config = default_fp16_config(MixedSpMVAlgo::Vector);
1348 let plan = plan_mixed_precision_spmv(&config, 10000, 10000, 100000).expect("test");
1349 let gflops = plan.estimated_gflops(1000.0);
1351 assert!(gflops > 0.0, "Expected positive GFLOPS, got {gflops}");
1352 assert!(gflops < 1000.0, "GFLOPS suspiciously high: {gflops}");
1354 }
1355
1356 #[test]
1359 fn precision_loss_fp16_bounded() {
1360 let loss = estimate_precision_loss(100.0, StoragePrecision::Fp16);
1361 assert!(loss > 0.0);
1364 assert!(loss < 0.1, "Precision loss bound too large: {loss}");
1365 }
1366
1367 #[test]
1368 fn precision_loss_bf16_larger_than_fp16() {
1369 let loss_fp16 = estimate_precision_loss(50.0, StoragePrecision::Fp16);
1370 let loss_bf16 = estimate_precision_loss(50.0, StoragePrecision::Bf16);
1371 assert!(
1373 loss_bf16 > loss_fp16,
1374 "BF16 loss ({loss_bf16}) should exceed FP16 loss ({loss_fp16})"
1375 );
1376 }
1377
1378 #[test]
1381 fn scalar_launch_params_correct() {
1382 let (grid, block) = scalar_launch_params(1000);
1383 assert_eq!(block, 256);
1384 assert_eq!(grid, 4); }
1386
1387 #[test]
1388 fn vector_launch_params_correct() {
1389 let (grid, block) = vector_launch_params(1000);
1390 assert_eq!(block, 256);
1391 assert_eq!(grid, 125);
1393 }
1394
1395 #[test]
1402 fn mixed_precision_scalar_ptx_contains_fp16_to_fp32_conversion() {
1403 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1404 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1405 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1406 let ptx = ptx.expect("test");
1407
1408 assert!(
1410 ptx.contains("cvt.f32.f16"),
1411 "scalar kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1412 );
1413 }
1414
1415 #[test]
1419 fn mixed_precision_scalar_ptx_contains_fma_rn_f32_accumulation() {
1420 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1421 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1422 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1423 let ptx = ptx.expect("test");
1424
1425 assert!(
1427 ptx.contains("fma.rn.f32"),
1428 "scalar kernel must contain fma.rn.f32 for FP32 accumulation"
1429 );
1430 }
1431
1432 #[test]
1434 fn mixed_precision_vector_ptx_contains_conversion_and_fma() {
1435 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Vector, SmVersion::Sm80);
1436 let ptx = generate_mixed_vector_spmv_ptx(&config);
1437 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1438 let ptx = ptx.expect("test");
1439
1440 assert!(
1441 ptx.contains("cvt.f32.f16"),
1442 "vector kernel must contain cvt.f32.f16 for FP16→FP32 widening"
1443 );
1444 assert!(
1445 ptx.contains("fma.rn.f32"),
1446 "vector kernel must contain fma.rn.f32 for FP32 accumulation"
1447 );
1448 }
1449
1450 #[test]
1452 fn mixed_precision_bf16_ptx_uses_bf16_conversion() {
1453 let config = MixedPrecisionConfig::bf16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1454 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1455 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1456 let ptx = ptx.expect("test");
1457
1458 assert!(
1459 ptx.contains("cvt.f32.bf16"),
1460 "BF16 kernel must contain cvt.f32.bf16 for BF16→FP32 widening"
1461 );
1462 assert!(
1464 !ptx.contains("cvt.f32.f16"),
1465 "BF16 kernel must NOT contain cvt.f32.f16"
1466 );
1467 }
1468
1469 #[test]
1471 fn mixed_precision_scalar_ptx_uses_rn_mode_for_scaling() {
1472 let config = MixedPrecisionConfig::fp16_fp32(MixedSpMVAlgo::Scalar, SmVersion::Sm80);
1473 let ptx = generate_mixed_scalar_spmv_ptx(&config);
1474 assert!(ptx.is_ok(), "PTX gen failed: {ptx:?}");
1475 let ptx = ptx.expect("test");
1476
1477 assert!(
1479 ptx.contains("mul.rn.f32"),
1480 "scalar kernel must contain mul.rn.f32 for alpha/beta scaling"
1481 );
1482 }
1483
1484 #[test]
1486 fn precision_loss_monotone_in_nnz_per_row() {
1487 let nnz_values = [1.0_f64, 10.0, 100.0, 1000.0];
1488 let mut prev_loss = 0.0_f64;
1489 for &nnz in &nnz_values {
1490 let loss = estimate_precision_loss(nnz, StoragePrecision::Fp16);
1491 assert!(
1492 loss >= prev_loss,
1493 "precision loss should increase with nnz/row: nnz={nnz}, loss={loss}, prev={prev_loss}"
1494 );
1495 prev_loss = loss;
1496 }
1497 }
1498
1499 #[test]
1504 fn precision_loss_linear_in_nnz_per_row() {
1505 let loss_10 = estimate_precision_loss(10.0, StoragePrecision::Fp16);
1506 let loss_20 = estimate_precision_loss(20.0, StoragePrecision::Fp16);
1507
1508 assert!(
1510 (loss_20 - 2.0 * loss_10).abs() < 1e-15,
1511 "Precision loss should be linear: loss(20)={loss_20} should be 2*loss(10)={loss_10}"
1512 );
1513 }
1514
1515 #[test]
1517 fn bandwidth_savings_bf16_vs_fp32() {
1518 let config = default_bf16_config(MixedSpMVAlgo::Scalar);
1519 let plan = plan_mixed_precision_spmv(&config, 1000, 1000, 10000).expect("test");
1520 let ratio = plan.bandwidth_savings_ratio();
1522 assert!(
1523 (ratio - 2.0).abs() < 1e-10,
1524 "BF16 bandwidth savings ratio should be ~2.0, got {ratio}"
1525 );
1526 }
1527
1528 #[test]
1530 fn mixed_plan_avg_nnz_per_row_calculation() {
1531 let config = default_fp16_config(MixedSpMVAlgo::Scalar);
1532 let plan = plan_mixed_precision_spmv(&config, 100, 1000, 500).expect("test");
1534 let avg = plan.avg_nnz_per_row();
1535 assert!(
1536 (avg - 5.0).abs() < 1e-10,
1537 "avg_nnz_per_row should be 5.0, got {avg}"
1538 );
1539 }
1540}