spark_model/
ssm_reserve.rs

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