#![allow(dead_code)]
use std::sync::Arc;
use oxicuda_blas::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::{fma_float, load_float_imm, load_global_float, store_global_float};
const SPGEMM_BLOCK_SIZE: u32 = 256;
const HASH_TABLE_SIZE: u32 = 512;
pub fn spgemm_symbolic<T: GpuFloat>(
handle: &SparseHandle,
a: &CsrMatrix<T>,
b: &CsrMatrix<T>,
) -> SparseResult<Vec<i32>> {
validate_spgemm_dims(a, b)?;
let m = a.rows();
if m == 0 {
return Ok(vec![0]);
}
let d_row_nnz = oxicuda_memory::DeviceBuffer::<i32>::zeroed(m as usize)?;
let ptx = emit_spgemm_symbolic_kernel::<T>(handle.sm_version())?;
let module = Arc::new(Module::from_ptx(&ptx)?);
let kernel = Kernel::from_module(module, "spgemm_symbolic")?;
let block_size = SPGEMM_BLOCK_SIZE;
let grid_size = grid_size_for(m, 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(),
b.row_ptr().as_device_ptr(),
b.col_idx().as_device_ptr(),
d_row_nnz.as_device_ptr(),
m,
b.cols(),
),
)?;
let mut h_row_nnz = vec![0i32; m as usize];
d_row_nnz.copy_to_host(&mut h_row_nnz)?;
let mut row_ptr = vec![0i32; m as usize + 1];
for i in 0..m as usize {
row_ptr[i + 1] = row_ptr[i] + h_row_nnz[i];
}
Ok(row_ptr)
}
#[allow(clippy::too_many_arguments)]
pub fn spgemm_numeric<T: GpuFloat>(
handle: &SparseHandle,
a: &CsrMatrix<T>,
b: &CsrMatrix<T>,
c_row_ptr: CUdeviceptr,
c_col_idx: CUdeviceptr,
c_values: CUdeviceptr,
) -> SparseResult<()> {
validate_spgemm_dims(a, b)?;
let m = a.rows();
if m == 0 {
return Ok(());
}
let ptx = emit_spgemm_numeric_kernel::<T>(handle.sm_version())?;
let module = Arc::new(Module::from_ptx(&ptx)?);
let kernel = Kernel::from_module(module, "spgemm_numeric")?;
let block_size = SPGEMM_BLOCK_SIZE;
let grid_size = grid_size_for(m, 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.row_ptr().as_device_ptr(),
b.col_idx().as_device_ptr(),
b.values().as_device_ptr(),
c_row_ptr,
c_col_idx,
c_values,
m,
b.cols(),
),
)?;
Ok(())
}
fn validate_spgemm_dims<T: GpuFloat>(a: &CsrMatrix<T>, b: &CsrMatrix<T>) -> SparseResult<()> {
if a.cols() != b.rows() {
return Err(SparseError::DimensionMismatch(format!(
"A.cols ({}) != B.rows ({})",
a.cols(),
b.rows()
)));
}
Ok(())
}
fn emit_spgemm_symbolic_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
let _ = T::PTX_TYPE;
KernelBuilder::new("spgemm_symbolic")
.target(sm)
.param("a_row_ptr", PtxType::U64)
.param("a_col_idx", PtxType::U64)
.param("b_row_ptr", PtxType::U64)
.param("b_col_idx", PtxType::U64)
.param("row_nnz", PtxType::U64)
.param("m", PtxType::U32)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let m_param = b.load_param_u32("m");
let gid_inner = gid.clone();
b.if_lt_u32(gid, m_param, move |b| {
let row = gid_inner;
let a_row_ptr = b.load_param_u64("a_row_ptr");
let a_col_idx = b.load_param_u64("a_col_idx");
let b_row_ptr = b.load_param_u64("b_row_ptr");
let _b_col_idx = b.load_param_u64("b_col_idx");
let row_nnz_ptr = b.load_param_u64("row_nnz");
let a_rs_addr = b.byte_offset_addr(a_row_ptr.clone(), row.clone(), 4);
let a_rs_i32 = b.load_global_i32(a_rs_addr);
let a_rs = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_rs}, {a_rs_i32};"));
let row_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
let a_re_addr = b.byte_offset_addr(a_row_ptr, row_p1, 4);
let a_re_i32 = b.load_global_i32(a_re_addr);
let a_re = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_re}, {a_re_i32};"));
let count = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {count}, 0;"));
let a_k = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {a_k}, {a_rs};"));
let outer_loop = b.fresh_label("spgemm_sym_outer");
let outer_done = b.fresh_label("spgemm_sym_outer_done");
b.label(&outer_loop);
let a_pred = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {a_pred}, {a_k}, {a_re};"));
b.branch_if(a_pred, &outer_done);
let a_ci_addr = b.byte_offset_addr(a_col_idx.clone(), a_k.clone(), 4);
let a_col_i32 = b.load_global_i32(a_ci_addr);
let a_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
let b_rs_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col.clone(), 4);
let b_rs_i32 = b.load_global_i32(b_rs_addr);
let b_rs = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {b_rs}, {b_rs_i32};"));
let a_col_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {a_col_p1}, {a_col}, 1;"));
let b_re_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col_p1, 4);
let b_re_i32 = b.load_global_i32(b_re_addr);
let b_re = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {b_re}, {b_re_i32};"));
let b_j = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {b_j}, {b_rs};"));
let inner_loop = b.fresh_label("spgemm_sym_inner");
let inner_done = b.fresh_label("spgemm_sym_inner_done");
b.label(&inner_loop);
let b_pred = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {b_pred}, {b_j}, {b_re};"));
b.branch_if(b_pred, &inner_done);
b.raw_ptx(&format!("add.u32 {count}, {count}, 1;"));
b.raw_ptx(&format!("add.u32 {b_j}, {b_j}, 1;"));
b.branch(&inner_loop);
b.label(&inner_done);
b.raw_ptx(&format!("add.u32 {a_k}, {a_k}, 1;"));
b.branch(&outer_loop);
b.label(&outer_done);
let out_addr = b.byte_offset_addr(row_nnz_ptr, row, 4);
b.store_global_i32(out_addr, count);
});
b.ret();
})
.build()
.map_err(|e| SparseError::PtxGeneration(e.to_string()))
}
fn emit_spgemm_numeric_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
let elem_bytes = T::size_u32();
let _is_f64 = T::SIZE == 8;
KernelBuilder::new("spgemm_numeric")
.target(sm)
.param("a_row_ptr", PtxType::U64)
.param("a_col_idx", PtxType::U64)
.param("a_values", PtxType::U64)
.param("b_row_ptr", PtxType::U64)
.param("b_col_idx", PtxType::U64)
.param("b_values", PtxType::U64)
.param("c_row_ptr", PtxType::U64)
.param("c_col_idx", PtxType::U64)
.param("c_values", PtxType::U64)
.param("m", PtxType::U32)
.param("n", PtxType::U32)
.body(move |b| {
let gid = b.global_thread_id_x();
let m_param = b.load_param_u32("m");
let gid_inner = gid.clone();
b.if_lt_u32(gid, m_param, move |b| {
let row = gid_inner;
let a_row_ptr = b.load_param_u64("a_row_ptr");
let a_col_idx = b.load_param_u64("a_col_idx");
let a_values = b.load_param_u64("a_values");
let b_row_ptr = b.load_param_u64("b_row_ptr");
let b_col_idx_p = b.load_param_u64("b_col_idx");
let b_values = b.load_param_u64("b_values");
let c_row_ptr = b.load_param_u64("c_row_ptr");
let c_col_idx_p = b.load_param_u64("c_col_idx");
let c_values = b.load_param_u64("c_values");
let a_rs_addr = b.byte_offset_addr(a_row_ptr.clone(), row.clone(), 4);
let a_rs_i32 = b.load_global_i32(a_rs_addr);
let a_rs = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_rs}, {a_rs_i32};"));
let row_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
let a_re_addr = b.byte_offset_addr(a_row_ptr, row_p1, 4);
let a_re_i32 = b.load_global_i32(a_re_addr);
let a_re = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_re}, {a_re_i32};"));
let c_rs_addr = b.byte_offset_addr(c_row_ptr, row, 4);
let c_rs_i32 = b.load_global_i32(c_rs_addr);
let c_pos = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {c_pos}, {c_rs_i32};"));
let a_k = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {a_k}, {a_rs};"));
let outer_loop = b.fresh_label("spgemm_num_outer");
let outer_done = b.fresh_label("spgemm_num_outer_done");
b.label(&outer_loop);
let a_pred = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {a_pred}, {a_k}, {a_re};"));
b.branch_if(a_pred, &outer_done);
let a_ci_addr = b.byte_offset_addr(a_col_idx.clone(), a_k.clone(), 4);
let a_col_i32 = b.load_global_i32(a_ci_addr);
let a_col = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
let a_v_addr = b.byte_offset_addr(a_values.clone(), a_k.clone(), elem_bytes);
let a_val = load_global_float::<T>(b, a_v_addr);
let b_rs_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col.clone(), 4);
let b_rs_i32 = b.load_global_i32(b_rs_addr);
let b_rs = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {b_rs}, {b_rs_i32};"));
let a_col_p1 = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("add.u32 {a_col_p1}, {a_col}, 1;"));
let b_re_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col_p1, 4);
let b_re_i32 = b.load_global_i32(b_re_addr);
let b_re = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.b32 {b_re}, {b_re_i32};"));
let b_j = b.alloc_reg(PtxType::U32);
b.raw_ptx(&format!("mov.u32 {b_j}, {b_rs};"));
let inner_loop = b.fresh_label("spgemm_num_inner");
let inner_done = b.fresh_label("spgemm_num_inner_done");
b.label(&inner_loop);
let b_pred = b.alloc_reg(PtxType::Pred);
b.raw_ptx(&format!("setp.hs.u32 {b_pred}, {b_j}, {b_re};"));
b.branch_if(b_pred, &inner_done);
let b_ci_addr = b.byte_offset_addr(b_col_idx_p.clone(), b_j.clone(), 4);
let b_col_i32 = b.load_global_i32(b_ci_addr);
let b_v_addr = b.byte_offset_addr(b_values.clone(), b_j.clone(), elem_bytes);
let b_val = load_global_float::<T>(b, b_v_addr);
let zero = load_float_imm::<T>(b, 0.0);
let c_val = fma_float::<T>(b, a_val.clone(), b_val, zero);
let c_ci_addr = b.byte_offset_addr(c_col_idx_p.clone(), c_pos.clone(), 4);
b.store_global_i32(c_ci_addr, b_col_i32);
let c_v_addr = b.byte_offset_addr(c_values.clone(), c_pos.clone(), elem_bytes);
store_global_float::<T>(b, c_v_addr, c_val);
b.raw_ptx(&format!("add.u32 {c_pos}, {c_pos}, 1;"));
b.raw_ptx(&format!("add.u32 {b_j}, {b_j}, 1;"));
b.branch(&inner_loop);
b.label(&inner_done);
b.raw_ptx(&format!("add.u32 {a_k}, {a_k}, 1;"));
b.branch(&outer_loop);
b.label(&outer_done);
});
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;
use oxicuda_ptx::arch::SmVersion;
#[test]
fn spgemm_symbolic_numeric_f32_f64_assemble_sm86() {
let sym_f32 = emit_spgemm_symbolic_kernel::<f32>(SmVersion::Sm86).expect("sym f32");
assert_assembles_and_clean("spgemm_symbolic_f32", &sym_f32);
let sym_f64 = emit_spgemm_symbolic_kernel::<f64>(SmVersion::Sm86).expect("sym f64");
assert_assembles_and_clean("spgemm_symbolic_f64", &sym_f64);
let num_f32 = emit_spgemm_numeric_kernel::<f32>(SmVersion::Sm86).expect("num f32");
assert_assembles_and_clean("spgemm_numeric_f32", &num_f32);
let num_f64 = emit_spgemm_numeric_kernel::<f64>(SmVersion::Sm86).expect("num f64");
assert_assembles_and_clean("spgemm_numeric_f64", &num_f64);
assert!(
!num_f64.contains("0F00000000"),
"f64 SpGEMM numeric kernel must not materialize an f32 0.0 immediate:\n{num_f64}"
);
}
#[test]
fn spgemm_symbolic_ptx_generates_f32() {
let ptx = emit_spgemm_symbolic_kernel::<f32>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry spgemm_symbolic"));
}
#[test]
fn spgemm_symbolic_ptx_generates_f64() {
let ptx = emit_spgemm_symbolic_kernel::<f64>(SmVersion::Sm80);
assert!(ptx.is_ok());
}
#[test]
fn spgemm_numeric_ptx_generates_f32() {
let ptx = emit_spgemm_numeric_kernel::<f32>(SmVersion::Sm80);
assert!(ptx.is_ok());
let ptx_str = ptx.expect("test: PTX gen should succeed");
assert!(ptx_str.contains(".entry spgemm_numeric"));
}
#[test]
fn spgemm_numeric_ptx_generates_f64() {
let ptx = emit_spgemm_numeric_kernel::<f64>(SmVersion::Sm80);
assert!(ptx.is_ok());
}
#[test]
fn validate_dims_mismatch() {
let err = SparseError::DimensionMismatch("A.cols (3) != B.rows (4)".to_string());
assert!(err.to_string().contains("A.cols"));
}
}