Skip to main content

trueno/matrix/ops/
arithmetic.rs

1//! Matrix arithmetic operations
2//!
3//! This module provides matrix multiplication and related operations:
4//! - `matmul()` - Standard matrix multiplication with SIMD optimization
5//! - `batched_matmul()` - Batched 3D tensor multiplication
6//! - `batched_matmul_4d()` - 4D tensor multiplication for attention
7//!
8//! ## Domain Separation (PMAT-018)
9//!
10//! Arithmetic operations (multiplication, addition) are separate from storage
11//! operations (allocation, indexing). This allows optimizing compute kernels
12//! independently of memory layout decisions.
13//!
14//! ## Performance Hierarchy
15//!
16//! 1. GPU for large matrices (≥500×500) - 2-10x speedup
17//! 2. BLIS/SIMD for medium-large matrices (>64×64) - 2-8x speedup
18//! 3. Naive for small matrices - lowest overhead
19
20use crate::TruenoError;
21
22#[cfg(feature = "tracing")]
23use tracing::instrument;
24
25use super::super::Matrix;
26
27impl Matrix<f32> {
28    /// Matrix multiplication (matmul)
29    ///
30    /// Computes `C = A × B` where A is `m×n`, B is `n×p`, and C is `m×p`.
31    ///
32    /// # Arguments
33    ///
34    /// * `other` - The matrix to multiply with (right operand)
35    ///
36    /// # Returns
37    ///
38    /// A new matrix containing the result of matrix multiplication
39    ///
40    /// # Errors
41    ///
42    /// Returns `InvalidInput` if matrix dimensions are incompatible
43    /// (i.e., `self.cols != other.rows`)
44    ///
45    /// # Example
46    ///
47    /// ```
48    /// use trueno::Matrix;
49    ///
50    /// let a = Matrix::from_vec(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
51    /// let b = Matrix::from_vec(2, 2, vec![5.0, 6.0, 7.0, 8.0]).unwrap();
52    /// let c = a.matmul(&b).unwrap();
53    ///
54    /// // [[1, 2],   [[5, 6],   [[19, 22],
55    /// //  [3, 4]] ×  [7, 8]] =  [43, 50]]
56    /// assert_eq!(c.get(0, 0), Some(&19.0));
57    /// assert_eq!(c.get(0, 1), Some(&22.0));
58    /// assert_eq!(c.get(1, 0), Some(&43.0));
59    /// assert_eq!(c.get(1, 1), Some(&50.0));
60    /// ```
61    // =========================================================================
62    // HOT PATH - PERFORMANCE CRITICAL
63    // =========================================================================
64    // Core matrix operation used in neural network forward passes.
65    // Changes to inner loops REQUIRE benchmark verification: make bench-check
66    // =========================================================================
67    #[cfg_attr(feature = "tracing", instrument(skip(self, other), fields(dims = %format!("{}x{} @ {}x{}", self.rows, self.cols, other.rows, other.cols))))]
68    pub fn matmul(&self, other: &Matrix<f32>) -> Result<Matrix<f32>, TruenoError> {
69        if self.cols != other.rows {
70            return Err(TruenoError::InvalidInput(format!(
71                "Matrix dimension mismatch for multiplication: {}×{} × {}×{} (inner dimensions {} and {} must match)",
72                self.rows, self.cols, other.rows, other.cols, self.cols, other.rows
73            )));
74        }
75
76        // Fast path for vector-matrix multiply (rows=1)
77        if self.rows == 1 {
78            return self.matmul_vector_matrix(other);
79        }
80
81        // NOTE: zeros required — BLIS GEMM accumulates (c += A*B) via load_c_tile.
82        let mut result = Matrix::zeros_with_backend(self.rows, other.cols, self.backend);
83
84        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
85        const GPU_THRESHOLD: usize = 500;
86        const SIMD_THRESHOLD: usize = 64;
87
88        // Try GPU first for very large matrices
89        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
90        {
91            if self.rows >= GPU_THRESHOLD
92                && self.cols >= GPU_THRESHOLD
93                && other.cols >= GPU_THRESHOLD
94            {
95                if let Ok(gpu_result) = self.matmul_gpu(other) {
96                    return Ok(gpu_result);
97                }
98            }
99        }
100
101        // Use SIMD for medium-large matrices
102        if self.rows >= SIMD_THRESHOLD
103            || self.cols >= SIMD_THRESHOLD
104            || other.cols >= SIMD_THRESHOLD
105        {
106            #[cfg(target_arch = "wasm32")]
107            {
108                self.matmul_wasm_tiled(other, &mut result)?;
109            }
110            #[cfg(not(target_arch = "wasm32"))]
111            {
112                crate::blis::parallel::gemm_blis_parallel(
113                    self.rows,
114                    other.cols,
115                    self.cols,
116                    &self.data,
117                    &other.data,
118                    &mut result.data,
119                )?;
120            }
121        } else {
122            self.matmul_naive(other, &mut result)?;
123        }
124
125        Ok(result)
126    }
127
128    /// Batched matrix multiplication for 3D tensors.
129    ///
130    /// Computes `[batch, m, k] @ [batch, k, n] -> [batch, m, n]` using SIMD for each batch.
131    #[cfg_attr(feature = "tracing", instrument(skip(a_data, b_data), fields(batch, m, k, n)))]
132    pub fn batched_matmul(
133        a_data: &[f32],
134        b_data: &[f32],
135        batch: usize,
136        m: usize,
137        k: usize,
138        n: usize,
139    ) -> Result<Vec<f32>, TruenoError> {
140        let a_stride = m * k;
141        let b_stride = k * n;
142        let out_stride = m * n;
143
144        if a_data.len() != batch * a_stride {
145            return Err(TruenoError::InvalidInput(format!(
146                "A data size mismatch: expected {} ({}×{}×{}), got {}",
147                batch * a_stride,
148                batch,
149                m,
150                k,
151                a_data.len()
152            )));
153        }
154        if b_data.len() != batch * b_stride {
155            return Err(TruenoError::InvalidInput(format!(
156                "B data size mismatch: expected {} ({}×{}×{}), got {}",
157                batch * b_stride,
158                batch,
159                k,
160                n,
161                b_data.len()
162            )));
163        }
164
165        // NOTE: zeros required — gemm_blis accumulates (c += A*B) via load_c_tile.
166        let mut output = vec![0.0f32; batch * out_stride];
167
168        // KAIZEN-039: Call gemm_blis directly on sub-slices instead of
169        // Matrix::from_slice (which copies data). Eliminates 2 × batch Vec
170        // allocations per call (e.g., 64 copies for 32-head attention).
171        for ba in 0..batch {
172            let a_offset = ba * a_stride;
173            let b_offset = ba * b_stride;
174            let out_offset = ba * out_stride;
175
176            let a_slice = &a_data[a_offset..a_offset + a_stride];
177            let b_slice = &b_data[b_offset..b_offset + b_stride];
178            let c_slice = &mut output[out_offset..out_offset + out_stride];
179
180            #[cfg(not(target_arch = "wasm32"))]
181            {
182                crate::blis::gemm_blis(m, n, k, a_slice, b_slice, c_slice, None)?;
183            }
184            #[cfg(target_arch = "wasm32")]
185            {
186                let a_mat = Matrix::from_slice(m, k, a_slice)?;
187                let b_mat = Matrix::from_slice(k, n, b_slice)?;
188                let result = a_mat.matmul(&b_mat)?;
189                c_slice.copy_from_slice(result.as_slice());
190            }
191        }
192
193        Ok(output)
194    }
195
196    /// Batched matrix multiplication for 4D tensors (attention pattern).
197    ///
198    /// Computes `[batch, heads, m, k] @ [batch, heads, k, n] -> [batch, heads, m, n]`
199    #[cfg_attr(
200        feature = "tracing",
201        instrument(skip(a_data, b_data), fields(batch, heads, m, k, n))
202    )]
203    pub fn batched_matmul_4d(
204        a_data: &[f32],
205        b_data: &[f32],
206        batch: usize,
207        heads: usize,
208        m: usize,
209        k: usize,
210        n: usize,
211    ) -> Result<Vec<f32>, TruenoError> {
212        let a_head_stride = m * k;
213        let b_head_stride = k * n;
214        let out_head_stride = m * n;
215        let total_heads = batch * heads;
216
217        let expected_a = total_heads * a_head_stride;
218        let expected_b = total_heads * b_head_stride;
219        if a_data.len() != expected_a {
220            return Err(TruenoError::InvalidInput(format!(
221                "A data size mismatch: expected {} ({}×{}×{}×{}), got {}",
222                expected_a,
223                batch,
224                heads,
225                m,
226                k,
227                a_data.len()
228            )));
229        }
230        if b_data.len() != expected_b {
231            return Err(TruenoError::InvalidInput(format!(
232                "B data size mismatch: expected {} ({}×{}×{}×{}), got {}",
233                expected_b,
234                batch,
235                heads,
236                k,
237                n,
238                b_data.len()
239            )));
240        }
241
242        // NOTE: zeros required — gemm_blis accumulates (c += A*B) via load_c_tile.
243        let mut output = vec![0.0f32; total_heads * out_head_stride];
244
245        // KAIZEN-039: Call gemm_blis directly — eliminates 2 × total_heads
246        // Vec copies per call (e.g., 64 copies for batch=1, heads=32).
247        for bh in 0..total_heads {
248            let a_offset = bh * a_head_stride;
249            let b_offset = bh * b_head_stride;
250            let out_offset = bh * out_head_stride;
251
252            let a_slice = &a_data[a_offset..a_offset + a_head_stride];
253            let b_slice = &b_data[b_offset..b_offset + b_head_stride];
254            let c_slice = &mut output[out_offset..out_offset + out_head_stride];
255
256            #[cfg(not(target_arch = "wasm32"))]
257            {
258                crate::blis::gemm_blis(m, n, k, a_slice, b_slice, c_slice, None)?;
259            }
260            #[cfg(target_arch = "wasm32")]
261            {
262                let a_mat = Matrix::from_slice(m, k, a_slice)?;
263                let b_mat = Matrix::from_slice(k, n, b_slice)?;
264                let result = a_mat.matmul(&b_mat)?;
265                c_slice.copy_from_slice(result.as_slice());
266            }
267        }
268
269        Ok(output)
270    }
271
272    /// Fast path for vector-matrix multiplication (1×K @ K×N → 1×N)
273    ///
274    /// Dispatches to AVX2 SIMD GEMV kernel when available (explicit VFMADD
275    /// with 4-way K-unrolling), falls back to scalar 4-way axpy.
276    /// Bypasses BLIS packing which dominates for M=1.
277    ///
278    /// Contract: matvec-kernel-v1, equation "matvec"
279    #[cfg_attr(feature = "tracing", instrument(skip(self, other), fields(k = self.cols, n = other.cols)))]
280    fn matmul_vector_matrix(&self, other: &Matrix<f32>) -> Result<Matrix<f32>, TruenoError> {
281        debug_assert_eq!(self.rows, 1);
282
283        let k = self.cols;
284        let n = other.cols;
285        // NOTE: zeros required — gemv accumulates (c[j] += a[k]*b[k*n+j]).
286        let mut c = vec![0.0f32; n];
287
288        crate::blis::gemv::gemv(k, n, &self.data, &other.data, &mut c);
289
290        Matrix::from_vec(1, n, c)
291    }
292
293    /// Small-matrix matmul (baseline for dims < 64, below the SIMD/GPU threshold).
294    ///
295    /// Cache-friendly `ikj` ordering: the innermost loop is a contiguous AXPY
296    /// (`c_row[j] += a_ik * b_row[j]`) over row-major memory, which LLVM
297    /// auto-vectorizes. The previous `ijk` form indexed `b[kk * n + j]` with a
298    /// column stride in the hot loop, defeating both the cache and SIMD; it
299    /// measured ~4.7x slower than this AXPY form on 32x32–63x63 matrices — the
300    /// shapes a small-MLP autograd training step hits (the matmul kernel that
301    /// powers the Pillar-1 LinearRegression beat already uses this ordering in
302    /// `aprender::primitives::Matrix::matmul`). `result.data` is pre-zeroed by
303    /// the caller (`Matrix::zeros_with_backend`), so the `+=` accumulation
304    /// starts from 0.
305    fn matmul_naive(
306        &self,
307        other: &Matrix<f32>,
308        result: &mut Matrix<f32>,
309    ) -> Result<(), TruenoError> {
310        let m = self.rows;
311        let k = self.cols;
312        let n = other.cols;
313        let a = &self.data;
314        let b = &other.data;
315        let c = &mut result.data;
316
317        for i in 0..m {
318            let a_row = &a[i * k..i * k + k];
319            let c_row = &mut c[i * n..i * n + n];
320            for kk in 0..k {
321                let a_ik = a_row[kk];
322                let b_row = &b[kk * n..kk * n + n];
323                for (cj, &bj) in c_row.iter_mut().zip(b_row.iter()) {
324                    *cj += a_ik * bj;
325                }
326            }
327        }
328        Ok(())
329    }
330
331    /// WASM-optimized tiled matrix multiplication
332    #[allow(dead_code)]
333    fn matmul_wasm_tiled(
334        &self,
335        other: &Matrix<f32>,
336        result: &mut Matrix<f32>,
337    ) -> Result<(), TruenoError> {
338        let m = self.rows;
339        let k = self.cols;
340        let n = other.cols;
341
342        for i in 0..m {
343            let a_row_start = i * k;
344            let result_row_start = i * n;
345
346            let simd_width = 8;
347            let n_simd = (n / simd_width) * simd_width;
348
349            #[allow(clippy::needless_range_loop)]
350            for j0 in (0..n_simd).step_by(simd_width) {
351                let mut acc = [0.0f32; 8];
352
353                for kk in 0..k {
354                    let a_val = self.data[a_row_start + kk];
355                    let b_row_start = kk * n + j0;
356
357                    for jj in 0..simd_width {
358                        acc[jj] += a_val * other.data[b_row_start + jj];
359                    }
360                }
361
362                for jj in 0..simd_width {
363                    result.data[result_row_start + j0 + jj] = acc[jj];
364                }
365            }
366
367            for j in n_simd..n {
368                let mut sum = 0.0f32;
369                for kk in 0..k {
370                    sum += self.data[a_row_start + kk] * other.data[kk * n + j];
371                }
372                result.data[result_row_start + j] = sum;
373            }
374        }
375
376        Ok(())
377    }
378
379    /// GPU-accelerated matrix multiplication
380    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
381    fn matmul_gpu(&self, other: &Matrix<f32>) -> Result<Matrix<f32>, TruenoError> {
382        // Track 1 (CGP-DBUF): Try cuBLAS first when cuda feature is enabled.
383        // cuBLAS at 105-150 TFLOP/s vs wgpu shader at ~5 TFLOP/s — 20-30× faster.
384        #[cfg(feature = "cuda")]
385        {
386            if let Ok(result) = self.matmul_cublas(other) {
387                return Ok(result);
388            }
389            // cuBLAS unavailable (no GPU, driver error) — fall through to wgpu
390        }
391
392        use crate::backends::gpu::GpuBackend;
393
394        if !GpuBackend::is_available() {
395            return Err(TruenoError::InvalidInput("GPU not available".to_string()));
396        }
397
398        let mut gpu = GpuBackend::new();
399        let result_data = gpu
400            .matmul(&self.data, &other.data, self.rows, self.cols, other.cols)
401            .map_err(|e| TruenoError::InvalidInput(format!("GPU matmul failed: {}", e)))?;
402
403        let mut result = Matrix::zeros(self.rows, other.cols);
404        result.data = result_data;
405
406        Ok(result)
407    }
408
409    /// cuBLAS FP32 GEMM via trueno-gpu's own FFI bindings (OWN THE STACK).
410    /// Uses cublasGemmEx with CUBLAS_COMPUTE_32F for numerical safety.
411    #[cfg(feature = "cuda")]
412    fn matmul_cublas(&self, other: &Matrix<f32>) -> Result<Matrix<f32>, TruenoError> {
413        use trueno_gpu::driver::{CublasHandle, CudaContext, CudaStream, GemmOp, GpuBuffer};
414
415        let m = self.rows;
416        let k = self.cols;
417        let n = other.cols;
418
419        let ctx = CudaContext::new(0)
420            .map_err(|e| TruenoError::InvalidInput(format!("CUDA init: {e}")))?;
421        let stream = CudaStream::new(&ctx)
422            .map_err(|e| TruenoError::InvalidInput(format!("CUDA stream: {e}")))?;
423        let handle = CublasHandle::new(&ctx)
424            .map_err(|e| TruenoError::InvalidInput(format!("cuBLAS init: {e}")))?;
425        handle
426            .set_stream(&stream)
427            .map_err(|e| TruenoError::InvalidInput(format!("cuBLAS stream: {e}")))?;
428
429        let a_buf = GpuBuffer::from_host(&ctx, &self.data)
430            .map_err(|e| TruenoError::InvalidInput(format!("GPU alloc A: {e}")))?;
431        let b_buf = GpuBuffer::from_host(&ctx, &other.data)
432            .map_err(|e| TruenoError::InvalidInput(format!("GPU alloc B: {e}")))?;
433        let c_data = vec![0.0f32; m * n];
434        let c_buf = GpuBuffer::from_host(&ctx, &c_data)
435            .map_err(|e| TruenoError::InvalidInput(format!("GPU alloc C: {e}")))?;
436
437        handle
438            .gemm_f32_row_major(
439                m as i32,
440                n as i32,
441                k as i32,
442                1.0,
443                a_buf.as_ptr(),
444                b_buf.as_ptr(),
445                0.0,
446                c_buf.as_ptr(),
447            )
448            .map_err(|e| TruenoError::InvalidInput(format!("cuBLAS GEMM: {e}")))?;
449
450        stream.synchronize().map_err(|e| TruenoError::InvalidInput(format!("CUDA sync: {e}")))?;
451
452        let mut result_data = vec![0.0f32; m * n];
453        c_buf
454            .copy_to_host(&mut result_data)
455            .map_err(|e| TruenoError::InvalidInput(format!("GPU readback: {e}")))?;
456
457        Ok(Matrix { rows: m, cols: n, data: result_data, backend: self.backend })
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_matmul_basic() {
467        let a = Matrix::from_vec(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
468        let b = Matrix::from_vec(2, 2, vec![5.0, 6.0, 7.0, 8.0]).unwrap();
469        let c = a.matmul(&b).unwrap();
470
471        assert_eq!(c.get(0, 0), Some(&19.0));
472        assert_eq!(c.get(0, 1), Some(&22.0));
473        assert_eq!(c.get(1, 0), Some(&43.0));
474        assert_eq!(c.get(1, 1), Some(&50.0));
475    }
476
477    #[test]
478    fn test_matmul_dimension_mismatch() {
479        let a = Matrix::from_vec(2, 3, vec![1.0; 6]).unwrap();
480        let b = Matrix::from_vec(2, 2, vec![1.0; 4]).unwrap();
481        assert!(a.matmul(&b).is_err());
482    }
483
484    #[test]
485    fn test_matmul_identity() {
486        let a = Matrix::from_vec(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
487        let i = Matrix::identity(2);
488        let result = a.matmul(&i).unwrap();
489        assert_eq!(result.as_slice(), a.as_slice());
490    }
491
492    #[test]
493    fn test_batched_matmul() {
494        let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; // 2 batches of 2×2
495        let b = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0]; // 2 identity matrices
496        let result = Matrix::batched_matmul(&a, &b, 2, 2, 2, 2).unwrap();
497        assert_eq!(result, a); // A × I = A
498    }
499
500    #[test]
501    fn test_batched_matmul_a_size_mismatch() {
502        let a = vec![1.0, 2.0, 3.0]; // Wrong size: should be 2*2*2=8
503        let b = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0];
504        let result = Matrix::batched_matmul(&a, &b, 2, 2, 2, 2);
505        assert!(matches!(result, Err(TruenoError::InvalidInput(_))));
506    }
507
508    #[test]
509    fn test_batched_matmul_b_size_mismatch() {
510        let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
511        let b = vec![1.0, 0.0]; // Wrong size: should be 2*2*2=8
512        let result = Matrix::batched_matmul(&a, &b, 2, 2, 2, 2);
513        assert!(matches!(result, Err(TruenoError::InvalidInput(_))));
514    }
515
516    #[test]
517    fn test_batched_matmul_single_batch() {
518        // Single batch 3x2 @ 2x4 = 3x4
519        let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 3x2
520        let b = vec![1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0]; // 2x4
521        let result = Matrix::batched_matmul(&a, &b, 1, 3, 2, 4).unwrap();
522        assert_eq!(result.len(), 12); // 3x4
523    }
524
525    #[test]
526    fn test_batched_matmul_4d_basic() {
527        // batch=1, heads=1, m=2, k=2, n=2
528        let a = vec![1.0, 2.0, 3.0, 4.0]; // 2x2
529        let b = vec![1.0, 0.0, 0.0, 1.0]; // identity
530        let result = Matrix::batched_matmul_4d(&a, &b, 1, 1, 2, 2, 2).unwrap();
531        assert_eq!(result, a);
532    }
533
534    #[test]
535    fn test_batched_matmul_4d_a_size_mismatch() {
536        let a = vec![1.0]; // Wrong: should be 2*2*3*4=48
537        let b: Vec<f32> = (0..80).map(|x| x as f32 * 0.1).collect();
538        let result = Matrix::batched_matmul_4d(&a, &b, 2, 2, 3, 4, 5);
539        assert!(matches!(result, Err(TruenoError::InvalidInput(_))));
540    }
541
542    #[test]
543    fn test_batched_matmul_4d_b_size_mismatch() {
544        let a: Vec<f32> = (0..48).map(|x| x as f32 * 0.1).collect();
545        let b = vec![1.0]; // Wrong: should be 2*2*4*5=80
546        let result = Matrix::batched_matmul_4d(&a, &b, 2, 2, 3, 4, 5);
547        assert!(matches!(result, Err(TruenoError::InvalidInput(_))));
548    }
549
550    #[test]
551    fn test_batched_matmul_4d_multi_head() {
552        // batch=1, heads=4, m=2, k=2, n=2 (like attention heads)
553        let total = 4 * 2 * 2; // 16 elements for A
554        let a: Vec<f32> = (0..total).map(|_| 1.0).collect();
555        let b: Vec<f32> = (0..total).map(|_| 1.0).collect();
556        let result = Matrix::batched_matmul_4d(&a, &b, 1, 4, 2, 2, 2).unwrap();
557        assert_eq!(result.len(), total);
558        // Each element should be 2.0 (dot product of two 1.0 vectors of length 2)
559        for val in &result {
560            assert!((*val - 2.0).abs() < 1e-5);
561        }
562    }
563
564    #[test]
565    fn test_matmul_vector_matrix_path() {
566        // 1×K @ K×N triggers the vector-matrix fast path
567        let a = Matrix::from_vec(1, 4, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
568        let b = Matrix::from_vec(
569            4,
570            3,
571            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],
572        )
573        .unwrap();
574        let result = a.matmul(&b).unwrap();
575        assert_eq!(result.rows(), 1);
576        assert_eq!(result.cols(), 3);
577        // [1*1+2*0+3*0+4*1, 1*0+2*1+3*0+4*1, 1*0+2*0+3*1+4*1] = [5, 6, 7]
578        assert!((result.get(0, 0).unwrap() - 5.0).abs() < 1e-5);
579        assert!((result.get(0, 1).unwrap() - 6.0).abs() < 1e-5);
580        assert!((result.get(0, 2).unwrap() - 7.0).abs() < 1e-5);
581    }
582
583    #[test]
584    fn test_matmul_vector_matrix_with_zeros() {
585        // Test that zero elements in the vector skip computation
586        let a = Matrix::from_vec(1, 3, vec![0.0, 2.0, 0.0]).unwrap();
587        let b = Matrix::from_vec(3, 2, vec![100.0, 200.0, 3.0, 4.0, 500.0, 600.0]).unwrap();
588        let result = a.matmul(&b).unwrap();
589        // Only the second row of B contributes: [2*3, 2*4] = [6, 8]
590        assert!((result.get(0, 0).unwrap() - 6.0).abs() < 1e-5);
591        assert!((result.get(0, 1).unwrap() - 8.0).abs() < 1e-5);
592    }
593
594    // =========================================================================
595    // matmul_wasm_tiled tests
596    // =========================================================================
597    // These tests call the private WASM-tiled matmul directly (not behind
598    // #[cfg(target_arch = "wasm32")]) to achieve coverage on non-WASM hosts.
599
600    #[test]
601    fn test_matmul_wasm_tiled_small_no_simd() {
602        // n=3 < simd_width(8), so only the remainder path executes.
603        // 2x4 @ 4x3 = 2x3
604        let a = Matrix::from_vec(2, 4, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]).unwrap();
605        let b = Matrix::from_vec(
606            4,
607            3,
608            vec![1.0, 0.0, 2.0, 0.0, 1.0, 0.0, 2.0, 0.0, 1.0, 0.0, 2.0, 0.0],
609        )
610        .unwrap();
611        let mut result = Matrix::zeros(2, 3);
612        a.matmul_wasm_tiled(&b, &mut result).unwrap();
613
614        // Row 0: [1*1+2*0+3*2+4*0, 1*0+2*1+3*0+4*2, 1*2+2*0+3*1+4*0] = [7, 10, 5]
615        assert!((result.get(0, 0).unwrap() - 7.0).abs() < 1e-5);
616        assert!((result.get(0, 1).unwrap() - 10.0).abs() < 1e-5);
617        assert!((result.get(0, 2).unwrap() - 5.0).abs() < 1e-5);
618
619        // Row 1: [5*1+6*0+7*2+8*0, 5*0+6*1+7*0+8*2, 5*2+6*0+7*1+8*0] = [19, 22, 17]
620        assert!((result.get(1, 0).unwrap() - 19.0).abs() < 1e-5);
621        assert!((result.get(1, 1).unwrap() - 22.0).abs() < 1e-5);
622        assert!((result.get(1, 2).unwrap() - 17.0).abs() < 1e-5);
623    }
624
625    #[test]
626    fn test_matmul_wasm_tiled_exact_simd_width() {
627        // n=8 exactly equals simd_width, so the SIMD path handles all columns
628        // and the remainder path has zero iterations.
629        // 2x3 @ 3x8 = 2x8
630        let a = Matrix::from_vec(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
631        let b_data: Vec<f32> = (1..=24).map(|x| x as f32).collect(); // 3x8
632        let b = Matrix::from_vec(3, 8, b_data).unwrap();
633        let mut result = Matrix::zeros(2, 8);
634        a.matmul_wasm_tiled(&b, &mut result).unwrap();
635
636        // Verify against naive matmul
637        let mut expected = Matrix::zeros(2, 8);
638        a.matmul_naive(&b, &mut expected).unwrap();
639        for i in 0..2 {
640            for j in 0..8 {
641                assert!(
642                    (result.get(i, j).unwrap() - expected.get(i, j).unwrap()).abs() < 1e-4,
643                    "Mismatch at ({}, {}): wasm_tiled={}, naive={}",
644                    i,
645                    j,
646                    result.get(i, j).unwrap(),
647                    expected.get(i, j).unwrap()
648                );
649            }
650        }
651    }
652
653    #[test]
654    fn test_matmul_wasm_tiled_simd_plus_remainder() {
655        // n=11 => n_simd=8 (SIMD path handles columns 0..8),
656        // remainder path handles columns 8..11. Both paths exercise.
657        // 3x4 @ 4x11 = 3x11
658        let a_data: Vec<f32> = (1..=12).map(|x| x as f32).collect();
659        let a = Matrix::from_vec(3, 4, a_data).unwrap();
660        let b_data: Vec<f32> = (1..=44).map(|x| x as f32 * 0.1).collect();
661        let b = Matrix::from_vec(4, 11, b_data).unwrap();
662        let mut result = Matrix::zeros(3, 11);
663        a.matmul_wasm_tiled(&b, &mut result).unwrap();
664
665        // Verify against naive
666        let mut expected = Matrix::zeros(3, 11);
667        a.matmul_naive(&b, &mut expected).unwrap();
668        for i in 0..3 {
669            for j in 0..11 {
670                assert!(
671                    (result.get(i, j).unwrap() - expected.get(i, j).unwrap()).abs() < 1e-3,
672                    "Mismatch at ({}, {}): wasm_tiled={}, naive={}",
673                    i,
674                    j,
675                    result.get(i, j).unwrap(),
676                    expected.get(i, j).unwrap()
677                );
678            }
679        }
680    }
681
682    #[test]
683    fn test_matmul_wasm_tiled_multiple_simd_blocks() {
684        // n=16 => two full SIMD blocks (0..8 and 8..16), no remainder.
685        // 2x2 @ 2x16 = 2x16
686        let a = Matrix::from_vec(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
687        let b_data: Vec<f32> = (1..=32).map(|x| x as f32).collect();
688        let b = Matrix::from_vec(2, 16, b_data).unwrap();
689        let mut result = Matrix::zeros(2, 16);
690        a.matmul_wasm_tiled(&b, &mut result).unwrap();
691
692        let mut expected = Matrix::zeros(2, 16);
693        a.matmul_naive(&b, &mut expected).unwrap();
694        for i in 0..2 {
695            for j in 0..16 {
696                assert!(
697                    (result.get(i, j).unwrap() - expected.get(i, j).unwrap()).abs() < 1e-3,
698                    "Mismatch at ({}, {})",
699                    i,
700                    j,
701                );
702            }
703        }
704    }
705
706    #[test]
707    fn test_matmul_wasm_tiled_single_row() {
708        // m=1, n=10 => SIMD block 0..8 + remainder 8..10
709        // 1x5 @ 5x10 = 1x10
710        let a = Matrix::from_vec(1, 5, vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
711        let b_data: Vec<f32> = (1..=50).map(|x| x as f32 * 0.1).collect();
712        let b = Matrix::from_vec(5, 10, b_data).unwrap();
713        let mut result = Matrix::zeros(1, 10);
714        a.matmul_wasm_tiled(&b, &mut result).unwrap();
715
716        let mut expected = Matrix::zeros(1, 10);
717        a.matmul_naive(&b, &mut expected).unwrap();
718        for j in 0..10 {
719            assert!(
720                (result.get(0, j).unwrap() - expected.get(0, j).unwrap()).abs() < 1e-3,
721                "Mismatch at col {}: wasm_tiled={}, naive={}",
722                j,
723                result.get(0, j).unwrap(),
724                expected.get(0, j).unwrap()
725            );
726        }
727    }
728
729    #[test]
730    fn test_matmul_wasm_tiled_identity() {
731        // Multiplying by identity should return the original matrix.
732        // 4x4 identity, n=4 < 8 so only remainder path.
733        let a = Matrix::from_vec(
734            4,
735            4,
736            vec![
737                1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
738                16.0,
739            ],
740        )
741        .unwrap();
742        let identity = Matrix::identity(4);
743        let mut result = Matrix::zeros(4, 4);
744        a.matmul_wasm_tiled(&identity, &mut result).unwrap();
745
746        assert_eq!(result.as_slice(), a.as_slice());
747    }
748
749    #[test]
750    fn test_matmul_wasm_tiled_large_mixed() {
751        // Larger test: 5x10 @ 10x19 = 5x19
752        // n=19 => n_simd=16, remainder 16..19
753        // Exercises multiple SIMD blocks (0..8, 8..16) plus remainder (16..19).
754        let a_data: Vec<f32> = (0..50).map(|x| (x as f32) * 0.1).collect();
755        let a = Matrix::from_vec(5, 10, a_data).unwrap();
756        let b_data: Vec<f32> = (0..190).map(|x| (x as f32) * 0.01).collect();
757        let b = Matrix::from_vec(10, 19, b_data).unwrap();
758        let mut result = Matrix::zeros(5, 19);
759        a.matmul_wasm_tiled(&b, &mut result).unwrap();
760
761        let mut expected = Matrix::zeros(5, 19);
762        a.matmul_naive(&b, &mut expected).unwrap();
763        for i in 0..5 {
764            for j in 0..19 {
765                assert!(
766                    (result.get(i, j).unwrap() - expected.get(i, j).unwrap()).abs() < 1e-2,
767                    "Mismatch at ({}, {}): wasm_tiled={}, naive={}",
768                    i,
769                    j,
770                    result.get(i, j).unwrap(),
771                    expected.get(i, j).unwrap()
772                );
773            }
774        }
775    }
776
777    // =========================================================================
778    // FALSIFY-MM: matmul-kernel-v1.yaml contract (trueno Matrix::matmul)
779    //
780    // Five-Whys (PMAT-354):
781    //   Why 1: trueno had 10+ matmul tests but zero FALSIFY-MM-* tests
782    //   Why 2: unit tests verify known products, not mathematical invariants
783    //   Why 3: no mapping from matmul-kernel-v1.yaml to trueno test names
784    //   Why 4: trueno predates the provable-contracts YAML convention
785    //   Why 5: matmul was "obviously correct" (standard GEMM)
786    //
787    // References:
788    //   - provable-contracts/contracts/matmul-kernel-v1.yaml
789    // =========================================================================
790
791    /// FALSIFY-MM-001: Shape correctness — matmul(A[m,p], B[p,n]) = [m,n]
792    #[test]
793    fn falsify_mm_001_shape_correctness() {
794        for &(m, p, n) in &[(1, 1, 1), (2, 3, 4), (16, 32, 8), (1, 100, 1), (64, 1, 64)] {
795            let a = Matrix::from_vec(m, p, vec![1.0; m * p]).unwrap();
796            let b = Matrix::from_vec(p, n, vec![1.0; p * n]).unwrap();
797            let c = a.matmul(&b).unwrap();
798            assert_eq!(
799                (c.rows(), c.cols()),
800                (m, n),
801                "FALSIFIED MM-001: matmul([{m},{p}], [{p},{n}]) shape = [{},{}], expected [{m},{n}]",
802                c.rows(),
803                c.cols()
804            );
805        }
806    }
807
808    /// FALSIFY-MM-005: Identity matrix — matmul(A, I) = A
809    #[test]
810    fn falsify_mm_005_identity_matrix() {
811        let a = Matrix::from_vec(3, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
812        let eye =
813            Matrix::from_vec(3, 3, vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap();
814
815        let ai = a.matmul(&eye).unwrap();
816        let ia = eye.matmul(&a).unwrap();
817
818        for i in 0..3 {
819            for j in 0..3 {
820                let expected = a.get(i, j).unwrap();
821                assert!(
822                    (*ai.get(i, j).unwrap() - expected).abs() < 1e-6,
823                    "FALSIFIED MM-005: (A*I)[{i},{j}] = {}, expected {expected}",
824                    ai.get(i, j).unwrap()
825                );
826                assert!(
827                    (*ia.get(i, j).unwrap() - expected).abs() < 1e-6,
828                    "FALSIFIED MM-005: (I*A)[{i},{j}] = {}, expected {expected}",
829                    ia.get(i, j).unwrap()
830                );
831            }
832        }
833    }
834
835    /// FALSIFY-MM-002: Numerical accuracy — known product verified
836    #[test]
837    fn falsify_mm_002_numerical_accuracy() {
838        let a = Matrix::from_vec(2, 2, vec![1.0, 2.0, 3.0, 4.0]).unwrap();
839        let b = Matrix::from_vec(2, 2, vec![5.0, 6.0, 7.0, 8.0]).unwrap();
840        let c = a.matmul(&b).unwrap();
841
842        let expected = [19.0, 22.0, 43.0, 50.0];
843        for (i, &exp) in expected.iter().enumerate() {
844            let row = i / 2;
845            let col = i % 2;
846            let val = *c.get(row, col).unwrap();
847            assert!(
848                (val - exp).abs() < 1e-5,
849                "FALSIFIED MM-002: C[{row},{col}] = {val}, expected {exp}"
850            );
851        }
852    }
853
854    /// FALSIFY-MM-002b: matmul(zeros, B) = zeros
855    #[test]
856    fn falsify_mm_002b_zero_annihilation() {
857        let zero = Matrix::from_vec(3, 4, vec![0.0; 12]).unwrap();
858        let b = Matrix::from_vec(4, 2, vec![1.0; 8]).unwrap();
859        let c = zero.matmul(&b).unwrap();
860
861        for i in 0..3 {
862            for j in 0..2 {
863                let val = *c.get(i, j).unwrap();
864                assert!(
865                    val.abs() < 1e-10,
866                    "FALSIFIED MM-002b: zeros*B [{i},{j}] = {val}, expected 0"
867                );
868            }
869        }
870    }
871}
872
873#[cfg(test)]
874#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
875mod gpu_tests {
876    use super::*;
877
878    /// Test matmul_gpu via public API with matrices large enough to exceed
879    /// GPU_THRESHOLD (all dimensions >= 500).
880    /// Uses identity multiplication: A * I = A.
881    #[test]
882    fn test_matmul_gpu_identity() {
883        use crate::backends::gpu::GpuBackend;
884
885        if !GpuBackend::is_available() {
886            eprintln!("GPU not available, skipping test_matmul_gpu_identity");
887            return;
888        }
889
890        let n = 500; // Meets GPU_THRESHOLD for all three dimensions
891
892        // Create a simple test matrix: A[i,j] = (i*n + j) mod 100 * 0.01
893        let a_data: Vec<f32> = (0..n * n).map(|i| (i % 100) as f32 * 0.01).collect();
894
895        // Identity matrix
896        let mut i_data = vec![0.0f32; n * n];
897        for i in 0..n {
898            i_data[i * n + i] = 1.0;
899        }
900
901        let a = Matrix::from_vec(n, n, a_data.clone()).expect("valid matrix A");
902        let identity = Matrix::from_vec(n, n, i_data).expect("valid identity matrix");
903
904        let result = a.matmul(&identity).expect("matmul should succeed");
905
906        assert_eq!(result.rows(), n);
907        assert_eq!(result.cols(), n);
908
909        // A * I = A: sample verification (check corners and center)
910        let check_indices = [(0, 0), (0, n - 1), (n - 1, 0), (n - 1, n - 1), (n / 2, n / 2)];
911        for &(r, c) in &check_indices {
912            let expected = a_data[r * n + c];
913            let actual = *result.get(r, c).unwrap();
914            assert!(
915                (actual - expected).abs() < 1e-2,
916                "A*I mismatch at ({},{}): gpu={}, expected={}",
917                r,
918                c,
919                actual,
920                expected
921            );
922        }
923    }
924
925    /// Test matmul_gpu with all-ones matrices: result should be all-K.
926    #[test]
927    fn test_matmul_gpu_ones() {
928        use crate::backends::gpu::GpuBackend;
929
930        if !GpuBackend::is_available() {
931            eprintln!("GPU not available, skipping test_matmul_gpu_ones");
932            return;
933        }
934
935        let m = 500;
936        let k = 500;
937        let n = 500;
938
939        let a = Matrix::from_vec(m, k, vec![1.0f32; m * k]).expect("valid matrix A");
940        let b = Matrix::from_vec(k, n, vec![1.0f32; k * n]).expect("valid matrix B");
941
942        let result = a.matmul(&b).expect("matmul should succeed");
943
944        assert_eq!(result.rows(), m);
945        assert_eq!(result.cols(), n);
946
947        // Each element of C should be K (dot product of K ones with K ones)
948        let expected = k as f32;
949        for i in 0..10 {
950            for j in 0..10 {
951                assert!(
952                    (result.get(i, j).unwrap() - expected).abs() < 1.0,
953                    "C[{},{}] = {}, expected {}",
954                    i,
955                    j,
956                    result.get(i, j).unwrap(),
957                    expected
958                );
959            }
960        }
961    }
962
963    /// Test matmul_gpu directly via the private helper method.
964    #[test]
965    fn test_matmul_gpu_direct() {
966        use crate::backends::gpu::GpuBackend;
967
968        if !GpuBackend::is_available() {
969            eprintln!("GPU not available, skipping test_matmul_gpu_direct");
970            return;
971        }
972
973        // Small matrix for direct private method test
974        let a = Matrix::from_vec(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("valid A");
975        let b = Matrix::from_vec(3, 2, vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0]).expect("valid B");
976
977        let result = a.matmul_gpu(&b).expect("matmul_gpu should succeed");
978
979        assert_eq!(result.rows(), 2);
980        assert_eq!(result.cols(), 2);
981
982        // C = A * B
983        // C[0,0] = 1*7 + 2*9 + 3*11 = 7 + 18 + 33 = 58
984        // C[0,1] = 1*8 + 2*10 + 3*12 = 8 + 20 + 36 = 64
985        // C[1,0] = 4*7 + 5*9 + 6*11 = 28 + 45 + 66 = 139
986        // C[1,1] = 4*8 + 5*10 + 6*12 = 32 + 50 + 72 = 154
987        assert!(
988            (result.get(0, 0).unwrap() - 58.0).abs() < 1e-2,
989            "Expected 58.0, got {}",
990            result.get(0, 0).unwrap()
991        );
992        assert!(
993            (result.get(0, 1).unwrap() - 64.0).abs() < 1e-2,
994            "Expected 64.0, got {}",
995            result.get(0, 1).unwrap()
996        );
997        assert!(
998            (result.get(1, 0).unwrap() - 139.0).abs() < 1e-2,
999            "Expected 139.0, got {}",
1000            result.get(1, 0).unwrap()
1001        );
1002        assert!(
1003            (result.get(1, 1).unwrap() - 154.0).abs() < 1e-2,
1004            "Expected 154.0, got {}",
1005            result.get(1, 1).unwrap()
1006        );
1007    }
1008
1009    /// Test matmul_gpu returns error when GPU is unavailable.
1010    #[test]
1011    fn test_matmul_gpu_not_available_path() {
1012        use crate::backends::gpu::GpuBackend;
1013
1014        // This test verifies the GpuBackend::is_available() check in matmul_gpu
1015        // If GPU IS available, the function should succeed; test the full path
1016        if !GpuBackend::is_available() {
1017            // If GPU is not available, matmul_gpu should return an error
1018            let a = Matrix::from_vec(2, 2, vec![1.0; 4]).unwrap();
1019            let b = Matrix::from_vec(2, 2, vec![1.0; 4]).unwrap();
1020            let result = a.matmul_gpu(&b);
1021            assert!(result.is_err(), "matmul_gpu should fail without GPU");
1022        }
1023    }
1024}