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}