#![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,
};
const SPTRSV_BLOCK_SIZE: u32 = 256;
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(());
}
let (h_row_ptr, h_col_idx, _h_values) = a.to_host()?;
let levels = analyze_levels(&h_row_ptr, &h_col_idx, n, fill_mode)?;
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")?;
for level_rows in &levels {
if level_rows.is_empty() {
continue;
}
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(
¶ms,
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,
),
)?;
handle.stream().synchronize()?;
}
Ok(())
}
fn analyze_levels(
row_ptr: &[i32],
col_idx: &[i32],
n: u32,
fill_mode: FillMode,
) -> SparseResult<Vec<Vec<u32>>> {
let n_usize = n as usize;
let mut depth = vec![0u32; n_usize];
let mut max_depth: u32 = 0;
match fill_mode {
FillMode::Lower => {
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 => {
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;
}
}
}
}
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)
}
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");
let row_addr = b.byte_offset_addr(level_rows_ptr, tid, 4);
let row = b.load_global_u32(row_addr);
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};"));
let b_addr = b.byte_offset_addr(b_ptr, row.clone(), elem_bytes);
let b_val = load_global_float::<T>(b, b_addr);
let sum = load_float_imm::<T>(b, 0.0);
let diag = load_float_imm::<T>(b, 1.0);
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);
let pred = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {pred}, {k}, {re};"));
b.branch_if(pred, &loop_done);
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);
let not_diag = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.ne.u32 {not_diag}, {col}, {row};"));
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);
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);
if is_lower {
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);
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 {
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);
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);
let result = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!(
"div.rn.{mov_suffix} {result}, {numerator}, {diag};"
));
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;
#[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() {
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() {
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);
}
}