metrale_model_layers/layers/ops/dense_gemm_m16_bf16.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launch wrappers for the tensor-core BF16 decode GEMM with a 16-row M tile
4//! (`dense_gemm_m16_bf16`, `dense_gemm_m16_bf16_n64`), the BF16 LM head's arm for 5 to 16
5//! rows.
6//!
7//! Owner: model-layers ops (kernels in `kernels/hopper/common/dense_gemm_m16_bf16.cu`).
8//! Invariants: a launch has `1 <= m <= 16`, `k` a multiple of 64, row pitches that cover
9//! `k` and `n`, and `a_row_stride` a multiple of 8; `launch` returns an error otherwise.
10//!
11//! Why it exists: measured 2026-09-11 on 1xH100 (Qwen3.8-27B-FP8 with a BF16 LM head,
12//! decode batch 16), the batched scalar GEMV `dense_gemv_bf16_batchm` took 3,571 µs for
13//! the head, about 710 GB/s, against 798 µs at batch 1, so the batched GEMV was FP32-FMA
14//! bound. This kernel feeds each 16-row tile to `mma.sync.m16n8k16` instead.
15//!
16//! Numerics: an MMA sums K products in the tensor core's own order, so outputs can
17//! differ from the scalar GEMV. The accepted difference is
18//! [`crate::layers::dense_ffn::m16_tc::within_m16_tc_budget`]: at most 2 ordinal BF16
19//! ULP, or an absolute error under the accumulation floor. The GPU check is the
20//! model-arch example `native_bf16_lm_head_m16_microtest`. Whether the head uses this
21//! arm is the target's `lm_head_m16_tc` default (on in `kernels/hopper/HARDWARE.toml`,
22//! off for gb10, b200 and b300), which `METRALE_LM_HEAD_M16_TC` overrides
23//! (`target_defaults.rs`); the head's route is in `model-engine lm_head_batched.rs`.
24//!
25//! There is no block scale and no dequant: B is BF16 already. Grid `ceil(N / N_TILE)`,
26//! block 128.
27
28use crate::weight_map::DenseWeight;
29use anyhow::{Result, ensure};
30use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
31use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
32
33/// 2026-09-25: N columns one CTA owns in the default instantiation. It must equal the
34/// kernel's `DGM16_N_TILE`.
35pub const DENSE_GEMM_M16_BF16_N_TILE: u32 = 32;
36
37/// 2026-09-25: N tile of the wide instantiation `dense_gemm_m16_bf16_n64` (kernel
38/// `DGM16_N_TILE_WIDE`), selected by `METRALE_LM_HEAD_M16_TC_NTILE=64`. It launches half
39/// the CTAs for a given N, so the A tile is re-read from L2 half as many times.
40pub const DENSE_GEMM_M16_BF16_N_TILE_WIDE: u32 = 64;
41
42/// 2026-09-25: The kernel's M tile. Rows past it are not computed, so the wrapper
43/// returns an error rather than leave part of the output stale.
44pub const DENSE_GEMM_M16_BF16_MAX_M: u32 = 16;
45
46/// 2026-09-25: K granularity: the kernel's 4-stage cp.async pipeline advances 64
47/// elements per step (`DGM16_K_STEP`), which also keeps every weight row 16-byte
48/// aligned.
49pub const DENSE_GEMM_M16_BF16_K_STEP: u32 = 64;
50
51/// 2026-09-25: The signature both instantiations share, so a caller can pick one and
52/// hold a single function pointer.
53pub type DenseM16Bf16Gemm = fn(
54 &dyn GpuBackend,
55 KernelHandle,
56 DevicePtr,
57 &DenseWeight,
58 DevicePtr,
59 u32,
60 u32,
61 u32,
62 u32,
63 u32,
64 u64,
65) -> Result<()>;
66
67/// 2026-09-25: The default 32-wide CTA. `input` is `[m, a_row_stride]` BF16 with `k`
68/// used, `weight.weight` is the `[n, k]` BF16 weight as loaded, and `output` is
69/// `[m, c_row_stride]` BF16 with `n` used.
70#[allow(clippy::too_many_arguments)]
71pub fn dense_gemm_m16_bf16(
72 gpu: &dyn GpuBackend,
73 kernel: KernelHandle,
74 input: DevicePtr,
75 weight: &DenseWeight,
76 output: DevicePtr,
77 m: u32,
78 n: u32,
79 k: u32,
80 a_row_stride: u32,
81 c_row_stride: u32,
82 stream: u64,
83) -> Result<()> {
84 launch(
85 gpu,
86 kernel,
87 DENSE_GEMM_M16_BF16_N_TILE,
88 "dense_gemm_m16_bf16",
89 input,
90 weight,
91 output,
92 m,
93 n,
94 k,
95 a_row_stride,
96 c_row_stride,
97 stream,
98 )
99}
100
101/// 2026-09-25: The `N_TILE = 64` instantiation of the same kernel template: the same
102/// arguments as [`dense_gemm_m16_bf16`], `ceil(n / 64)` CTAs instead of `ceil(n / 32)`.
103#[allow(clippy::too_many_arguments)]
104pub fn dense_gemm_m16_bf16_n64(
105 gpu: &dyn GpuBackend,
106 kernel: KernelHandle,
107 input: DevicePtr,
108 weight: &DenseWeight,
109 output: DevicePtr,
110 m: u32,
111 n: u32,
112 k: u32,
113 a_row_stride: u32,
114 c_row_stride: u32,
115 stream: u64,
116) -> Result<()> {
117 launch(
118 gpu,
119 kernel,
120 DENSE_GEMM_M16_BF16_N_TILE_WIDE,
121 "dense_gemm_m16_bf16_n64",
122 input,
123 weight,
124 output,
125 m,
126 n,
127 k,
128 a_row_stride,
129 c_row_stride,
130 stream,
131 )
132}
133
134/// 2026-09-25: The checks and launch both instantiations share; only `n_tile` differs.
135#[allow(clippy::too_many_arguments)]
136fn launch(
137 gpu: &dyn GpuBackend,
138 kernel: KernelHandle,
139 n_tile: u32,
140 who: &str,
141 input: DevicePtr,
142 weight: &DenseWeight,
143 output: DevicePtr,
144 m: u32,
145 n: u32,
146 k: u32,
147 a_row_stride: u32,
148 c_row_stride: u32,
149 stream: u64,
150) -> Result<()> {
151 ensure!(
152 (1..=DENSE_GEMM_M16_BF16_MAX_M).contains(&m),
153 "{who}: m={m} outside 1..={DENSE_GEMM_M16_BF16_MAX_M} (kernel M tile; \
154 rows past it are never computed, not a launch failure)"
155 );
156 ensure!(
157 k.is_multiple_of(DENSE_GEMM_M16_BF16_K_STEP),
158 "{who}: K={k} not a multiple of {DENSE_GEMM_M16_BF16_K_STEP} \
159 (cp.async pipeline step, and what keeps each [n, k] weight row 16B-aligned)"
160 );
161 ensure!(
162 a_row_stride >= k && c_row_stride >= n,
163 "{who}: row pitches (a={a_row_stride}, c={c_row_stride}) must cover the \
164 used extents (k={k}, n={n})"
165 );
166 ensure!(
167 a_row_stride.is_multiple_of(8),
168 "{who}: a_row_stride={a_row_stride} must keep rows 16B-aligned \
169 (cp.async stages A in 16-byte chunks)"
170 );
171 KernelLaunch::new(gpu, kernel)
172 .grid([div_ceil(n, n_tile), 1, 1])
173 .block([128, 1, 1])
174 .arg_ptr(input)
175 .arg_ptr(weight.weight)
176 .arg_ptr(output)
177 .arg_u32(m)
178 .arg_u32(n)
179 .arg_u32(k)
180 .arg_u32(a_row_stride)
181 .arg_u32(c_row_stride)
182 .launch(stream)
183}
184
185/// 2026-09-25: Host simulation of the kernel's staging, fragment and store index maps,
186/// checked against a reference GEMM.
187#[cfg(test)]
188#[path = "dense_gemm_m16_bf16_tests.rs"]
189mod tests;
190
191/// 2026-09-25: Host simulation of the accumulation floor that `within_m16_tc_budget`
192/// applies, and its margins.
193#[cfg(test)]
194#[path = "dense_gemm_m16_bf16_floor_tests.rs"]
195mod floor_tests;