use oxicuda_blas::GpuFloat;
use oxicuda_memory::DeviceBuffer;
use crate::error::{SparseError, SparseResult};
pub const CSR5_TILE_WIDTH: u32 = 32;
pub const CSR5_SIGMA: u32 = 32;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(C)]
pub struct TileDescriptor {
pub seg_mask: u32,
pub first_row: u32,
}
unsafe impl Send for TileDescriptor {}
unsafe impl Sync for TileDescriptor {}
pub struct Csr5Matrix<T: GpuFloat> {
rows: u32,
cols: u32,
nnz: u32,
num_tiles: u32,
row_ptr: DeviceBuffer<i32>,
col_idx: DeviceBuffer<i32>,
values: DeviceBuffer<T>,
tile_ptr: DeviceBuffer<u32>,
tile_desc: DeviceBuffer<TileDescriptor>,
calibrator: DeviceBuffer<T>,
}
impl<T: GpuFloat> Csr5Matrix<T> {
pub fn from_csr_host(
rows: u32,
cols: u32,
row_ptr: &[i32],
col_idx: &[i32],
values: &[T],
) -> SparseResult<Self> {
if rows == 0 || cols == 0 {
return Err(SparseError::InvalidFormat(
"rows and cols must be non-zero".to_string(),
));
}
if row_ptr.len() != rows as usize + 1 {
return Err(SparseError::InvalidFormat(format!(
"row_ptr length ({}) must be rows + 1 ({})",
row_ptr.len(),
rows as usize + 1
)));
}
if col_idx.len() != values.len() {
return Err(SparseError::InvalidFormat(format!(
"col_idx length ({}) must equal values length ({})",
col_idx.len(),
values.len()
)));
}
let nnz = col_idx.len() as u32;
if nnz == 0 {
return Err(SparseError::ZeroNnz);
}
let num_tiles = nnz.div_ceil(CSR5_TILE_WIDTH);
let mut h_tile_ptr = Vec::with_capacity(num_tiles as usize + 1);
for t in 0..=num_tiles {
h_tile_ptr.push((t * CSR5_TILE_WIDTH).min(nnz));
}
let mut h_tile_desc = Vec::with_capacity(num_tiles as usize);
for t in 0..num_tiles {
let tile_start = h_tile_ptr[t as usize];
let tile_end = h_tile_ptr[t as usize + 1];
let first_row = find_row_for_element(row_ptr, tile_start);
let mut seg_mask: u32 = 0;
for lane in 0..CSR5_TILE_WIDTH {
let elem_idx = tile_start + lane;
if elem_idx >= tile_end {
break;
}
let elem_row = find_row_for_element(row_ptr, elem_idx);
if lane == 0 {
} else {
let prev_row = find_row_for_element(row_ptr, elem_idx - 1);
if elem_row != prev_row {
seg_mask |= 1 << lane;
}
}
}
h_tile_desc.push(TileDescriptor {
seg_mask,
first_row,
});
}
let h_calibrator = vec![T::gpu_zero(); rows as usize];
let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
let d_col_idx = DeviceBuffer::from_host(col_idx)?;
let d_values = DeviceBuffer::from_host(values)?;
let d_tile_ptr = DeviceBuffer::from_host(&h_tile_ptr)?;
let d_tile_desc = DeviceBuffer::from_host(&h_tile_desc)?;
let d_calibrator = DeviceBuffer::from_host(&h_calibrator)?;
Ok(Self {
rows,
cols,
nnz,
num_tiles,
row_ptr: d_row_ptr,
col_idx: d_col_idx,
values: d_values,
tile_ptr: d_tile_ptr,
tile_desc: d_tile_desc,
calibrator: d_calibrator,
})
}
pub fn from_csr(csr: &super::CsrMatrix<T>) -> SparseResult<Self> {
let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
Self::from_csr_host(csr.rows(), csr.cols(), &h_row_ptr, &h_col_idx, &h_values)
}
#[inline]
pub fn rows(&self) -> u32 {
self.rows
}
#[inline]
pub fn cols(&self) -> u32 {
self.cols
}
#[inline]
pub fn nnz(&self) -> u32 {
self.nnz
}
#[inline]
pub fn num_tiles(&self) -> u32 {
self.num_tiles
}
#[inline]
pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
&self.row_ptr
}
#[inline]
pub fn col_idx(&self) -> &DeviceBuffer<i32> {
&self.col_idx
}
#[inline]
pub fn values(&self) -> &DeviceBuffer<T> {
&self.values
}
#[inline]
pub fn tile_ptr(&self) -> &DeviceBuffer<u32> {
&self.tile_ptr
}
#[inline]
pub fn tile_desc(&self) -> &DeviceBuffer<TileDescriptor> {
&self.tile_desc
}
#[inline]
pub fn calibrator(&self) -> &DeviceBuffer<T> {
&self.calibrator
}
pub fn tile_metadata_to_host(&self) -> SparseResult<(Vec<u32>, Vec<TileDescriptor>)> {
let mut h_tile_ptr = vec![0u32; self.tile_ptr.len()];
let mut h_tile_desc = vec![TileDescriptor::default(); self.tile_desc.len()];
self.tile_ptr.copy_to_host(&mut h_tile_ptr)?;
self.tile_desc.copy_to_host(&mut h_tile_desc)?;
Ok((h_tile_ptr, h_tile_desc))
}
}
fn find_row_for_element(row_ptr: &[i32], elem_idx: u32) -> u32 {
let elem = elem_idx as i32;
let num_rows = row_ptr.len() - 1;
let mut lo: usize = 0;
let mut hi: usize = num_rows;
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if row_ptr[mid] <= elem {
lo = mid;
} else {
hi = mid - 1;
}
}
lo as u32
}
#[cfg(test)]
mod tests {
use super::*;
struct Csr5CpuRef {
#[allow(dead_code)]
pub n_rows: usize,
#[allow(dead_code)]
pub nnz: usize,
pub tile_width: usize,
pub tile_count: usize,
pub tile_ptr: Vec<u32>,
}
impl Csr5CpuRef {
fn from_csr(n_rows: usize, row_ptr: &[i32], nnz: usize) -> Self {
let tile_width = CSR5_TILE_WIDTH as usize;
let tile_count = nnz.div_ceil(tile_width);
let mut tile_ptr = Vec::with_capacity(tile_count + 1);
for tile in 0..tile_count {
let first_elem = (tile * tile_width) as u32;
tile_ptr.push(find_row_for_element(row_ptr, first_elem));
}
tile_ptr.push(n_rows as u32);
Self {
n_rows,
nnz,
tile_width,
tile_count,
tile_ptr,
}
}
}
#[test]
fn csr5_tile_width() {
assert_eq!(CSR5_TILE_WIDTH, 32);
}
#[test]
fn find_row_for_element_basic() {
let row_ptr = [0i32, 1, 2, 3];
assert_eq!(find_row_for_element(&row_ptr, 0), 0);
assert_eq!(find_row_for_element(&row_ptr, 1), 1);
assert_eq!(find_row_for_element(&row_ptr, 2), 2);
}
#[test]
fn find_row_for_element_varying_density() {
let row_ptr = [0i32, 3, 4, 6];
assert_eq!(find_row_for_element(&row_ptr, 0), 0);
assert_eq!(find_row_for_element(&row_ptr, 1), 0);
assert_eq!(find_row_for_element(&row_ptr, 2), 0);
assert_eq!(find_row_for_element(&row_ptr, 3), 1);
assert_eq!(find_row_for_element(&row_ptr, 4), 2);
assert_eq!(find_row_for_element(&row_ptr, 5), 2);
}
#[test]
fn tile_descriptor_default() {
let td = TileDescriptor::default();
assert_eq!(td.seg_mask, 0);
assert_eq!(td.first_row, 0);
}
#[test]
fn csr5_tile_count_computation() {
let num_tiles = 10_u32.div_ceil(CSR5_TILE_WIDTH);
assert_eq!(num_tiles, 1);
let num_tiles = 64_u32.div_ceil(CSR5_TILE_WIDTH);
assert_eq!(num_tiles, 2);
let num_tiles = 33_u32.div_ceil(CSR5_TILE_WIDTH);
assert_eq!(num_tiles, 2);
}
#[test]
fn csr5_sigma_value() {
assert_eq!(CSR5_SIGMA, 32);
}
#[test]
fn csr5_tile_count_32_nnz() {
let n_rows = 32usize;
let row_ptr: Vec<i32> = (0..=32_i32).collect();
let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 32);
assert_eq!(mat.tile_count, 1, "32 nnz should yield exactly 1 tile");
}
#[test]
fn csr5_tile_count_33_nnz() {
let n_rows = 33usize;
let row_ptr: Vec<i32> = (0..=33_i32).collect();
let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 33);
assert_eq!(mat.tile_count, 2, "33 nnz should yield 2 tiles");
}
#[test]
fn csr5_tile_count_1024_nnz() {
let n_rows = 1024usize;
let row_ptr: Vec<i32> = (0..=1024_i32).collect();
let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 1024);
assert_eq!(mat.tile_count, 32, "1024 nnz should yield 32 tiles");
}
#[test]
fn csr5_tile_width_is_32() {
let n_rows = 4usize;
let row_ptr = vec![0i32, 1, 2, 3, 4];
let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 4);
assert_eq!(
mat.tile_width, 32,
"tile_width must always be 32 (warp size)"
);
}
#[test]
fn csr5_tile_ptr_maps_row_correctly() {
let n_rows = 4usize;
let row_ptr = vec![0i32, 1, 2, 3, 4];
let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 4);
assert_eq!(mat.tile_count, 1);
assert_eq!(
mat.tile_ptr[0], 0,
"tile 0 starts at row 0 for a 4x4 identity matrix"
);
assert_eq!(
mat.tile_ptr[1], n_rows as u32,
"tile_ptr[tile_count] must equal n_rows"
);
}
}