1use 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
12pub const SPARSE_MAX_ROW_FILL: f64 = 0.5;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct SparseRows {
19 pub channels: usize,
20 pub width: usize,
22 pub values: Vec<f32>,
24 pub indices: Vec<u32>,
26}
27
28impl SparseRows {
29 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 pub fn fill(&self) -> f64 {
50 self.width as f64 / self.channels.max(1) as f64
51 }
52}
53
54#[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
84pub 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#[derive(Debug, Clone)]
102pub enum DeviceSpatialMatrix {
103 Dense { weights: Handle },
104 Sparse { values: Handle, indices: Handle, width: usize },
105}
106
107impl DeviceSpatialMatrix {
108 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 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}