oxicuda-sparse 0.4.0

OxiCUDA Sparse - GPU-accelerated sparse matrix operations (cuSPARSE equivalent)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! Sparse triangular solve (SpTRSV).
//!
//! Solves `L * x = b` (lower triangular) or `U * x = b` (upper triangular)
//! where the triangular matrix is stored in CSR format.
//!
//! ## Algorithm
//!
//! Uses a **level-set parallelism** approach:
//! 1. **Analysis phase** (`analyze_levels`): topologically sorts rows into
//!    dependency levels. Rows within the same level are independent and can be
//!    solved in parallel.
//! 2. **Solve phase**: for each level, launches a PTX kernel where each thread
//!    applies forward or backward substitution for one row, reading only
//!    already-solved entries from previous levels.
#![allow(dead_code)]

use std::sync::Arc;

use oxicuda_blas::{FillMode, GpuFloat};
use oxicuda_driver::Module;
use oxicuda_driver::ffi::CUdeviceptr;
use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
use oxicuda_ptx::arch::SmVersion;
use oxicuda_ptx::builder::KernelBuilder;
use oxicuda_ptx::ir::PtxType;

use crate::error::{SparseError, SparseResult};
use crate::format::CsrMatrix;
use crate::handle::SparseHandle;
use crate::ptx_helpers::{
    add_float, fma_float, load_float_imm, load_global_float, mul_float, store_global_float,
};

/// Default block size for SpTRSV level-solve kernels.
const SPTRSV_BLOCK_SIZE: u32 = 256;

/// Sparse triangular solve: solves `T * x = b` where `T` is triangular CSR.
///
/// # Arguments
///
/// * `handle` -- Sparse handle.
/// * `fill_mode` -- Whether `a` stores a lower (`L`) or upper (`U`) triangle.
/// * `a` -- Sparse CSR triangular matrix.
/// * `b_ptr` -- Device pointer to the right-hand side vector `b`.
/// * `x_ptr` -- Device pointer to the output vector `x`.
///
/// # Errors
///
/// Returns [`SparseError::DimensionMismatch`] if the matrix is not square.
/// Returns [`SparseError::SingularMatrix`] if a zero diagonal is detected.
/// Returns [`SparseError::PtxGeneration`] if kernel generation fails.
pub fn sptrsv<T: GpuFloat>(
    handle: &SparseHandle,
    fill_mode: FillMode,
    a: &CsrMatrix<T>,
    b_ptr: CUdeviceptr,
    x_ptr: CUdeviceptr,
) -> SparseResult<()> {
    if a.rows() != a.cols() {
        return Err(SparseError::DimensionMismatch(format!(
            "triangular solve requires square matrix, got {}x{}",
            a.rows(),
            a.cols()
        )));
    }

    let n = a.rows();
    if n == 0 {
        return Ok(());
    }

    // Download structure for analysis
    let (h_row_ptr, h_col_idx, _h_values) = a.to_host()?;

    // Build dependency levels
    let levels = analyze_levels(&h_row_ptr, &h_col_idx, n, fill_mode)?;

    // Generate the per-level solve kernel
    let ptx = emit_sptrsv_level_kernel::<T>(fill_mode, handle.sm_version())?;
    let module = Arc::new(Module::from_ptx(&ptx)?);
    let kernel = Kernel::from_module(module, "sptrsv_level")?;

    // Execute level by level
    for level_rows in &levels {
        if level_rows.is_empty() {
            continue;
        }

        // Upload the row indices for this level
        let d_level_rows = oxicuda_memory::DeviceBuffer::<u32>::from_host(level_rows)?;
        let num_rows_in_level = level_rows.len() as u32;

        let block_size = SPTRSV_BLOCK_SIZE;
        let grid_size = grid_size_for(num_rows_in_level, block_size);
        let params = LaunchParams::new(grid_size, block_size);

        kernel.launch(
            &params,
            handle.stream(),
            &(
                a.row_ptr().as_device_ptr(),
                a.col_idx().as_device_ptr(),
                a.values().as_device_ptr(),
                b_ptr,
                x_ptr,
                d_level_rows.as_device_ptr(),
                num_rows_in_level,
                n,
            ),
        )?;

        // Synchronize to ensure this level completes before the next
        handle.stream().synchronize()?;
    }

    Ok(())
}

/// Analyzes the dependency structure and groups rows into parallel levels.
///
/// For lower triangular: row `i` depends on all columns `j < i` in its row.
/// For upper triangular: row `i` depends on all columns `j > i` in its row.
///
/// Rows with no unsolved dependencies at a given level can be solved in parallel.
fn analyze_levels(
    row_ptr: &[i32],
    col_idx: &[i32],
    n: u32,
    fill_mode: FillMode,
) -> SparseResult<Vec<Vec<u32>>> {
    let n_usize = n as usize;

    // Compute the dependency depth for each row using BFS / topological sort
    let mut depth = vec![0u32; n_usize];
    let mut max_depth: u32 = 0;

    match fill_mode {
        FillMode::Lower => {
            // Process rows top-to-bottom
            for i in 0..n_usize {
                let start = row_ptr[i] as usize;
                let end = row_ptr[i + 1] as usize;
                let mut max_dep = 0u32;
                for &cj in &col_idx[start..end] {
                    let j = cj as usize;
                    if j < i {
                        let d = depth[j] + 1;
                        if d > max_dep {
                            max_dep = d;
                        }
                    }
                }
                depth[i] = max_dep;
                if max_dep > max_depth {
                    max_depth = max_dep;
                }
            }
        }
        FillMode::Upper | FillMode::Full => {
            // Process rows bottom-to-top for upper triangular
            for i in (0..n_usize).rev() {
                let start = row_ptr[i] as usize;
                let end = row_ptr[i + 1] as usize;
                let mut max_dep = 0u32;
                for &cj in &col_idx[start..end] {
                    let j = cj as usize;
                    if j > i {
                        let d = depth[j] + 1;
                        if d > max_dep {
                            max_dep = d;
                        }
                    }
                }
                depth[i] = max_dep;
                if max_dep > max_depth {
                    max_depth = max_dep;
                }
            }
        }
    }

    // Group rows by depth into levels
    let num_levels = max_depth as usize + 1;
    let mut levels: Vec<Vec<u32>> = vec![Vec::new(); num_levels];
    for (i, &d) in depth.iter().enumerate() {
        levels[d as usize].push(i as u32);
    }

    Ok(levels)
}

/// Generates PTX for one level of the SpTRSV solve.
///
/// Each thread picks one row from the level-rows array and performs
/// forward (lower) or backward (upper) substitution. Since all dependencies
/// are guaranteed to be solved in previous levels, each thread can read
/// `x[j]` for dependency columns safely.
fn emit_sptrsv_level_kernel<T: GpuFloat>(
    fill_mode: FillMode,
    sm: SmVersion,
) -> SparseResult<String> {
    let elem_bytes = T::size_u32();
    let is_f64 = T::SIZE == 8;
    let is_lower = matches!(fill_mode, FillMode::Lower);

    KernelBuilder::new("sptrsv_level")
        .target(sm)
        .param("row_ptr", PtxType::U64)
        .param("col_idx", PtxType::U64)
        .param("values", PtxType::U64)
        .param("b_ptr", PtxType::U64)
        .param("x_ptr", PtxType::U64)
        .param("level_rows", PtxType::U64)
        .param("num_level_rows", PtxType::U32)
        .param("n", PtxType::U32)
        .body(move |b| {
            let gid = b.global_thread_id_x();
            let num_level_rows = b.load_param_u32("num_level_rows");

            let gid_inner = gid.clone();
            b.if_lt_u32(gid, num_level_rows, move |b| {
                let tid = gid_inner;
                let level_rows_ptr = b.load_param_u64("level_rows");
                let row_ptr_base = b.load_param_u64("row_ptr");
                let col_idx_base = b.load_param_u64("col_idx");
                let values_base = b.load_param_u64("values");
                let b_ptr = b.load_param_u64("b_ptr");
                let x_ptr = b.load_param_u64("x_ptr");

                // Load the actual row index from level_rows[tid]
                let row_addr = b.byte_offset_addr(level_rows_ptr, tid, 4);
                let row = b.load_global_u32(row_addr);

                // Load row bounds
                let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
                let rs_i32 = b.load_global_i32(rp_addr);
                let rs = b.alloc_reg(PtxType::U32);
                b.raw_ptx(&format!("mov.b32 {rs}, {rs_i32};"));

                let row_p1 = b.alloc_reg(PtxType::U32);
                b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
                let re_addr = b.byte_offset_addr(row_ptr_base, row_p1, 4);
                let re_i32 = b.load_global_i32(re_addr);
                let re = b.alloc_reg(PtxType::U32);
                b.raw_ptx(&format!("mov.b32 {re}, {re_i32};"));

                // Load b[row]
                let b_addr = b.byte_offset_addr(b_ptr, row.clone(), elem_bytes);
                let b_val = load_global_float::<T>(b, b_addr);

                // Accumulate: sum = sum of A[row,j]*x[j] for off-diagonal j
                let sum = load_float_imm::<T>(b, 0.0);
                let diag = load_float_imm::<T>(b, 1.0); // will be overwritten

                let k = b.alloc_reg(PtxType::U32);
                b.raw_ptx(&format!("mov.u32 {k}, {rs};"));

                let loop_label = b.fresh_label("sptrsv_loop");
                let loop_done = b.fresh_label("sptrsv_done");

                b.label(&loop_label);
                // Exit when k >= row_end (inverted skip-branch via branch_if so
                // the `$`-prefixed label target matches the `b.label` def).
                let pred = b.alloc_reg(PtxType::Pred);
                b.raw_ptx(&format!("setp.hs.u32 {pred}, {k}, {re};"));
                b.branch_if(pred, &loop_done);

                // Load column and value
                let ci_addr = b.byte_offset_addr(col_idx_base.clone(), k.clone(), 4);
                let col_i32 = b.load_global_i32(ci_addr);
                let col = b.alloc_reg(PtxType::U32);
                b.raw_ptx(&format!("mov.b32 {col}, {col_i32};"));

                let v_addr = b.byte_offset_addr(values_base.clone(), k.clone(), elem_bytes);
                let a_val = load_global_float::<T>(b, v_addr);

                // Check if off-diagonal (col != row). Inverted skip-branch:
                // the original tested `setp.eq` then `@!is_diag bra skip_diag`;
                // branch to skip_diag exactly when col != row.
                let not_diag = b.alloc_reg(PtxType::Pred);
                b.raw_ptx(&format!("setp.ne.u32 {not_diag}, {col}, {row};"));

                // If diagonal, save it; otherwise accumulate
                let skip_diag = b.fresh_label("sptrsv_skip_diag");
                let after_diag = b.fresh_label("sptrsv_after_diag");
                b.branch_if(not_diag, &skip_diag);

                // Save diagonal value
                let mov_suffix = if is_f64 { "f64" } else { "f32" };
                b.raw_ptx(&format!("mov.{mov_suffix} {diag}, {a_val};"));
                b.branch(&after_diag);

                b.label(&skip_diag);

                // Off-diagonal: check if it's a dependency column
                if is_lower {
                    // For lower: depend on j < row
                    // Skip non-dependency columns: branch when col >= row
                    // (inverted `setp.lo` -> `setp.hs`).
                    let not_dep = b.alloc_reg(PtxType::Pred);
                    b.raw_ptx(&format!("setp.hs.u32 {not_dep}, {col}, {row};"));
                    let skip_acc = b.fresh_label("sptrsv_skip_acc");
                    b.branch_if(not_dep, &skip_acc);

                    // Load x[col] and accumulate
                    let x_addr = b.byte_offset_addr(x_ptr.clone(), col, elem_bytes);
                    let x_val = load_global_float::<T>(b, x_addr);
                    let new_sum = fma_float::<T>(b, a_val, x_val, sum.clone());
                    b.raw_ptx(&format!("mov.{mov_suffix} {sum}, {new_sum};"));

                    b.label(&skip_acc);
                } else {
                    // For upper: depend on j > row
                    // Skip non-dependency columns: branch when col <= row
                    // (inverted `setp.hi` -> `setp.ls`).
                    let not_dep = b.alloc_reg(PtxType::Pred);
                    b.raw_ptx(&format!("setp.ls.u32 {not_dep}, {col}, {row};"));
                    let skip_acc = b.fresh_label("sptrsv_skip_acc");
                    b.branch_if(not_dep, &skip_acc);

                    let x_addr = b.byte_offset_addr(x_ptr.clone(), col, elem_bytes);
                    let x_val = load_global_float::<T>(b, x_addr);
                    let new_sum = fma_float::<T>(b, a_val, x_val, sum.clone());
                    b.raw_ptx(&format!("mov.{mov_suffix} {sum}, {new_sum};"));

                    b.label(&skip_acc);
                }

                b.label(&after_diag);

                b.raw_ptx(&format!("add.u32 {k}, {k}, 1;"));
                b.branch(&loop_label);
                b.label(&loop_done);

                // x[row] = (b[row] - sum) / diag
                // Compute b_val - sum
                let neg_one = load_float_imm::<T>(b, -1.0);
                let neg_sum = mul_float::<T>(b, sum, neg_one);
                let numerator = add_float::<T>(b, b_val, neg_sum);

                // Divide by diagonal: x = numerator * (1/diag)
                // Use rcp for f32, div for both
                let result = b.alloc_reg(T::PTX_TYPE);
                b.raw_ptx(&format!(
                    "div.rn.{mov_suffix} {result}, {numerator}, {diag};"
                ));

                // Store x[row]
                let x_out_addr = b.byte_offset_addr(x_ptr, row, elem_bytes);
                store_global_float::<T>(b, x_out_addr, result);
            });

            b.ret();
        })
        .build()
        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ptx_helpers::test_support::assert_assembles_and_clean;

    /// The SpTRSV level kernel (lower and upper) must assemble for sm_86 in both
    /// precisions with `$`-prefixed branch targets and no `.b64` shuffle.
    #[test]
    fn sptrsv_lower_upper_f32_f64_assemble_sm86() {
        for fill in [FillMode::Lower, FillMode::Upper] {
            let tag = if matches!(fill, FillMode::Lower) {
                "lower"
            } else {
                "upper"
            };
            let f32_ptx =
                emit_sptrsv_level_kernel::<f32>(fill, SmVersion::Sm86).expect("f32 sptrsv");
            assert_assembles_and_clean(&format!("sptrsv_{tag}_f32"), &f32_ptx);

            let f64_ptx =
                emit_sptrsv_level_kernel::<f64>(fill, SmVersion::Sm86).expect("f64 sptrsv");
            assert_assembles_and_clean(&format!("sptrsv_{tag}_f64"), &f64_ptx);
            assert!(
                !f64_ptx.contains("0F00000000"),
                "f64 sptrsv {tag} kernel must not materialize an f32 0.0 immediate:\n{f64_ptx}"
            );
        }
    }
    use oxicuda_ptx::arch::SmVersion;

    #[test]
    fn sptrsv_lower_ptx_generates_f32() {
        let ptx = emit_sptrsv_level_kernel::<f32>(FillMode::Lower, SmVersion::Sm80);
        assert!(ptx.is_ok());
        let ptx_str = ptx.expect("test: PTX gen should succeed");
        assert!(ptx_str.contains(".entry sptrsv_level"));
    }

    #[test]
    fn sptrsv_upper_ptx_generates_f32() {
        let ptx = emit_sptrsv_level_kernel::<f32>(FillMode::Upper, SmVersion::Sm80);
        assert!(ptx.is_ok());
    }

    #[test]
    fn sptrsv_lower_ptx_generates_f64() {
        let ptx = emit_sptrsv_level_kernel::<f64>(FillMode::Lower, SmVersion::Sm80);
        assert!(ptx.is_ok());
    }

    #[test]
    fn analyze_levels_lower_triangular() {
        // 3x3 lower triangular:
        // row 0: [0]        -> no deps, level 0
        // row 1: [0, 1]     -> depends on row 0, level 1
        // row 2: [0, 1, 2]  -> depends on row 1 (level 1), so level 2
        let row_ptr = vec![0, 1, 3, 6];
        let col_idx = vec![0, 0, 1, 0, 1, 2];
        let levels = analyze_levels(&row_ptr, &col_idx, 3, FillMode::Lower);
        assert!(levels.is_ok());
        let levels = levels.expect("test: analyze should succeed");
        assert_eq!(levels.len(), 3);
        assert_eq!(levels[0], vec![0]);
        assert_eq!(levels[1], vec![1]);
        assert_eq!(levels[2], vec![2]);
    }

    #[test]
    fn analyze_levels_identity() {
        // 3x3 identity: all rows are independent
        let row_ptr = vec![0, 1, 2, 3];
        let col_idx = vec![0, 1, 2];
        let levels = analyze_levels(&row_ptr, &col_idx, 3, FillMode::Lower);
        assert!(levels.is_ok());
        let levels = levels.expect("test: analyze should succeed");
        assert_eq!(levels.len(), 1);
        assert_eq!(levels[0].len(), 3);
    }
}