use cubecl::prelude::*;
use cubecl::server::Handle;
use dspk_core::compute::LaunchGeometry;
use super::kernels::sparse_rows_multiply_kernel;
use crate::core::{buffer, cast_f32, DspFloat};
use crate::linalg::{matmul, MatrixView};
pub const SPARSE_MAX_ROW_FILL: f64 = 0.5;
#[derive(Debug, Clone, PartialEq)]
pub struct SparseRows {
pub channels: usize,
pub width: usize,
pub values: Vec<f32>,
pub indices: Vec<u32>,
}
impl SparseRows {
pub fn from_dense(matrix: &[f32], channels: usize) -> Self {
assert_eq!(matrix.len(), channels * channels);
let rows: Vec<Vec<(u32, f32)>> = matrix
.chunks(channels.max(1))
.map(|row| row.iter().enumerate().filter(|(_, w)| **w != 0.0).map(|(i, w)| (i as u32, *w)).collect())
.collect();
let width = rows.iter().map(Vec::len).max().unwrap_or(0).max(1);
let mut values = vec![0.0f32; channels * width];
let mut indices = vec![0u32; channels * width];
for (o, row) in rows.iter().enumerate() {
for (k, &(i, w)) in row.iter().enumerate() {
values[o * width + k] = w;
indices[o * width + k] = i;
}
}
Self { channels, width, values, indices }
}
pub fn fill(&self) -> f64 {
self.width as f64 / self.channels.max(1) as f64
}
}
#[allow(clippy::too_many_arguments)]
pub fn execute_sparse_rows_multiply<F: DspFloat>(
client: &Client,
input: &Handle,
values: &Handle,
indices: &Handle,
output: &Handle,
channels: usize,
samples: usize,
width: usize,
) {
let geom = LaunchGeometry::channels_samples(client, channels, samples);
let total = channels * samples;
unsafe {
sparse_rows_multiply_kernel::launch::<F>(
client,
geom.cube_count,
geom.cube_dim,
BufferArg::from_raw_parts(input.clone(), total),
BufferArg::from_raw_parts(values.clone(), channels * width),
BufferArg::from_raw_parts(indices.clone(), channels * width),
BufferArg::from_raw_parts(output.clone(), total),
channels as u32,
samples as u32,
width as u32,
);
}
}
pub fn execute_spatial_matrix_multiply<F: DspFloat>(
client: &Client,
input: &Handle,
weights: &Handle,
output: &Handle,
channels: usize,
samples: usize,
) {
let w = MatrixView::row_major(weights, channels * channels, channels, channels);
let x = MatrixView::row_major(input, channels * samples, channels, samples);
matmul::<F>(client, &w, &x, output, channels * samples);
}
#[derive(Debug, Clone)]
pub enum DeviceSpatialMatrix {
Dense { weights: Handle },
Sparse { values: Handle, indices: Handle, width: usize },
}
impl DeviceSpatialMatrix {
pub fn upload<F: DspFloat>(client: &Client, matrix: &[f32], channels: usize) -> Self {
let sparse = SparseRows::from_dense(matrix, channels);
if sparse.fill() <= SPARSE_MAX_ROW_FILL {
Self::Sparse {
values: buffer::upload(client, &cast_f32::<F>(&sparse.values)),
indices: buffer::upload(client, &sparse.indices),
width: sparse.width,
}
} else {
Self::Dense { weights: buffer::upload(client, &cast_f32::<F>(matrix)) }
}
}
pub fn apply<F: DspFloat>(&self, client: &Client, input: &Handle, output: &Handle, channels: usize, samples: usize) {
match self {
Self::Dense { weights } => execute_spatial_matrix_multiply::<F>(client, input, weights, output, channels, samples),
Self::Sparse { values, indices, width } => {
execute_sparse_rows_multiply::<F>(client, input, values, indices, output, channels, samples, *width)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spatial::SurfaceLaplacian;
fn sparse_matches_dense(client: &Client) {
let lap = SurfaceLaplacian::from_grid_2d(4, 8);
let (channels, samples) = (32usize, 257usize);
let x: Vec<f32> = (0..channels * samples).map(|i| ((i * 7919) % 211) as f32 - 100.0).collect();
let input = buffer::upload(client, &x);
let op = DeviceSpatialMatrix::upload::<f32>(client, &lap.matrix, channels);
assert!(matches!(op, DeviceSpatialMatrix::Sparse { width: 5, .. }), "a 5-point Laplacian is stored as 5-wide rows");
let out = buffer::empty::<f32>(client, x.len());
op.apply::<f32>(client, &input, &out, channels, samples);
let got = buffer::download::<f32>(client, out);
let want = lap.apply_cpu(&x, channels, samples);
for (g, w) in got.iter().zip(&want) {
assert!((g - w).abs() < 1e-3, "{}: {g} vs {w}", client.name());
}
}
runtime_test!(test_sparse_rows_match_dense, sparse_matches_dense);
}