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 let elem_size = match T::SIZE {
1052 8 => 8,
1053 _ => 4,
1054 };
1055 let zero_literal = match T::SIZE {
1056 8 => "0d0000000000000000",
1057 _ => "0f00000000",
1058 };
1059
1060 format!(
1061 r#"//
1062// Batched SpMV kernel for {type_name}
1063// Generated by oxicuda-sparse batched module
1064//
1065.version 7.0
1066.target sm_70
1067.address_size 64
1068
1069.visible .entry batched_spmv_{type_name}(
1070 .param .u64 concat_row_ptr,
1071 .param .u64 concat_col_idx,
1072 .param .u64 concat_values,
1073 .param .u64 batch_offsets_rp,
1074 .param .u64 batch_offsets_nnz,
1075 .param .u64 row_counts,
1076 .param .u64 x_ptrs,
1077 .param .u64 y_ptrs,
1078 .param {ptx_type} alpha,
1079 .param {ptx_type} beta,
1080 .param .u32 batch_size
1081)
1082{{
1083 .reg .u32 %r<16>;
1084 .reg .u64 %rd<32>;
1085 .reg {ptx_type} %f<8>;
1086 .reg .pred %p<4>;
1087
1088 // blockIdx.x = matrix index in batch
1089 mov.u32 %r0, %ctaid.x;
1090 // Early exit if blockIdx >= batch_size
1091 ld.param.u32 %r1, [batch_size];
1092 setp.ge.u32 %p0, %r0, %r1;
1093 @%p0 ret;
1094
1095 // threadIdx.x = local row within this matrix
1096 mov.u32 %r2, %tid.x;
1097
1098 // Load row_count for this matrix
1099 ld.param.u64 %rd0, [row_counts];
1100 cvt.u64.u32 %rd1, %r0;
1101 mad.wide.u32 %rd2, %r0, 4, %rd0;
1102 ld.global.u32 %r3, [%rd2];
1103
1104 // Early exit if tid >= row_count
1105 setp.ge.u32 %p1, %r2, %r3;
1106 @%p1 ret;
1107
1108 // Load row_ptr offset for this matrix
1109 ld.param.u64 %rd3, [batch_offsets_rp];
1110 mad.wide.u32 %rd4, %r0, 4, %rd3;
1111 ld.global.u32 %r4, [%rd4];
1112
1113 // row_start = concat_row_ptr[rp_offset + tid], row_end = next entry
1114 ld.param.u64 %rd5, [concat_row_ptr];
1115 add.u32 %r11, %r4, %r2;
1116 mul.wide.u32 %rd6, %r11, 4;
1117 add.u64 %rd5, %rd5, %rd6;
1118 ld.global.u32 %r5, [%rd5];
1119 add.u64 %rd6, %rd5, 4;
1120 ld.global.u32 %r6, [%rd6];
1121
1122 // Load nnz offset for this matrix
1123 ld.param.u64 %rd7, [batch_offsets_nnz];
1124 mad.wide.u32 %rd8, %r0, 4, %rd7;
1125 ld.global.u32 %r7, [%rd8];
1126
1127 // Load x and y pointers for this matrix
1128 ld.param.u64 %rd9, [x_ptrs];
1129 mad.wide.u32 %rd10, %r0, 8, %rd9;
1130 ld.global.u64 %rd10, [%rd10];
1131 ld.param.u64 %rd11, [y_ptrs];
1132 mad.wide.u32 %rd12, %r0, 8, %rd11;
1133 ld.global.u64 %rd11, [%rd12];
1134
1135 // Load concatenated col_idx / values bases and alpha / beta scalars
1136 ld.param.u64 %rd12, [concat_col_idx];
1137 ld.param.u64 %rd13, [concat_values];
1138 ld.param{ptx_type} %f6, [alpha];
1139 ld.param{ptx_type} %f7, [beta];
1140
1141 // acc = 0; iterate row_start .. row_end
1142 mov{ptx_type} %f0, {zero_literal};
1143 mov.u32 %r8, %r5;
1144
1145$ROW_LOOP:
1146 setp.lt.u32 %p2, %r8, %r6;
1147 @!%p2 bra $ROW_DONE;
1148
1149 // k = nnz_offset + absolute row nnz index
1150 add.u32 %r10, %r7, %r8;
1151
1152 // col = concat_col_idx[k]
1153 mul.wide.u32 %rd14, %r10, 4;
1154 add.u64 %rd15, %rd12, %rd14;
1155 ld.global.u32 %r9, [%rd15];
1156
1157 // val = concat_values[k]
1158 mul.wide.u32 %rd16, %r10, {elem_size};
1159 add.u64 %rd17, %rd13, %rd16;
1160 ld.global{ptx_type} %f1, [%rd17];
1161
1162 // x_val = x[col]
1163 mul.wide.u32 %rd18, %r9, {elem_size};
1164 add.u64 %rd19, %rd10, %rd18;
1165 ld.global{ptx_type} %f2, [%rd19];
1166
1167 // acc += val * x_val
1168 fma.rn{ptx_type} %f0, %f1, %f2, %f0;
1169
1170 add.u32 %r8, %r8, 1;
1171 bra $ROW_LOOP;
1172
1173$ROW_DONE:
1174 // y[tid] = alpha * acc + beta * y[tid]
1175 mul.wide.u32 %rd20, %r2, {elem_size};
1176 add.u64 %rd21, %rd11, %rd20;
1177 ld.global{ptx_type} %f3, [%rd21];
1178 mul.rn{ptx_type} %f4, %f6, %f0;
1179 mul.rn{ptx_type} %f5, %f7, %f3;
1180 add.rn{ptx_type} %f4, %f4, %f5;
1181 st.global{ptx_type} [%rd21], %f4;
1182
1183 ret;
1184}}
1185"#
1186 )
1187}
1188
1189pub fn batched_spmv_cpu(
1213 n_rows: usize,
1214 _n_cols: usize,
1215 row_ptr: &[u32],
1216 col_idx: &[u32],
1217 values: &[f32],
1218 x_batch: &[f32],
1219 batch_size: usize,
1220) -> Vec<f32> {
1221 let mut y = vec![0.0f32; n_rows * batch_size];
1222 for row in 0..n_rows {
1223 let start = row_ptr[row] as usize;
1224 let end = row_ptr[row + 1] as usize;
1225 for idx in start..end {
1226 let col = col_idx[idx] as usize;
1227 let val = values[idx];
1228 for b in 0..batch_size {
1229 y[row * batch_size + b] += val * x_batch[col * batch_size + b];
1230 }
1231 }
1232 }
1233 y
1234}
1235
1236pub fn mixed_precision_spmv_cpu(
1260 n_rows: usize,
1261 row_ptr: &[u32],
1262 col_idx: &[u32],
1263 values_fp16: &[f32],
1264 x: &[f32],
1265) -> Vec<f32> {
1266 let mut y = vec![0.0f32; n_rows];
1267 for row in 0..n_rows {
1268 let start = row_ptr[row] as usize;
1269 let end = row_ptr[row + 1] as usize;
1270 let mut acc = 0.0f64;
1272 for idx in start..end {
1273 let col = col_idx[idx] as usize;
1274 let val = values_fp16[idx] as f64;
1275 acc += val * (x[col] as f64);
1276 }
1277 y[row] = acc as f32;
1278 }
1279 y
1280}
1281
1282#[cfg(test)]
1287mod tests {
1288 use super::*;
1289
1290 #[test]
1293 fn scheduler_sequential_for_small_batch_large_matrices() {
1294 let s = BatchScheduler::new();
1295 assert_eq!(s.select_strategy(2, 50_000), Strategy::Sequential);
1296 assert_eq!(s.select_strategy(4, 10_000), Strategy::Sequential);
1297 }
1298
1299 #[test]
1300 fn scheduler_fused_for_large_batch_small_matrices() {
1301 let s = BatchScheduler::new();
1302 assert_eq!(s.select_strategy(100, 100), Strategy::Fused);
1303 assert_eq!(s.select_strategy(64, 255), Strategy::Fused);
1304 }
1305
1306 #[test]
1307 fn scheduler_concurrent_for_medium_cases() {
1308 let s = BatchScheduler::new();
1309 let strat = s.select_strategy(16, 1000);
1310 match strat {
1311 Strategy::Concurrent(n) => assert!((1..=8).contains(&n)),
1312 other => panic!("expected Concurrent, got {:?}", other),
1313 }
1314 }
1315
1316 #[test]
1317 fn scheduler_concurrent_caps_at_8_streams() {
1318 let strat = BatchScheduler::select_strategy_static(32, 1000);
1319 assert_eq!(strat, Strategy::Concurrent(8));
1320 }
1321
1322 #[test]
1323 fn scheduler_static_matches_instance() {
1324 let s = BatchScheduler::new();
1325 for (bs, nnz) in [(1, 100), (10, 500), (64, 100), (3, 20_000)] {
1326 assert_eq!(
1327 s.select_strategy(bs, nnz),
1328 BatchScheduler::select_strategy_static(bs, nnz)
1329 );
1330 }
1331 }
1332
1333 #[test]
1334 fn scheduler_default_trait() {
1335 let s = BatchScheduler::default();
1336 let _ = s.select_strategy(1, 1);
1338 }
1339
1340 #[test]
1343 fn plan_from_host_arrays_basic() {
1344 let rp = vec![vec![0, 1, 2], vec![0, 1, 2]];
1346 let ci = vec![vec![0, 1], vec![0, 1]];
1347 let vals: Vec<Vec<f32>> = vec![vec![1.0, 1.0], vec![2.0, 2.0]];
1348 let rows = vec![2, 2];
1349 let cols = vec![2, 2];
1350
1351 let plan = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &rows, &cols)
1352 .expect("plan creation should succeed");
1353
1354 assert_eq!(plan.batch_size, 2);
1355 assert_eq!(plan.row_counts, vec![2, 2]);
1356 assert_eq!(plan.nnz_counts, vec![2, 2]);
1357 assert_eq!(plan.total_nnz(), 4);
1358 assert_eq!(plan.total_rows(), 4);
1359 assert_eq!(plan.avg_nnz(), 2);
1360 assert_eq!(plan.batch_offsets_row_ptr, vec![0, 3]);
1361 assert_eq!(plan.batch_offsets_nnz, vec![0, 2]);
1362 assert_eq!(plan.concat_row_ptr, vec![0, 1, 2, 0, 1, 2]);
1363 assert_eq!(plan.concat_col_idx, vec![0, 1, 0, 1]);
1364 }
1365
1366 #[test]
1367 fn plan_from_host_arrays_empty_batch() {
1368 let result = BatchedSpMVPlan::<f32>::from_host_arrays(&[], &[], &[], &[], &[]);
1369 assert!(result.is_err());
1370 }
1371
1372 #[test]
1373 fn plan_from_host_arrays_length_mismatch() {
1374 let rp = vec![vec![0, 1]];
1375 let ci = vec![vec![0], vec![1]]; let vals: Vec<Vec<f64>> = vec![vec![1.0]];
1377 let result = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &[1], &[1]);
1378 assert!(result.is_err());
1379 }
1380
1381 #[test]
1384 fn spmv_host_identity_batch() {
1385 let rp = vec![vec![0, 1, 2, 3], vec![0, 1, 2, 3]];
1387 let ci = vec![vec![0, 1, 2], vec![0, 1, 2]];
1388 let vals = vec![vec![1.0_f64, 1.0, 1.0], vec![1.0, 1.0, 1.0]];
1389 let rows = vec![3, 3];
1390 let cols = vec![3, 3];
1391
1392 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols)
1393 .expect("batch creation should succeed");
1394 assert_eq!(batch.batch_size(), 2);
1395
1396 let xs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1397 let mut ys = vec![vec![0.0; 3], vec![0.0; 3]];
1398
1399 batch
1400 .execute(&xs, &mut ys, 1.0, 0.0)
1401 .expect("execute should succeed");
1402
1403 assert_eq!(ys[0], vec![1.0, 2.0, 3.0]);
1404 assert_eq!(ys[1], vec![4.0, 5.0, 6.0]);
1405 }
1406
1407 #[test]
1408 fn spmv_host_alpha_beta() {
1409 let rp = vec![vec![0, 1, 2]];
1411 let ci = vec![vec![0, 1]];
1412 let vals = vec![vec![2.0_f32, 3.0]];
1413 let rows = vec![2];
1414 let cols = vec![2];
1415
1416 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1417
1418 let xs = vec![vec![1.0, 1.0]];
1419 let mut ys = vec![vec![10.0, 20.0]];
1420
1421 batch
1425 .execute(&xs, &mut ys, 2.0, 0.5)
1426 .expect("execute should succeed");
1427
1428 assert!((ys[0][0] - 9.0).abs() < 1e-6);
1429 assert!((ys[0][1] - 16.0).abs() < 1e-6);
1430 }
1431
1432 #[test]
1433 fn spmv_host_dimension_mismatch() {
1434 let rp = vec![vec![0, 1]];
1435 let ci = vec![vec![0]];
1436 let vals = vec![vec![1.0_f32]];
1437 let rows = vec![1];
1438 let cols = vec![2];
1439
1440 let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1441
1442 let xs = vec![vec![1.0; 2], vec![1.0; 2]];
1444 let mut ys = vec![vec![0.0], vec![0.0]];
1445 assert!(batch.execute(&xs, &mut ys, 1.0, 0.0).is_err());
1446 }
1447
1448 #[test]
1449 fn spmv_host_empty_batch_error() {
1450 let result = BatchedSpMV::<f64>::from_host(vec![], vec![], vec![], vec![], vec![]);
1451 assert!(result.is_err());
1452 }
1453
1454 #[test]
1457 fn uniform_spmv_host_basic() {
1458 let row_ptr = vec![0, 1, 2];
1460 let col_idx = vec![0, 1];
1461 let batch_values = vec![
1462 vec![1.0_f64, 1.0], vec![2.0, 3.0], ];
1465
1466 let uniform = UniformBatchedSpMV::from_host_arrays(2, 2, row_ptr, col_idx, batch_values)
1467 .expect("creation should succeed");
1468 assert_eq!(uniform.batch_size(), 2);
1469
1470 let xs = vec![vec![1.0, 2.0], vec![1.0, 2.0]];
1471 let mut ys = vec![vec![0.0; 2], vec![0.0; 2]];
1472
1473 uniform
1474 .execute(&xs, &mut ys, 1.0, 0.0)
1475 .expect("execute should succeed");
1476
1477 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); }
1481
1482 #[test]
1483 fn uniform_spmv_validation_errors() {
1484 let result =
1486 UniformBatchedSpMV::<f32>::from_host_arrays(2, 2, vec![0, 1, 2], vec![0, 1], vec![]);
1487 assert!(result.is_err());
1488
1489 let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1491 2,
1492 2,
1493 vec![0, 1, 2],
1494 vec![0, 1],
1495 vec![vec![1.0]], );
1497 assert!(result.is_err());
1498
1499 let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1501 2,
1502 2,
1503 vec![0, 1], vec![0, 1],
1505 vec![vec![1.0, 2.0]],
1506 );
1507 assert!(result.is_err());
1508 }
1509
1510 #[test]
1513 fn tri_solve_host_basic() {
1514 let rp = vec![vec![0, 1, 3]];
1518 let ci = vec![vec![0, 0, 1]];
1519 let vals = vec![vec![2.0_f64, 1.0, 3.0]];
1520 let sizes = vec![2];
1521 let rhs = vec![vec![4.0, 7.0]];
1522
1523 let results = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs)
1524 .expect("solve should succeed");
1525
1526 assert_eq!(results.len(), 1);
1527 assert!((results[0][0] - 2.0).abs() < 1e-10);
1528 assert!((results[0][1] - 5.0 / 3.0).abs() < 1e-10);
1529 }
1530
1531 #[test]
1532 fn tri_solve_host_singular() {
1533 let rp = vec![vec![0, 1, 2]];
1535 let ci = vec![vec![0, 0]]; let vals = vec![vec![1.0_f64, 2.0]];
1537 let sizes = vec![2];
1538 let rhs = vec![vec![1.0, 1.0]];
1539
1540 let result = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs);
1541 assert!(result.is_err());
1542 }
1543
1544 #[allow(dead_code)]
1551 fn dense_matvec(a: &[f64], rows: usize, cols: usize, x: &[f64]) -> Vec<f64> {
1552 let mut y = vec![0.0_f64; rows];
1553 for r in 0..rows {
1554 for c in 0..cols {
1555 y[r] += a[r * cols + c] * x[c];
1556 }
1557 }
1558 y
1559 }
1560
1561 fn dense_matmul(a: &[f64], m: usize, k: usize, b: &[f64], n: usize) -> Vec<f64> {
1564 let mut c = vec![0.0_f64; m * n];
1565 for i in 0..m {
1566 for j in 0..n {
1567 let mut acc = 0.0_f64;
1568 for p in 0..k {
1569 acc += a[i * k + p] * b[p * n + j];
1570 }
1571 c[i * n + j] = acc;
1572 }
1573 }
1574 c
1575 }
1576
1577 #[test]
1590 fn test_spmv_numerical_accuracy_small() {
1591 let row_ptr = vec![vec![0i32, 2, 3, 5, 6, 8]];
1593 let col_idx = vec![vec![0i32, 2, 1, 2, 4, 3, 0, 4]];
1594 let values = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]];
1595 let rows = vec![5usize];
1596 let cols = vec![5usize];
1597
1598 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols)
1599 .expect("batch creation should succeed");
1600
1601 let x = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]];
1602 let mut y = vec![vec![0.0_f64; 5]];
1603
1604 batch
1605 .execute(&x, &mut y, 1.0, 0.0)
1606 .expect("execute should succeed");
1607
1608 let expected = [7.0_f64, 6.0, 37.0, 24.0, 47.0];
1609 for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1610 assert!(
1611 (got - exp).abs() < 1e-14,
1612 "y[{i}]: expected {exp}, got {got}"
1613 );
1614 }
1615 }
1616
1617 #[test]
1619 fn test_spmv_numerical_accuracy_value_spread() {
1620 let big = 1e6_f64;
1622 let small = 1e-6_f64;
1623 let row_ptr = vec![vec![0i32, 1, 2, 3, 4]];
1625 let col_idx = vec![vec![0i32, 1, 2, 3]];
1626 let values = vec![vec![small, big, small, big]];
1627 let rows = vec![4usize];
1628 let cols = vec![4usize];
1629
1630 let batch =
1631 BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols).expect("batch creation");
1632
1633 let x = vec![vec![1.0_f64, 1.0, 1.0, 1.0]];
1634 let mut y = vec![vec![0.0_f64; 4]];
1635 batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1636
1637 let expected = [small, big, small, big];
1638 for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1639 let rel_err = (got - exp).abs() / exp.abs().max(1e-300);
1640 assert!(
1641 rel_err < 1e-10,
1642 "y[{i}]: relative error {rel_err:.3e} exceeds threshold"
1643 );
1644 }
1645 }
1646
1647 #[test]
1649 fn test_spmv_alpha_beta_scaling() {
1650 let row_ptr = vec![vec![0i32, 1, 2]];
1654 let col_idx = vec![vec![0i32, 1]];
1655 let values = vec![vec![2.0_f64, 3.0]];
1656
1657 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![2], vec![2])
1658 .expect("batch creation");
1659
1660 let x = vec![vec![1.0_f64, 1.0]];
1661 let mut y = vec![vec![10.0_f64, 20.0]];
1662 batch.execute(&x, &mut y, 5.0, 0.25).expect("execute");
1663
1664 assert!((y[0][0] - 12.5).abs() < 1e-13, "y[0] = {}", y[0][0]);
1665 assert!((y[0][1] - 20.0).abs() < 1e-13, "y[1] = {}", y[0][1]);
1666 }
1667
1668 #[test]
1670 fn test_spmv_identity_matrix() {
1671 let n = 6usize;
1672 let row_ptr = vec![(0..=(n as i32)).collect::<Vec<i32>>()];
1673 let col_idx = vec![(0..n as i32).collect::<Vec<i32>>()];
1674 let values = vec![vec![1.0_f64; n]];
1675
1676 let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![n], vec![n])
1677 .expect("batch creation");
1678
1679 let x_data: Vec<f64> = (1..=(n as i64)).map(|v| v as f64).collect();
1680 let x = vec![x_data.clone()];
1681 let mut y = vec![vec![0.0_f64; n]];
1682 batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1683
1684 for (i, (&got, &exp)) in y[0].iter().zip(x_data.iter()).enumerate() {
1685 assert!(
1686 (got - exp).abs() < 1e-14,
1687 "identity: y[{i}] = {got}, expected {exp}"
1688 );
1689 }
1690 }
1691
1692 #[test]
1697 fn test_spmm_numerical_accuracy() {
1698 let a_dense = [
1704 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,
1705 ];
1706
1707 let row_ptr = vec![0i32, 2, 4, 6, 8];
1708 let col_idx = vec![0i32, 2, 1, 3, 0, 2, 1, 3];
1709 let values = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1710
1711 let b_dense = [
1713 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,
1714 ];
1715
1716 let c_ref = dense_matmul(&a_dense, 4, 4, &b_dense, 3);
1718
1719 let n_cols = 3usize;
1721 let mut c_got = vec![0.0_f64; 4 * n_cols];
1722
1723 for col in 0..n_cols {
1724 let x_col: Vec<f64> = (0..4).map(|r| b_dense[r * n_cols + col]).collect();
1726
1727 let batch = BatchedSpMV::from_host(
1728 vec![row_ptr.clone()],
1729 vec![col_idx.clone()],
1730 vec![values.clone()],
1731 vec![4],
1732 vec![4],
1733 )
1734 .expect("batch creation");
1735
1736 let mut y = vec![vec![0.0_f64; 4]];
1737 batch.execute(&[x_col], &mut y, 1.0, 0.0).expect("execute");
1738
1739 for row in 0..4 {
1740 c_got[row * n_cols + col] = y[0][row];
1741 }
1742 }
1743
1744 for i in 0..4 * n_cols {
1745 assert!(
1746 (c_got[i] - c_ref[i]).abs() < 1e-13,
1747 "C[{i}]: got {}, expected {}",
1748 c_got[i],
1749 c_ref[i]
1750 );
1751 }
1752 }
1753
1754 #[test]
1757 fn ptx_generation_f32() {
1758 let ptx = generate_batched_spmv_ptx::<f32>();
1759 assert!(ptx.contains("batched_spmv_f32"));
1760 assert!(ptx.contains(".f32"));
1761 assert!(ptx.contains(".version"));
1762 }
1763
1764 #[test]
1765 fn ptx_generation_f64() {
1766 let ptx = generate_batched_spmv_ptx::<f64>();
1767 assert!(ptx.contains("batched_spmv_f64"));
1768 assert!(ptx.contains(".f64"));
1769 }
1770
1771 #[test]
1772 fn batched_spmv_ptx_contains_loop() {
1773 let ptx = generate_batched_spmv_ptx::<f32>();
1774 assert!(ptx.contains("ROW_LOOP"));
1775 assert!(ptx.contains("fma.rn"));
1776 assert!(ptx.contains("ld.global"));
1777 }
1778
1779 #[test]
1782 fn spgemm_host_identity_times_matrix() {
1783 let i_rp = vec![vec![0, 1, 2]];
1785 let i_ci = vec![vec![0, 1]];
1786 let i_vals: Vec<Vec<f64>> = vec![vec![1.0, 1.0]];
1787
1788 let a_rp = vec![vec![0, 2, 3]]; let a_ci = vec![vec![0, 1, 1]];
1790 let a_vals: Vec<Vec<f64>> = vec![vec![2.0, 3.0, 4.0]];
1791
1792 let results = BatchedSpGEMM::execute_host(
1793 &i_rp,
1794 &i_ci,
1795 &i_vals,
1796 &[2],
1797 &[2],
1798 &a_rp,
1799 &a_ci,
1800 &a_vals,
1801 &[2],
1802 )
1803 .expect("spgemm should succeed");
1804
1805 assert_eq!(results.len(), 1);
1806 let (c_rp, c_ci, c_vals, m, n) = &results[0];
1807 assert_eq!(*m, 2);
1808 assert_eq!(*n, 2);
1809 let r0_start = c_rp[0] as usize;
1811 let r0_end = c_rp[1] as usize;
1812 assert_eq!(r0_end - r0_start, 2);
1813 assert_eq!(c_ci[r0_start], 0);
1814 assert_eq!(c_ci[r0_start + 1], 1);
1815 assert!((c_vals[r0_start] - 2.0).abs() < 1e-10);
1816 assert!((c_vals[r0_start + 1] - 3.0).abs() < 1e-10);
1817 }
1818
1819 #[test]
1822 fn batched_spmv_identity_2rhs() {
1823 let n_rows = 4usize;
1825 let n_cols = 4usize;
1826 let row_ptr = vec![0u32, 1, 2, 3, 4];
1827 let col_idx = vec![0u32, 1, 2, 3];
1828 let values = vec![1.0f32; 4];
1829
1830 let batch_size = 2usize;
1837 let x_batch = vec![1.0f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
1838
1839 let y = batched_spmv_cpu(
1840 n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1841 );
1842
1843 assert_eq!(y.len(), n_rows * batch_size);
1849 assert!((y[0] - 1.0).abs() < 1e-6, "row=0, b=0 should be 1.0");
1850 assert!((y[1] - 5.0).abs() < 1e-6, "row=0, b=1 should be 5.0");
1851 assert!((y[2] - 2.0).abs() < 1e-6, "row=1, b=0 should be 2.0");
1852 assert!((y[3] - 6.0).abs() < 1e-6, "row=1, b=1 should be 6.0");
1853 assert!((y[4] - 3.0).abs() < 1e-6, "row=2, b=0 should be 3.0");
1854 assert!((y[5] - 7.0).abs() < 1e-6, "row=2, b=1 should be 7.0");
1855 assert!((y[6] - 4.0).abs() < 1e-6, "row=3, b=0 should be 4.0");
1856 assert!((y[7] - 8.0).abs() < 1e-6, "row=3, b=1 should be 8.0");
1857 }
1858
1859 #[test]
1860 fn batched_spmv_correctness_3rhs() {
1861 let n_rows = 3usize;
1869 let n_cols = 3usize;
1870 let row_ptr = vec![0u32, 2, 4, 6];
1871 let col_idx = vec![0u32, 1, 1, 2, 0, 2];
1872 let values = vec![2.0f32, 1.0, 3.0, 1.0, 1.0, 4.0];
1873 let batch_size = 3usize;
1874
1875 let x_batch = vec![
1878 1.0f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, ];
1882
1883 let y = batched_spmv_cpu(
1884 n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1885 );
1886
1887 assert!((y[0] - 2.0).abs() < 1e-6, "A*e0 row0 = 2");
1891 assert!((y[1] - 1.0).abs() < 1e-6, "A*e1 row0 = 1");
1892 assert!((y[2] - 0.0).abs() < 1e-6, "A*e2 row0 = 0");
1893 assert!((y[3] - 0.0).abs() < 1e-6, "A*e0 row1 = 0");
1894 assert!((y[4] - 3.0).abs() < 1e-6, "A*e1 row1 = 3");
1895 assert!((y[5] - 1.0).abs() < 1e-6, "A*e2 row1 = 1");
1896 assert!((y[6] - 1.0).abs() < 1e-6, "A*e0 row2 = 1");
1897 assert!((y[7] - 0.0).abs() < 1e-6, "A*e1 row2 = 0");
1898 assert!((y[8] - 4.0).abs() < 1e-6, "A*e2 row2 = 4");
1899 }
1900
1901 #[test]
1904 fn mixed_precision_spmv_correctness() {
1905 let n_rows = 4usize;
1907 let row_ptr = vec![0u32, 1, 2, 3, 4];
1908 let col_idx = vec![0u32, 1, 2, 3];
1909 let values_fp16 = vec![1.0f32; 4];
1910 let x = vec![1.0f32, 2.0, 3.0, 4.0];
1911
1912 let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1913
1914 assert_eq!(y.len(), n_rows);
1915 for (i, &yi) in y.iter().enumerate() {
1916 assert!(
1917 (yi - (i + 1) as f32).abs() < 1e-4,
1918 "y[{}] should be {} but got {}",
1919 i,
1920 i + 1,
1921 yi
1922 );
1923 }
1924 }
1925
1926 #[test]
1927 fn mixed_precision_accumulation_fp32() {
1928 let n_rows = 1000usize;
1931 let row_ptr: Vec<u32> = (0..=1000).map(|i| i as u32).collect();
1932 let col_idx: Vec<u32> = (0..1000).map(|i| i as u32).collect();
1933 let values_fp16 = vec![1.0f32; 1000];
1934 let x = vec![1.0f32; 1000];
1935
1936 let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1937
1938 assert_eq!(y.len(), n_rows);
1939 for (i, &yi) in y.iter().enumerate() {
1941 assert!(yi.is_finite(), "y[{}] = {} is not finite", i, yi);
1942 assert!(
1943 (yi - 1.0).abs() < 1e-4,
1944 "y[{}] should be 1.0, got {}",
1945 i,
1946 yi
1947 );
1948 }
1949 }
1950}