1use crate::autograd::{BackwardOp, Tensor};
8use crate::trace::{TraceStep, TRACER};
9use ndarray::Array1;
10use std::cell::RefCell;
11use std::rc::Rc;
12
13#[cfg(all(feature = "realizar", feature = "cuda"))]
14use std::sync::atomic::{AtomicBool, Ordering};
15#[cfg(all(feature = "realizar", feature = "cuda"))]
16use std::sync::{Mutex, OnceLock};
17
18#[cfg(all(feature = "realizar", feature = "cuda"))]
19use realizar::cuda::CudaExecutor;
20
21#[cfg(all(feature = "realizar", feature = "cuda"))]
26static CUDA_MATMUL_DISABLED: AtomicBool = AtomicBool::new(false);
27
28#[cfg(all(feature = "realizar", feature = "cuda"))]
30static CUDA_EXECUTOR: OnceLock<Option<Mutex<CudaExecutor>>> = OnceLock::new();
31
32#[cfg(all(feature = "realizar", feature = "cuda"))]
34fn get_cuda_executor() -> Option<&'static Mutex<CudaExecutor>> {
35 CUDA_EXECUTOR
36 .get_or_init(|| match CudaExecutor::new(0) {
37 Ok(executor) => {
38 TRACER.end(TraceStep::Transfer, "realizar CUDA executor initialized on GPU 0");
39 Some(Mutex::new(executor))
40 }
41 Err(_e) => {
42 CUDA_MATMUL_DISABLED.store(true, Ordering::Relaxed);
43 None
44 }
45 })
46 .as_ref()
47}
48
49#[inline]
52pub fn transpose(data: &[f32], rows: usize, cols: usize) -> Vec<f32> {
53 contract_pre_transpose!(data);
54 TRACER.start(TraceStep::Transpose);
55 let mut transposed = vec![0.0f32; rows * cols];
56
57 const BLOCK_SIZE: usize = 32;
58 if rows >= BLOCK_SIZE && cols >= BLOCK_SIZE {
59 transpose_blocked(data, &mut transposed, rows, cols, BLOCK_SIZE);
60 } else {
61 transpose_simple(data, &mut transposed, rows, cols);
62 }
63
64 TRACER.end(TraceStep::Transpose, format!("{rows}x{cols}"));
65 transposed
66}
67
68pub fn transpose_tracked(tensor: &Tensor, rows: usize, cols: usize) -> Tensor {
80 contract_pre_transpose_tracked!();
81 let data = tensor.data();
82 let slice = data.as_slice().expect("transpose_tracked: tensor must be contiguous");
83 let transposed_data = transpose(slice, rows, cols);
84 let mut result = Tensor::from_vec(transposed_data, tensor.requires_grad());
85
86 if tensor.requires_grad() {
87 let backward_op = Rc::new(TransposeBackward {
88 original: tensor.clone(),
89 rows,
90 cols,
91 result_grad: result.grad_cell(),
92 });
93 result.set_backward_op(backward_op);
94 }
95
96 result
97}
98
99struct TransposeBackward {
105 original: Tensor,
106 rows: usize,
107 cols: usize,
108 result_grad: Rc<RefCell<Option<Array1<f32>>>>,
109}
110
111impl BackwardOp for TransposeBackward {
112 fn backward(&self) {
113 if let Some(grad) = self.result_grad.borrow().as_ref() {
114 let grad_slice = grad.as_slice().expect("gradient must be contiguous");
115 let grad_original = transpose(grad_slice, self.cols, self.rows);
117 self.original.accumulate_grad(Array1::from(grad_original));
118 if let Some(op) = self.original.backward_op() {
119 op.backward();
120 }
121 }
122 }
123}
124
125#[inline]
127fn transpose_blocked(src: &[f32], dst: &mut [f32], rows: usize, cols: usize, block: usize) {
128 for r_block in (0..rows).step_by(block) {
129 for c_block in (0..cols).step_by(block) {
130 let r_end = (r_block + block).min(rows);
131 let c_end = (c_block + block).min(cols);
132 for r in r_block..r_end {
133 for c in c_block..c_end {
134 dst[c * rows + r] = src[r * cols + c];
135 }
136 }
137 }
138 }
139}
140
141#[inline]
143fn transpose_simple(src: &[f32], dst: &mut [f32], rows: usize, cols: usize) {
144 for r in 0..rows {
145 for c in 0..cols {
146 dst[c * rows + r] = src[r * cols + c];
147 }
148 }
149}
150
151#[cfg(all(feature = "realizar", feature = "cuda"))]
156pub fn matmul_compute(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
157 contract_pre_matmul!(a);
158 if !CUDA_MATMUL_DISABLED.load(Ordering::Relaxed) {
161 if let Some(executor_mutex) = get_cuda_executor() {
162 if let Ok(mut executor) = executor_mutex.lock() {
163 match cuda_matmul(&mut executor, a, b, m, k, n) {
164 Ok(result) => return result,
165 Err(_e) => {
166 CUDA_MATMUL_DISABLED.store(true, Ordering::Relaxed);
168 TRACER.end(
169 TraceStep::Matmul,
170 "realizar CUDA matmul disabled (JIT failure), using trueno SIMD",
171 );
172 }
173 }
174 }
175 }
176 }
177
178 #[cfg(feature = "gpu")]
181 if !WGPU_BATCH_MODE.load(std::sync::atomic::Ordering::Relaxed) && m * k * n > 32_768 {
182 if let Some(result) = wgpu_matmul(a, b, m, k, n) {
183 return result;
184 }
185 }
186
187 cpu_matmul(a, b, m, k, n)
189}
190
191#[cfg(all(feature = "realizar", feature = "cuda"))]
206pub fn pre_warm_realizador_gemm(
207 seq_len: usize,
208 hidden_size: usize,
209 kv_hidden_size: usize,
210 intermediate_size: usize,
211 lora_rank: usize,
212 num_classes: usize,
213) -> usize {
214 let executor_mutex = match get_cuda_executor() {
215 Some(e) => e,
216 None => return 0,
217 };
218 let mut executor = match executor_mutex.lock() {
219 Ok(e) => e,
220 Err(_) => return 0,
221 };
222
223 let s = seq_len;
225 let h = hidden_size;
226 let kv = kv_hidden_size;
227 let i = intermediate_size;
228 let r = lora_rank;
229
230 let mut shapes: Vec<(usize, usize, usize)> = vec![
231 (s, h, h), (s, h, kv), (s, h, i), (s, i, h), (s, h, r), (s, r, h), (s, kv, r), (s, r, kv), (s, kv, h), (s, i, h), (s, h, i), (h, s, h), (h, s, kv), (h, s, i), (i, s, h), (s, r, h), (h, s, r), (s, h, r), (r, s, h), (r, s, kv), (1, h, num_classes),
259 ];
260
261 shapes.sort_unstable();
263 shapes.dedup();
264 shapes.retain(|&(m, k, n)| m > 0 && k > 0 && n > 0);
266
267 let mut warmed = 0usize;
268 for &(m, k, n) in &shapes {
269 let a = vec![0.0f32; m * k];
270 let b = vec![0.0f32; k * n];
271 match cuda_matmul(&mut executor, &a, &b, m, k, n) {
272 Ok(_) => warmed += 1,
273 Err(e) => {
274 eprintln!("[CUDA] realizador GEMM pre-warm failed for ({m},{k},{n}): {e}");
275 }
276 }
277 }
278
279 if warmed == 0 {
280 CUDA_MATMUL_DISABLED.store(true, Ordering::Relaxed);
281 }
282
283 warmed
284}
285
286#[cfg(all(feature = "realizar", feature = "cuda"))]
288fn cuda_matmul(
289 executor: &mut CudaExecutor,
290 a: &[f32],
291 b: &[f32],
292 m: usize,
293 k: usize,
294 n: usize,
295) -> Result<Vec<f32>, String> {
296 TRACER.start(TraceStep::Alloc);
297 let mut c = vec![0.0f32; m * n];
298 TRACER.end(TraceStep::Alloc, format!("{m}x{n}"));
299
300 TRACER.start(TraceStep::Matmul);
301 executor.gemm(a, b, &mut c, m as u32, n as u32, k as u32).map_err(|e| format!("{e:?}"))?;
302 TRACER.end(TraceStep::Matmul, format!("{m}x{k}x{n}"));
303 Ok(c)
304}
305
306fn cpu_matmul(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
308 let mut c = vec![0.0f32; m * n];
309
310 if let Err(_e) = trueno::blis::gemm(m, n, k, a, b, &mut c) {
311 for i in 0..m {
313 for j in 0..n {
314 let mut sum = 0.0;
315 for p in 0..k {
316 sum += a[i * k + p] * b[p * n + j];
317 }
318 c[i * n + j] = sum;
319 }
320 }
321 }
322
323 c
324}
325
326#[cfg(feature = "gpu")]
330static WGPU_BATCH_MODE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
331
332#[cfg(feature = "gpu")]
339pub fn suppress_per_op_wgpu() {
340 WGPU_BATCH_MODE.store(true, std::sync::atomic::Ordering::Relaxed);
341}
342
343#[cfg(feature = "gpu")]
345pub fn unsuppress_per_op_wgpu() {
346 WGPU_BATCH_MODE.store(false, std::sync::atomic::Ordering::Relaxed);
347}
348
349#[cfg(not(all(feature = "realizar", feature = "cuda")))]
355pub fn matmul_compute(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
356 #[cfg(feature = "gpu")]
357 {
358 if !WGPU_BATCH_MODE.load(std::sync::atomic::Ordering::Relaxed)
363 && m * k * n > 32_768
364 && m > 1
365 && n > 1
366 {
367 if let Some(result) = wgpu_matmul(a, b, m, k, n) {
368 return result;
369 }
370 }
371 }
372 cpu_matmul(a, b, m, k, n)
373}
374
375#[cfg(feature = "gpu")]
380fn wgpu_matmul(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Option<Vec<f32>> {
381 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
382 use std::sync::OnceLock;
383 static WGPU_DISABLED: AtomicBool = AtomicBool::new(false);
384 static WGPU_LOGGED: AtomicBool = AtomicBool::new(false);
385 static WGPU_CALLS: AtomicU64 = AtomicU64::new(0);
386 static WGPU_DEVICE: OnceLock<Option<trueno::backends::gpu::GpuDevice>> = OnceLock::new();
387
388 if WGPU_DISABLED.load(Ordering::Relaxed) {
389 return None;
390 }
391
392 let device_opt = WGPU_DEVICE.get_or_init(|| {
393 if !trueno::backends::gpu::GpuBackend::is_available() {
394 eprintln!("[wgpu] No GPU available, using CPU");
395 return None;
396 }
397 match trueno::backends::gpu::GpuDevice::new() {
398 Ok(d) => {
399 eprintln!("[wgpu] GPU device initialized for matmul");
400 Some(d)
401 }
402 Err(e) => {
403 eprintln!("[wgpu] GPU init failed: {e}, using CPU");
404 None
405 }
406 }
407 });
408
409 let device = match device_opt.as_ref() {
410 Some(d) => d,
411 None => {
412 WGPU_DISABLED.store(true, Ordering::Relaxed);
413 return None;
414 }
415 };
416
417 let mut result = vec![0.0f32; m * n];
418 match device.matmul(a, b, &mut result, m, k, n) {
419 Ok(()) => {
420 let calls = WGPU_CALLS.fetch_add(1, Ordering::Relaxed);
421 if !WGPU_LOGGED.swap(true, Ordering::Relaxed) {
422 eprintln!("[wgpu] GPU matmul active ({m}x{k}x{n})");
423 }
424 if calls > 0 && calls.is_multiple_of(10_000) {
426 eprintln!("[wgpu] {calls} GPU matmuls completed");
427 }
428 Some(result)
429 }
430 Err(_e) => {
431 WGPU_DISABLED.store(true, Ordering::Relaxed);
432 None
433 }
434 }
435}
436
437#[provable_contracts_macros::contract("matmul-v1", equation = "matmul")]
453pub fn matmul(a: &Tensor, b: &Tensor, m: usize, k: usize, n: usize) -> Tensor {
454 assert_eq!(a.len(), m * k, "Matrix A size mismatch");
455 assert_eq!(b.len(), k * n, "Matrix B size mismatch");
456
457 let result_data = matmul_compute(
459 a.data().as_slice().expect("matrix A must be contiguous"),
460 b.data().as_slice().expect("matrix B must be contiguous"),
461 m,
462 k,
463 n,
464 );
465
466 let requires_grad = a.requires_grad() || b.requires_grad();
467 let mut result = Tensor::new(Array1::from(result_data), requires_grad);
468
469 if requires_grad {
470 let a_clone = a.clone();
471 let b_clone = b.clone();
472 let backward_op = Rc::new(MatmulBackward {
473 a: a_clone,
474 b: b_clone,
475 m,
476 k,
477 n,
478 result_grad: result.grad_cell(),
479 });
480 result.set_backward_op(backward_op);
481 }
482
483 contract_post_matmul!(result.data().as_slice().unwrap_or(&[]));
484 result
485}
486
487struct MatmulBackward {
488 a: Tensor,
489 b: Tensor,
490 m: usize,
491 k: usize,
492 n: usize,
493 result_grad: Rc<RefCell<Option<Array1<f32>>>>,
494}
495
496impl BackwardOp for MatmulBackward {
497 fn backward(&self) {
498 if let Some(grad_output) = self.result_grad.borrow().as_ref() {
499 let grad_c = grad_output.as_slice().expect("gradient output must be contiguous");
503 let a_data = self.a.data();
504 let b_data = self.b.data();
505 let a_slice = a_data.as_slice().expect("matrix A must be contiguous");
506 let b_slice = b_data.as_slice().expect("matrix B must be contiguous");
507
508 if self.a.requires_grad() {
509 let b_t = transpose(b_slice, self.k, self.n);
513 let grad_a = matmul_compute(grad_c, &b_t, self.m, self.n, self.k);
514 self.a.accumulate_grad(Array1::from(grad_a));
515 }
516
517 if self.b.requires_grad() {
518 let a_t = transpose(a_slice, self.m, self.k);
522 let grad_b = matmul_compute(&a_t, grad_c, self.k, self.m, self.n);
523 self.b.accumulate_grad(Array1::from(grad_b));
524 }
525
526 if let Some(op) = self.a.backward_op() {
528 op.backward();
529 }
530 if let Some(op) = self.b.backward_op() {
531 op.backward();
532 }
533 }
534 }
535}
536
537#[provable_contracts_macros::contract("matmul-v1", equation = "matmul_nt")]
553pub fn matmul_nt(a: &Tensor, b: &Tensor, m: usize, k: usize, n: usize) -> Tensor {
554 assert_eq!(
555 a.len(),
556 m * k,
557 "Matrix A size mismatch: expected {}×{} = {}, got {}",
558 m,
559 k,
560 m * k,
561 a.len()
562 );
563 assert_eq!(
564 b.len(),
565 n * k,
566 "Matrix B size mismatch: expected {}×{} = {}, got {}",
567 n,
568 k,
569 n * k,
570 b.len()
571 );
572
573 let a_slice = a.data();
574 let b_slice = b.data();
575 let a_data = a_slice.as_slice().expect("matrix A must be contiguous");
576 let b_data = b_slice.as_slice().expect("matrix B must be contiguous");
577
578 let result_data = matmul_nt_compute(a_data, b_data, m, k, n);
580
581 let requires_grad = a.requires_grad() || b.requires_grad();
582 let mut result = Tensor::new(Array1::from(result_data), requires_grad);
583
584 if requires_grad {
585 let a_clone = a.clone();
586 let b_clone = b.clone();
587 let backward_op = Rc::new(MatmulNtBackward {
588 a: a_clone,
589 b: b_clone,
590 m,
591 k,
592 n,
593 result_grad: result.grad_cell(),
594 });
595 result.set_backward_op(backward_op);
596 }
597
598 contract_post_matmul!(result.data().as_slice().unwrap_or(&[]));
599 result
600}
601
602pub fn matmul_nt_compute(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
606 let b_t = transpose(b, n, k); cpu_matmul(a, &b_t, m, k, n)
609}
610
611struct MatmulNtBackward {
612 a: Tensor,
613 b: Tensor,
614 m: usize,
615 k: usize,
616 n: usize,
617 result_grad: Rc<RefCell<Option<Array1<f32>>>>,
618}
619
620impl BackwardOp for MatmulNtBackward {
621 fn backward(&self) {
622 if let Some(grad_output) = self.result_grad.borrow().as_ref() {
623 let grad_c = grad_output.as_slice().expect("gradient output must be contiguous");
629
630 if self.a.requires_grad() {
631 let b_data = self.b.data();
633 let b_slice = b_data.as_slice().expect("matrix B must be contiguous");
634 let grad_a = matmul_compute(grad_c, b_slice, self.m, self.n, self.k);
635 self.a.accumulate_grad(Array1::from(grad_a));
636 }
637
638 if self.b.requires_grad() {
639 let a_data = self.a.data();
641 let a_slice = a_data.as_slice().expect("matrix A must be contiguous");
642 let grad_c_t = transpose(grad_c, self.m, self.n);
643 let grad_b = matmul_compute(&grad_c_t, a_slice, self.n, self.m, self.k);
644 self.b.accumulate_grad(Array1::from(grad_b));
645 }
646
647 if let Some(op) = self.a.backward_op() {
649 op.backward();
650 }
651 if let Some(op) = self.b.backward_op() {
652 op.backward();
653 }
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_transpose_identity() {
664 let data = vec![5.0];
666 let result = transpose(&data, 1, 1);
667 assert_eq!(result, vec![5.0]);
668 }
669
670 #[test]
671 fn test_transpose_2x3() {
672 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
676 let result = transpose(&data, 2, 3);
677 assert_eq!(result, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
682 }
683
684 #[test]
685 fn test_transpose_3x2() {
686 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
688 let result = transpose(&data, 3, 2);
689 assert_eq!(result, vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0]);
691 }
692
693 #[test]
694 fn test_matmul_compute_2x2() {
695 let a = vec![1.0, 2.0, 3.0, 4.0];
699 let b = vec![5.0, 6.0, 7.0, 8.0];
700 let c = matmul_compute(&a, &b, 2, 2, 2);
701 assert_eq!(c, vec![19.0, 22.0, 43.0, 50.0]);
702 }
703
704 #[test]
705 fn test_matmul_compute_2x3_3x2() {
706 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
710 let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
711 let c = matmul_compute(&a, &b, 2, 3, 2);
712 assert_eq!(c, vec![58.0, 64.0, 139.0, 154.0]);
715 }
716
717 #[test]
718 fn test_matmul_no_grad() {
719 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), false);
720 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), false);
721 let c = matmul(&a, &b, 2, 2, 2);
722 assert!(!c.requires_grad());
723 assert_eq!(
724 c.data().as_slice().expect("operation should succeed"),
725 &[19.0, 22.0, 43.0, 50.0]
726 );
727 }
728
729 #[test]
730 fn test_matmul_with_grad() {
731 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), true);
732 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), true);
733 let c = matmul(&a, &b, 2, 2, 2);
734 assert!(c.requires_grad());
735 assert!(c.backward_op().is_some());
736 }
737
738 #[test]
739 fn test_matmul_backward() {
740 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), true);
741 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), true);
742 let c = matmul(&a, &b, 2, 2, 2);
743
744 c.set_grad(Array1::from(vec![1.0, 1.0, 1.0, 1.0]));
746
747 if let Some(op) = c.backward_op() {
749 op.backward();
750 }
751
752 assert!(a.grad().is_some());
754 assert!(b.grad().is_some());
755 }
756
757 #[test]
758 fn test_matmul_a_requires_grad_only() {
759 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), true);
760 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), false);
761 let c = matmul(&a, &b, 2, 2, 2);
762 assert!(c.requires_grad());
763
764 c.set_grad(Array1::from(vec![1.0, 1.0, 1.0, 1.0]));
765 if let Some(op) = c.backward_op() {
766 op.backward();
767 }
768
769 assert!(a.grad().is_some());
770 assert!(b.grad().is_none());
771 }
772
773 #[test]
774 fn test_matmul_b_requires_grad_only() {
775 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), false);
776 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), true);
777 let c = matmul(&a, &b, 2, 2, 2);
778 assert!(c.requires_grad());
779
780 c.set_grad(Array1::from(vec![1.0, 1.0, 1.0, 1.0]));
781 if let Some(op) = c.backward_op() {
782 op.backward();
783 }
784
785 assert!(a.grad().is_none());
786 assert!(b.grad().is_some());
787 }
788
789 #[test]
790 #[should_panic(expected = "Pre-condition violated")]
791 fn test_matmul_size_mismatch_a() {
792 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0]), false);
793 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), false);
794 let _ = matmul(&a, &b, 2, 2, 2);
795 }
796
797 #[test]
798 #[should_panic(expected = "Pre-condition violated")]
799 fn test_matmul_size_mismatch_b() {
800 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), false);
801 let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0]), false);
802 let _ = matmul(&a, &b, 2, 2, 2);
803 }
804
805 #[test]
806 fn test_transpose_double_transpose() {
807 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
809 let t1 = transpose(&data, 2, 3);
810 let t2 = transpose(&t1, 3, 2);
811 assert_eq!(data, t2);
812 }
813
814 #[test]
830 fn falsify_mm_001e_shape_correctness() {
831 for (m, k, n) in [(2, 3, 4), (1, 5, 1), (4, 4, 4), (3, 1, 2)] {
832 let result = matmul_compute(&vec![1.0; m * k], &vec![1.0; k * n], m, k, n);
833 assert_eq!(
834 result.len(),
835 m * n,
836 "FALSIFIED MM-001e: output len = {}, expected {} for ({m}x{k}) @ ({k}x{n})",
837 result.len(),
838 m * n
839 );
840 }
841 }
842
843 #[test]
845 fn falsify_mm_005e_identity_matrix() {
846 let m = 3;
847 let k = 4;
848 let a: Vec<f32> = (0..m * k).map(|i| (i as f32 + 1.0) * 0.5).collect();
849 let mut identity = vec![0.0; k * k];
850 for i in 0..k {
851 identity[i * k + i] = 1.0;
852 }
853 let result = matmul_compute(&a, &identity, m, k, k);
854 for (i, (&got, &exp)) in result.iter().zip(a.iter()).enumerate() {
855 assert!(
856 (got - exp).abs() < 1e-5,
857 "FALSIFIED MM-005e: (A@I)[{i}] = {got}, expected {exp}"
858 );
859 }
860 }
861
862 #[test]
864 fn falsify_mm_002e_numerical_accuracy() {
865 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
867 let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
868 let result = matmul_compute(&a, &b, 2, 3, 2);
869 let expected = [58.0, 64.0, 139.0, 154.0];
870 for (i, (&got, &exp)) in result.iter().zip(expected.iter()).enumerate() {
871 assert!(
872 (got - exp).abs() < 1e-4,
873 "FALSIFIED MM-002e: result[{i}] = {got}, expected {exp}"
874 );
875 }
876 }
877
878 #[test]
883 fn test_matmul_nt_compute_2x2() {
884 let a = vec![1.0, 2.0, 3.0, 4.0];
891 let b = vec![5.0, 6.0, 7.0, 8.0];
892 let c = matmul_nt_compute(&a, &b, 2, 2, 2);
893 assert_eq!(c, vec![17.0, 23.0, 39.0, 53.0]);
894 }
895
896 #[test]
897 fn test_matmul_nt_compute_2x3_4x3() {
898 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
910 let b = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
911 let c = matmul_nt_compute(&a, &b, 2, 3, 4);
912 assert_eq!(c, vec![1.0, 2.0, 3.0, 6.0, 4.0, 5.0, 6.0, 15.0]);
913 }
914
915 #[test]
916 fn test_matmul_nt_equivalence_to_transpose_matmul() {
917 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0]; let b_t = transpose(&b, 2, 3); let c_nt = matmul_nt_compute(&a, &b, 2, 3, 2);
923 let c_ref = matmul_compute(&a, &b_t, 2, 3, 2);
924
925 for (i, (&got, &exp)) in c_nt.iter().zip(c_ref.iter()).enumerate() {
926 assert!(
927 (got - exp).abs() < 1e-5,
928 "matmul_nt[{i}] = {got}, matmul(A, B^T)[{i}] = {exp}"
929 );
930 }
931 }
932
933 #[test]
934 fn test_matmul_nt_backward_grad_flows_to_b() {
935 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), false); let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), true); let c = matmul_nt(&a, &b, 2, 2, 2);
940 assert!(c.requires_grad());
941
942 c.set_grad(Array1::from(vec![1.0, 1.0, 1.0, 1.0]));
943 if let Some(op) = c.backward_op() {
944 op.backward();
945 }
946
947 let b_grad = b.grad().expect("KAIZEN-011: B must receive gradient from matmul_nt");
949
950 let expected_grad_b = vec![4.0, 6.0, 4.0, 6.0];
953 for (i, (&got, &exp)) in b_grad.iter().zip(expected_grad_b.iter()).enumerate() {
954 assert!((got - exp).abs() < 1e-4, "KAIZEN-011: grad_B[{i}] = {got}, expected {exp}");
955 }
956 }
957
958 #[test]
959 fn test_matmul_nt_backward_grad_flows_to_a() {
960 let a = Tensor::new(Array1::from(vec![1.0, 2.0, 3.0, 4.0]), true); let b = Tensor::new(Array1::from(vec![5.0, 6.0, 7.0, 8.0]), false); let c = matmul_nt(&a, &b, 2, 2, 2);
964 c.set_grad(Array1::from(vec![1.0, 1.0, 1.0, 1.0]));
965 if let Some(op) = c.backward_op() {
966 op.backward();
967 }
968
969 let a_grad = a.grad().expect("A must receive gradient");
970
971 let expected_grad_a = vec![12.0, 14.0, 12.0, 14.0];
973 for (i, (&got, &exp)) in a_grad.iter().zip(expected_grad_a.iter()).enumerate() {
974 assert!((got - exp).abs() < 1e-4, "grad_A[{i}] = {got}, expected {exp}");
975 }
976 }
977
978 mod mm_proptest_falsify {
979 use super::*;
980 use proptest::prelude::*;
981
982 proptest! {
984 #![proptest_config(ProptestConfig::with_cases(100))]
985
986 #[test]
987 fn falsify_mm_001e_prop_shape(
988 m in 1..=8usize,
989 k in 1..=8usize,
990 n in 1..=8usize,
991 ) {
992 let result = matmul_compute(&vec![1.0; m * k], &vec![1.0; k * n], m, k, n);
993 prop_assert_eq!(result.len(), m * n);
994 }
995 }
996
997 proptest! {
999 #![proptest_config(ProptestConfig::with_cases(50))]
1000
1001 #[test]
1002 fn falsify_mm_005e_prop_identity(
1003 m in 1..=6usize,
1004 k in 1..=6usize,
1005 seed in 0..500u32,
1006 ) {
1007 let a: Vec<f32> = (0..m * k)
1008 .map(|i| ((i as f32 + seed as f32) * 0.37).sin())
1009 .collect();
1010 let mut identity = vec![0.0; k * k];
1011 for i in 0..k {
1012 identity[i * k + i] = 1.0;
1013 }
1014 let result = matmul_compute(&a, &identity, m, k, k);
1015 for (i, (&got, &exp)) in result.iter().zip(a.iter()).enumerate() {
1016 prop_assert!(
1017 (got - exp).abs() < 1e-4,
1018 "FALSIFIED MM-005e-prop: (A@I)[{}] = {}, expected {}",
1019 i, got, exp
1020 );
1021 }
1022 }
1023 }
1024
1025 proptest! {
1027 #![proptest_config(ProptestConfig::with_cases(50))]
1028
1029 #[test]
1030 fn falsify_mm_nt_equivalence(
1031 m in 1..=6usize,
1032 k in 1..=6usize,
1033 n in 1..=6usize,
1034 seed in 0..500u32,
1035 ) {
1036 let a: Vec<f32> = (0..m * k)
1037 .map(|i| ((i as f32 + seed as f32) * 0.31).sin())
1038 .collect();
1039 let b: Vec<f32> = (0..n * k)
1040 .map(|i| ((i as f32 + seed as f32 + 100.0) * 0.47).cos())
1041 .collect();
1042
1043 let c_nt = matmul_nt_compute(&a, &b, m, k, n);
1044 let b_t = transpose(&b, n, k);
1045 let c_ref = matmul_compute(&a, &b_t, m, k, n);
1046
1047 for (i, (&got, &exp)) in c_nt.iter().zip(c_ref.iter()).enumerate() {
1048 prop_assert!(
1049 (got - exp).abs() < 1e-3,
1050 "matmul_nt[{}] = {}, expected {}",
1051 i, got, exp
1052 );
1053 }
1054 }
1055 }
1056 }
1057
1058 #[test]
1061 fn test_transpose_tracked_backward_gradient_flow() {
1062 let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], true);
1064
1065 let a_t = transpose_tracked(&a, 2, 3);
1067 assert_eq!(a_t.len(), 6);
1068
1069 let at_data = a_t.data();
1071 let at_slice = at_data.as_slice().expect("contiguous");
1072 assert_eq!(at_slice, &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
1073
1074 a_t.set_grad(Array1::from(vec![10.0, 40.0, 20.0, 50.0, 30.0, 60.0]));
1077
1078 if let Some(op) = a_t.backward_op() {
1080 op.backward();
1081 }
1082
1083 let grad = a.grad().expect("original tensor should have gradient");
1085 let grad_slice = grad.as_slice().expect("contiguous");
1086 assert_eq!(grad_slice, &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
1088 }
1089
1090 #[test]
1093 fn test_transpose_tracked_lora_gradient_chain() {
1094 let lora_a = Tensor::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], true);
1096 let x = Tensor::from_vec(vec![1.0, 2.0, 3.0], true); let lora_a_t = transpose_tracked(&lora_a, 2, 3);
1100
1101 let result = matmul(&x, &lora_a_t, 1, 3, 2);
1103 assert_eq!(result.len(), 2);
1104
1105 result.set_grad(Array1::from(vec![1.0, 1.0]));
1107
1108 if let Some(op) = result.backward_op() {
1110 op.backward();
1111 }
1112
1113 let grad = lora_a.grad().expect("LoRA A should receive gradient via transpose_tracked");
1115 assert_eq!(grad.len(), 6);
1116
1117 for (i, &val) in grad.as_slice().expect("contiguous").iter().enumerate() {
1119 assert!(val.is_finite(), "Gradient element {i} is not finite: {val}");
1120 }
1121 let grad_sum: f32 = grad.iter().sum();
1122 assert!(grad_sum.abs() > 1e-6, "Gradient should be non-zero, got sum={grad_sum}");
1123 }
1124}