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;