metrale_model_layers/layers/ops/
w8a16_gemm_m16.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the tensor-core W8A16 decode GEMM with a 16-row M
4//! tile (`kernels/hopper/common/w8a16_gemm_m16.cu`, module `w8a16_gemm_m16`;
5//! entries `w8a16_gemm_m16`, `w8a16_gemm_m16_n64`, `w8a16_gemm_m16_strided`).
6//!
7//! Where `w8a16_gemv_batch16` runs 16 scalar FFMA per weight byte, this kernel
8//! runs `mma.sync.m16n8k16`, which reduces 16 K-products in the tensor core's
9//! own order. It is therefore not bit-identical to the scalar GEMVs. Its
10//! contract is `within_m16_tc_budget` (`dense_ffn_m16_tc_oracle.rs`): within
11//! `M16_TC_MAX_ULP` BF16 ULP of the reference, or an absolute error under
12//! `m16_tc_acc_floor` for the reduction depth and the row's RMS; the
13//! `native_fp8_ffn_m16_tc_microtest` example applies it. The callers gate it
14//! on the target-declared `ffn_m16_tc` (dense FFN) and `attn_m16_tc`
15//! (attention) levers.
16//!
17//! The 128-K block scale is folded once per block onto an FP32 outer
18//! accumulator, never per element and never into BF16.
19//!
20//! Grid: (ceil(N/N_TILE), 1, 1)  Block: (128, 1, 1).
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: N columns one CTA owns on the default instantiation. The launch
30/// grid and `m16_tc_kernel` (`dense_ffn_m16_tc.rs`) both use it; the kernel's
31/// `M16_N_TILE` must equal it.
32pub const W8A16_GEMM_M16_N_TILE: u32 = 32;
33
34/// 2026-09-25: The wide instantiation's N tile (`w8a16_gemm_m16_n64`, kernel
35/// `M16_N_TILE_WIDE`), selected for the dense FFN by
36/// `METRALE_FFN_M16_TC_NTILE=64`. It halves the CTA count for a given N.
37pub const W8A16_GEMM_M16_N_TILE_WIDE: u32 = 64;
38
39/// 2026-09-25: The shared signature of [`w8a16_gemm_m16`] and
40/// [`w8a16_gemm_m16_n64`], so `m16_tc_kernel` (`dense_ffn_m16_tc.rs`) can
41/// return either as one function pointer.
42pub type ContiguousM16Gemm = fn(
43    &dyn GpuBackend,
44    KernelHandle,
45    DevicePtr,
46    DevicePtr,
47    DevicePtr,
48    DevicePtr,
49    u32,
50    u32,
51    u32,
52    u64,
53) -> Result<()>;
54
55/// 2026-09-25: Contiguous `input` `[m, k]` BF16 and `output` `[m, n]` BF16;
56/// `weight` / `block_scale` are the raw `w8a16_gemv` pointers (`[N, K]` FP8
57/// E4M3 and `[N/128, K/128]` FP32).
58///
59/// Returns an error for `m` outside `1..=16` or `k` not a multiple of 128: the
60/// kernel's M tile is the MMA's 16 rows, and rows past it would be left
61/// unwritten rather than fail. Callers with 17..=32 rows run it twice on
62/// contiguous row halves, as `dense_ffn_m16_tc.rs` does.
63#[allow(clippy::too_many_arguments)]
64pub fn w8a16_gemm_m16(
65    gpu: &dyn GpuBackend,
66    kernel: KernelHandle,
67    input: DevicePtr,
68    weight: DevicePtr,
69    block_scale: DevicePtr,
70    output: DevicePtr,
71    m: u32,
72    n: u32,
73    k: u32,
74    stream: u64,
75) -> Result<()> {
76    ensure!(
77        (1..=16).contains(&m),
78        "w8a16_gemm_m16: m={m} outside 1..=16 (kernel M tile)"
79    );
80    ensure!(
81        k.is_multiple_of(128),
82        "w8a16_gemm_m16: K={k} not a multiple of 128 (block-scale granularity)"
83    );
84    launch_contiguous(
85        gpu,
86        kernel,
87        W8A16_GEMM_M16_N_TILE,
88        input,
89        weight,
90        block_scale,
91        output,
92        m,
93        n,
94        k,
95        stream,
96    )
97}
98
99/// 2026-09-25: The `N_TILE=64` instantiation of [`w8a16_gemm_m16`]: the same
100/// arguments and guards, `ceil(N/64)` CTAs instead of `ceil(N/32)`.
101#[allow(clippy::too_many_arguments)]
102pub fn w8a16_gemm_m16_n64(
103    gpu: &dyn GpuBackend,
104    kernel: KernelHandle,
105    input: DevicePtr,
106    weight: DevicePtr,
107    block_scale: DevicePtr,
108    output: DevicePtr,
109    m: u32,
110    n: u32,
111    k: u32,
112    stream: u64,
113) -> Result<()> {
114    ensure!(
115        (1..=16).contains(&m),
116        "w8a16_gemm_m16_n64: m={m} outside 1..=16 (kernel M tile)"
117    );
118    ensure!(
119        k.is_multiple_of(128),
120        "w8a16_gemm_m16_n64: K={k} not a multiple of 128 (block-scale granularity)"
121    );
122    launch_contiguous(
123        gpu,
124        kernel,
125        W8A16_GEMM_M16_N_TILE_WIDE,
126        input,
127        weight,
128        block_scale,
129        output,
130        m,
131        n,
132        k,
133        stream,
134    )
135}
136
137/// 2026-09-25: The launch both contiguous instantiations share; only the CTA
138/// width differs.
139#[allow(clippy::too_many_arguments)]
140fn launch_contiguous(
141    gpu: &dyn GpuBackend,
142    kernel: KernelHandle,
143    n_tile: u32,
144    input: DevicePtr,
145    weight: DevicePtr,
146    block_scale: DevicePtr,
147    output: DevicePtr,
148    m: u32,
149    n: u32,
150    k: u32,
151    stream: u64,
152) -> Result<()> {
153    KernelLaunch::new(gpu, kernel)
154        .grid([div_ceil(n, n_tile), 1, 1])
155        .block([128, 1, 1])
156        .arg_ptr(input)
157        .arg_ptr(weight)
158        .arg_ptr(block_scale)
159        .arg_ptr(output)
160        .arg_u32(m)
161        .arg_u32(n)
162        .arg_u32(k)
163        .launch(stream)
164}
165
166/// 2026-09-25: Strided [`w8a16_gemm_m16`]: `a_row_stride` / `c_row_stride` are
167/// the A and C row pitches in elements, for callers whose rows are not
168/// contiguous. The multi-seq decode QKV buffer (`qkv_fp8_batch.rs`) is
169/// `[n, per_seq_qkv]` with Q at offset 0, K after Q and V after K in every row,
170/// so one launch per projection writes all `m` rows into their slots. Same
171/// argument order as `w8a16_gemv_batch16_strided`.
172///
173/// `a_row_stride` must keep each activation row 16-byte aligned (a multiple of
174/// 8 BF16): the kernel stages A in 16-byte `cp.async` chunks.
175#[allow(clippy::too_many_arguments)]
176pub fn w8a16_gemm_m16_strided(
177    gpu: &dyn GpuBackend,
178    kernel: KernelHandle,
179    input: DevicePtr,
180    weight: DevicePtr,
181    block_scale: DevicePtr,
182    output: DevicePtr,
183    m: u32,
184    n: u32,
185    k: u32,
186    a_row_stride: u32,
187    c_row_stride: u32,
188    stream: u64,
189) -> Result<()> {
190    ensure!(
191        (1..=16).contains(&m),
192        "w8a16_gemm_m16_strided: m={m} outside 1..=16 (kernel M tile)"
193    );
194    ensure!(
195        k.is_multiple_of(128),
196        "w8a16_gemm_m16_strided: K={k} not a multiple of 128 (block-scale granularity)"
197    );
198    ensure!(
199        a_row_stride >= k && c_row_stride >= n,
200        "w8a16_gemm_m16_strided: row pitches (a={a_row_stride}, c={c_row_stride}) \
201         must cover the used extents (k={k}, n={n})"
202    );
203    ensure!(
204        a_row_stride.is_multiple_of(8),
205        "w8a16_gemm_m16_strided: a_row_stride={a_row_stride} must keep rows \
206         16B-aligned (cp.async stages A in 16-byte chunks)"
207    );
208    KernelLaunch::new(gpu, kernel)
209        .grid([div_ceil(n, W8A16_GEMM_M16_N_TILE), 1, 1])
210        .block([128, 1, 1])
211        .arg_ptr(input)
212        .arg_ptr(weight)
213        .arg_ptr(block_scale)
214        .arg_ptr(output)
215        .arg_u32(m)
216        .arg_u32(n)
217        .arg_u32(k)
218        .arg_u32(a_row_stride)
219        .arg_u32(c_row_stride)
220        .launch(stream)
221}