Skip to main content

dspk_ops/spatial/kernels/
car.rs

1use cubecl::prelude::*;
2use dspk_core::compute::{element_position, sample_position};
3
4use crate::core::kernels::{load_lanes, store_lanes};
5
6/// Single-pass Common Average Referencing: one unit per sample ([`fn@sample_position`]) averages that
7/// sample over all channels and subtracts the average, with no intermediate buffer. Units of a plane
8/// read consecutive samples of one channel at a time (coalesced).
9#[cube(launch)]
10pub fn direct_car_kernel<F: Float>(input: &[F], output: &mut [F], num_channels: u32, num_samples: u32) {
11    let sample_idx = sample_position();
12
13    if sample_idx < num_samples {
14        let mut sum = F::new(0.0f32);
15        let mut c = 0u32;
16        while c < num_channels {
17            sum += input[(c * num_samples + sample_idx) as usize];
18            c += 1u32;
19        }
20
21        let avg = sum / F::cast_from(num_channels);
22
23        c = 0u32;
24        while c < num_channels {
25            let idx = (c * num_samples + sample_idx) as usize;
26            output[idx] = input[idx] - avg;
27            c += 1u32;
28        }
29    }
30}
31
32/// Common **median** reference: one cube per sample ([`fn@dspk_core::compute::row_position`]); its
33/// units load the sample's channels into shared memory, find the median by rank counting (each
34/// value's count of smaller and equal values; NumPy's median: the mean of the two middle values
35/// for an even count), and subtract it. `max_channels` (comptime) sizes the shared buffer.
36#[cube(launch)]
37pub fn common_median_kernel<F: Float>(
38    input: &[F],
39    output: &mut [F],
40    num_channels: u32,
41    num_samples: u32,
42    #[comptime] max_channels: u32,
43    #[comptime] units: u32,
44) {
45    let s = dspk_core::compute::row_position();
46    if s < num_samples {
47        let unit = UNIT_POS_X;
48        let mut col = Shared::<[F]>::new_slice(comptime!(max_channels as usize));
49        let mut mid = Shared::<[F]>::new_slice(2usize);
50        let mut c = unit;
51        while c < num_channels {
52            col[c as usize] = input[(c * num_samples + s) as usize];
53            c += units;
54        }
55        sync_cube();
56        let k1 = (num_channels - 1u32) / 2u32;
57        let k2 = num_channels / 2u32;
58        c = unit;
59        while c < num_channels {
60            let x = col[c as usize];
61            let mut less = 0u32;
62            let mut equal = 0u32;
63            let mut j = 0u32;
64            while j < num_channels {
65                let y = col[j as usize];
66                if y < x {
67                    less += 1u32;
68                }
69                if y == x {
70                    equal += 1u32;
71                }
72                j += 1u32;
73            }
74            if k1 >= less && k1 < less + equal {
75                mid[0] = x;
76            }
77            if k2 >= less && k2 < less + equal {
78                mid[1] = x;
79            }
80            c += units;
81        }
82        sync_cube();
83        let median = (mid[0] + mid[1]) * F::new(0.5f32);
84        c = unit;
85        while c < num_channels {
86            output[(c * num_samples + s) as usize] = col[c as usize] - median;
87            c += units;
88        }
89    }
90}
91
92/// Common median reference by **bitonic sort**: one cube per sample ([`fn@dspk_core::compute::row_position`]);
93/// its units load the sample's channels into a shared column padded to `padded` (a power of two)
94/// with the largest finite value (WGSL has no infinity literal; padding only ever sorts after the
95/// channels), sort it with Batcher's bitonic network (`log₂ P · (log₂ P + 1) / 2` steps, each unit
96/// doing `P / (2 · units)` compare-exchanges per step, one barrier per step), take the two middle
97/// values (NumPy's median) and subtract their mean from the original samples.
98///
99/// The alternative to [`fn@common_median_kernel`]'s rank counting (`C` comparisons per value, no
100/// barriers): the autotuner picks per device; barriers are cheap on GPUs and costly on the CPU
101/// runtime.
102#[cube(launch)]
103pub fn common_median_bitonic_kernel<F: Float>(
104    input: &[F],
105    output: &mut [F],
106    num_channels: u32,
107    num_samples: u32,
108    #[comptime] padded: u32,
109    #[comptime] units: u32,
110) {
111    let s = dspk_core::compute::row_position();
112    // Shared memory declared outside any branch (CubeCL lowering)
113    let mut col = Shared::<[F]>::new_slice(comptime!(padded as usize));
114    // `s` is uniform across the cube, so every unit takes the same branch
115    if s < num_samples {
116        let unit = UNIT_POS_X;
117        let mut i = unit;
118        while i < padded {
119            // A statement, not an `if` expression: both arms of a select would be evaluated, and
120            // the read is out of bounds for padding rows
121            let mut v = F::max_value();
122            if i < num_channels {
123                v = input[(i * num_samples + s) as usize];
124            }
125            col[i as usize] = v;
126            i += units;
127        }
128        sync_cube();
129        // Runtime counters: a `let mut` from a constant would be comptime and could not halve
130        let k = RuntimeCell::<u32>::new(2u32);
131        while k.read() <= padded {
132            let kk = k.read();
133            let j = RuntimeCell::<u32>::new(kk / 2u32);
134            while j.read() > 0u32 {
135                let jj = j.read();
136                let mut a_i = unit;
137                while a_i < padded {
138                    let b_i = a_i ^ jj;
139                    if b_i > a_i {
140                        let (a, b) = (col[a_i as usize], col[b_i as usize]);
141                        let ascending = (a_i & kk) == 0u32;
142                        let swap = if ascending { a > b } else { a < b };
143                        if swap {
144                            col[a_i as usize] = b;
145                            col[b_i as usize] = a;
146                        }
147                    }
148                    a_i += units;
149                }
150                sync_cube();
151                j.store(jj / 2u32);
152            }
153            k.store(kk * 2u32);
154        }
155        let median = (col[((num_channels - 1u32) / 2u32) as usize] + col[(num_channels / 2u32) as usize]) * F::new(0.5f32);
156        let mut c = unit;
157        while c < num_channels {
158            let idx = (c * num_samples + s) as usize;
159            output[idx] = input[idx] - median;
160            c += units;
161        }
162    }
163}
164
165/// Common median reference by rank counting on `N` consecutive samples per unit (`num_samples` a
166/// multiple of `N`): channel `c` of the unit's samples is one aligned vector load, and each lane
167/// counts, for every channel value, the channels below and equal to it, as
168/// [`fn@common_median_kernel`] does for one sample. Same comparisons, same median `(m₁ + m₂) / 2`,
169/// so the output is bit-identical; no shared memory or barriers (CubeCL's CPU runtime maps the
170/// vectors to SIMD).
171#[cube(launch)]
172pub fn common_median_vector_kernel<F: Float, N: Size>(input: &[Vector<F, N>], output: &mut [Vector<F, N>], num_channels: u32, vectors_per_row: u32) {
173    let v = element_position();
174    if v < vectors_per_row as usize {
175        let row = vectors_per_row as usize;
176        let one = Vector::<u32, N>::new(1u32);
177        let zero = Vector::<u32, N>::new(0u32);
178        let k1 = Vector::<u32, N>::new((num_channels - 1u32) / 2u32);
179        let k2 = Vector::<u32, N>::new(num_channels / 2u32);
180        let mut mid1 = Vector::<F, N>::new(F::new(0.0f32));
181        let mut mid2 = Vector::<F, N>::new(F::new(0.0f32));
182        let mut c = 0u32;
183        while c < num_channels {
184            let x = input[c as usize * row + v];
185            let mut less = zero;
186            let mut equal = zero;
187            let mut j = 0u32;
188            while j < num_channels {
189                let y = input[j as usize * row + v];
190                less += select_many(y.less_than(&x), one, zero);
191                equal += select_many(y.equal(&x), one, zero);
192                j += 1u32;
193            }
194            let upto = less + equal;
195            mid1 = select_many(k1.greater_equal(&less).vec_and(k1.less_than(&upto)), x, mid1);
196            mid2 = select_many(k2.greater_equal(&less).vec_and(k2.less_than(&upto)), x, mid2);
197            c += 1u32;
198        }
199        let median = (mid1 + mid2) * Vector::new(F::new(0.5f32));
200        c = 0u32;
201        while c < num_channels {
202            let idx = c as usize * row + v;
203            output[idx] = input[idx] - median;
204            c += 1u32;
205        }
206    }
207}
208
209/// [`fn@direct_car_kernel`] on `N` consecutive samples per unit (`num_samples` a multiple of `N`):
210/// channel `c` of the unit's samples is one aligned vector load; channels are summed in the same
211/// order, so the output is bit-identical.
212#[cube(launch)]
213pub fn direct_car_vector_kernel<F: Float, N: Size>(input: &[Vector<F, N>], output: &mut [Vector<F, N>], num_channels: u32, vectors_per_row: u32) {
214    let v = element_position();
215    if v < vectors_per_row as usize {
216        let row = vectors_per_row as usize;
217        let mut sum = Vector::<F, N>::new(F::new(0.0f32));
218        let mut c = 0u32;
219        while c < num_channels {
220            sum += input[c as usize * row + v];
221            c += 1u32;
222        }
223        let avg = sum / Vector::new(F::cast_from(num_channels));
224        c = 0u32;
225        while c < num_channels {
226            let idx = c as usize * row + v;
227            output[idx] = input[idx] - avg;
228            c += 1u32;
229        }
230    }
231}
232
233/// [`fn@common_median_vector_kernel`] for any `num_samples`: `ceil(num_samples / N)` units, channel
234/// `c`'s samples gathered lane by lane ([`fn@load_lanes`]) and the result stored lane by lane
235/// ([`fn@store_lanes`]); the same comparisons per lane, so bit-identical to the scalar kernel.
236#[cube(launch)]
237pub fn common_median_vector_unaligned_kernel<F: Float, N: Size>(input: &[F], output: &mut [F], num_channels: u32, num_samples: u32, num_units: u32) {
238    let v = element_position() as u32;
239    if v < num_units {
240        let n = N::value() as u32;
241        let i = v * n;
242        let valid = u32::min(n, num_samples - i);
243        let one = Vector::<u32, N>::new(1u32);
244        let zero = Vector::<u32, N>::new(0u32);
245        let k1 = Vector::<u32, N>::new((num_channels - 1u32) / 2u32);
246        let k2 = Vector::<u32, N>::new(num_channels / 2u32);
247        let mut mid1 = Vector::<F, N>::new(F::new(0.0f32));
248        let mut mid2 = Vector::<F, N>::new(F::new(0.0f32));
249        let mut c = 0u32;
250        while c < num_channels {
251            let x = load_lanes::<F, N>(input, (c * num_samples + i) as usize, valid);
252            let mut less = zero;
253            let mut equal = zero;
254            let mut j = 0u32;
255            while j < num_channels {
256                let y = load_lanes::<F, N>(input, (j * num_samples + i) as usize, valid);
257                less += select_many(y.less_than(&x), one, zero);
258                equal += select_many(y.equal(&x), one, zero);
259                j += 1u32;
260            }
261            let upto = less + equal;
262            mid1 = select_many(k1.greater_equal(&less).vec_and(k1.less_than(&upto)), x, mid1);
263            mid2 = select_many(k2.greater_equal(&less).vec_and(k2.less_than(&upto)), x, mid2);
264            c += 1u32;
265        }
266        let median = (mid1 + mid2) * Vector::new(F::new(0.5f32));
267        c = 0u32;
268        while c < num_channels {
269            let start = (c * num_samples + i) as usize;
270            store_lanes::<F, N>(output, start, load_lanes::<F, N>(input, start, valid) - median, valid);
271            c += 1u32;
272        }
273    }
274}
275
276/// [`fn@direct_car_vector_kernel`] for any `num_samples` (as
277/// [`fn@common_median_vector_unaligned_kernel`]): channels summed in the same order, bit-identical.
278#[cube(launch)]
279pub fn direct_car_vector_unaligned_kernel<F: Float, N: Size>(input: &[F], output: &mut [F], num_channels: u32, num_samples: u32, num_units: u32) {
280    let v = element_position() as u32;
281    if v < num_units {
282        let n = N::value() as u32;
283        let i = v * n;
284        let valid = u32::min(n, num_samples - i);
285        let mut sum = Vector::<F, N>::new(F::new(0.0f32));
286        let mut c = 0u32;
287        while c < num_channels {
288            sum += load_lanes::<F, N>(input, (c * num_samples + i) as usize, valid);
289            c += 1u32;
290        }
291        let avg = sum / Vector::new(F::cast_from(num_channels));
292        c = 0u32;
293        while c < num_channels {
294            let start = (c * num_samples + i) as usize;
295            store_lanes::<F, N>(output, start, load_lanes::<F, N>(input, start, valid) - avg, valid);
296            c += 1u32;
297        }
298    }
299}