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"));
}
}