Skip to main content

oxicuda_sparse/ops/
spmm.rs

1//! Sparse matrix-dense matrix multiplication (SpMM).
2//!
3//! Computes `C = alpha * A * B + beta * C` where `A` is a sparse CSR matrix
4//! and `B`, `C` are dense matrices.
5//!
6//! ## Strategy
7//!
8//! Each warp processes one row of `A` and tiles across columns of `B`.
9//! Within each row, the warp collaboratively loads non-zero entries and
10//! accumulates partial sums into the output columns.
11
12use 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
28/// Default block size for SpMM kernel.
29const SPMM_BLOCK_SIZE: u32 = 256;
30
31/// Number of columns of B processed per thread in one tile.
32const SPMM_TILE_COLS: u32 = 4;
33
34/// Sparse matrix-dense matrix multiplication: `C = alpha * A * B + beta * C`.
35///
36/// `A` is a sparse CSR matrix of shape `(m, k)`. `B` is a dense matrix of
37/// shape `(k, n)`. `C` is a dense output matrix of shape `(m, n)`.
38///
39/// # Arguments
40///
41/// * `handle` -- Sparse handle.
42/// * `alpha` -- Scalar multiplier for `A * B`.
43/// * `a` -- Sparse CSR matrix `A`.
44/// * `b` -- Dense matrix descriptor for `B`.
45/// * `beta` -- Scalar multiplier for existing `C`.
46/// * `c` -- Dense mutable matrix descriptor for `C`.
47///
48/// # Errors
49///
50/// Returns [`SparseError::DimensionMismatch`] if dimensions are incompatible.
51/// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
52/// Returns [`SparseError::Cuda`] on kernel launch failure.
53pub 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    // Validate dimensions: A(m, k) * B(k, n) = C(m, n)
62    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    // Grid: one thread per (row, col_tile) pair
92    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        &params,
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
118/// Generates PTX for the SpMM kernel.
119///
120/// Each thread handles one row of A and a tile of SPMM_TILE_COLS columns of B.
121/// For simplicity, we implement a scalar approach: each thread iterates over
122/// the non-zeros of its row and accumulates into multiple output columns.
123fn 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            // Compute which row and col tile we handle
145            let n_param = b.load_param_u32("n");
146            let m_param = b.load_param_u32("m");
147
148            // tiles_per_row = (n + tile_cols - 1) / tile_cols
149            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            // row = gid / tiles_per_row, tile_id = gid % tiles_per_row
158            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                // col_start = tile_id * tile_cols
184                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                // Load row bounds
191                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                // We process 1 column (simplified approach for correctness)
204                // In production, we'd unroll tile_cols times
205                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                // Accumulate: acc = sum(A[row,k] * B[k, col])
215                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                // Load A value and column
228                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                // Load B[a_col, col] = b_ptr + (a_col * ldb + col) * elem_bytes
237                // Row-major: B[a_col][col] = b_ptr + a_col * ldb + col
238                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                // acc += a_val * b_val
246                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                // Write C[row, col] = alpha * acc + beta * C_old
255                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    // ---------------------------------------------------------------------------
281    // CPU reference SpMM for numerical accuracy verification
282    // ---------------------------------------------------------------------------
283
284    /// CPU reference CSR SpMM: computes C = A * B (no alpha/beta scaling).
285    ///
286    /// * `row_ptr`, `col_idx`, `values` — CSR representation of A (m×k sparse).
287    /// * `b` — row-major dense matrix B of shape (k, n) with leading dimension `ldb`.
288    /// * `n` — number of columns in B (and C).
289    ///
290    /// Returns C as a row-major Vec<f32> of shape m×n (leading dimension n).
291    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                // A[row, a_col] * B[a_col, col] for all cols
306                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    // ---------------------------------------------------------------------------
315    // PTX generation tests
316    // ---------------------------------------------------------------------------
317
318    #[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        // Should contain FMA for the accumulation step
338        assert!(
339            ptx.contains("fma") || ptx.contains("mul"),
340            "SpMM PTX should contain arithmetic instructions"
341        );
342    }
343
344    // ---------------------------------------------------------------------------
345    // CPU reference numerical accuracy tests
346    // ---------------------------------------------------------------------------
347
348    /// 4×4 identity sparse × 4×3 dense = 4×3 dense (same as dense matrix).
349    ///
350    /// A = I_4, B:
351    ///   [1 2 3]
352    ///   [4 5 6]
353    ///   [7 8 9]
354    ///   [10 11 12]
355    ///
356    /// C = A * B = B.
357    #[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        // B: 4×3 row-major
364        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        // C should equal B
373        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    /// 2×3 sparse A × 3×2 dense B = 2×2 dense C with known values.
380    ///
381    /// A (CSR):
382    ///   Row 0: A[0,0]=1, A[0,2]=3
383    ///   Row 1: A[1,1]=2, A[1,2]=4
384    ///
385    /// B (row-major, 3×2):
386    ///   [1 2]
387    ///   [3 4]
388    ///   [5 6]
389    ///
390    /// C = A*B:
391    ///   C[0,0] = 1*1 + 3*5 = 16,  C[0,1] = 1*2 + 3*6 = 20
392    ///   C[1,0] = 2*3 + 4*5 = 26,  C[1,1] = 2*4 + 4*6 = 32
393    #[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]; // 3×2 row-major
400        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    /// 4×4 diagonal sparse A × 4×3 dense B = 4×3 dense C.
413    ///
414    /// A = diag(2, 3, 4, 5), B rows are [1,0,0], [0,1,0], [0,0,1], [1,1,1].
415    ///
416    /// C[i] = A[i,i] * B[i] for each row i.
417    #[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        // B: 4×3, each row is a unit vector or all-ones
424        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        // Row 0: 2 * [1,0,0] = [2,0,0]
433        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        // Row 1: 3 * [0,1,0] = [0,3,0]
438        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        // Row 2: 4 * [0,0,1] = [0,0,4]
443        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        // Row 3: 5 * [1,1,1] = [5,5,5]
448        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    /// Verify SpMM with a zero sparse matrix produces an all-zero output.
454    #[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]; // 3×2
461        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}