1use crate::TruenoError;
21
22#[cfg(feature = "tracing")]
23use tracing::instrument;
24
25use super::super::Matrix;
26
27impl Matrix<f32> {
28 #[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 if self.rows == 1 {
78 return self.matmul_vector_matrix(other);
79 }
80
81 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 #[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 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 #[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 let mut output = vec![0.0f32; batch * out_stride];
167
168 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 #[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 let mut output = vec![0.0f32; total_heads * out_head_stride];
244
245 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 #[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 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 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 #[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 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
381 fn matmul_gpu(&self, other: &Matrix<f32>) -> Result<Matrix<f32>, TruenoError> {
382 #[cfg(feature = "cuda")]
385 {
386 if let Ok(result) = self.matmul_cublas(other) {
387 return Ok(result);
388 }
389 }
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 #[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]; let b = vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0]; let result = Matrix::batched_matmul(&a, &b, 2, 2, 2, 2).unwrap();
497 assert_eq!(result, a); }
499
500 #[test]
501 fn test_batched_matmul_a_size_mismatch() {
502 let a = vec![1.0, 2.0, 3.0]; 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]; 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 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b = vec![1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0]; let result = Matrix::batched_matmul(&a, &b, 1, 3, 2, 4).unwrap();
522 assert_eq!(result.len(), 12); }
524
525 #[test]
526 fn test_batched_matmul_4d_basic() {
527 let a = vec![1.0, 2.0, 3.0, 4.0]; let b = vec![1.0, 0.0, 0.0, 1.0]; 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]; 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]; 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 let total = 4 * 2 * 2; 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 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 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 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 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 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 #[test]
601 fn test_matmul_wasm_tiled_small_no_simd() {
602 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 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 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 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(); 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 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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]
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; let a_data: Vec<f32> = (0..n * n).map(|i| (i % 100) as f32 * 0.01).collect();
894
895 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 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]
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 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]
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 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 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]
1011 fn test_matmul_gpu_not_available_path() {
1012 use crate::backends::gpu::GpuBackend;
1013
1014 if !GpuBackend::is_available() {
1017 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}