metrale_model_layers/layers/ops/target_defaults.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The compiled target's serving defaults, resolved against the
4//! environment: the target's declaration first, a `METRALE_*` override second.
5//!
6//! The declaration is [`metrale_kernels::TARGET_DEFAULTS`], which `build.rs`
7//! bakes from the `[defaults]` table of the compiled target's
8//! `kernels/<hw>/HARDWARE.toml`. A target with no table gets
9//! `build_defaults::baseline` (`crates/kernels/build_defaults.rs`).
10//!
11//! # The override grammar (boolean levers, [`resolve_toggle`])
12//!
13//! | input | meaning |
14//! |---|---|
15//! | variable absent | the target's declaration |
16//! | `0`, `false`, `off`, `no` (any case, trimmed) | off, from the environment |
17//! | any other value, including empty | on, from the environment |
18//!
19//! Two presence-gated kill switches outrank both the declaration and the
20//! positive variable, whatever their value: `METRALE_NO_DECODE_SPLIT_SILU`
21//! (`decode_split_silu`) and `METRALE_NO_ATTN_DECODE_BATCH` (`attn_ncol_gemv`).
22//! The head band ([`resolve_batchm_max`]), the W8A8 row caps
23//! ([`resolve_max_m`]) and the split-K policy have their own rules, documented
24//! on their resolvers.
25//!
26//! # One resolution, one log line
27//!
28//! [`resolved`] is the process-wide resolution. The dispatch sites read it,
29//! and `metrale-server` logs it once as [`summary_line`], which tags values
30//! that came from the environment with ` (env)`. `ModelLevers` resolves
31//! `decode_split_silu` itself, from [`declared`] through the same
32//! [`resolve_toggle`].
33//!
34//! # Adding a lever
35//!
36//! A lever needs all of: the field in `metrale_kernels::TargetDefaults`
37//! (`crates/kernels/src/target_defaults.rs`), the parse arm in
38//! `crates/kernels/build_defaults.rs`, the row in every
39//! `kernels/<hw>/HARDWARE.toml` that has a `[defaults]` table, the
40//! [`TargetLevers`] field and its arm in [`resolve`], its entry in
41//! [`format_levers`], and a test. `parse_defaults` panics on an unknown key,
42//! so a row without its parse arm fails the build.
43//!
44//! Owner: model-layers ops (target serving defaults).
45//! Invariants:
46//! - [`resolved`] keeps its first completed resolution; later calls return
47//! that table.
48//! - A value is tagged [`Source::Env`] exactly when an environment variable
49//! decided it.
50
51use metrale_kernels::attn_splitk::{self, SplitkPolicy};
52
53use super::gemm_quant::{DENSE_GEMV_BATCHM_DECODE_MAX_M, DENSE_GEMV_BATCHM_MAX_M};
54
55/// 2026-09-25: Where a resolved value came from.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Source {
58 /// 2026-09-25: `kernels/<hw>/HARDWARE.toml` `[defaults]`, or the baseline.
59 Target,
60 /// 2026-09-25: A `METRALE_*` variable in the process environment.
61 Env,
62}
63
64impl Source {
65 /// 2026-09-25: The suffix the boot line appends to an environment-sourced value.
66 pub fn tag(self) -> &'static str {
67 match self {
68 Source::Target => "",
69 Source::Env => " (env)",
70 }
71 }
72}
73
74/// 2026-09-25: A resolved lever and where it came from.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Resolved<T> {
77 pub value: T,
78 pub source: Source,
79}
80
81impl<T> Resolved<T> {
82 fn target(value: T) -> Self {
83 Self {
84 value,
85 source: Source::Target,
86 }
87 }
88 fn env(value: T) -> Self {
89 Self {
90 value,
91 source: Source::Env,
92 }
93 }
94 /// 2026-09-25: True when an environment variable decided the value.
95 pub fn from_env(&self) -> bool {
96 self.source == Source::Env
97 }
98}
99
100/// 2026-09-25: The override grammar for a boolean lever, as a pure function.
101///
102/// `raw` is the positive variable's value (`None` = absent). `legacy_off` is
103/// the presence of the matching `METRALE_NO_*` kill switch, which wins over
104/// everything, so a stale positive variable cannot veto it.
105pub fn resolve_toggle(default_on: bool, raw: Option<&str>, legacy_off: bool) -> Resolved<bool> {
106 if legacy_off {
107 return Resolved::env(false);
108 }
109 match raw {
110 None => Resolved::target(default_on),
111 Some(v) => match v.trim().to_ascii_lowercase().as_str() {
112 "0" | "false" | "off" | "no" => Resolved::env(false),
113 _ => Resolved::env(true),
114 },
115 }
116}
117
118/// 2026-09-25: The BF16 decode head's batched-GEMV band: the widest `m` that
119/// takes `dense_gemv_batchm`. Wider batches take a tile GEMM that reassociates
120/// the reduction, so moving the edge changes the head's output bits.
121///
122/// Clamped to [`DENSE_GEMV_BATCHM_MAX_M`], the kernel's row bound:
123/// `dense_gemv_batchm` returns an error above it, which would fail every
124/// decode step. An unparseable or `0` environment value keeps the target's
125/// declaration; the band has no "off".
126pub fn resolve_batchm_max(default_max: u32, raw: Option<&str>) -> Resolved<u32> {
127 let clamp = |v: u32| v.min(DENSE_GEMV_BATCHM_MAX_M);
128 match raw
129 .and_then(|v| v.trim().parse::<u32>().ok())
130 .filter(|&v| v > 0)
131 {
132 Some(v) => Resolved::env(clamp(v)),
133 None => Resolved::target(clamp(default_max)),
134 }
135}
136
137/// 2026-09-25: Upper `M` for the W8A8 dense-FFN prefill, per projection shape.
138///
139/// Unlike [`resolve_batchm_max`], a parsed `0` is honoured: it means "never
140/// take the W8A8 arm on this shape". Anything that is not a u32 keeps the
141/// target's declaration.
142pub fn resolve_max_m(default_max: u32, raw: Option<&str>) -> Resolved<u32> {
143 match raw.and_then(|v| v.trim().parse::<u32>().ok()) {
144 Some(v) => Resolved::env(v),
145 None => Resolved::target(default_max),
146 }
147}
148
149/// 2026-09-25: Every serving lever this target declares, resolved against the
150/// environment.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct TargetLevers {
153 /// 2026-09-25: The `kernels/<hw>` tree this binary was compiled from.
154 pub hw: &'static str,
155 pub lm_head_batchm_max: Resolved<u32>,
156 pub ssm_batched_recurrent: Resolved<bool>,
157 pub gdn_prefill_tc: Resolved<bool>,
158 pub ssm_ba_gates_hopper: Resolved<bool>,
159 pub fp8_act_quant_hopper: Resolved<bool>,
160 pub decode_split_silu: Resolved<bool>,
161 pub attn_decode_splitk: Resolved<SplitkPolicy>,
162 /// 2026-09-25: The `w8a16_gemm_m16` tier on the dense-FFN decode arm.
163 pub ffn_m16_tc: Resolved<bool>,
164 /// 2026-09-25: The `w8a16_gemm_m16` tiers on the attention decode projections.
165 pub attn_m16_tc: Resolved<bool>,
166 /// 2026-09-25: The `dense_gemm_m16_bf16` arm on the BF16 decode head.
167 pub lm_head_m16_tc: Resolved<bool>,
168 /// 2026-09-25: `w8a16_gemv_batch16_ncol{2,4}` on the attention decode
169 /// projections. Every target with a `[defaults]` table declares it off.
170 pub attn_ncol_gemv: Resolved<bool>,
171 pub ffn_gateup_fused: Resolved<bool>,
172 pub w8a8_prefill_max_m_widening: Resolved<u32>,
173 pub w8a8_prefill_max_m_narrowing: Resolved<u32>,
174}
175
176/// 2026-09-25: The whole table, as a pure function of a declaration and a
177/// variable lookup, so tests resolve any target's table without touching the
178/// process environment.
179pub fn resolve(
180 defaults: &metrale_kernels::TargetDefaults,
181 mut var: impl FnMut(&str) -> Option<String>,
182) -> TargetLevers {
183 let split_silu_off = var("METRALE_NO_DECODE_SPLIT_SILU").is_some();
184
185 TargetLevers {
186 hw: defaults.hw,
187 lm_head_batchm_max: resolve_batchm_max(
188 defaults.lm_head_batchm_max,
189 var("METRALE_LM_HEAD_BATCHM_MAX").as_deref(),
190 ),
191 w8a8_prefill_max_m_widening: resolve_max_m(
192 defaults.w8a8_prefill_max_m_widening,
193 var("METRALE_W8A8_PREFILL_MAX_M_WIDENING").as_deref(),
194 ),
195 w8a8_prefill_max_m_narrowing: resolve_max_m(
196 defaults.w8a8_prefill_max_m_narrowing,
197 var("METRALE_W8A8_PREFILL_MAX_M_NARROWING").as_deref(),
198 ),
199 // 2026-09-25: `kernels/hopper` declares it on, the other tables off. A
200 // pinned `--ssm-batched-recurrent` outranks this row (`serve_flags.rs`).
201 ssm_batched_recurrent: resolve_toggle(
202 defaults.ssm_batched_recurrent,
203 var("METRALE_SSM_BATCHED_RECURRENT").as_deref(),
204 false,
205 ),
206 // 2026-09-25: `METRALE_GDN_PREFILL_TC=0` turns the tensor-core GDN
207 // prefill off, and with it the remnant twins that read the same bit.
208 gdn_prefill_tc: resolve_toggle(
209 defaults.gdn_prefill_tc,
210 var("METRALE_GDN_PREFILL_TC").as_deref(),
211 false,
212 ),
213 // 2026-09-25: The BA-gates twin. `kernels/hopper` declares it on; it has
214 // no `METRALE_NO_*` spelling.
215 ssm_ba_gates_hopper: resolve_toggle(
216 defaults.ssm_ba_gates_hopper,
217 var("METRALE_SSM_BA_GATES_HOPPER").as_deref(),
218 false,
219 ),
220 // 2026-09-25: The FP8 activation-quant twin. `kernels/hopper` declares
221 // it on; it has no `METRALE_NO_*` spelling. When on, the twin still
222 // passes a CTA-count floor (`fp8_act_quant_floor.rs`) before it takes a
223 // launch; `METRALE_FP8_ACT_QUANT_HOPPER=0` declines it at every width.
224 fp8_act_quant_hopper: resolve_toggle(
225 defaults.fp8_act_quant_hopper,
226 var("METRALE_FP8_ACT_QUANT_HOPPER").as_deref(),
227 false,
228 ),
229 // 2026-09-25: The declaration and the presence-gated
230 // `METRALE_NO_DECODE_SPLIT_SILU`; there is no positive variable.
231 decode_split_silu: resolve_toggle(defaults.decode_split_silu, None, split_silu_off),
232 // 2026-09-25: The paged-decode split-K policy. The rule is
233 // `metrale_kernels::attn_splitk::resolve_policy`, which the buffer arena
234 // (`gpu-runtime/src/buffers/sizes.rs`, through `policy_from_env`) also
235 // calls to size the split-K workspace, so the split count a launch uses
236 // and the one the workspace was sized for come from one function.
237 attn_decode_splitk: {
238 let (policy, from_env) = attn_splitk::resolve_policy(
239 defaults.attn_decode_splitk,
240 var("METRALE_ATTN_DECODE_SPLITK").as_deref(),
241 );
242 if from_env {
243 Resolved::env(policy)
244 } else {
245 Resolved::target(policy)
246 }
247 },
248 // 2026-09-25: Two rows for one kernel family. `METRALE_M16_TC` is an
249 // umbrella read for both rows, under the same grammar, when the row's
250 // own variable is absent, so `METRALE_M16_TC=0` turns both off. The
251 // row's own variable wins when both are set.
252 ffn_m16_tc: resolve_toggle(
253 defaults.ffn_m16_tc,
254 var("METRALE_FFN_M16_TC")
255 .or_else(|| var("METRALE_M16_TC"))
256 .as_deref(),
257 false,
258 ),
259 attn_m16_tc: resolve_toggle(
260 defaults.attn_m16_tc,
261 var("METRALE_ATTN_M16_TC")
262 .or_else(|| var("METRALE_M16_TC"))
263 .as_deref(),
264 false,
265 ),
266 // 2026-09-25: Not under `METRALE_M16_TC`: only its own variable
267 // overrides the declaration.
268 lm_head_m16_tc: resolve_toggle(
269 defaults.lm_head_m16_tc,
270 var("METRALE_LM_HEAD_M16_TC").as_deref(),
271 false,
272 ),
273 // 2026-09-25: The presence-gated `METRALE_NO_ATTN_DECODE_BATCH` outranks
274 // both the declaration and `METRALE_ATTN_NCOL_GEMV`.
275 attn_ncol_gemv: resolve_toggle(
276 defaults.attn_ncol_gemv,
277 var("METRALE_ATTN_NCOL_GEMV").as_deref(),
278 var("METRALE_NO_ATTN_DECODE_BATCH").is_some(),
279 ),
280 // 2026-09-25: `kernels/hopper` declares it on. `=0` turns the arm off, so
281 // gate and up run as two GEMMs. On a target whose tree lacks
282 // `silu_mul_strided.cu`, turning it on finds no kernel
283 // (`try_target_kernel` returns 0) and the layer declines the arm.
284 ffn_gateup_fused: resolve_toggle(
285 defaults.ffn_gateup_fused,
286 var("METRALE_FFN_GATEUP_FUSED").as_deref(),
287 false,
288 ),
289 }
290}
291
292/// 2026-09-25: The process-wide resolution, computed on the first call and
293/// cached.
294///
295/// Cached because the dispatch sites read it per projection per step, and the
296/// route must stay constant across CUDA-graph replays: a per-call environment
297/// read could change the launch set between capture and replay.
298pub fn resolved() -> &'static TargetLevers {
299 static LEVERS: std::sync::OnceLock<TargetLevers> = std::sync::OnceLock::new();
300 LEVERS.get_or_init(|| {
301 resolve(
302 &metrale_kernels::TARGET_DEFAULTS,
303 metrale_config::levers::var,
304 )
305 })
306}
307
308/// 2026-09-25: The baked declaration this binary carries, for callers that
309/// resolve from their own inputs (`ModelLevers`).
310pub fn declared() -> &'static metrale_kernels::TargetDefaults {
311 &metrale_kernels::TARGET_DEFAULTS
312}
313
314/// 2026-09-25: `target defaults (<hw>): …`, the one line naming every resolved
315/// value and which came from the environment. Built here so the line and the
316/// resolution are the same code.
317pub fn summary_line() -> String {
318 format_levers(resolved())
319}
320
321/// 2026-09-25: [`summary_line`] over a table the caller already has. Pure, so
322/// tests can format any target's line without touching the process
323/// environment or the cached [`resolved`] table.
324pub fn format_levers(l: &TargetLevers) -> String {
325 let onoff =
326 |r: Resolved<bool>| format!("{}{}", if r.value { "on" } else { "off" }, r.source.tag());
327 // 2026-09-25: `u32::MAX` is the no-cap baseline; it prints as `max`.
328 let cap = |v: u32| {
329 if v == u32::MAX {
330 "max".to_string()
331 } else {
332 v.to_string()
333 }
334 };
335 format!(
336 "target defaults ({hw}): sm_count={sms} \
337 lm_head_batchm_max={batchm}{batchm_src} \
338 ssm_batched_recurrent={recurrent} gdn_prefill_tc={gdn_tc} \
339 ssm_ba_gates_hopper={ba_gates} decode_split_silu={silu} \
340 attn_decode_splitk={splitk}{splitk_src} ffn_m16_tc={ffn_m16_tc} \
341 attn_m16_tc={attn_m16_tc} lm_head_m16_tc={lm_head_m16_tc} \
342 attn_ncol_gemv={attn_ncol_gemv} ffn_gateup_fused={gateup} \
343 fp8_act_quant_hopper={act_quant} \
344 w8a8_prefill_max_m={w8a8_wide}/{w8a8_narrow}{w8a8_src}",
345 hw = if l.hw.is_empty() { "unknown" } else { l.hw },
346 // 2026-09-25: Not a lever: the target's `[hardware] sm_count`, which
347 // `arch_preflight::check_sm_count` compares with the device at boot.
348 sms = metrale_kernels::TARGET_SM_COUNT,
349 batchm = l.lm_head_batchm_max.value,
350 batchm_src = l.lm_head_batchm_max.source.tag(),
351 recurrent = onoff(l.ssm_batched_recurrent),
352 gdn_tc = onoff(l.gdn_prefill_tc),
353 ba_gates = onoff(l.ssm_ba_gates_hopper),
354 act_quant = onoff(l.fp8_act_quant_hopper),
355 silu = onoff(l.decode_split_silu),
356 splitk = l.attn_decode_splitk.value.label(),
357 splitk_src = l.attn_decode_splitk.source.tag(),
358 ffn_m16_tc = onoff(l.ffn_m16_tc),
359 attn_m16_tc = onoff(l.attn_m16_tc),
360 lm_head_m16_tc = onoff(l.lm_head_m16_tc),
361 attn_ncol_gemv = onoff(l.attn_ncol_gemv),
362 gateup = onoff(l.ffn_gateup_fused),
363 // 2026-09-25: Printed as widening/narrowing, with the widening row's
364 // source tag.
365 w8a8_wide = cap(l.w8a8_prefill_max_m_widening.value),
366 w8a8_narrow = cap(l.w8a8_prefill_max_m_narrowing.value),
367 w8a8_src = l.w8a8_prefill_max_m_widening.source.tag(),
368 )
369}
370
371/// 2026-09-25: The head band a target without a `[defaults]` table gets.
372/// `build_defaults::baseline` (`crates/kernels/build_defaults.rs`) repeats it
373/// as a literal. `target_defaults_tests::the_baseline_band_is_the_frozen_one`
374/// checks this constant against `DENSE_GEMV_BATCHM_DECODE_MAX_M` and the test
375/// file's copy of the gb10 declaration;
376/// `gb10_declares_the_baseline_apart_from_the_measured_w8a8_ceiling`
377/// (`crates/kernels/tests/target_defaults.rs`) checks the real gb10
378/// declaration against the literal.
379pub const BASELINE_BATCHM_MAX: u32 = DENSE_GEMV_BATCHM_DECODE_MAX_M;
380
381#[cfg(test)]
382#[path = "target_defaults_tests.rs"]
383mod tests;