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;