spark_model/layers/qwen3_ssm/gdn_flags.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! GDN / SSM decode-path flags, resolved ONCE from the serve command line.
4//!
5//! These three select KERNELS on the GDN decode path, and they are coupled:
6//! the FP16 h-state twins only exist on the fused-norm arm, so `h_f16` without
7//! `fused_norm` reaches an FP32-only kernel that would read the FP16 pool as
8//! FP32 — plausible numbers, silent garbage. That coupling is checked at serve
9//! time by `spark-server`'s arg validation, not discovered at the first decode
10//! step.
11//!
12//! ## Why these are set, not read
13//!
14//! They were three independent `std::env::var` reads scattered across six call
15//! sites, each with its own convention (`ATLAS_SSM_H_FP16` presence-gated —
16//! where `=0` meant ON — and the other two `== "1"`). That is how the same
17//! flag came to be decoded two different ways in one binary. They are now ONE
18//! cell, written once from [`set_from_cli`] before any model is built.
19//!
20//! The environment variables remain honoured when the setter never runs (a
21//! test, a microbenchmark example, an older script), so nothing that worked
22//! before stops working; the CLI wins when both are present.
23//!
24//! Follow-up: this is process-scoped, so a hot-swap to a model with a
25//! different recipe keeps the first model's kernel selection. The proper home
26//! is `ModelLevers`, which is carried per model — deferred because the h-state
27//! dtype is read from `SsmLayerState` construction sites that have no
28//! `ForwardContext`.
29
30/// The resolved flags. `None` until `set_from_cli` or the first env fallback.
31static FLAGS: std::sync::OnceLock<GdnFlags> = std::sync::OnceLock::new();
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct GdnFlags {
35 /// `--ssm-h-dtype f16`: store the GDN decode h-state as FP16.
36 pub h_f16: bool,
37 /// Stage 3 of the f16 h-state: additionally SIZE the h pools at 2 bytes
38 /// per element. Must imply `h_f16` (a narrow pool holding FP32 would be
39 /// an OOB write, not a mode). NOT serveable yet and therefore has NO
40 /// CLI surface — the CLI mapping always publishes `false`, and
41 /// `ssm_h_fp16_preconditions` refuses it besides (defense in depth) —
42 /// but the sizing plumbing keys off THIS field so the pool, preflight
43 /// and every byte-copier already agree on the storage width when
44 /// prefill narrowing lands.
45 pub h_f16_pool: bool,
46 /// `--gdn-fused-norm`: fused GDN output-norm decode kernel.
47 pub fused_norm: bool,
48 /// `--ssm-batched-recurrent`: one strided recurrent launch per batch.
49 pub batched_recurrent: bool,
50 /// `--exact-verify`: run the sequential-decode-EXACT per-token MTP-verify
51 /// chain (issue #435 route (a)) instead of the default WY-chunkwise /
52 /// fused BF16-conv arms. OPT-IN, default OFF; the measured decode-step
53 /// cost (~+22-36% at the n=8/16/32 verify rungs) is why.
54 ///
55 /// SCOPE: this makes the GDN/SSM verify chain exact. It does NOT deliver
56 /// end-to-end spec-on == spec-off, because every FFN and attention
57 /// projection dispatches on ROW COUNT (verify K=4 takes
58 /// `w4a16_gemv_batch4`, decode takes `w4a16_gemv`) and those separate
59 /// implementations round differently — ~5e-5 of lanes by 1 ULP, on every
60 /// shape measured (#459). Closing that needs single-row routing for the
61 /// whole verify forward, which is future work.
62 ///
63 /// ★ Attribution warning, learned the hard way: a 2026-08-21 measurement
64 /// showed gross output degeneration (video-fidelity 0/2, 0/4 at C=2/C=4)
65 /// that this flag appeared to fix. The real cause was the K=4 verdict
66 /// rewind bug (#699); this flag only changed dispatch so the bug stopped
67 /// firing. The 1-ULP divergence this flag actually closes has never been
68 /// shown to cause more than an occasional flipped token at temperature 0.
69 /// If flipping this flag changes gross behavior, suspect a dispatch-
70 /// sensitive scheduler bug first. Details on `ServeArgs::exact_verify`.
71 pub exact_verify: bool,
72}
73
74impl GdnFlags {
75 /// Whether the MTP-verify pass must run the sequential-decode-exact
76 /// conv+GDN chain (issue #435 route (a)). Default FALSE: exact verify is
77 /// opt-in via `--exact-verify`, so with default settings spec-on output
78 /// is NOT bitwise-equal to spec-off (the #435 divergence ships).
79 ///
80 /// Pure so it is testable without touching the process-global flags cell.
81 /// `h_f16` forces non-exact even when requested, because an FP16 h-state
82 /// is a whole-chain numerics change that is not bit-comparable to the
83 /// FP32 reference in the first place, and the exact arm's kernels are
84 /// FP32 readers (reading the FP16 pool through them would be silent
85 /// garbage, not an error). CLI validation additionally REJECTS the
86 /// explicit pair, so this clause is defense in depth, not the interface.
87 pub fn verify_exact_active(self) -> bool {
88 self.exact_verify && !self.h_f16
89 }
90 /// The legacy environment reading, used when the CLI never set anything.
91 ///
92 /// `ATLAS_SSM_H_FP16` stays PRESENCE-gated here on purpose: that is how
93 /// every script and ledger in the campaign wrote it, and silently changing
94 /// `=0` from ON to OFF would retroactively re-label measurements. New
95 /// configuration should use `--ssm-h-dtype`.
96 fn from_env() -> Self {
97 Self {
98 h_f16: std::env::var("ATLAS_SSM_H_FP16").is_ok(),
99 // No environment fallback on purpose (house rule: no new env
100 // knobs) — stage 3 has no CLI surface either until prefill
101 // narrowing lands; only unit tests exercise the sizing.
102 h_f16_pool: false,
103 fused_norm: std::env::var("ATLAS_GDN_FUSED_NORM").as_deref() == Ok("1"),
104 batched_recurrent: std::env::var("ATLAS_SSM_BATCHED_RECURRENT").as_deref() == Ok("1"),
105 // No legacy environment variable on purpose (house rule: CLI flags
106 // or defaults, no new env knobs). Default = the legacy WY arms;
107 // exact verify is CLI-opt-in only (`--exact-verify`).
108 exact_verify: false,
109 }
110 }
111}
112
113/// Publish the command line's resolution. Call once, before the model builds.
114///
115/// Returns the value in force, which is the argument unless something already
116/// read a flag (in which case the read wins and the caller should say so
117/// rather than pretend the setting took).
118pub fn set_from_cli(flags: GdnFlags) -> GdnFlags {
119 let _ = FLAGS.set(flags);
120 *FLAGS.get().expect("just set")
121}
122
123/// The resolved flags, falling back to the environment on first touch.
124pub fn flags() -> GdnFlags {
125 *FLAGS.get_or_init(GdnFlags::from_env)
126}
127
128/// Widest chain-verify K with an FP16 h-state twin
129/// (`gated_delta_rule_wy{5..16}_f16`).
130///
131/// The SSOT for "can this verify width run under the f16 pool". K=17 — the
132/// DFlash arm at gamma 16 — has no twin, and the FP32 wy17 kernel over an
133/// FP16 h-state emits fluent garbage rather than faulting, so the CLI
134/// validator and the serve preflight both gate on this instead of a literal.
135///
136/// Expressed as K, not gamma: the DFlash verify width is gamma + 1, and
137/// conflating the two is how the width check came to admit gamma 16 (K=17,
138/// no twin) while its message claimed to cover "widths 5..16".
139pub const MAX_F16_TWIN_K: usize = 16;
140
141/// The largest `--dflash-gamma` whose verify width still has an FP16 twin.
142pub const MAX_F16_TWIN_DFLASH_GAMMA: usize = MAX_F16_TWIN_K - 1;
143
144/// The served DFlash gamma for a drafter of this trained block size, when
145/// no `--dflash-gamma` was given.
146///
147/// THE SSOT, and it must stay that way: the drafter head resolves its gamma
148/// through this, and so does every preflight that sizes a pool or a reserve
149/// from a peeked `dflash_config.block_size`. Two spellings of this rule is
150/// how the SSM MTP intermediates came to be reserved for K=9 while verify
151/// asked for K=10, a hard error at the first verify step, mid-graph-capture.
152///
153/// `block + 2`, because these drafters chain PAST their trained block:
154/// measured on Qwen3.8-27B DFlash2 (block 8), gamma 8 runs 7 drafts, one
155/// short, while gamma 10 holds full-block 9/9 accepts and is the fastest
156/// measured serve (63.0 vs 56.2 tok/s on GB10, 2026-08-29).
157///
158/// Clamped to `MAX_F16_TWIN_DFLASH_GAMMA` so a block-16-class drafter lands
159/// on 15, the widest verify width with kernel coverage under both h-state
160/// dtypes, instead of gamma 18 / K=19, which no wyN kernel serves.
161pub const fn default_dflash_gamma(trained_block_size: usize) -> usize {
162 let bumped = trained_block_size + 2;
163 if bumped > MAX_F16_TWIN_DFLASH_GAMMA {
164 MAX_F16_TWIN_DFLASH_GAMMA
165 } else {
166 bumped
167 }
168}
169
170/// `--ssm-h-dtype f16` (legacy `ATLAS_SSM_H_FP16`).
171pub fn ssm_h_fp16_enabled() -> bool {
172 flags().h_f16
173}
174
175/// Stage 3 of the f16 h-state: h pools SIZED at 2 bytes/element
176/// (`--ssm-h-dtype f16-pool`). Implies [`ssm_h_fp16_enabled`] — a narrow
177/// pool holding FP32 would be an OOB write, not a mode — which
178/// [`ssm_h_dtype_bits`] guarantees at the one place the value is decoded.
179pub fn ssm_h_f16_pool_enabled() -> bool {
180 flags().h_f16_pool
181}
182
183/// SSOT decode of `--ssm-h-dtype` into the two h-state bits it publishes:
184/// `(h_f16, h_f16_pool)`.
185///
186/// Both the CLI validator (which rejects the pairs the mode cannot serve)
187/// and `publish_kernel_flags` (which publishes the cell the kernels
188/// dispatch on) go through THIS, so a validator that accepted one reading
189/// while the kernels took another is not expressible. Anything that is not
190/// exactly `f16` or `f16-pool` — including `f32` and an absent flag — is
191/// FP32; `check_enum` has already rejected unknown spellings by the time
192/// this runs, and defaulting an unknown one to FP32 here is the safe arm
193/// besides.
194pub fn ssm_h_dtype_bits(dtype: Option<&str>) -> (bool, bool) {
195 match dtype {
196 Some("f16") => (true, false),
197 // f16-pool is f16 PLUS the narrow pool: never one without the other.
198 Some("f16-pool") => (true, true),
199 _ => (false, false),
200 }
201}
202
203/// `--gdn-fused-norm` (legacy `ATLAS_GDN_FUSED_NORM=1`).
204pub fn gdn_fused_norm_enabled() -> bool {
205 flags().fused_norm
206}
207
208/// `--ssm-batched-recurrent` (legacy `ATLAS_SSM_BATCHED_RECURRENT=1`).
209pub fn ssm_batched_recurrent_enabled() -> bool {
210 flags().batched_recurrent
211}
212
213/// `--exact-verify` given (and h-state is FP32): the MTP-verify pass runs
214/// the sequential-decode-exact chain. FALSE by default — without the flag the
215/// verify pass runs the WY/chunkwise arms and #435's spec-on/spec-off output
216/// divergence remains. See [`GdnFlags::verify_exact_active`].
217pub fn verify_exact_enabled() -> bool {
218 flags().verify_exact_active()
219}
220
221/// Batch width at which the multi-seq decode projections switch to the
222/// 128-row M-tile. `None` (kill switch `ATLAS_NO_SSM_M128`, PRESENCE check —
223/// `=0` is NOT "off") keeps the 64-row twin at every width.
224///
225/// 65 is the DERIVED crossover, not a tuned constant: `ceil(m/64) >
226/// ceil(m/128)` first holds at m=65, so m<=64 gains no weight-read reduction
227/// from the wider tile and would only pad MMA rows. Identical rule to the
228/// dense-FFN prefill macro's `m <= 64` small-M arm.
229pub(crate) fn ssm_m128_min_m() -> Option<u32> {
230 static M: std::sync::OnceLock<Option<u32>> = std::sync::OnceLock::new();
231 *M.get_or_init(|| {
232 if std::env::var("ATLAS_NO_SSM_M128").is_ok() {
233 None
234 } else {
235 Some(65)
236 }
237 })
238}
239
240#[cfg(test)]
241mod tests {
242 use super::{GdnFlags, ssm_h_dtype_bits};
243
244 const BASE: GdnFlags = GdnFlags {
245 h_f16: false,
246 h_f16_pool: false,
247 fused_norm: false,
248 batched_recurrent: false,
249 exact_verify: false,
250 };
251
252 /// POSITIVE (the default): with no flags the verify pass runs the legacy
253 /// WY/chunkwise arms, NOT the exact chain. Exact verify became OPT-IN
254 /// (every surveyed production engine ships exactness opt-in; its measured
255 /// decode-step cost here is ~+22-36%), so the #435 divergence is the
256 /// documented default behaviour — this test pins that polarity.
257 #[test]
258 fn legacy_wy_verify_is_the_default() {
259 assert!(
260 !BASE.verify_exact_active(),
261 "default must be the legacy WY arms — exact verify is opt-in"
262 );
263 // Orthogonal flags do not sneak exact mode on.
264 assert!(
265 !GdnFlags {
266 fused_norm: true,
267 batched_recurrent: true,
268 ..BASE
269 }
270 .verify_exact_active()
271 );
272 }
273
274 /// POSITIVE (the opt-in): `--exact-verify` selects the exact chain, alone
275 /// and beside the orthogonal GDN flags.
276 #[test]
277 fn exact_verify_flag_selects_the_exact_chain() {
278 assert!(
279 GdnFlags {
280 exact_verify: true,
281 ..BASE
282 }
283 .verify_exact_active()
284 );
285 assert!(
286 GdnFlags {
287 exact_verify: true,
288 fused_norm: true,
289 batched_recurrent: true,
290 ..BASE
291 }
292 .verify_exact_active()
293 );
294 }
295
296 /// The environment fallback can NEVER turn exact verify on: there is no
297 /// `ATLAS_*` variable for it on purpose (house rule: no new env knobs),
298 /// so a serve that skips `set_from_cli` still defaults to the WY arms.
299 /// Deterministic despite reading the process environment, because only
300 /// the `exact_verify` field is asserted and no variable feeds it.
301 #[test]
302 fn env_fallback_never_enables_exact_verify() {
303 assert!(!GdnFlags::from_env().exact_verify);
304 // Same rule for the stage-3 pool sizing: no env variable feeds it.
305 // `--ssm-h-dtype f16-pool` is the ONLY way to publish it, so a
306 // legacy `ATLAS_SSM_H_FP16=1` script keeps the FP32-sized pool.
307 assert!(!GdnFlags::from_env().h_f16_pool);
308 }
309
310 /// A narrow pool holding FP32 is an out-of-bounds write, not a mode, so
311 /// `h_f16_pool` without `h_f16` must not be expressible from any input.
312 /// This is the ONE decode both the validator and the publisher use, so
313 /// pinning it here pins it for both.
314 #[test]
315 fn the_pool_bit_is_never_set_without_the_dtype_bit() {
316 for (spelling, expected) in [
317 (None, (false, false)),
318 (Some("f32"), (false, false)),
319 (Some("f16"), (true, false)),
320 (Some("f16-pool"), (true, true)),
321 (Some(""), (false, false)),
322 (Some("F16-POOL"), (false, false)),
323 (Some("f16 "), (false, false)),
324 ] {
325 assert_eq!(ssm_h_dtype_bits(spelling), expected, "{spelling:?}");
326 }
327 }
328
329 /// NEGATIVE: an FP16 h-state forces non-exact EVEN WHEN exact was
330 /// requested — the exact arm's FP32 kernels must never read the FP16
331 /// pool. (CLI validation rejects the explicit pair; this is the
332 /// defense-in-depth layer beneath it.)
333 #[test]
334 fn h_f16_forces_non_exact_even_when_requested() {
335 assert!(
336 !GdnFlags {
337 exact_verify: true,
338 h_f16: true,
339 ..BASE
340 }
341 .verify_exact_active()
342 );
343 assert!(
344 !GdnFlags {
345 h_f16: true,
346 ..BASE
347 }
348 .verify_exact_active()
349 );
350 }
351}