1use std::sync::Arc;
13
14use oxicuda_blas::GpuFloat;
15use oxicuda_blas::types::{MatrixDesc, MatrixDescMut};
16use oxicuda_driver::Module;
17use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
18use oxicuda_ptx::prelude::*;
19
20use crate::error::{SparseError, SparseResult};
21use crate::format::CsrMatrix;
22use crate::handle::SparseHandle;
23use crate::ptx_helpers::{
24 add_float, fma_float, load_float_imm, load_global_float, mul_float, reinterpret_bits_to_float,
25 store_global_float,
26};
27
28const SPMM_BLOCK_SIZE: u32 = 256;
30
31const SPMM_TILE_COLS: u32 = 4;
33
34pub fn spmm<T: GpuFloat>(
54 handle: &SparseHandle,
55 alpha: T,
56 a: &CsrMatrix<T>,
57 b: &MatrixDesc<T>,
58 beta: T,
59 c: &mut MatrixDescMut<T>,
60) -> SparseResult<()> {
61 if a.cols() != b.rows {
63 return Err(SparseError::DimensionMismatch(format!(
64 "A.cols ({}) != B.rows ({})",
65 a.cols(),
66 b.rows
67 )));
68 }
69 if a.rows() != c.rows {
70 return Err(SparseError::DimensionMismatch(format!(
71 "A.rows ({}) != C.rows ({})",
72 a.rows(),
73 c.rows
74 )));
75 }
76 if b.cols != c.cols {
77 return Err(SparseError::DimensionMismatch(format!(
78 "B.cols ({}) != C.cols ({})",
79 b.cols, c.cols
80 )));
81 }
82
83 if a.rows() == 0 || a.cols() == 0 || b.cols == 0 {
84 return Ok(());
85 }
86
87 let ptx = emit_spmm_kernel::<T>(handle.sm_version())?;
88 let module = Arc::new(Module::from_ptx(&ptx)?);
89 let kernel = Kernel::from_module(module, "spmm")?;
90
91 let block_size = SPMM_BLOCK_SIZE;
93 let total_work = a.rows() * b.cols.div_ceil(SPMM_TILE_COLS);
94 let grid_size = grid_size_for(total_work, block_size);
95 let params = LaunchParams::new(grid_size, block_size);
96
97 kernel.launch(
98 ¶ms,
99 handle.stream(),
100 &(
101 a.row_ptr().as_device_ptr(),
102 a.col_idx().as_device_ptr(),
103 a.values().as_device_ptr(),
104 b.ptr,
105 c.ptr,
106 alpha.to_bits_u64(),
107 beta.to_bits_u64(),
108 a.rows(),
109 b.cols,
110 b.ld,
111 c.ld,
112 ),
113 )?;
114
115 Ok(())
116}
117
118fn emit_spmm_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
124 let elem_bytes = T::size_u32();
125 let is_f64 = T::SIZE == 8;
126 let tile_cols = SPMM_TILE_COLS;
127
128 KernelBuilder::new("spmm")
129 .target(sm)
130 .param("row_ptr", PtxType::U64)
131 .param("col_idx", PtxType::U64)
132 .param("values", PtxType::U64)
133 .param("b_ptr", PtxType::U64)
134 .param("c_ptr", PtxType::U64)
135 .param("alpha_bits", PtxType::U64)
136 .param("beta_bits", PtxType::U64)
137 .param("m", PtxType::U32)
138 .param("n", PtxType::U32)
139 .param("ldb", PtxType::U32)
140 .param("ldc", PtxType::U32)
141 .body(move |b| {
142 let gid = b.global_thread_id_x();
143
144 let n_param = b.load_param_u32("n");
146 let m_param = b.load_param_u32("m");
147
148 let tiles_per_row = b.alloc_reg(PtxType::U32);
150 let n_plus = b.alloc_reg(PtxType::U32);
151 b.raw_ptx(&format!("add.u32 {n_plus}, {n_param}, {};", tile_cols - 1));
152 b.raw_ptx(&format!(
153 "div.u32 {tiles_per_row}, {n_plus}, {};",
154 tile_cols
155 ));
156
157 let row = b.alloc_reg(PtxType::U32);
159 let tile_id = b.alloc_reg(PtxType::U32);
160 b.raw_ptx(&format!("div.u32 {row}, {gid}, {tiles_per_row};"));
161 b.raw_ptx(&format!("rem.u32 {tile_id}, {gid}, {tiles_per_row};"));
162
163 let row_inner = row.clone();
164 let tile_id_inner = tile_id.clone();
165 b.if_lt_u32(row, m_param, move |b| {
166 let row = row_inner;
167 let tile_id = tile_id_inner;
168
169 let row_ptr_base = b.load_param_u64("row_ptr");
170 let col_idx_base = b.load_param_u64("col_idx");
171 let values_base = b.load_param_u64("values");
172 let b_ptr = b.load_param_u64("b_ptr");
173 let c_ptr = b.load_param_u64("c_ptr");
174 let alpha_bits = b.load_param_u64("alpha_bits");
175 let beta_bits = b.load_param_u64("beta_bits");
176 let n_param = b.load_param_u32("n");
177 let ldb = b.load_param_u32("ldb");
178 let ldc = b.load_param_u32("ldc");
179
180 let alpha = reinterpret_bits_to_float::<T>(b, alpha_bits);
181 let beta = reinterpret_bits_to_float::<T>(b, beta_bits);
182
183 let col_start = b.alloc_reg(PtxType::U32);
185 b.raw_ptx(&format!(
186 "mul.lo.u32 {col_start}, {tile_id}, {};",
187 tile_cols
188 ));
189
190 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
192 let rs_i32 = b.load_global_i32(rp_addr);
193 let rs = b.alloc_reg(PtxType::U32);
194 b.raw_ptx(&format!("mov.b32 {rs}, {rs_i32};"));
195
196 let row_p1 = b.alloc_reg(PtxType::U32);
197 b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
198 let rp_addr_next = b.byte_offset_addr(row_ptr_base, row_p1, 4);
199 let re_i32 = b.load_global_i32(rp_addr_next);
200 let re = b.alloc_reg(PtxType::U32);
201 b.raw_ptx(&format!("mov.b32 {re}, {re_i32};"));
202
203 let mov_suffix = if is_f64 { "f64" } else { "f32" };
210 for tc in 0..tile_cols {
211 let col = b.alloc_reg(PtxType::U32);
212 b.raw_ptx(&format!("add.u32 {col}, {col_start}, {tc};"));
213
214 let col_oob = b.alloc_reg(PtxType::Pred);
218 b.raw_ptx(&format!("setp.hs.u32 {col_oob}, {col}, {n_param};"));
219 let skip_col = b.fresh_label("spmm_skip_col");
220 b.branch_if(col_oob, &skip_col);
221
222 let acc = load_float_imm::<T>(b, 0.0);
224 let k_reg = b.alloc_reg(PtxType::U32);
225 b.raw_ptx(&format!("mov.u32 {k_reg}, {rs};"));
226
227 let loop_label = b.fresh_label("spmm_loop");
228 let done_label = b.fresh_label("spmm_done");
229
230 b.label(&loop_label);
231 let pred = b.alloc_reg(PtxType::Pred);
233 b.raw_ptx(&format!("setp.hs.u32 {pred}, {k_reg}, {re};"));
234 b.branch_if(pred, &done_label);
235
236 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k_reg.clone(), 4);
238 let a_col_i32 = b.load_global_i32(ci_addr);
239 let a_col = b.alloc_reg(PtxType::U32);
240 b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
241
242 let v_addr = b.byte_offset_addr(values_base.clone(), k_reg.clone(), elem_bytes);
243 let a_val = load_global_float::<T>(b, v_addr);
244
245 let b_row_off = b.alloc_reg(PtxType::U32);
248 b.raw_ptx(&format!("mul.lo.u32 {b_row_off}, {a_col}, {ldb};"));
249 let b_idx = b.alloc_reg(PtxType::U32);
250 b.raw_ptx(&format!("add.u32 {b_idx}, {b_row_off}, {col};"));
251 let b_addr = b.byte_offset_addr(b_ptr.clone(), b_idx, elem_bytes);
252 let b_val = load_global_float::<T>(b, b_addr);
253
254 let new_acc = fma_float::<T>(b, a_val, b_val, acc.clone());
256 b.raw_ptx(&format!("mov.{mov_suffix} {acc}, {new_acc};"));
257
258 b.raw_ptx(&format!("add.u32 {k_reg}, {k_reg}, 1;"));
259 b.branch(&loop_label);
260 b.label(&done_label);
261
262 let c_row_off = b.alloc_reg(PtxType::U32);
264 b.raw_ptx(&format!("mul.lo.u32 {c_row_off}, {row}, {ldc};"));
265 let c_idx = b.alloc_reg(PtxType::U32);
266 b.raw_ptx(&format!("add.u32 {c_idx}, {c_row_off}, {col};"));
267 let c_addr = b.byte_offset_addr(c_ptr.clone(), c_idx, elem_bytes);
268 let c_old = load_global_float::<T>(b, c_addr.clone());
269
270 let alpha_acc = mul_float::<T>(b, alpha.clone(), acc);
271 let beta_c = mul_float::<T>(b, beta.clone(), c_old);
272 let result = add_float::<T>(b, alpha_acc, beta_c);
273 store_global_float::<T>(b, c_addr, result);
274
275 b.label(&skip_col);
276 }
277 });
278
279 b.ret();
280 })
281 .build()
282 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::ptx_helpers::test_support::assert_assembles_and_clean;
289
290 #[test]
293 fn spmm_f32_f64_assemble_sm86() {
294 let f32_ptx = emit_spmm_kernel::<f32>(SmVersion::Sm86).expect("f32 SpMM PTX");
295 assert_assembles_and_clean("spmm_f32", &f32_ptx);
296
297 let f64_ptx = emit_spmm_kernel::<f64>(SmVersion::Sm86).expect("f64 SpMM PTX");
298 assert_assembles_and_clean("spmm_f64", &f64_ptx);
299 assert!(
300 !f64_ptx.contains("0F00000000"),
301 "f64 SpMM kernel must not materialize an f32 0.0 immediate:\n{f64_ptx}"
302 );
303 }
304
305 fn cpu_csr_spmm(
317 row_ptr: &[usize],
318 col_idx: &[usize],
319 values: &[f32],
320 b: &[f32],
321 n: usize,
322 ldb: usize,
323 ) -> Vec<f32> {
324 let m = row_ptr.len() - 1;
325 let mut c = vec![0.0_f32; m * n];
326 for row in 0..m {
327 for nnz_idx in row_ptr[row]..row_ptr[row + 1] {
328 let a_col = col_idx[nnz_idx];
329 let a_val = values[nnz_idx];
330 for col in 0..n {
332 c[row * n + col] += a_val * b[a_col * ldb + col];
333 }
334 }
335 }
336 c
337 }
338
339 #[test]
344 fn spmm_ptx_generates_f32() {
345 let ptx = emit_spmm_kernel::<f32>(SmVersion::Sm80);
346 assert!(ptx.is_ok());
347 let ptx = ptx.expect("test: PTX gen should succeed");
348 assert!(ptx.contains(".entry spmm"));
349 }
350
351 #[test]
352 fn spmm_ptx_generates_f64() {
353 let ptx = emit_spmm_kernel::<f64>(SmVersion::Sm80);
354 assert!(ptx.is_ok());
355 }
356
357 #[test]
358 fn spmm_ptx_contains_arithmetic_instructions() {
359 let ptx = emit_spmm_kernel::<f32>(SmVersion::Sm80);
360 assert!(ptx.is_ok());
361 let ptx = ptx.expect("test: PTX gen should succeed");
362 assert!(
364 ptx.contains("fma") || ptx.contains("mul"),
365 "SpMM PTX should contain arithmetic instructions"
366 );
367 }
368
369 #[test]
383 fn spmm_identity_times_dense_equals_dense() {
384 let row_ptr = vec![0usize, 1, 2, 3, 4];
385 let col_idx = vec![0usize, 1, 2, 3];
386 let values = vec![1.0_f32; 4];
387
388 let b = vec![
390 1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
391 ];
392 let n = 3usize;
393 let ldb = 3usize;
394
395 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
396
397 assert_eq!(c.len(), 4 * 3);
399 for (i, (&ci, &bi)) in c.iter().zip(b.iter()).enumerate() {
400 assert!((ci - bi).abs() < 1e-6, "C[{}] = {ci} expected {bi}", i);
401 }
402 }
403
404 #[test]
419 fn spmm_small_sparse_times_dense_known_values() {
420 let row_ptr = vec![0usize, 2, 4];
421 let col_idx = vec![0usize, 2, 1, 2];
422 let values = vec![1.0_f32, 3.0, 2.0, 4.0];
423
424 let b = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let n = 2usize;
426 let ldb = 2usize;
427
428 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
429
430 assert_eq!(c.len(), 4);
431 assert!((c[0] - 16.0).abs() < 1e-5, "C[0,0] = {} expected 16", c[0]);
432 assert!((c[1] - 20.0).abs() < 1e-5, "C[0,1] = {} expected 20", c[1]);
433 assert!((c[2] - 26.0).abs() < 1e-5, "C[1,0] = {} expected 26", c[2]);
434 assert!((c[3] - 32.0).abs() < 1e-5, "C[1,1] = {} expected 32", c[3]);
435 }
436
437 #[test]
443 fn spmm_diagonal_times_dense_row_scaling() {
444 let row_ptr = vec![0usize, 1, 2, 3, 4];
445 let col_idx = vec![0usize, 1, 2, 3];
446 let values = vec![2.0_f32, 3.0, 4.0, 5.0];
447
448 let b = vec![
450 1.0_f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0,
451 ];
452 let n = 3usize;
453 let ldb = 3usize;
454
455 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
456
457 assert!((c[0] - 2.0).abs() < 1e-6, "C[0,0] = {}", c[0]);
459 assert!(c[1].abs() < 1e-6, "C[0,1] = {}", c[1]);
460 assert!(c[2].abs() < 1e-6, "C[0,2] = {}", c[2]);
461
462 assert!(c[3].abs() < 1e-6, "C[1,0] = {}", c[3]);
464 assert!((c[4] - 3.0).abs() < 1e-6, "C[1,1] = {}", c[4]);
465 assert!(c[5].abs() < 1e-6, "C[1,2] = {}", c[5]);
466
467 assert!(c[6].abs() < 1e-6, "C[2,0] = {}", c[6]);
469 assert!(c[7].abs() < 1e-6, "C[2,1] = {}", c[7]);
470 assert!((c[8] - 4.0).abs() < 1e-6, "C[2,2] = {}", c[8]);
471
472 assert!((c[9] - 5.0).abs() < 1e-6, "C[3,0] = {}", c[9]);
474 assert!((c[10] - 5.0).abs() < 1e-6, "C[3,1] = {}", c[10]);
475 assert!((c[11] - 5.0).abs() < 1e-6, "C[3,2] = {}", c[11]);
476 }
477
478 #[test]
480 fn spmm_zero_sparse_matrix_produces_zero_output() {
481 let row_ptr = vec![0usize, 0, 0, 0];
482 let col_idx: Vec<usize> = vec![];
483 let values: Vec<f32> = vec![];
484
485 let b = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let n = 2usize;
487 let ldb = 2usize;
488
489 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
490
491 assert_eq!(c.len(), 6);
492 for (i, &ci) in c.iter().enumerate() {
493 assert!(
494 ci.abs() < 1e-6,
495 "C[{i}] = {ci}, expected 0.0 for zero sparse matrix"
496 );
497 }
498 }
499}
500
501#[cfg(all(test, feature = "gpu-tests"))]
506mod gpu_device_tests {
507 use super::*;
508 use crate::gpu_test_support::{assert_close, gpu_handle};
509 use crate::host_csr::{f64_to_gpu, gpu_to_f64};
510 use oxicuda_blas::types::Layout;
511 use oxicuda_memory::DeviceBuffer;
512
513 #[allow(clippy::too_many_arguments)]
515 fn cpu_spmm(
516 m: usize,
517 n: usize,
518 row_ptr: &[i32],
519 col_idx: &[i32],
520 values: &[f64],
521 b: &[f64],
522 c0: &[f64],
523 alpha: f64,
524 beta: f64,
525 ) -> Vec<f64> {
526 let mut c = vec![0.0_f64; m * n];
527 for row in 0..m {
528 let mut acc = vec![0.0_f64; n];
529 for k in row_ptr[row] as usize..row_ptr[row + 1] as usize {
530 let a_col = col_idx[k] as usize;
531 let a_val = values[k];
532 for (col, slot) in acc.iter_mut().enumerate() {
533 *slot += a_val * b[a_col * n + col];
534 }
535 }
536 for col in 0..n {
537 c[row * n + col] = alpha * acc[col] + beta * c0[row * n + col];
538 }
539 }
540 c
541 }
542
543 #[allow(clippy::too_many_arguments)]
545 fn run_spmm<T: GpuFloat>(
546 m: u32,
547 k: u32,
548 n: u32,
549 row_ptr: &[i32],
550 col_idx: &[i32],
551 values: &[f64],
552 b_dense: &[f64],
553 c0: &[f64],
554 alpha: f64,
555 beta: f64,
556 tol: f64,
557 tag: &str,
558 ) {
559 let Some(handle) = gpu_handle() else {
560 return;
561 };
562 let dev_values: Vec<T> = values.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
563 let a = CsrMatrix::<T>::from_host(m, k, row_ptr, col_idx, &dev_values)
564 .expect("test: build CSR");
565
566 let dev_b: Vec<T> = b_dense.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
567 let dev_c: Vec<T> = c0.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
568 let b_buf = DeviceBuffer::from_host(&dev_b).expect("test: upload B");
569 let c_buf = DeviceBuffer::from_host(&dev_c).expect("test: upload C");
570
571 let b_desc = MatrixDesc::<T>::from_raw(b_buf.as_device_ptr(), k, n, n, Layout::RowMajor);
572 let mut c_desc =
573 MatrixDescMut::<T>::from_raw(c_buf.as_device_ptr(), m, n, n, Layout::RowMajor);
574
575 spmm::<T>(
576 &handle,
577 f64_to_gpu::<T>(alpha),
578 &a,
579 &b_desc,
580 f64_to_gpu::<T>(beta),
581 &mut c_desc,
582 )
583 .expect("test: spmm launch");
584 handle.stream().synchronize().expect("test: sync");
585
586 let mut out = vec![T::gpu_zero(); (m * n) as usize];
587 c_buf.copy_to_host(&mut out).expect("test: download C");
588 let got: Vec<f64> = out.iter().map(|&v| gpu_to_f64(v)).collect();
589 let want = cpu_spmm(
590 m as usize, n as usize, row_ptr, col_idx, values, b_dense, c0, alpha, beta,
591 );
592 assert_close(&got, &want, tol, tag);
593 }
594
595 fn matrix_3x4() -> (u32, u32, Vec<i32>, Vec<i32>, Vec<f64>) {
600 let row_ptr = vec![0, 2, 4, 6];
601 let col_idx = vec![0, 2, 1, 3, 0, 3];
602 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
603 (3, 4, row_ptr, col_idx, values)
604 }
605
606 fn dense(k: usize, n: usize, base: f64) -> Vec<f64> {
608 (0..k * n)
609 .map(|idx| base + 0.5 * (idx as f64) - 0.1 * ((idx % 3) as f64))
610 .collect()
611 }
612
613 #[test]
614 fn spmm_f64_n3_alpha_beta() {
615 let (m, k, rp, ci, v) = matrix_3x4();
618 let n = 3usize;
619 let b = dense(k as usize, n, 1.0);
620 let c0 = dense(m as usize, n, 7.0);
621 run_spmm::<f64>(
622 m,
623 k,
624 n as u32,
625 &rp,
626 &ci,
627 &v,
628 &b,
629 &c0,
630 1.75,
631 -0.5,
632 1e-10,
633 "spmm_f64_n3",
634 );
635 }
636
637 #[test]
638 fn spmm_f64_n5_alpha_beta() {
639 let (m, k, rp, ci, v) = matrix_3x4();
641 let n = 5usize;
642 let b = dense(k as usize, n, -2.0);
643 let c0 = dense(m as usize, n, 0.25);
644 run_spmm::<f64>(
645 m,
646 k,
647 n as u32,
648 &rp,
649 &ci,
650 &v,
651 &b,
652 &c0,
653 2.0,
654 0.5,
655 1e-10,
656 "spmm_f64_n5",
657 );
658 }
659
660 #[test]
661 fn spmm_f32_n3_alpha_beta() {
662 let (m, k, rp, ci, v) = matrix_3x4();
663 let n = 3usize;
664 let b = dense(k as usize, n, 0.5);
665 let c0 = dense(m as usize, n, 3.0);
666 run_spmm::<f32>(
667 m,
668 k,
669 n as u32,
670 &rp,
671 &ci,
672 &v,
673 &b,
674 &c0,
675 1.25,
676 -0.75,
677 1e-4,
678 "spmm_f32_n3",
679 );
680 }
681
682 #[test]
683 fn spmm_f64_single_column_beta_zero() {
684 let (m, k, rp, ci, v) = matrix_3x4();
686 let n = 1usize;
687 let b = dense(k as usize, n, 1.0);
688 let c0 = vec![1e8; (m as usize) * n];
689 run_spmm::<f64>(
690 m,
691 k,
692 n as u32,
693 &rp,
694 &ci,
695 &v,
696 &b,
697 &c0,
698 1.0,
699 0.0,
700 1e-10,
701 "spmm_f64_n1",
702 );
703 }
704}