1use std::ops::Range;
32
33use cubecl::prelude::*;
34use cubecl::server::Handle;
35use dspk_core::compute::LaunchGeometry;
36
37use super::eigen::{symmetric_eigen, EigenOptions, SymmetricEigen};
38use super::tridiagonal::symmetric_eigen_cpu;
39use super::kernels::{add_assign_kernel, centre_columns_kernel, gather_rows_kernel};
40use super::matmul::{matmul, MatrixView};
41use crate::core::buffer;
42
43pub const HOST_EIGEN_MAX: usize = 512;
46
47pub const GRAM_ROWS: usize = 4096;
49
50pub trait RowSource {
52 fn rows(&self) -> usize;
53 fn dim(&self) -> usize;
54 fn batch(&self, client: &Client, range: Range<usize>) -> Handle;
56}
57
58#[derive(Debug, Clone, Copy)]
60pub struct HostRows<'a> {
61 pub data: &'a [f32],
62 pub dim: usize,
63}
64
65impl RowSource for HostRows<'_> {
66 fn rows(&self) -> usize {
67 if self.dim == 0 { 0 } else { self.data.len() / self.dim }
68 }
69 fn dim(&self) -> usize {
70 self.dim
71 }
72 fn batch(&self, client: &Client, range: Range<usize>) -> Handle {
73 buffer::upload(client, &self.data[range.start * self.dim..range.end * self.dim])
74 }
75}
76
77#[derive(Debug, Clone, Copy)]
80pub struct IndexedRows<'a> {
81 pub data: &'a [f32],
82 pub dim: usize,
83 pub rows: &'a [usize],
84}
85
86impl RowSource for IndexedRows<'_> {
87 fn rows(&self) -> usize {
88 self.rows.len()
89 }
90 fn dim(&self) -> usize {
91 self.dim
92 }
93 fn batch(&self, client: &Client, range: Range<usize>) -> Handle {
94 let d = self.dim;
95 let mut out = Vec::with_capacity(range.len() * d);
96 for &i in &self.rows[range] {
97 out.extend_from_slice(&self.data[i * d..(i + 1) * d]);
98 }
99 buffer::upload(client, &out)
100 }
101}
102
103#[derive(Debug, Clone, Copy)]
106pub struct DeviceRows<'a> {
107 pub data: &'a Handle,
108 pub len: usize,
109 pub dim: usize,
110 pub rows: &'a [u32],
111}
112
113impl RowSource for DeviceRows<'_> {
114 fn rows(&self) -> usize {
115 self.rows.len()
116 }
117 fn dim(&self) -> usize {
118 self.dim
119 }
120 fn batch(&self, client: &Client, range: Range<usize>) -> Handle {
121 let b = range.len();
122 let total = b * self.dim;
123 let out = buffer::empty::<f32>(client, total.max(1));
124 if total > 0 {
125 let geom = LaunchGeometry::elementwise(client, total);
126 unsafe {
128 gather_rows_kernel::launch::<f32>(
129 client,
130 geom.cube_count,
131 geom.cube_dim,
132 BufferArg::from_raw_parts(self.data.clone(), self.len),
133 BufferArg::from_raw_parts(buffer::upload(client, &self.rows[range]), b),
134 BufferArg::from_raw_parts(out.clone(), total),
135 self.dim as u32,
136 total as u32,
137 );
138 }
139 }
140 out
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq)]
146pub struct TopComponentsOptions {
147 pub exact_cap: usize,
149 pub oversamples: usize,
151 pub randomized_iterations: Option<usize>,
153 pub tolerance: f64,
155 pub max_iterations: usize,
156 pub seed: u64,
157 pub batch_elements: usize,
159 pub dual_max_rows: usize,
162}
163
164impl Default for TopComponentsOptions {
165 fn default() -> Self {
166 Self {
167 exact_cap: 8000,
168 oversamples: 10,
169 randomized_iterations: None,
170 tolerance: 1e-6,
171 max_iterations: 300,
172 seed: 0,
173 batch_elements: 1 << 25,
174 dual_max_rows: 0,
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct TopComponents {
182 pub dim: usize,
183 pub k: usize,
184 pub mean: Vec<f32>,
186 pub components: Vec<f32>,
188 pub variances: Vec<f64>,
190 pub iterations: usize,
192 mean_h: Handle,
193 v_h: Handle,
195}
196
197struct Normal(u64);
199
200impl Normal {
201 fn uniform(&mut self) -> f64 {
202 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
203 let mut z = self.0;
204 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
205 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
206 (((z ^ (z >> 31)) >> 11) as f64 + 0.5) / (1u64 << 53) as f64
207 }
208 fn next(&mut self) -> f64 {
209 let (u, v) = (self.uniform(), self.uniform());
210 (-2.0 * u.ln()).sqrt() * (std::f64::consts::TAU * v).cos()
211 }
212}
213
214enum Operator<'a, S: RowSource> {
216 Formed(Handle),
218 Implicit(&'a S),
219}
220
221impl TopComponents {
222 pub fn fit<S: RowSource>(client: &Client, src: &S, k: usize, opts: &TopComponentsOptions) -> Self {
228 let (l, d) = (src.rows(), src.dim());
229 assert!(l > 0 && d > 0, "PCA of {l} rows of {d} features");
230 let k = k.min(l).min(d);
231 let batch_rows = (opts.batch_elements / d).clamp(1, GRAM_ROWS);
232
233 let mut sums = vec![0.0f64; d];
235 let ones = buffer::filled::<f32>(client, batch_rows, 1.0);
236 let row = buffer::empty::<f32>(client, d);
237 for r in batches(l, batch_rows) {
238 let b = r.len();
239 let x = src.batch(client, r);
240 matmul::<f32>(client, &MatrixView::row_major(&ones, batch_rows, 1, b), &MatrixView::row_major(&x, b * d, b, d), &row, d);
241 for (s, v) in sums.iter_mut().zip(buffer::download::<f32>(client, row.clone())) {
242 *s += v as f64;
243 }
244 }
245 let mean: Vec<f32> = sums.iter().map(|s| (s / l as f64) as f32).collect();
246 let mean_h = buffer::upload(client, &mean);
247 let denom = (l.max(2) - 1) as f64;
248 if l <= opts.dual_max_rows && l < d && l * d <= opts.batch_elements {
249 return Self::fit_dual(client, src, k, mean, mean_h, denom);
250 }
251
252 let r = (k + opts.oversamples).min(d);
253 let exact = d <= opts.exact_cap;
254 let operator = if exact {
255 let c = buffer::zeros::<f32>(client, d * d);
256 let part = buffer::empty::<f32>(client, d * d);
257 for rr in batches(l, batch_rows) {
258 let b = rr.len();
259 let x = centred(client, src, rr, &mean_h);
260 let xv = MatrixView::row_major(&x, b * d, b, d);
261 matmul::<f32>(client, &xv.transposed(), &xv, &part, d * d);
262 add_assign(client, &c, &part, d * d);
263 }
264 if 2 * r >= d {
265 let eig = eigen_of(client, &c, d);
266 let vectors: Vec<f64> = (0..d * k).map(|e| eig.vectors[(e / k) * d + e % k]).collect();
267 let values = eig.values[..k].iter().map(|v| v.max(0.0) / denom).collect();
268 return Self::finish(client, d, k, mean, mean_h, vectors, values, 0);
269 }
270 Operator::Formed(c)
271 } else {
272 Operator::Implicit(src)
273 };
274 let iterations = if exact {
275 opts.max_iterations
276 } else {
277 opts.randomized_iterations.unwrap_or(if (k as f64) < 0.1 * l.min(d) as f64 { 7 } else { 4 })
278 };
279
280 let mut rng = Normal(opts.seed);
281 let start: Vec<f32> = (0..d * r).map(|_| rng.next() as f32).collect();
282 let (mut q, _) = orthonormalise(client, &buffer::upload(client, &start), d, r);
283 let mut previous: Option<Vec<f64>> = None;
284 let mut ran = 0;
285 for _ in 0..iterations {
286 ran += 1;
287 let y = apply(client, &operator, &q, d, r, l, batch_rows, &mean_h);
288 let (next, sigmas) = orthonormalise(client, &y, d, r);
289 q = next;
290 if exact {
291 let lead = &sigmas[..k];
292 if let Some(p) = &previous {
293 let change = lead.iter().zip(p).map(|(a, b)| (a - b).abs() / a.abs().max(f64::MIN_POSITIVE)).fold(0.0, f64::max);
294 if change < opts.tolerance {
295 break;
296 }
297 }
298 previous = Some(lead.to_vec());
299 }
300 }
301 let y = apply(client, &operator, &q, d, r, l, batch_rows, &mean_h);
303 let bmat = buffer::empty::<f32>(client, r * r);
304 let qv = MatrixView::row_major(&q, d * r, d, r);
305 matmul::<f32>(client, &qv.transposed(), &MatrixView::row_major(&y, d * r, d, r), &bmat, r * r);
306 let mut bh = buffer::download::<f32>(client, bmat);
308 for i in 0..r {
309 for j in 0..i {
310 let m = 0.5 * (bh[i * r + j] + bh[j * r + i]);
311 bh[i * r + j] = m;
312 bh[j * r + i] = m;
313 }
314 }
315 let eig = if r <= HOST_EIGEN_MAX {
316 symmetric_eigen_cpu(&bh.iter().map(|&v| v as f64).collect::<Vec<_>>(), r)
317 } else {
318 symmetric_eigen::<f32>(client, &buffer::upload(client, &bh), r, EigenOptions::default())
319 };
320 let u: Vec<f32> = (0..r * k).map(|e| eig.vectors[(e / k) * r + e % k] as f32).collect();
321 let v = buffer::empty::<f32>(client, d * k);
322 matmul::<f32>(client, &qv, &MatrixView::row_major(&buffer::upload(client, &u), r * k, r, k), &v, d * k);
323 let vectors: Vec<f64> = buffer::download::<f32>(client, v).into_iter().map(f64::from).collect();
324 let values = eig.values[..k].iter().map(|v| v.max(0.0) / denom).collect();
325 Self::finish(client, d, k, mean, mean_h, vectors, values, ran)
326 }
327
328 fn fit_dual<S: RowSource>(client: &Client, src: &S, k: usize, mean: Vec<f32>, mean_h: Handle, denom: f64) -> Self {
330 let (l, d) = (src.rows(), src.dim());
331 let x = centred(client, src, 0..l, &mean_h);
332 let xv = MatrixView::row_major(&x, l * d, l, d);
333 let g = buffer::empty::<f32>(client, l * l);
334 matmul::<f32>(client, &xv, &xv.transposed(), &g, l * l);
335 let host: Vec<f64> = buffer::download::<f32>(client, g).into_iter().take(l * l).map(f64::from).collect();
336 let sym: Vec<f64> = (0..l * l).map(|e| 0.5 * (host[e] + host[(e % l) * l + e / l])).collect();
338 let eig = symmetric_eigen_cpu(&sym, l);
339 let top = eig.values.first().copied().unwrap_or(0.0).max(0.0);
342 let floor = top * f32::EPSILON as f64;
343 let mut w = vec![0.0f32; l * k];
344 for c in 0..k {
345 let lambda = eig.values[c];
346 if lambda > floor && lambda > 0.0 {
347 let s = 1.0 / lambda.sqrt();
348 for i in 0..l {
349 w[i * k + c] = (eig.vectors[i * l + c] * s) as f32;
350 }
351 }
352 }
353 let v = buffer::empty::<f32>(client, d * k);
354 matmul::<f32>(client, &xv.transposed(), &MatrixView::row_major(&buffer::upload(client, &w), l * k, l, k), &v, d * k);
355 let vectors: Vec<f64> = buffer::download::<f32>(client, v).into_iter().map(f64::from).collect();
356 let values = eig.values[..k].iter().map(|v| v.max(0.0) / denom).collect();
357 Self::finish(client, d, k, mean, mean_h, vectors, values, 0)
358 }
359
360 #[allow(clippy::too_many_arguments)]
362 fn finish(client: &Client, d: usize, k: usize, mean: Vec<f32>, mean_h: Handle, vectors: Vec<f64>, variances: Vec<f64>, iterations: usize) -> Self {
363 let mut components = vec![0.0f32; k * d];
364 for c in 0..k {
365 let big = (0..d).map(|t| vectors[t * k + c]).fold(0.0f64, |m, v| if v.abs() > m.abs() { v } else { m });
366 let sign = if big < 0.0 { -1.0 } else { 1.0 };
367 for t in 0..d {
368 components[c * d + t] = (sign * vectors[t * k + c]) as f32;
369 }
370 }
371 let columns: Vec<f32> = (0..d * k).map(|e| components[(e % k) * d + e / k]).collect();
372 Self { dim: d, k, mean, components, variances, iterations, mean_h, v_h: buffer::upload(client, &columns) }
373 }
374
375 pub fn transform<S: RowSource>(&self, client: &Client, src: &S, batch_elements: usize) -> Vec<f32> {
381 let (l, d, k) = (src.rows(), self.dim, self.k);
382 assert_eq!(src.dim(), d, "rows of {} features, fitted on {d}", src.dim());
383 let batch_rows = (batch_elements / d.max(k)).max(1);
384 let mut out = Vec::with_capacity(l * k);
385 let f = buffer::empty::<f32>(client, batch_rows * k);
386 for r in batches(l, batch_rows) {
387 let b = r.len();
388 let x = centred(client, src, r, &self.mean_h);
389 matmul::<f32>(client, &MatrixView::row_major(&x, b * d, b, d), &MatrixView::row_major(&self.v_h, d * k, d, k), &f, batch_rows * k);
390 out.extend(buffer::download_prefix::<f32>(client, f.clone(), b * k));
391 }
392 out
393 }
394}
395
396fn eigen_of(client: &Client, m: &Handle, n: usize) -> SymmetricEigen {
399 if n <= HOST_EIGEN_MAX {
400 let host: Vec<f64> = buffer::download::<f32>(client, m.clone()).into_iter().take(n * n).map(f64::from).collect();
401 let sym: Vec<f64> = (0..n * n).map(|e| 0.5 * (host[e] + host[(e % n) * n + e / n])).collect();
403 symmetric_eigen_cpu(&sym, n)
404 } else {
405 symmetric_eigen::<f32>(client, m, n, EigenOptions::default())
406 }
407}
408
409fn batches(l: usize, rows: usize) -> impl Iterator<Item = Range<usize>> {
410 (0..l.div_ceil(rows)).map(move |i| i * rows..((i + 1) * rows).min(l))
411}
412
413fn centred<S: RowSource>(client: &Client, src: &S, r: Range<usize>, mean: &Handle) -> Handle {
415 let (b, d) = (r.len(), src.dim());
416 let x = src.batch(client, r);
417 let geom = LaunchGeometry::elementwise(client, b * d);
418 unsafe {
420 centre_columns_kernel::launch::<f32>(
421 client,
422 geom.cube_count,
423 geom.cube_dim,
424 BufferArg::from_raw_parts(x.clone(), b * d),
425 BufferArg::from_raw_parts(mean.clone(), d),
426 d as u32,
427 (b * d) as u32,
428 );
429 }
430 x
431}
432
433fn add_assign(client: &Client, acc: &Handle, x: &Handle, n: usize) {
434 let geom = LaunchGeometry::elementwise(client, n);
435 unsafe {
437 add_assign_kernel::launch::<f32>(
438 client,
439 geom.cube_count,
440 geom.cube_dim,
441 BufferArg::from_raw_parts(acc.clone(), n),
442 BufferArg::from_raw_parts(x.clone(), n),
443 n as u32,
444 );
445 }
446}
447
448#[allow(clippy::too_many_arguments)]
450fn apply<S: RowSource>(client: &Client, op: &Operator<'_, S>, q: &Handle, d: usize, r: usize, l: usize, batch_rows: usize, mean: &Handle) -> Handle {
451 let qv = MatrixView::row_major(q, d * r, d, r);
452 let y = buffer::empty::<f32>(client, d * r);
453 match op {
454 Operator::Formed(c) => matmul::<f32>(client, &MatrixView::row_major(c, d * d, d, d), &qv, &y, d * r),
455 Operator::Implicit(src) => {
456 let acc = buffer::zeros::<f32>(client, d * r);
457 let z = buffer::empty::<f32>(client, batch_rows * r);
458 for rr in batches(l, batch_rows) {
459 let b = rr.len();
460 let x = centred(client, *src, rr, mean);
461 let xv = MatrixView::row_major(&x, b * d, b, d);
462 matmul::<f32>(client, &xv, &qv, &z, batch_rows * r);
463 matmul::<f32>(client, &xv.transposed(), &MatrixView::row_major(&z, b * r, b, r), &y, d * r);
464 add_assign(client, &acc, &y, d * r);
465 }
466 return acc;
467 }
468 }
469 y
470}
471
472fn orthonormalise(client: &Client, y: &Handle, d: usize, r: usize) -> (Handle, Vec<f64>) {
475 let yv = MatrixView::row_major(y, d * r, d, r);
476 let g = buffer::empty::<f32>(client, r * r);
477 matmul::<f32>(client, &yv.transposed(), &yv, &g, r * r);
478 let eig = eigen_of(client, &g, r);
479 let top = eig.values.first().copied().unwrap_or(0.0).max(0.0);
480 let floor = top * f32::EPSILON as f64;
481 let mut t = vec![0.0f32; r * r];
482 for c in 0..r {
483 let lambda = eig.values[c];
484 if lambda > floor && lambda > 0.0 {
485 let s = 1.0 / lambda.sqrt();
486 for i in 0..r {
487 t[i * r + c] = (eig.vectors[i * r + c] * s) as f32;
488 }
489 }
490 }
491 let q = buffer::empty::<f32>(client, d * r);
492 matmul::<f32>(client, &yv, &MatrixView::row_major(&buffer::upload(client, &t), r * r, r, r), &q, d * r);
493 (q, eig.values.iter().map(|v| v.max(0.0).sqrt()).collect())
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::linalg::symmetric_eigen_host;
500
501 fn data(l: usize, d: usize, seed: u64) -> Vec<f32> {
503 let mut rng = Normal(seed);
504 let scales: Vec<f64> = (0..d).map(|i| 10.0 * 0.8f64.powi(i as i32) + 0.01).collect();
505 (0..l)
507 .flat_map(|_| {
508 let z: Vec<f64> = scales.iter().map(|s| s * rng.next()).collect();
509 (0..d).map(move |j| (z[j] + 0.5 * z[(j + 1) % d] + 3.0) as f32).collect::<Vec<_>>()
510 })
511 .collect()
512 }
513
514 fn reference(client: &Client, x: &[f32], l: usize, d: usize, k: usize) -> Vec<f64> {
516 let mean: Vec<f64> = (0..d).map(|j| (0..l).map(|i| x[i * d + j] as f64).sum::<f64>() / l as f64).collect();
517 let mut c = vec![0.0f64; d * d];
518 for i in 0..l {
519 for a in 0..d {
520 for b in 0..d {
521 c[a * d + b] += (x[i * d + a] as f64 - mean[a]) * (x[i * d + b] as f64 - mean[b]);
522 }
523 }
524 }
525 c.iter_mut().for_each(|v| *v /= (l - 1) as f64);
526 symmetric_eigen_host::<f32>(client, &c, d, EigenOptions::default()).values[..k].to_vec()
527 }
528
529 fn check(client: &Client, opts: TopComponentsOptions, iterate: bool) {
530 let (l, d, k) = (3000usize, 64usize, 5usize);
531 let x = data(l, d, 1);
532 let src = HostRows { data: &x, dim: d };
533 let fit = TopComponents::fit(client, &src, k, &opts);
534 assert_eq!(fit.iterations > 0, iterate, "{opts:?}");
535 let want = reference(client, &x, l, d, k);
536 for (g, w) in fit.variances.iter().zip(&want) {
537 assert!((g - w).abs() < 2e-3 * w, "variances {:?} vs {want:?}", fit.variances);
538 }
539 for a in 0..k {
541 for b in 0..k {
542 let dot: f32 = (0..d).map(|t| fit.components[a * d + t] * fit.components[b * d + t]).sum();
543 assert!((dot - if a == b { 1.0 } else { 0.0 }).abs() < 1e-3, "<{a},{b}> = {dot}");
544 }
545 }
546 let f = fit.transform(client, &src, 1 << 12);
547 assert_eq!(f.len(), l * k);
548 let var0 = (0..l).map(|i| (f[i * k] as f64).powi(2)).sum::<f64>() / (l - 1) as f64;
549 assert!((var0 - want[0]).abs() < 2e-3 * want[0], "{var0} vs {}", want[0]);
550 }
551
552 fn all_paths(client: &Client) {
553 let base = TopComponentsOptions { batch_elements: 64 * 100, ..Default::default() };
555 check(client, TopComponentsOptions { oversamples: 30, ..base }, false);
557 check(client, TopComponentsOptions { oversamples: 3, ..base }, true);
559 check(client, TopComponentsOptions { exact_cap: 10, oversamples: 10, randomized_iterations: Some(12), ..base }, true);
561 }
562 runtime_test!(test_top_components_match_the_covariance, all_paths);
563
564 fn dual_path(client: &Client) {
567 let (l, d, k) = (120usize, 300usize, 100usize);
568 let x = data(l, d, 2);
569 let src = HostRows { data: &x, dim: d };
570 let opts = TopComponentsOptions { dual_max_rows: 4096, ..Default::default() };
571 let fit = TopComponents::fit(client, &src, k, &opts);
572 assert_eq!((fit.iterations, fit.k), (0, k));
573 let want = reference(client, &x, l, d, 10);
574 for (g, w) in fit.variances.iter().zip(&want) {
575 assert!((g - w).abs() < 2e-3 * w, "variances {:?} vs {want:?}", &fit.variances[..10]);
576 }
577 for a in 0..10 {
578 for b in 0..10 {
579 let dot: f32 = (0..d).map(|t| fit.components[a * d + t] * fit.components[b * d + t]).sum();
580 assert!((dot - if a == b { 1.0 } else { 0.0 }).abs() < 1e-3, "<{a},{b}> = {dot}");
581 }
582 }
583 }
584 runtime_test!(test_top_components_dual_path, dual_path);
585}