1use std::sync::Arc;
10
11use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
12use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
13
14use crate::math::dot_partial::{dot_partial, OP_ID as DOT_PARTIAL_OP_ID};
15use crate::math::symmetric_eigen_jacobi::jacobi_eigen_region;
16
17fn tt_idx(row: Expr, cols: u32, col: Expr) -> Expr {
19 Expr::add(Expr::mul(row, Expr::u32(cols)), col)
20}
21
22pub const OP_ID: &str = "vyre-primitives::math::tensor_train_decompose";
24
25#[must_use]
36pub fn tensor_train_decompose_step(
37 input_matrix: &str,
38 u_out: &str,
39 rem_out: &str,
40 r_prev: u32,
41 nk: u32,
42 rem: u32,
43 r_next: u32,
44) -> Program {
45 let Some(input_rows) = r_prev.checked_mul(nk) else {
46 return crate::invalid_output_program(
47 OP_ID,
48 u_out,
49 DataType::F32,
50 "Fix: tensor_train_decompose_step r_prev * nk must fit in u32.".to_owned(),
51 );
52 };
53 let Some(input_count) = input_rows.checked_mul(rem) else {
54 return crate::invalid_output_program(
55 OP_ID,
56 u_out,
57 DataType::F32,
58 "Fix: tensor_train_decompose_step input count must fit in u32.".to_owned(),
59 );
60 };
61 let Some(u_count) = input_rows.checked_mul(r_next) else {
62 return crate::invalid_output_program(
63 OP_ID,
64 u_out,
65 DataType::F32,
66 "Fix: tensor_train_decompose_step core count must fit in u32.".to_owned(),
67 );
68 };
69 let Some(rem_count) = r_next.checked_mul(rem) else {
70 return crate::invalid_output_program(
71 OP_ID,
72 u_out,
73 DataType::F32,
74 "Fix: tensor_train_decompose_step remainder count must fit in u32.".to_owned(),
75 );
76 };
77 let Some(gram_count) = rem.checked_mul(rem) else {
78 return crate::invalid_output_program(
79 OP_ID,
80 u_out,
81 DataType::F32,
82 "Fix: tensor_train_decompose_step Gram matrix count must fit in u32.".to_owned(),
83 );
84 };
85 if r_prev == 0 || nk == 0 || rem == 0 || r_next == 0 {
86 return crate::invalid_output_program(
87 OP_ID,
88 u_out,
89 DataType::F32,
90 "Fix: tensor_train_decompose_step dimensions and ranks must be non-zero.".to_owned(),
91 );
92 }
93 if r_next > rem {
94 return crate::invalid_output_program(
95 OP_ID,
96 u_out,
97 DataType::F32,
98 "Fix: tensor_train_decompose_step requires r_next <= rem for emitted rank columns."
99 .to_owned(),
100 );
101 }
102
103 let m = input_rows;
113 let n = rem;
114 let neg_big = Expr::f32(-1.0e30);
115 let mut body: Vec<Node> = Vec::new();
116
117 body.push(Node::loop_for(
119 "tt_ca",
120 Expr::u32(0),
121 Expr::u32(n),
122 vec![Node::loop_for(
123 "tt_cb",
124 Expr::u32(0),
125 Expr::u32(n),
126 vec![
127 Node::let_bind("tt_gacc", Expr::f32(0.0)),
128 Node::loop_for(
129 "tt_grow",
130 Expr::u32(0),
131 Expr::u32(m),
132 vec![Node::assign(
133 "tt_gacc",
134 Expr::add(
135 Expr::var("tt_gacc"),
136 Expr::mul(
137 Expr::load(
138 input_matrix,
139 tt_idx(Expr::var("tt_grow"), n, Expr::var("tt_ca")),
140 ),
141 Expr::load(
142 input_matrix,
143 tt_idx(Expr::var("tt_grow"), n, Expr::var("tt_cb")),
144 ),
145 ),
146 ),
147 )],
148 ),
149 Node::store(
150 "tt_ata",
151 tt_idx(Expr::var("tt_ca"), n, Expr::var("tt_cb")),
152 Expr::var("tt_gacc"),
153 ),
154 ],
155 )],
156 ));
157
158 body.push(jacobi_eigen_region(
163 OP_ID, "tt_ata", "tt_evec", "tt_eval", n,
164 ));
165
166 body.push(Node::loop_for(
168 "tt_rank",
169 Expr::u32(0),
170 Expr::u32(r_next),
171 vec![
172 Node::let_bind("tt_best", neg_big.clone()),
174 Node::let_bind("tt_eidx", Expr::u32(0)),
175 Node::loop_for(
176 "tt_e",
177 Expr::u32(0),
178 Expr::u32(n),
179 vec![
180 Node::let_bind("tt_ev", Expr::load("tt_eval", Expr::var("tt_e"))),
181 Node::let_bind("tt_gt", Expr::gt(Expr::var("tt_ev"), Expr::var("tt_best"))),
182 Node::assign(
183 "tt_eidx",
184 Expr::select(Expr::var("tt_gt"), Expr::var("tt_e"), Expr::var("tt_eidx")),
185 ),
186 Node::assign(
187 "tt_best",
188 Expr::select(Expr::var("tt_gt"), Expr::var("tt_ev"), Expr::var("tt_best")),
189 ),
190 ],
191 ),
192 Node::let_bind(
194 "tt_sigma",
195 Expr::sqrt(Expr::max(Expr::var("tt_best"), Expr::f32(0.0))),
196 ),
197 Node::loop_for(
199 "tt_vc",
200 Expr::u32(0),
201 Expr::u32(n),
202 vec![Node::store(
203 rem_out,
204 tt_idx(Expr::var("tt_rank"), n, Expr::var("tt_vc")),
205 Expr::load(
206 "tt_evec",
207 tt_idx(Expr::var("tt_vc"), n, Expr::var("tt_eidx")),
208 ),
209 )],
210 ),
211 Node::loop_for(
213 "tt_ur",
214 Expr::u32(0),
215 Expr::u32(m),
216 vec![
217 Node::let_bind("tt_dot", Expr::f32(0.0)),
218 Node::Region {
223 generator: Ident::from(DOT_PARTIAL_OP_ID),
224 source_region: Some(GeneratorRef {
225 name: OP_ID.to_string(),
226 }),
227 body: Arc::new(vec![dot_partial(
228 input_matrix,
229 rem_out,
230 "tt_dot",
231 Expr::mul(Expr::var("tt_ur"), Expr::u32(n)),
232 Expr::mul(Expr::var("tt_rank"), Expr::u32(n)),
233 n,
234 )]),
235 },
236 Node::store(
237 u_out,
238 tt_idx(Expr::var("tt_ur"), r_next, Expr::var("tt_rank")),
239 Expr::select(
240 Expr::gt(Expr::var("tt_sigma"), Expr::f32(1.0e-6)),
241 Expr::div(Expr::var("tt_dot"), Expr::var("tt_sigma")),
242 Expr::f32(0.0),
243 ),
244 ),
245 ],
246 ),
247 Node::loop_for(
249 "tt_sc",
250 Expr::u32(0),
251 Expr::u32(n),
252 vec![Node::store(
253 rem_out,
254 tt_idx(Expr::var("tt_rank"), n, Expr::var("tt_sc")),
255 Expr::mul(
256 Expr::load(rem_out, tt_idx(Expr::var("tt_rank"), n, Expr::var("tt_sc"))),
257 Expr::var("tt_sigma"),
258 ),
259 )],
260 ),
261 Node::store("tt_eval", Expr::var("tt_eidx"), neg_big.clone()),
263 ],
264 ));
265
266 Program::wrapped(
267 vec![
268 BufferDecl::storage(input_matrix, 0, BufferAccess::ReadOnly, DataType::F32)
269 .with_count(input_count),
270 BufferDecl::storage(u_out, 1, BufferAccess::ReadWrite, DataType::F32)
271 .with_count(u_count),
272 BufferDecl::storage(rem_out, 2, BufferAccess::ReadWrite, DataType::F32)
273 .with_count(rem_count),
274 BufferDecl::storage("tt_ata", 3, BufferAccess::ReadWrite, DataType::F32)
275 .with_count(gram_count),
276 BufferDecl::storage("tt_evec", 4, BufferAccess::ReadWrite, DataType::F32)
277 .with_count(gram_count),
278 BufferDecl::storage("tt_eval", 5, BufferAccess::ReadWrite, DataType::F32).with_count(n),
279 ],
280 [1, 1, 1],
281 vec![Node::Region {
282 generator: Ident::from(OP_ID),
283 source_region: None,
284 body: Arc::new(vec![Node::if_then(
285 Expr::eq(Expr::InvocationId { axis: 0 }, Expr::u32(0)),
286 body,
287 )]),
288 }],
289 )
290}
291
292#[cfg(any(test, feature = "cpu-parity"))]
294#[must_use]
295pub fn cpu_ref(tensor: &[f64], dims: &[u32], target_ranks: &[u32]) -> Vec<Vec<f64>> {
296 let mut cores = Vec::new();
297 let mut scratch = TensorTrainCpuScratch::default();
298 cpu_ref_into(tensor, dims, target_ranks, &mut cores, &mut scratch);
299 cores
300}
301
302#[cfg(any(test, feature = "cpu-parity"))]
304#[derive(Debug, Default)]
305pub struct TensorTrainCpuScratch {
306 c: Vec<f64>,
307 next_c: Vec<f64>,
308 u: Vec<f64>,
309 s: Vec<f64>,
310 vt: Vec<f64>,
311 ata: Vec<f64>,
312 eigenvalues: Vec<f64>,
313 eigenvectors: Vec<f64>,
314 order: Vec<usize>,
315}
316
317#[cfg(any(test, feature = "cpu-parity"))]
318impl TensorTrainCpuScratch {
319 #[must_use]
321 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn clear(&mut self) {
327 self.c.clear();
328 self.next_c.clear();
329 self.u.clear();
330 self.s.clear();
331 self.vt.clear();
332 self.ata.clear();
333 self.eigenvalues.clear();
334 self.eigenvectors.clear();
335 self.order.clear();
336 }
337}
338
339#[cfg(any(test, feature = "cpu-parity"))]
341pub fn cpu_ref_into(
342 tensor: &[f64],
343 dims: &[u32],
344 target_ranks: &[u32],
345 cores: &mut Vec<Vec<f64>>,
346 scratch: &mut TensorTrainCpuScratch,
347) {
348 let d = dims.len();
349 if d == 0 || dims.iter().any(|&dim| dim == 0) || target_ranks.len() != d + 1 {
350 cores.clear();
351 scratch.clear();
352 return;
353 }
354 let Some(expected_len) = dims
355 .iter()
356 .try_fold(1usize, |acc, &dim| acc.checked_mul(dim as usize))
357 else {
358 cores.clear();
359 scratch.clear();
360 return;
361 };
362
363 scratch.c.clear();
364 scratch.c.resize(expected_len, 0.0);
365 let copy_len = expected_len.min(tensor.len());
366 scratch.c[..copy_len].copy_from_slice(&tensor[..copy_len]);
367 let mut r_prev = 1usize;
368 let mut core_index = 0usize;
369
370 for k in 0..(d - 1) {
371 let nk = dims[k] as usize;
372 let r_next = (target_ranks[k + 1] as usize).max(1);
373 let m = r_prev * nk;
374 if m == 0 || scratch.c.len() % m != 0 {
375 cores.truncate(core_index);
376 return;
377 }
378 let n = scratch.c.len() / m;
379
380 truncated_svd_into(
381 &scratch.c,
382 m,
383 n,
384 r_next,
385 &mut scratch.u,
386 &mut scratch.s,
387 &mut scratch.vt,
388 &mut scratch.ata,
389 &mut scratch.eigenvalues,
390 &mut scratch.eigenvectors,
391 &mut scratch.order,
392 );
393
394 write_core(cores, core_index, &scratch.u);
395 core_index += 1;
396
397 scratch.next_c.clear();
398 scratch.next_c.resize(r_next * n, 0.0);
399 for i in 0..r_next {
400 for j in 0..n {
401 scratch.next_c[i * n + j] = scratch.s[i] * scratch.vt[i * n + j];
402 }
403 }
404 std::mem::swap(&mut scratch.c, &mut scratch.next_c);
405 r_prev = r_next;
406 }
407 write_core(cores, core_index, &scratch.c);
408 core_index += 1;
409 cores.truncate(core_index);
410}
411
412#[cfg(any(test, feature = "cpu-parity"))]
413fn write_core(cores: &mut Vec<Vec<f64>>, index: usize, values: &[f64]) {
414 if index == cores.len() {
415 cores.push(Vec::new());
416 }
417 cores[index].clear();
418 cores[index].extend_from_slice(values);
419}
420
421#[cfg(any(test, feature = "cpu-parity"))]
422fn truncated_svd(matrix: &[f64], m: usize, n: usize, r: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
423 let mut u = Vec::new();
424 let mut s = Vec::new();
425 let mut vt = Vec::new();
426 let mut ata = Vec::new();
427 let mut eigenvalues = Vec::new();
428 let mut eigenvectors = Vec::new();
429 let mut order = Vec::new();
430 truncated_svd_into(
431 matrix,
432 m,
433 n,
434 r,
435 &mut u,
436 &mut s,
437 &mut vt,
438 &mut ata,
439 &mut eigenvalues,
440 &mut eigenvectors,
441 &mut order,
442 );
443 (u, s, vt)
444}
445
446#[cfg(any(test, feature = "cpu-parity"))]
447#[allow(clippy::too_many_arguments)]
448fn truncated_svd_into(
449 matrix: &[f64],
450 m: usize,
451 n: usize,
452 r: usize,
453 u: &mut Vec<f64>,
454 s: &mut Vec<f64>,
455 vt: &mut Vec<f64>,
456 ata: &mut Vec<f64>,
457 eigenvalues: &mut Vec<f64>,
458 eigenvectors: &mut Vec<f64>,
459 order: &mut Vec<usize>,
460) {
461 u.clear();
462 s.clear();
463 vt.clear();
464 let Some(matrix_len) = m.checked_mul(n) else {
465 return;
466 };
467 let Some(u_len) = m.checked_mul(r) else {
468 return;
469 };
470 let Some(vt_len) = r.checked_mul(n) else {
471 return;
472 };
473 if n == 0 || r == 0 || matrix.len() != matrix_len || r > n {
474 u.resize(u_len, 0.0);
475 s.resize(r, 0.0);
476 vt.resize(vt_len, 0.0);
477 return;
478 }
479
480 ata.clear();
481 ata.resize(n * n, 0.0);
482 for row in 0..m {
483 for col_a in 0..n {
484 let a = matrix[row * n + col_a];
485 for col_b in 0..n {
486 ata[col_a * n + col_b] += a * matrix[row * n + col_b];
487 }
488 }
489 }
490
491 symmetric_eigen_jacobi_into(ata, n, eigenvalues, eigenvectors);
492 order.clear();
493 order.extend(0..n);
494 order.sort_by(|&left, &right| {
495 eigenvalues[right]
496 .partial_cmp(&eigenvalues[left])
497 .unwrap_or(std::cmp::Ordering::Equal)
498 });
499
500 u.resize(u_len, 0.0);
501 s.resize(r, 0.0);
502 vt.resize(vt_len, 0.0);
503
504 for rank in 0..r {
505 let eig_index = order[rank];
506 let sigma = eigenvalues[eig_index].max(0.0).sqrt();
507 s[rank] = sigma;
508 for col in 0..n {
509 vt[rank * n + col] = eigenvectors[col * n + eig_index];
510 }
511 if sigma > 1e-12 {
512 for row in 0..m {
513 let mut dot = 0.0;
514 for col in 0..n {
515 dot += matrix[row * n + col] * vt[rank * n + col];
516 }
517 u[row * r + rank] = dot / sigma;
518 }
519 }
520 }
521}
522
523#[cfg(any(test, feature = "cpu-parity"))]
524fn symmetric_eigen_jacobi(mut a: Vec<f64>, n: usize) -> (Vec<f64>, Vec<f64>) {
525 let mut eigenvalues = Vec::new();
526 let mut eigenvectors = Vec::new();
527 symmetric_eigen_jacobi_into(&mut a, n, &mut eigenvalues, &mut eigenvectors);
528 (eigenvalues, eigenvectors)
529}
530
531#[cfg(any(test, feature = "cpu-parity"))]
532fn symmetric_eigen_jacobi_into(
533 a: &mut Vec<f64>,
534 n: usize,
535 eigenvalues: &mut Vec<f64>,
536 eigenvectors: &mut Vec<f64>,
537) {
538 eigenvalues.clear();
539 eigenvectors.clear();
540 let Some(square_len) = n.checked_mul(n) else {
541 return;
542 };
543 if n == 0 {
544 return;
545 }
546 a.resize(square_len, 0.0);
547 eigenvectors.resize(square_len, 0.0);
548 for i in 0..n {
549 eigenvectors[i * n + i] = 1.0;
550 }
551
552 let max_sweeps = (16 * n.max(1) * n.max(1)).max(32);
553 for _ in 0..max_sweeps {
554 let mut p = 0usize;
555 let mut q = 0usize;
556 let mut max_offdiag = 0.0;
557 for i in 0..n {
558 for j in (i + 1)..n {
559 let value = a[i * n + j].abs();
560 if value > max_offdiag {
561 max_offdiag = value;
562 p = i;
563 q = j;
564 }
565 }
566 }
567 if max_offdiag <= 1e-12 {
568 break;
569 }
570
571 let app = a[p * n + p];
572 let aqq = a[q * n + q];
573 let apq = a[p * n + q];
574 let tau = (aqq - app) / (2.0 * apq);
575 let t = tau.signum() / (tau.abs() + (1.0 + tau * tau).sqrt());
576 let c = 1.0 / (1.0 + t * t).sqrt();
577 let s = t * c;
578
579 for k in 0..n {
580 let akp = a[k * n + p];
581 let akq = a[k * n + q];
582 a[k * n + p] = c * akp - s * akq;
583 a[k * n + q] = s * akp + c * akq;
584 }
585 for k in 0..n {
586 let apk = a[p * n + k];
587 let aqk = a[q * n + k];
588 a[p * n + k] = c * apk - s * aqk;
589 a[q * n + k] = s * apk + c * aqk;
590 }
591 a[p * n + q] = 0.0;
592 a[q * n + p] = 0.0;
593
594 for k in 0..n {
595 let vkp = eigenvectors[k * n + p];
596 let vkq = eigenvectors[k * n + q];
597 eigenvectors[k * n + p] = c * vkp - s * vkq;
598 eigenvectors[k * n + q] = s * vkp + c * vkq;
599 }
600 }
601
602 eigenvalues.extend((0..n).map(|i| a[i * n + i]));
603}
604
605#[cfg(feature = "inventory-registry")]
606inventory::submit! {
607 vyre_foundation::operation::OperationRegistration::primitive(
608 OP_ID,
609 || tensor_train_decompose_step("in", "u", "rem", 2, 2, 2, 1),
618 Some(|| {
619 let to_bytes = |vals: &[f32]| crate::wire::pack_f32_slice(vals);
620 vec![vec![
623 to_bytes(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]), to_bytes(&[0.0; 4]), to_bytes(&[0.0; 2]), to_bytes(&[0.0; 4]), to_bytes(&[0.0; 4]), to_bytes(&[0.0; 2]), ]]
630 }),
631 Some(|| {
646 let to_bytes = |vals: &[f32]| crate::wire::pack_f32_slice(vals);
647 vec![vec![
648 to_bytes(&[0.15248324, 0.3499184, 0.5473535, 0.7447886]), to_bytes(&[9.152527, 10.947071]), to_bytes(&[0.39291155, 0.0, 0.0, 203.6071]), to_bytes(&[0.7671874, 0.64142305, -0.64142305, 0.7671874]), to_bytes(&[0.39291155, -1.0e30]), ]]
654 }),
655 )
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn cpu_ref_rank_1_decomposition() {
664 let tensor = vec![1.0; 4];
666 let dims = vec![2, 2];
667 let ranks = vec![1, 1, 1];
668 let cores = cpu_ref(&tensor, &dims, &ranks);
669 assert_eq!(cores.len(), 2);
670 assert_eq!(cores[0].len(), 2); assert_eq!(cores[1].len(), 2); }
673
674 #[test]
675 fn cpu_ref_3mode() {
676 let tensor = vec![1.0; 8];
677 let dims = vec![2, 2, 2];
678 let ranks = vec![1, 1, 1, 1];
679 let cores = cpu_ref(&tensor, &dims, &ranks);
680 assert_eq!(cores.len(), 3);
681 }
682
683 #[test]
684 fn cpu_ref_varying_ranks() {
685 let tensor = vec![0.0; 12]; let dims = vec![2, 3, 2];
687 let ranks = vec![1, 2, 2, 1];
688 let cores = cpu_ref(&tensor, &dims, &ranks);
689 assert_eq!(cores.len(), 3);
690 assert_eq!(cores[0].len(), 4); assert_eq!(cores[1].len(), 12); assert_eq!(cores[2].len(), 4); }
694
695 #[test]
696 fn cpu_ref_into_reuses_core_vectors_and_svd_scratch() {
697 let tensor = vec![1.0; 8];
698 let dims = vec![2, 2, 2];
699 let ranks = vec![1, 1, 1, 1];
700 let mut cores = vec![
701 vec![99.0; 16],
702 vec![88.0; 16],
703 vec![77.0; 16],
704 vec![66.0; 16],
705 ];
706 let core_caps = cores.iter().map(Vec::capacity).collect::<Vec<_>>();
707 let mut scratch = TensorTrainCpuScratch::new();
708 scratch.c.reserve(32);
709 scratch.next_c.reserve(32);
710 scratch.u.reserve(32);
711 scratch.s.reserve(8);
712 scratch.vt.reserve(32);
713 scratch.ata.reserve(32);
714 scratch.eigenvalues.reserve(8);
715 scratch.eigenvectors.reserve(32);
716 scratch.order.reserve(8);
717 let scratch_caps = [
718 scratch.c.capacity(),
719 scratch.next_c.capacity(),
720 scratch.u.capacity(),
721 scratch.s.capacity(),
722 scratch.vt.capacity(),
723 scratch.ata.capacity(),
724 scratch.eigenvalues.capacity(),
725 scratch.eigenvectors.capacity(),
726 scratch.order.capacity(),
727 ];
728
729 cpu_ref_into(&tensor, &dims, &ranks, &mut cores, &mut scratch);
730
731 assert_eq!(cores.len(), 3);
732 assert_eq!(cores[0].len(), 2);
733 assert_eq!(cores[1].len(), 2);
734 assert_eq!(cores[2].len(), 2);
735 assert_eq!(cores[0].capacity(), core_caps[0]);
736 assert_eq!(cores[1].capacity(), core_caps[1]);
737 assert_eq!(cores[2].capacity(), core_caps[2]);
738 assert_eq!(scratch.c.capacity(), scratch_caps[0]);
739 assert_eq!(scratch.next_c.capacity(), scratch_caps[1]);
740 assert_eq!(scratch.u.capacity(), scratch_caps[2]);
741 assert_eq!(scratch.s.capacity(), scratch_caps[3]);
742 assert_eq!(scratch.vt.capacity(), scratch_caps[4]);
743 assert_eq!(scratch.ata.capacity(), scratch_caps[5]);
744 assert_eq!(scratch.eigenvalues.capacity(), scratch_caps[6]);
745 assert_eq!(scratch.eigenvectors.capacity(), scratch_caps[7]);
746 assert_eq!(scratch.order.capacity(), scratch_caps[8]);
747
748 cpu_ref_into(&tensor[..4], &[2, 2], &[1, 1, 1], &mut cores, &mut scratch);
749 assert_eq!(cores.len(), 2);
750 assert_eq!(cores[0].len(), 2);
751 assert_eq!(cores[1].len(), 2);
752 assert_eq!(cores[0].capacity(), core_caps[0]);
753 assert_eq!(cores[1].capacity(), core_caps[1]);
754 }
755
756 #[test]
757 fn truncated_svd_into_reuses_all_supplied_buffers() {
758 let matrix = vec![1.0, 2.0, 3.0, 4.0];
759 let mut u = Vec::with_capacity(8);
760 let mut s = Vec::with_capacity(4);
761 let mut vt = Vec::with_capacity(8);
762 let mut ata = Vec::with_capacity(8);
763 let mut eigenvalues = Vec::with_capacity(4);
764 let mut eigenvectors = Vec::with_capacity(8);
765 let mut order = Vec::with_capacity(4);
766 let caps = [
767 u.capacity(),
768 s.capacity(),
769 vt.capacity(),
770 ata.capacity(),
771 eigenvalues.capacity(),
772 eigenvectors.capacity(),
773 order.capacity(),
774 ];
775
776 truncated_svd_into(
777 &matrix,
778 2,
779 2,
780 2,
781 &mut u,
782 &mut s,
783 &mut vt,
784 &mut ata,
785 &mut eigenvalues,
786 &mut eigenvectors,
787 &mut order,
788 );
789
790 assert_eq!(u.len(), 4);
791 assert_eq!(s.len(), 2);
792 assert_eq!(vt.len(), 4);
793 assert_eq!(u.capacity(), caps[0]);
794 assert_eq!(s.capacity(), caps[1]);
795 assert_eq!(vt.capacity(), caps[2]);
796 assert_eq!(ata.capacity(), caps[3]);
797 assert_eq!(eigenvalues.capacity(), caps[4]);
798 assert_eq!(eigenvectors.capacity(), caps[5]);
799 assert_eq!(order.capacity(), caps[6]);
800 }
801
802 #[test]
803 fn truncated_svd_columns_are_orthonormal() {
804 let matrix = vec![1.0, 2.0, 3.0, 4.0];
805 let (u, _, _) = truncated_svd(&matrix, 2, 2, 2);
806 let dot = u[0] * u[1] + u[2] * u[3];
807 let n0 = u[0] * u[0] + u[2] * u[2];
808 let n1 = u[1] * u[1] + u[3] * u[3];
809 assert!(dot.abs() < 1e-8, "left singular vectors must be orthogonal");
810 assert!((n0 - 1.0).abs() < 1e-8, "first vector must be unit length");
811 assert!((n1 - 1.0).abs() < 1e-8, "second vector must be unit length");
812 }
813
814 #[test]
815 fn truncated_svd_full_rank_reconstructs_matrix() {
816 let matrix = vec![1.0, 2.0, 3.0, 4.0];
817 let (u, s, vt) = truncated_svd(&matrix, 2, 2, 2);
818 let mut reconstructed = [0.0_f64; 4];
819 for row in 0..2 {
820 for col in 0..2 {
821 for rank in 0..2 {
822 reconstructed[row * 2 + col] +=
823 u[row * 2 + rank] * s[rank] * vt[rank * 2 + col];
824 }
825 }
826 }
827 for (actual, expected) in reconstructed.iter().zip(matrix.iter()) {
828 assert!(
829 (actual - expected).abs() < 1e-8,
830 "full-rank SVD reconstruction drifted: actual={actual}, expected={expected}"
831 );
832 }
833 }
834
835 #[test]
836 fn program_buffer_layout() {
837 use vyre_foundation::ir::{BufferAccess, DataType};
838 let p = tensor_train_decompose_step("in", "u", "rem", 1, 2, 4, 1);
839 assert_eq!(p.buffers.len(), 6);
842 assert!(p.buffers.iter().all(|b| b.element() == DataType::F32));
843 assert_eq!(p.buffers[0].access(), BufferAccess::ReadOnly);
844 assert!(p.buffers[1..]
845 .iter()
846 .all(|b| b.access() == BufferAccess::ReadWrite));
847 }
848}