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;