Skip to main content

rten_gemm/
lib.rs

1//! This crate provides implementations of matrix multiplication for various
2//! architectures, for both floating point and int8 inputs.
3
4use std::cell::RefCell;
5use std::marker::PhantomData;
6use std::mem::MaybeUninit;
7use std::ops::{Add, Mul, Range};
8
9use rayon::prelude::*;
10use rten_base::byte_cast::FromByteArray;
11use rten_base::iter::range_chunks;
12use rten_base::num::Identities;
13use rten_parallel::par_iter::MaybeParIter;
14use rten_tensor::layout::{MutLayout, OverlapPolicy};
15use rten_tensor::prelude::*;
16use rten_tensor::storage::{Alloc, GlobalAlloc};
17use rten_tensor::{
18    AssumeInit, Matrix, MatrixLayout, MatrixMut, NdLayout, NdTensor, NdTensorView, Storage,
19};
20
21mod block_quant;
22mod errors;
23mod i8dot;
24mod im2col;
25mod kernels;
26mod packing;
27mod prepack;
28mod tiles;
29
30pub use block_quant::{BlockQuantizedGemm, BlockQuantizedMatrix, ComputeMode};
31pub use errors::{BlockQuantizedError, GemmError};
32pub use im2col::{ColOffsets, Im2Col, RowOffsets};
33pub use kernels::QuantParams;
34use kernels::generic::GenericKernel;
35use kernels::{Kernel, MatVecOutput};
36use packing::PackingBuffer;
37pub use prepack::{PackedAMatrix, PackedBMatrix};
38use tiles::OutputTiles;
39
40pub type GemmResult<T = ()> = Result<T, GemmError>;
41
42/// Left-hand or "A" input for a GEMM operation.
43#[derive(Copy, Clone)]
44pub enum GemmInputA<'a, T> {
45    /// A standard unpacked matrix.
46    Unpacked(Matrix<'a, T>),
47
48    /// A matrix which has been pre-packed by [`GemmExecutor::prepack_a`].
49    Packed(&'a PackedAMatrix<T>),
50}
51
52impl<T> GemmInputA<'_, T> {
53    pub fn rows(&self) -> usize {
54        match self {
55            Self::Unpacked(m) => m.rows(),
56            Self::Packed(pm) => pm.rows(),
57        }
58    }
59
60    pub fn cols(&self) -> usize {
61        match self {
62            Self::Unpacked(m) => m.cols(),
63            Self::Packed(pm) => pm.cols(),
64        }
65    }
66}
67
68/// Trait implemented by GEMM input types.
69pub trait GemmInT: Copy + Default + Send + Sync + Identities + FromByteArray {}
70impl GemmInT for i8 {}
71impl GemmInT for u8 {}
72impl GemmInT for f32 {}
73
74/// Trait implemented by GEMM output types.
75pub trait GemmOutT:
76    Copy
77    + Default
78    + PartialEq
79    + Send
80    + Sync
81    + Mul<Self, Output = Self>
82    + Add<Self, Output = Self>
83    + Identities
84    + FromByteArray
85{
86}
87impl GemmOutT for i32 {}
88impl GemmOutT for f32 {}
89
90/// Right-hand or "B" input for a GEMM operation.
91#[derive(Copy, Clone)]
92pub enum GemmInputB<'a, T> {
93    /// A standard unpacked matrix.
94    Unpacked(Matrix<'a, T>),
95
96    /// A matrix which has been pre-packed by [`GemmExecutor::prepack_b`].
97    Packed(&'a PackedBMatrix<T>),
98
99    /// An image which is transformed into a matrix using an im2col transformation.
100    ///
101    /// Note: When using this type of input with quantized data, the zero point
102    /// is assumed to be the same for every column.
103    Im2Col(&'a Im2Col<'a, T>),
104
105    /// A matrix which is quantized and packed into blocks along the K dimension.
106    ///
107    /// This is used for weight-only quantization where the de-quantized element
108    /// type is T.
109    BlockQuantized(BlockQuantizedMatrix<'a, T>),
110}
111
112impl<T: Copy + Default> GemmInputB<'_, T> {
113    pub fn rows(&self) -> usize {
114        match self {
115            Self::Unpacked(m) => m.rows(),
116            Self::Packed(pm) => pm.rows(),
117            Self::Im2Col(im) => im.rows(),
118            Self::BlockQuantized(m) => m.rows(),
119        }
120    }
121
122    pub fn cols(&self) -> usize {
123        match self {
124            Self::Unpacked(m) => m.cols(),
125            Self::Packed(pm) => pm.cols(),
126            Self::Im2Col(im) => im.cols(),
127            Self::BlockQuantized(m) => m.cols(),
128        }
129    }
130}
131
132/// A bias to add to the output of a matrix multiplication.
133#[derive(Copy, Clone, PartialEq, Debug)]
134pub enum BiasVector<'a, T> {
135    /// Slice of values treated as a column vector. The length must match the
136    /// number of rows in the LHS / A input.
137    Column(&'a [T]),
138
139    /// Slice of values treated as a column vector. The length must match the
140    /// number of columns in the RHS / B input.
141    Row(&'a [T]),
142}
143
144/// Argument for [`GemmExecutor::with_kernel`] specifying which kernel to use.
145#[derive(Clone, Copy, Debug)]
146enum F32KernelType {
147    /// Use the fallback/generic kernel. Always available.
148    #[allow(unused)]
149    Generic,
150
151    /// Use the AVX 2 + FMA kernel. Intel x64 only.
152    #[cfg(target_arch = "x86_64")]
153    Fma,
154
155    /// Use the AVX 512 kernel. Intel x64 only.
156    #[cfg(target_arch = "x86_64")]
157    Avx512,
158
159    /// Use the ARM NEON kernel. ARM 64 only.
160    #[cfg(target_arch = "aarch64")]
161    ArmNeon,
162
163    /// Use the WASM SIMD kernel. WASM only.
164    #[cfg(target_arch = "wasm32")]
165    #[cfg(target_feature = "simd128")]
166    Wasm,
167}
168
169#[derive(Clone, Copy, Debug)]
170enum Int8KernelType {
171    #[allow(unused)]
172    Generic,
173
174    #[cfg(target_arch = "x86_64")]
175    Avx2,
176    #[cfg(target_arch = "x86_64")]
177    Avx512,
178
179    #[cfg(target_arch = "aarch64")]
180    ArmI8mm,
181    #[cfg(target_arch = "aarch64")]
182    ArmDot,
183    #[cfg(target_arch = "aarch64")]
184    ArmNeon,
185
186    #[cfg(target_arch = "wasm32")]
187    #[cfg(target_feature = "simd128")]
188    Wasm,
189}
190
191/// Executes matrix multiplication operations.
192///
193/// ## Prepacking
194///
195/// Prepacking is useful when an input will be reused in multiple GEMM
196/// operations. In this case the work to pack (re-layout) the input for maximum
197/// computational efficiency, which is normally does internally on each call,
198/// can be done just once for the reused input.
199pub struct GemmExecutor<LhsT: GemmInT = f32, RhsT: GemmInT = LhsT, OutT: GemmOutT = LhsT> {
200    kernel: Box<dyn Kernel<LhsT, RhsT, OutT>>,
201}
202
203impl<LhsT: GemmInT, RhsT: GemmInT, OutT: GemmOutT> GemmExecutor<LhsT, RhsT, OutT> {
204    pub fn new() -> Self
205    where
206        Self: Default,
207    {
208        Self::default()
209    }
210
211    /// Return the name of the kernel that this executor is using.
212    #[allow(dead_code)]
213    pub fn kernel_name(&self) -> &str {
214        self.kernel.name()
215    }
216
217    /// Prepack a matrix for use as the left-hand or "A" input.
218    #[allow(unused)]
219    pub fn prepack_a(&self, a: Matrix<LhsT>) -> PackedAMatrix<LhsT> {
220        self.prepack_a_in(GlobalAlloc::new(), a)
221    }
222
223    /// Variant of [`prepack_a`](GemmExecutor::prepack_a) which takes an
224    /// allocator.
225    pub fn prepack_a_in<A: Alloc>(&self, alloc: A, a: Matrix<LhsT>) -> PackedAMatrix<LhsT> {
226        prepack::prepack_a(&*self.kernel, alloc, a)
227    }
228
229    /// Return column count step for building an [`Im2Col`] input.
230    ///
231    /// The number of columns in [`ColOffsets`] must be a multiple of this.
232    pub fn im2col_col_count_step(&self) -> usize {
233        self.kernel.im2col_col_count_step()
234    }
235
236    /// Return row count step for building an [`Im2Col`] input.
237    ///
238    /// The number of rows in [`RowOffsets`] must be a multiple of this.
239    pub fn im2col_row_count_step(&self) -> usize {
240        self.kernel.im2col_row_count_step()
241    }
242
243    /// Prepack a matrix for use as the right-hand or "B" matrix input.
244    #[allow(unused)]
245    pub fn prepack_b(&self, b: Matrix<RhsT>) -> PackedBMatrix<RhsT> {
246        self.prepack_b_in(GlobalAlloc::new(), b)
247    }
248
249    /// Variant of [`prepack_b`](GemmExecutor::prepack_b) which takes an
250    /// allocator.
251    pub fn prepack_b_in<A: Alloc>(&self, alloc: A, b: Matrix<RhsT>) -> PackedBMatrix<RhsT> {
252        prepack::prepack_b(&*self.kernel, alloc, b)
253    }
254
255    /// Perform a General Matrix Multiplication ("gemm").
256    ///
257    /// This computes `output = alpha * (a @ b) + beta * output` where `@` is
258    /// matrix multiplication.
259    ///
260    /// As a special case, when beta is `0.0`, the computation is simplified to
261    /// `output = alpha * (a @ b)`. ie. existing values in `output` are not
262    /// used. This matters if the existing values include infinities or NaNs.
263    pub fn gemm(
264        &self,
265        out_data: &mut [OutT],
266        a: GemmInputA<LhsT>,
267        b: GemmInputB<RhsT>,
268        opts: GemmOptions<LhsT, RhsT, OutT>,
269    ) -> GemmResult {
270        let GemmOptions {
271            alpha,
272            beta,
273            bias,
274            a_quant,
275            b_quant,
276        } = opts;
277
278        gemm_impl(
279            &*self.kernel,
280            // Safety: `gemm_impl` only writes initialized values to `out_data`.
281            unsafe { std::mem::transmute::<&mut [OutT], &mut [MaybeUninit<OutT>]>(out_data) },
282            a,
283            b,
284            alpha,
285            beta,
286            bias,
287            a_quant,
288            b_quant,
289        )
290        .map(|_| ())
291    }
292
293    /// Perform a General Matrix Multiplication ("gemm").
294    ///
295    /// This is the same as [`GemmExecutor::gemm`] but takes an uninitialized
296    /// output slice. The `beta` value is implicitly set to zero.
297    pub fn gemm_uninit<'a>(
298        &self,
299        out_data: &'a mut [MaybeUninit<OutT>],
300        a: GemmInputA<LhsT>,
301        b: GemmInputB<RhsT>,
302        opts: GemmUninitOptions<LhsT, RhsT, OutT>,
303    ) -> GemmResult<&'a mut [OutT]> {
304        let GemmUninitOptions {
305            alpha,
306            bias,
307            a_quant,
308            b_quant,
309        } = opts;
310
311        gemm_impl(
312            &*self.kernel,
313            out_data,
314            a,
315            b,
316            alpha,
317            OutT::zero(),
318            bias,
319            a_quant,
320            b_quant,
321        )
322    }
323
324    /// Perform a batched matrix multiplication.
325    ///
326    /// This performs a series of matrix multiplications between `M x K` sized
327    /// matrices in `a` and `K x N` sized matrices in `b`, writing `M x N`-sized
328    /// results into `out_data`.
329    pub fn batched_gemm_uninit<'a>(
330        &self,
331        out_data: &'a mut [MaybeUninit<OutT>],
332        a: &[GemmInputA<LhsT>],
333        b: &[GemmInputB<RhsT>],
334        opts: GemmUninitOptions<LhsT, RhsT, OutT>,
335    ) -> GemmResult<&'a mut [OutT]> {
336        if a.len() != b.len() {
337            return Err(GemmError::BatchSizeMismatch);
338        }
339
340        let out_mat_stride = match (a, b) {
341            ([a, ..], [b, ..]) => a.rows() * b.cols(),
342            _ => 0,
343        };
344
345        if a.len() * out_mat_stride != out_data.len() {
346            return Err(GemmError::OutputSizeMismatch);
347        }
348
349        match (a, b) {
350            ([], []) => {
351                // Safety: Output is empty and thus already initialized.
352                Ok(unsafe { out_data.assume_init() })
353            }
354            ([a], [b]) => {
355                // Skip parallel iteration for batch size of 1
356                self.gemm_uninit(out_data, *a, *b, opts)
357            }
358            (a, b) => {
359                a.par_iter()
360                    .zip(b)
361                    .zip(out_data.par_chunks_mut(out_mat_stride))
362                    .try_for_each(|((a_mat, b_mat), out_mat)| {
363                        self.gemm_uninit(out_mat, *a_mat, *b_mat, opts.clone())
364                            .map(|_| ())
365                    })?;
366
367                // Safety: All elements in `out_data` have been initialized by calls to
368                // `gemm_uninit`.
369                Ok(unsafe { out_data.assume_init() })
370            }
371        }
372    }
373
374    /// Return true if the GEMM kernel may encounter saturation in a data type
375    /// that is smaller than the output.
376    ///
377    /// In this case the output may be incorrect if the range of the inputs
378    /// is not restricted to account for this.
379    ///
380    /// The main offender is the `vpmaddubsw` instruction used in int8 kernels
381    /// on x64, on systems which don't support VNNI.
382    pub fn may_saturate(&self) -> bool {
383        self.kernel.may_saturate()
384    }
385
386    fn from_kernel<K: Kernel<LhsT, RhsT, OutT> + 'static>() -> Option<Self> {
387        K::new().map(|kernel| GemmExecutor {
388            kernel: Box::new(kernel),
389        })
390    }
391}
392
393#[derive(Clone)]
394pub struct GemmOptions<'a, LhsT, RhsT, OutT: Default> {
395    /// Alpha value for `C = alpha * AB + beta * C`. Defaults to 1.0.
396    pub alpha: f32,
397
398    /// Beta value for `C = alpha * AB + beta * C`. Defaults to zero.
399    pub beta: OutT,
400
401    /// Row or column biases to add to the output.
402    pub bias: Option<BiasVector<'a, OutT>>,
403
404    /// Quantization parameters (scale, zero point) for LHS / A input.
405    pub a_quant: Option<QuantParams<'a, LhsT>>,
406
407    /// Quantization parameters (scale, zero point) for RHS / B input.
408    pub b_quant: Option<QuantParams<'a, RhsT>>,
409}
410
411impl<'a, LhsT, RhsT, OutT: Default> Default for GemmOptions<'a, LhsT, RhsT, OutT> {
412    fn default() -> Self {
413        Self {
414            alpha: 1.0,
415            beta: OutT::default(),
416            bias: None,
417            a_quant: None,
418            b_quant: None,
419        }
420    }
421}
422
423/// Options for [`GemmExecutor::gemm_uninit`] and [`GemmExecutor::batched_gemm_uninit`].
424///
425/// This is identical to [`GemmOptions`] except that it has no `beta` field,
426/// since the output is uninitialized and beta is implicitly zero.
427#[derive(Clone)]
428pub struct GemmUninitOptions<'a, LhsT, RhsT, OutT> {
429    /// Alpha value for `C = alpha * AB`. Defaults to 1.0.
430    pub alpha: f32,
431
432    /// Row or column biases to add to the output.
433    pub bias: Option<BiasVector<'a, OutT>>,
434
435    /// Quantization parameters (scale, zero point) for LHS / A input.
436    pub a_quant: Option<QuantParams<'a, LhsT>>,
437
438    /// Quantization parameters (scale, zero point) for RHS / B input.
439    pub b_quant: Option<QuantParams<'a, RhsT>>,
440}
441
442impl<'a, LhsT, RhsT, OutT> Default for GemmUninitOptions<'a, LhsT, RhsT, OutT> {
443    fn default() -> Self {
444        Self {
445            alpha: 1.0,
446            bias: None,
447            a_quant: None,
448            b_quant: None,
449        }
450    }
451}
452
453/// Try to construct a [`GemmExecutor`] with a given kernel type.
454macro_rules! try_kernel {
455    ($hint:expr) => {
456        if let Some(gemm) = Self::with_kernel($hint) {
457            return gemm;
458        }
459    };
460}
461
462/// Trait for instantiating a [`GemmExecutor`] with a particular kernel.
463///
464/// This primarily exists to support creating tests which are run against
465/// all available kernels.
466trait WithKernel: Sized {
467    /// Enum specifying kernel to use.
468    type KernelType;
469
470    /// Try to instantiate this executor with a given kernel. Returns None if
471    /// the kernel is not supported on this system.
472    fn with_kernel(kern_type: Self::KernelType) -> Option<Self>;
473
474    /// Instantiate this executor with the generic/fallback kernel.
475    fn with_generic_kernel() -> Self;
476
477    /// Return all the kernel types supported on the current system.
478    #[allow(unused)]
479    fn kernel_types() -> Vec<Self::KernelType>;
480}
481
482impl WithKernel for GemmExecutor<f32, f32, f32> {
483    type KernelType = F32KernelType;
484
485    /// Create a [`GemmExecutor`] using the given kernel. Returns `None` if the
486    /// kernel is not supported.
487    #[allow(dead_code)] // Currently only used in tests
488    fn with_kernel(hint: F32KernelType) -> Option<Self> {
489        match hint {
490            #[cfg(target_arch = "x86_64")]
491            F32KernelType::Avx512 => Self::from_kernel::<kernels::x86_64::Avx512Kernel>(),
492            #[cfg(target_arch = "x86_64")]
493            F32KernelType::Fma => Self::from_kernel::<kernels::x86_64::FmaKernel>(),
494            #[cfg(target_arch = "aarch64")]
495            F32KernelType::ArmNeon => Self::from_kernel::<kernels::aarch64::ArmNeonKernel>(),
496            #[cfg(target_arch = "wasm32")]
497            #[cfg(target_feature = "simd128")]
498            F32KernelType::Wasm => Self::from_kernel::<kernels::wasm::WasmKernel>(),
499            F32KernelType::Generic => Some(Self::with_generic_kernel()),
500        }
501    }
502
503    fn kernel_types() -> Vec<F32KernelType> {
504        let mut types = Vec::new();
505
506        #[cfg(target_arch = "x86_64")]
507        {
508            types.push(F32KernelType::Avx512);
509            types.push(F32KernelType::Fma);
510        }
511
512        #[cfg(target_arch = "aarch64")]
513        {
514            types.push(F32KernelType::ArmNeon);
515        }
516
517        #[cfg(target_arch = "wasm32")]
518        #[cfg(target_feature = "simd128")]
519        {
520            types.push(F32KernelType::Wasm);
521        }
522
523        types.push(F32KernelType::Generic);
524
525        types
526    }
527
528    /// Construct a GemmExecutor that uses the generic kernel.
529    fn with_generic_kernel() -> Self {
530        Self::from_kernel::<GenericKernel>().unwrap()
531    }
532}
533
534impl Default for GemmExecutor<f32, f32, f32> {
535    fn default() -> Self {
536        #[cfg(target_arch = "x86_64")]
537        try_kernel!(F32KernelType::Avx512);
538        #[cfg(target_arch = "x86_64")]
539        try_kernel!(F32KernelType::Fma);
540        #[cfg(target_arch = "aarch64")]
541        try_kernel!(F32KernelType::ArmNeon);
542        #[cfg(target_arch = "wasm32")]
543        #[cfg(target_feature = "simd128")]
544        try_kernel!(F32KernelType::Wasm);
545        Self::with_generic_kernel()
546    }
547}
548
549impl WithKernel for GemmExecutor<u8, i8, i32> {
550    type KernelType = Int8KernelType;
551
552    fn with_kernel(hint: Int8KernelType) -> Option<Self> {
553        match hint {
554            #[cfg(target_arch = "x86_64")]
555            Int8KernelType::Avx512 => Self::from_kernel::<kernels::x86_64::Avx512Int8Kernel>(),
556            #[cfg(target_arch = "x86_64")]
557            Int8KernelType::Avx2 => Self::from_kernel::<kernels::x86_64::Avx2Int8Kernel>(),
558            #[cfg(target_arch = "aarch64")]
559            Int8KernelType::ArmNeon => Self::from_kernel::<kernels::aarch64::ArmInt8MlalKernel>(),
560            #[cfg(target_arch = "aarch64")]
561            Int8KernelType::ArmDot => Self::from_kernel::<kernels::aarch64::ArmInt8DotKernel>(),
562            #[cfg(target_arch = "aarch64")]
563            Int8KernelType::ArmI8mm => Self::from_kernel::<kernels::aarch64::ArmInt8MMKernel>(),
564            #[cfg(target_arch = "wasm32")]
565            #[cfg(target_feature = "simd128")]
566            Int8KernelType::Wasm => Self::from_kernel::<kernels::wasm::WasmInt8Kernel>(),
567            Int8KernelType::Generic => Self::from_kernel::<GenericKernel>(),
568        }
569    }
570
571    fn kernel_types() -> Vec<Int8KernelType> {
572        let mut types = Vec::new();
573
574        #[cfg(target_arch = "x86_64")]
575        {
576            types.push(Int8KernelType::Avx512);
577            types.push(Int8KernelType::Avx2);
578        }
579
580        #[cfg(target_arch = "aarch64")]
581        {
582            types.push(Int8KernelType::ArmI8mm);
583            types.push(Int8KernelType::ArmDot);
584            types.push(Int8KernelType::ArmNeon);
585        }
586
587        #[cfg(target_arch = "wasm32")]
588        #[cfg(target_feature = "simd128")]
589        {
590            types.push(Int8KernelType::Wasm);
591        }
592
593        types.push(Int8KernelType::Generic);
594
595        types
596    }
597
598    fn with_generic_kernel() -> Self {
599        Self::from_kernel::<GenericKernel>().unwrap()
600    }
601}
602
603impl Default for GemmExecutor<u8, i8, i32> {
604    fn default() -> Self {
605        #[cfg(target_arch = "x86_64")]
606        {
607            try_kernel!(Int8KernelType::Avx512);
608            try_kernel!(Int8KernelType::Avx2);
609        }
610        #[cfg(target_arch = "aarch64")]
611        {
612            try_kernel!(Int8KernelType::ArmI8mm);
613            try_kernel!(Int8KernelType::ArmDot);
614            try_kernel!(Int8KernelType::ArmNeon);
615        }
616        #[cfg(target_arch = "wasm32")]
617        #[cfg(target_feature = "simd128")]
618        {
619            try_kernel!(Int8KernelType::Wasm);
620        }
621        Self::with_generic_kernel()
622    }
623}
624
625/// Return the block size for the K / depth dimension of a GEMM operation.
626///
627/// This is chosen such that a `depth_block_size * nr` panel of B fits in the L1
628/// cache, and can be reused in the loop over row tiles within each row block.
629/// On AVX2 with f32 GEMM for example, NR=16 so `256 * 16 * 4 = 16KB`.
630fn depth_block_size<RhsT>(a_cols: usize, min_size: Option<usize>) -> usize {
631    let max = 1024 / size_of::<RhsT>();
632    max.min(a_cols).max(min_size.unwrap_or(0))
633}
634
635/// Return the block size for the N / column dimension of a GEMM operation.
636///
637/// The block size is always a multiple of `nr`, as the block is divided into
638/// column tiles of width `nr`.
639///
640/// The value should be chosen such that a block of B of size
641/// `row_block_size * depth_block_size` fits in the L3 cache. This is then
642/// reused for each loop over row blocks within the column panel. In the current
643/// implementation the column block size is also adjusted dynamically to enable
644/// parallelism. In a multi-threaded context, all column blocks that exist
645/// concurrently need to fit in the L3 cache.
646fn col_block_size(b_cols: usize, nr: usize) -> usize {
647    let parallelism = rayon::current_num_threads();
648    let lower_bound = 128.min(b_cols);
649    let unrounded = (b_cols / parallelism).max(lower_bound).min(1024);
650    unrounded.next_multiple_of(nr)
651}
652
653/// Return the block size for the M / row dimension of a GEMM operation.
654///
655/// The block size is always a multiple of `mr`, as the block is divided into
656/// row tiles of height `mr`.
657///
658/// The value should be chosen such that a panel of A of size `row_block_size *
659/// depth_block_size` fits in the L2 cache. This is then reused for each
660/// column tile that is visited within a block.
661fn row_block_size(a_rows: usize, mr: usize) -> usize {
662    64.min(a_rows).next_multiple_of(mr)
663}
664
665/// Compute a vector-matrix product.
666///
667/// This operation is called "gemv" in BLAS APIs.
668fn gemv<'a, LhsT: GemmInT, RhsT: GemmInT, OutT: GemmOutT>(
669    kernel: &dyn Kernel<LhsT, RhsT, OutT>,
670    a: NdTensorView<LhsT, 1>,
671    b: Matrix<RhsT>,
672    out_data: &'a mut [MaybeUninit<OutT>],
673    alpha: f32,
674    beta: OutT,
675    bias: Option<BiasVector<OutT>>,
676    a_quant: Option<QuantParams<LhsT>>,
677    b_quant: Option<QuantParams<RhsT>>,
678) -> &'a mut [OutT] {
679    let a_cols = a.size(0);
680    let b_cols = b.cols();
681
682    let a = a.to_contiguous();
683    let a_data = a.data();
684
685    // The matrix is partitioned into column blocks that are processed in
686    // parallel.
687    //
688    // Each column block is partitioned into row blocks for calls to the kernel.
689    // The kernel internally divides the row blocks into column tiles. The
690    // kernel prefers tall row blocks if B has unit row stride, or short row
691    // blocks if it has unit column stride.
692    let b_block_size = b_cols.div_ceil(rayon::current_num_threads()).max(128);
693    let k_block_size = if b.row_stride() == 1 { 512 } else { 8 };
694
695    out_data
696        .par_chunks_mut(b_block_size)
697        .enumerate()
698        .for_each(|(col_block_idx, out_chunk)| {
699            let col_block =
700                (col_block_idx * b_block_size)..((col_block_idx + 1) * b_block_size).min(b_cols);
701            let mut effective_beta = beta;
702
703            let b_quant = b_quant.map(|bq| QuantParams {
704                zero_point: &bq.zero_point[col_block.clone()],
705            });
706
707            for (k_block, a_block) in
708                range_chunks(0..a_cols, k_block_size).zip(a_data.chunks(k_block_size))
709            {
710                let b_block = slice_matrix(b, k_block, col_block.clone());
711
712                let mat_vec_out = if effective_beta == OutT::zero() {
713                    MatVecOutput::from_uninit_slice(out_chunk)
714                } else {
715                    // Safety: Output is initialized if `effective_beta` is non-zero.
716                    MatVecOutput::from_slice(unsafe { out_chunk.assume_init() }, effective_beta)
717                };
718
719                kernel.gemv_kernel(mat_vec_out, a_block, b_block, alpha, a_quant, b_quant);
720
721                // Reset `beta` so that subsequent updates for each column
722                // accumulate into the first update.
723                effective_beta = OutT::one();
724            }
725
726            // Safety: Calls to `gemv_kernel` initialized all output elements.
727            let out_chunk = unsafe { out_chunk.assume_init() };
728            match bias {
729                Some(BiasVector::Column(bias)) => {
730                    let bias = bias[0];
731                    for x in out_chunk {
732                        *x = *x + bias;
733                    }
734                }
735                Some(BiasVector::Row(bias)) => {
736                    let bias_block = &bias[col_block.clone()];
737                    for (x, bias) in out_chunk.iter_mut().zip(bias_block) {
738                        *x = *x + *bias;
739                    }
740                }
741                None => {}
742            }
743        });
744
745    // Safety: All output elements were initialized.
746    unsafe { out_data.assume_init() }
747}
748
749/// Return a 2D slice of a matrix.
750///
751/// This is a specialized version of [`Matrix::slice`] that is less flexible
752/// but more efficient.
753fn slice_matrix<T>(mat: Matrix<T>, rows: Range<usize>, cols: Range<usize>) -> Matrix<T> {
754    let layout = NdLayout::from_shape_and_strides(
755        [rows.len(), cols.len()],
756        [mat.row_stride(), mat.col_stride()],
757        OverlapPolicy::AllowOverlap,
758    )
759    .unwrap();
760    let offset = rows.start * mat.row_stride() + cols.start * mat.col_stride();
761    Matrix::from_storage_and_layout(
762        mat.storage().slice(offset..offset + layout.min_data_len()),
763        layout,
764    )
765}
766
767/// Perform matrix multiplication with a given kernel.
768///
769/// # Implementation notes
770///
771/// The implementation uses the general approach of
772/// [BLIS](https://github.com/flame/blis), and was informed by the
773/// [matrixmultiply crate](https://github.com/bluss/matrixmultiply). See [^1]
774/// for an overview.
775///
776/// The main ideas of the implementation are 1) to minimize the overhead of
777/// transferring data between memory and compute by effectively exploiting the
778/// cache hierarchy and 2) to take advantage of available parallelism. The
779/// operation is split into three levels, each comprised of several nested
780/// loops. See pages 3-5 of [^1] for additional details.
781///
782///  1. The outer level divides the M/N/K dimensions of the problem into blocks,
783///     sized to fit into different cache levels, and packs the corresponding
784///     elements of the input matrices into a layout that is efficient for the
785///     kernel to operate on.
786///  2. The mid level ("macrokernel") divides the M / N dimensions into tiles
787///     that are sized to fit into CPU registers.
788///  3. The innermost level ("microkernel", or just "kernel" in this
789///     implementation) updates a single output tile within the current block.
790///
791/// [^1]: Low, Tze Meng, et al. "Analytical modeling is enough for
792///       high-performance BLIS." ACM Transactions on Mathematical Software (TOMS)
793///       43.2 (2016): 1-18. https://dl.acm.org/doi/pdf/10.1145/2925987
794fn gemm_impl<'a, LhsT: GemmInT, RhsT: GemmInT, OutT: GemmOutT>(
795    kernel: &dyn Kernel<LhsT, RhsT, OutT>,
796    out_data: &'a mut [MaybeUninit<OutT>],
797    a: GemmInputA<LhsT>,
798    b: GemmInputB<RhsT>,
799    alpha: f32,
800    beta: OutT,
801    bias: Option<BiasVector<OutT>>,
802    a_quant: Option<QuantParams<LhsT>>,
803    b_quant: Option<QuantParams<RhsT>>,
804) -> GemmResult<&'a mut [OutT]> {
805    if a.cols() != b.rows() {
806        return Err(GemmError::KSizeMismatch);
807    }
808
809    let bias_ok = match bias {
810        Some(BiasVector::Row(bias)) => bias.len() == b.cols(),
811        Some(BiasVector::Column(bias)) => bias.len() == a.rows(),
812        None => true,
813    };
814    if !bias_ok {
815        return Err(GemmError::WrongBiasSize);
816    }
817
818    if let Some(a_quant) = a_quant
819        && a_quant.zero_point.len() != a.rows()
820    {
821        return Err(GemmError::WrongQuantParamSize);
822    }
823
824    if let Some(b_quant) = b_quant
825        && b_quant.zero_point.len() != b.cols()
826    {
827        return Err(GemmError::WrongQuantParamSize);
828    }
829
830    // Construct a Matrix from the implied dimensions, to validate the slice length.
831    let mut output_mat =
832        MatrixMut::<MaybeUninit<OutT>>::try_from_data([a.rows(), b.cols()], out_data)
833            .map_err(|_| GemmError::OutputSizeMismatch)?;
834
835    // Handle case where output is empty.
836    if a.rows() == 0 || b.cols() == 0 {
837        let empty = NdTensor::zeros(output_mat.shape());
838        return Ok(output_mat.init_from(&empty).into_slice_mut().unwrap());
839    }
840
841    // Handle case where depth is zero. We still need to initialize the output
842    // in this case.
843    if a.cols() == 0 {
844        let mut output_mat = if beta == OutT::zero() {
845            output_mat.fill(MaybeUninit::new(OutT::zero()));
846
847            // Safety: We just initialized the output.
848            unsafe { output_mat.assume_init() }
849        } else {
850            // Safety: If beta is non-zero we assume the caller initialized the output.
851            let mut output_mat = unsafe { output_mat.assume_init() };
852            output_mat.apply(|x| *x * beta);
853            output_mat
854        };
855
856        if let Some(bias) = bias {
857            let bias_mat = match bias {
858                BiasVector::Column(bias) => {
859                    NdTensorView::from_data([a.rows(), 1], bias).broadcast([a.rows(), b.cols()])
860                }
861                BiasVector::Row(bias) => {
862                    NdTensorView::from_data([1, b.cols()], bias).broadcast([a.rows(), b.cols()])
863                }
864            };
865            for r in 0..a.rows() {
866                for c in 0..b.cols() {
867                    let out_el = &mut output_mat[[r, c]];
868                    *out_el = *out_el + bias_mat[[r, c]];
869                }
870            }
871        }
872        return Ok(output_mat.into_slice_mut().unwrap());
873    }
874
875    // Use optimized path for vector-matrix products.
876    if let (1, GemmInputA::Unpacked(a), GemmInputB::Unpacked(b)) = (a.rows(), a, b) {
877        let output = gemv(
878            kernel,
879            a.slice(0),
880            b,
881            output_mat.into_slice_mut().unwrap(),
882            alpha,
883            beta,
884            // nb. We checked above that, if present, the bias length matches
885            // `a.rows()` or `b.cols()` as appropriate.
886            bias,
887            a_quant,
888            b_quant,
889        );
890        return Ok(output);
891    }
892
893    let output_tiles = OutputTiles::new(output_mat.view_mut(), kernel.mr(), kernel.nr());
894
895    // For block-quantized inputs, row ranges must have a start and length that
896    // is a multiple of the block size.
897    let depth_min = match &b {
898        GemmInputB::BlockQuantized(mat) => Some(mat.elements_per_block()),
899        _ => None,
900    };
901
902    // Sizes of blocks that the width (nc), depth (kc) and height (mc)
903    // dimensions are partitioned into in the outer loops. These are chosen so
904    // that blocks can fit in specific cache levels. See
905    // https://dl.acm.org/doi/pdf/10.1145/2925987 for notes on choosing the
906    // values.
907    let nc = col_block_size(b.cols(), kernel.nr());
908    let mc = row_block_size(a.rows(), kernel.mr());
909    let kc = depth_block_size::<RhsT>(a.cols(), depth_min);
910
911    // If using prepacked inputs, make sure they were packed with the same
912    // configuration we are using now.
913    if let GemmInputA::Packed(packed) = &a {
914        packed.validate(kernel, kc)?;
915    }
916    if let GemmInputB::Packed(packed) = &b {
917        packed.validate(kernel, kc)?;
918    }
919
920    let rhs_packer = match b {
921        GemmInputB::BlockQuantized(mat) => {
922            let packer = kernel
923                .pack_block_quant(mat)
924                .ok_or(GemmError::BlockQuantizedInputNotSupported)?;
925            Some(packer)
926        }
927        _ => None,
928    };
929
930    // Buffers for packed blocks of the matrix.
931    thread_local!(static PACKED_A: RefCell<PackingBuffer> = const { RefCell::new(PackingBuffer::new()) });
932    thread_local!(static PACKED_B: RefCell<PackingBuffer> = const { RefCell::new(PackingBuffer::new()) });
933
934    let n_col_blocks = b.cols().div_ceil(nc);
935    let n_row_blocks = a.rows().div_ceil(mc);
936
937    // In a single-threaded context we get better performance by avoiding Rayon
938    // overhead altogether.
939    let parallel = rayon::current_num_threads() > 1;
940
941    let (mr, nr) = (kernel.mr(), kernel.nr());
942
943    // Loop over column blocks.
944    (0..n_col_blocks)
945        .maybe_par_iter(parallel)
946        .for_each(|col_idx| {
947            let col_start = col_idx * nc;
948            let col_end = (col_start + nc).min(b.cols());
949            let col_range = col_start..col_end;
950
951            // Loop over depth blocks. This is not parallelized because output
952            // tiles are shared across iterations.
953            for (depth_block_idx, depth_range) in range_chunks(0..a.cols(), kc).enumerate() {
954                // Borrowed packing buffer for current thread. Returned after
955                // the GEMM block is computed.
956                let mut thread_local_packed_b: Option<PackingBuffer> = None;
957
958                let rhs_block = match b {
959                    GemmInputB::Unpacked(_)
960                    | GemmInputB::Im2Col(_)
961                    | GemmInputB::BlockQuantized(_) => PACKED_B.with(|cell| {
962                        let mut packed_b = cell.take();
963
964                        let layout =
965                            kernel.packed_b_layout(depth_range.len(), col_end - col_start, b_quant);
966                        let packed_uninit = packed_b.alloc(layout.size(), layout.align());
967
968                        match b {
969                            GemmInputB::Unpacked(b) => kernel.pack_b_block(
970                                packed_uninit,
971                                b,
972                                depth_range.clone(),
973                                col_start..col_end,
974                                b_quant,
975                            ),
976                            GemmInputB::Im2Col(im) => kernel.pack_im2col(
977                                packed_uninit,
978                                im,
979                                depth_range.clone(),
980                                col_start..col_end,
981                                // Zero point is expected to be the same for every column for
982                                // im2col. This should really be captured in the type of
983                                // `zero_point`.
984                                b_quant.map(|q| q.zero_point[0]),
985                            ),
986                            GemmInputB::BlockQuantized(_) => rhs_packer
987                                .as_deref()
988                                .expect("should have packer")
989                                .pack(packed_uninit, depth_range.clone(), col_start..col_end),
990                            GemmInputB::Packed(_) => unreachable!(),
991                        }
992
993                        // Safety: `pack_b_block` will have initialized `layout.size()` bytes.
994                        unsafe {
995                            packed_b.set_len(layout.size());
996                        }
997                        thread_local_packed_b = Some(packed_b);
998                        RhsBlock {
999                            data: thread_local_packed_b.as_ref().unwrap().as_bytes(),
1000                            panel_stride: layout.panel_stride(),
1001                            _marker: PhantomData,
1002                        }
1003                    }),
1004                    GemmInputB::Packed(pm) => pm.block(col_range.clone(), depth_block_idx),
1005                };
1006
1007                // Only use provided `beta` on the first write to this output
1008                // tile. For subsequent updates accumulate.
1009                let effective_beta = if depth_range.start == 0 {
1010                    beta
1011                } else {
1012                    OutT::one()
1013                };
1014
1015                // Loop over row blocks.
1016                (0..n_row_blocks)
1017                    .maybe_par_iter(parallel)
1018                    .for_each(|row_idx| {
1019                        let row_start = row_idx * mc;
1020                        let row_end = (row_start + mc).min(a.rows());
1021                        let row_range = row_start..row_end;
1022
1023                        // Borrowed packing buffer for current thread. Returned after
1024                        // the GEMM block is computed.
1025                        let mut thread_local_packed_a: Option<PackingBuffer> = None;
1026
1027                        let lhs_block = match a {
1028                            GemmInputA::Unpacked(a) => PACKED_A.with(|cell| {
1029                                let layout = kernel.packed_a_layout(
1030                                    a,
1031                                    row_end - row_start,
1032                                    depth_range.len(),
1033                                    a_quant,
1034                                );
1035                                if !layout.must_pack {
1036                                    return LhsBlock::Unpacked(a);
1037                                };
1038
1039                                let mut packed_a = cell.take();
1040                                let packed_uninit = packed_a.alloc(layout.size(), layout.align());
1041
1042                                kernel.pack_a_block(
1043                                    packed_uninit,
1044                                    a,
1045                                    row_start..row_end,
1046                                    depth_range.clone(),
1047                                    a_quant,
1048                                );
1049
1050                                // Safety: We initialized `layout.size` bytes.
1051                                unsafe {
1052                                    packed_a.set_len(layout.size());
1053                                }
1054                                thread_local_packed_a = Some(packed_a);
1055                                LhsBlock::Packed {
1056                                    data: thread_local_packed_a.as_ref().unwrap().as_bytes(),
1057                                    panel_stride: layout.panel_stride(),
1058                                }
1059                            }),
1060                            GemmInputA::Packed(pm) => pm.block(row_range.clone(), depth_block_idx),
1061                        };
1062
1063                        gemm_block(
1064                            kernel,
1065                            &output_tiles,
1066                            col_start / nr..col_end.div_ceil(nr),
1067                            row_start / mr..row_end.div_ceil(mr),
1068                            depth_range.clone(),
1069                            lhs_block,
1070                            rhs_block,
1071                            alpha,
1072                            effective_beta,
1073                            bias,
1074                            a_quant,
1075                            b_quant,
1076                        );
1077
1078                        if let Some(packed_a) = thread_local_packed_a {
1079                            PACKED_A.with(|cell| cell.replace(packed_a));
1080                        }
1081                    });
1082
1083                if let Some(packed_b) = thread_local_packed_b {
1084                    PACKED_B.with(|cell| cell.replace(packed_b));
1085                }
1086            }
1087        });
1088
1089    // Safety: All elements of output matrix have been initialized.
1090    let output = unsafe { output_mat.assume_init() };
1091
1092    Ok(output.into_slice_mut().unwrap())
1093}
1094
1095/// LHS / A input for a call to [`gemm_block`].
1096#[derive(Copy, Clone)]
1097enum LhsBlock<'a, T> {
1098    /// Packed block of A matrix, arranged as a sequence of row panels.
1099    Packed {
1100        data: &'a [u8],
1101
1102        /// Stride between each row panel.
1103        panel_stride: usize,
1104    },
1105
1106    /// Unpacked A matrix. This must have a column stride of 1.
1107    Unpacked(Matrix<'a, T>),
1108}
1109
1110/// LHS / B input for a call to [`gemm_block`]. Currently always packed as
1111/// a sequence of column panels.
1112#[derive(Copy, Clone)]
1113struct RhsBlock<'a, T> {
1114    data: &'a [u8],
1115
1116    /// Stride between each column panel.
1117    panel_stride: usize,
1118
1119    _marker: PhantomData<T>,
1120}
1121
1122/// Process a single block (ie. a slice along each of the M/N/K dimensions) of a
1123/// matrix multiplication.
1124///
1125/// `col_tiles` and `row_tiles` specifies the range of output tiles to update.
1126/// `a` and `b` are the inputs. `depth_range` specifies the range along the K
1127/// dimension.
1128fn gemm_block<LhsT: Sync, RhsT: Sync, OutT: GemmOutT>(
1129    kernel: &dyn Kernel<LhsT, RhsT, OutT>,
1130    output: &OutputTiles<MaybeUninit<OutT>>,
1131    col_tiles: Range<usize>,
1132    row_tiles: Range<usize>,
1133    depth_range: Range<usize>,
1134    a: LhsBlock<LhsT>,
1135    b: RhsBlock<RhsT>,
1136    alpha: f32,
1137    beta: OutT,
1138    bias: Option<BiasVector<OutT>>,
1139    a_quant: Option<QuantParams<LhsT>>,
1140    b_quant: Option<QuantParams<RhsT>>,
1141) {
1142    let (mr, nr) = (kernel.mr(), kernel.nr());
1143
1144    // Sanity check input length here rather than inner loop.
1145    if let LhsBlock::Unpacked(mat) = &a {
1146        assert!(mat.rows().div_ceil(mr) >= row_tiles.end);
1147        assert!(mat.cols() >= depth_range.end);
1148    }
1149
1150    // Loop over column tiles.
1151    col_tiles
1152        .enumerate()
1153        .for_each(|(block_col_tile, col_tile)| {
1154            let b_panel_offset = block_col_tile * b.panel_stride;
1155            let b_panel = &b.data[b_panel_offset..b_panel_offset + b.panel_stride];
1156            let b_quant_tile = b_quant.map(|bq| {
1157                let col_range = col_tile * nr..(col_tile * nr + nr).min(bq.zero_point.len());
1158                QuantParams {
1159                    zero_point: &bq.zero_point[col_range],
1160                }
1161            });
1162
1163            // Loop over row tiles.
1164            for (block_row_tile, row_tile) in row_tiles.clone().enumerate() {
1165                // Safety:
1166                //  - The loops in this function and its caller are set up so that
1167                //    every output tile is processed by one thread at a time.
1168                let out_tile = unsafe { output.tile(row_tile, col_tile) };
1169
1170                let a_quant_tile = a_quant.map(|aq| {
1171                    let row_range = row_tile * mr..(row_tile * mr + mr).min(aq.zero_point.len());
1172                    QuantParams {
1173                        zero_point: &aq.zero_point[row_range],
1174                    }
1175                });
1176
1177                let kernel_lhs = match a {
1178                    LhsBlock::Packed {
1179                        data,
1180                        panel_stride,
1181                        // panel_len,
1182                    } => {
1183                        let a_panel_offset = block_row_tile * panel_stride;
1184                        let a_panel = &data[a_panel_offset..a_panel_offset + panel_stride];
1185                        kernels::Lhs::Packed(a_panel)
1186                    }
1187                    LhsBlock::Unpacked(mat) => {
1188                        let storage = mat.storage();
1189                        let offset =
1190                            row_tile * mr * mat.row_stride() + depth_range.start * mat.col_stride();
1191                        kernels::Lhs::Unpacked {
1192                            // Safety:
1193                            //  - `offset` is a valid storage offset within `mat`
1194                            data: unsafe { storage.as_ptr().add(offset) },
1195                            len: storage.len().saturating_sub(offset),
1196                            row_stride: mat.row_stride(),
1197                        }
1198                    }
1199                };
1200
1201                // Safety:
1202                //  - Kernel is supported on current system
1203                //  - Output tile is initialized if beta is non-zero
1204                unsafe {
1205                    kernel.kernel(
1206                        out_tile.ptr as *mut OutT,
1207                        out_tile.row_stride,
1208                        kernel_lhs,
1209                        b_panel,
1210                        out_tile.used_rows,
1211                        out_tile.used_cols,
1212                        depth_range.len(),
1213                        alpha,
1214                        beta,
1215                        a_quant_tile,
1216                        b_quant_tile,
1217                    );
1218                }
1219
1220                // Add bias vector on first write to an output tile.
1221                if depth_range.start == 0 {
1222                    // After the kernel is called, all elements of the output
1223                    // tile are now initialized.
1224                    let out_ptr = out_tile.ptr as *mut OutT;
1225                    match bias {
1226                        Some(BiasVector::Column(bias)) => {
1227                            for row in 0..out_tile.used_rows {
1228                                for col in 0..out_tile.used_cols {
1229                                    // Safety:
1230                                    //  - Row and column indices are valid for current tile
1231                                    //  - Bias length was checked at start of `gemm_impl`
1232                                    unsafe {
1233                                        let out_el = out_ptr.add(row * out_tile.row_stride + col);
1234                                        *out_el =
1235                                            *out_el + *bias.get_unchecked(row_tile * mr + row);
1236                                    }
1237                                }
1238                            }
1239                        }
1240                        Some(BiasVector::Row(bias)) => {
1241                            for row in 0..out_tile.used_rows {
1242                                for col in 0..out_tile.used_cols {
1243                                    // Safety:
1244                                    //  - Row and column indices are valid for current tile
1245                                    //  - Bias length was checked at start of `gemm_impl`
1246                                    unsafe {
1247                                        let out_el = out_ptr.add(row * out_tile.row_stride + col);
1248                                        *out_el =
1249                                            *out_el + *bias.get_unchecked(col_tile * nr + col);
1250                                    }
1251                                }
1252                            }
1253                        }
1254                        None => {}
1255                    }
1256                }
1257            }
1258        });
1259}
1260
1261mod reduced_range_rng;
1262
1263/// TODO - Move this to the rten-base crate.
1264#[doc(hidden)]
1265pub use reduced_range_rng::ReducedRangeRng;
1266
1267#[cfg(test)]
1268mod tests;