metrale_model_layers/layers/dense_ffn_m16_tc.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The tensor-core `w8a16_gemm_m16` arm of the FP8 dense-FFN ladder, 5..=32 rows.
4//!
5//! Owner: model-layers (dense FFN).
6//! Invariants:
7//! - `m16_tc_plan` returns a plan only when the lever is on, the kernel handle
8//! is loaded, `k` is a multiple of 128 and `m` is in 5..=32.
9//! - Every launch of a plan covers at most 16 rows: 17..=32 runs as two
10//! launches, the first `m.div_ceil(2)` rows and then the rest.
11//!
12//! In `dense_ffn.rs`'s `w8_gemm!` ladder this arm sits after
13//! `w8a16_gemv_batch4` (m <= 4) and ahead of the `w8a16_gemv_batch16` rungs,
14//! the W8A8 prefill arm and the tile GEMMs. An m16n8k16 MMA sums K in a
15//! different order from the scalar `w8a16_gemv`, so its outputs are graded by
16//! `within_m16_tc_budget` (2 ordinal BF16 ULP, or an absolute error under
17//! `m16_tc_acc_floor`) rather than by bit equality.
18//!
19//! The target declares the arm in `kernels/<hw>/HARDWARE.toml` `[defaults]
20//! ffn_m16_tc`; every target leaves it off, and the hopper row carries the H100
21//! serve receipt. `METRALE_FFN_M16_TC`, else the `METRALE_M16_TC` umbrella,
22//! overrides the declaration: `0`, `false`, `off` or `no` turns it off and any
23//! other value turns it on (`ops::target_defaults::resolve`).
24
25use anyhow::Result;
26use metrale_gpu_runtime::gpu::DevicePtr;
27
28/// 2026-09-25: The tier's numerics contract, shared by the GPU microtest
29/// (`examples/native_fp8_ffn_m16_tc_microtest.rs`) and the host simulation in
30/// `dense_ffn_m16_tc_m32_tests.rs`.
31#[path = "dense_ffn_m16_tc_oracle.rs"]
32pub mod oracle;
33
34pub use oracle::{
35 M16_TC_ACC_FLOOR_MARGIN, M16_TC_MAX_ULP, bf16_ord, m16_tc_acc_floor, within_m16_tc_budget,
36};
37
38use super::DenseFfnLayer;
39use crate::layer::ForwardContext;
40use crate::layers::ops;
41use crate::weight_map::Fp8Weight;
42use metrale_gpu_runtime::gpu::KernelHandle;
43
44/// 2026-09-25: The resolved `w8a16_gemm_m16` levers: which projection families
45/// take the tier, and the FFN arm's CTA N width. `DenseFfnLayer::new_with_activation`
46/// and the attention layer's init copy them into fields at construction.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct M16TcLevers {
49 /// 2026-09-25: The dense-FFN arm (`TargetLevers::ffn_m16_tc`).
50 pub ffn: bool,
51 /// 2026-09-25: The attention FP8 QKV and o_proj tiers
52 /// (`TargetLevers::attn_m16_tc`).
53 pub attn: bool,
54 /// 2026-09-25: CTA N width for the FFN arm: `ops::W8A16_GEMM_M16_N_TILE` (32),
55 /// or `ops::W8A16_GEMM_M16_N_TILE_WIDE` (64) when
56 /// `METRALE_FFN_M16_TC_NTILE=64`. The attention tiers always launch with 32.
57 pub ffn_n_tile: u32,
58}
59
60/// 2026-09-25: The pure resolver. `ffn` and `attn` are the resolved target
61/// levers; `n_tile` is the raw `METRALE_FFN_M16_TC_NTILE` value. Only `"64"`
62/// selects the wide tile. Any other value, or none, keeps 32 without failing
63/// the boot, and the route log prints the tile that ran.
64pub(crate) fn resolve_m16_tc_levers(ffn: bool, attn: bool, n_tile: Option<&str>) -> M16TcLevers {
65 M16TcLevers {
66 ffn,
67 attn,
68 ffn_n_tile: match n_tile {
69 Some("64") => ops::W8A16_GEMM_M16_N_TILE_WIDE,
70 _ => ops::W8A16_GEMM_M16_N_TILE,
71 },
72 }
73}
74
75/// 2026-09-25: The resolved levers for this process, computed once
76/// (`OnceLock`).
77pub fn m16_tc_levers() -> M16TcLevers {
78 static ON: std::sync::OnceLock<M16TcLevers> = std::sync::OnceLock::new();
79 *ON.get_or_init(|| {
80 let n_tile = std::env::var("METRALE_FFN_M16_TC_NTILE").ok();
81 resolve_m16_tc_levers(
82 // 2026-09-25: The target's `[defaults]` rows, with `METRALE_FFN_M16_TC`,
83 // `METRALE_ATTN_M16_TC` and the `METRALE_M16_TC` umbrella already
84 // applied by `ops::target_defaults::resolve`. When both a narrow
85 // variable and the umbrella are set, the narrow one wins.
86 ops::target_defaults::resolved().ffn_m16_tc.value,
87 ops::target_defaults::resolved().attn_m16_tc.value,
88 n_tile.as_deref(),
89 )
90 })
91}
92
93/// 2026-09-25: How the tier splits `m` rows into launches; `m16_tc_plan`
94/// returns `None` for a width it does not claim. Same shape as `Batch16Plan`.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub(crate) enum M16TcPlan {
97 /// 2026-09-25: One launch over rows `0..m`, `m <= 16`.
98 Single,
99 /// 2026-09-25: Two launches on contiguous row halves: rows `0..first`, then
100 /// `first..m`. `first` is `ceil(m/2)`, so both halves are <= 16 for every
101 /// m <= 32 and the first half is the wider one (m=17 -> 9 + 8).
102 Halves { first: u32 },
103}
104
105/// 2026-09-25: The selection rule, as a pure function of the row count, the
106/// reduction depth, the handle's presence and the lever.
107///
108/// A `k` that is not a multiple of 128 is declined here, not refused at
109/// launch: the kernel reads one FP32 scale per 128-wide K block
110/// (`kernels/hopper/common/w8a16_gemm_m16.cu`), and `ops::w8a16_gemm_m16`
111/// returns an error for such a `k`.
112pub(crate) fn m16_tc_plan(m: u32, k: u32, loaded: bool, enabled: bool) -> Option<M16TcPlan> {
113 if !enabled || !loaded || !k.is_multiple_of(128) {
114 return None;
115 }
116 match m {
117 5..=16 => Some(M16TcPlan::Single),
118 // 2026-09-25: Both halves must be <= 16, the kernel's M tile. `div_ceil`
119 // puts the odd row in the first half.
120 17..=32 => Some(M16TcPlan::Halves {
121 first: m.div_ceil(2),
122 }),
123 _ => None,
124 }
125}
126
127/// 2026-09-25: Which contiguous instantiation the FFN arm launches: the `n64`
128/// kernel when the wide tile is asked for and its handle is loaded, otherwise
129/// the 32-wide kernel. The fallback does not warn; the route log prints the
130/// tile that ran.
131pub(crate) fn m16_tc_kernel(
132 n_tile: u32,
133 narrow: KernelHandle,
134 wide: KernelHandle,
135) -> (ops::ContiguousM16Gemm, KernelHandle, u32) {
136 if n_tile == ops::W8A16_GEMM_M16_N_TILE_WIDE && wide.0 != 0 {
137 (
138 ops::w8a16_gemm_m16_n64,
139 wide,
140 ops::W8A16_GEMM_M16_N_TILE_WIDE,
141 )
142 } else {
143 (ops::w8a16_gemm_m16, narrow, ops::W8A16_GEMM_M16_N_TILE)
144 }
145}
146
147impl DenseFfnLayer {
148 /// 2026-09-25: The plan for `m` rows at reduction depth `k` on this layer:
149 /// handle presence plus the lever.
150 pub(crate) fn ffn_m16_tc_plan(&self, m: u32, k: u32) -> Option<M16TcPlan> {
151 m16_tc_plan(m, k, self.w8a16_gemm_m16_k.0 != 0, self.m16_tc)
152 }
153
154 /// 2026-09-25: Run one dense-FFN projection through the kernel
155 /// `m16_tc_kernel` picks.
156 ///
157 /// `input` is `[m, k]` and `out` is `[m, n]`, both contiguous BF16, so the
158 /// second launch of a `Halves` plan starts `first` rows into each.
159 /// (`ops::w8a16_gemm_m16_strided` takes row pitches instead; the attention
160 /// QKV tier uses it.)
161 #[allow(clippy::too_many_arguments)]
162 pub(crate) fn w8a16_m16_tc_proj(
163 &self,
164 ctx: &ForwardContext,
165 plan: M16TcPlan,
166 w: &Fp8Weight,
167 input: DevicePtr,
168 out: DevicePtr,
169 m: u32,
170 n: u32,
171 k: u32,
172 stream: u64,
173 ) -> Result<()> {
174 let (gemm, kernel, n_tile) = m16_tc_kernel(
175 self.m16_tc_n_tile,
176 self.w8a16_gemm_m16_k,
177 self.w8a16_gemm_m16_n64_k,
178 );
179 self.log_m16_tc_route(ctx, plan, n_tile);
180 const BF16: usize = 2;
181 let launch = |rows: u32, first: u32| {
182 gemm(
183 ctx.gpu,
184 kernel,
185 input.offset(first as usize * k as usize * BF16),
186 w.weight,
187 w.row_scale,
188 out.offset(first as usize * n as usize * BF16),
189 rows,
190 n,
191 k,
192 stream,
193 )
194 };
195 match plan {
196 M16TcPlan::Single => launch(m, 0),
197 M16TcPlan::Halves { first } => {
198 launch(first, 0)?;
199 launch(m - first, first)
200 }
201 }
202 }
203
204 /// 2026-09-25: Logs the route once per model (`log:ffn_m16_tc_decode`), with
205 /// the tile and the launch split, so a report at 5..=32 rows can tell this
206 /// tier from the batch16 GEMV.
207 fn log_m16_tc_route(&self, ctx: &ForwardContext, plan: M16TcPlan, n_tile: u32) {
208 if ctx.stats.once("log:ffn_m16_tc_decode") {
209 let how = match plan {
210 M16TcPlan::Single => "one launch",
211 M16TcPlan::Halves { .. } => "two launches on contiguous row halves",
212 };
213 let asked = self.m16_tc_n_tile;
214 tracing::info!(
215 "[metrale] dense FFN decode: METRALE_FFN_M16_TC — tensor-core w8a16_gemm_m16 \
216 N_TILE={n_tile} (asked {asked}) ({how}) for 5..=32 rows, ahead of \
217 w8a16_gemv_batch16. One weight pass, m16n8k16 MMA, so outputs are \
218 REASSOCIATED vs the scalar w8a16_gemv (<= 2 BF16 ULP), unlike the batch16 \
219 tier. This lever no longer reaches the attention tiers — that is \
220 METRALE_ATTN_M16_TC, and METRALE_M16_TC is both. Unset it to restore the \
221 bit-exact tier (#927)."
222 );
223 }
224 }
225}
226
227#[cfg(test)]
228#[path = "dense_ffn_m16_tc_lever_tests.rs"]
229mod lever_tests;
230
231#[cfg(test)]
232#[path = "dense_ffn_m16_tc_tests.rs"]
233mod tests;
234
235/// 2026-09-25: Host simulation of the two-halves geometry at M=32, graded by
236/// the oracle's comparison.
237#[cfg(test)]
238#[path = "dense_ffn_m16_tc_m32_tests.rs"]
239mod m32_tests;