metrale_model_layers/ssm_reserve/decode_ring.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Depth of the per-sequence SSM decode-rollback ring: the depth
4//! decision, the published depth, and the fit to free memory.
5//!
6//! Owner: model-layers (SSM reserve).
7//! Invariants:
8//! - The published depth is set at most once per process; later writes leave
9//! the first value in force.
10//! - `parse_decode_ring_slots` accepts only `auto` and
11//! `0..=DECODE_ROLLBACK_RING_SLOTS`.
12//!
13//! The server's preflight reserve (`preflight/decode_ring.rs`, unless
14//! `METRALE_SSM_RESERVE_RING_FULL` is present) and `TransformerModel::new`
15//! (`impl_a1.rs`) both take the depth from [`decode_rollback_ring_slots`].
16//! When preflight fits a smaller depth to free memory, it publishes that depth
17//! through [`set_decode_ring_slots`] so the allocation follows it. The ring is
18//! written by the scheduler's `rollback::snapshot_boundary_if_ssm` and read by
19//! `rollback::rollback_to_boundary`; on a model with SSM layers and a depth of
20//! 0, a rollback declines with `RollbackFallback::NoSsmSnapshot`.
21//!
22//! Environment, read in [`decode_rollback_ring_slots`]:
23//! - `METRALE_SSM_DECODE_RING`: `1` forces the full depth and `0` forces 0,
24//! when no depth is published.
25//! - `METRALE_DISABLE_WATCHDOGS`: `1` or `true` skips the ring when no depth
26//! is published and `METRALE_SSM_DECODE_RING` is not `1`.
27
28/// 2026-09-25: Outcome of the ring-depth decision. `skip_reason` is `Some` only for
29/// the implicit skips (speculative decode, watchdogs disabled), so the
30/// allocating call site can log the saving.
31pub struct DecodeRingDecision {
32 pub slots: usize,
33 pub skip_reason: Option<&'static str>,
34}
35
36/// 2026-09-25: Depths the preflight fit may choose, largest first. The first equals
37/// [`metrale_kernels::DECODE_ROLLBACK_RING_SLOTS`] (8); each next depth halves
38/// the ring's share of the reserve.
39pub const DECODE_RING_FIT_LADDER: [usize; 5] = [8, 4, 2, 1, 0];
40
41/// 2026-09-25: The published depth: an explicit `--ssm-decode-ring-slots N`
42/// (`serve_flags.rs`, before preflight) or the depth preflight fitted. `auto`
43/// publishes nothing, so `METRALE_SSM_DECODE_RING` applies unless preflight
44/// publishes a fitted depth.
45static DECODE_RING_SLOTS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
46
47/// 2026-09-25: Publishes the ring depth and returns the depth in force; the first
48/// write wins. Preflight skips its fit when a depth is already published
49/// ([`published_decode_ring_slots`]), so an explicit depth is not shrunk.
50pub fn set_decode_ring_slots(slots: usize) -> usize {
51 let _ = DECODE_RING_SLOTS.set(slots);
52 *DECODE_RING_SLOTS.get().expect("just set")
53}
54
55/// 2026-09-25: The published depth, or `None`. Reads the cell without setting it.
56pub fn published_decode_ring_slots() -> Option<usize> {
57 DECODE_RING_SLOTS.get().copied()
58}
59
60/// 2026-09-25: Parses `--ssm-decode-ring-slots`: `auto` gives `None` (fit at
61/// preflight), a number up to `DECODE_ROLLBACK_RING_SLOTS` gives `Some`, and
62/// anything else an error. `validate_serve_args` and `publish_kernel_flags`
63/// both parse through this.
64pub fn parse_decode_ring_slots(s: &str) -> Result<Option<usize>, String> {
65 if s == "auto" {
66 return Ok(None);
67 }
68 let n: usize = s
69 .parse()
70 .map_err(|_| format!("unknown ssm-decode-ring-slots '{s}' (valid: auto, 0..=8)"))?;
71 if n > metrale_kernels::DECODE_ROLLBACK_RING_SLOTS {
72 return Err(format!(
73 "ssm-decode-ring-slots {n} exceeds the {} the ring is sized for",
74 metrale_kernels::DECODE_ROLLBACK_RING_SLOTS
75 ));
76 }
77 Ok(Some(n))
78}
79
80/// 2026-09-25: The per-sequence ring depth, from the published depth and the
81/// environment ([`decode_rollback_ring_slots_with`]). Preflight passes
82/// `args.speculative || args.dflash` as `use_speculative`; the allocation must
83/// receive the same flag, or the reserve and the ring differ.
84pub fn decode_rollback_ring_slots(
85 num_ssm_layers: usize,
86 use_speculative: bool,
87) -> DecodeRingDecision {
88 let watchdogs_value = std::env::var("METRALE_DISABLE_WATCHDOGS").ok();
89 let watchdogs_disabled = watchdogs_disabled_from_value(watchdogs_value.as_deref());
90 let ring_override = std::env::var("METRALE_SSM_DECODE_RING").ok();
91 decode_rollback_ring_slots_with(
92 num_ssm_layers,
93 use_speculative,
94 published_decode_ring_slots(),
95 ring_override.as_deref(),
96 watchdogs_disabled,
97 )
98}
99
100/// 2026-09-25: `METRALE_DISABLE_WATCHDOGS` truthiness: `1` or `true`, trimmed and
101/// case-insensitive, as the server's `parse_disable_watchdogs` reads it.
102pub fn watchdogs_disabled_from_value(value: Option<&str>) -> bool {
103 value
104 .map(|v| {
105 let v = v.trim().to_ascii_lowercase();
106 v == "1" || v == "true"
107 })
108 .unwrap_or(false)
109}
110
111/// 2026-09-25: Env-free core of [`decode_rollback_ring_slots`]. Precedence, highest
112/// first:
113///
114/// 1. no SSM layers: 0;
115/// 2. `published`: that depth, also under speculative decode or with watchdogs
116/// disabled;
117/// 3. `ring_override` `"1"` or `"0"`: `DECODE_ROLLBACK_RING_SLOTS` or 0;
118/// 4. speculative decode or watchdogs disabled: 0, with a `skip_reason`;
119/// 5. otherwise `DECODE_ROLLBACK_RING_SLOTS`.
120pub fn decode_rollback_ring_slots_with(
121 num_ssm_layers: usize,
122 use_speculative: bool,
123 published: Option<usize>,
124 ring_override: Option<&str>,
125 watchdogs_disabled: bool,
126) -> DecodeRingDecision {
127 if num_ssm_layers == 0 {
128 return DecodeRingDecision {
129 slots: 0,
130 skip_reason: None,
131 };
132 }
133 if let Some(slots) = published {
134 return DecodeRingDecision {
135 slots,
136 skip_reason: None,
137 };
138 }
139 match ring_override {
140 Some("1") => DecodeRingDecision {
141 slots: metrale_kernels::DECODE_ROLLBACK_RING_SLOTS,
142 skip_reason: None,
143 },
144 Some("0") => DecodeRingDecision {
145 slots: 0,
146 skip_reason: None,
147 },
148 _ if use_speculative || watchdogs_disabled => DecodeRingDecision {
149 slots: 0,
150 skip_reason: Some(if use_speculative {
151 "speculative decode active"
152 } else {
153 "watchdogs disabled"
154 }),
155 },
156 _ => DecodeRingDecision {
157 slots: metrale_kernels::DECODE_ROLLBACK_RING_SLOTS,
158 skip_reason: None,
159 },
160 }
161}
162
163/// 2026-09-25: Largest [`DECODE_RING_FIT_LADDER`] depth, at most `start_slots`, with
164/// `reserve_without_ring + depth * bytes_per_ring_slot <= free_mem` (saturating
165/// arithmetic). `bytes_per_ring_slot` is one depth unit of the ring. Returns 0
166/// also when no depth fits; the caller tells the two apart by comparing the
167/// total against its limit (`preflight/decode_ring.rs`).
168pub fn fit_decode_ring_slots(
169 start_slots: usize,
170 reserve_without_ring: usize,
171 bytes_per_ring_slot: usize,
172 free_mem: usize,
173) -> usize {
174 DECODE_RING_FIT_LADDER
175 .iter()
176 .copied()
177 .filter(|&slots| slots <= start_slots)
178 .find(|&slots| {
179 reserve_without_ring.saturating_add(slots.saturating_mul(bytes_per_ring_slot))
180 <= free_mem
181 })
182 .unwrap_or(0)
183}
184
185#[cfg(test)]
186#[path = "decode_ring_tests.rs"]
187mod decode_ring_tests;