use cubecl::prelude::*;
use cubecl::server::Handle;
use dspk_core::compute::LaunchGeometry;
use super::kernels::eigen::{
jacobi_cols_kernel, jacobi_offdiag_ratio_kernel, jacobi_rotations_kernel, jacobi_rows_kernel, jacobi_shared_kernel,
};
use crate::core::{buffer, cast, to_f64, DspFloat};
use crate::math::execute_scaling;
pub const EIGEN_MAX_SWEEPS: usize = 30;
const SHARED_MATRICES: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EigenOptions {
pub tolerance: Option<f64>,
pub max_sweeps: usize,
}
impl Default for EigenOptions {
fn default() -> Self {
Self { tolerance: None, max_sweeps: EIGEN_MAX_SWEEPS }
}
}
pub fn rounding_tolerance<F: DspFloat>(n: usize) -> f64 {
(n.max(1) as f64).sqrt() * to_f64(F::EPSILON)
}
#[derive(Debug, Clone, PartialEq)]
pub struct SymmetricEigen {
pub n: usize,
pub values: Vec<f64>,
pub vectors: Vec<f64>,
}
fn fits_shared<F: DspFloat>(client: &Client, n: usize) -> bool {
let hw = &client.properties().hardware;
let bytes = SHARED_MATRICES * n * n * size_of::<F>() + n * (2 * size_of::<F>() + size_of::<u32>()) + 2 * hw.max_units_per_cube as usize * size_of::<F>();
bytes <= hw.max_shared_memory_size
}
pub fn symmetric_eigen_batched<F: DspFloat>(
client: &Client,
matrices: &Handle,
batch: usize,
n: usize,
options: EigenOptions,
) -> Vec<SymmetricEigen> {
if batch == 0 || n == 0 {
return vec![SymmetricEigen { n, values: Vec::new(), vectors: Vec::new() }; batch];
}
let len = batch * n * n;
let floor = rounding_tolerance::<F>(n);
let tolerance = options.tolerance.map_or(floor, |t| t.max(floor));
let a = buffer::empty::<F>(client, len);
let v = buffer::empty::<F>(client, len);
execute_scaling::<F>(client, matrices, &a, len, cast(1.0), cast(0.0));
let (a, v) = if fits_shared::<F>(client, n) {
run_shared::<F>(client, a, v, batch, n, tolerance, options.max_sweeps)
} else {
run_global::<F>(client, a, v, batch, n, tolerance, options.max_sweeps)
};
let a = buffer::download::<F>(client, a);
let v = buffer::download::<F>(client, v);
(0..batch)
.map(|b| {
let (ab, vb) = (&a[b * n * n..(b + 1) * n * n], &v[b * n * n..(b + 1) * n * n]);
let mut order: Vec<usize> = (0..n).collect();
let diag: Vec<f64> = (0..n).map(|i| to_f64(ab[i * n + i])).collect();
order.sort_by(|&x, &y| diag[y].total_cmp(&diag[x]));
let values = order.iter().map(|&k| diag[k]).collect();
let mut vectors = vec![0.0f64; n * n];
for (new_k, &k) in order.iter().enumerate() {
for row in 0..n {
vectors[row * n + new_k] = to_f64(vb[row * n + k]);
}
}
SymmetricEigen { n, values, vectors }
})
.collect()
}
pub fn symmetric_eigen<F: DspFloat>(client: &Client, matrix: &Handle, n: usize, options: EigenOptions) -> SymmetricEigen {
symmetric_eigen_batched::<F>(client, matrix, 1, n, options).remove(0)
}
pub fn symmetric_eigen_host<F: DspFloat>(client: &Client, matrix: &[f64], n: usize, options: EigenOptions) -> SymmetricEigen {
assert_eq!(matrix.len(), n * n, "matrix size mismatch");
let handle = buffer::upload(client, &matrix.iter().map(|&x| cast::<F>(x)).collect::<Vec<F>>());
symmetric_eigen::<F>(client, &handle, n, options)
}
fn run_shared<F: DspFloat>(client: &Client, a: Handle, v: Handle, batch: usize, n: usize, tolerance: f64, max_sweeps: usize) -> (Handle, Handle) {
let geom = LaunchGeometry::per_row(client, batch, n * n);
unsafe {
jacobi_shared_kernel::launch::<F>(
client,
geom.cube_count,
geom.cube_dim.clone(),
BufferArg::from_raw_parts(a.clone(), batch * n * n),
BufferArg::from_raw_parts(v.clone(), batch * n * n),
batch as u32,
cast::<F>(tolerance),
max_sweeps as u32,
n as u32,
geom.cube_dim.x,
);
}
(a, v)
}
fn run_global<F: DspFloat>(client: &Client, a: Handle, v: Handle, batch: usize, n: usize, tolerance: f64, max_sweeps: usize) -> (Handle, Handle) {
let len = batch * n * n;
let m = n + n % 2;
let identity: Vec<F> = (0..len).map(|e| cast::<F>(if (e % (n * n)) / n == e % n { 1.0 } else { 0.0 })).collect();
let (a, a_tmp) = (a, buffer::empty::<F>(client, len));
let (mut v, mut v_tmp) = (buffer::upload(client, &identity), v);
let partner = buffer::empty::<u32>(client, batch * n);
let coef_self = buffer::empty::<F>(client, batch * n);
let coef_other = buffer::empty::<F>(client, batch * n);
let ratio = buffer::empty::<F>(client, batch);
let elems = LaunchGeometry::elementwise(client, len);
let pairs = LaunchGeometry::elementwise(client, batch * m / 2);
let per_matrix = LaunchGeometry::per_row(client, batch, n * n);
for sweep in 0..=max_sweeps {
unsafe {
jacobi_offdiag_ratio_kernel::launch::<F>(
client,
per_matrix.cube_count.clone(),
per_matrix.cube_dim.clone(),
BufferArg::from_raw_parts(a.clone(), len),
BufferArg::from_raw_parts(ratio.clone(), batch),
batch as u32,
n as u32,
per_matrix.cube_dim.x,
);
}
let worst = buffer::download::<F>(client, ratio.clone());
let converged = worst.iter().all(|&r| to_f64(r) <= tolerance);
if converged || sweep == max_sweeps {
break;
}
for round in 0..m - 1 {
unsafe {
jacobi_rotations_kernel::launch::<F>(
client,
pairs.cube_count.clone(),
pairs.cube_dim.clone(),
BufferArg::from_raw_parts(a.clone(), len),
BufferArg::from_raw_parts(partner.clone(), batch * n),
BufferArg::from_raw_parts(coef_self.clone(), batch * n),
BufferArg::from_raw_parts(coef_other.clone(), batch * n),
batch as u32,
n as u32,
m as u32,
round as u32,
cast::<F>(tolerance),
);
jacobi_rows_kernel::launch::<F>(
client,
elems.cube_count.clone(),
elems.cube_dim.clone(),
BufferArg::from_raw_parts(a.clone(), len),
BufferArg::from_raw_parts(partner.clone(), batch * n),
BufferArg::from_raw_parts(coef_self.clone(), batch * n),
BufferArg::from_raw_parts(coef_other.clone(), batch * n),
BufferArg::from_raw_parts(a_tmp.clone(), len),
batch as u32,
n as u32,
);
jacobi_cols_kernel::launch::<F>(
client,
elems.cube_count.clone(),
elems.cube_dim.clone(),
BufferArg::from_raw_parts(a_tmp.clone(), len),
BufferArg::from_raw_parts(v.clone(), len),
BufferArg::from_raw_parts(partner.clone(), batch * n),
BufferArg::from_raw_parts(coef_self.clone(), batch * n),
BufferArg::from_raw_parts(coef_other.clone(), batch * n),
BufferArg::from_raw_parts(a.clone(), len),
BufferArg::from_raw_parts(v_tmp.clone(), len),
batch as u32,
n as u32,
);
}
std::mem::swap(&mut v, &mut v_tmp);
}
}
(a, v)
}
#[cfg(test)]
mod tests {
use super::*;
fn check(name: &str, a: &[f64], e: &SymmetricEigen, tol: f64) {
let n = e.n;
let scale = a.iter().fold(0.0f64, |m, x| m.max(x.abs())).max(1.0);
for k in 0..n {
for i in 0..n {
let av: f64 = (0..n).map(|j| a[i * n + j] * e.vectors[j * n + k]).sum();
assert!((av - e.values[k] * e.vectors[i * n + k]).abs() < tol * scale, "{name}: A v_{k} ≠ λ v_{k} at row {i}");
}
for l in 0..n {
let dot: f64 = (0..n).map(|i| e.vectors[i * n + k] * e.vectors[i * n + l]).sum();
let want = if k == l { 1.0 } else { 0.0 };
assert!((dot - want).abs() < tol, "{name}: vectors {k}·{l} = {dot}");
}
}
assert!(e.values.windows(2).all(|w| w[0] >= w[1]), "{name}: values not descending");
}
fn spd(n: usize, seed: usize) -> Vec<f64> {
let m: Vec<f64> = (0..n * n).map(|i| (((i + seed) * 7919) % 211) as f64 / 211.0 - 0.5).collect();
(0..n * n)
.map(|e| {
let (i, j) = (e / n, e % n);
(0..n).map(|k| m[k * n + i] * m[k * n + j]).sum::<f64>() + if i == j { 0.1 * i as f64 } else { 0.0 }
})
.collect()
}
fn solves(client: &Client) {
for n in [1usize, 2, 5, 16, 33, 160] {
let a = spd(n, n);
let e = symmetric_eigen_host::<f32>(client, &a, n, EigenOptions::default());
check(&format!("{} n={n} shared={}", client.name(), fits_shared::<f32>(client, n)), &a, &e, 2e-3);
}
let (batch, n) = (7usize, 12usize);
let mats: Vec<f64> = (0..batch).flat_map(|b| spd(n, 31 * b)).collect();
let handle = buffer::upload(client, &mats.iter().map(|&x| x as f32).collect::<Vec<_>>());
let all = symmetric_eigen_batched::<f32>(client, &handle, batch, n, EigenOptions::default());
for (b, e) in all.iter().enumerate() {
check(&format!("{} batch {b}", client.name()), &mats[b * n * n..(b + 1) * n * n], e, 2e-3);
}
}
runtime_test!(test_symmetric_eigen, solves);
}