use std::sync::Arc;
use oxicuda_blas::GpuFloat;
use oxicuda_driver::Module;
use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
use oxicuda_memory::DeviceBuffer;
use oxicuda_ptx::prelude::*;
use crate::error::{SparseError, SparseResult};
use crate::format::csr5::Csr5Matrix;
use crate::handle::SparseHandle;
use crate::ptx_helpers::{
add_float, emit_shfl_float, load_float_imm, load_global_float, mul_float,
reinterpret_bits_to_float, store_global_float,
};
const CSR5_TILE_BLOCK: u32 = 256;
const CSR5_CALIBRATE_BLOCK: u32 = 256;
pub fn csr5_spmv<T: GpuFloat>(
handle: &SparseHandle,
csr5: &Csr5Matrix<T>,
x: &DeviceBuffer<T>,
y: &mut DeviceBuffer<T>,
alpha: T,
beta: T,
) -> SparseResult<()> {
if csr5.rows() == 0 || csr5.cols() == 0 {
return Ok(());
}
if x.len() < csr5.cols() as usize {
return Err(SparseError::DimensionMismatch(format!(
"x length ({}) must be >= cols ({})",
x.len(),
csr5.cols()
)));
}
if y.len() < csr5.rows() as usize {
return Err(SparseError::DimensionMismatch(format!(
"y length ({}) must be >= rows ({})",
y.len(),
csr5.rows()
)));
}
let tile_ptx = emit_csr5_tile_kernel::<T>(handle.sm_version())?;
let tile_module = Arc::new(Module::from_ptx(&tile_ptx)?);
let tile_kernel = Kernel::from_module(tile_module, "csr5_tile")?;
let warps_per_block = CSR5_TILE_BLOCK / 32;
let tile_grid = grid_size_for(csr5.num_tiles(), warps_per_block);
tile_kernel.launch(
&LaunchParams::new(tile_grid, CSR5_TILE_BLOCK),
handle.stream(),
&(
csr5.row_ptr().as_device_ptr(),
csr5.col_idx().as_device_ptr(),
csr5.values().as_device_ptr(),
csr5.tile_ptr().as_device_ptr(),
csr5.tile_desc().as_device_ptr(),
x.as_device_ptr(),
y.as_device_ptr(),
csr5.calibrator().as_device_ptr(),
alpha.to_bits_u64(),
beta.to_bits_u64(),
csr5.rows(),
csr5.num_tiles(),
csr5.nnz(),
),
)?;
let cal_ptx = emit_csr5_calibrate_kernel::<T>(handle.sm_version())?;
let cal_module = Arc::new(Module::from_ptx(&cal_ptx)?);
let cal_kernel = Kernel::from_module(cal_module, "csr5_calibrate")?;
let cal_grid = grid_size_for(csr5.rows(), CSR5_CALIBRATE_BLOCK);
cal_kernel.launch(
&LaunchParams::new(cal_grid, CSR5_CALIBRATE_BLOCK),
handle.stream(),
&(
y.as_device_ptr(),
csr5.calibrator().as_device_ptr(),
csr5.rows(),
),
)?;
Ok(())
}
fn emit_csr5_tile_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
let elem_bytes = T::size_u32();
let is_f64 = T::SIZE == 8;
let mov_suffix = if is_f64 { "f64" } else { "f32" };
KernelBuilder::new("csr5_tile")
.target(sm)
.param("row_ptr", PtxType::U64)
.param("col_idx", PtxType::U64)
.param("values_ptr", PtxType::U64)
.param("tile_ptr", PtxType::U64)
.param("tile_desc", PtxType::U64)
.param("x_ptr", PtxType::U64)
.param("y_ptr", PtxType::U64)
.param("calibrator_ptr", PtxType::U64)
.param("alpha_bits", PtxType::U64)
.param("beta_bits", PtxType::U64)
.param("num_rows", PtxType::U32)
.param("num_tiles", PtxType::U32)
.param("nnz", PtxType::U32)
.body(move |b| {
let tid_global = b.global_thread_id_x();
let num_tiles = b.load_param_u32("num_tiles");
let lane = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("and.b32 {lane}, {tid_global}, 31;"));
let tile_id = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("shr.u32 {tile_id}, {tid_global}, 5;"));
let tile_id_inner = tile_id.clone();
let lane_inner = lane.clone();
b.if_lt_u32(tile_id, num_tiles, move |b| {
let tile_id = tile_id_inner;
let lane = lane_inner;
let col_idx_base = b.load_param_u64("col_idx");
let values_base = b.load_param_u64("values_ptr");
let tile_ptr_base = b.load_param_u64("tile_ptr");
let tile_desc_base = b.load_param_u64("tile_desc");
let x_ptr = b.load_param_u64("x_ptr");
let _y_ptr = b.load_param_u64("y_ptr");
let calibrator_ptr = b.load_param_u64("calibrator_ptr");
let alpha_bits = b.load_param_u64("alpha_bits");
let beta_bits = b.load_param_u64("beta_bits");
let num_rows_reg = b.load_param_u32("num_rows");
let nnz_reg = b.load_param_u32("nnz");
let alpha = reinterpret_bits_to_float::<T>(b, alpha_bits);
let _beta = reinterpret_bits_to_float::<T>(b, beta_bits);
let tp_addr = b.byte_offset_addr(tile_ptr_base.clone(), tile_id.clone(), 4);
let tile_start = b.load_global_u32(tp_addr);
let elem_idx = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {elem_idx}, {tile_start}, {lane};"));
let oob = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {oob}, {elem_idx}, {nnz_reg};"));
let product = load_float_imm::<T>(b, 0.0);
let compute_label = b.fresh_label("csr5_compute");
let after_compute = b.fresh_label("csr5_after_compute");
b.branch_if(oob, &after_compute);
b.label(&compute_label);
let ci_addr = b.byte_offset_addr(col_idx_base, elem_idx.clone(), 4);
let col_i32 = b.load_global_i32(ci_addr);
let col_u32 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {col_u32}, {col_i32};"));
let v_addr = b.byte_offset_addr(values_base, elem_idx, elem_bytes);
let val = load_global_float::<T>(b, v_addr);
let x_addr = b.byte_offset_addr(x_ptr, col_u32, elem_bytes);
let x_val = load_global_float::<T>(b, x_addr);
let prod = mul_float::<T>(b, val, x_val);
b.raw_ptx(&format!("mov.{mov_suffix} {product}, {prod};"));
b.label(&after_compute);
let desc_addr = b.byte_offset_addr(tile_desc_base, tile_id.clone(), 8);
let seg_mask = b.load_global_u32(desc_addr.clone());
let desc_addr_plus4 = b.alloc_reg(PtxType::U64);
b.raw_ptx(&format!("add.u64 {desc_addr_plus4}, {desc_addr}, 4;"));
let first_row = b.load_global_u32(desc_addr_plus4);
let lane_plus_1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {lane_plus_1}, {lane}, 1;"));
let lane_mask = b.alloc_reg(PtxType::U32);
let one = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {one}, 1;"));
b.raw_ptx(&format!("shl.b32 {lane_mask}, {one}, {lane_plus_1};"));
let lane_mask_sub = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("sub.u32 {lane_mask_sub}, {lane_mask}, 1;"));
let masked_seg = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"and.b32 {masked_seg}, {seg_mask}, {lane_mask_sub};"
));
let row_offset = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("popc.b32 {row_offset}, {masked_seg};"));
let my_row = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {my_row}, {first_row}, {row_offset};"));
let acc = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!("mov.{mov_suffix} {acc}, {product};"));
for offset in [1u32, 2, 4, 8, 16] {
let shuffled =
emit_shfl_float::<T>(b, "up", acc.clone(), &offset.to_string(), "0");
let src_lane = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("sub.u32 {src_lane}, {lane}, {offset};"));
let valid = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.ge.u32 {valid}, {lane}, {offset};"));
let sum = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!("add.{mov_suffix} {sum}, {acc}, {shuffled};"));
let src_lane_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {src_lane_p1}, {src_lane}, 1;"));
let src_mask = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("shl.b32 {src_mask}, {one}, {src_lane_p1};"));
let src_mask_sub = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("sub.u32 {src_mask_sub}, {src_mask}, 1;"));
let src_masked = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!(
"and.b32 {src_masked}, {seg_mask}, {src_mask_sub};"
));
let src_row_off = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("popc.b32 {src_row_off}, {src_masked};"));
let src_row = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {src_row}, {first_row}, {src_row_off};"));
let same_row = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.eq.u32 {same_row}, {src_row}, {my_row};"));
let do_add = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("and.pred {do_add}, {valid}, {same_row};"));
b.raw_ptx(&format!("selp.{mov_suffix} {acc}, {sum}, {acc}, {do_add};"));
}
let is_last = b.alloc_reg(PtxType::Pred);
let is_lane_31 = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.eq.u32 {is_lane_31}, {lane}, 31;"));
let next_lane = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {next_lane}, {lane}, 1;"));
let next_bit = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("shr.b32 {next_bit}, {seg_mask}, {next_lane};"));
let next_bit_masked = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("and.b32 {next_bit_masked}, {next_bit}, 1;"));
let next_is_new_seg = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!(
"setp.ne.u32 {next_is_new_seg}, {next_bit_masked}, 0;"
));
b.raw_ptx(&format!(
"or.pred {is_last}, {is_lane_31}, {next_is_new_seg};"
));
let not_last = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("not.pred {not_last}, {is_last};"));
let write_label = b.fresh_label("csr5_write");
let skip_write = b.fresh_label("csr5_skip_write");
b.branch_if(not_last, &skip_write);
b.label(&write_label);
let row_oob = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {row_oob}, {my_row}, {num_rows_reg};"));
let row_skip = b.fresh_label("csr5_row_skip");
b.branch_if(row_oob, &row_skip);
let scaled_acc = mul_float::<T>(b, alpha.clone(), acc);
let cal_addr = b.byte_offset_addr(calibrator_ptr, my_row.clone(), elem_bytes);
let _old = b.alloc_reg(T::PTX_TYPE);
b.raw_ptx(&format!(
"atom.global.add.{mov_suffix} {_old}, [{cal_addr}], {scaled_acc};"
));
b.label(&row_skip);
b.label(&skip_write);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_csr5_calibrate_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
let elem_bytes = T::size_u32();
let _is_f64 = T::SIZE == 8;
KernelBuilder::new("csr5_calibrate")
.target(sm)
.param("y_ptr", PtxType::U64)
.param("calibrator_ptr", PtxType::U64)
.param("num_rows", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let num_rows = b.load_param_u32("num_rows");
let gid_inner = gid.clone();
b.if_lt_u32(gid, num_rows, move |b| {
let row = gid_inner;
let y_ptr = b.load_param_u64("y_ptr");
let cal_ptr = b.load_param_u64("calibrator_ptr");
let cal_addr = b.byte_offset_addr(cal_ptr, row.clone(), elem_bytes);
let cal_val = load_global_float::<T>(b, cal_addr);
let y_addr = b.byte_offset_addr(y_ptr, row, elem_bytes);
let y_val = load_global_float::<T>(b, y_addr.clone());
let result = add_float::<T>(b, y_val, cal_val);
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 csr5_tile_calibrate_f32_f64_assemble_sm86() {
let tile_f32 = emit_csr5_tile_kernel::<f32>(SmVersion::Sm86).expect("f32 tile");
assert_assembles_and_clean("csr5_tile_f32", &tile_f32);
let tile_f64 = emit_csr5_tile_kernel::<f64>(SmVersion::Sm86).expect("f64 tile");
assert_assembles_and_clean("csr5_tile_f64", &tile_f64);
assert!(
!tile_f64.contains("shfl.sync.up.b64"),
"f64 CSR5 tile kernel must not emit shfl.sync.up.b64:\n{tile_f64}"
);
assert!(
tile_f64.contains("shfl.sync.up.b32"),
"f64 CSR5 tile kernel must reduce via paired b32 up-shuffles:\n{tile_f64}"
);
assert!(
!tile_f64.contains("0F00000000"),
"f64 CSR5 tile kernel must not materialize an f32 0.0 immediate:\n{tile_f64}"
);
let cal_f32 = emit_csr5_calibrate_kernel::<f32>(SmVersion::Sm86).expect("f32 cal");
assert_assembles_and_clean("csr5_calibrate_f32", &cal_f32);
let cal_f64 = emit_csr5_calibrate_kernel::<f64>(SmVersion::Sm86).expect("f64 cal");
assert_assembles_and_clean("csr5_calibrate_f64", &cal_f64);
}
#[test]
fn csr5_tile_ptx_generates_f32() {
let ptx = emit_csr5_tile_kernel::<f32>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains(".entry csr5_tile"));
assert!(ptx_text.contains(".target sm_80"));
}
#[test]
fn csr5_tile_ptx_generates_f64() {
let ptx = emit_csr5_tile_kernel::<f64>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains(".entry csr5_tile"));
}
#[test]
fn csr5_calibrate_ptx_generates_f32() {
let ptx = emit_csr5_calibrate_kernel::<f32>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains(".entry csr5_calibrate"));
}
#[test]
fn csr5_calibrate_ptx_generates_f64() {
let ptx = emit_csr5_calibrate_kernel::<f64>(SmVersion::Sm80);
assert!(ptx.is_ok());
}
#[test]
fn csr5_tile_ptx_contains_segmented_reduction() {
let ptx = emit_csr5_tile_kernel::<f32>(SmVersion::Sm80);
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains("shfl.sync.up"));
assert!(ptx_text.contains("popc.b32"));
}
#[test]
fn csr5_tile_ptx_contains_atomic_add() {
let ptx = emit_csr5_tile_kernel::<f32>(SmVersion::Sm80);
let ptx_text = ptx.expect("test: PTX gen should succeed");
assert!(ptx_text.contains("atom.global.add"));
}
#[test]
fn csr5_block_sizes_are_warp_aligned() {
assert_eq!(CSR5_TILE_BLOCK % 32, 0);
assert_eq!(CSR5_CALIBRATE_BLOCK % 32, 0);
}
}