spark_model/layers/dflash_head/levers.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! DFlash drafter levers, resolved once per head and then carried.
4//!
5//! # ★ THE ENVIRONMENT IS READ EXACTLY ONCE PER HEAD. KEEP IT THAT WAY.
6//!
7//! `forward_block` runs once per DECODE STEP, and its per-layer helpers run
8//! `num_layers` times inside that. Every `std::env::var` on that path
9//! allocates a `String` and takes the PROCESS-WIDE environment lock, so
10//! concurrent decode threads serialise against each other on it —
11//! MEASURED on GB10: a 30-variable resolve costs 0.57 us single-threaded but
12//! **4.00 us at 8 threads and 5.76 us at 16**. The cost grows with
13//! concurrency, which is exactly why no single-stream benchmark shows it.
14//!
15//! Before this module `forward_block` read **31** variables per propose, ten
16//! of them duplicates of a variable it had already read in the same call, and
17//! eleven of them in a single `&&` chain whose only job was to answer one
18//! question: is any diagnostic armed?
19//!
20//! These are resolved at head construction rather than in a `OnceLock`
21//! static, for the reason [`crate::layers::ops::ModelLevers`] gives: a static
22//! outlives the model whose flags it encodes, so a second model silently
23//! keeps the first one's branches. A field on the head cannot go stale,
24//! because a new head is a new resolution.
25
26/// Diagnostic and A/B levers for one loaded DFlash drafter.
27///
28/// Plain `Copy` data. Every field is a pure function of one `ATLAS_*`
29/// variable except [`Self::any_diagnostic_armed`], which is a function of
30/// eleven of them.
31// `Eq` is deliberately absent: `conf_tau` is an f32 threshold. Comparing two
32// resolutions for equality is a test-only need and `PartialEq` covers it.
33#[derive(Clone, Copy, Debug, PartialEq, Default)]
34pub struct DFlashLevers {
35 /// Any of the eleven diagnostic variables is **SET**, at any value.
36 ///
37 /// This is the CUDA-graph kill switch: a diagnostic that dumps or logs
38 /// from inside the captured region would be captured with it and then
39 /// replayed silently, so an armed diagnostic forces the eager path.
40 ///
41 /// ★ Presence, not truth — `ATLAS_DFLASH_BLOCK_DUMP=0` suppresses graph
42 /// capture while enabling no dump at all. That is the shipped behaviour
43 /// (the chain this replaces tested `std::env::var(..).is_err()`), and it
44 /// is pinned by a test rather than quietly fixed: the variables are
45 /// operator-facing A/B switches, and a graph capture that appears only
46 /// when a flag is spelled a particular way is a worse surprise than an
47 /// over-eager kill switch.
48 ///
49 /// One deliberate divergence from the `is_err()` chain this replaces: it
50 /// tested `std::env::var`, which reports a NON-UTF-8 value as absent, so
51 /// `ATLAS_DFLASH_BLOCK_DUMP=<invalid utf-8>` used to leave capture ON.
52 /// This uses `var_os`, so such a value now suppresses capture — the same
53 /// direction as every other spelling, and the same `present` idiom
54 /// `ModelLevers` uses.
55 pub any_diagnostic_armed: bool,
56
57 // ── One-shot dumps ──
58 /// `ATLAS_DFLASH_DEBUG_DUMP=1` — first 10 BF16 floats of each key
59 /// intermediate, for element-wise comparison against a Python reference.
60 pub debug_dump: bool,
61 /// `ATLAS_DFLASH_DEBUG_DUMP_FULL=1` — full tensors, not the first 10.
62 pub debug_dump_full: bool,
63 /// `ATLAS_DFLASH_LOG_DRAFTS=1` — log the γ drafts each propose returns.
64 pub log_drafts: bool,
65 /// `ATLAS_DFLASH_BLOCK_DUMP=1` — per-layer `.bin` dumps of the block
66 /// inputs and every layer's output.
67 pub block_dump: bool,
68 /// `ATLAS_DFLASH_BLOCK_DUMP_AT_POS=<n>` — arm the block dump only at
69 /// decode position ≥ n, so the dump can be taken in the regime where
70 /// absolute positions have diverged from ctx slot indices. Default 0
71 /// (dump at the first propose).
72 pub block_dump_at_pos: usize,
73 /// `ATLAS_DFLASH_OPTION_B_DIAG=1` — read back layer 0's first cached
74 /// K/V row from the paged drafter cache.
75 pub option_b_diag: bool,
76
77 // ── Forced inputs (reference-comparison A/B) ──
78 /// `ATLAS_DFLASH_DEBUG_FORCE_PATTERN=1` — overwrite the captured target
79 /// hidden with a deterministic pattern the PyTorch reference also makes.
80 pub force_pattern: bool,
81 /// `ATLAS_DFLASH_DEBUG_FORCE_NOISE_PATTERN=1` — same, for the noise rows.
82 pub force_noise_pattern: bool,
83 /// `ATLAS_DFLASH_DEBUG_CTX_OFF=1` — drop ctx conditioning entirely
84 /// (`eff_ctx = 0`), the A/B for whether the drafter responds to ctx.
85 pub force_no_ctx: bool,
86 /// `ATLAS_DFLASH_DEBUG_CTX_USED=<n>` — pin `eff_ctx` to exactly n.
87 pub force_ctx_used: Option<usize>,
88
89 // ── Precompute diagnostics ──
90 /// `ATLAS_DFLASH_PRECOMPUTE=1` — run the ctx K/V precompute chain from
91 /// `forward_block` (the production path runs it from `propose`).
92 pub precompute: bool,
93 /// `ATLAS_DFLASH_PRECOMPUTE_COMMIT=1` — let that diagnostic run write to
94 /// the paged cache. Off by default because `forward_block` does not
95 /// guarantee a valid block table.
96 pub precompute_commit: bool,
97
98 // ── Graph capture ──
99 /// `ATLAS_DFLASH_PROPOSE_WARMUP_N=<n>` — eager warm-up passes before
100 /// capture. Default 2: two passes warm the PTX→SASS cache, ramp GB10
101 /// clocks, and pull hot weight tiles into L2 before capture freezes the
102 /// SASS variants the driver picked.
103 pub propose_warmup_n: usize,
104
105 // ── Path selection ──
106 /// The Option-B paged drafter cache. Ships ON since the 54.5 record
107 /// config (#649); `ATLAS_DFLASH_OPTION_B=0` is the kill switch.
108 ///
109 /// The POLARITY has already been flipped by accident once: a merge on
110 /// 2026-08-30 turned `!= Some("0")` into `== Some("1")`, and propose went
111 /// 19.8 -> 618.7 ms (49.9 -> 5.5 tok/s) because the legacy path launches
112 /// one `dense_gemv` per accumulated ctx row over a 262 MB `fc` weight.
113 /// Nothing logged a change. Resolution goes through
114 /// `super::option_b_from` so the predicate keeps its own tests.
115 ///
116 /// Deliberately NOT an intra-doc link: `option_b_from` is `pub(super)`,
117 /// and rustdoc rejects a link from public documentation to a private
118 /// item under this crate's `deny(warnings)`. Widening the function to
119 /// `pub` to satisfy the link would export a predicate the module keeps
120 /// internal on purpose — the wrong half of the trade.
121 pub option_b: bool,
122 /// `ATLAS_DFLASH_OPTION_B_NO_CTX=1` — force `ctx_count = 0` in the layer
123 /// body so paged attention sees only the γ K/V written in-layer. If the
124 /// accept rate is bad even here, the bug is in the cache write/read path
125 /// rather than in precompute.
126 pub option_b_no_ctx: bool,
127 /// The DFlash2 conv+selector path. Ships ON when the checkpoint carries
128 /// the components; `ATLAS_DFLASH2=0` disables.
129 pub dflash2: bool,
130 /// `ATLAS_DFLASH_BATCH_PROPOSE=<width>` caps the cross-sequence batch.
131 /// `usize::MAX` (unset) means "as wide as the scratch bands allow";
132 /// `1` or `0` restores the per-sequence loop. Numeric rather than boolean
133 /// because bisecting the WIDTH against acceptance is what localises a
134 /// banding bug — "correct at 2 bands, wrong at 4" found the lm_head tile
135 /// bound, and an on/off flag cannot ask that question.
136 pub batch_propose_width: usize,
137 /// `ATLAS_DFLASH_DRAFT_CAP=<n>` — submit at most n drafts per propose.
138 /// `None` means the head's own γ.
139 pub draft_cap: Option<usize>,
140
141 // ── Propose-path diagnostics ──
142 /// `ATLAS_DFLASH_VERIFY_TRACE=1` — log all γ drafts BEFORE the cap, so
143 /// an echo at position 0 can be told from an echo on every noise row.
144 pub verify_trace: bool,
145 /// `ATLAS_DFLASH_PRECOMPUTE_DUMP=1` — one-shot dump of the fused ctx K/V
146 /// GEMM inputs and outputs.
147 pub precompute_dump: bool,
148 /// `ATLAS_DFLASH_CTX_PARITY_DUMP=1` — one-shot dump of the accumulated
149 /// ctx hidden rows for a PyTorch parity diff.
150 pub ctx_parity_dump: bool,
151 /// `ATLAS_DFLASH_DEBUG_NO_DECODE_APPEND=1` — skip the post-decode ctx
152 /// append entirely.
153 pub no_decode_append: bool,
154 /// `ATLAS_DFLASH_DEBUG_FULL_PRECOMPUTE=1` — recompute the whole ctx
155 /// prefix each step (`committed = 0`) instead of the incremental
156 /// watermark path, for accept-rate parity A/B. O(ctx_len^2).
157 pub full_precompute: bool,
158 /// `ATLAS_DFLASH_CTXLEN_PROBE=1` — assert `ctx_positions` is strictly
159 /// increasing, and log ctx_len against position every 16 steps. Both
160 /// probes are host-side scans, so they stay behind one flag.
161 pub ctxlen_probe: bool,
162
163 // ── DSpark ──
164 /// The sequential Markov fixup. Ships ON when the drafter carries the
165 /// head; `ATLAS_DSPARK_MARKOV=0` degrades to the batched argmax path
166 /// bit-for-bit.
167 pub dspark_markov: bool,
168 /// `ATLAS_DSPARK_CONF_TAU=<t>` — sigmoid-space acceptance threshold for
169 /// the confidence head. `0.0` (unset) disables the head entirely,
170 /// matching the reference's `threshold <= 0.0 -> full block`.
171 pub conf_tau: f32,
172 /// `ATLAS_DSPARK_SHIFT=1|0` forces the SpecForge shifted-row convention
173 /// on or off; unset (`None`) defers to the drafter config.
174 pub dspark_shift: Option<bool>,
175 /// Row 0 carries the Markov anchor bias. Ships ON;
176 /// `ATLAS_DSPARK_ANCHOR_BIAS=0` exempts it. Confidence truncation reads
177 /// this too — rows without the chain never write their confidence slot.
178 pub dspark_anchor_bias: bool,
179 /// `ATLAS_DSPARK_CONF_TRACE=1` — log the confidence logits and sigmoids.
180 pub dspark_conf_trace: bool,
181}
182
183/// The eleven variables whose mere PRESENCE forces the eager path.
184///
185/// Named as one list because they are one predicate. Adding a diagnostic that
186/// writes from inside `forward_block` and forgetting to add it here means the
187/// diagnostic gets captured into the graph and replayed — which reads as a
188/// dump that never updates, not as an error.
189const GRAPH_SUPPRESSING_DIAGNOSTICS: [&str; 11] = [
190 "ATLAS_DFLASH_PROPOSE_NO_GRAPH",
191 "ATLAS_DFLASH_DEBUG_DUMP_FULL",
192 "ATLAS_DFLASH_OPTION_B_DIAG",
193 "ATLAS_DFLASH_PRECOMPUTE_DUMP",
194 "ATLAS_DFLASH_VERIFY_TRACE",
195 "ATLAS_DFLASH_LOG_DRAFTS",
196 "ATLAS_DFLASH_DEBUG_FORCE_PATTERN",
197 "ATLAS_DFLASH_DEBUG_FORCE_NOISE_PATTERN",
198 "ATLAS_DFLASH_DEBUG_CTX_OFF",
199 "ATLAS_DFLASH_DEBUG_CTX_USED",
200 "ATLAS_DFLASH_BLOCK_DUMP",
201];
202
203fn from_values(
204 mut value: impl FnMut(&str) -> Option<String>,
205 mut present: impl FnMut(&str) -> bool,
206) -> DFlashLevers {
207 fn opt_in(value: Option<&str>) -> bool {
208 value == Some("1")
209 }
210 fn parsed<T: std::str::FromStr>(value: Option<&str>) -> Option<T> {
211 value.and_then(|v| v.parse().ok())
212 }
213
214 DFlashLevers {
215 any_diagnostic_armed: GRAPH_SUPPRESSING_DIAGNOSTICS.iter().any(|var| present(var)),
216
217 debug_dump: opt_in(value("ATLAS_DFLASH_DEBUG_DUMP").as_deref()),
218 debug_dump_full: opt_in(value("ATLAS_DFLASH_DEBUG_DUMP_FULL").as_deref()),
219 log_drafts: opt_in(value("ATLAS_DFLASH_LOG_DRAFTS").as_deref()),
220 block_dump: opt_in(value("ATLAS_DFLASH_BLOCK_DUMP").as_deref()),
221 block_dump_at_pos: parsed(value("ATLAS_DFLASH_BLOCK_DUMP_AT_POS").as_deref()).unwrap_or(0),
222 option_b_diag: opt_in(value("ATLAS_DFLASH_OPTION_B_DIAG").as_deref()),
223
224 force_pattern: opt_in(value("ATLAS_DFLASH_DEBUG_FORCE_PATTERN").as_deref()),
225 force_noise_pattern: opt_in(value("ATLAS_DFLASH_DEBUG_FORCE_NOISE_PATTERN").as_deref()),
226 force_no_ctx: opt_in(value("ATLAS_DFLASH_DEBUG_CTX_OFF").as_deref()),
227 force_ctx_used: parsed(value("ATLAS_DFLASH_DEBUG_CTX_USED").as_deref()),
228
229 precompute: opt_in(value("ATLAS_DFLASH_PRECOMPUTE").as_deref()),
230 precompute_commit: opt_in(value("ATLAS_DFLASH_PRECOMPUTE_COMMIT").as_deref()),
231
232 option_b: super::option_b_from(value("ATLAS_DFLASH_OPTION_B").as_deref()),
233 option_b_no_ctx: opt_in(value("ATLAS_DFLASH_OPTION_B_NO_CTX").as_deref()),
234 dflash2: value("ATLAS_DFLASH2").as_deref() != Some("0"),
235 batch_propose_width: parsed(value("ATLAS_DFLASH_BATCH_PROPOSE").as_deref())
236 .unwrap_or(usize::MAX),
237 draft_cap: parsed(value("ATLAS_DFLASH_DRAFT_CAP").as_deref()),
238
239 verify_trace: opt_in(value("ATLAS_DFLASH_VERIFY_TRACE").as_deref()),
240 precompute_dump: opt_in(value("ATLAS_DFLASH_PRECOMPUTE_DUMP").as_deref()),
241 ctx_parity_dump: opt_in(value("ATLAS_DFLASH_CTX_PARITY_DUMP").as_deref()),
242 no_decode_append: opt_in(value("ATLAS_DFLASH_DEBUG_NO_DECODE_APPEND").as_deref()),
243 full_precompute: opt_in(value("ATLAS_DFLASH_DEBUG_FULL_PRECOMPUTE").as_deref()),
244 ctxlen_probe: opt_in(value("ATLAS_DFLASH_CTXLEN_PROBE").as_deref()),
245
246 dspark_markov: value("ATLAS_DSPARK_MARKOV").as_deref() != Some("0"),
247 conf_tau: parsed(value("ATLAS_DSPARK_CONF_TAU").as_deref()).unwrap_or(0.0),
248
249 propose_warmup_n: parsed(value("ATLAS_DFLASH_PROPOSE_WARMUP_N").as_deref()).unwrap_or(2),
250
251 dspark_shift: match value("ATLAS_DSPARK_SHIFT").as_deref() {
252 Some("1") => Some(true),
253 Some("0") => Some(false),
254 _ => None,
255 },
256 dspark_anchor_bias: value("ATLAS_DSPARK_ANCHOR_BIAS").as_deref() != Some("0"),
257 dspark_conf_trace: opt_in(value("ATLAS_DSPARK_CONF_TRACE").as_deref()),
258 }
259}
260
261impl DFlashLevers {
262 /// Resolve from the environment. Called ONCE, when the head is built.
263 ///
264 /// ★ Do not call this from `forward_block`, `propose`, or anything they
265 /// reach. Take `self.levers` from the head instead — that is why the
266 /// field exists, and `dflash_levers_are_resolved_once` fails the build if
267 /// a raw `std::env::var` reappears on those paths.
268 pub fn from_env() -> Self {
269 from_values(
270 |var| std::env::var(var).ok(),
271 |var| std::env::var_os(var).is_some(),
272 )
273 }
274
275 /// What a head resolves to with no `ATLAS_*` set: every diagnostic off,
276 /// the anchor bias on, two warm-up passes. Tests construct this rather
277 /// than mutating the process environment, which `set_var` makes unsafe
278 /// and which would race every other test in the binary.
279 pub fn defaults() -> Self {
280 Self {
281 // Every OPT-OUT lever must be spelled here — `Self::default()`
282 // would ship each of them OFF, which for `option_b` alone is the
283 // measured 49.9 -> 5.5 tok/s collapse.
284 option_b: true,
285 dflash2: true,
286 dspark_markov: true,
287 batch_propose_width: usize::MAX,
288 dspark_anchor_bias: true,
289 // Not a boolean default — the warm-up count is load-bearing for
290 // graph capture, so it is spelled out rather than derived from
291 // `usize::default()`.
292 propose_warmup_n: 2,
293 ..Self::default()
294 }
295 }
296
297 /// The block dump is armed for this decode position.
298 ///
299 /// Three sites asked this question with two env reads each; it is one
300 /// predicate over already-resolved data.
301 pub fn block_dump_armed_at(&self, position: usize) -> bool {
302 self.block_dump && position >= self.block_dump_at_pos
303 }
304}
305
306#[cfg(test)]
307#[path = "levers_tests.rs"]
308mod tests;