#![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::*;
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(launch_unchecked)]
pub fn csc_to_csr_scatter_column<F: Numeric>(
col_indptr: &Tensor<u32>,
row_idx: &Tensor<u32>,
values: &Tensor<F>,
cursor: &mut Tensor<u32>,
out_indices: &mut Tensor<u32>,
out_values: &mut Tensor<F>,
col: u32,
len: u32,
) {
let t = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * CUBE_DIM_X + UNIT_POS_X;
if t >= len {
terminate!();
}
let p = col_indptr[col as usize] + t;
let row = row_idx[p as usize];
let slot = cursor[row as usize];
out_indices[slot as usize] = col;
out_values[slot as usize] = values[p as usize];
cursor[row as usize] = slot + 1u32;
}
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,
});
}
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)?;
for col in 0..m {
let len = host_col_indptr[col + 1] - host_col_indptr[col];
if len == 0 {
continue;
}
let (gx, gy) = grid_2d(len.div_ceil(WORKGROUP_256), &limits)?;
let count = checked_cube_count("csc_to_csr_scatter_column", gx, gy, 1, &limits)?;
unsafe {
csc_to_csr_scatter_column::launch_unchecked::<F, R>(
client,
count,
CubeDim::new_1d(WORKGROUP_256),
csc.indptr.clone().into_tensor_arg(),
csc.indices.clone().into_tensor_arg(),
csc.values.clone().into_tensor_arg(),
cursor.clone().into_tensor_arg(),
indices.clone().into_tensor_arg(),
values.clone().into_tensor_arg(),
col as u32,
len,
);
}
}
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)
}
#[test]
fn test_csc_to_csr_gpu_matches_host() {
let Some(device) = try_device() else { return };
let client = WgpuRuntime::client(&device);
let (n, m) = (300usize, 40usize);
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(&csc_gpu, &indptr, &indices, &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()
);
}
}