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;