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(),
beta.to_bits_u64(),
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();
KernelBuilder::new("csr5_calibrate")
.target(sm)
.param("y_ptr", PtxType::U64)
.param("calibrator_ptr", PtxType::U64)
.param("beta_bits", 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 beta_bits = b.load_param_u64("beta_bits");
let beta = reinterpret_bits_to_float::<T>(b, beta_bits);
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 beta_y = mul_float::<T>(b, beta, y_val);
let result = add_float::<T>(b, cal_val, 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 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);
}
}
#[cfg(all(test, feature = "gpu-tests"))]
mod gpu_device_tests {
use super::*;
use crate::format::CsrMatrix;
use crate::gpu_test_support::{assert_close, gpu_handle};
use crate::host_csr::{f64_to_gpu, gpu_to_f64};
use oxicuda_memory::DeviceBuffer;
fn cpu_csr_spmv(
row_ptr: &[i32],
col_idx: &[i32],
values: &[f64],
x: &[f64],
y0: &[f64],
alpha: f64,
beta: f64,
) -> Vec<f64> {
let rows = row_ptr.len() - 1;
let mut y = vec![0.0_f64; rows];
for (i, slot) in y.iter_mut().enumerate() {
let mut acc = 0.0_f64;
for k in row_ptr[i] as usize..row_ptr[i + 1] as usize {
acc += values[k] * x[col_idx[k] as usize];
}
*slot = alpha * acc + beta * y0[i];
}
y
}
#[allow(clippy::too_many_arguments)]
fn run_csr5<T: GpuFloat>(
rows: u32,
cols: 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 csr = CsrMatrix::<T>::from_host(rows, cols, row_ptr, col_idx, &dev_values)
.expect("test: build CSR");
let csr5 = Csr5Matrix::<T>::from_csr(&csr).expect("test: build CSR5");
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");
csr5_spmv::<T>(
&handle,
&csr5,
&x_buf,
&mut y_buf,
f64_to_gpu::<T>(alpha),
f64_to_gpu::<T>(beta),
)
.expect("test: csr5_spmv 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_csr_spmv(row_ptr, col_idx, values, x, y0, alpha, beta);
assert_close(&got, &want, tol, tag);
}
fn tridiagonal(n: usize) -> (u32, u32, Vec<i32>, Vec<i32>, Vec<f64>) {
let mut row_ptr = vec![0i32];
let mut col_idx = Vec::new();
let mut values = Vec::new();
for i in 0..n {
if i > 0 {
col_idx.push((i - 1) as i32);
values.push(-1.0);
}
col_idx.push(i as i32);
values.push(4.0 + 0.01 * (i as f64));
if i + 1 < n {
col_idx.push((i + 1) as i32);
values.push(-1.0);
}
row_ptr.push(col_idx.len() as i32);
}
(n as u32, n as u32, row_ptr, col_idx, values)
}
#[test]
fn csr5_single_tile_f64_beta_one() {
let (r, c, rp, ci, v) = tridiagonal(10);
let x: Vec<f64> = (0..r as usize).map(|i| 1.0 + i as f64).collect();
let y0 = vec![0.0_f64; r as usize];
run_csr5::<f64>(
r,
c,
&rp,
&ci,
&v,
&x,
&y0,
1.0,
1.0,
1e-10,
"csr5_f64_single",
);
}
#[test]
fn csr5_cross_tile_f64_beta_nonunit() {
let (r, c, rp, ci, v) = tridiagonal(20);
let x: Vec<f64> = (0..r as usize).map(|i| 0.5 + 0.25 * i as f64).collect();
let y0: Vec<f64> = (0..r as usize).map(|i| 100.0 - i as f64).collect();
run_csr5::<f64>(
r,
c,
&rp,
&ci,
&v,
&x,
&y0,
2.0,
-0.5,
1e-10,
"csr5_f64_cross",
);
}
#[test]
fn csr5_cross_tile_f32_alpha_beta() {
let (r, c, rp, ci, v) = tridiagonal(20);
let x: Vec<f64> = (0..r as usize).map(|i| 1.0 + 0.1 * i as f64).collect();
let y0: Vec<f64> = (0..r as usize).map(|i| 5.0 + i as f64).collect();
run_csr5::<f32>(
r,
c,
&rp,
&ci,
&v,
&x,
&y0,
1.5,
0.25,
1e-4,
"csr5_f32_cross",
);
}
#[test]
fn csr5_beta_zero_overwrites() {
let (r, c, rp, ci, v) = tridiagonal(12);
let x = vec![1.0_f64; r as usize];
let y0 = vec![1e9_f64; r as usize];
run_csr5::<f64>(r, c, &rp, &ci, &v, &x, &y0, 1.0, 0.0, 1e-10, "csr5_beta0");
}
}