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 reading used when the CLI never set anything: the compiled
91 /// target's declaration, with the environment overriding.
92 ///
93 /// `batched_recurrent` takes its DEFAULT from
94 /// `kernels/<hw>/HARDWARE.toml` `[defaults] ssm_batched_recurrent` —
95 /// `hopper` declares it ON (+6% on the serve, md5-identical output to the
96 /// per-sequence launches), `gb10` and `b200` declare it OFF, unchanged. It
97 /// used to be `ATLAS_SSM_BATCHED_RECURRENT=1` in an H100 launch script
98 /// outside this repository, which is the structure the 2026-09-11
99 /// maintainer review asked for. `ATLAS_SSM_BATCHED_RECURRENT` still
100 /// overrides, and `=0` now means OFF rather than reading as absent (see
101 /// `layers::ops::target_defaults`); everything that ever set it set it
102 /// to `1`.
103 ///
104 /// `ATLAS_SSM_H_FP16` stays PRESENCE-gated here on purpose: that is how
105 /// every script and ledger in the campaign wrote it, and silently changing
106 /// `=0` from ON to OFF would retroactively re-label measurements. New
107 /// configuration should use `--ssm-h-dtype`.
108 fn from_env() -> Self {
109 Self {
110 h_f16: std::env::var("ATLAS_SSM_H_FP16").is_ok(),
111 // No environment fallback on purpose (house rule: no new env
112 // knobs) — stage 3 has no CLI surface either until prefill
113 // narrowing lands; only unit tests exercise the sizing.
114 h_f16_pool: false,
115 fused_norm: std::env::var("ATLAS_GDN_FUSED_NORM").as_deref() == Ok("1"),
116 batched_recurrent: crate::layers::ops::target_defaults::resolved()
117 .ssm_batched_recurrent
118 .value,
119 // No legacy environment variable on purpose (house rule: CLI flags
120 // or defaults, no new env knobs). Default = the legacy WY arms;
121 // exact verify is CLI-opt-in only (`--exact-verify`).
122 exact_verify: false,
123 }
124 }
125}
126
127/// Publish the command line's resolution. Call once, before the model builds.
128///
129/// Returns the value in force, which is the argument unless something already
130/// read a flag (in which case the read wins and the caller should say so
131/// rather than pretend the setting took).
132pub fn set_from_cli(flags: GdnFlags) -> GdnFlags {
133 let _ = FLAGS.set(flags);
134 *FLAGS.get().expect("just set")
135}
136
137/// The resolved flags, falling back to the environment on first touch.
138pub fn flags() -> GdnFlags {
139 *FLAGS.get_or_init(GdnFlags::from_env)
140}
141
142/// Widest chain-verify K with an FP16 h-state twin
143/// (`gated_delta_rule_wy{5..16}_f16`).
144///
145/// The SSOT for "can this verify width run under the f16 pool". K=17 — the
146/// DFlash arm at gamma 16 — has no twin, and the FP32 wy17 kernel over an
147/// FP16 h-state emits fluent garbage rather than faulting, so the CLI
148/// validator and the serve preflight both gate on this instead of a literal.
149///
150/// Expressed as K, not gamma: the DFlash verify width is gamma + 1, and
151/// conflating the two is how the width check came to admit gamma 16 (K=17,
152/// no twin) while its message claimed to cover "widths 5..16".
153pub const MAX_F16_TWIN_K: usize = 16;
154
155/// The largest `--dflash-gamma` whose verify width still has an FP16 twin.
156pub const MAX_F16_TWIN_DFLASH_GAMMA: usize = MAX_F16_TWIN_K - 1;
157
158/// The served DFlash gamma for a drafter of this trained block size, when
159/// no `--dflash-gamma` was given.
160///
161/// THE SSOT, and it must stay that way: the drafter head resolves its gamma
162/// through this, and so does every preflight that sizes a pool or a reserve
163/// from a peeked `dflash_config.block_size`. Two spellings of this rule is
164/// how the SSM MTP intermediates came to be reserved for K=9 while verify
165/// asked for K=10, a hard error at the first verify step, mid-graph-capture.
166///
167/// `block + 2`, because these drafters chain PAST their trained block:
168/// measured on Qwen3.8-27B DFlash2 (block 8), gamma 8 runs 7 drafts, one
169/// short, while gamma 10 holds full-block 9/9 accepts and is the fastest
170/// measured serve (63.0 vs 56.2 tok/s on GB10, 2026-08-29).
171///
172/// Clamped to `MAX_F16_TWIN_DFLASH_GAMMA` so a block-16-class drafter lands
173/// on 15, the widest verify width with kernel coverage under both h-state
174/// dtypes, instead of gamma 18 / K=19, which no wyN kernel serves.
175pub const fn default_dflash_gamma(trained_block_size: usize) -> usize {
176 let bumped = trained_block_size + 2;
177 if bumped > MAX_F16_TWIN_DFLASH_GAMMA {
178 MAX_F16_TWIN_DFLASH_GAMMA
179 } else {
180 bumped
181 }
182}
183
184/// `--ssm-h-dtype f16` (legacy `ATLAS_SSM_H_FP16`).
185pub fn ssm_h_fp16_enabled() -> bool {
186 flags().h_f16
187}
188
189/// Stage 3 of the f16 h-state: h pools SIZED at 2 bytes/element
190/// (`--ssm-h-dtype f16-pool`). Implies [`ssm_h_fp16_enabled`] — a narrow
191/// pool holding FP32 would be an OOB write, not a mode — which
192/// [`ssm_h_dtype_bits`] guarantees at the one place the value is decoded.
193pub fn ssm_h_f16_pool_enabled() -> bool {
194 flags().h_f16_pool
195}
196
197/// SSOT decode of `--ssm-h-dtype` into the two h-state bits it publishes:
198/// `(h_f16, h_f16_pool)`.
199///
200/// Both the CLI validator (which rejects the pairs the mode cannot serve)
201/// and `publish_kernel_flags` (which publishes the cell the kernels
202/// dispatch on) go through THIS, so a validator that accepted one reading
203/// while the kernels took another is not expressible. Anything that is not
204/// exactly `f16` or `f16-pool` — including `f32` and an absent flag — is
205/// FP32; `check_enum` has already rejected unknown spellings by the time
206/// this runs, and defaulting an unknown one to FP32 here is the safe arm
207/// besides.
208pub fn ssm_h_dtype_bits(dtype: Option<&str>) -> (bool, bool) {
209 match dtype {
210 Some("f16") => (true, false),
211 // f16-pool is f16 PLUS the narrow pool: never one without the other.
212 Some("f16-pool") => (true, true),
213 _ => (false, false),
214 }
215}
216
217/// `--gdn-fused-norm` (legacy `ATLAS_GDN_FUSED_NORM=1`).
218pub fn gdn_fused_norm_enabled() -> bool {
219 flags().fused_norm
220}
221
222/// `--ssm-batched-recurrent` (legacy `ATLAS_SSM_BATCHED_RECURRENT=1`).
223pub fn ssm_batched_recurrent_enabled() -> bool {
224 flags().batched_recurrent
225}
226
227/// `--exact-verify` given (and h-state is FP32): the MTP-verify pass runs
228/// the sequential-decode-exact chain. FALSE by default — without the flag the
229/// verify pass runs the WY/chunkwise arms and #435's spec-on/spec-off output
230/// divergence remains. See [`GdnFlags::verify_exact_active`].
231pub fn verify_exact_enabled() -> bool {
232 flags().verify_exact_active()
233}
234
235/// Batch width at which the multi-seq decode projections switch to the
236/// 128-row M-tile. `None` (kill switch `ATLAS_NO_SSM_M128`, PRESENCE check —
237/// `=0` is NOT "off") keeps the 64-row twin at every width.
238///
239/// 65 is the DERIVED crossover, not a tuned constant: `ceil(m/64) >
240/// ceil(m/128)` first holds at m=65, so m<=64 gains no weight-read reduction
241/// from the wider tile and would only pad MMA rows. Identical rule to the
242/// dense-FFN prefill macro's `m <= 64` small-M arm.
243pub(crate) fn ssm_m128_min_m() -> Option<u32> {
244 static M: std::sync::OnceLock<Option<u32>> = std::sync::OnceLock::new();
245 *M.get_or_init(|| {
246 if std::env::var("ATLAS_NO_SSM_M128").is_ok() {
247 None
248 } else {
249 Some(65)
250 }
251 })
252}
253
254#[cfg(test)]
255mod tests {
256 use super::{GdnFlags, ssm_h_dtype_bits};
257
258 const BASE: GdnFlags = GdnFlags {
259 h_f16: false,
260 h_f16_pool: false,
261 fused_norm: false,
262 batched_recurrent: false,
263 exact_verify: false,
264 };
265
266 /// POSITIVE (the default): with no flags the verify pass runs the legacy
267 /// WY/chunkwise arms, NOT the exact chain. Exact verify became OPT-IN
268 /// (every surveyed production engine ships exactness opt-in; its measured
269 /// decode-step cost here is ~+22-36%), so the #435 divergence is the
270 /// documented default behaviour — this test pins that polarity.
271 #[test]
272 fn legacy_wy_verify_is_the_default() {
273 assert!(
274 !BASE.verify_exact_active(),
275 "default must be the legacy WY arms — exact verify is opt-in"
276 );
277 // Orthogonal flags do not sneak exact mode on.
278 assert!(
279 !GdnFlags {
280 fused_norm: true,
281 batched_recurrent: true,
282 ..BASE
283 }
284 .verify_exact_active()
285 );
286 }
287
288 /// POSITIVE (the opt-in): `--exact-verify` selects the exact chain, alone
289 /// and beside the orthogonal GDN flags.
290 #[test]
291 fn exact_verify_flag_selects_the_exact_chain() {
292 assert!(
293 GdnFlags {
294 exact_verify: true,
295 ..BASE
296 }
297 .verify_exact_active()
298 );
299 assert!(
300 GdnFlags {
301 exact_verify: true,
302 fused_norm: true,
303 batched_recurrent: true,
304 ..BASE
305 }
306 .verify_exact_active()
307 );
308 }
309
310 /// The environment fallback can NEVER turn exact verify on: there is no
311 /// `ATLAS_*` variable for it on purpose (house rule: no new env knobs),
312 /// so a serve that skips `set_from_cli` still defaults to the WY arms.
313 /// Deterministic despite reading the process environment, because only
314 /// the `exact_verify` field is asserted and no variable feeds it.
315 #[test]
316 fn env_fallback_never_enables_exact_verify() {
317 assert!(!GdnFlags::from_env().exact_verify);
318 // Same rule for the stage-3 pool sizing: no env variable feeds it.
319 // `--ssm-h-dtype f16-pool` is the ONLY way to publish it, so a
320 // legacy `ATLAS_SSM_H_FP16=1` script keeps the FP32-sized pool.
321 assert!(!GdnFlags::from_env().h_f16_pool);
322 }
323
324 /// A narrow pool holding FP32 is an out-of-bounds write, not a mode, so
325 /// `h_f16_pool` without `h_f16` must not be expressible from any input.
326 /// This is the ONE decode both the validator and the publisher use, so
327 /// pinning it here pins it for both.
328 #[test]
329 fn the_pool_bit_is_never_set_without_the_dtype_bit() {
330 for (spelling, expected) in [
331 (None, (false, false)),
332 (Some("f32"), (false, false)),
333 (Some("f16"), (true, false)),
334 (Some("f16-pool"), (true, true)),
335 (Some(""), (false, false)),
336 (Some("F16-POOL"), (false, false)),
337 (Some("f16 "), (false, false)),
338 ] {
339 assert_eq!(ssm_h_dtype_bits(spelling), expected, "{spelling:?}");
340 }
341 }
342
343 /// NEGATIVE: an FP16 h-state forces non-exact EVEN WHEN exact was
344 /// requested — the exact arm's FP32 kernels must never read the FP16
345 /// pool. (CLI validation rejects the explicit pair; this is the
346 /// defense-in-depth layer beneath it.)
347 #[test]
348 fn h_f16_forces_non_exact_even_when_requested() {
349 assert!(
350 !GdnFlags {
351 exact_verify: true,
352 h_f16: true,
353 ..BASE
354 }
355 .verify_exact_active()
356 );
357 assert!(
358 !GdnFlags {
359 h_f16: true,
360 ..BASE
361 }
362 .verify_exact_active()
363 );
364 }
365}