metrale_model_layers/layers/ops/w8a16_gemm_pipelined_m32.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `w8a16_gemm_pipelined_m32`, the 32-row M-tile twin of
4//! [`super::w8a16_gemm_pipelined`], and the rule for when the twin runs.
5//!
6//! At 1..=32 rows the 128-row tile of `w8a16_gemm_pipelined` is mostly
7//! padding. The twin is a 32x32 tile with 128-K steps and caller-supplied A/C
8//! row pitches (kernel header: `kernels/gb10/common/w8a16_gemm_pipelined_m32.cu`),
9//! so one kernel serves the contiguous GDN in_proj_qkvz/out_proj arms
10//! ([`w8a16_gemm_pipelined_by_m`]), the attention o_proj and the strided
11//! multi-seq Q/K/V tier.
12//!
13//! Numerics: it uses the same sub-MMA windows and fold points as
14//! `w8a16_gemm_pipelined`, and the `native_fp8_gdn_proj_m32_microtest` example
15//! checks its output byte for byte against it. Against the scalar GEMV family
16//! it reassociates the K reduction, as the `*_m16` tiers do.
17//!
18//! The kernel has its own module (`w8a16_gemm_pipelined_m32`). Callers resolve
19//! it with `try_target_kernel`, so a target without the module gets a zero
20//! handle and its callers keep to their other kernels.
21//!
22//! Owner: model-layers ops.
23//! Invariants: none beyond the types.
24
25use anyhow::{Result, ensure};
26use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
27use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
28
29/// 2026-09-25: Rows one M tile covers (`PM32_M_TILE`). Above it the kernel
30/// still runs, with `grid.y = ceil(M / 32)` and one weight pass per tile;
31/// [`w8a16_pipelined_prefers_m32`] sends contiguous callers to the 128-tile
32/// kernel past this bound.
33pub const W8A16_M32_TILE_ROWS: u32 = 32;
34
35/// 2026-09-25: N tile (`PM32_N_TILE`), the grid.x granularity.
36const W8A16_M32_TILE_COLS: u32 = 32;
37
38/// 2026-09-27: Rows the skinny body of the same kernel takes (`PM16_MAX_M`),
39/// when `K` is at most [`W8A16_M16_MAX_K`]. It gives each 8-column block the
40/// canonical tile's per-output arithmetic with the 128-K steps' products
41/// computed in parallel by 8 warps and added in step order.
42pub const W8A16_M16_MAX_ROWS: u32 = 16;
43
44/// 2026-09-27: Largest `K` of the skinny body (`PM16_MAX_K`): one 1152-byte
45/// shared slot per 128-K step within 48 KiB.
46pub const W8A16_M16_MAX_K: u32 = 5120;
47
48/// 2026-09-27: Dynamic shared memory of the 32-row tile: `PM32_SMEM_BYTES`
49/// (A and BF16 B stages, the raw B stages and the E4M3 table).
50const W8A16_M32_SMEM_BYTES: u32 = 44_032;
51
52/// 2026-09-27: Dynamic shared memory of a skinny block for `k`:
53/// `PM16_SMEM_BYTES(K)`, the block's 8 weight rows `k + 16` bytes apart.
54pub fn w8a16_m16_smem_bytes(k: u32) -> u32 {
55 8 * (k + 16)
56}
57
58/// 2026-09-25: K granularity: one FP8 scale block per K-step (`PM32_K_STEP ==
59/// PM32_FP8_BLOCK`), which is why `K % 128 == 0` is required rather than
60/// padded.
61pub const W8A16_M32_K_BLOCK: u32 = 128;
62
63/// 2026-09-25: The selection rule between the 128-tile `w8a16_gemm_pipelined`
64/// and its 32-tile twin at a contiguous call site: the twin takes `1..=32`
65/// rows when its handle is nonzero and `K` is a positive multiple of 128.
66/// Above 32 rows the twin would need `grid.y > 1`, streaming the weight once
67/// per 32-row tile. [`w8a16_gemm_pipelined_by_m`] applies it.
68pub fn w8a16_pipelined_prefers_m32(m: u32, k: u32, m32_kernel: KernelHandle) -> bool {
69 (1..=W8A16_M32_TILE_ROWS).contains(&m)
70 && k >= W8A16_M32_K_BLOCK
71 && k.is_multiple_of(W8A16_M32_K_BLOCK)
72 && m32_kernel.0 != 0
73}
74
75/// 2026-09-25: Strided launch of `w8a16_gemm_pipelined_m32`.
76///
77/// `input` `[M, a_row_stride]` BF16 (first `k` of each row read), `weight`
78/// `[N, K]` FP8 E4M3 with `block_scale` `[N/128, K/128]` FP32, `output`
79/// `[M, c_row_stride]` BF16 (first `n` of each row written). Strides in
80/// elements. The A rows feed 16-byte `cp.async` chunks, so `a_row_stride`
81/// must keep them 16 B-aligned (multiple of 8).
82///
83/// Returns an error for `m == 0`, for `k` not a positive multiple of 128, for
84/// pitches shorter than `k` / `n`, and for an `a_row_stride` that is not a
85/// multiple of 8. Same argument order as `w8a16_gemv_batch{4,16}_strided`, so
86/// the multi-seq Q/K/V tier holds it as one more `StridedBatchGemv` arm.
87///
88/// Grid: (ceil(N/32), ceil(M/32), 1) Block: (256, 1, 1); 2026-09-27: at
89/// 1..=16 rows with K <= [`W8A16_M16_MAX_K`], (ceil(N / 8), 1, 1).
90#[allow(clippy::too_many_arguments)]
91pub fn w8a16_gemm_pipelined_m32_strided(
92 gpu: &dyn GpuBackend,
93 kernel: KernelHandle,
94 input: DevicePtr,
95 weight: DevicePtr,
96 block_scale: DevicePtr,
97 output: DevicePtr,
98 m: u32,
99 n: u32,
100 k: u32,
101 a_row_stride: u32,
102 c_row_stride: u32,
103 stream: u64,
104) -> Result<()> {
105 ensure!(
106 m >= 1,
107 "w8a16_gemm_pipelined_m32: m=0 has no rows to compute"
108 );
109 ensure!(
110 k >= W8A16_M32_K_BLOCK && k.is_multiple_of(W8A16_M32_K_BLOCK),
111 "w8a16_gemm_pipelined_m32: K={k} must be a positive multiple of \
112 {W8A16_M32_K_BLOCK} (one FP8 scale block per K-step)"
113 );
114 ensure!(
115 a_row_stride >= k && c_row_stride >= n,
116 "w8a16_gemm_pipelined_m32: row pitches (a={a_row_stride}, c={c_row_stride}) \
117 must cover the used extents (k={k}, n={n})"
118 );
119 ensure!(
120 a_row_stride.is_multiple_of(8),
121 "w8a16_gemm_pipelined_m32: a_row_stride={a_row_stride} must keep rows \
122 16B-aligned (cp.async A tile)"
123 );
124 // 2026-09-28: The row-tile kernel under the canonical tiers (`w8a16_tc_rows.rs`).
125 if super::w8a16_tc_rows_launch(
126 gpu,
127 input,
128 weight,
129 block_scale,
130 output,
131 m,
132 n,
133 k,
134 a_row_stride,
135 c_row_stride,
136 stream,
137 )? {
138 return Ok(());
139 }
140 // 2026-09-27: 1..=16 rows (K <= 5120) run the kernel's skinny body.
141 let (grid, block, smem) = if m <= W8A16_M16_MAX_ROWS && k <= W8A16_M16_MAX_K {
142 ([div_ceil(n, 8), 1, 1], [256, 1, 1], w8a16_m16_smem_bytes(k))
143 } else {
144 (
145 [
146 div_ceil(n, W8A16_M32_TILE_COLS),
147 div_ceil(m, W8A16_M32_TILE_ROWS),
148 1,
149 ],
150 [256, 1, 1],
151 W8A16_M32_SMEM_BYTES,
152 )
153 };
154 KernelLaunch::new(gpu, kernel)
155 .grid(grid)
156 .block(block)
157 .shared_mem(smem)
158 .arg_ptr(input)
159 .arg_ptr(weight)
160 .arg_ptr(block_scale)
161 .arg_ptr(output)
162 .arg_u32(m)
163 .arg_u32(n)
164 .arg_u32(k)
165 .arg_u32(a_row_stride)
166 .arg_u32(c_row_stride)
167 .launch(stream)
168}
169
170/// 2026-09-25: Contiguous `[M, K]` -> `[M, N]` launch of the same kernel: the
171/// pitches are `k` and `n`. Same signature as [`super::w8a16_gemm_pipelined`].
172#[allow(clippy::too_many_arguments)]
173pub fn w8a16_gemm_pipelined_m32(
174 gpu: &dyn GpuBackend,
175 kernel: KernelHandle,
176 input: DevicePtr,
177 weight: DevicePtr,
178 block_scale: DevicePtr,
179 output: DevicePtr,
180 m: u32,
181 n: u32,
182 k: u32,
183 stream: u64,
184) -> Result<()> {
185 w8a16_gemm_pipelined_m32_strided(
186 gpu,
187 kernel,
188 input,
189 weight,
190 block_scale,
191 output,
192 m,
193 n,
194 k,
195 k,
196 n,
197 stream,
198 )
199}
200
201/// 2026-09-26: Rows of the `w8a16_gemm_pipelined_m64` tile.
202pub const W8A16_M64_TILE_ROWS: u32 = 64;
203
204/// 2026-09-26: 33..=64 rows take the 64-row twin `w8a16_gemm_pipelined_m64`
205/// when its handle is nonzero and `K` is a positive multiple of 128: one weight
206/// pass for a whole k = 2 or 3 batched verify at C = 16, where the 32-row twin
207/// would stream the weight per tile and the 128-row tile is three-quarters
208/// empty. The three kernels give every output the same sub-MMAs in the same
209/// order (`native_fp8_gdn_proj_m32_microtest` compares them byte for byte).
210pub fn w8a16_pipelined_prefers_m64(m: u32, k: u32, m64_kernel: KernelHandle) -> bool {
211 (W8A16_M32_TILE_ROWS + 1..=W8A16_M64_TILE_ROWS).contains(&m)
212 && k >= W8A16_M32_K_BLOCK
213 && k.is_multiple_of(W8A16_M32_K_BLOCK)
214 && m64_kernel.0 != 0
215}
216
217/// 2026-09-26: Strided launch of `w8a16_gemm_pipelined_m64`: the arguments
218/// and checks of [`w8a16_gemm_pipelined_m32_strided`], grid
219/// (ceil(N/32), ceil(M/64), 1), block 256. The multi-seq attention Q/K/V and
220/// O arms take it above 32 rows under `ModelLevers::fp8_attn_m32`.
221#[allow(clippy::too_many_arguments)]
222pub fn w8a16_gemm_pipelined_m64_strided(
223 gpu: &dyn GpuBackend,
224 kernel: KernelHandle,
225 input: DevicePtr,
226 weight: DevicePtr,
227 block_scale: DevicePtr,
228 output: DevicePtr,
229 m: u32,
230 n: u32,
231 k: u32,
232 a_row_stride: u32,
233 c_row_stride: u32,
234 stream: u64,
235) -> Result<()> {
236 ensure!(
237 m >= 1 && k >= W8A16_M32_K_BLOCK && k.is_multiple_of(W8A16_M32_K_BLOCK),
238 "w8a16_gemm_pipelined_m64: m={m} must be positive and K={k} a positive multiple \
239 of {W8A16_M32_K_BLOCK}"
240 );
241 ensure!(
242 a_row_stride >= k && c_row_stride >= n && a_row_stride.is_multiple_of(8),
243 "w8a16_gemm_pipelined_m64: row pitches (a={a_row_stride}, c={c_row_stride}) must \
244 cover k={k} / n={n}, and a must keep rows 16B-aligned"
245 );
246 if super::w8a16_tc_rows_launch(
247 gpu,
248 input,
249 weight,
250 block_scale,
251 output,
252 m,
253 n,
254 k,
255 a_row_stride,
256 c_row_stride,
257 stream,
258 )? {
259 return Ok(());
260 }
261 KernelLaunch::new(gpu, kernel)
262 .grid([
263 div_ceil(n, W8A16_M32_TILE_COLS),
264 div_ceil(m, W8A16_M64_TILE_ROWS),
265 1,
266 ])
267 .block([256, 1, 1])
268 .arg_ptr(input)
269 .arg_ptr(weight)
270 .arg_ptr(block_scale)
271 .arg_ptr(output)
272 .arg_u32(m)
273 .arg_u32(n)
274 .arg_u32(k)
275 .arg_u32(a_row_stride)
276 .arg_u32(c_row_stride)
277 .launch(stream)
278}
279
280/// 2026-09-26: Contiguous launch of `w8a16_gemm_pipelined_m64` (pitches `k`
281/// and `n`). Same signature as [`super::w8a16_gemm_pipelined`].
282#[allow(clippy::too_many_arguments)]
283pub fn w8a16_gemm_pipelined_m64(
284 gpu: &dyn GpuBackend,
285 kernel: KernelHandle,
286 input: DevicePtr,
287 weight: DevicePtr,
288 block_scale: DevicePtr,
289 output: DevicePtr,
290 m: u32,
291 n: u32,
292 k: u32,
293 stream: u64,
294) -> Result<()> {
295 w8a16_gemm_pipelined_m64_strided(
296 gpu,
297 kernel,
298 input,
299 weight,
300 block_scale,
301 output,
302 m,
303 n,
304 k,
305 k,
306 n,
307 stream,
308 )
309}
310
311/// 2026-09-25: Contiguous block-scaled W8A16 GEMM, tile chosen by
312/// [`w8a16_pipelined_prefers_m32`]: the 32-tile twin on `m32_kernel`, else
313/// (2026-09-26) [`w8a16_pipelined_prefers_m64`]'s 64-tile twin on `m64_kernel`,
314/// else the 128-tile `w8a16_gemm_pipelined` on `full_kernel`. The GDN
315/// `in_proj_qkvz` / `out_proj` batched-verify arms (`trait_decode_batched.rs`)
316/// call it.
317#[allow(clippy::too_many_arguments)]
318pub fn w8a16_gemm_pipelined_by_m(
319 gpu: &dyn GpuBackend,
320 full_kernel: KernelHandle,
321 m32_kernel: KernelHandle,
322 m64_kernel: KernelHandle,
323 input: DevicePtr,
324 weight: DevicePtr,
325 block_scale: DevicePtr,
326 output: DevicePtr,
327 m: u32,
328 n: u32,
329 k: u32,
330 stream: u64,
331) -> Result<()> {
332 if w8a16_pipelined_prefers_m32(m, k, m32_kernel) {
333 w8a16_gemm_pipelined_m32(
334 gpu,
335 m32_kernel,
336 input,
337 weight,
338 block_scale,
339 output,
340 m,
341 n,
342 k,
343 stream,
344 )
345 } else if w8a16_pipelined_prefers_m64(m, k, m64_kernel) {
346 w8a16_gemm_pipelined_m64(
347 gpu,
348 m64_kernel,
349 input,
350 weight,
351 block_scale,
352 output,
353 m,
354 n,
355 k,
356 stream,
357 )
358 } else {
359 super::w8a16_gemm_pipelined(
360 gpu,
361 full_kernel,
362 input,
363 weight,
364 block_scale,
365 output,
366 m,
367 n,
368 k,
369 stream,
370 )
371 }
372}
373
374#[cfg(test)]
375#[path = "w8a16_gemm_pipelined_m32_tests.rs"]
376mod tests;