use std::sync::Arc;
use oxicuda_blas::GpuFloat;
use oxicuda_driver::Module;
use oxicuda_launch::{Kernel, LaunchParams};
use oxicuda_memory::DeviceBuffer;
use oxicuda_ptx::prelude::*;
use crate::error::{SparseError, SparseResult};
use crate::format::BsrMatrix;
use crate::handle::SparseHandle;
use crate::ptx_helpers::{
add_float, fma_float, load_float_imm, load_global_float, mul_float, reinterpret_bits_to_float,
store_global_float,
};
const SPMV_BSR_MAX_BLOCK: u32 = 256;
pub fn spmv_bsr<T: GpuFloat>(
handle: &SparseHandle,
bsr: &BsrMatrix<T>,
x: &DeviceBuffer<T>,
y: &mut DeviceBuffer<T>,
alpha: T,
beta: T,
) -> SparseResult<()> {
if bsr.rows() == 0 || bsr.cols() == 0 {
return Ok(());
}
if x.len() < bsr.cols() as usize {
return Err(SparseError::DimensionMismatch(format!(
"x length ({}) must be >= cols ({})",
x.len(),
bsr.cols()
)));
}
if y.len() < bsr.rows() as usize {
return Err(SparseError::DimensionMismatch(format!(
"y length ({}) must be >= rows ({})",
y.len(),
bsr.rows()
)));
}
let block_dim = bsr.block_dim();
let block_rows = bsr.block_rows();
let threads_per_block = (block_dim.div_ceil(32) * 32).min(SPMV_BSR_MAX_BLOCK);
let ptx = emit_spmv_bsr::<T>(handle.sm_version(), block_dim)?;
let module = Arc::new(Module::from_ptx(&ptx)?);
let kernel = Kernel::from_module(module, "spmv_bsr")?;
let params = LaunchParams::new(block_rows, threads_per_block);
kernel.launch(
¶ms,
handle.stream(),
&(
bsr.row_ptr().as_device_ptr(),
bsr.col_idx().as_device_ptr(),
bsr.values().as_device_ptr(),
x.as_device_ptr(),
y.as_device_ptr(),
alpha.to_bits_u64(),
beta.to_bits_u64(),
bsr.rows(),
block_dim,
),
)?;
Ok(())
}
fn emit_spmv_bsr<T: GpuFloat>(sm: SmVersion, _block_dim: u32) -> SparseResult<String> {
let elem_bytes = T::size_u32();
let is_f64 = T::SIZE == 8;
KernelBuilder::new("spmv_bsr")
.target(sm)
.param("row_ptr", PtxType::U64)
.param("col_idx", PtxType::U64)
.param("values_ptr", PtxType::U64)
.param("x_ptr", PtxType::U64)
.param("y_ptr", PtxType::U64)
.param("alpha_bits", PtxType::U64)
.param("beta_bits", PtxType::U64)
.param("num_rows", PtxType::U32)
.param("block_dim", PtxType::U32)
.body(move |b| {
let block_row = b.block_id_x();
let tid = b.thread_id_x();
let block_dim_reg = b.load_param_u32("block_dim");
let tid_inner = tid.clone();
let block_row_inner = block_row.clone();
b.if_lt_u32(tid, block_dim_reg, move |b| {
let tid = tid_inner;
let block_row = block_row_inner;
let block_dim_reg = b.load_param_u32("block_dim");
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_ptr");
let x_ptr = b.load_param_u64("x_ptr");
let y_ptr = b.load_param_u64("y_ptr");
let alpha_bits = b.load_param_u64("alpha_bits");
let beta_bits = b.load_param_u64("beta_bits");
let alpha = reinterpret_bits_to_float::<T>(b, alpha_bits);
let beta = reinterpret_bits_to_float::<T>(b, beta_bits);
let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), block_row.clone(), 4);
let blk_start_i32 = b.load_global_i32(rp_addr);
let blk_start = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {blk_start}, {blk_start_i32};"));
let block_row_plus_1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {block_row_plus_1}, {block_row}, 1;"));
let rp_addr_next = b.byte_offset_addr(row_ptr_base, block_row_plus_1, 4);
let blk_end_i32 = b.load_global_i32(rp_addr_next);
let blk_end = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {blk_end}, {blk_end_i32};"));
let acc = load_float_imm::<T>(b, 0.0);
let blk_sq = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"mul.lo.u32 {blk_sq}, {block_dim_reg}, {block_dim_reg};"
));
let blk_idx = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {blk_idx}, {blk_start};"));
let blk_loop = b.fresh_label("bsr_blk_loop");
let blk_done = b.fresh_label("bsr_blk_done");
b.label(&blk_loop);
let pred_blk = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {pred_blk}, {blk_idx}, {blk_end};"));
b.branch_if(pred_blk, &blk_done);
let ci_addr = b.byte_offset_addr(col_idx_base.clone(), blk_idx.clone(), 4);
let blk_col_i32 = b.load_global_i32(ci_addr);
let blk_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {blk_col}, {blk_col_i32};"));
let val_block_offset = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"mul.lo.u32 {val_block_offset}, {blk_idx}, {blk_sq};"
));
let row_in_block_offset = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"mul.lo.u32 {row_in_block_offset}, {tid}, {block_dim_reg};"
));
let x_col_base = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"mul.lo.u32 {x_col_base}, {blk_col}, {block_dim_reg};"
));
let j = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {j}, 0;"));
let inner_loop = b.fresh_label("bsr_inner");
let inner_done = b.fresh_label("bsr_inner_done");
b.label(&inner_loop);
let pred_j = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {pred_j}, {j}, {block_dim_reg};"));
b.branch_if(pred_j, &inner_done);
let val_flat = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"add.u32 {val_flat}, {val_block_offset}, {row_in_block_offset};"
));
let val_idx = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {val_idx}, {val_flat}, {j};"));
let v_addr = b.byte_offset_addr(values_base.clone(), val_idx, elem_bytes);
let val = load_global_float::<T>(b, v_addr);
let x_idx = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {x_idx}, {x_col_base}, {j};"));
let x_addr = b.byte_offset_addr(x_ptr.clone(), x_idx, elem_bytes);
let x_val = load_global_float::<T>(b, x_addr);
let new_acc = fma_float::<T>(b, val, x_val, acc.clone());
let mov_suffix = if is_f64 { "f64" } else { "f32" };
b.raw_ptx(&format!("mov.{mov_suffix} {acc}, {new_acc};"));
b.raw_ptx(&format!("add.u32 {j}, {j}, 1;"));
b.branch(&inner_loop);
b.label(&inner_done);
b.raw_ptx(&format!("add.u32 {blk_idx}, {blk_idx}, 1;"));
b.branch(&blk_loop);
b.label(&blk_done);
let global_row = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"mad.lo.u32 {global_row}, {block_row}, {block_dim_reg}, {tid};"
));
let y_addr = b.byte_offset_addr(y_ptr, global_row, elem_bytes);
let y_old = load_global_float::<T>(b, y_addr.clone());
let alpha_acc = mul_float::<T>(b, alpha, acc);
let beta_y = mul_float::<T>(b, beta, y_old);
let result = add_float::<T>(b, alpha_acc, beta_y);
store_global_float::<T>(b, y_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 spmv_bsr_f32_f64_assemble_sm86() {
let f32_ptx = emit_spmv_bsr::<f32>(SmVersion::Sm86, 4).expect("f32 BSR PTX");
assert_assembles_and_clean("spmv_bsr_f32", &f32_ptx);
let f64_ptx = emit_spmv_bsr::<f64>(SmVersion::Sm86, 4).expect("f64 BSR PTX");
assert_assembles_and_clean("spmv_bsr_f64", &f64_ptx);
assert!(
!f64_ptx.contains("0F00000000"),
"f64 BSR kernel must not materialize an f32 0.0 immediate:\n{f64_ptx}"
);
}
#[test]
fn spmv_bsr_ptx_generates_f32() {
let ptx = emit_spmv_bsr::<f32>(SmVersion::Sm80, 2);
assert!(ptx.is_ok());
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains(".entry spmv_bsr"));
assert!(ptx_text.contains(".target sm_80"));
}
#[test]
fn spmv_bsr_ptx_generates_f64() {
let ptx = emit_spmv_bsr::<f64>(SmVersion::Sm80, 4);
assert!(ptx.is_ok());
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains(".entry spmv_bsr"));
}
#[test]
fn spmv_bsr_ptx_block_sizes() {
for bd in [2, 4, 8] {
let ptx = emit_spmv_bsr::<f32>(SmVersion::Sm80, bd);
assert!(ptx.is_ok(), "BSR PTX generation failed for block_dim={bd}");
}
}
#[test]
fn spmv_bsr_threads_per_block() {
for block_dim in [2u32, 4, 8, 16, 32, 64] {
let threads = (block_dim.div_ceil(32) * 32).min(SPMV_BSR_MAX_BLOCK);
assert!(threads >= block_dim);
assert_eq!(threads % 32, 0);
assert!(threads <= SPMV_BSR_MAX_BLOCK);
}
}
#[test]
fn spmv_bsr_ptx_contains_block_multiply() {
let ptx = emit_spmv_bsr::<f32>(SmVersion::Sm80, 4);
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains("bsr_blk_loop"));
assert!(ptx_text.contains("bsr_inner"));
}
}
#[cfg(all(test, feature = "gpu-tests"))]
mod gpu_device_tests {
use super::*;
use crate::gpu_test_support::{assert_close, gpu_handle};
use crate::host_csr::{f64_to_gpu, gpu_to_f64};
#[allow(clippy::too_many_arguments)]
fn cpu_bsr_spmv(
rows: usize,
block_dim: usize,
row_ptr: &[i32],
col_idx: &[i32],
values: &[f64],
x: &[f64],
y0: &[f64],
alpha: f64,
beta: f64,
) -> Vec<f64> {
let block_rows = rows / block_dim;
let bd2 = block_dim * block_dim;
let mut acc = vec![0.0_f64; rows];
for block_row in 0..block_rows {
for blk in row_ptr[block_row] as usize..row_ptr[block_row + 1] as usize {
let blk_col = col_idx[blk] as usize;
for r in 0..block_dim {
for c in 0..block_dim {
let v = values[blk * bd2 + r * block_dim + c];
acc[block_row * block_dim + r] += v * x[blk_col * block_dim + c];
}
}
}
}
(0..rows).map(|g| alpha * acc[g] + beta * y0[g]).collect()
}
#[allow(clippy::too_many_arguments)]
fn run_bsr<T: GpuFloat>(
rows: u32,
cols: u32,
block_dim: u32,
row_ptr: &[i32],
col_idx: &[i32],
values: &[f64],
x: &[f64],
y0: &[f64],
alpha: f64,
beta: f64,
tol: f64,
tag: &str,
) {
let Some(handle) = gpu_handle() else {
return;
};
let dev_values: Vec<T> = values.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
let bsr = BsrMatrix::<T>::from_host(rows, cols, block_dim, row_ptr, col_idx, &dev_values)
.expect("test: build BSR");
let dev_x: Vec<T> = x.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
let dev_y: Vec<T> = y0.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
let x_buf = DeviceBuffer::from_host(&dev_x).expect("test: upload x");
let mut y_buf = DeviceBuffer::from_host(&dev_y).expect("test: upload y");
spmv_bsr::<T>(
&handle,
&bsr,
&x_buf,
&mut y_buf,
f64_to_gpu::<T>(alpha),
f64_to_gpu::<T>(beta),
)
.expect("test: spmv_bsr launch");
handle.stream().synchronize().expect("test: sync");
let mut out = vec![T::gpu_zero(); rows as usize];
y_buf.copy_to_host(&mut out).expect("test: download y");
let got: Vec<f64> = out.iter().map(|&v| gpu_to_f64(v)).collect();
let want = cpu_bsr_spmv(
rows as usize,
block_dim as usize,
row_ptr,
col_idx,
values,
x,
y0,
alpha,
beta,
);
assert_close(&got, &want, tol, tag);
}
fn bsr_4x4_b2() -> (u32, u32, u32, Vec<i32>, Vec<i32>, Vec<f64>) {
let row_ptr = vec![0, 2, 3];
let col_idx = vec![0, 1, 1];
let values = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, ];
(4, 4, 2, row_ptr, col_idx, values)
}
fn bsr_6x6_b3() -> (u32, u32, u32, Vec<i32>, Vec<i32>, Vec<f64>) {
let row_ptr = vec![0, 2, 3];
let col_idx = vec![0, 1, 0];
let mut values = Vec::new();
for blk in 0..3 {
for r in 0..3 {
for c in 0..3 {
values.push(1.0 + (blk * 9 + r * 3 + c) as f64 * 0.5);
}
}
}
(6, 6, 3, row_ptr, col_idx, values)
}
#[test]
fn bsr_b2_f64_alpha_beta() {
let (r, c, bd, rp, ci, v) = bsr_4x4_b2();
let x = vec![1.0, 2.0, 3.0, 4.0];
let y0 = vec![10.0, 20.0, 30.0, 40.0];
run_bsr::<f64>(
r,
c,
bd,
&rp,
&ci,
&v,
&x,
&y0,
1.5,
-0.25,
1e-10,
"bsr_b2_f64",
);
}
#[test]
fn bsr_b2_f32_alpha_beta() {
let (r, c, bd, rp, ci, v) = bsr_4x4_b2();
let x = vec![0.5, -1.0, 2.0, 1.5];
let y0 = vec![1.0, 2.0, 3.0, 4.0];
run_bsr::<f32>(
r,
c,
bd,
&rp,
&ci,
&v,
&x,
&y0,
2.0,
0.5,
1e-4,
"bsr_b2_f32",
);
}
#[test]
fn bsr_b3_f64_alpha_beta() {
let (r, c, bd, rp, ci, v) = bsr_6x6_b3();
let x: Vec<f64> = (0..r as usize).map(|i| 1.0 + i as f64).collect();
let y0: Vec<f64> = (0..r as usize).map(|i| 100.0 - 2.0 * i as f64).collect();
run_bsr::<f64>(
r,
c,
bd,
&rp,
&ci,
&v,
&x,
&y0,
1.25,
0.75,
1e-10,
"bsr_b3_f64",
);
}
#[test]
fn bsr_b2_f64_beta_zero() {
let (r, c, bd, rp, ci, v) = bsr_4x4_b2();
let x = vec![1.0, 1.0, 1.0, 1.0];
let y0 = vec![1e9, -1e9, 5e8, -5e8];
run_bsr::<f64>(
r,
c,
bd,
&rp,
&ci,
&v,
&x,
&y0,
1.0,
0.0,
1e-10,
"bsr_b2_beta0",
);
}
}