use std::ops::Range;
use cubecl::prelude::*;
use cubecl::server::Handle;
use dspk_core::compute::LaunchGeometry;
use super::kernels::{split_rows_kernel, sum_slices_kernel};
use super::matmul::{matmul, MatrixView};
use crate::core::{buffer, cast, reduce, DspFloat, Scratch};
pub const COVARIANCE_SPLIT_SAMPLES: usize = 4096;
#[derive(Debug, Default)]
struct SplitBuffers {
splits: Scratch,
partial: Scratch,
}
struct Samples<'a> {
input: &'a Handle,
channels: usize,
row_len: usize,
cols: Range<usize>,
means: Option<&'a Handle>,
}
fn gram<F: DspFloat>(client: &Client, src: Samples<'_>, buffers: &mut SplitBuffers, out: &Handle, accumulate: bool) {
let Samples { input, channels, row_len, cols, means } = src;
let n = cols.len();
let splits = n.div_ceil(COVARIANCE_SPLIT_SAMPLES).max(1);
let (len, split_len) = (channels * channels, channels * COVARIANCE_SPLIT_SAMPLES);
let total = splits * split_len;
let split_major = buffers.splits.get::<F>(client, total);
let partial = buffers.partial.get::<F>(client, splits * len);
let centred = means.is_some();
let means = means.unwrap_or(out).clone();
let geom = LaunchGeometry::elementwise(client, total);
unsafe {
split_rows_kernel::launch::<F>(
client,
geom.cube_count,
geom.cube_dim,
BufferArg::from_raw_parts(input.clone(), channels * row_len),
BufferArg::from_raw_parts(means, channels),
BufferArg::from_raw_parts(split_major.clone(), total),
cols.start as u32,
row_len as u32,
n as u32,
channels as u32,
COVARIANCE_SPLIT_SAMPLES as u32,
total as u32,
centred,
);
}
let x = MatrixView::batched_row_major(&split_major, total, splits, channels, COVARIANCE_SPLIT_SAMPLES);
matmul::<F>(client, &x, &x.transposed(), &partial, splits * len);
let geom = LaunchGeometry::elementwise(client, len);
unsafe {
sum_slices_kernel::launch::<F>(
client,
geom.cube_count,
geom.cube_dim,
BufferArg::from_raw_parts(partial, splits * len),
BufferArg::from_raw_parts(out.clone(), len),
len as u32,
splits as u32,
cast::<F>(1.0 / n.max(1) as f64),
accumulate,
);
}
}
pub fn covariance<F: DspFloat>(
client: &Client,
input: &Handle,
out_cov: &Handle,
out_mean: &Handle,
channels: usize,
samples: usize,
) {
if channels == 0 {
return;
}
let std_scratch = buffer::empty::<F>(client, channels);
reduce::row_mean_std::<F>(client, input, out_mean, &std_scratch, channels, samples);
let src = Samples { input, channels, row_len: samples, cols: 0..samples, means: Some(out_mean) };
gram::<F>(client, src, &mut SplitBuffers::default(), out_cov, false);
}
pub struct SecondMomentAccumulator<F: DspFloat> {
client: Client,
channels: usize,
sum: Handle,
batches: usize,
buffers: SplitBuffers,
_float: std::marker::PhantomData<F>,
}
impl<F: DspFloat> SecondMomentAccumulator<F> {
pub fn new(client: &Client, channels: usize) -> Self {
Self {
client: client.clone(),
channels,
sum: buffer::zeros::<F>(client, channels * channels),
batches: 0,
buffers: SplitBuffers::default(),
_float: std::marker::PhantomData,
}
}
pub fn add(&mut self, input: &Handle, row_len: usize, cols: Range<usize>) {
assert!(cols.end <= row_len, "columns {cols:?} outside rows of {row_len}");
if self.channels == 0 || cols.is_empty() {
return;
}
let src = Samples { input, channels: self.channels, row_len, cols, means: None };
gram::<F>(&self.client, src, &mut self.buffers, &self.sum, true);
self.batches += 1;
}
pub fn batches(&self) -> usize {
self.batches
}
pub fn into_sum(self) -> Handle {
self.sum
}
pub fn finish(self) -> Vec<f64> {
let k = self.batches.max(1) as f64;
buffer::download::<F>(&self.client, self.sum).into_iter().map(|v| crate::core::to_f64(v) / k).collect()
}
}
pub fn covariance_of_host<F: DspFloat>(client: &Client, data: &[f32], channels: usize, samples: usize) -> (Handle, Vec<f64>) {
assert_eq!(data.len(), channels * samples, "data size mismatch");
let input = buffer::upload(client, &crate::core::cast_f32::<F>(data));
let cov = buffer::empty::<F>(client, channels * channels);
let mean = buffer::empty::<F>(client, channels);
covariance::<F>(client, &input, &cov, &mean, channels, samples);
let mean = buffer::download::<F>(client, mean).into_iter().map(crate::core::to_f64).collect();
(cov, mean)
}
#[cfg(test)]
mod tests {
use super::*;
fn matches_host(client: &Client) {
for (channels, samples) in [(1usize, 10usize), (5, 9_000), (5, 9_001), (17, 1_234)] {
let data: Vec<f32> = (0..channels * samples)
.map(|i| {
let (c, t) = ((i / samples) as f32, (i % samples) as f32);
(t * 0.013 * (c + 1.0)).sin() * 20.0 + ((i * 7919) % 101) as f32 * 0.1 + c * 50.0
})
.collect();
let input = buffer::upload(client, &data);
let (cov, mean) = (buffer::empty::<f32>(client, channels * channels), buffer::empty::<f32>(client, channels));
covariance::<f32>(client, &input, &cov, &mean, channels, samples);
let cov = buffer::download::<f32>(client, cov);
let m: Vec<f64> = (0..channels).map(|c| data[c * samples..(c + 1) * samples].iter().map(|v| *v as f64).sum::<f64>() / samples as f64).collect();
for i in 0..channels {
for j in 0..channels {
let want: f64 = (0..samples)
.map(|t| (data[i * samples + t] as f64 - m[i]) * (data[j * samples + t] as f64 - m[j]))
.sum::<f64>()
/ samples as f64;
let got = cov[i * channels + j] as f64;
assert!((got - want).abs() < 1e-3 * want.abs().max(1.0), "{} {channels}x{samples} [{i},{j}]: {got} vs {want}", client.name());
}
}
}
}
runtime_test!(test_covariance_matches_host, matches_host);
fn second_moment_matches_host(client: &Client) {
let channels = 3;
let batches = [(40usize, 5..35usize), (25, 3..20)];
let mut acc = SecondMomentAccumulator::<f32>::new(client, channels);
let mut want = vec![0.0f64; channels * channels];
for (b, (row_len, cols)) in batches.iter().enumerate() {
let data: Vec<f32> = (0..channels * row_len).map(|i| ((i * 31 + b * 7) % 17) as f32 - 8.0).collect();
acc.add(&buffer::upload(client, &data), *row_len, cols.clone());
for i in 0..channels {
for j in 0..channels {
let s: f64 = cols.clone().map(|t| data[i * row_len + t] as f64 * data[j * row_len + t] as f64).sum();
want[i * channels + j] += s / cols.len() as f64 / batches.len() as f64;
}
}
}
assert_eq!(acc.batches(), 2);
for (k, (got, want)) in acc.finish().iter().zip(&want).enumerate() {
assert!((got - want).abs() < 1e-3 * want.abs().max(1.0), "{} [{k}]: {got} vs {want}", client.name());
}
}
runtime_test!(test_second_moment_matches_host, second_moment_matches_host);
fn keeps_f64(client: &Client) {
if !client.properties().supports_type(f64::elem_type_native()) {
return;
}
let (channels, samples) = (5usize, 9_001usize);
let data: Vec<f64> = (0..channels * samples).map(|i| ((i * 7919) % 1013) as f64 / 7.0 + (i / samples) as f64 * 1e3).collect();
let m: Vec<f64> = (0..channels).map(|c| data[c * samples..(c + 1) * samples].iter().sum::<f64>() / samples as f64).collect();
let input = buffer::upload(client, &data);
let (cov, mean) = (buffer::empty::<f64>(client, channels * channels), buffer::empty::<f64>(client, channels));
covariance::<f64>(client, &input, &cov, &mean, channels, samples);
let cov = buffer::download::<f64>(client, cov);
let mut acc = SecondMomentAccumulator::<f64>::new(client, channels);
acc.add(&input, samples, 3..samples);
let moment = acc.finish();
for i in 0..channels {
for j in 0..channels {
let row = |c: usize| &data[c * samples..(c + 1) * samples];
let want: f64 = row(i).iter().zip(row(j)).map(|(a, b)| (a - m[i]) * (b - m[j])).sum::<f64>() / samples as f64;
let got = cov[i * channels + j];
assert!((got - want).abs() < 1e-12 * want.abs().max(1.0), "{} f64 cov [{i},{j}]: {got} vs {want}", client.name());
let want: f64 = row(i)[3..].iter().zip(&row(j)[3..]).map(|(a, b)| a * b).sum::<f64>() / (samples - 3) as f64;
let got = moment[i * channels + j];
assert!((got - want).abs() < 1e-12 * want.abs().max(1.0), "{} f64 moment [{i},{j}]: {got} vs {want}", client.name());
}
}
}
runtime_test!(test_covariance_keeps_f64, keeps_f64);
}