Files
wifi-densepose/vendor/ruvector/examples/prime-radiant/benches/spectral_bench.rs

742 lines
22 KiB
Rust

//! Spectral Analysis Benchmarks for Prime-Radiant
//!
//! Benchmarks for spectral graph theory computations including:
//! - Eigenvalue computation (power iteration vs Lanczos)
//! - Cheeger constant computation
//! - Spectral clustering
//! - SIMD-accelerated operations
//!
//! Target metrics:
//! - Eigenvalue (power iteration): < 5ms for 1K nodes
//! - Eigenvalue (Lanczos): < 50ms for 10K nodes
//! - Cheeger constant: < 10ms for 1K nodes
//! - Spectral clustering: < 100ms for 5K nodes
use criterion::{
black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use std::collections::HashSet;
// ============================================================================
// SPARSE MATRIX TYPES
// ============================================================================
/// CSR (Compressed Sparse Row) format for efficient matrix-vector multiplication
#[derive(Clone)]
struct CsrMatrix {
rows: usize,
cols: usize,
row_ptr: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<f64>,
}
impl CsrMatrix {
fn from_edges(num_nodes: usize, edges: &[(usize, usize)]) -> Self {
// Build adjacency lists
let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); num_nodes];
let mut degrees = vec![0.0; num_nodes];
for &(u, v) in edges {
adj[u].push((v, -1.0));
adj[v].push((u, -1.0));
degrees[u] += 1.0;
degrees[v] += 1.0;
}
// Build CSR representation of Laplacian
let mut row_ptr = vec![0];
let mut col_indices = Vec::new();
let mut values = Vec::new();
for i in 0..num_nodes {
// Add diagonal (degree)
col_indices.push(i);
values.push(degrees[i]);
// Add off-diagonal entries
adj[i].sort_by_key(|&(j, _)| j);
for &(j, val) in &adj[i] {
col_indices.push(j);
values.push(val);
}
row_ptr.push(col_indices.len());
}
Self {
rows: num_nodes,
cols: num_nodes,
row_ptr,
col_indices,
values,
}
}
fn matvec(&self, x: &[f64]) -> Vec<f64> {
let mut y = vec![0.0; self.rows];
for i in 0..self.rows {
let start = self.row_ptr[i];
let end = self.row_ptr[i + 1];
let mut sum = 0.0;
for k in start..end {
sum += self.values[k] * x[self.col_indices[k]];
}
y[i] = sum;
}
y
}
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn matvec_simd(&self, x: &[f64]) -> Vec<f64> {
let mut y = vec![0.0; self.rows];
for i in 0..self.rows {
let start = self.row_ptr[i];
let end = self.row_ptr[i + 1];
let len = end - start;
// Process in chunks of 4 for SIMD
let mut sum = 0.0;
let chunks = len / 4;
let remainder = len % 4;
for c in 0..chunks {
let base = start + c * 4;
let v0 = self.values[base] * x[self.col_indices[base]];
let v1 = self.values[base + 1] * x[self.col_indices[base + 1]];
let v2 = self.values[base + 2] * x[self.col_indices[base + 2]];
let v3 = self.values[base + 3] * x[self.col_indices[base + 3]];
sum += v0 + v1 + v2 + v3;
}
for k in (start + chunks * 4)..(start + chunks * 4 + remainder) {
sum += self.values[k] * x[self.col_indices[k]];
}
y[i] = sum;
}
y
}
}
// ============================================================================
// EIGENVALUE COMPUTATION
// ============================================================================
/// Power iteration for largest eigenvalue
fn power_iteration(matrix: &CsrMatrix, max_iter: usize, tol: f64) -> (f64, Vec<f64>) {
let n = matrix.rows;
if n == 0 {
return (0.0, Vec::new());
}
// Initialize with random-ish vector
let mut v: Vec<f64> = (0..n).map(|i| ((i as f64 + 1.0).sqrt()).sin()).collect();
let mut eigenvalue = 0.0;
// Normalize
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-10 {
for x in &mut v {
*x /= norm;
}
}
for _ in 0..max_iter {
// y = Ax
let y = matrix.matvec(&v);
// Rayleigh quotient: eigenvalue = v^T y / v^T v
let new_eigenvalue: f64 = v.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
// Normalize y
let norm: f64 = y.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-10 {
break;
}
v = y.iter().map(|x| x / norm).collect();
// Check convergence
if (new_eigenvalue - eigenvalue).abs() < tol {
eigenvalue = new_eigenvalue;
break;
}
eigenvalue = new_eigenvalue;
}
(eigenvalue, v)
}
/// Lanczos algorithm for multiple eigenvalues
struct LanczosComputation {
tridiag_alpha: Vec<f64>,
tridiag_beta: Vec<f64>,
basis_vectors: Vec<Vec<f64>>,
}
impl LanczosComputation {
fn compute(matrix: &CsrMatrix, num_eigenvalues: usize, max_iter: usize) -> Self {
let n = matrix.rows;
let k = num_eigenvalues.min(max_iter).min(n);
let mut alpha = Vec::with_capacity(k);
let mut beta = Vec::with_capacity(k);
let mut basis = Vec::with_capacity(k + 1);
// Start with random vector
let mut v: Vec<f64> = (0..n).map(|i| ((i as f64 + 1.0).sqrt()).sin()).collect();
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-10 {
for x in &mut v {
*x /= norm;
}
}
basis.push(v.clone());
let mut w = matrix.matvec(&v);
for i in 0..k {
// alpha_i = v_i^T w
let a: f64 = basis[i].iter().zip(w.iter()).map(|(a, b)| a * b).sum();
alpha.push(a);
// w = w - alpha_i v_i
for (j, wj) in w.iter_mut().enumerate() {
*wj -= a * basis[i][j];
}
// w = w - beta_{i-1} v_{i-1}
if i > 0 && i - 1 < beta.len() {
let b = beta[i - 1];
for (j, wj) in w.iter_mut().enumerate() {
*wj -= b * basis[i - 1][j];
}
}
// beta_i = ||w||
let b: f64 = w.iter().map(|x| x * x).sum::<f64>().sqrt();
if b < 1e-10 || i + 1 >= k {
break;
}
beta.push(b);
// v_{i+1} = w / beta_i
let new_v: Vec<f64> = w.iter().map(|x| x / b).collect();
basis.push(new_v.clone());
// w = A v_{i+1}
w = matrix.matvec(&new_v);
}
Self {
tridiag_alpha: alpha,
tridiag_beta: beta,
basis_vectors: basis,
}
}
fn eigenvalues(&self) -> Vec<f64> {
// Compute eigenvalues of tridiagonal matrix using QR iteration
let n = self.tridiag_alpha.len();
if n == 0 {
return Vec::new();
}
let mut d = self.tridiag_alpha.clone();
let mut e = self.tridiag_beta.clone();
// Simple eigenvalue estimation using Gershgorin circles
let mut eigenvalues = Vec::with_capacity(n);
for i in 0..n {
let off_diag = if i > 0 && i - 1 < e.len() { e[i - 1].abs() } else { 0.0 }
+ if i < e.len() { e[i].abs() } else { 0.0 };
eigenvalues.push(d[i] + off_diag * 0.5); // Center of Gershgorin disk
}
eigenvalues.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
eigenvalues
}
}
// ============================================================================
// CHEEGER CONSTANT
// ============================================================================
/// Compute Cheeger constant (isoperimetric number) approximation
struct CheegerComputation {
graph_edges: Vec<(usize, usize)>,
num_nodes: usize,
}
impl CheegerComputation {
fn new(num_nodes: usize, edges: Vec<(usize, usize)>) -> Self {
Self {
graph_edges: edges,
num_nodes,
}
}
/// Approximate Cheeger constant using spectral methods
/// h(G) >= lambda_2 / 2 (Cheeger inequality)
fn compute_spectral_lower_bound(&self) -> f64 {
let laplacian = CsrMatrix::from_edges(self.num_nodes, &self.graph_edges);
// Find second smallest eigenvalue using deflation
let (lambda_1, v1) = power_iteration(&laplacian, 100, 1e-8);
// Shift to find lambda_2
// We use a simplified approach: estimate from Fiedler vector
let fiedler = self.compute_fiedler_vector(&laplacian, &v1);
let lambda_2 = self.rayleigh_quotient(&laplacian, &fiedler);
lambda_2 / 2.0
}
fn compute_fiedler_vector(&self, laplacian: &CsrMatrix, ground_state: &[f64]) -> Vec<f64> {
let n = laplacian.rows;
// Start with vector orthogonal to ground state
let mut v: Vec<f64> = (0..n).map(|i| ((i as f64 * 2.0 + 1.0).sqrt()).cos()).collect();
// Gram-Schmidt orthogonalization against ground state
let dot: f64 = v.iter().zip(ground_state.iter()).map(|(a, b)| a * b).sum();
for (i, vi) in v.iter_mut().enumerate() {
*vi -= dot * ground_state[i];
}
// Normalize
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-10 {
for vi in &mut v {
*vi /= norm;
}
}
// A few power iterations with orthogonalization
for _ in 0..50 {
let mut y = laplacian.matvec(&v);
// Orthogonalize against ground state
let dot: f64 = y.iter().zip(ground_state.iter()).map(|(a, b)| a * b).sum();
for (i, yi) in y.iter_mut().enumerate() {
*yi -= dot * ground_state[i];
}
// Normalize
let norm: f64 = y.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < 1e-10 {
break;
}
v = y.iter().map(|x| x / norm).collect();
}
v
}
fn rayleigh_quotient(&self, laplacian: &CsrMatrix, v: &[f64]) -> f64 {
let lv = laplacian.matvec(v);
let numerator: f64 = v.iter().zip(lv.iter()).map(|(a, b)| a * b).sum();
let denominator: f64 = v.iter().map(|x| x * x).sum();
if denominator > 1e-10 {
numerator / denominator
} else {
0.0
}
}
/// Direct Cheeger constant computation via sweep cut on Fiedler vector
fn compute_sweep_cut(&self) -> f64 {
let laplacian = CsrMatrix::from_edges(self.num_nodes, &self.graph_edges);
let (_, v1) = power_iteration(&laplacian, 100, 1e-8);
let fiedler = self.compute_fiedler_vector(&laplacian, &v1);
// Sort vertices by Fiedler vector values
let mut indices: Vec<usize> = (0..self.num_nodes).collect();
indices.sort_by(|&a, &b| {
fiedler[a].partial_cmp(&fiedler[b]).unwrap_or(std::cmp::Ordering::Equal)
});
// Sweep through cuts
let mut min_cheeger = f64::MAX;
let mut cut_edges = 0;
let mut left_set: HashSet<usize> = HashSet::new();
for &idx in indices.iter().take(self.num_nodes - 1) {
left_set.insert(idx);
// Update cut size
for &(u, v) in &self.graph_edges {
let u_in = left_set.contains(&u);
let v_in = left_set.contains(&v);
if u_in != v_in {
if (u_in && u == idx) || (v_in && v == idx) {
cut_edges += 1;
}
}
}
// Compute Cheeger ratio
let left_size = left_set.len();
let right_size = self.num_nodes - left_size;
let min_size = left_size.min(right_size);
if min_size > 0 {
let ratio = cut_edges as f64 / min_size as f64;
min_cheeger = min_cheeger.min(ratio);
}
}
min_cheeger
}
}
// ============================================================================
// SPECTRAL CLUSTERING
// ============================================================================
struct SpectralClustering {
num_clusters: usize,
eigenvectors: Vec<Vec<f64>>,
}
impl SpectralClustering {
fn compute(matrix: &CsrMatrix, num_clusters: usize) -> Self {
let lanczos = LanczosComputation::compute(matrix, num_clusters + 1, 100);
// Get first k eigenvectors (corresponding to smallest eigenvalues)
let eigenvectors = lanczos.basis_vectors.into_iter().take(num_clusters).collect();
Self {
num_clusters,
eigenvectors,
}
}
fn cluster_assignments(&self) -> Vec<usize> {
let n = if self.eigenvectors.is_empty() {
0
} else {
self.eigenvectors[0].len()
};
if n == 0 || self.eigenvectors.is_empty() {
return Vec::new();
}
// Simple k-means on spectral embedding
let k = self.num_clusters;
let dim = self.eigenvectors.len();
// Extract embedding matrix (n x dim)
let embedding: Vec<Vec<f64>> = (0..n)
.map(|i| self.eigenvectors.iter().map(|v| v[i]).collect())
.collect();
// Initialize centroids
let mut centroids: Vec<Vec<f64>> = (0..k)
.map(|i| embedding[i * n / k].clone())
.collect();
let mut assignments = vec![0; n];
// K-means iterations
for _ in 0..20 {
// Assign points to nearest centroid
for (i, point) in embedding.iter().enumerate() {
let mut min_dist = f64::MAX;
for (j, centroid) in centroids.iter().enumerate() {
let dist: f64 = point
.iter()
.zip(centroid.iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
if dist < min_dist {
min_dist = dist;
assignments[i] = j;
}
}
}
// Update centroids
let mut counts = vec![0usize; k];
let mut new_centroids = vec![vec![0.0; dim]; k];
for (i, point) in embedding.iter().enumerate() {
let cluster = assignments[i];
counts[cluster] += 1;
for (j, &val) in point.iter().enumerate() {
new_centroids[cluster][j] += val;
}
}
for (j, centroid) in new_centroids.iter_mut().enumerate() {
if counts[j] > 0 {
for val in centroid.iter_mut() {
*val /= counts[j] as f64;
}
}
}
centroids = new_centroids;
}
assignments
}
}
// ============================================================================
// GRAPH GENERATORS
// ============================================================================
fn generate_random_graph(num_nodes: usize, edge_probability: f64, seed: u64) -> Vec<(usize, usize)> {
let mut edges = Vec::new();
let mut rng_state = seed;
for i in 0..num_nodes {
for j in (i + 1)..num_nodes {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let random = (rng_state >> 33) as f64 / (u32::MAX as f64);
if random < edge_probability {
edges.push((i, j));
}
}
}
edges
}
fn generate_planted_partition(
num_clusters: usize,
cluster_size: usize,
p_in: f64,
p_out: f64,
seed: u64,
) -> Vec<(usize, usize)> {
let num_nodes = num_clusters * cluster_size;
let mut edges = Vec::new();
let mut rng_state = seed;
for i in 0..num_nodes {
for j in (i + 1)..num_nodes {
let cluster_i = i / cluster_size;
let cluster_j = j / cluster_size;
let prob = if cluster_i == cluster_j { p_in } else { p_out };
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let random = (rng_state >> 33) as f64 / (u32::MAX as f64);
if random < prob {
edges.push((i, j));
}
}
}
edges
}
// ============================================================================
// BENCHMARKS
// ============================================================================
fn bench_power_iteration(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/power_iteration");
group.sample_size(30);
for &num_nodes in &[100, 500, 1000, 2000, 5000] {
let edges = generate_random_graph(num_nodes, 5.0 / num_nodes as f64, 42);
let matrix = CsrMatrix::from_edges(num_nodes, &edges);
group.throughput(Throughput::Elements(num_nodes as u64));
group.bench_with_input(
BenchmarkId::new("standard", num_nodes),
&matrix,
|b, matrix| {
b.iter(|| {
black_box(power_iteration(black_box(matrix), 100, 1e-8))
})
},
);
}
group.finish();
}
fn bench_lanczos(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/lanczos");
group.sample_size(20);
for &num_nodes in &[500, 1000, 2000, 5000, 10000] {
let edges = generate_random_graph(num_nodes, 5.0 / num_nodes as f64, 42);
let matrix = CsrMatrix::from_edges(num_nodes, &edges);
group.throughput(Throughput::Elements(num_nodes as u64));
for &num_eig in &[5, 10, 20] {
group.bench_with_input(
BenchmarkId::new(format!("{}_eigenvalues", num_eig), num_nodes),
&(&matrix, num_eig),
|b, (matrix, k)| {
b.iter(|| {
let lanczos = LanczosComputation::compute(black_box(matrix), *k, 100);
black_box(lanczos.eigenvalues())
})
},
);
}
}
group.finish();
}
fn bench_cheeger_constant(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/cheeger");
group.sample_size(20);
for &num_nodes in &[100, 500, 1000, 2000] {
let edges = generate_random_graph(num_nodes, 5.0 / num_nodes as f64, 42);
let cheeger = CheegerComputation::new(num_nodes, edges);
group.throughput(Throughput::Elements(num_nodes as u64));
group.bench_with_input(
BenchmarkId::new("spectral_bound", num_nodes),
&cheeger,
|b, cheeger| {
b.iter(|| {
black_box(cheeger.compute_spectral_lower_bound())
})
},
);
group.bench_with_input(
BenchmarkId::new("sweep_cut", num_nodes),
&cheeger,
|b, cheeger| {
b.iter(|| {
black_box(cheeger.compute_sweep_cut())
})
},
);
}
group.finish();
}
fn bench_spectral_clustering(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/clustering");
group.sample_size(20);
for &cluster_size in &[50, 100, 200, 500] {
let num_clusters = 5;
let num_nodes = num_clusters * cluster_size;
let edges = generate_planted_partition(num_clusters, cluster_size, 0.3, 0.01, 42);
let matrix = CsrMatrix::from_edges(num_nodes, &edges);
group.throughput(Throughput::Elements(num_nodes as u64));
group.bench_with_input(
BenchmarkId::new("compute_embedding", num_nodes),
&(&matrix, num_clusters),
|b, (matrix, k)| {
b.iter(|| {
black_box(SpectralClustering::compute(black_box(matrix), *k))
})
},
);
let clustering = SpectralClustering::compute(&matrix, num_clusters);
group.bench_with_input(
BenchmarkId::new("assign_clusters", num_nodes),
&clustering,
|b, clustering| {
b.iter(|| {
black_box(clustering.cluster_assignments())
})
},
);
}
group.finish();
}
fn bench_matvec_simd(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/matvec");
group.sample_size(50);
for &num_nodes in &[1000, 5000, 10000] {
let edges = generate_random_graph(num_nodes, 10.0 / num_nodes as f64, 42);
let matrix = CsrMatrix::from_edges(num_nodes, &edges);
let x: Vec<f64> = (0..num_nodes).map(|i| (i as f64).sin()).collect();
group.throughput(Throughput::Elements(num_nodes as u64));
group.bench_with_input(
BenchmarkId::new("standard", num_nodes),
&(&matrix, &x),
|b, (matrix, x)| {
b.iter(|| {
black_box(matrix.matvec(black_box(x)))
})
},
);
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
group.bench_with_input(
BenchmarkId::new("simd", num_nodes),
&(&matrix, &x),
|b, (matrix, x)| {
b.iter(|| {
black_box(matrix.matvec_simd(black_box(x)))
})
},
);
}
group.finish();
}
fn bench_graph_laplacian_construction(c: &mut Criterion) {
let mut group = c.benchmark_group("spectral/laplacian_construction");
group.sample_size(30);
for &num_nodes in &[500, 1000, 5000, 10000] {
let edges = generate_random_graph(num_nodes, 5.0 / num_nodes as f64, 42);
group.throughput(Throughput::Elements(num_nodes as u64));
group.bench_with_input(
BenchmarkId::new("csr_format", num_nodes),
&(num_nodes, &edges),
|b, (n, edges)| {
b.iter(|| {
black_box(CsrMatrix::from_edges(*n, black_box(edges)))
})
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_power_iteration,
bench_lanczos,
bench_cheeger_constant,
bench_spectral_clustering,
bench_matvec_simd,
bench_graph_laplacian_construction,
);
criterion_main!(benches);