Skip to main content

hermes_simd_core/tiling/
mod.rs

1//! Register-blocked, cache-aware tiling for dot products, GEMV, and GEMM.
2//!
3//! # Motivation
4//!
5//! A standard `dot`/GEMV/GEMM loop has a loop-carried FMA dependency chain that
6//! limits throughput to one FMA per FMA-latency window. Unrolling into `TILE_M`
7//! independent accumulator registers breaks this chain, saturating the FMA issue
8//! ports on modern CPUs:
9//!
10//! - **AVX-512** (32 ZMM regs): larger tiles (e.g. `TILE_M = 8`) fit the register
11//!   file; the GEMM dispatcher selects `<3,4>` for f64.
12//! - **AVX2** (16 YMM regs): the GEMM dispatcher selects the register-resident
13//!   `<3,3>` f64 tile (see [`gemm`] Theorem 3).
14//! - **Scalar**: `TILE_M = TILE_N = 1` is the standard loop; const
15//!   monomorphization eliminates all tiling overhead at zero cost.
16//!
17//! # Structure (separation of concerns)
18//!
19//! This module owns the [`TilingStrategy`] trait surface and the zero-sized
20//! [`TilingPolicy`] tile-shape marker; the per-operation kernels live in vertical
21//! leaf modules, each carrying its own correctness/throughput theorems:
22//!
23//! - [`dot`] — register-blocked dot product (dependency-chain throughput theorem).
24//! - [`gemv`] — matrix–vector product (operand-reuse theorem).
25//! - [`gemm`] — matrix multiplication (correctness, packing-invariance,
26//!   register-residency, and cache cost-model theorems).
27//!
28//! The [`TilingPolicy`] trait methods are thin monomorphizing delegators to those
29//! leaf kernels — one authoritative implementation per operation (SSOT/DRY).
30
31use crate::{
32    align::Alignment,
33    arch::SimdArch,
34    kernel::SimdKernel,
35    scalar::Scalar,
36    view::{SimdError, SimdView},
37};
38use core::marker::PhantomData;
39
40mod dims;
41pub mod dot;
42pub mod gemm;
43pub mod gemv;
44pub mod gemv_transpose;
45
46/// Trait representing a monomorphized register-blocking/tiling strategy.
47///
48/// Implemented as a blanket over [`TilingPolicy<TILE_M, TILE_N>`]. The const
49/// generic parameters encode the tile shape so the compiler emits loop-unrolled,
50/// register-blocked kernels with zero runtime overhead.
51///
52/// # Examples
53///
54/// ```rust
55/// use hermes_simd_core::tiling::{TilingPolicy, TilingStrategy};
56/// use hermes_simd_core::view::SimdView;
57/// use hermes_simd_core::align::Unaligned;
58/// use hermes_simd_intrinsics::Scalar;
59///
60/// let a = [1.0_f32; 4];
61/// let b = [2.0_f32; 4];
62/// let va = SimdView::<f32, Scalar, Unaligned>::new(&a).unwrap();
63/// let vb = SimdView::<f32, Scalar, Unaligned>::new(&b).unwrap();
64/// let dot = <TilingPolicy<1, 1> as TilingStrategy<f32, Scalar, Unaligned>>::dot(&va, &vb)
65///     .expect("lengths equal");
66/// assert!((dot - 8.0_f32).abs() < 1e-6);
67/// ```
68pub trait TilingStrategy<T, Arch: SimdArch, Align: Alignment> {
69    /// The number of rows in the register block.
70    const TILE_M: usize;
71    /// The number of columns (vectors of size `LANE_COUNT`) in the register block.
72    const TILE_N: usize;
73
74    /// Perform tiled matrix multiplication `c += a * b` using this strategy.
75    fn gemm(
76        a: &SimdView<'_, T, Arch, Align>,
77        b: &SimdView<'_, T, Arch, Align>,
78        c: &mut [T],
79        m: usize,
80        n: usize,
81        k: usize,
82    ) -> Result<(), SimdError>;
83
84    /// Perform tiled matrix-vector multiplication `y += A * x` using this strategy.
85    fn gemv(
86        a: &SimdView<'_, T, Arch, Align>,
87        x: &SimdView<'_, T, Arch, Align>,
88        y: &mut [T],
89        nrows: usize,
90        ncols: usize,
91    ) -> Result<(), SimdError>;
92
93    /// Perform tiled transposed matrix-vector multiplication `y += Aᵀ * x`
94    /// (`A` row-major `nrows × ncols`, `x` length `nrows`, `y` length `ncols`).
95    fn gemv_transpose(
96        a: &SimdView<'_, T, Arch, Align>,
97        x: &SimdView<'_, T, Arch, Align>,
98        y: &mut [T],
99        nrows: usize,
100        ncols: usize,
101    ) -> Result<(), SimdError>;
102
103    /// Perform tiled matrix-vector multiplication `y += A * x` over a row-major
104    /// **sub-matrix**: `nrows × ncols` with row stride `lda ≥ ncols`
105    /// (`lda = ncols` is the packed [`Self::gemv`]).
106    fn gemv_strided(
107        a: &SimdView<'_, T, Arch, Align>,
108        x: &SimdView<'_, T, Arch, Align>,
109        y: &mut [T],
110        nrows: usize,
111        ncols: usize,
112        lda: usize,
113    ) -> Result<(), SimdError>;
114
115    /// Perform tiled transposed matrix-vector multiplication `y += Aᵀ * x` over a
116    /// row-major **sub-matrix**: `nrows × ncols` with row stride `lda ≥ ncols`
117    /// (`lda = ncols` is the packed [`Self::gemv_transpose`]).
118    fn gemv_transpose_strided(
119        a: &SimdView<'_, T, Arch, Align>,
120        x: &SimdView<'_, T, Arch, Align>,
121        y: &mut [T],
122        nrows: usize,
123        ncols: usize,
124        lda: usize,
125    ) -> Result<(), SimdError>;
126
127    /// Perform tiled dot product computation using this strategy.
128    fn dot(
129        a: &SimdView<'_, T, Arch, Align>,
130        b: &SimdView<'_, T, Arch, Align>,
131    ) -> Result<T, SimdError>;
132}
133
134/// Compute the dot product of two slices using `TILE_M` independent vector accumulators.
135///
136/// The inner loop processes `TILE_M * LANE_COUNT` elements per iteration, holding
137/// `TILE_M` accumulator registers simultaneously to saturate FMA throughput.
138#[inline(always)]
139pub fn tiled_dot<T, Arch, Align, const TILE_M: usize>(
140    a: &SimdView<'_, T, Arch, Align>,
141    b: &SimdView<'_, T, Arch, Align>,
142) -> Result<T, SimdError>
143where
144    Arch: SimdArch + SimdKernel<T>,
145    Align: Alignment,
146    T: Scalar,
147{
148    <TilingPolicy<TILE_M, 1> as TilingStrategy<T, Arch, Align>>::dot(a, b)
149}
150
151/// Zero-sized strategy marker for tiled execution policy.
152///
153/// Encode tile shape in the type system as a ZST so tiling parameters are
154/// resolved at compile time with no runtime storage.
155///
156/// # Examples
157///
158/// Dot product via the `tiled_dot` free function (preferred API):
159///
160/// ```rust
161/// use hermes_simd_core::tiling::tiled_dot;
162/// use hermes_simd_core::view::SimdView;
163/// use hermes_simd_core::align::Unaligned;
164/// use hermes_simd_intrinsics::Scalar;
165///
166/// let a = [1.0_f32, 2.0, 3.0, 4.0];
167/// let b = [1.0_f32, 1.0, 1.0, 1.0];
168/// let va = SimdView::<f32, Scalar, Unaligned>::new(&a).unwrap();
169/// let vb = SimdView::<f32, Scalar, Unaligned>::new(&b).unwrap();
170/// // TILE_M = 4 unrolls into 4 independent FMA accumulators.
171/// let result = tiled_dot::<f32, Scalar, Unaligned, 4>(&va, &vb).unwrap();
172/// assert!((result - 10.0_f32).abs() < 1e-6);
173/// ```
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct TilingPolicy<const TILE_M: usize, const TILE_N: usize>;
176
177impl<const TILE_M: usize, const TILE_N: usize> TilingPolicy<TILE_M, TILE_N> {
178    /// Standard tile for AVX2 (4 accumulators x 8 f32 lanes = 256 bits).
179    pub const AVX2_STANDARD: TilingPolicy<4, 4> = TilingPolicy;
180    /// Optimal tile for AVX-512 (8 accumulators x 16 f32 lanes = 1024 bits).
181    pub const AVX512_OPTIMAL: TilingPolicy<8, 4> = TilingPolicy;
182    /// Scalar degenerate tile: `TILE_M = 1`, `TILE_N = 1` — no tiling overhead.
183    ///
184    /// Named `SCALAR_DEGENERATE` (SCREAMING_SNAKE_CASE) to satisfy Rust naming lints.
185    pub const SCALAR_DEGENERATE: TilingPolicy<1, 1> = TilingPolicy;
186
187    /// Verify that the tile shape is valid at compile time.
188    pub const fn validate(&self) {
189        assert!(TILE_M >= 1, "TILE_M must be >= 1");
190        assert!(TILE_N >= 1, "TILE_N must be >= 1");
191    }
192}
193
194/// Zero-sized type used to force `TilingPolicy` into a generic bound without runtime cost.
195pub struct _TileMarker<const M: usize, const N: usize>(PhantomData<TilingPolicy<M, N>>);
196
197/// Compute a register-blocked tiled GEMV: `y += A * x`.
198///
199/// Processes `TILE_M` rows of `A` simultaneously to reuse loaded elements of `x` across those rows.
200/// Accepts `SimdView`-typed operands to enforce alignment typestates at the call boundary.
201#[inline(always)]
202pub fn tiled_gemv<T, Arch, Align, const TILE_M: usize>(
203    a: &SimdView<'_, T, Arch, Align>,
204    x: &SimdView<'_, T, Arch, Align>,
205    y: &mut [T],
206    nrows: usize,
207    ncols: usize,
208) -> Result<(), SimdError>
209where
210    Arch: SimdArch + SimdKernel<T>,
211    Align: Alignment,
212    T: Scalar,
213{
214    <TilingPolicy<TILE_M, 1> as TilingStrategy<T, Arch, Align>>::gemv(a, x, y, nrows, ncols)
215}
216
217/// Compute a register-blocked tiled GEMM: `c += A * B`.
218///
219/// Multiplies matrix `a` (dimensions `m * k`) and matrix `b` (dimensions `k * n`),
220/// accumulating the result into `c` (dimensions `m * n`).
221#[inline(always)]
222pub fn tiled_gemm<T, Arch, Align, const TILE_M: usize, const TILE_N: usize>(
223    a: &SimdView<'_, T, Arch, Align>,
224    b: &SimdView<'_, T, Arch, Align>,
225    c: &mut [T],
226    m: usize,
227    n: usize,
228    k: usize,
229) -> Result<(), SimdError>
230where
231    Arch: SimdArch + SimdKernel<T>,
232    Align: Alignment,
233    T: Scalar,
234{
235    <TilingPolicy<TILE_M, TILE_N> as TilingStrategy<T, Arch, Align>>::gemm(a, b, c, m, n, k)
236}
237
238impl<T, Arch, Align, const TILE_M: usize, const TILE_N: usize> TilingStrategy<T, Arch, Align>
239    for TilingPolicy<TILE_M, TILE_N>
240where
241    Arch: SimdArch + SimdKernel<T>,
242    Align: Alignment,
243    T: Scalar,
244{
245    const TILE_M: usize = TILE_M;
246    const TILE_N: usize = TILE_N;
247
248    #[inline]
249    fn gemm(
250        a: &SimdView<'_, T, Arch, Align>,
251        b: &SimdView<'_, T, Arch, Align>,
252        c: &mut [T],
253        m: usize,
254        n: usize,
255        k: usize,
256    ) -> Result<(), SimdError> {
257        gemm::gemm_impl::<T, Arch, Align, TILE_M, TILE_N>(a, b, c, m, n, k)
258    }
259
260    #[inline]
261    fn gemv(
262        a: &SimdView<'_, T, Arch, Align>,
263        x: &SimdView<'_, T, Arch, Align>,
264        y: &mut [T],
265        nrows: usize,
266        ncols: usize,
267    ) -> Result<(), SimdError> {
268        gemv::gemv_impl::<T, Arch, Align, TILE_M>(a, x, y, nrows, ncols)
269    }
270
271    #[inline]
272    fn gemv_transpose(
273        a: &SimdView<'_, T, Arch, Align>,
274        x: &SimdView<'_, T, Arch, Align>,
275        y: &mut [T],
276        nrows: usize,
277        ncols: usize,
278    ) -> Result<(), SimdError> {
279        // `TILE_N` blocks the output (`y`) lane-chunks for the transpose, mirroring
280        // how `TILE_N` blocks the `B`/`c` columns in GEMM.
281        gemv_transpose::gemv_transpose_impl::<T, Arch, Align, TILE_N>(a, x, y, nrows, ncols)
282    }
283
284    #[inline]
285    fn gemv_strided(
286        a: &SimdView<'_, T, Arch, Align>,
287        x: &SimdView<'_, T, Arch, Align>,
288        y: &mut [T],
289        nrows: usize,
290        ncols: usize,
291        lda: usize,
292    ) -> Result<(), SimdError> {
293        gemv::gemv_strided_impl::<T, Arch, Align, TILE_M>(a, x, y, nrows, ncols, lda)
294    }
295
296    #[inline]
297    fn gemv_transpose_strided(
298        a: &SimdView<'_, T, Arch, Align>,
299        x: &SimdView<'_, T, Arch, Align>,
300        y: &mut [T],
301        nrows: usize,
302        ncols: usize,
303        lda: usize,
304    ) -> Result<(), SimdError> {
305        gemv_transpose::gemv_transpose_strided_impl::<T, Arch, Align, TILE_N>(
306            a, x, y, nrows, ncols, lda,
307        )
308    }
309
310    #[inline]
311    fn dot(
312        a: &SimdView<'_, T, Arch, Align>,
313        b: &SimdView<'_, T, Arch, Align>,
314    ) -> Result<T, SimdError> {
315        dot::dot_impl::<T, Arch, Align, TILE_M>(a, b)
316    }
317}