use cubecl::Runtime;
use cubecl_utils_rs::prelude::*;
use cubek::matmul::definition::MatmulPrecision;
use faer::Mat;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand_distr::{Distribution, Normal};
use std::time::Instant;
use crate::core::math::pca_svd::SvdResults;
use crate::core::math::*;
use crate::gpu::linalg::cholesky_gpu::{cholesky_qr2, dense_gemm};
use crate::gpu::linalg::sparse_gpu::{GpuCompressedSparseData, csc_to_csr_gpu};
use crate::gpu::linalg::spmm::{
launch_dense_column_sum, launch_dense_column_weighted_sum, launch_spmm_csc_transpose,
launch_spmm_csr_forward,
};
use crate::prelude::*;
#[derive(Clone, Copy, Debug)]
pub struct RandSvdGpuParams {
pub n_power_iters: usize,
pub oversampling: usize,
}
impl RandSvdGpuParams {
pub fn new(n_power_iters: usize, oversampling: usize) -> Self {
Self {
n_power_iters,
oversampling,
}
}
}
impl Default for RandSvdGpuParams {
fn default() -> Self {
Self {
n_power_iters: DEFAULT_N_POWER_ITERS_RAND_SVD,
oversampling: DEFAULT_OVERSAMPLING_RAND_SVD,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn randomised_sparse_svd_gpu<R, T, MP>(
data: CompressedSparseData2<T, T>,
col_means: &[T],
col_stds: &[T],
row_offsets: Option<&[T]>,
n_components: usize,
params: Option<RandSvdGpuParams>,
seed: u64,
device: R::Device,
verbose: usize,
) -> Result<SvdResults<T>, BixverseErrors>
where
R: Runtime,
T: cubecl::prelude::Float + cubecl::CubeElement + BixverseFloat,
MP: MatmulPrecision,
{
let verbosity = parse_verbosity_level(verbose);
let start = Instant::now();
let params = params.unwrap_or_default();
if !data.cs_type.is_csc() {
return Err(BixverseErrors::SparseLayoutMismatch {
expected: CompressedSparseFormat::Csc,
got: data.cs_type,
});
}
let (n, m) = data.shape;
if col_means.len() != m || col_stds.len() != m {
return Err(BixverseErrors::DimensionMisMatchSparse {
indices_len: col_means.len(),
data_len: m,
});
}
if let Some(off) = row_offsets
&& off.len() != n
{
return Err(BixverseErrors::DimensionMisMatchSparse {
indices_len: off.len(),
data_len: n,
});
}
let use_clr = row_offsets.is_some();
let s = n_components + params.oversampling;
let client = R::client(&device);
let csc_gpu =
GpuCompressedSparseData::<R, T>::from_compressed_sparse_data_2(&data, true, &client)?;
let csr_gpu = csc_to_csr_gpu(&csc_gpu, &data.indptr, &data.indices, &client)?;
let mu_gpu = GpuTensor::<R, T>::from_slice(col_means, vec![m], &client)?;
let sigma_gpu = GpuTensor::<R, T>::from_slice(col_stds, vec![m], &client)?;
let sigma_sq: Vec<T> = col_stds.iter().map(|&x| x * x).collect();
let sigma_sq_gpu = GpuTensor::<R, T>::from_slice(&sigma_sq, vec![m], &client)?;
let zero_n = vec![T::from(0.0).unwrap(); n];
let row_offsets_gpu = match row_offsets {
Some(off) => GpuTensor::<R, T>::from_slice(off, vec![n], &client)?,
None => GpuTensor::<R, T>::from_slice(&zero_n, vec![n], &client)?,
};
drop(data);
let mut rng = StdRng::seed_from_u64(seed);
let normal = Normal::new(0.0, 1.0).expect("unit normal is always valid");
let omega_scaled: Vec<T> = (0..m * s)
.map(|i| {
let j = i / s;
T::from(normal.sample(&mut rng)).unwrap() / col_stds[j]
})
.collect();
let omega_gpu = GpuTensor::<R, T>::from_slice(&omega_scaled, vec![m, s], &client)?;
let y_buf = GpuTensor::<R, T>::empty(vec![n, s], &client)?;
let q_buf = GpuTensor::<R, T>::empty(vec![n, s], &client)?;
let z_buf = GpuTensor::<R, T>::empty(vec![m, s], &client)?;
let c_buf = GpuTensor::<R, T>::empty(vec![s], &client)?;
let q_sum_buf = GpuTensor::<R, T>::empty(vec![s], &client)?;
let zero_s = vec![T::from(0.0).unwrap(); s];
let x_sum_buf = GpuTensor::<R, T>::from_slice(&zero_s, vec![s], &client)?;
let m_dot_q_buf = GpuTensor::<R, T>::from_slice(&zero_s, vec![s], &client)?;
let q1_scratch = GpuTensor::<R, T>::empty(vec![n, s], &client)?;
let g_scratch = GpuTensor::<R, T>::empty(vec![s, s], &client)?;
launch_dense_column_weighted_sum::<R, T>(&mu_gpu, &omega_gpu, &c_buf, m, s, &client)?;
if use_clr {
launch_dense_column_sum::<R, T>(&omega_gpu, &x_sum_buf, m, s, &client)?;
}
launch_spmm_csr_forward::<R, T, T>(
&csr_gpu,
&omega_gpu,
&c_buf,
&row_offsets_gpu,
&x_sum_buf,
&y_buf,
s,
&client,
)?;
cholesky_qr2::<R, T, MP>(&client, &y_buf, &q_buf, &q1_scratch, &g_scratch, n, s)?;
for iter in 0..params.n_power_iters {
launch_dense_column_sum::<R, T>(&q_buf, &q_sum_buf, n, s, &client)?;
if use_clr {
launch_dense_column_weighted_sum::<R, T>(
&row_offsets_gpu,
&q_buf,
&m_dot_q_buf,
n,
s,
&client,
)?;
}
launch_spmm_csc_transpose::<R, T, T>(
&csc_gpu,
&q_buf,
&q_sum_buf,
&mu_gpu,
&sigma_sq_gpu,
&m_dot_q_buf,
&z_buf,
s,
&client,
)?;
launch_dense_column_weighted_sum::<R, T>(&mu_gpu, &z_buf, &c_buf, m, s, &client)?;
if use_clr {
launch_dense_column_sum::<R, T>(&z_buf, &x_sum_buf, m, s, &client)?;
}
launch_spmm_csr_forward::<R, T, T>(
&csr_gpu,
&z_buf,
&c_buf,
&row_offsets_gpu,
&x_sum_buf,
&y_buf,
s,
&client,
)?;
cholesky_qr2::<R, T, MP>(&client, &y_buf, &q_buf, &q1_scratch, &g_scratch, n, s)?;
if verbosity.detailed_verbosity() {
println!(
" Power iter {}/{} done: {:.2?}",
iter + 1,
params.n_power_iters,
start.elapsed()
);
}
}
launch_dense_column_sum::<R, T>(&q_buf, &q_sum_buf, n, s, &client)?;
if use_clr {
launch_dense_column_weighted_sum::<R, T>(
&row_offsets_gpu,
&q_buf,
&m_dot_q_buf,
n,
s,
&client,
)?;
}
launch_spmm_csc_transpose::<R, T, T>(
&csc_gpu,
&q_buf,
&q_sum_buf,
&mu_gpu,
&sigma_gpu,
&m_dot_q_buf,
&z_buf,
s,
&client,
)?;
let z_host = z_buf.read(&client)?;
let z_mat = Mat::<T>::from_fn(m, s, |i, j| z_host[i * s + j]);
let svd = z_mat
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let v_z = svd.V();
let v_z_trunc: Vec<T> = (0..s)
.flat_map(|i| (0..n_components).map(move |j| v_z[(i, j)]))
.collect();
let v_z_gpu = GpuTensor::<R, T>::from_slice(&v_z_trunc, vec![s, n_components], &client)?;
let u_gpu = GpuTensor::<R, T>::empty(vec![n, n_components], &client)?;
dense_gemm::<R, MP>(
q_buf.handle(),
[n, s],
false,
v_z_gpu.handle(),
[s, n_components],
false,
u_gpu.handle(),
[n, n_components],
None,
&client,
)?;
let u_host = u_gpu.read(&client)?;
if verbosity.detailed_verbosity() {
println!(
" Host SVD + U reconstruction done: {:.2?}",
start.elapsed()
);
}
let u_mat = Mat::<T>::from_fn(n, n_components, |i, j| u_host[i * n_components + j]);
let u_z = svd.U();
let v_mat = Mat::<T>::from_fn(m, n_components, |i, j| u_z[(i, j)]);
let s_vec: Vec<T> = (0..n_components).map(|i| svd.S()[i]).collect();
if verbosity.normal_verbosity() {
println!(
" Finished randomised sparse SVD (GPU) in {:.2?}",
start.elapsed()
);
}
Ok(SvdResults {
u: u_mat,
s: s_vec,
v: v_mat,
})
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl::wgpu::{WgpuDevice, WgpuRuntime};
use faer::Mat;
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::DefaultDevice;
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
WgpuRuntime::client(&device);
}))
.ok()
.map(|_| device)
}
fn dense_to_csc(a: &[f32], n: usize, m: usize) -> (Vec<f32>, Vec<u32>, Vec<u32>) {
let mut values = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0u32];
for j in 0..m {
for i in 0..n {
let v = a[i * m + j];
if v != 0.0 {
values.push(v);
indices.push(i as u32);
}
}
indptr.push(values.len() as u32);
}
(values, indices, indptr)
}
fn build_dense(n: usize, m: usize) -> Vec<f32> {
(0..n * m)
.map(|k| {
let i = k / m;
let j = k % m;
if (i + 2 * j).is_multiple_of(5) {
((i * 7 + j * 13 + 3) % 19) as f32 * 0.3 + 0.5
} else {
0.0
}
})
.collect()
}
#[test]
fn test_randomised_sparse_svd_gpu_singular_values() {
let Some(device) = try_device() else { return };
let (n, m, n_components) = (60usize, 20usize, 3usize);
let a_dense = build_dense(n, m);
let (values, indices, indptr) = dense_to_csc(&a_dense, n, m);
let csc = CompressedSparseData2::<f32, f32>::new_csc(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let mu = vec![0.0f32; m];
let sigma = vec![1.0f32; m];
let svd = randomised_sparse_svd_gpu::<WgpuRuntime, f32, f32>(
csc,
&mu,
&sigma,
None,
n_components,
Some(RandSvdGpuParams::new(2, 8)),
42,
device,
0,
)
.unwrap();
let a_mat = Mat::<f32>::from_fn(n, m, |i, j| a_dense[i * m + j]);
let true_svd = a_mat.thin_svd().unwrap();
let true_s = true_svd.S();
for i in 0..n_components {
let want = true_s[i];
let got = svd.s[i];
let rel = (got - want).abs() / want.abs();
assert!(
rel < 1e-3,
"singular value {}: got {}, want {}, rel err {}",
i,
got,
want,
rel
);
}
for i in 0..n_components {
assert!(svd.s[i] >= 0.0, "negative singular value at {}", i);
if i > 0 {
assert!(svd.s[i - 1] >= svd.s[i], "not sorted at {}", i);
}
}
}
#[test]
fn test_randomised_sparse_svd_gpu_scaled_parity() {
let Some(device) = try_device() else { return };
let (n, m, n_components) = (400usize, 40usize, 5usize);
let mut dense = vec![0.0f32; n * m];
for i in 0..n {
for j in 0..m {
if (i * 3 + j) % 4 == 0 {
continue;
}
let mut v = 0.0f32;
for f in 0..6 {
let a = ((i * (f + 3) + 7 * f) % 17) as f32 / 17.0 - 0.5;
let b = ((j * (2 * f + 5) + 3 * f) % 13) as f32 / 13.0 - 0.5;
v += 4.0 * a * b / (1.0 + f as f32);
}
v += 0.05 * ((i * 31 + j * 17) % 11) as f32 / 11.0 + 0.3;
let scale = 0.2 + 4.8 * j as f32 / (m - 1) as f32;
dense[i * m + j] = v * scale;
}
}
let (values, indices, indptr) = dense_to_csc(&dense, n, m);
let csc = CompressedSparseData2::<f32, f32>::new_csc(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let mut mu = vec![0.0f32; m];
let mut sigma = vec![0.0f32; m];
for j in 0..m {
let mean = (0..n).map(|i| dense[i * m + j] as f64).sum::<f64>() / n as f64;
let var = (0..n)
.map(|i| (dense[i * m + j] as f64 - mean).powi(2))
.sum::<f64>()
/ (n - 1) as f64;
mu[j] = mean as f32;
sigma[j] = var.sqrt() as f32;
}
let got = randomised_sparse_svd_gpu::<WgpuRuntime, f32, f32>(
csc,
&mu,
&sigma,
None,
n_components,
Some(RandSvdGpuParams::new(2, 10)),
42,
device,
0,
)
.unwrap();
let scaled = Mat::<f64>::from_fn(n, m, |i, j| {
(dense[i * m + j] as f64 - mu[j] as f64) / sigma[j] as f64
});
let want = scaled.thin_svd().unwrap();
for i in 0..n_components {
let rel = (got.s[i] as f64 - want.S()[i]).abs() / want.S()[i];
assert!(
rel < 1e-4,
"singular value {i}: got {}, want {}, rel err {rel:.2e}",
got.s[i],
want.S()[i]
);
}
let u_got = Mat::<f64>::from_fn(n, n_components, |i, j| got.u[(i, j)] as f64);
let cross = want.U().subcols(0, n_components).transpose() * &u_got;
let cos_min = cross
.thin_svd()
.unwrap()
.S()
.column_vector()
.iter()
.fold(f64::MAX, |a, &b| a.min(b));
assert!(
cos_min > 0.9999,
"score subspace drifted: smallest principal cosine {cos_min:.5}"
);
}
#[test]
#[cfg(feature = "large-test")]
fn test_randomised_sparse_svd_gpu_accuracy_vs_dense() {
let Some(device) = try_device() else { return };
let (n, m, n_components, oversampling) = (3000usize, 300usize, 20usize, 30usize);
let hash = |a: usize, b: usize| -> f32 {
let mut h = (a as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add((b as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
h ^= h >> 29;
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
h ^= h >> 32;
((h >> 40) as f32) / 16_777_216.0
};
let mut dense = vec![0.0f32; n * m];
for i in 0..n {
for j in 0..m {
if (i + j) % 10 != 0 {
continue;
}
let mut v = 0.0f32;
for f in 0..24 {
v += 6.0 * (hash(i, f) - 0.5) * (hash(j, f + 100) - 0.5)
/ (1.0 + f as f32).sqrt();
}
dense[i * m + j] = v + 0.15 * hash(i, j + 7) + 0.1;
}
}
let mut values = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0u32];
for j in 0..m {
for i in 0..n {
let v = dense[i * m + j];
if v != 0.0 {
values.push(v);
indices.push(i as u32);
}
}
indptr.push(values.len() as u32);
}
let mut mu = vec![0.0f32; m];
let mut sigma = vec![0.0f32; m];
for j in 0..m {
let mean: f64 = (0..n).map(|i| dense[i * m + j] as f64).sum::<f64>() / n as f64;
let var: f64 = (0..n)
.map(|i| {
let d = dense[i * m + j] as f64 - mean;
d * d
})
.sum::<f64>()
/ n as f64;
mu[j] = mean as f32;
sigma[j] = (var.sqrt() as f32).max(1e-6);
}
let scaled = Mat::<f32>::from_fn(n, m, |i, j| (dense[i * m + j] - mu[j]) / sigma[j]);
let want = scaled.thin_svd().unwrap();
let mut worst_at = Vec::new();
for n_power in [0usize, 1, 2] {
let csc = CompressedSparseData2::<f32, f32>::new_csc(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let got = randomised_sparse_svd_gpu::<WgpuRuntime, f32, f32>(
csc,
&mu,
&sigma,
None,
n_components,
Some(RandSvdGpuParams::new(n_power, oversampling)),
42,
device.clone(),
0,
)
.unwrap();
let mut worst = 0.0f32;
for i in 0..n_components {
let rel = (got.s[i] - want.S()[i]).abs() / want.S()[i].abs();
worst = worst.max(rel);
}
println!(
"n_power_iters={}: worst relative error {:.5}",
n_power, worst
);
worst_at.push(worst);
}
assert!(
worst_at[0] > 4.0 * worst_at[2],
"error barely moved across power iterations ({:.5} -> {:.5}); \
this gate cannot detect sketch quality",
worst_at[0],
worst_at[2]
);
assert!(
worst_at[2] < 0.015,
"worst relative singular-value error {:.5} at the production setting",
worst_at[2]
);
}
#[test]
fn test_randomised_sparse_svd_gpu_csr_input_rejected() {
let Some(device) = try_device() else { return };
let (n, m) = (10usize, 4usize);
let a_dense = build_dense(n, m);
let mut values = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0u32];
for i in 0..n {
for j in 0..m {
let v = a_dense[i * m + j];
if v != 0.0 {
values.push(v);
indices.push(j as u32);
}
}
indptr.push(values.len() as u32);
}
let csr = CompressedSparseData2::<f32, f32>::new_csr(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let mu = vec![0.0f32; m];
let sigma = vec![1.0f32; m];
let res = randomised_sparse_svd_gpu::<WgpuRuntime, f32, f32>(
csr, &mu, &sigma, None, 2, None, 42, device, 0,
);
assert!(matches!(
res,
Err(BixverseErrors::SparseLayoutMismatch { .. })
));
}
#[test]
fn test_randomised_sparse_svd_gpu_dim_mismatch_rejected() {
let Some(device) = try_device() else { return };
let (n, m) = (10usize, 4usize);
let a_dense = build_dense(n, m);
let (values, indices, indptr) = dense_to_csc(&a_dense, n, m);
let csc = CompressedSparseData2::<f32, f32>::new_csc(
&values,
&indices,
&indptr,
Some(&values),
(n, m),
);
let mu = vec![0.0f32; m + 1];
let sigma = vec![1.0f32; m + 1];
let res = randomised_sparse_svd_gpu::<WgpuRuntime, f32, f32>(
csc, &mu, &sigma, None, 2, None, 42, device, 0,
);
assert!(matches!(
res,
Err(BixverseErrors::DimensionMisMatchSparse { .. })
));
}
}