Skip to main content

numr/ops/cpu/
matmul.rs

1//! CPU implementation of matrix multiplication operations.
2
3use crate::dtype::DType;
4use crate::error::{Error, Result};
5use crate::ops::{Kernel, MatmulOps};
6use crate::runtime::cpu::{
7    CpuClient, CpuRuntime,
8    helpers::{dispatch_dtype, ensure_contiguous},
9};
10use crate::tensor::Tensor;
11
12/// MatmulOps implementation for CPU runtime.
13impl MatmulOps<CpuRuntime> for CpuClient {
14    fn matmul(&self, a: &Tensor<CpuRuntime>, b: &Tensor<CpuRuntime>) -> Result<Tensor<CpuRuntime>> {
15        use crate::ops::matmul_output_shape;
16
17        // Validate dtypes match
18        if a.dtype() != b.dtype() {
19            return Err(Error::DTypeMismatch {
20                lhs: a.dtype(),
21                rhs: b.dtype(),
22            });
23        }
24
25        let dtype = a.dtype();
26
27        // Compute output shape
28        let out_shape = matmul_output_shape(a.shape(), b.shape()).ok_or(Error::ShapeMismatch {
29            expected: a.shape().to_vec(),
30            got: b.shape().to_vec(),
31        })?;
32
33        // Get matrix dimensions (last two dims)
34        let a_shape = a.shape();
35        let b_shape = b.shape();
36        let m = if a_shape.len() >= 2 {
37            a_shape[a_shape.len() - 2]
38        } else {
39            1
40        };
41        let k = a_shape[a_shape.len() - 1];
42        let n = b_shape[b_shape.len() - 1];
43
44        // Calculate batch size from output shape, and per-operand batch sizes for broadcasting
45        let batch_size: usize = out_shape
46            .iter()
47            .take(out_shape.len().saturating_sub(2))
48            .product();
49        let batch_size = batch_size.max(1);
50
51        let a_batch: usize = a_shape
52            .iter()
53            .take(a_shape.len().saturating_sub(2))
54            .product::<usize>()
55            .max(1);
56        let b_batch: usize = b_shape
57            .iter()
58            .take(b_shape.len().saturating_sub(2))
59            .product::<usize>()
60            .max(1);
61
62        // GEMV-BT fast path: detect transposed B and use dot-product kernel
63        // When B has shape [K,N] with strides [1,K], it's a transpose of contiguous [N,K].
64        // For small M (decode), we can dot A rows against B's original [N,K] rows directly,
65        // avoiding the costly contiguous copy (e.g. 500MB for lm_head weights).
66        if m <= 16 && b_shape.len() >= 2 && dtype != DType::I8 {
67            let b_strides = b.strides();
68            let ndim = b_shape.len();
69            let stride_row = b_strides[ndim - 2]; // stride for K dimension
70            let stride_col = b_strides[ndim - 1]; // stride for N dimension
71
72            // Check if B is a simple transpose: shape [K,N], strides [1, K]
73            // meaning the underlying data is contiguous [N,K]
74            if stride_row == 1 && stride_col == k as isize {
75                let a_contig = ensure_contiguous(a)?;
76                let a_ptr = a_contig.ptr();
77                let b_ptr = b.ptr(); // Use original ptr - data is contiguous [N,K]
78
79                // Create output tensor
80                let out = Tensor::<CpuRuntime>::empty(&out_shape, dtype, &self.device);
81                let out_ptr = out.ptr();
82                let ldc = n;
83
84                dispatch_dtype!(dtype, T => {
85                    for batch in 0..batch_size {
86                        let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
87                        let b_offset = if b_batch > 1 { batch * n * k } else { 0 };
88                        let out_offset = batch * m * n;
89
90                        #[cfg(feature = "rayon")]
91                        {
92                            use rayon::prelude::*;
93
94                            // Parallelize over output columns for large N
95                            // Each thread computes a chunk of columns independently
96                            let min_cols_per_thread = 64usize;
97                            let num_threads = rayon::current_num_threads();
98                            let chunk_size = ((n + num_threads - 1) / num_threads).max(min_cols_per_thread);
99
100                            if n > min_cols_per_thread && num_threads > 1 {
101                                // Convert to usize for Send safety - each thread
102                                // accesses disjoint memory regions
103                                let a_send = (a_ptr as usize) + a_offset * std::mem::size_of::<T>();
104                                let b_send = (b_ptr as usize) + b_offset * std::mem::size_of::<T>();
105                                let out_send = (out_ptr as usize) + out_offset * std::mem::size_of::<T>();
106                                let elem_size = std::mem::size_of::<T>();
107
108                                self.install_parallelism(|| {
109                                    (0..n).into_par_iter().step_by(chunk_size).for_each(|col_start| {
110                                        let col_end = (col_start + chunk_size).min(n);
111                                        let chunk_n = col_end - col_start;
112                                        unsafe {
113                                            let a_base = a_send as *const T;
114                                            let b_chunk = (b_send + col_start * k * elem_size) as *const T;
115                                            let out_chunk = (out_send + col_start * elem_size) as *mut T;
116
117                                            crate::runtime::cpu::kernels::gemv_bt_kernel::<T>(
118                                                a_base,
119                                                b_chunk,
120                                                out_chunk,
121                                                m, chunk_n, k, n,
122                                            );
123                                        }
124                                    });
125                                });
126                            } else {
127                                unsafe {
128                                    crate::runtime::cpu::kernels::gemv_bt_kernel::<T>(
129                                        (a_ptr as *const T).add(a_offset),
130                                        (b_ptr as *const T).add(b_offset),
131                                        (out_ptr as *mut T).add(out_offset),
132                                        m, n, k, ldc,
133                                    );
134                                }
135                            }
136                        }
137
138                        #[cfg(not(feature = "rayon"))]
139                        unsafe {
140                            crate::runtime::cpu::kernels::gemv_bt_kernel::<T>(
141                                (a_ptr as *const T).add(a_offset),
142                                (b_ptr as *const T).add(b_offset),
143                                (out_ptr as *mut T).add(out_offset),
144                                m, n, k, ldc,
145                            );
146                        }
147                    }
148                }, "matmul_gemv_bt");
149
150                return Ok(out);
151            }
152        }
153
154        // Require row-major contiguous tensors for SIMD-optimized packing
155        // Non-contiguous tensors (transposed, views) are copied to contiguous layout
156        let a_contig = ensure_contiguous(a)?;
157        let b_contig = ensure_contiguous(b)?;
158
159        let a_ptr = a_contig.ptr();
160        let b_ptr = b_contig.ptr();
161
162        // Leading dimensions for contiguous row-major matrices
163        let lda = k;
164        let ldb = n;
165        let ldc = n;
166
167        // Special case: i8 × i8 → i32 matmul (quantized accumulation)
168        if dtype == DType::I8 {
169            use crate::runtime::cpu::kernels::matmul_i8_to_i32_kernel;
170
171            let out = Tensor::<CpuRuntime>::empty(&out_shape, DType::I32, &self.device);
172            let out_ptr = out.ptr();
173
174            #[cfg(feature = "rayon")]
175            {
176                use rayon::prelude::*;
177
178                if batch_size > 1 {
179                    let min_len = self.rayon_min_len();
180                    self.install_parallelism(|| {
181                        (0..batch_size)
182                            .into_par_iter()
183                            .with_min_len(min_len)
184                            .for_each(|batch| unsafe {
185                                let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
186                                let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
187                                let out_offset = batch * m * n;
188
189                                matmul_i8_to_i32_kernel(
190                                    (a_ptr as *const i8).add(a_offset),
191                                    (b_ptr as *const i8).add(b_offset),
192                                    (out_ptr as *mut i32).add(out_offset),
193                                    m,
194                                    n,
195                                    k,
196                                    lda,
197                                    ldb,
198                                    ldc,
199                                );
200                            });
201                    });
202                } else {
203                    unsafe {
204                        matmul_i8_to_i32_kernel(
205                            a_ptr as *const i8,
206                            b_ptr as *const i8,
207                            out_ptr as *mut i32,
208                            m,
209                            n,
210                            k,
211                            lda,
212                            ldb,
213                            ldc,
214                        );
215                    }
216                }
217            }
218
219            #[cfg(not(feature = "rayon"))]
220            unsafe {
221                for batch in 0..batch_size {
222                    let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
223                    let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
224                    let out_offset = batch * m * n;
225
226                    matmul_i8_to_i32_kernel(
227                        (a_ptr as *const i8).add(a_offset),
228                        (b_ptr as *const i8).add(b_offset),
229                        (out_ptr as *mut i32).add(out_offset),
230                        m,
231                        n,
232                        k,
233                        lda,
234                        ldb,
235                        ldc,
236                    );
237                }
238            }
239
240            return Ok(out);
241        }
242
243        // Create output tensor
244        let out = Tensor::<CpuRuntime>::empty(&out_shape, dtype, &self.device);
245        let out_ptr = out.ptr();
246
247        // Dispatch based on dtype
248        dispatch_dtype!(dtype, T => {
249            #[cfg(feature = "rayon")]
250            {
251                use rayon::prelude::*;
252
253                if batch_size > 1 {
254                    let min_len = self.rayon_min_len();
255                    self.install_parallelism(|| {
256                        (0..batch_size)
257                            .into_par_iter()
258                            .with_min_len(min_len)
259                            .for_each(|batch| unsafe {
260                            let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
261                            let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
262                            let out_offset = batch * m * n;
263
264                            <Self as Kernel<CpuRuntime>>::matmul::<T>(
265                                self,
266                                (a_ptr as *const T).add(a_offset),
267                                (b_ptr as *const T).add(b_offset),
268                                (out_ptr as *mut T).add(out_offset),
269                                m,
270                                n,
271                                k,
272                                lda,
273                                ldb,
274                                ldc,
275                            );
276                        });
277                    });
278                } else {
279                    unsafe {
280                        let a_offset = 0;
281                        let b_offset = 0;
282                        let out_offset = 0;
283                        <Self as Kernel<CpuRuntime>>::matmul::<T>(
284                            self,
285                            (a_ptr as *const T).add(a_offset),
286                            (b_ptr as *const T).add(b_offset),
287                            (out_ptr as *mut T).add(out_offset),
288                            m,
289                            n,
290                            k,
291                            lda,
292                            ldb,
293                            ldc,
294                        );
295                    }
296                }
297            }
298
299            #[cfg(not(feature = "rayon"))]
300            unsafe {
301                for batch in 0..batch_size {
302                    let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
303                    let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
304                    let out_offset = batch * m * n;
305
306                    <Self as Kernel<CpuRuntime>>::matmul::<T>(
307                        self,
308                        (a_ptr as *const T).add(a_offset),
309                        (b_ptr as *const T).add(b_offset),
310                        (out_ptr as *mut T).add(out_offset),
311                        m,
312                        n,
313                        k,
314                        lda,
315                        ldb,
316                        ldc,
317                    );
318                }
319            }
320        }, "matmul");
321
322        Ok(out)
323    }
324
325    fn matmul_bias(
326        &self,
327        a: &Tensor<CpuRuntime>,
328        b: &Tensor<CpuRuntime>,
329        bias: &Tensor<CpuRuntime>,
330    ) -> Result<Tensor<CpuRuntime>> {
331        use crate::ops::{matmul_bias_output_shape, validate_matmul_bias_dtypes};
332        use crate::runtime::cpu::kernels::matmul_bias_kernel;
333
334        // Validate dtypes using unified helper (ensures consistent error handling across backends)
335        let dtype = validate_matmul_bias_dtypes(a.dtype(), b.dtype(), bias.dtype())?;
336
337        // Compute output shape (also validates bias shape)
338        let out_shape = matmul_bias_output_shape(a.shape(), b.shape(), bias.shape()).ok_or(
339            Error::ShapeMismatch {
340                expected: a.shape().to_vec(),
341                got: b.shape().to_vec(),
342            },
343        )?;
344
345        // Get matrix dimensions (last two dims)
346        let a_shape = a.shape();
347        let b_shape = b.shape();
348        let m = if a_shape.len() >= 2 {
349            a_shape[a_shape.len() - 2]
350        } else {
351            1
352        };
353        let k = a_shape[a_shape.len() - 1];
354        let n = b_shape[b_shape.len() - 1];
355
356        // Require row-major contiguous tensors for SIMD-optimized packing
357        let a_contig = ensure_contiguous(a)?;
358        let b_contig = ensure_contiguous(b)?;
359        let bias_contig = ensure_contiguous(bias)?;
360
361        // Calculate batch size from output shape, and per-operand batch sizes for broadcasting
362        let batch_size: usize = out_shape
363            .iter()
364            .take(out_shape.len().saturating_sub(2))
365            .product();
366        let batch_size = batch_size.max(1);
367
368        let a_batch: usize = a_shape
369            .iter()
370            .take(a_shape.len().saturating_sub(2))
371            .product::<usize>()
372            .max(1);
373        let b_batch: usize = b_shape
374            .iter()
375            .take(b_shape.len().saturating_sub(2))
376            .product::<usize>()
377            .max(1);
378
379        // Create output tensor
380        let out = Tensor::<CpuRuntime>::empty(&out_shape, dtype, &self.device);
381
382        let a_ptr = a_contig.ptr();
383        let b_ptr = b_contig.ptr();
384        let bias_ptr = bias_contig.ptr();
385        let out_ptr = out.ptr();
386
387        // Leading dimensions for contiguous row-major matrices
388        let lda = k;
389        let ldb = n;
390        let ldc = n;
391
392        // Dispatch based on dtype
393        dispatch_dtype!(dtype, T => {
394            #[cfg(feature = "rayon")]
395            {
396                use rayon::prelude::*;
397
398                if batch_size > 1 {
399                    let min_len = self.rayon_min_len();
400                    self.install_parallelism(|| {
401                        (0..batch_size)
402                            .into_par_iter()
403                            .with_min_len(min_len)
404                            .for_each(|batch| unsafe {
405                            let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
406                            let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
407                            let out_offset = batch * m * n;
408
409                            matmul_bias_kernel::<T>(
410                                (a_ptr as *const T).add(a_offset),
411                                (b_ptr as *const T).add(b_offset),
412                                bias_ptr as *const T, // bias is 1D, same for all batches
413                                (out_ptr as *mut T).add(out_offset),
414                                m,
415                                n,
416                                k,
417                                lda,
418                                ldb,
419                                ldc,
420                            );
421                        });
422                    });
423                } else {
424                    unsafe {
425                        let a_offset = 0;
426                        let b_offset = 0;
427                        let out_offset = 0;
428
429                        matmul_bias_kernel::<T>(
430                            (a_ptr as *const T).add(a_offset),
431                            (b_ptr as *const T).add(b_offset),
432                            bias_ptr as *const T,
433                            (out_ptr as *mut T).add(out_offset),
434                            m,
435                            n,
436                            k,
437                            lda,
438                            ldb,
439                            ldc,
440                        );
441                    }
442                }
443            }
444
445            #[cfg(not(feature = "rayon"))]
446            unsafe {
447                for batch in 0..batch_size {
448                    let a_offset = if a_batch > 1 { batch * m * k } else { 0 };
449                    let b_offset = if b_batch > 1 { batch * k * n } else { 0 };
450                    let out_offset = batch * m * n;
451
452                    matmul_bias_kernel::<T>(
453                        (a_ptr as *const T).add(a_offset),
454                        (b_ptr as *const T).add(b_offset),
455                        bias_ptr as *const T, // bias is 1D, same for all batches
456                        (out_ptr as *mut T).add(out_offset),
457                        m,
458                        n,
459                        k,
460                        lda,
461                        ldb,
462                        ldc,
463                    );
464                }
465            }
466        }, "matmul_bias");
467
468        Ok(out)
469    }
470}