spark_model/
ssm_reserve.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! SSOT for the Phase-C decode-rollback ring depth.
4//!
5//! Two call sites MUST agree on this number or a serve either
6//! under-reserves (runtime CUDA alloc failure after weights load) or
7//! over-reserves (preflight refuses batch sizes the runtime could fund):
8//!
9//! * `spark-server` `preflight_reserve` — sizes the SSM-snapshot GPU
10//!   reservation before weights load;
11//! * `TransformerModel::new` (`impl_a1.rs`) — allocates the actual ring.
12//!
13//! The ring's ONLY writer (scheduler `snapshot_boundary_if_ssm`) and reader
14//! (content-loop `rollback_to_boundary`) live on the PLAIN decode path — the
15//! speculative path does its rejection rollback through the verify snapshot,
16//! never this ring. Under `--speculative` the ring is unreachable, and it is
17//! NOT cheap: 8 slots × max_batch × the full SSM blob (27B: 158.9 MB) is
18//! ~19 GB at batch 16 and ~38 GB at batch 32. Reserving it unconditionally
19//! while the runtime skipped it capped the native batch at ~20 on GB10
20//! (SSM reserve 75.2 GB vs an 85.2 GB budget at util 0.70).
21//!
22//! Env contract (read HERE and nowhere else):
23//!
24//! * `ATLAS_SSM_DECODE_RING=1` force-allocates the ring even under spec
25//!   (mixed workloads whose grammar-bound sequences fall to plain decode and
26//!   should keep loop re-steer); `=0` force-disables it even without spec.
27//! * `ATLAS_DISABLE_WATCHDOGS=1|true` (trimmed, case-insensitive — mirrors
28//!   spark-server's `parse_disable_watchdogs`): the ring's only reader can
29//!   never fire, so the ring is skipped.
30
31/// Outcome of the ring-depth decision.
32///
33/// `skip_reason` is `Some` only for the IMPLICIT skip (speculative decode /
34/// watchdogs off) — never for an explicit `ATLAS_SSM_DECODE_RING=0`
35/// override — so the allocating call site can log the savings once.
36pub struct DecodeRingDecision {
37    pub slots: usize,
38    pub skip_reason: Option<&'static str>,
39}
40
41/// Number of SSM-pool slots the MTP/DFlash VERIFY state pools (per-token
42/// intermediates + pre-verify checkpoints) must cover.
43///
44/// Three call sites MUST agree on this number (same contract as the decode
45/// ring above):
46///
47/// * `spark-server` `preflight_reserve` — sizes the pre-load GPU reserve;
48/// * `SsmStatePool::new` — allocates the intermediate/checkpoint pools;
49/// * the scheduler's spec dispatch — gates every speculative step on
50///   `slot_idx < mtp_state_slots(..)` so an uncovered slot can never be
51///   verified (uncovered slots plain-decode until retirement-time
52///   compaction migrates them under the cap).
53///
54/// WHY a cap exists: the verify pools were sized `max_batch_size × K` even
55/// though spec dispatch is bounded by `speculative::mtp_max_seqs()`
56/// (default 32 — the widest batched-verify chunk,
57/// `layer::VERIFY_WY_TABLE_SEQS`). On the 27B at `--max-batch-size 64`
58/// with `--num-drafts 3` that is 32 dead slots × 5 SSM blobs × 158.9 MB =
59/// 25.4 GB of reserve for states no code path can ever touch — the
60/// difference between bs=64 refusing at preflight (util 0.70) and booting.
61///
62/// The cap NEVER bites at `max_batch_size <= 32`: the floor is
63/// `VERIFY_WY_TABLE_SEQS` (32), so bs<=32 sizing and behavior are
64/// byte-identical in every env combination (slots are always `< bs`).
65///
66/// Env contract (read HERE and nowhere else):
67///
68/// * `ATLAS_MTP_POOL_FULL_WIDTH` (presence, house convention — `=0` is NOT
69///   off): restore full-width pools (`max_batch_size` slots) and make the
70///   scheduler guard vacuous. Kill switch for the bs>32 reserve diet.
71/// * `ATLAS_EP_PROTOCOL=v2` implies full width: v2 pins slots in place for
72///   the worker mirror (no compaction — see `retire_finished_sequences`),
73///   so a high slot may legitimately speculate forever.
74/// * `ATLAS_MTP_MAX_SEQS` participates via [`crate::speculative::mtp_max_seqs`]:
75///   raising the dispatch cap above 32 widens the pools with it.
76///
77/// ★ WHAT THE DIET COSTS, AND THE UTILISATION FLOOR IT SETS (wave 47,
78/// dgx3, 27B W4A4). The diet is what makes a single serve able to cover the
79/// whole concurrency ladder — speculation is dispatch-capped at 32, so one
80/// serve at `--max-batch-size 128 --speculative --num-drafts 3` speculates
81/// at C<=32 and plain-decodes above it. But the verify pools it keeps are
82/// still sized by `--num-drafts`, and at bs=128 that is not free. Measured
83/// preflight reserve, `--max-seq-len 4096`, blob 151.5 MB:
84///
85/// | config | base | verify pools | snapshot/misc | reserve |
86/// |---|---|---|---|---|
87/// | bs=128, spec OFF | 18.9 GB (128 blobs) | — | 5.5 GB | **24.3 GB** |
88/// | bs=128, spec ON, 3 drafts | 18.9 GB | **23.7 GB** (32 slots x 5 blobs) | 8.9 GB | **51.5 GB** |
89///
90/// With 39.8 GB already consumed before KV, that reserve REFUSES at
91/// `--gpu-memory-utilization 0.70` (39.8 + 51.5 = 91.3 GB committed against
92/// an 85.2 GB budget) and boots at 0.85 (103.4 GB budget, 13.3 GB left for
93/// KV = 217k tokens). The floor for the one-serve ladder is therefore
94/// **util ~0.82**, and it is set HERE, by the verify pools — not by the KV
95/// dtype, which moves the answer by well under a GB at these widths. A
96/// cheaper diet (row-budget-sized intermediates rather than slot-major)
97/// would recover ~9 GB and still not reach 0.70; the reserve, not the
98/// speculation regime, is what makes the low-util single config impossible.
99pub fn mtp_state_slots(max_batch_size: usize) -> usize {
100    mtp_state_slots_with(
101        max_batch_size,
102        crate::speculative::mtp_max_seqs(),
103        mtp_pool_full_width(),
104    )
105}
106
107/// The `ATLAS_MTP_POOL_FULL_WIDTH` kill switch (PRESENCE, house convention —
108/// `=0` is NOT off), plus the EP-v2 implication (v2 pins slots in place for
109/// the worker mirror, so a high slot may legitimately speculate forever).
110/// SSOT for BOTH pool diets it disables: the bs>32 slot-count cap
111/// ([`mtp_state_slots`]) and the tiered per-slot verify capacity
112/// ([`verify_slot_drafts`]) — one switch restores the full-width,
113/// uniform-K sizing everywhere (pool, preflight, scheduler clamp).
114pub fn mtp_pool_full_width() -> bool {
115    std::env::var_os("ATLAS_MTP_POOL_FULL_WIDTH").is_some()
116        || matches!(std::env::var("ATLAS_EP_PROTOCOL").as_deref(), Ok("v2"))
117}
118
119/// Pure core of [`mtp_state_slots`] (env-free, unit-testable).
120///
121/// `spec_dispatch_cap` is `speculative::mtp_max_seqs()` — the scheduler
122/// never dispatches a speculative step wider than this. The floor
123/// `VERIFY_WY_TABLE_SEQS` (32) guarantees bs<=32 configs are untouched even
124/// under `ATLAS_NO_MTP_K_LADDER` (which drops the dispatch cap to 4).
125pub fn mtp_state_slots_with(
126    max_batch_size: usize,
127    spec_dispatch_cap: usize,
128    full_width: bool,
129) -> usize {
130    if full_width {
131        return max_batch_size;
132    }
133    max_batch_size.min(spec_dispatch_cap.max(crate::layer::VERIFY_WY_TABLE_SEQS))
134}
135
136/// Per-slot verify DRAFT capacity — the tiered half of the verify-pool
137/// diet (2026-08-16). Pure core; `drafts_at(n)` is the ladder policy
138/// (`speculative::mtp_ladder_drafts`).
139///
140/// A sequence occupying pool slot `slot_idx` can only be co-active with at
141/// least `slot_idx + 1` sequences UNDER the contiguity invariant ("active
142/// sequences occupy contiguous slots [0..n)"), so the deepest draft count
143/// the ladder can ever hand it is the max over widths `n > slot_idx`. The
144/// invariant is TRANSIENTLY breakable (LIFO free-list claim after churn),
145/// which is why this number is also ENFORCED at dispatch: the scheduler
146/// clamps the step's draft count to the minimum capacity across the active
147/// slots (`step_mtp`), so a high-slotted straggler shrinks K for its step
148/// instead of overflowing its slot's pools.
149///
150/// Default ladder (`4:3,8:3,16:1,32:1`, `--num-drafts 3`): slots 0..8 keep
151/// capacity 3 (K=4), slots 8.. get capacity 1 (K=2). NOTE the runtime
152/// `adaptive_rung` lift (n in 9..=16 to 2 drafts on tool-shaped accept
153/// stats) EXCEEDS the static ladder this sizing derives from; under the
154/// tiered default it is clamped back to K=2 whenever any active sequence
155/// sits in a capacity-1 slot — i.e. at every n >= 9 under contiguity.
156/// `ATLAS_MTP_POOL_FULL_WIDTH` restores uniform full-K pools and re-enables
157/// the lift.
158pub fn verify_slot_drafts_with(
159    slot_idx: usize,
160    dispatch_cap: usize,
161    num_drafts: usize,
162    drafts_at: impl Fn(usize) -> usize,
163) -> usize {
164    if num_drafts == 0 {
165        return 0;
166    }
167    let hi = dispatch_cap.max(slot_idx + 1);
168    ((slot_idx + 1)..=hi)
169        .map(&drafts_at)
170        .max()
171        .unwrap_or(num_drafts)
172        .clamp(1, num_drafts)
173}
174
175/// Env-reading wrapper of [`verify_slot_drafts_with`]: the ladder policy
176/// (with its `ATLAS_MTP_K_LADDER` / `ATLAS_NO_MTP_K_LADDER` overrides — a
177/// disabled ladder returns `num_drafts` at every width, making the tiers
178/// vacuous) plus the [`mtp_pool_full_width`] kill switch.
179pub fn verify_slot_drafts(slot_idx: usize, num_drafts: usize) -> usize {
180    if mtp_pool_full_width() {
181        return num_drafts;
182    }
183    verify_slot_drafts_with(
184        slot_idx,
185        crate::speculative::mtp_max_seqs(),
186        num_drafts,
187        |n| crate::speculative::mtp_ladder_drafts(n, num_drafts),
188    )
189}
190
191/// Number of per-token H-state intermediates the verify pools allocate for
192/// pool slot `slot_idx`: exactly the slot's draft capacity (K-1 snapshots
193/// for a K-row verify). `uniform_verify` (DFlash-γ pools, whose verify
194/// width does not follow the MTP ladder) sizes every slot at the full
195/// `num_drafts`.
196///
197/// WHY K-1 and not K (2026-08-16 audit): no verify arm ever writes OR
198/// reads H intermediate index K-1. The fused WY kernels write
199/// Hi_0..Hi_{K-2} plus the final H in place (`gdn_decode_wy{2,3,4}`,
200/// `wyn`/`wy17`, the strided `_snap` twins NULL-skip index K-1), the
201/// single-seq K=2/3/4 arms and the exact arm skip the dead snapshot
202/// explicitly, and the sequential fallback now skips t = K-1 too. Every
203/// reader is bounded at index K-2: `commit_accepted_prefix` pins the
204/// reachable index to [0, k-2], `rollback_ssm_states` validates against
205/// the vec length with callers guaranteeing a rejected draft, and
206/// `start_rollback_and_checkpoint_async` is only called with 1..=K-1
207/// (index ≤ K-2). See the reader enumeration in
208/// `trait_decode_batched_conv_gdn.rs`.
209///
210/// Only the H side tiers. The CONV intermediates stay UNIFORM at
211/// `num_drafts + 1` per slot: the batched conv verify kernel
212/// (`gdn_verify_fused_conv_kn_batched`) requires a uniform cross-sequence
213/// snapshot stride (checked against the actual pointers in
214/// `trait_decode_batched_conv_gdn_multi.rs`) and writes all K snapshots —
215/// tiering conv would silently decline the two-launch fast path for every
216/// spec batch spanning the tier boundary (all n >= 9). Conv is ~5% of the
217/// blob, so the forgone saving is ~0.35 GiB at 32 slots while the H side
218/// carries the other 6.75 GiB.
219pub fn verify_slot_h_intermediates(
220    slot_idx: usize,
221    num_drafts: usize,
222    uniform_verify: bool,
223) -> usize {
224    if uniform_verify {
225        return num_drafts;
226    }
227    verify_slot_drafts(slot_idx, num_drafts)
228}
229
230/// Storage width of one h-state blob in the SSM state pools (stage 3 of
231/// `--ssm-h-dtype f16`): 2 bytes per element under the f16-SIZED pool, the
232/// FP32 4 bytes otherwise. SSOT — `SsmStatePool::new` (allocation strides),
233/// `preflight_reserve` (the pre-load reserve) and every byte-copier that
234/// moves h-state between pool regions derive their width from THIS, so
235/// sizing and copies cannot disagree.
236///
237/// `f16_pool` is `gdn_flags::ssm_h_f16_pool_enabled()` at the production
238/// call sites (`--ssm-h-dtype f16-pool`), passed as a parameter so pool
239/// construction and sizing stay testable without the process-global flag
240/// cell. NOTE stage 1/2 (`--ssm-h-dtype f16`) deliberately keep the pool
241/// FP32-SIZED (`f16_pool = false`): the state bits are FP16 during decode
242/// but prefill still writes FP32 in place, so the slot must stay wide.
243pub fn ssm_h_stored_bytes(h_f32_bytes: usize, f16_pool: bool) -> usize {
244    assert!(
245        h_f32_bytes.is_multiple_of(4),
246        "h-state blobs are FP32-element sized"
247    );
248    if f16_pool {
249        h_f32_bytes / 2
250    } else {
251        h_f32_bytes
252    }
253}
254
255/// FP32 h-state PREFILL STAGING bytes (stage 3 of `--ssm-h-dtype f16`).
256///
257/// Under the f16-SIZED pool a slot's h region is 2 bytes/element, but every
258/// GDN prefill kernel family reads and writes the running h-state as FP32 in
259/// place — over a 2-byte slot that is an overrun into the neighbouring slot.
260/// Stage 3 therefore gives each pool slot ONE FP32 staging blob, and the
261/// layer widens the slot into it before its FP32 kernels run and narrows it
262/// back after (`ssm_h_fp16::prefill_h_begin` / `prefill_h_end`).
263///
264/// ★ ONE blob per SLOT, **not** per slot per layer. The staging blob is live
265/// only for the duration of one SSM layer's prefill call: the layers of a
266/// pass are issued in order on a single stream, each narrowing back before
267/// the next widens, so layer L+1 reuses layer L's blob. Sizing it per slot
268/// (rather than per concurrently-prefilling sequence) is what makes that
269/// safe without knowing the co-dispatch width: a sequence owns exactly one
270/// slot for its whole life, so two sequences can never share a blob.
271///
272/// `h_layer_f32_bytes` is ONE layer's FP32 h blob (`ssm_h_state_bytes()`) —
273/// NOT the across-layers per-seq total the pool-reserve terms use. Zero when
274/// the pool is FP32-sized: prefill then writes the slot in place as it
275/// always has, and no staging exists to reserve.
276///
277/// SSOT for both `SsmStatePool::new` (which allocates it, passing
278/// `max_slots + 1` for the dummy slot) and the preflight reserve (which
279/// passes `max_batch_size`, matching its standing convention of not
280/// counting the dummy — the CUDA headroom term absorbs it).
281pub fn ssm_h_prefill_stage_bytes(slots: usize, h_layer_f32_bytes: usize, f16_pool: bool) -> usize {
282    if f16_pool {
283        slots * h_layer_f32_bytes
284    } else {
285        0
286    }
287}
288
289/// SSM state-pool reserve bytes for the pre-load preflight — MUST mirror
290/// what `SsmStatePool::new` allocates (modulo the +1 dummy slot per pool,
291/// which preflight has never counted; the CUDA headroom term absorbs it):
292///
293/// * base: `max_batch_size` live per-seq blobs (h_state + conv_state across
294///   all SSM layers);
295/// * spec, per verify slot (`mtp_state_slots` of them):
296///   - H intermediates: [`verify_slot_h_intermediates`] × h blob (TIERED,
297///     and K-1 per K-row verify — index K-1 is never written or read);
298///   - conv intermediates: `num_drafts + 1` × conv blob (uniform AND still
299///     K — the fused conv kernels write all K snapshots on-device; see
300///     [`verify_slot_h_intermediates`] for why conv does not tier);
301///   - 1 pre-verify checkpoint blob (h + conv).
302///
303/// `h_blob_bytes` / `conv_blob_bytes` are the per-seq totals across all SSM
304/// layers (`num_ssm_layers × ssm_h_state_bytes/ssm_conv_state_bytes`),
305/// ALWAYS at the FP32 width — `h_f16_pool` narrows every h term through
306/// [`ssm_h_stored_bytes`] inside, so preflight and `SsmStatePool::new`
307/// cannot narrow differently.
308/// The historical sizing was `max_batch × blob × (1 + (num_drafts+1) + 1)`;
309/// today's uniform mode differs from it by exactly one h blob per slot
310/// (the dead K-1 intermediate).
311pub fn ssm_pool_reserve_bytes(
312    max_batch_size: usize,
313    h_blob_bytes: usize,
314    conv_blob_bytes: usize,
315    spec_on: bool,
316    num_drafts: usize,
317    mtp_state_slots: usize,
318    uniform_verify: bool,
319    h_f16_pool: bool,
320    rollback: SsmRollbackMode,
321) -> usize {
322    let h_blob_bytes = ssm_h_stored_bytes(h_blob_bytes, h_f16_pool);
323    let blob = h_blob_bytes + conv_blob_bytes;
324    let base = max_batch_size * blob;
325    if !spec_on {
326        return base;
327    }
328    let verify: usize = (0..mtp_state_slots)
329        .map(|slot| match rollback {
330            SsmRollbackMode::Snapshot => {
331                verify_slot_h_intermediates(slot, num_drafts, uniform_verify) * h_blob_bytes
332                    + (num_drafts + 1) * conv_blob_bytes
333                    + blob
334            }
335            // Replay keeps ONLY the pre-verify checkpoint blob per slot —
336            // partial accepts are reconstructed by replaying the accepted
337            // tokens from it, so no per-token h/conv snapshots exist. The
338            // verify-window input ring is a SEPARATE term
339            // ([`ssm_replay_ring_bytes`]) because it is sized by activation
340            // rows, not state blobs.
341            SsmRollbackMode::Replay => blob,
342        })
343        .sum();
344    base + verify
345}
346
347/// SSM verify-rollback mode (`--ssm-rollback-mode`, EXPERIMENTAL scaffold).
348///
349/// * `Snapshot` (the serve default, explicit in the CLI): every verify arm
350///   writes per-token h/conv state snapshots; a partial accept restores from
351///   `intermediates[num_accepted - 1]`. This is the only mode whose device
352///   path is wired — its sizing and behavior are pinned byte-for-byte.
353/// * `Replay`: keep ONLY the pre-verify checkpoint blob per verify slot and
354///   cache the verify window's per-token GDN INPUTS (the deinterleaved qkvz
355///   row each conv1d consumes plus the gate/beta row — the tensors the WY
356///   verify kernels read) in a small ring; a partial accept re-runs the
357///   accepted tokens from the checkpoint through the existing sequential
358///   recurrent path. Device wiring (capture + replay) is NOT implemented:
359///   a serve in this mode boots — the reserve shows the capacity win — and
360///   every speculative verify entry refuses loudly
361///   (`SsmStatePool::require_verify_rollback_supported`).
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
363pub enum SsmRollbackMode {
364    Snapshot,
365    Replay,
366}
367
368impl std::str::FromStr for SsmRollbackMode {
369    type Err = String;
370    /// SSOT parse for the `--ssm-rollback-mode` value (CLI validation and
371    /// the serve publication both go through this).
372    fn from_str(s: &str) -> Result<Self, Self::Err> {
373        match s {
374            "snapshot" => Ok(Self::Snapshot),
375            "replay" => Ok(Self::Replay),
376            other => Err(format!(
377                "unknown ssm-rollback-mode '{other}' (valid: snapshot, replay)"
378            )),
379        }
380    }
381}
382
383/// The published rollback mode. Written once from the serve command line
384/// (which carries an EXPLICIT `default_value = "snapshot"`), read by pool
385/// construction and preflight. Same first-write-wins cell pattern as
386/// `gdn_flags`.
387static ROLLBACK_MODE: std::sync::OnceLock<SsmRollbackMode> = std::sync::OnceLock::new();
388
389/// Publish the command line's mode. Returns the value in force (first
390/// write wins, matching `gdn_flags::set_from_cli`).
391pub fn set_ssm_rollback_mode(mode: SsmRollbackMode) -> SsmRollbackMode {
392    let _ = ROLLBACK_MODE.set(mode);
393    *ROLLBACK_MODE.get().expect("just set")
394}
395
396/// The mode in force. `Snapshot` when nothing was published — mirroring the
397/// CLI's explicit default for non-serve contexts (tests, examples), which
398/// never carry the flag. Production sizing/pool call sites take the mode as
399/// a PARAMETER and read this only at the outermost boundary, so unit tests
400/// never depend on the process-global cell.
401pub fn ssm_rollback_mode() -> SsmRollbackMode {
402    *ROLLBACK_MODE.get_or_init(|| SsmRollbackMode::Snapshot)
403}
404
405/// One cached verify-row of GDN inputs for replay, per SSM layer: the
406/// deinterleaved qkvz row (`qkvz_elems` BF16 — what conv1d consumes; Z
407/// included, the gated norm needs it) + the gate/beta row (`nv * 2` FP32).
408/// These are exactly the per-token tensors the WY verify kernels read
409/// (`ConvGdnArgs::deinterleaved` / `gates_buf` rows), and re-running them
410/// through the sequential conv+GDN path from the checkpoint reproduces the
411/// snapshot the dropped intermediates used to hold.
412pub fn ssm_replay_row_bytes(qkvz_elems: usize, nv: usize) -> usize {
413    qkvz_elems * 2 + nv * 2 * 4
414}
415
416/// Replay-mode verify-window input ring: `k_ceiling - 1` cached rows per
417/// covered slot per SSM layer (a partial accept replays at most K-1 tokens
418/// — rows 0..K-2; a full accept replays nothing). Reserved by preflight and
419/// allocated by `SsmStatePool::new` through THIS function so the two cannot
420/// disagree. Zero when speculation is off or the mode is `Snapshot`.
421pub fn ssm_replay_ring_bytes(
422    num_ssm_layers: usize,
423    row_bytes: usize,
424    k_ceiling: usize,
425    mtp_state_slots: usize,
426) -> usize {
427    mtp_state_slots * k_ceiling.saturating_sub(1) * num_ssm_layers * row_bytes
428}
429
430/// Decide the per-sequence decode-rollback ring depth.
431///
432/// `use_speculative` MUST be the same flag `factory::build_model` receives
433/// (`--speculative || --dflash` as plumbed by spark-server) at every call
434/// site, or preflight and allocation diverge.
435pub fn decode_rollback_ring_slots(
436    num_ssm_layers: usize,
437    use_speculative: bool,
438) -> DecodeRingDecision {
439    let watchdogs_value = std::env::var("ATLAS_DISABLE_WATCHDOGS").ok();
440    let watchdogs_disabled = watchdogs_disabled_from_value(watchdogs_value.as_deref());
441    let ring_override = std::env::var("ATLAS_SSM_DECODE_RING").ok();
442    decode_rollback_ring_slots_with(
443        num_ssm_layers,
444        use_speculative,
445        ring_override.as_deref(),
446        watchdogs_disabled,
447    )
448}
449
450fn watchdogs_disabled_from_value(value: Option<&str>) -> bool {
451    value
452        .map(|v| {
453            let v = v.trim().to_ascii_lowercase();
454            v == "1" || v == "true"
455        })
456        .unwrap_or(false)
457}
458
459fn decode_rollback_ring_slots_with(
460    num_ssm_layers: usize,
461    use_speculative: bool,
462    ring_override: Option<&str>,
463    watchdogs_disabled: bool,
464) -> DecodeRingDecision {
465    if num_ssm_layers == 0 {
466        return DecodeRingDecision {
467            slots: 0,
468            skip_reason: None,
469        };
470    }
471    match ring_override {
472        Some("1") => DecodeRingDecision {
473            slots: atlas_kernels::DECODE_ROLLBACK_RING_SLOTS,
474            skip_reason: None,
475        },
476        Some("0") => DecodeRingDecision {
477            slots: 0,
478            skip_reason: None,
479        },
480        _ if use_speculative || watchdogs_disabled => DecodeRingDecision {
481            slots: 0,
482            skip_reason: Some(if use_speculative {
483                "speculative decode active"
484            } else {
485                "watchdogs disabled"
486            }),
487        },
488        _ => DecodeRingDecision {
489            slots: atlas_kernels::DECODE_ROLLBACK_RING_SLOTS,
490            skip_reason: None,
491        },
492    }
493}
494
495/// Outcome of the Marconi snapshot-slot decision.
496///
497/// `skip_reason` is `Some` only for the IMPLICIT skip (prefix caching
498/// inactive) — never for an explicit `--ssm-cache-slots 0` and never for an
499/// `ATLAS_SSM_MARCONI_FULL` override — so the allocating call site can log
500/// the savings exactly once.
501pub struct MarconiSlotDecision {
502    pub slots: usize,
503    pub skip_reason: Option<&'static str>,
504}
505
506/// Number of Marconi SSM-snapshot slots to RESERVE and ALLOCATE.
507///
508/// Two call sites MUST agree on this number, exactly as they must for the
509/// decode-rollback ring above, or a serve either under-reserves (runtime
510/// CUDA alloc failure after weights load) or over-reserves (preflight
511/// refuses a configuration the runtime could fund):
512///
513/// * `spark-server` `preflight_reserve` — sizes the pre-load GPU reserve;
514/// * `TransformerModel::new` (`impl_a1.rs`) — allocates `SsmSnapshotPool`.
515///
516/// WHY a gate exists. The Marconi region's ONLY consumer is the prefix
517/// cache: a slot is written by `prefill_b_save_checkpoint` /
518/// `insert_*_snapshot` and can only ever be READ BACK through a prefix-cache
519/// lookup that returns an `ssm_snapshot` id (`prefix_cache.rs`, "SSM state
520/// snapshot ID at the deepest matched node (Marconi caching)"). Without
521/// `--enable-prefix-caching`, `build_prefix_cache` installs `NoPrefixCaching`
522/// — no radix tree exists, no lookup can ever produce a snapshot id, and
523/// every reserved slot is unreachable for the life of the process. Yet
524/// `--ssm-cache-slots` defaults to **16** and was sized independently of the
525/// flag, so a serve with prefix caching disabled still reserved
526/// `16 × num_ssm_layers × (h_state + conv_state)` bytes that nothing can
527/// restore from.
528///
529/// Measured on GLM-5.3-Flash NVFP4, 2× GB10, TP=2 EP=2, K=3, batch 1,
530/// GMU 0.90: **2380 MiB per rank** — 16 slots × 34 KDA layers ×
531/// (h 4.000 MiB + conv 0.375 MiB). Both widths are FP32 by construction
532/// (`ModelConfig::ssm_h_state_bytes` / `ssm_conv_state_bytes` each end in
533/// `* 4`), and `--ssm-h-dtype f16-pool` is opt-in, so the FP32 figure is
534/// what an ordinary serve reserves AND allocates: `SsmStatePool` reads the
535/// same two accessors (`ssm_pool.rs:182`), so reserve and residency agree.
536/// Confirmed by a paired A/B, same session, 90 s apart, identical flags
537/// (`2 131072 1 0.90`): post-load requirement **13.58 → 11.25 GB**, a
538/// 2.33 GB drop that matches 2380 MiB exactly. The gated default now needs
539/// precisely what the same image required only when an operator passed
540/// `--ssm-cache-slots 0` by hand (ANOMALIES A68).
541///
542/// 🪤 `GLM53-MEMORY-LEDGER-20260830.md` §2/§4 records this region as
543/// "16 slots × 74.4 MB = 1190 MB". That is the FP16-width arithmetic
544/// (h 2.000 + conv 0.1875 MiB/layer) and is exactly half; the same halving
545/// applies to its "SSM live state pool 1 slot × 34 layers = 74 MB" row.
546/// Trust the FP32 figure — it is what the code allocates and what the live
547/// A/B measured.
548///
549/// This is the same defect class the decode ring above already fixed:
550/// a pool reserved unconditionally while nothing could reach it.
551///
552/// Nothing degrades when the slots are dropped. `prefill_b_save_checkpoint`
553/// early-returns on `!ssm_snapshots.is_enabled()`, so there is no work and
554/// no warning spam on the prefill path; the only user-visible difference is
555/// that prefix-cache hits would recompute SSM state — and with the cache
556/// inactive there are no hits.
557///
558/// Env contract (read HERE and nowhere else):
559///
560/// * `ATLAS_SSM_MARCONI_FULL` (PRESENCE, house convention — `=0` is NOT
561///   "off"): restore the old unconditional reservation. Accounting-safe
562///   over-reserve; the kill switch for this diet.
563pub fn marconi_snapshot_slots(
564    requested: usize,
565    prefix_caching_active: bool,
566) -> MarconiSlotDecision {
567    marconi_snapshot_slots_with(requested, prefix_caching_active, marconi_reserve_full())
568}
569
570/// The `ATLAS_SSM_MARCONI_FULL` kill switch (PRESENCE, house convention).
571pub fn marconi_reserve_full() -> bool {
572    std::env::var_os("ATLAS_SSM_MARCONI_FULL").is_some()
573}
574
575/// Pure core of [`marconi_snapshot_slots`] (env-free, unit-testable).
576pub fn marconi_snapshot_slots_with(
577    requested: usize,
578    prefix_caching_active: bool,
579    full_reserve: bool,
580) -> MarconiSlotDecision {
581    if requested == 0 || prefix_caching_active || full_reserve {
582        return MarconiSlotDecision {
583            slots: requested,
584            skip_reason: None,
585        };
586    }
587    MarconiSlotDecision {
588        slots: 0,
589        skip_reason: Some("prefix caching inactive — Marconi snapshot slots are unreachable"),
590    }
591}
592
593/// Whether the prefix cache this serve will actually install is a REAL cache.
594///
595/// SSOT mirror of `spark-server`'s `build_prefix_cache`: the flag alone is
596/// not enough, because a compressed DeepSeek-V4 config downgrades to
597/// `NoPrefixCaching` even with `--enable-prefix-caching` (the cache does not
598/// preserve the compressor pool/ring state required for exact reuse). The
599/// allocating call site asks the constructed cache directly
600/// (`PrefixCache::is_active`); preflight runs before it exists and must
601/// reproduce the same predicate from `args` + `config`.
602pub fn prefix_caching_active(enable_flag: bool, kv_only_prefix_cache_is_safe: bool) -> bool {
603    enable_flag && kv_only_prefix_cache_is_safe
604}
605
606#[cfg(test)]
607#[path = "ssm_reserve_tests.rs"]
608mod mtp_state_slot_tests;