Skip to main content

dspk_ops/spatial/
sparse.rs

1//! Spatial `[C, C]` operators on the device, stored dense or as sparse rows depending on how many
2//! weights are nonzero.
3
4use cubecl::prelude::*;
5use cubecl::server::Handle;
6use dspk_core::compute::LaunchGeometry;
7
8use super::kernels::sparse_rows_multiply_kernel;
9use crate::core::{buffer, cast_f32, DspFloat};
10use crate::linalg::{matmul, MatrixView};
11
12/// A matrix is stored as sparse rows when its widest row holds at most this fraction of the
13/// columns; denser matrices run the dense kernel (fewer index reads).
14pub const SPARSE_MAX_ROW_FILL: f64 = 0.5;
15
16/// Rows of a square matrix keeping only nonzero weights, padded to the widest row (ELLPACK).
17#[derive(Debug, Clone, PartialEq)]
18pub struct SparseRows {
19    pub channels: usize,
20    /// Entries per row (the widest row's nonzero count).
21    pub width: usize,
22    /// `[channels, width]` weights; padding entries are 0.
23    pub values: Vec<f32>,
24    /// `[channels, width]` input channel of each weight; padding entries point at channel 0.
25    pub indices: Vec<u32>,
26}
27
28impl SparseRows {
29    /// The nonzero entries of row-major `matrix` (`[channels, channels]`).
30    pub fn from_dense(matrix: &[f32], channels: usize) -> Self {
31        assert_eq!(matrix.len(), channels * channels);
32        let rows: Vec<Vec<(u32, f32)>> = matrix
33            .chunks(channels.max(1))
34            .map(|row| row.iter().enumerate().filter(|(_, w)| **w != 0.0).map(|(i, w)| (i as u32, *w)).collect())
35            .collect();
36        let width = rows.iter().map(Vec::len).max().unwrap_or(0).max(1);
37        let mut values = vec![0.0f32; channels * width];
38        let mut indices = vec![0u32; channels * width];
39        for (o, row) in rows.iter().enumerate() {
40            for (k, &(i, w)) in row.iter().enumerate() {
41                values[o * width + k] = w;
42                indices[o * width + k] = i;
43            }
44        }
45        Self { channels, width, values, indices }
46    }
47
48    /// Fraction of the columns the widest row holds.
49    pub fn fill(&self) -> f64 {
50        self.width as f64 / self.channels.max(1) as f64
51    }
52}
53
54/// `output = W · input` for a sparse-rows `W` (`values` / `indices` uploaded from [`SparseRows`]).
55#[allow(clippy::too_many_arguments)]
56pub fn execute_sparse_rows_multiply<F: DspFloat>(
57    client: &Client,
58    input: &Handle,
59    values: &Handle,
60    indices: &Handle,
61    output: &Handle,
62    channels: usize,
63    samples: usize,
64    width: usize,
65) {
66    let geom = LaunchGeometry::channels_samples(client, channels, samples);
67    let total = channels * samples;
68    unsafe {
69        sparse_rows_multiply_kernel::launch::<F>(
70            client,
71            geom.cube_count,
72            geom.cube_dim,
73            BufferArg::from_raw_parts(input.clone(), total),
74            BufferArg::from_raw_parts(values.clone(), channels * width),
75            BufferArg::from_raw_parts(indices.clone(), channels * width),
76            BufferArg::from_raw_parts(output.clone(), total),
77            channels as u32,
78            samples as u32,
79            width as u32,
80        );
81    }
82}
83
84/// `output = W · input` for a dense row-major `W` (`[channels, channels]`) on a `[channels, samples]`
85/// buffer of `F`: one matrix product ([`fn@crate::linalg::matmul`]).
86pub fn execute_spatial_matrix_multiply<F: DspFloat>(
87    client: &Client,
88    input: &Handle,
89    weights: &Handle,
90    output: &Handle,
91    channels: usize,
92    samples: usize,
93) {
94    let w = MatrixView::row_major(weights, channels * channels, channels, channels);
95    let x = MatrixView::row_major(input, channels * samples, channels, samples);
96    matmul::<F>(client, &w, &x, output, channels * samples);
97}
98
99/// A `[C, C]` spatial operator uploaded once, dense or as sparse rows (chosen by
100/// [`SPARSE_MAX_ROW_FILL`] from the matrix itself).
101#[derive(Debug, Clone)]
102pub enum DeviceSpatialMatrix {
103    Dense { weights: Handle },
104    Sparse { values: Handle, indices: Handle, width: usize },
105}
106
107impl DeviceSpatialMatrix {
108    /// Uploads row-major `matrix` (`[channels, channels]`) as `F`.
109    pub fn upload<F: DspFloat>(client: &Client, matrix: &[f32], channels: usize) -> Self {
110        let sparse = SparseRows::from_dense(matrix, channels);
111        if sparse.fill() <= SPARSE_MAX_ROW_FILL {
112            Self::Sparse {
113                values: buffer::upload(client, &cast_f32::<F>(&sparse.values)),
114                indices: buffer::upload(client, &sparse.indices),
115                width: sparse.width,
116            }
117        } else {
118            Self::Dense { weights: buffer::upload(client, &cast_f32::<F>(matrix)) }
119        }
120    }
121
122    /// `output = W · input` on a `[channels, samples]` buffer of `F`.
123    pub fn apply<F: DspFloat>(&self, client: &Client, input: &Handle, output: &Handle, channels: usize, samples: usize) {
124        match self {
125            Self::Dense { weights } => execute_spatial_matrix_multiply::<F>(client, input, weights, output, channels, samples),
126            Self::Sparse { values, indices, width } => {
127                execute_sparse_rows_multiply::<F>(client, input, values, indices, output, channels, samples, *width)
128            }
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::spatial::SurfaceLaplacian;
137
138    fn sparse_matches_dense(client: &Client) {
139        let lap = SurfaceLaplacian::from_grid_2d(4, 8);
140        let (channels, samples) = (32usize, 257usize);
141        let x: Vec<f32> = (0..channels * samples).map(|i| ((i * 7919) % 211) as f32 - 100.0).collect();
142        let input = buffer::upload(client, &x);
143
144        let op = DeviceSpatialMatrix::upload::<f32>(client, &lap.matrix, channels);
145        assert!(matches!(op, DeviceSpatialMatrix::Sparse { width: 5, .. }), "a 5-point Laplacian is stored as 5-wide rows");
146        let out = buffer::empty::<f32>(client, x.len());
147        op.apply::<f32>(client, &input, &out, channels, samples);
148        let got = buffer::download::<f32>(client, out);
149
150        let want = lap.apply_cpu(&x, channels, samples);
151        for (g, w) in got.iter().zip(&want) {
152            assert!((g - w).abs() < 1e-3, "{}: {g} vs {w}", client.name());
153        }
154    }
155    runtime_test!(test_sparse_rows_match_dense, sparse_matches_dense);
156}