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 col = col_start;
206 let col_bound = b.alloc_reg(PtxType::Pred);
207 b.raw_ptx(&format!("setp.lo.u32 {col_bound}, {col}, {n_param};"));
208
209 let do_col = b.fresh_label("spmm_do_col");
210 let skip_col = b.fresh_label("spmm_skip_col");
211 b.raw_ptx(&format!("@!{col_bound} bra {skip_col};"));
212 b.label(&do_col);
213
214 let acc = load_float_imm::<T>(b, 0.0);
216 let k_reg = b.alloc_reg(PtxType::U32);
217 b.raw_ptx(&format!("mov.u32 {k_reg}, {rs};"));
218
219 let loop_label = b.fresh_label("spmm_loop");
220 let done_label = b.fresh_label("spmm_done");
221
222 b.label(&loop_label);
223 let pred = b.alloc_reg(PtxType::Pred);
224 b.raw_ptx(&format!("setp.lo.u32 {pred}, {k_reg}, {re};"));
225 b.raw_ptx(&format!("@!{pred} bra {done_label};"));
226
227 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k_reg.clone(), 4);
229 let a_col_i32 = b.load_global_i32(ci_addr);
230 let a_col = b.alloc_reg(PtxType::U32);
231 b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
232
233 let v_addr = b.byte_offset_addr(values_base.clone(), k_reg.clone(), elem_bytes);
234 let a_val = load_global_float::<T>(b, v_addr);
235
236 let b_row_off = b.alloc_reg(PtxType::U32);
239 b.raw_ptx(&format!("mul.lo.u32 {b_row_off}, {a_col}, {ldb};"));
240 let b_idx = b.alloc_reg(PtxType::U32);
241 b.raw_ptx(&format!("add.u32 {b_idx}, {b_row_off}, {col};"));
242 let b_addr = b.byte_offset_addr(b_ptr.clone(), b_idx, elem_bytes);
243 let b_val = load_global_float::<T>(b, b_addr);
244
245 let new_acc = fma_float::<T>(b, a_val, b_val, acc.clone());
247 let mov_suffix = if is_f64 { "f64" } else { "f32" };
248 b.raw_ptx(&format!("mov.{mov_suffix} {acc}, {new_acc};"));
249
250 b.raw_ptx(&format!("add.u32 {k_reg}, {k_reg}, 1;"));
251 b.branch(&loop_label);
252 b.label(&done_label);
253
254 let c_row_off = b.alloc_reg(PtxType::U32);
256 b.raw_ptx(&format!("mul.lo.u32 {c_row_off}, {row}, {ldc};"));
257 let c_idx = b.alloc_reg(PtxType::U32);
258 b.raw_ptx(&format!("add.u32 {c_idx}, {c_row_off}, {col};"));
259 let c_addr = b.byte_offset_addr(c_ptr, c_idx, elem_bytes);
260 let c_old = load_global_float::<T>(b, c_addr.clone());
261
262 let alpha_acc = mul_float::<T>(b, alpha, acc);
263 let beta_c = mul_float::<T>(b, beta, c_old);
264 let result = add_float::<T>(b, alpha_acc, beta_c);
265 store_global_float::<T>(b, c_addr, result);
266
267 b.label(&skip_col);
268 });
269
270 b.ret();
271 })
272 .build()
273 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn cpu_csr_spmm(
292 row_ptr: &[usize],
293 col_idx: &[usize],
294 values: &[f32],
295 b: &[f32],
296 n: usize,
297 ldb: usize,
298 ) -> Vec<f32> {
299 let m = row_ptr.len() - 1;
300 let mut c = vec![0.0_f32; m * n];
301 for row in 0..m {
302 for nnz_idx in row_ptr[row]..row_ptr[row + 1] {
303 let a_col = col_idx[nnz_idx];
304 let a_val = values[nnz_idx];
305 for col in 0..n {
307 c[row * n + col] += a_val * b[a_col * ldb + col];
308 }
309 }
310 }
311 c
312 }
313
314 #[test]
319 fn spmm_ptx_generates_f32() {
320 let ptx = emit_spmm_kernel::<f32>(SmVersion::Sm80);
321 assert!(ptx.is_ok());
322 let ptx = ptx.expect("test: PTX gen should succeed");
323 assert!(ptx.contains(".entry spmm"));
324 }
325
326 #[test]
327 fn spmm_ptx_generates_f64() {
328 let ptx = emit_spmm_kernel::<f64>(SmVersion::Sm80);
329 assert!(ptx.is_ok());
330 }
331
332 #[test]
333 fn spmm_ptx_contains_arithmetic_instructions() {
334 let ptx = emit_spmm_kernel::<f32>(SmVersion::Sm80);
335 assert!(ptx.is_ok());
336 let ptx = ptx.expect("test: PTX gen should succeed");
337 assert!(
339 ptx.contains("fma") || ptx.contains("mul"),
340 "SpMM PTX should contain arithmetic instructions"
341 );
342 }
343
344 #[test]
358 fn spmm_identity_times_dense_equals_dense() {
359 let row_ptr = vec![0usize, 1, 2, 3, 4];
360 let col_idx = vec![0usize, 1, 2, 3];
361 let values = vec![1.0_f32; 4];
362
363 let b = vec![
365 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,
366 ];
367 let n = 3usize;
368 let ldb = 3usize;
369
370 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
371
372 assert_eq!(c.len(), 4 * 3);
374 for (i, (&ci, &bi)) in c.iter().zip(b.iter()).enumerate() {
375 assert!((ci - bi).abs() < 1e-6, "C[{}] = {ci} expected {bi}", i);
376 }
377 }
378
379 #[test]
394 fn spmm_small_sparse_times_dense_known_values() {
395 let row_ptr = vec![0usize, 2, 4];
396 let col_idx = vec![0usize, 2, 1, 2];
397 let values = vec![1.0_f32, 3.0, 2.0, 4.0];
398
399 let b = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let n = 2usize;
401 let ldb = 2usize;
402
403 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
404
405 assert_eq!(c.len(), 4);
406 assert!((c[0] - 16.0).abs() < 1e-5, "C[0,0] = {} expected 16", c[0]);
407 assert!((c[1] - 20.0).abs() < 1e-5, "C[0,1] = {} expected 20", c[1]);
408 assert!((c[2] - 26.0).abs() < 1e-5, "C[1,0] = {} expected 26", c[2]);
409 assert!((c[3] - 32.0).abs() < 1e-5, "C[1,1] = {} expected 32", c[3]);
410 }
411
412 #[test]
418 fn spmm_diagonal_times_dense_row_scaling() {
419 let row_ptr = vec![0usize, 1, 2, 3, 4];
420 let col_idx = vec![0usize, 1, 2, 3];
421 let values = vec![2.0_f32, 3.0, 4.0, 5.0];
422
423 let b = vec![
425 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,
426 ];
427 let n = 3usize;
428 let ldb = 3usize;
429
430 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
431
432 assert!((c[0] - 2.0).abs() < 1e-6, "C[0,0] = {}", c[0]);
434 assert!(c[1].abs() < 1e-6, "C[0,1] = {}", c[1]);
435 assert!(c[2].abs() < 1e-6, "C[0,2] = {}", c[2]);
436
437 assert!(c[3].abs() < 1e-6, "C[1,0] = {}", c[3]);
439 assert!((c[4] - 3.0).abs() < 1e-6, "C[1,1] = {}", c[4]);
440 assert!(c[5].abs() < 1e-6, "C[1,2] = {}", c[5]);
441
442 assert!(c[6].abs() < 1e-6, "C[2,0] = {}", c[6]);
444 assert!(c[7].abs() < 1e-6, "C[2,1] = {}", c[7]);
445 assert!((c[8] - 4.0).abs() < 1e-6, "C[2,2] = {}", c[8]);
446
447 assert!((c[9] - 5.0).abs() < 1e-6, "C[3,0] = {}", c[9]);
449 assert!((c[10] - 5.0).abs() < 1e-6, "C[3,1] = {}", c[10]);
450 assert!((c[11] - 5.0).abs() < 1e-6, "C[3,2] = {}", c[11]);
451 }
452
453 #[test]
455 fn spmm_zero_sparse_matrix_produces_zero_output() {
456 let row_ptr = vec![0usize, 0, 0, 0];
457 let col_idx: Vec<usize> = vec![];
458 let values: Vec<f32> = vec![];
459
460 let b = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let n = 2usize;
462 let ldb = 2usize;
463
464 let c = cpu_csr_spmm(&row_ptr, &col_idx, &values, &b, n, ldb);
465
466 assert_eq!(c.len(), 6);
467 for (i, &ci) in c.iter().enumerate() {
468 assert!(
469 ci.abs() < 1e-6,
470 "C[{i}] = {ci}, expected 0.0 for zero sparse matrix"
471 );
472 }
473 }
474}