1use std::ops::{Add, AddAssign, Mul};
17
18use oxicuda_blas::GpuFloat;
19
20use crate::error::{SparseError, SparseResult};
21use crate::format::CsrMatrix;
22
23type SpGEMMResultU32<T> = (Vec<i32>, Vec<i32>, Vec<T>, u32, u32);
25
26type SpGEMMResultUsize<T> = (Vec<i32>, Vec<i32>, Vec<T>, usize, usize);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Strategy {
36 Sequential,
38 Concurrent(usize),
40 Fused,
42}
43
44#[derive(Debug, Clone)]
54pub struct BatchScheduler {
55 _private: (),
56}
57
58impl BatchScheduler {
59 #[inline]
61 pub fn new() -> Self {
62 Self { _private: () }
63 }
64
65 pub fn select_strategy(&self, batch_size: usize, avg_nnz: usize) -> Strategy {
72 Self::select_strategy_static(batch_size, avg_nnz)
73 }
74
75 pub fn select_strategy_static(batch_size: usize, avg_nnz: usize) -> Strategy {
78 if batch_size <= 4 && avg_nnz >= 10_000 {
80 return Strategy::Sequential;
81 }
82 if batch_size >= 64 && avg_nnz < 256 {
84 return Strategy::Fused;
85 }
86 let streams = batch_size.clamp(1, 8);
88 Strategy::Concurrent(streams)
89 }
90}
91
92impl Default for BatchScheduler {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[derive(Debug, Clone)]
112pub struct BatchedSpMVPlan<T> {
113 pub concat_row_ptr: Vec<i32>,
115 pub concat_col_idx: Vec<i32>,
117 pub concat_values: Vec<T>,
119 pub batch_offsets_row_ptr: Vec<usize>,
121 pub batch_offsets_nnz: Vec<usize>,
123 pub row_counts: Vec<usize>,
125 pub col_counts: Vec<usize>,
127 pub nnz_counts: Vec<usize>,
129 pub batch_size: usize,
131 pub strategy: Strategy,
133}
134
135impl<T: GpuFloat> BatchedSpMVPlan<T> {
136 pub fn from_matrices(matrices: &[CsrMatrix<T>]) -> SparseResult<Self> {
146 if matrices.is_empty() {
147 return Err(SparseError::InvalidArgument(
148 "batch must contain at least one matrix".to_string(),
149 ));
150 }
151
152 let batch_size = matrices.len();
153 let mut concat_row_ptr = Vec::new();
154 let mut concat_col_idx = Vec::new();
155 let mut concat_values: Vec<T> = Vec::new();
156 let mut batch_offsets_row_ptr = Vec::with_capacity(batch_size);
157 let mut batch_offsets_nnz = Vec::with_capacity(batch_size);
158 let mut row_counts = Vec::with_capacity(batch_size);
159 let mut col_counts = Vec::with_capacity(batch_size);
160 let mut nnz_counts = Vec::with_capacity(batch_size);
161
162 for mat in matrices {
163 let (h_rp, h_ci, h_vals) = mat.to_host()?;
164
165 batch_offsets_row_ptr.push(concat_row_ptr.len());
166 batch_offsets_nnz.push(concat_col_idx.len());
167 row_counts.push(mat.rows() as usize);
168 col_counts.push(mat.cols() as usize);
169 nnz_counts.push(mat.nnz() as usize);
170
171 concat_row_ptr.extend_from_slice(&h_rp);
172 concat_col_idx.extend_from_slice(&h_ci);
173 concat_values.extend_from_slice(&h_vals);
174 }
175
176 let total_nnz = nnz_counts.iter().copied().sum::<usize>();
177 let avg_nnz = total_nnz.checked_div(batch_size).unwrap_or(0);
178 let strategy = BatchScheduler::select_strategy_static(batch_size, avg_nnz);
179
180 Ok(Self {
181 concat_row_ptr,
182 concat_col_idx,
183 concat_values,
184 batch_offsets_row_ptr,
185 batch_offsets_nnz,
186 row_counts,
187 col_counts,
188 nnz_counts,
189 batch_size,
190 strategy,
191 })
192 }
193
194 pub fn from_host_arrays(
203 row_ptrs: &[Vec<i32>],
204 col_indices: &[Vec<i32>],
205 values: &[Vec<T>],
206 rows: &[usize],
207 cols: &[usize],
208 ) -> SparseResult<Self> {
209 let batch_size = row_ptrs.len();
210 if batch_size == 0 {
211 return Err(SparseError::InvalidArgument(
212 "batch must contain at least one matrix".to_string(),
213 ));
214 }
215 if col_indices.len() != batch_size
216 || values.len() != batch_size
217 || rows.len() != batch_size
218 || cols.len() != batch_size
219 {
220 return Err(SparseError::InvalidArgument(
221 "all input slices must have the same length".to_string(),
222 ));
223 }
224
225 let mut concat_row_ptr = Vec::new();
226 let mut concat_col_idx = Vec::new();
227 let mut concat_values: Vec<T> = Vec::new();
228 let mut batch_offsets_row_ptr = Vec::with_capacity(batch_size);
229 let mut batch_offsets_nnz = Vec::with_capacity(batch_size);
230 let mut row_counts = Vec::with_capacity(batch_size);
231 let mut col_counts = Vec::with_capacity(batch_size);
232 let mut nnz_counts = Vec::with_capacity(batch_size);
233
234 for i in 0..batch_size {
235 batch_offsets_row_ptr.push(concat_row_ptr.len());
236 batch_offsets_nnz.push(concat_col_idx.len());
237 row_counts.push(rows[i]);
238 col_counts.push(cols[i]);
239 nnz_counts.push(values[i].len());
240
241 concat_row_ptr.extend_from_slice(&row_ptrs[i]);
242 concat_col_idx.extend_from_slice(&col_indices[i]);
243 concat_values.extend_from_slice(&values[i]);
244 }
245
246 let total_nnz = nnz_counts.iter().copied().sum::<usize>();
247 let avg_nnz = total_nnz.checked_div(batch_size).unwrap_or(0);
248 let strategy = BatchScheduler::select_strategy_static(batch_size, avg_nnz);
249
250 Ok(Self {
251 concat_row_ptr,
252 concat_col_idx,
253 concat_values,
254 batch_offsets_row_ptr,
255 batch_offsets_nnz,
256 row_counts,
257 col_counts,
258 nnz_counts,
259 batch_size,
260 strategy,
261 })
262 }
263
264 #[inline]
266 pub fn total_nnz(&self) -> usize {
267 self.nnz_counts.iter().copied().sum()
268 }
269
270 #[inline]
272 pub fn total_rows(&self) -> usize {
273 self.row_counts.iter().copied().sum()
274 }
275
276 #[inline]
278 pub fn avg_nnz(&self) -> usize {
279 if self.batch_size == 0 {
280 return 0;
281 }
282 self.total_nnz() / self.batch_size
283 }
284}
285
286#[derive(Debug)]
299pub struct BatchedSpMV<T: GpuFloat> {
300 matrices: Vec<HostCsr<T>>,
302}
303
304#[derive(Debug, Clone)]
306struct HostCsr<T> {
307 rows: usize,
308 cols: usize,
309 row_ptr: Vec<i32>,
310 col_idx: Vec<i32>,
311 values: Vec<T>,
312}
313
314impl<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign> BatchedSpMV<T> {
315 pub fn from_device(matrices: &[CsrMatrix<T>]) -> SparseResult<Self> {
322 if matrices.is_empty() {
323 return Err(SparseError::InvalidArgument(
324 "batch must contain at least one matrix".to_string(),
325 ));
326 }
327
328 let mut host_mats = Vec::with_capacity(matrices.len());
329 for mat in matrices {
330 let (rp, ci, vals) = mat.to_host()?;
331 host_mats.push(HostCsr {
332 rows: mat.rows() as usize,
333 cols: mat.cols() as usize,
334 row_ptr: rp,
335 col_idx: ci,
336 values: vals,
337 });
338 }
339
340 Ok(Self {
341 matrices: host_mats,
342 })
343 }
344
345 pub fn from_host(
351 row_ptrs: Vec<Vec<i32>>,
352 col_indices: Vec<Vec<i32>>,
353 values: Vec<Vec<T>>,
354 rows: Vec<usize>,
355 cols: Vec<usize>,
356 ) -> SparseResult<Self> {
357 let n = row_ptrs.len();
358 if n == 0 {
359 return Err(SparseError::InvalidArgument(
360 "batch must contain at least one matrix".to_string(),
361 ));
362 }
363 if col_indices.len() != n || values.len() != n || rows.len() != n || cols.len() != n {
364 return Err(SparseError::InvalidArgument(
365 "all input vectors must have the same length".to_string(),
366 ));
367 }
368
369 let mut host_mats = Vec::with_capacity(n);
370 for i in 0..n {
371 host_mats.push(HostCsr {
372 rows: rows[i],
373 cols: cols[i],
374 row_ptr: row_ptrs[i].clone(),
375 col_idx: col_indices[i].clone(),
376 values: values[i].clone(),
377 });
378 }
379
380 Ok(Self {
381 matrices: host_mats,
382 })
383 }
384
385 #[inline]
387 pub fn batch_size(&self) -> usize {
388 self.matrices.len()
389 }
390
391 pub fn execute(&self, xs: &[Vec<T>], ys: &mut [Vec<T>], alpha: T, beta: T) -> SparseResult<()> {
408 let n = self.matrices.len();
409 if xs.len() != n || ys.len() != n {
410 return Err(SparseError::DimensionMismatch(format!(
411 "expected {} vectors, got xs={}, ys={}",
412 n,
413 xs.len(),
414 ys.len()
415 )));
416 }
417
418 for (i, mat) in self.matrices.iter().enumerate() {
419 if xs[i].len() < mat.cols {
420 return Err(SparseError::DimensionMismatch(format!(
421 "matrix {} has {} cols but x has {} elements",
422 i,
423 mat.cols,
424 xs[i].len()
425 )));
426 }
427 if ys[i].len() < mat.rows {
428 return Err(SparseError::DimensionMismatch(format!(
429 "matrix {} has {} rows but y has {} elements",
430 i,
431 mat.rows,
432 ys[i].len()
433 )));
434 }
435
436 host_csr_spmv(mat, &xs[i], &mut ys[i], alpha, beta);
437 }
438
439 Ok(())
440 }
441}
442
443fn host_csr_spmv<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
445 mat: &HostCsr<T>,
446 x: &[T],
447 y: &mut [T],
448 alpha: T,
449 beta: T,
450) {
451 for (row, y_row) in y.iter_mut().enumerate().take(mat.rows) {
452 let start = mat.row_ptr[row] as usize;
453 let end = mat.row_ptr[row + 1] as usize;
454
455 let mut acc = T::gpu_zero();
456 for j in start..end {
457 let col = mat.col_idx[j] as usize;
458 acc += mat.values[j] * x[col];
459 }
460
461 *y_row = alpha * acc + beta * *y_row;
462 }
463}
464
465#[derive(Debug, Clone)]
475pub struct UniformBatchedSpMV<T> {
476 rows: usize,
478 cols: usize,
480 row_ptr: Vec<i32>,
482 col_idx: Vec<i32>,
484 batch_values: Vec<Vec<T>>,
486}
487
488impl<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign> UniformBatchedSpMV<T> {
489 pub fn from_pattern(pattern: &CsrMatrix<T>, batch_values: Vec<Vec<T>>) -> SparseResult<Self> {
500 if batch_values.is_empty() {
501 return Err(SparseError::InvalidArgument(
502 "batch_values must not be empty".to_string(),
503 ));
504 }
505
506 let nnz = pattern.nnz() as usize;
507 for (i, vals) in batch_values.iter().enumerate() {
508 if vals.len() != nnz {
509 return Err(SparseError::InvalidArgument(format!(
510 "batch_values[{}] has length {} but pattern nnz is {}",
511 i,
512 vals.len(),
513 nnz
514 )));
515 }
516 }
517
518 let (rp, ci, _) = pattern.to_host()?;
519
520 Ok(Self {
521 rows: pattern.rows() as usize,
522 cols: pattern.cols() as usize,
523 row_ptr: rp,
524 col_idx: ci,
525 batch_values,
526 })
527 }
528
529 pub fn from_host_arrays(
535 rows: usize,
536 cols: usize,
537 row_ptr: Vec<i32>,
538 col_idx: Vec<i32>,
539 batch_values: Vec<Vec<T>>,
540 ) -> SparseResult<Self> {
541 if batch_values.is_empty() {
542 return Err(SparseError::InvalidArgument(
543 "batch_values must not be empty".to_string(),
544 ));
545 }
546 if row_ptr.len() != rows + 1 {
547 return Err(SparseError::InvalidArgument(format!(
548 "row_ptr length {} != rows + 1 ({})",
549 row_ptr.len(),
550 rows + 1
551 )));
552 }
553 let nnz = col_idx.len();
554 for (i, vals) in batch_values.iter().enumerate() {
555 if vals.len() != nnz {
556 return Err(SparseError::InvalidArgument(format!(
557 "batch_values[{}] length {} != nnz {}",
558 i,
559 vals.len(),
560 nnz
561 )));
562 }
563 }
564
565 Ok(Self {
566 rows,
567 cols,
568 row_ptr,
569 col_idx,
570 batch_values,
571 })
572 }
573
574 #[inline]
576 pub fn batch_size(&self) -> usize {
577 self.batch_values.len()
578 }
579
580 pub fn execute(&self, xs: &[Vec<T>], ys: &mut [Vec<T>], alpha: T, beta: T) -> SparseResult<()> {
586 let n = self.batch_values.len();
587 if xs.len() != n || ys.len() != n {
588 return Err(SparseError::DimensionMismatch(format!(
589 "expected {} vectors, got xs={}, ys={}",
590 n,
591 xs.len(),
592 ys.len()
593 )));
594 }
595
596 for i in 0..n {
597 if xs[i].len() < self.cols {
598 return Err(SparseError::DimensionMismatch(format!(
599 "x[{}] length {} < cols {}",
600 i,
601 xs[i].len(),
602 self.cols
603 )));
604 }
605 if ys[i].len() < self.rows {
606 return Err(SparseError::DimensionMismatch(format!(
607 "y[{}] length {} < rows {}",
608 i,
609 ys[i].len(),
610 self.rows
611 )));
612 }
613
614 let mat = HostCsr {
615 rows: self.rows,
616 cols: self.cols,
617 row_ptr: self.row_ptr.clone(),
618 col_idx: self.col_idx.clone(),
619 values: self.batch_values[i].clone(),
620 };
621 host_csr_spmv(&mat, &xs[i], &mut ys[i], alpha, beta);
622 }
623
624 Ok(())
625 }
626}
627
628#[derive(Debug)]
638pub struct BatchedSpGEMM {
639 _private: (),
640}
641
642impl BatchedSpGEMM {
643 #[inline]
645 pub fn new() -> Self {
646 Self { _private: () }
647 }
648
649 pub fn execute<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
663 a_batch: &[CsrMatrix<T>],
664 b_batch: &[CsrMatrix<T>],
665 ) -> SparseResult<Vec<SpGEMMResultU32<T>>> {
666 if a_batch.is_empty() {
667 return Err(SparseError::InvalidArgument(
668 "batch must not be empty".to_string(),
669 ));
670 }
671 if a_batch.len() != b_batch.len() {
672 return Err(SparseError::InvalidArgument(format!(
673 "a_batch length {} != b_batch length {}",
674 a_batch.len(),
675 b_batch.len()
676 )));
677 }
678
679 let mut results = Vec::with_capacity(a_batch.len());
680
681 for (i, (a, b)) in a_batch.iter().zip(b_batch.iter()).enumerate() {
682 if a.cols() != b.rows() {
683 return Err(SparseError::DimensionMismatch(format!(
684 "pair {}: A.cols ({}) != B.rows ({})",
685 i,
686 a.cols(),
687 b.rows()
688 )));
689 }
690
691 let (a_rp, a_ci, a_vals) = a.to_host()?;
692 let (b_rp, b_ci, b_vals) = b.to_host()?;
693
694 let (c_rp, c_ci, c_vals) = host_spgemm(
695 &a_rp,
696 &a_ci,
697 &a_vals,
698 a.rows() as usize,
699 &b_rp,
700 &b_ci,
701 &b_vals,
702 b.cols() as usize,
703 );
704
705 results.push((c_rp, c_ci, c_vals, a.rows(), b.cols()));
706 }
707
708 Ok(results)
709 }
710
711 #[allow(clippy::too_many_arguments)]
718 pub fn execute_host<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
719 a_row_ptrs: &[Vec<i32>],
720 a_col_indices: &[Vec<i32>],
721 a_values: &[Vec<T>],
722 a_rows: &[usize],
723 a_cols: &[usize],
724 b_row_ptrs: &[Vec<i32>],
725 b_col_indices: &[Vec<i32>],
726 b_values: &[Vec<T>],
727 b_cols: &[usize],
728 ) -> SparseResult<Vec<SpGEMMResultUsize<T>>> {
729 let n = a_row_ptrs.len();
730 if n == 0 {
731 return Err(SparseError::InvalidArgument(
732 "batch must not be empty".to_string(),
733 ));
734 }
735 if b_row_ptrs.len() != n
736 || a_col_indices.len() != n
737 || a_values.len() != n
738 || a_rows.len() != n
739 || a_cols.len() != n
740 || b_col_indices.len() != n
741 || b_values.len() != n
742 || b_cols.len() != n
743 {
744 return Err(SparseError::InvalidArgument(
745 "all input slices must have the same length".to_string(),
746 ));
747 }
748
749 let mut results = Vec::with_capacity(n);
750 for i in 0..n {
751 let b_rows_i = a_cols[i]; let (c_rp, c_ci, c_vals) = host_spgemm(
753 &a_row_ptrs[i],
754 &a_col_indices[i],
755 &a_values[i],
756 a_rows[i],
757 &b_row_ptrs[i],
758 &b_col_indices[i],
759 &b_values[i],
760 b_cols[i],
761 );
762 let _ = b_rows_i; results.push((c_rp, c_ci, c_vals, a_rows[i], b_cols[i]));
764 }
765
766 Ok(results)
767 }
768}
769
770impl Default for BatchedSpGEMM {
771 fn default() -> Self {
772 Self::new()
773 }
774}
775
776#[allow(clippy::too_many_arguments)]
778fn host_spgemm<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
779 a_rp: &[i32],
780 a_ci: &[i32],
781 a_vals: &[T],
782 a_rows: usize,
783 b_rp: &[i32],
784 b_ci: &[i32],
785 b_vals: &[T],
786 b_cols: usize,
787) -> (Vec<i32>, Vec<i32>, Vec<T>) {
788 use std::collections::BTreeMap;
789
790 let mut c_row_ptr = vec![0i32; a_rows + 1];
791 let mut c_col_idx = Vec::new();
792 let mut c_values: Vec<T> = Vec::new();
793
794 for row in 0..a_rows {
795 let a_start = a_rp[row] as usize;
796 let a_end = a_rp[row + 1] as usize;
797
798 let mut acc: BTreeMap<usize, T> = BTreeMap::new();
800
801 for ja in a_start..a_end {
802 let a_col = a_ci[ja] as usize;
803 let a_val = a_vals[ja];
804
805 let b_start = b_rp[a_col] as usize;
806 let b_end = b_rp[a_col + 1] as usize;
807
808 for jb in b_start..b_end {
809 let b_col = b_ci[jb] as usize;
810 if b_col < b_cols {
811 let product = a_val * b_vals[jb];
812 acc.entry(b_col)
813 .and_modify(|v| *v += product)
814 .or_insert(product);
815 }
816 }
817 }
818
819 for (&col, &val) in &acc {
820 c_col_idx.push(col as i32);
821 c_values.push(val);
822 }
823 c_row_ptr[row + 1] = c_col_idx.len() as i32;
824 }
825
826 (c_row_ptr, c_col_idx, c_values)
827}
828
829#[derive(Debug)]
838pub struct BatchedTriSolve {
839 _private: (),
840}
841
842impl BatchedTriSolve {
843 #[inline]
845 pub fn new() -> Self {
846 Self { _private: () }
847 }
848
849 pub fn execute<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
860 l_batch: &[CsrMatrix<T>],
861 b_batch: &[Vec<T>],
862 ) -> SparseResult<Vec<Vec<T>>> {
863 if l_batch.is_empty() {
864 return Err(SparseError::InvalidArgument(
865 "batch must not be empty".to_string(),
866 ));
867 }
868 if l_batch.len() != b_batch.len() {
869 return Err(SparseError::InvalidArgument(format!(
870 "l_batch length {} != b_batch length {}",
871 l_batch.len(),
872 b_batch.len()
873 )));
874 }
875
876 let mut results = Vec::with_capacity(l_batch.len());
877
878 for (i, (l, b)) in l_batch.iter().zip(b_batch.iter()).enumerate() {
879 if l.rows() != l.cols() {
880 return Err(SparseError::DimensionMismatch(format!(
881 "matrix {} is not square: {}x{}",
882 i,
883 l.rows(),
884 l.cols()
885 )));
886 }
887 if b.len() < l.rows() as usize {
888 return Err(SparseError::DimensionMismatch(format!(
889 "matrix {} has {} rows but rhs has {} elements",
890 i,
891 l.rows(),
892 b.len()
893 )));
894 }
895
896 let (rp, ci, vals) = l.to_host()?;
897 let x = host_forward_solve(&rp, &ci, &vals, l.rows() as usize, b)?;
898 results.push(x);
899 }
900
901 Ok(results)
902 }
903
904 pub fn execute_host<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
911 row_ptrs: &[Vec<i32>],
912 col_indices: &[Vec<i32>],
913 values: &[Vec<T>],
914 sizes: &[usize],
915 rhs: &[Vec<T>],
916 ) -> SparseResult<Vec<Vec<T>>> {
917 let n = row_ptrs.len();
918 if n == 0 {
919 return Err(SparseError::InvalidArgument(
920 "batch must not be empty".to_string(),
921 ));
922 }
923 if col_indices.len() != n || values.len() != n || sizes.len() != n || rhs.len() != n {
924 return Err(SparseError::InvalidArgument(
925 "all input slices must have the same length".to_string(),
926 ));
927 }
928
929 let mut results = Vec::with_capacity(n);
930 for i in 0..n {
931 let x =
932 host_forward_solve(&row_ptrs[i], &col_indices[i], &values[i], sizes[i], &rhs[i])?;
933 results.push(x);
934 }
935
936 Ok(results)
937 }
938}
939
940impl Default for BatchedTriSolve {
941 fn default() -> Self {
942 Self::new()
943 }
944}
945
946fn host_forward_solve<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
948 row_ptr: &[i32],
949 col_idx: &[i32],
950 values: &[T],
951 n: usize,
952 b: &[T],
953) -> SparseResult<Vec<T>> {
954 let mut x = vec![T::gpu_zero(); n];
955
956 for row in 0..n {
957 let start = row_ptr[row] as usize;
958 let end = row_ptr[row + 1] as usize;
959
960 let mut off_diag_sum = T::gpu_zero();
963 let mut diag = T::gpu_zero();
964
965 for j in start..end {
966 let col = col_idx[j] as usize;
967 if col < row {
968 off_diag_sum += values[j] * x[col];
969 } else if col == row {
970 diag = values[j];
971 }
972 }
973
974 if diag == T::gpu_zero() {
976 return Err(SparseError::SingularMatrix);
977 }
978
979 let neg_sum = negate_float(off_diag_sum);
983 let numerator = b[row] + neg_sum;
984 x[row] = divide_float(numerator, diag);
985 }
986
987 Ok(x)
988}
989
990#[inline]
992fn negate_float<T: GpuFloat>(val: T) -> T {
993 let bits = val.to_bits_u64();
994 if T::SIZE == 4 {
995 T::from_bits_u64(bits ^ (1u64 << 31))
997 } else {
998 T::from_bits_u64(bits ^ (1u64 << 63))
1000 }
1001}
1002
1003#[inline]
1005fn divide_float<T: GpuFloat>(num: T, den: T) -> T {
1006 let n_bits = num.to_bits_u64();
1008 let d_bits = den.to_bits_u64();
1009 if T::SIZE == 4 {
1010 let n = f32::from_bits(n_bits as u32) as f64;
1011 let d = f32::from_bits(d_bits as u32) as f64;
1012 let result = (n / d) as f32;
1013 T::from_bits_u64(u64::from(result.to_bits()))
1014 } else {
1015 let n = f64::from_bits(n_bits);
1016 let d = f64::from_bits(d_bits);
1017 let result = n / d;
1018 T::from_bits_u64(result.to_bits())
1019 }
1020}
1021
1022pub fn generate_batched_spmv_ptx<T: GpuFloat>() -> String {
1045 let type_name = T::NAME;
1046 let ptx_type = match T::SIZE {
1047 4 => ".f32",
1048 8 => ".f64",
1049 _ => ".f32",
1050 };
1051
1052 format!(
1053 r#"//
1054// Batched SpMV kernel for {type_name}
1055// Generated by oxicuda-sparse batched module
1056//
1057.version 7.0
1058.target sm_70
1059.address_size 64
1060
1061.visible .entry batched_spmv_{type_name}(
1062 .param .u64 concat_row_ptr,
1063 .param .u64 concat_col_idx,
1064 .param .u64 concat_values,
1065 .param .u64 batch_offsets_rp,
1066 .param .u64 batch_offsets_nnz,
1067 .param .u64 row_counts,
1068 .param .u64 x_ptrs,
1069 .param .u64 y_ptrs,
1070 .param {ptx_type} alpha,
1071 .param {ptx_type} beta,
1072 .param .u32 batch_size
1073)
1074{{
1075 .reg .u32 %r<16>;
1076 .reg .u64 %rd<32>;
1077 .reg {ptx_type} %f<8>;
1078 .reg .pred %p<4>;
1079
1080 // blockIdx.x = matrix index in batch
1081 mov.u32 %r0, %ctaid.x;
1082 // Early exit if blockIdx >= batch_size
1083 ld.param.u32 %r1, [batch_size];
1084 setp.ge.u32 %p0, %r0, %r1;
1085 @%p0 ret;
1086
1087 // threadIdx.x = local row within this matrix
1088 mov.u32 %r2, %tid.x;
1089
1090 // Load row_count for this matrix
1091 ld.param.u64 %rd0, [row_counts];
1092 cvt.u64.u32 %rd1, %r0;
1093 mad.wide.u32 %rd2, %r0, 4, %rd0;
1094 ld.global.u32 %r3, [%rd2];
1095
1096 // Early exit if tid >= row_count
1097 setp.ge.u32 %p1, %r2, %r3;
1098 @%p1 ret;
1099
1100 ret;
1101}}
1102"#
1103 )
1104}
1105
1106pub fn batched_spmv_cpu(
1130 n_rows: usize,
1131 _n_cols: usize,
1132 row_ptr: &[u32],
1133 col_idx: &[u32],
1134 values: &[f32],
1135 x_batch: &[f32],
1136 batch_size: usize,
1137) -> Vec<f32> {
1138 let mut y = vec![0.0f32; n_rows * batch_size];
1139 for row in 0..n_rows {
1140 let start = row_ptr[row] as usize;
1141 let end = row_ptr[row + 1] as usize;
1142 for idx in start..end {
1143 let col = col_idx[idx] as usize;
1144 let val = values[idx];
1145 for b in 0..batch_size {
1146 y[row * batch_size + b] += val * x_batch[col * batch_size + b];
1147 }
1148 }
1149 }
1150 y
1151}
1152
1153pub fn mixed_precision_spmv_cpu(
1177 n_rows: usize,
1178 row_ptr: &[u32],
1179 col_idx: &[u32],
1180 values_fp16: &[f32],
1181 x: &[f32],
1182) -> Vec<f32> {
1183 let mut y = vec![0.0f32; n_rows];
1184 for row in 0..n_rows {
1185 let start = row_ptr[row] as usize;
1186 let end = row_ptr[row + 1] as usize;
1187 let mut acc = 0.0f64;
1189 for idx in start..end {
1190 let col = col_idx[idx] as usize;
1191 let val = values_fp16[idx] as f64;
1192 acc += val * (x[col] as f64);
1193 }
1194 y[row] = acc as f32;
1195 }
1196 y
1197}
1198
1199#[cfg(test)]
1204mod tests {
1205 use super::*;
1206
1207 #[test]
1210 fn scheduler_sequential_for_small_batch_large_matrices() {
1211 let s = BatchScheduler::new();
1212 assert_eq!(s.select_strategy(2, 50_000), Strategy::Sequential);
1213 assert_eq!(s.select_strategy(4, 10_000), Strategy::Sequential);
1214 }
1215
1216 #[test]
1217 fn scheduler_fused_for_large_batch_small_matrices() {
1218 let s = BatchScheduler::new();
1219 assert_eq!(s.select_strategy(100, 100), Strategy::Fused);
1220 assert_eq!(s.select_strategy(64, 255), Strategy::Fused);
1221 }
1222
1223 #[test]
1224 fn scheduler_concurrent_for_medium_cases() {
1225 let s = BatchScheduler::new();
1226 let strat = s.select_strategy(16, 1000);
1227 match strat {
1228 Strategy::Concurrent(n) => assert!((1..=8).contains(&n)),
1229 other => panic!("expected Concurrent, got {:?}", other),
1230 }
1231 }
1232
1233 #[test]
1234 fn scheduler_concurrent_caps_at_8_streams() {
1235 let strat = BatchScheduler::select_strategy_static(32, 1000);
1236 assert_eq!(strat, Strategy::Concurrent(8));
1237 }
1238
1239 #[test]
1240 fn scheduler_static_matches_instance() {
1241 let s = BatchScheduler::new();
1242 for (bs, nnz) in [(1, 100), (10, 500), (64, 100), (3, 20_000)] {
1243 assert_eq!(
1244 s.select_strategy(bs, nnz),
1245 BatchScheduler::select_strategy_static(bs, nnz)
1246 );
1247 }
1248 }
1249
1250 #[test]
1251 fn scheduler_default_trait() {
1252 let s = BatchScheduler::default();
1253 let _ = s.select_strategy(1, 1);
1255 }
1256
1257 #[test]
1260 fn plan_from_host_arrays_basic() {
1261 let rp = vec![vec![0, 1, 2], vec![0, 1, 2]];
1263 let ci = vec![vec![0, 1], vec![0, 1]];
1264 let vals: Vec<Vec<f32>> = vec![vec![1.0, 1.0], vec![2.0, 2.0]];
1265 let rows = vec![2, 2];
1266 let cols = vec![2, 2];
1267
1268 let plan = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &rows, &cols)
1269 .expect("plan creation should succeed");
1270
1271 assert_eq!(plan.batch_size, 2);
1272 assert_eq!(plan.row_counts, vec![2, 2]);
1273 assert_eq!(plan.nnz_counts, vec![2, 2]);
1274 assert_eq!(plan.total_nnz(), 4);
1275 assert_eq!(plan.total_rows(), 4);
1276 assert_eq!(plan.avg_nnz(), 2);
1277 assert_eq!(plan.batch_offsets_row_ptr, vec![0, 3]);
1278 assert_eq!(plan.batch_offsets_nnz, vec![0, 2]);
1279 assert_eq!(plan.concat_row_ptr, vec![0, 1, 2, 0, 1, 2]);
1280 assert_eq!(plan.concat_col_idx, vec![0, 1, 0, 1]);
1281 }
1282
1283 #[test]
1284 fn plan_from_host_arrays_empty_batch() {
1285 let result = BatchedSpMVPlan::<f32>::from_host_arrays(&[], &[], &[], &[], &[]);
1286 assert!(result.is_err());
1287 }
1288
1289 #[test]
1290 fn plan_from_host_arrays_length_mismatch() {
1291 let rp = vec![vec![0, 1]];
1292 let ci = vec![vec![0], vec![1]]; let vals: Vec<Vec<f64>> = vec![vec![1.0]];
1294 let result = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &[1], &[1]);
1295 assert!(result.is_err());
1296 }
1297
1298 #[test]
1301 fn spmv_host_identity_batch() {
1302 let rp = vec![vec![0, 1, 2, 3], vec![0, 1, 2, 3]];
1304 let ci = vec![vec![0, 1, 2], vec![0, 1, 2]];
1305 let vals = vec![vec![1.0_f64, 1.0, 1.0], vec![1.0, 1.0, 1.0]];
1306 let rows = vec![3, 3];
1307 let cols = vec![3, 3];
1308
1309 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols)
1310 .expect("batch creation should succeed");
1311 assert_eq!(batch.batch_size(), 2);
1312
1313 let xs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1314 let mut ys = vec![vec![0.0; 3], vec![0.0; 3]];
1315
1316 batch
1317 .execute(&xs, &mut ys, 1.0, 0.0)
1318 .expect("execute should succeed");
1319
1320 assert_eq!(ys[0], vec![1.0, 2.0, 3.0]);
1321 assert_eq!(ys[1], vec![4.0, 5.0, 6.0]);
1322 }
1323
1324 #[test]
1325 fn spmv_host_alpha_beta() {
1326 let rp = vec![vec![0, 1, 2]];
1328 let ci = vec![vec![0, 1]];
1329 let vals = vec![vec![2.0_f32, 3.0]];
1330 let rows = vec![2];
1331 let cols = vec![2];
1332
1333 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1334
1335 let xs = vec![vec![1.0, 1.0]];
1336 let mut ys = vec![vec![10.0, 20.0]];
1337
1338 batch
1342 .execute(&xs, &mut ys, 2.0, 0.5)
1343 .expect("execute should succeed");
1344
1345 assert!((ys[0][0] - 9.0).abs() < 1e-6);
1346 assert!((ys[0][1] - 16.0).abs() < 1e-6);
1347 }
1348
1349 #[test]
1350 fn spmv_host_dimension_mismatch() {
1351 let rp = vec![vec![0, 1]];
1352 let ci = vec![vec![0]];
1353 let vals = vec![vec![1.0_f32]];
1354 let rows = vec![1];
1355 let cols = vec![2];
1356
1357 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1358
1359 let xs = vec![vec![1.0; 2], vec![1.0; 2]];
1361 let mut ys = vec![vec![0.0], vec![0.0]];
1362 assert!(batch.execute(&xs, &mut ys, 1.0, 0.0).is_err());
1363 }
1364
1365 #[test]
1366 fn spmv_host_empty_batch_error() {
1367 let result = BatchedSpMV::<f64>::from_host(vec![], vec![], vec![], vec![], vec![]);
1368 assert!(result.is_err());
1369 }
1370
1371 #[test]
1374 fn uniform_spmv_host_basic() {
1375 let row_ptr = vec![0, 1, 2];
1377 let col_idx = vec![0, 1];
1378 let batch_values = vec![
1379 vec![1.0_f64, 1.0], vec![2.0, 3.0], ];
1382
1383 let uniform = UniformBatchedSpMV::from_host_arrays(2, 2, row_ptr, col_idx, batch_values)
1384 .expect("creation should succeed");
1385 assert_eq!(uniform.batch_size(), 2);
1386
1387 let xs = vec![vec![1.0, 2.0], vec![1.0, 2.0]];
1388 let mut ys = vec![vec![0.0; 2], vec![0.0; 2]];
1389
1390 uniform
1391 .execute(&xs, &mut ys, 1.0, 0.0)
1392 .expect("execute should succeed");
1393
1394 assert_eq!(ys[0], vec![1.0, 2.0]); assert!((ys[1][0] - 2.0).abs() < 1e-10); assert!((ys[1][1] - 6.0).abs() < 1e-10); }
1398
1399 #[test]
1400 fn uniform_spmv_validation_errors() {
1401 let result =
1403 UniformBatchedSpMV::<f32>::from_host_arrays(2, 2, vec![0, 1, 2], vec![0, 1], vec![]);
1404 assert!(result.is_err());
1405
1406 let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1408 2,
1409 2,
1410 vec![0, 1, 2],
1411 vec![0, 1],
1412 vec![vec![1.0]], );
1414 assert!(result.is_err());
1415
1416 let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1418 2,
1419 2,
1420 vec![0, 1], vec![0, 1],
1422 vec![vec![1.0, 2.0]],
1423 );
1424 assert!(result.is_err());
1425 }
1426
1427 #[test]
1430 fn tri_solve_host_basic() {
1431 let rp = vec![vec![0, 1, 3]];
1435 let ci = vec![vec![0, 0, 1]];
1436 let vals = vec![vec![2.0_f64, 1.0, 3.0]];
1437 let sizes = vec![2];
1438 let rhs = vec![vec![4.0, 7.0]];
1439
1440 let results = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs)
1441 .expect("solve should succeed");
1442
1443 assert_eq!(results.len(), 1);
1444 assert!((results[0][0] - 2.0).abs() < 1e-10);
1445 assert!((results[0][1] - 5.0 / 3.0).abs() < 1e-10);
1446 }
1447
1448 #[test]
1449 fn tri_solve_host_singular() {
1450 let rp = vec![vec![0, 1, 2]];
1452 let ci = vec![vec![0, 0]]; let vals = vec![vec![1.0_f64, 2.0]];
1454 let sizes = vec![2];
1455 let rhs = vec![vec![1.0, 1.0]];
1456
1457 let result = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs);
1458 assert!(result.is_err());
1459 }
1460
1461 #[allow(dead_code)]
1468 fn dense_matvec(a: &[f64], rows: usize, cols: usize, x: &[f64]) -> Vec<f64> {
1469 let mut y = vec![0.0_f64; rows];
1470 for r in 0..rows {
1471 for c in 0..cols {
1472 y[r] += a[r * cols + c] * x[c];
1473 }
1474 }
1475 y
1476 }
1477
1478 fn dense_matmul(a: &[f64], m: usize, k: usize, b: &[f64], n: usize) -> Vec<f64> {
1481 let mut c = vec![0.0_f64; m * n];
1482 for i in 0..m {
1483 for j in 0..n {
1484 let mut acc = 0.0_f64;
1485 for p in 0..k {
1486 acc += a[i * k + p] * b[p * n + j];
1487 }
1488 c[i * n + j] = acc;
1489 }
1490 }
1491 c
1492 }
1493
1494 #[test]
1507 fn test_spmv_numerical_accuracy_small() {
1508 let row_ptr = vec![vec![0i32, 2, 3, 5, 6, 8]];
1510 let col_idx = vec![vec![0i32, 2, 1, 2, 4, 3, 0, 4]];
1511 let values = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]];
1512 let rows = vec![5usize];
1513 let cols = vec![5usize];
1514
1515 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols)
1516 .expect("batch creation should succeed");
1517
1518 let x = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]];
1519 let mut y = vec![vec![0.0_f64; 5]];
1520
1521 batch
1522 .execute(&x, &mut y, 1.0, 0.0)
1523 .expect("execute should succeed");
1524
1525 let expected = [7.0_f64, 6.0, 37.0, 24.0, 47.0];
1526 for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1527 assert!(
1528 (got - exp).abs() < 1e-14,
1529 "y[{i}]: expected {exp}, got {got}"
1530 );
1531 }
1532 }
1533
1534 #[test]
1536 fn test_spmv_numerical_accuracy_value_spread() {
1537 let big = 1e6_f64;
1539 let small = 1e-6_f64;
1540 let row_ptr = vec![vec![0i32, 1, 2, 3, 4]];
1542 let col_idx = vec![vec![0i32, 1, 2, 3]];
1543 let values = vec![vec![small, big, small, big]];
1544 let rows = vec![4usize];
1545 let cols = vec![4usize];
1546
1547 let batch =
1548 BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols).expect("batch creation");
1549
1550 let x = vec![vec![1.0_f64, 1.0, 1.0, 1.0]];
1551 let mut y = vec![vec![0.0_f64; 4]];
1552 batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1553
1554 let expected = [small, big, small, big];
1555 for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1556 let rel_err = (got - exp).abs() / exp.abs().max(1e-300);
1557 assert!(
1558 rel_err < 1e-10,
1559 "y[{i}]: relative error {rel_err:.3e} exceeds threshold"
1560 );
1561 }
1562 }
1563
1564 #[test]
1566 fn test_spmv_alpha_beta_scaling() {
1567 let row_ptr = vec![vec![0i32, 1, 2]];
1571 let col_idx = vec![vec![0i32, 1]];
1572 let values = vec![vec![2.0_f64, 3.0]];
1573
1574 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![2], vec![2])
1575 .expect("batch creation");
1576
1577 let x = vec![vec![1.0_f64, 1.0]];
1578 let mut y = vec![vec![10.0_f64, 20.0]];
1579 batch.execute(&x, &mut y, 5.0, 0.25).expect("execute");
1580
1581 assert!((y[0][0] - 12.5).abs() < 1e-13, "y[0] = {}", y[0][0]);
1582 assert!((y[0][1] - 20.0).abs() < 1e-13, "y[1] = {}", y[0][1]);
1583 }
1584
1585 #[test]
1587 fn test_spmv_identity_matrix() {
1588 let n = 6usize;
1589 let row_ptr = vec![(0..=(n as i32)).collect::<Vec<i32>>()];
1590 let col_idx = vec![(0..n as i32).collect::<Vec<i32>>()];
1591 let values = vec![vec![1.0_f64; n]];
1592
1593 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![n], vec![n])
1594 .expect("batch creation");
1595
1596 let x_data: Vec<f64> = (1..=(n as i64)).map(|v| v as f64).collect();
1597 let x = vec![x_data.clone()];
1598 let mut y = vec![vec![0.0_f64; n]];
1599 batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1600
1601 for (i, (&got, &exp)) in y[0].iter().zip(x_data.iter()).enumerate() {
1602 assert!(
1603 (got - exp).abs() < 1e-14,
1604 "identity: y[{i}] = {got}, expected {exp}"
1605 );
1606 }
1607 }
1608
1609 #[test]
1614 fn test_spmm_numerical_accuracy() {
1615 let a_dense = [
1621 1.0_f64, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 4.0, 5.0, 0.0, 6.0, 0.0, 0.0, 7.0, 0.0, 8.0,
1622 ];
1623
1624 let row_ptr = vec![0i32, 2, 4, 6, 8];
1625 let col_idx = vec![0i32, 2, 1, 3, 0, 2, 1, 3];
1626 let values = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1627
1628 let b_dense = [
1630 1.0_f64, 0.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 3.0, 0.0, 1.0, 4.0,
1631 ];
1632
1633 let c_ref = dense_matmul(&a_dense, 4, 4, &b_dense, 3);
1635
1636 let n_cols = 3usize;
1638 let mut c_got = vec![0.0_f64; 4 * n_cols];
1639
1640 for col in 0..n_cols {
1641 let x_col: Vec<f64> = (0..4).map(|r| b_dense[r * n_cols + col]).collect();
1643
1644 let batch = BatchedSpMV::from_host(
1645 vec![row_ptr.clone()],
1646 vec![col_idx.clone()],
1647 vec![values.clone()],
1648 vec![4],
1649 vec![4],
1650 )
1651 .expect("batch creation");
1652
1653 let mut y = vec![vec![0.0_f64; 4]];
1654 batch.execute(&[x_col], &mut y, 1.0, 0.0).expect("execute");
1655
1656 for row in 0..4 {
1657 c_got[row * n_cols + col] = y[0][row];
1658 }
1659 }
1660
1661 for i in 0..4 * n_cols {
1662 assert!(
1663 (c_got[i] - c_ref[i]).abs() < 1e-13,
1664 "C[{i}]: got {}, expected {}",
1665 c_got[i],
1666 c_ref[i]
1667 );
1668 }
1669 }
1670
1671 #[test]
1674 fn ptx_generation_f32() {
1675 let ptx = generate_batched_spmv_ptx::<f32>();
1676 assert!(ptx.contains("batched_spmv_f32"));
1677 assert!(ptx.contains(".f32"));
1678 assert!(ptx.contains(".version"));
1679 }
1680
1681 #[test]
1682 fn ptx_generation_f64() {
1683 let ptx = generate_batched_spmv_ptx::<f64>();
1684 assert!(ptx.contains("batched_spmv_f64"));
1685 assert!(ptx.contains(".f64"));
1686 }
1687
1688 #[test]
1691 fn spgemm_host_identity_times_matrix() {
1692 let i_rp = vec![vec![0, 1, 2]];
1694 let i_ci = vec![vec![0, 1]];
1695 let i_vals: Vec<Vec<f64>> = vec![vec![1.0, 1.0]];
1696
1697 let a_rp = vec![vec![0, 2, 3]]; let a_ci = vec![vec![0, 1, 1]];
1699 let a_vals: Vec<Vec<f64>> = vec![vec![2.0, 3.0, 4.0]];
1700
1701 let results = BatchedSpGEMM::execute_host(
1702 &i_rp,
1703 &i_ci,
1704 &i_vals,
1705 &[2],
1706 &[2],
1707 &a_rp,
1708 &a_ci,
1709 &a_vals,
1710 &[2],
1711 )
1712 .expect("spgemm should succeed");
1713
1714 assert_eq!(results.len(), 1);
1715 let (c_rp, c_ci, c_vals, m, n) = &results[0];
1716 assert_eq!(*m, 2);
1717 assert_eq!(*n, 2);
1718 let r0_start = c_rp[0] as usize;
1720 let r0_end = c_rp[1] as usize;
1721 assert_eq!(r0_end - r0_start, 2);
1722 assert_eq!(c_ci[r0_start], 0);
1723 assert_eq!(c_ci[r0_start + 1], 1);
1724 assert!((c_vals[r0_start] - 2.0).abs() < 1e-10);
1725 assert!((c_vals[r0_start + 1] - 3.0).abs() < 1e-10);
1726 }
1727
1728 #[test]
1731 fn batched_spmv_identity_2rhs() {
1732 let n_rows = 4usize;
1734 let n_cols = 4usize;
1735 let row_ptr = vec![0u32, 1, 2, 3, 4];
1736 let col_idx = vec![0u32, 1, 2, 3];
1737 let values = vec![1.0f32; 4];
1738
1739 let batch_size = 2usize;
1746 let x_batch = vec![1.0f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
1747
1748 let y = batched_spmv_cpu(
1749 n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1750 );
1751
1752 assert_eq!(y.len(), n_rows * batch_size);
1758 assert!((y[0] - 1.0).abs() < 1e-6, "row=0, b=0 should be 1.0");
1759 assert!((y[1] - 5.0).abs() < 1e-6, "row=0, b=1 should be 5.0");
1760 assert!((y[2] - 2.0).abs() < 1e-6, "row=1, b=0 should be 2.0");
1761 assert!((y[3] - 6.0).abs() < 1e-6, "row=1, b=1 should be 6.0");
1762 assert!((y[4] - 3.0).abs() < 1e-6, "row=2, b=0 should be 3.0");
1763 assert!((y[5] - 7.0).abs() < 1e-6, "row=2, b=1 should be 7.0");
1764 assert!((y[6] - 4.0).abs() < 1e-6, "row=3, b=0 should be 4.0");
1765 assert!((y[7] - 8.0).abs() < 1e-6, "row=3, b=1 should be 8.0");
1766 }
1767
1768 #[test]
1769 fn batched_spmv_correctness_3rhs() {
1770 let n_rows = 3usize;
1778 let n_cols = 3usize;
1779 let row_ptr = vec![0u32, 2, 4, 6];
1780 let col_idx = vec![0u32, 1, 1, 2, 0, 2];
1781 let values = vec![2.0f32, 1.0, 3.0, 1.0, 1.0, 4.0];
1782 let batch_size = 3usize;
1783
1784 let x_batch = vec![
1787 1.0f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, ];
1791
1792 let y = batched_spmv_cpu(
1793 n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1794 );
1795
1796 assert!((y[0] - 2.0).abs() < 1e-6, "A*e0 row0 = 2");
1800 assert!((y[1] - 1.0).abs() < 1e-6, "A*e1 row0 = 1");
1801 assert!((y[2] - 0.0).abs() < 1e-6, "A*e2 row0 = 0");
1802 assert!((y[3] - 0.0).abs() < 1e-6, "A*e0 row1 = 0");
1803 assert!((y[4] - 3.0).abs() < 1e-6, "A*e1 row1 = 3");
1804 assert!((y[5] - 1.0).abs() < 1e-6, "A*e2 row1 = 1");
1805 assert!((y[6] - 1.0).abs() < 1e-6, "A*e0 row2 = 1");
1806 assert!((y[7] - 0.0).abs() < 1e-6, "A*e1 row2 = 0");
1807 assert!((y[8] - 4.0).abs() < 1e-6, "A*e2 row2 = 4");
1808 }
1809
1810 #[test]
1813 fn mixed_precision_spmv_correctness() {
1814 let n_rows = 4usize;
1816 let row_ptr = vec![0u32, 1, 2, 3, 4];
1817 let col_idx = vec![0u32, 1, 2, 3];
1818 let values_fp16 = vec![1.0f32; 4];
1819 let x = vec![1.0f32, 2.0, 3.0, 4.0];
1820
1821 let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1822
1823 assert_eq!(y.len(), n_rows);
1824 for (i, &yi) in y.iter().enumerate() {
1825 assert!(
1826 (yi - (i + 1) as f32).abs() < 1e-4,
1827 "y[{}] should be {} but got {}",
1828 i,
1829 i + 1,
1830 yi
1831 );
1832 }
1833 }
1834
1835 #[test]
1836 fn mixed_precision_accumulation_fp32() {
1837 let n_rows = 1000usize;
1840 let row_ptr: Vec<u32> = (0..=1000).map(|i| i as u32).collect();
1841 let col_idx: Vec<u32> = (0..1000).map(|i| i as u32).collect();
1842 let values_fp16 = vec![1.0f32; 1000];
1843 let x = vec![1.0f32; 1000];
1844
1845 let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1846
1847 assert_eq!(y.len(), n_rows);
1848 for (i, &yi) in y.iter().enumerate() {
1850 assert!(yi.is_finite(), "y[{}] = {} is not finite", i, yi);
1851 assert!(
1852 (yi - 1.0).abs() < 1e-4,
1853 "y[{}] should be 1.0, got {}",
1854 i,
1855 yi
1856 );
1857 }
1858 }
1859}