#![allow(missing_docs)]
use cubecl::Runtime;
use cubecl::prelude::*;
use cubecl_utils_rs::prelude::*;
use rayon::prelude::*;
use crate::gpu::WORKGROUP_256;
use crate::prelude::*;
const MASK_BITS: usize = 32;
const CSR_BUILD_BUDGET_BYTES: usize = 256 << 20;
pub struct GpuCompressedSparseData<R, F>
where
R: Runtime,
F: cubecl::CubeElement + cubecl::prelude::Numeric,
{
pub indptr: GpuTensor<R, u32>,
pub indices: GpuTensor<R, u32>,
pub values: GpuTensor<R, F>,
pub cs_type: CompressedSparseFormat,
pub shape: (usize, usize),
pub nnz: usize,
}
impl<R, F> Clone for GpuCompressedSparseData<R, F>
where
R: Runtime,
F: cubecl::CubeElement + cubecl::prelude::Numeric,
{
fn clone(&self) -> Self {
Self {
indptr: self.indptr.clone(),
indices: self.indices.clone(),
values: self.values.clone(),
cs_type: self.cs_type,
shape: self.shape,
nnz: self.nnz,
}
}
}
impl<R, F> GpuCompressedSparseData<R, F>
where
R: Runtime,
F: cubecl::CubeElement + cubecl::prelude::Numeric,
{
pub fn from_parts(
values: &[F],
indices: &[u32],
indptr: &[u32],
cs_type: CompressedSparseFormat,
shape: (usize, usize),
client: &ComputeClient<R>,
) -> Result<Self, BixverseErrors> {
let nnz = values.len();
Ok(Self {
indptr: GpuTensor::from_slice(indptr, vec![indptr.len()], client)?,
indices: GpuTensor::from_slice(indices, vec![nnz], client)?,
values: GpuTensor::from_slice(values, vec![nnz], client)?,
cs_type,
shape,
nnz,
})
}
pub fn from_compressed_sparse_data_2<T, U>(
src: &CompressedSparseData2<T, U>,
use_second_layer: bool,
client: &ComputeClient<R>,
) -> Result<Self, BixverseErrors>
where
T: BixverseNumeric + Into<F>,
U: BixverseNumeric + Into<F>,
{
let values: Vec<F> = if use_second_layer {
let layer = src
.data_2
.as_ref()
.ok_or(BixverseErrors::Data2NotAvailable)?;
layer.iter().copied().map(Into::into).collect()
} else {
src.data.iter().copied().map(Into::into).collect()
};
Self::from_parts(
&values,
&src.indices,
&src.indptr,
src.cs_type,
src.shape,
client,
)
}
pub fn vram_bytes(&self) -> usize {
self.indptr.vram_bytes() + self.indices.vram_bytes() + self.values.vram_bytes()
}
}
#[cube]
fn column_of_position(col_indptr: &Tensor<u32>, p: u32, col_lo: u32, col_hi: u32) -> u32 {
let mut lo = col_lo;
let mut hi = col_hi;
while hi - lo > 1u32 {
let mid = (lo + hi) / 2u32;
if col_indptr[mid as usize] <= p {
lo = mid;
} else {
hi = mid;
}
}
lo
}
#[cube(launch_unchecked)]
pub fn csr_zero_masks(buf: &mut Tensor<Atomic<u32>>, len: u32) {
let t = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * CUBE_DIM_X + UNIT_POS_X;
if t >= len {
terminate!();
}
Atomic::store(&buf[t as usize], 0u32);
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn csr_mark_columns(
col_indptr: &Tensor<u32>,
row_idx: &Tensor<u32>,
mask: &mut Tensor<Atomic<u32>>,
col_lo: u32,
col_hi: u32,
nnz_lo: u32,
nnz_len: u32,
n_rows: u32,
) {
let t = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * CUBE_DIM_X + UNIT_POS_X;
if t >= nnz_len {
terminate!();
}
let p = nnz_lo + t;
let local = column_of_position(col_indptr, p, col_lo, col_hi) - col_lo;
let row = row_idx[p as usize];
let slot = (local / 32u32) * n_rows + row;
Atomic::fetch_or(&mask[slot as usize], 1u32 << (local % 32u32));
}
#[cube(launch_unchecked)]
pub fn csr_rank_rows(
mask: &Tensor<Atomic<u32>>,
base: &mut Tensor<u32>,
cursor: &mut Tensor<u32>,
n_rows: u32,
words: u32,
) {
let row = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * CUBE_DIM_X + UNIT_POS_X;
if row >= n_rows {
terminate!();
}
let mut next = cursor[row as usize];
for w in 0..words {
let slot = (w * n_rows + row) as usize;
base[slot] = next;
next += u32::count_ones(Atomic::load(&mask[slot]));
}
cursor[row as usize] = next;
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn csr_scatter_group<F: Numeric>(
col_indptr: &Tensor<u32>,
row_idx: &Tensor<u32>,
values: &Tensor<F>,
mask: &Tensor<Atomic<u32>>,
base: &Tensor<u32>,
out_indices: &mut Tensor<u32>,
out_values: &mut Tensor<F>,
col_lo: u32,
col_hi: u32,
nnz_lo: u32,
nnz_len: u32,
n_rows: u32,
) {
let t = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * CUBE_DIM_X + UNIT_POS_X;
if t >= nnz_len {
terminate!();
}
let p = nnz_lo + t;
let col = column_of_position(col_indptr, p, col_lo, col_hi);
let local = col - col_lo;
let row = row_idx[p as usize];
let w_slot = ((local / 32u32) * n_rows + row) as usize;
let below = Atomic::load(&mask[w_slot]) & ((1u32 << (local % 32u32)) - 1u32);
let slot = base[w_slot] + u32::count_ones(below);
out_indices[slot as usize] = col;
out_values[slot as usize] = values[p as usize];
}
fn csr_indptr_from_csc_rows(row_idx: &[u32], n_rows: usize) -> Vec<u32> {
let n_threads = rayon::current_num_threads().max(1);
let chunk = row_idx.len().div_ceil(n_threads).max(1);
let counts = row_idx
.par_chunks(chunk)
.map(|slice| {
let mut c = vec![0u32; n_rows];
for &r in slice {
c[r as usize] += 1;
}
c
})
.reduce(
|| vec![0u32; n_rows],
|mut a, b| {
a.iter_mut().zip(b.iter()).for_each(|(x, y)| *x += y);
a
},
);
let mut indptr = Vec::with_capacity(n_rows + 1);
let mut acc = 0u32;
indptr.push(0);
for c in counts {
acc += c;
indptr.push(acc);
}
indptr
}
pub fn csc_to_csr_gpu<R, F>(
csc: &GpuCompressedSparseData<R, F>,
host_col_indptr: &[u32],
host_row_idx: &[u32],
client: &ComputeClient<R>,
) -> Result<GpuCompressedSparseData<R, F>, BixverseErrors>
where
R: Runtime,
F: cubecl::CubeElement + Numeric,
{
if !csc.cs_type.is_csc() {
return Err(BixverseErrors::SparseLayoutMismatch {
expected: CompressedSparseFormat::Csc,
got: csc.cs_type,
});
}
csc_to_csr_gpu_grouped(
csc,
host_col_indptr,
host_row_idx,
CSR_BUILD_BUDGET_BYTES,
client,
)
}
fn csc_to_csr_gpu_grouped<R, F>(
csc: &GpuCompressedSparseData<R, F>,
host_col_indptr: &[u32],
host_row_idx: &[u32],
budget_bytes: usize,
client: &ComputeClient<R>,
) -> Result<GpuCompressedSparseData<R, F>, BixverseErrors>
where
R: Runtime,
F: cubecl::CubeElement + Numeric,
{
let (n, m) = csc.shape;
let limits = GpuLimits::from_client(client);
let row_ptr = csr_indptr_from_csc_rows(host_row_idx, n);
let indptr = GpuTensor::<R, u32>::from_slice(&row_ptr, vec![n + 1], client)?;
let cursor = GpuTensor::<R, u32>::from_slice(&row_ptr[..n], vec![n], client)?;
let indices = GpuTensor::<R, u32>::empty(vec![csc.nnz], client)?;
let values = GpuTensor::<R, F>::empty(vec![csc.nnz], client)?;
let total_words = m.div_ceil(MASK_BITS);
let group_words = (budget_bytes / (8 * n.max(1))).clamp(1, total_words.max(1));
let mask = GpuTensor::<R, u32>::empty(vec![group_words * n.max(1)], client)?;
let base = GpuTensor::<R, u32>::empty(vec![group_words * n.max(1)], client)?;
let wg = CubeDim::new_1d(WORKGROUP_256);
for first_word in (0..total_words).step_by(group_words) {
let col_lo = first_word * MASK_BITS;
let col_hi = ((first_word + group_words) * MASK_BITS).min(m);
let words = (col_hi - col_lo).div_ceil(MASK_BITS);
let nnz_lo = host_col_indptr[col_lo];
let nnz_len = host_col_indptr[col_hi] - nnz_lo;
if nnz_len == 0 {
continue;
}
let mask_len = (words * n) as u32;
let (zx, zy) = grid_2d(mask_len.div_ceil(WORKGROUP_256), &limits)?;
let zero_count = checked_cube_count("csr_zero_masks", zx, zy, 1, &limits)?;
let (gx, gy) = grid_2d(nnz_len.div_ceil(WORKGROUP_256), &limits)?;
let nnz_count = checked_cube_count("csr_scatter_group", gx, gy, 1, &limits)?;
let (rx, ry) = grid_2d((n as u32).div_ceil(WORKGROUP_256), &limits)?;
let row_count = checked_cube_count("csr_rank_rows", rx, ry, 1, &limits)?;
unsafe {
csr_zero_masks::launch_unchecked::<R>(
client,
zero_count,
wg,
mask.clone().into_tensor_arg(),
mask_len,
);
csr_mark_columns::launch_unchecked::<R>(
client,
nnz_count.clone(),
wg,
csc.indptr.clone().into_tensor_arg(),
csc.indices.clone().into_tensor_arg(),
mask.clone().into_tensor_arg(),
col_lo as u32,
col_hi as u32,
nnz_lo,
nnz_len,
n as u32,
);
csr_rank_rows::launch_unchecked::<R>(
client,
row_count,
wg,
mask.clone().into_tensor_arg(),
base.clone().into_tensor_arg(),
cursor.clone().into_tensor_arg(),
n as u32,
words as u32,
);
csr_scatter_group::launch_unchecked::<F, R>(
client,
nnz_count,
wg,
csc.indptr.clone().into_tensor_arg(),
csc.indices.clone().into_tensor_arg(),
csc.values.clone().into_tensor_arg(),
mask.clone().into_tensor_arg(),
base.clone().into_tensor_arg(),
indices.clone().into_tensor_arg(),
values.clone().into_tensor_arg(),
col_lo as u32,
col_hi as u32,
nnz_lo,
nnz_len,
n as u32,
);
}
}
Ok(GpuCompressedSparseData {
indptr,
indices,
values,
cs_type: CompressedSparseFormat::Csr,
shape: (n, m),
nnz: csc.nnz,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::math::sparse::transpose_sparse_single_layer;
use cubecl::wgpu::{WgpuDevice, WgpuRuntime};
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::DefaultDevice;
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
WgpuRuntime::client(&device);
}))
.ok()
.map(|_| device)
}
fn check_csc_to_csr(n: usize, m: usize, budget_bytes: usize) {
let Some(device) = try_device() else { return };
let client = WgpuRuntime::client(&device);
let mut values = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0u32];
for j in 0..m {
for i in 0..n {
if j != 7 && i % 50 != 0 && (i * 3 + j * 7) % 5 == 0 {
values.push((i * 31 + j) as f32 * 0.01);
indices.push(i as u32);
}
}
indptr.push(values.len() as u32);
}
let csc = CompressedSparseData2::<f32, f32>::new_csc(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let want = transpose_sparse_single_layer(&csc, true).unwrap();
let csc_gpu = GpuCompressedSparseData::<WgpuRuntime, f32>::from_parts(
&values,
&indices,
&indptr,
CompressedSparseFormat::Csc,
(n, m),
&client,
)
.unwrap();
let got =
csc_to_csr_gpu_grouped(&csc_gpu, &indptr, &indices, budget_bytes, &client).unwrap();
assert!(got.cs_type.is_csr());
assert_eq!(got.indptr.read(&client).unwrap(), want.indptr);
assert_eq!(got.indices.read(&client).unwrap(), want.indices);
assert_eq!(
got.values.read(&client).unwrap(),
*want.data_2.as_ref().unwrap()
);
}
#[test]
fn test_csc_to_csr_gpu_matches_host() {
check_csc_to_csr(300, 40, CSR_BUILD_BUDGET_BYTES);
}
#[test]
fn test_csc_to_csr_gpu_column_groups() {
let n = 300;
check_csc_to_csr(n, 100, 8 * n);
}
}