1use std::sync::Arc;
34
35use torsh_core::{
36 dtype::TensorElement,
37 error::{Result, TorshError},
38};
39
40use crate::core_ops::Tensor;
41
42const BLOCK_K: usize = 64;
44const BLOCK_N: usize = 128;
45
46pub(crate) fn gemm_into<T>(
51 m: usize,
52 k: usize,
53 n: usize,
54 a: &[T],
55 b: &[T],
56 c: &mut [T],
57) -> Result<()>
58where
59 T: TensorElement + Copy + num_traits::Float,
60{
61 if a.len() < m * k || b.len() < k * n || c.len() < m * n {
62 return Err(TorshError::InvalidArgument(format!(
63 "matmul: buffer too small for {}x{} @ {}x{} (got {}, {}, {})",
64 m,
65 k,
66 k,
67 n,
68 a.len(),
69 b.len(),
70 c.len()
71 )));
72 }
73
74 if m == 0 || n == 0 {
75 return Ok(());
76 }
77
78 if gemm_blas_f32(m, k, n, a, b, c) || gemm_blas_f64(m, k, n, a, b, c) {
79 return Ok(());
80 }
81
82 gemm_blocked(m, k, n, a, b, c);
83 Ok(())
84}
85
86fn gemm_blas_f32<T: TensorElement + Copy>(
89 m: usize,
90 k: usize,
91 n: usize,
92 a: &[T],
93 b: &[T],
94 c: &mut [T],
95) -> bool {
96 if std::any::TypeId::of::<T>() != std::any::TypeId::of::<f32>() {
97 return false;
98 }
99 let a_f32: &[f32] = unsafe { std::slice::from_raw_parts(a.as_ptr() as *const f32, m * k) };
101 let b_f32: &[f32] = unsafe { std::slice::from_raw_parts(b.as_ptr() as *const f32, k * n) };
102 let c_f32: &mut [f32] =
103 unsafe { std::slice::from_raw_parts_mut(c.as_mut_ptr() as *mut f32, m * n) };
104 gemm_ndarray_f32(m, k, n, a_f32, b_f32, c_f32)
105}
106
107fn gemm_blas_f64<T: TensorElement + Copy>(
109 m: usize,
110 k: usize,
111 n: usize,
112 a: &[T],
113 b: &[T],
114 c: &mut [T],
115) -> bool {
116 if std::any::TypeId::of::<T>() != std::any::TypeId::of::<f64>() {
117 return false;
118 }
119 let a_f64: &[f64] = unsafe { std::slice::from_raw_parts(a.as_ptr() as *const f64, m * k) };
121 let b_f64: &[f64] = unsafe { std::slice::from_raw_parts(b.as_ptr() as *const f64, k * n) };
122 let c_f64: &mut [f64] =
123 unsafe { std::slice::from_raw_parts_mut(c.as_mut_ptr() as *mut f64, m * n) };
124 gemm_ndarray_f64(m, k, n, a_f64, b_f64, c_f64)
125}
126
127macro_rules! ndarray_gemm_impl {
128 ($name:ident, $ty:ty) => {
129 fn $name(m: usize, k: usize, n: usize, a: &[$ty], b: &[$ty], c: &mut [$ty]) -> bool {
131 use scirs2_core::ndarray::{linalg::general_mat_mul, ArrayView2, ArrayViewMut2};
132
133 let a_view = match ArrayView2::from_shape((m, k), &a[..m * k]) {
134 Ok(view) => view,
135 Err(_) => return false,
136 };
137 let b_view = match ArrayView2::from_shape((k, n), &b[..k * n]) {
138 Ok(view) => view,
139 Err(_) => return false,
140 };
141 let mut c_view = match ArrayViewMut2::from_shape((m, n), &mut c[..m * n]) {
142 Ok(view) => view,
143 Err(_) => return false,
144 };
145 general_mat_mul(1.0, &a_view, &b_view, 0.0, &mut c_view);
146 true
147 }
148 };
149}
150
151ndarray_gemm_impl!(gemm_ndarray_f32, f32);
152ndarray_gemm_impl!(gemm_ndarray_f64, f64);
153
154fn gemm_blocked<T>(m: usize, k: usize, n: usize, a: &[T], b: &[T], c: &mut [T])
156where
157 T: TensorElement + Copy + num_traits::Float,
158{
159 let zero = <T as num_traits::Zero>::zero();
160 for value in c[..m * n].iter_mut() {
161 *value = zero;
162 }
163
164 for k_block in (0..k).step_by(BLOCK_K) {
165 let k_end = (k_block + BLOCK_K).min(k);
166 for n_block in (0..n).step_by(BLOCK_N) {
167 let n_end = (n_block + BLOCK_N).min(n);
168 for i in 0..m {
169 let a_row = &a[i * k..i * k + k];
170 let c_row = &mut c[i * n..i * n + n];
171 for (kk, &a_ik) in a_row.iter().enumerate().take(k_end).skip(k_block) {
172 if a_ik == zero {
173 continue;
174 }
175 let b_row = &b[kk * n + n_block..kk * n + n_end];
176 for (c_val, &b_val) in c_row[n_block..n_end].iter_mut().zip(b_row.iter()) {
177 *c_val = *c_val + a_ik * b_val;
178 }
179 }
180 }
181 }
182 }
183}
184
185fn broadcast_batch_dims(lhs: &[usize], rhs: &[usize]) -> Result<Vec<usize>> {
187 let rank = lhs.len().max(rhs.len());
188 let mut out = vec![1usize; rank];
189 for i in 0..rank {
190 let l = if i + lhs.len() >= rank {
191 lhs[i + lhs.len() - rank]
192 } else {
193 1
194 };
195 let r = if i + rhs.len() >= rank {
196 rhs[i + rhs.len() - rank]
197 } else {
198 1
199 };
200 out[i] = if l == r {
201 l
202 } else if l == 1 {
203 r
204 } else if r == 1 {
205 l
206 } else {
207 return Err(TorshError::ShapeMismatch {
208 expected: lhs.to_vec(),
209 got: rhs.to_vec(),
210 });
211 };
212 }
213 Ok(out)
214}
215
216fn batch_offset(coords: &[usize], shape: &[usize], matrix_stride: usize) -> usize {
218 let rank = shape.len();
219 let mut offset = 0usize;
220 let mut stride = matrix_stride;
221 for axis in (0..rank).rev() {
222 let coord_idx = coords.len() + axis - rank;
223 let coord = if shape[axis] == 1 {
224 0
225 } else {
226 coords[coord_idx]
227 };
228 offset += coord * stride;
229 stride *= shape[axis];
230 }
231 offset
232}
233
234impl<T: TensorElement + Copy> Tensor<T> {
235 pub fn matmul(&self, other: &Self) -> Result<Self>
242 where
243 T: num_traits::Float + std::iter::Sum,
244 {
245 let mut result = self.basic_matmul(other)?;
246
247 if crate::should_record_grad(self.requires_grad || other.requires_grad) {
251 result.requires_grad = true;
252 result.operation = crate::core_ops::Operation::MatMul {
253 lhs: Arc::new(self.clone()),
254 rhs: Arc::new(other.clone()),
255 };
256 }
257 Ok(result)
258 }
259
260 pub(crate) fn basic_matmul(&self, other: &Self) -> Result<Self>
263 where
264 T: num_traits::Float + std::iter::Sum,
265 {
266 let lhs_binding = self.shape();
267 let lhs_shape = lhs_binding.dims().to_vec();
268 let rhs_binding = other.shape();
269 let rhs_shape = rhs_binding.dims().to_vec();
270
271 if lhs_shape.is_empty() || rhs_shape.is_empty() {
272 return Err(TorshError::InvalidArgument(
273 "matmul requires tensors with at least one dimension".to_string(),
274 ));
275 }
276
277 let lhs_was_vector = lhs_shape.len() == 1;
279 let rhs_was_vector = rhs_shape.len() == 1;
280
281 let mut lhs_dims = lhs_shape.clone();
282 if lhs_was_vector {
283 lhs_dims.insert(0, 1);
284 }
285 let mut rhs_dims = rhs_shape.clone();
286 if rhs_was_vector {
287 rhs_dims.push(1);
288 }
289
290 let m = lhs_dims[lhs_dims.len() - 2];
291 let k = lhs_dims[lhs_dims.len() - 1];
292 let k_rhs = rhs_dims[rhs_dims.len() - 2];
293 let n = rhs_dims[rhs_dims.len() - 1];
294
295 if k != k_rhs {
296 return Err(TorshError::ShapeMismatch {
297 expected: vec![m, k],
298 got: vec![k_rhs, n],
299 });
300 }
301
302 let lhs_batch = lhs_dims[..lhs_dims.len() - 2].to_vec();
303 let rhs_batch = rhs_dims[..rhs_dims.len() - 2].to_vec();
304 let batch_shape = broadcast_batch_dims(&lhs_batch, &rhs_batch)?;
305 let batch_count: usize = batch_shape.iter().product();
306
307 let mut result_data = vec![<T as num_traits::Zero>::zero(); batch_count * m * n];
308
309 let rhs_data = other.to_vec()?;
314
315 self.with_contiguous_data(|lhs_data| {
316 {
317 let rhs_data: &[T] = &rhs_data;
318 if batch_shape.is_empty() {
319 return gemm_into(m, k, n, lhs_data, rhs_data, &mut result_data);
320 }
321
322 let mut coords = vec![0usize; batch_shape.len()];
323 for batch in 0..batch_count {
324 let mut remaining = batch;
326 for axis in (0..batch_shape.len()).rev() {
327 coords[axis] = remaining % batch_shape[axis];
328 remaining /= batch_shape[axis];
329 }
330
331 let lhs_offset = batch_offset(&coords, &lhs_batch, m * k);
332 let rhs_offset = batch_offset(&coords, &rhs_batch, k * n);
333 let out_offset = batch * m * n;
334
335 gemm_into(
336 m,
337 k,
338 n,
339 &lhs_data[lhs_offset..lhs_offset + m * k],
340 &rhs_data[rhs_offset..rhs_offset + k * n],
341 &mut result_data[out_offset..out_offset + m * n],
342 )?;
343 }
344 Ok(())
345 }
346 })?;
347
348 let mut out_shape = batch_shape;
350 out_shape.push(m);
351 out_shape.push(n);
352 if rhs_was_vector {
353 out_shape.pop();
354 }
355 if lhs_was_vector {
356 let row_axis = out_shape.len() - if rhs_was_vector { 1 } else { 2 };
357 out_shape.remove(row_axis);
358 }
359
360 Self::from_data(result_data, out_shape, self.device())
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use torsh_core::device::DeviceType;
368
369 #[test]
370 fn gemm_into_matches_reference_for_f32() {
371 let (m, k, n) = (3usize, 4usize, 2usize);
372 let a: Vec<f32> = (0..m * k).map(|i| i as f32 * 0.5).collect();
373 let b: Vec<f32> = (0..k * n).map(|i| i as f32 - 3.0).collect();
374 let mut c = vec![0.0f32; m * n];
375 gemm_into(m, k, n, &a, &b, &mut c).expect("gemm should succeed");
376
377 let mut expected = vec![0.0f32; m * n];
378 for (i, row) in expected.chunks_mut(n).enumerate() {
379 for kk in 0..k {
380 for (j, out) in row.iter_mut().enumerate() {
381 *out += a[i * k + kk] * b[kk * n + j];
382 }
383 }
384 }
385 assert_eq!(c, expected);
386 }
387
388 #[test]
389 fn gemm_blocked_matches_reference() {
390 let (m, k, n) = (5usize, 7usize, 6usize);
392 let a: Vec<f64> = (0..m * k).map(|i| (i % 5) as f64 - 2.0).collect();
393 let b: Vec<f64> = (0..k * n).map(|i| (i % 7) as f64 - 3.0).collect();
394 let mut c = vec![0.0f64; m * n];
395 gemm_blocked(m, k, n, &a, &b, &mut c);
396
397 let mut expected = vec![0.0f64; m * n];
398 for (i, row) in expected.chunks_mut(n).enumerate() {
399 for kk in 0..k {
400 for (j, out) in row.iter_mut().enumerate() {
401 *out += a[i * k + kk] * b[kk * n + j];
402 }
403 }
404 }
405 assert_eq!(c, expected);
406 }
407
408 #[test]
409 fn matmul_batched_autograd_is_recorded() {
410 let a = Tensor::from_data(
411 (1..=12).map(|v| v as f32).collect(),
412 vec![2, 2, 3],
413 DeviceType::Cpu,
414 )
415 .expect("tensor creation should succeed")
416 .requires_grad_(true);
417 let b = Tensor::from_data(
418 (1..=12).map(|v| v as f32).collect(),
419 vec![2, 3, 2],
420 DeviceType::Cpu,
421 )
422 .expect("tensor creation should succeed");
423
424 let product = a
425 .matmul(&b)
426 .expect("batched matmul must record the graph, not error out");
427 assert_eq!(product.shape().dims(), &[2, 2, 2]);
428 assert!(
429 product.requires_grad(),
430 "a batched matmul over a requires_grad operand must join the graph"
431 );
432 assert!(
433 matches!(product.operation, crate::core_ops::Operation::MatMul { .. }),
434 "the recorded node must be a MatMul"
435 );
436
437 product.sum().expect("sum").backward().expect("backward");
438 let grad = a.grad().expect("lhs gradient").to_vec().expect("to_vec");
439 assert_eq!(grad.len(), 12);
440 }
441
442 #[test]
446 fn matmul_with_itself_and_with_a_clone_does_not_deadlock() {
447 let a = Tensor::from_data(vec![1.0f32, 2.0, 3.0, 4.0], vec![2, 2], DeviceType::Cpu)
449 .expect("tensor creation should succeed");
450 let self_product = a.matmul(&a).expect("a @ a should succeed");
451 assert_eq!(
452 self_product.to_vec().expect("to_vec"),
453 vec![7.0, 10.0, 15.0, 22.0]
454 );
455
456 let b = a.clone();
457 let clone_product = a.matmul(&b).expect("a @ a.clone() should succeed");
458 assert_eq!(
459 clone_product.to_vec().expect("to_vec"),
460 vec![7.0, 10.0, 15.0, 22.0]
461 );
462 }
463
464 #[test]
465 fn broadcast_batch_dims_rules() {
466 assert_eq!(
467 broadcast_batch_dims(&[2, 1], &[3]).expect("broadcast"),
468 vec![2, 3]
469 );
470 assert_eq!(broadcast_batch_dims(&[], &[4]).expect("broadcast"), vec![4]);
471 assert!(broadcast_batch_dims(&[2], &[3]).is_err());
472 }
473}