metrale_circuit/state.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: The state a circuit keeps between steps, declared per block
4//! (`[[block.<name>.state]]`), and the one function that sizes it (M5, LIFECYCLE-DESIGN.md
5//! section 3.3): the recurrent slots of GatedDeltaNet and Mamba2 layers with their verify
6//! intermediates and checkpoints, and the paged KV blocks of attention layers.
7//!
8//! A declaration says what one unit of a state holds: one sequence slot of a recurrent state,
9//! one token of one side of a KV cache. How many units exist (slots, the padding dummy, verify
10//! intermediates per slot, KV blocks) is a plan input ([`StateInputs`]), decided by the engine
11//! and passed in explicitly; this module multiplies and never reads a process global.
12//!
13//! Owner: metrale-circuit.
14//! Invariants:
15//! - Every byte count is `units x elements x element size`, the element count evaluated from
16//! the circuit's dims when it is instantiated; one function ([`StatePlan::new`]) produces
17//! every term, so the reserve and the allocation cannot size a state two ways.
18//! - A format named by a key (`{kv_cache_dtype}`) must be given in [`StateInputs::formats`]; a
19//! missing key is an error, never a default.
20
21use std::collections::BTreeMap;
22
23#[cfg(doc)]
24use crate::ir::Circuit;
25use crate::ir::Section;
26
27#[path = "state_kinds.rs"]
28mod kinds;
29pub use kinds::{Lifetime, StateKind};
30
31/// 2026-09-30: Which verify intermediate count a recurrent state keeps per verify slot.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub enum VerifySteps {
34 /// 2026-09-30: [`VerifyInputs::h_steps`] of the slot (`K - 1`: the state after the last
35 /// row stays live).
36 H,
37 /// 2026-09-30: [`VerifyInputs::conv_steps`] (all `K` windows).
38 Conv,
39}
40
41impl VerifySteps {
42 /// 2026-09-30: The spelling in the circuit TOML (`verify = "h_steps"`).
43 pub fn parse(s: &str) -> Option<Self> {
44 match s {
45 "h_steps" => Some(Self::H),
46 "conv_steps" => Some(Self::Conv),
47 _ => None,
48 }
49 }
50}
51
52/// 2026-09-30: How a node touches a state of its block (`state = { h = "update" }`).
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum StateAccess {
55 /// 2026-09-30: Reads it (attention over the KV cache).
56 Read,
57 /// 2026-09-30: Writes new units without reading them (the KV write of this step's rows).
58 Write,
59 /// 2026-09-30: Reads and replaces it in place (a recurrence, a conv window).
60 Update,
61 /// 2026-09-30: Copies it after each row but the last into the verify intermediates.
62 Snapshot,
63}
64
65impl StateAccess {
66 /// 2026-09-30: The spelling in the circuit TOML.
67 pub fn parse(s: &str) -> Option<Self> {
68 match s {
69 "read" => Some(Self::Read),
70 "write" => Some(Self::Write),
71 "update" => Some(Self::Update),
72 "snapshot" => Some(Self::Snapshot),
73 _ => None,
74 }
75 }
76}
77
78/// 2026-09-30: The storage element of a state.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub enum StateDtype {
81 F32,
82 F16,
83 Bf16,
84 /// 2026-09-30: One E4M3 byte per element; its scales live outside the state (a static
85 /// per-layer KV scale).
86 Fp8,
87 /// 2026-10-02: Integer elements of the caches: masks and flags (`u8`), token ids and
88 /// counts (`i32`, `u32`), keys and device pointers (`u64`).
89 U8,
90 I32,
91 U32,
92 U64,
93}
94
95impl StateDtype {
96 /// 2026-09-30: The spelling in the circuit TOML and in [`StateInputs::formats`].
97 pub fn parse(s: &str) -> Option<Self> {
98 match s {
99 "f32" => Some(Self::F32),
100 "f16" => Some(Self::F16),
101 "bf16" => Some(Self::Bf16),
102 "fp8" => Some(Self::Fp8),
103 "u8" => Some(Self::U8),
104 "i32" => Some(Self::I32),
105 "u32" => Some(Self::U32),
106 "u64" => Some(Self::U64),
107 _ => None,
108 }
109 }
110
111 /// 2026-10-02: The spelling.
112 pub fn name(self) -> &'static str {
113 match self {
114 Self::F32 => "f32",
115 Self::F16 => "f16",
116 Self::Bf16 => "bf16",
117 Self::Fp8 => "fp8",
118 Self::U8 => "u8",
119 Self::I32 => "i32",
120 Self::U32 => "u32",
121 Self::U64 => "u64",
122 }
123 }
124
125 /// 2026-09-30: Bytes per element.
126 pub fn size(self) -> u64 {
127 match self {
128 Self::F32 | Self::I32 | Self::U32 => 4,
129 Self::F16 | Self::Bf16 => 2,
130 Self::Fp8 | Self::U8 => 1,
131 Self::U64 => 8,
132 }
133 }
134}
135
136/// 2026-09-30: A state's storage format: fixed by the circuit, or named by a key the plan
137/// inputs give.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum StateFormat {
140 Fixed(StateDtype),
141 Keyed(String),
142}
143
144impl StateFormat {
145 /// 2026-09-30: `{key}` or a dtype spelling.
146 pub fn parse(s: &str) -> Option<Self> {
147 match s.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
148 Some(key) if !key.is_empty() => Some(Self::Keyed(key.to_string())),
149 Some(_) => None,
150 None => StateDtype::parse(s).map(Self::Fixed),
151 }
152 }
153}
154
155/// 2026-09-30: One declared state of one block instance.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct StateDecl {
158 /// 2026-09-30: `<block prefix>.<local>` (`l0.gdn.h`, `draft.attn.k`).
159 pub id: String,
160 /// 2026-09-30: The template-local id (`h`, `conv`, `k`, `v`).
161 pub local: String,
162 /// 2026-09-30: The block template.
163 pub block: String,
164 /// 2026-09-30: The layer, when the block is a layer's.
165 pub layer: Option<usize>,
166 /// 2026-09-30: Main or draft.
167 pub section: Section,
168 /// 2026-09-30: What it is.
169 pub kind: StateKind,
170 /// 2026-09-30: Storage format.
171 pub format: StateFormat,
172 /// 2026-09-30: Elements per unit (slot or token).
173 pub elements: u64,
174 /// 2026-09-30: The verify intermediates a recurrent state keeps; `None` for a KV side.
175 pub verify: Option<VerifySteps>,
176 /// 2026-09-30: How long a live unit lives ([`Lifetime::of_kind`]).
177 pub lifetime: Lifetime,
178}
179
180/// 2026-09-30: Why a state plan could not be made.
181#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
182pub enum StateError {
183 /// 2026-09-30: A keyed format the inputs do not give.
184 #[error("state `{state}` reads format key `{key}`, which the plan inputs do not give")]
185 MissingFormat {
186 /// 2026-09-30: The state.
187 state: String,
188 /// 2026-09-30: The key.
189 key: String,
190 },
191 /// 2026-09-30: Inputs that do not fit the circuit.
192 #[error("state plan: {0}")]
193 Inputs(String),
194 /// 2026-09-30: A size that does not fit u64.
195 #[error("state `{0}` overflows")]
196 Overflow(String),
197}
198
199/// 2026-09-30: The verify pools of a speculative serve over the recurrent states.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct VerifyInputs {
202 /// 2026-09-30: Per verify slot (the dummy, when there is one, last): the h intermediates it
203 /// holds (`K - 1` for a K-row verify, tiered by the engine's ladder).
204 pub h_steps: Vec<u64>,
205 /// 2026-09-30: Conv intermediates every verify slot holds (all K).
206 pub conv_steps: u64,
207}
208
209/// 2026-09-30: One paged KV cache: blocks of `block_size` tokens.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub struct KvInputs {
212 pub blocks: u64,
213 pub block_size: u64,
214}
215
216/// 2026-09-30: The plan inputs the engine decides.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct StateInputs {
219 /// 2026-09-30: Storage format per key the circuit names (`kv_cache_dtype`, `ssm_h_storage`).
220 pub formats: BTreeMap<String, StateDtype>,
221 /// 2026-09-30: Recurrent slots, the padding dummy included when the plan has one.
222 pub slots: u64,
223 /// 2026-09-30: The speculative verify pools; `None` without speculation.
224 pub verify: Option<VerifyInputs>,
225 /// 2026-09-30: The target's KV cache; `None` leaves the attention states out.
226 pub kv: Option<KvInputs>,
227 /// 2026-09-30: The draft head's own KV cache; `None` leaves the draft's states out.
228 pub draft_kv: Option<KvInputs>,
229}
230
231/// 2026-09-30: What a term of the plan holds.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
233pub enum Holding {
234 /// 2026-09-30: The live state of every slot.
235 Live,
236 /// 2026-09-30: Verify intermediates (the per-row states of a verify).
237 Steps,
238 /// 2026-09-30: The pre-verify checkpoint, one per verify slot.
239 Checkpoint,
240 /// 2026-09-30: KV blocks.
241 Blocks,
242}
243
244impl Holding {
245 /// 2026-09-30: How long this holding of a state declared `declared` lives: the verify
246 /// intermediates and checkpoints only across one verify, the rest as declared.
247 pub fn lifetime(self, declared: Lifetime) -> Lifetime {
248 match self {
249 Holding::Steps | Holding::Checkpoint => Lifetime::Verify,
250 Holding::Live | Holding::Blocks => declared,
251 }
252 }
253}
254
255/// 2026-09-30: One sized term: a state, what it holds, its units and bytes.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct StateTerm {
258 pub state: String,
259 pub holding: Holding,
260 /// 2026-09-30: `holding.lifetime(declared)`.
261 pub lifetime: Lifetime,
262 pub dtype: StateDtype,
263 pub units: u64,
264 pub bytes: u64,
265}
266
267/// 2026-09-30: Every state of a circuit, sized.
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct StatePlan {
270 pub terms: Vec<StateTerm>,
271}
272
273impl StatePlan {
274 /// 2026-09-30: Size every state of `states` (a circuit's [`Circuit::states`], or the
275 /// declarations of one block) under `inputs`. 2026-10-02: Cache kinds
276 /// ([`StateKind::is_cache`]) are left out: `memory::caches` sizes them.
277 pub fn new(states: &[StateDecl], inputs: &StateInputs) -> Result<Self, StateError> {
278 let mut terms = Vec::new();
279 for s in states {
280 let dtype = match &s.format {
281 StateFormat::Fixed(d) => *d,
282 StateFormat::Keyed(k) => {
283 *inputs
284 .formats
285 .get(k)
286 .ok_or_else(|| StateError::MissingFormat {
287 state: s.id.clone(),
288 key: k.clone(),
289 })?
290 }
291 };
292 let unit = s
293 .elements
294 .checked_mul(dtype.size())
295 .ok_or_else(|| StateError::Overflow(s.id.clone()))?;
296 let mut push = |holding, units: u64| -> Result<(), StateError> {
297 let bytes = unit
298 .checked_mul(units)
299 .ok_or_else(|| StateError::Overflow(s.id.clone()))?;
300 terms.push(StateTerm {
301 state: s.id.clone(),
302 holding,
303 lifetime: holding.lifetime(s.lifetime),
304 dtype,
305 units,
306 bytes,
307 });
308 Ok(())
309 };
310 match s.kind {
311 StateKind::Recurrent => {
312 push(Holding::Live, inputs.slots)?;
313 if let Some(v) = &inputs.verify {
314 let steps = match s.verify {
315 Some(VerifySteps::H) => v.h_steps.iter().sum(),
316 Some(VerifySteps::Conv) => v.conv_steps * v.h_steps.len() as u64,
317 None => {
318 return Err(StateError::Inputs(format!(
319 "recurrent state `{}` declares no verify intermediates",
320 s.id
321 )));
322 }
323 };
324 push(Holding::Steps, steps)?;
325 push(Holding::Checkpoint, v.h_steps.len() as u64)?;
326 }
327 }
328 StateKind::PagedKv => {
329 let kv = match s.section {
330 Section::Main => inputs.kv,
331 Section::Draft => inputs.draft_kv,
332 };
333 if let Some(kv) = kv {
334 let tokens = kv
335 .blocks
336 .checked_mul(kv.block_size)
337 .ok_or_else(|| StateError::Overflow(s.id.clone()))?;
338 push(Holding::Blocks, tokens)?;
339 }
340 }
341 // 2026-10-02: Caches are sized by `memory::caches` from their own inputs.
342 StateKind::PrefixSnapshot
343 | StateKind::RingSnapshot
344 | StateKind::CarryStash
345 | StateKind::CarryTable
346 | StateKind::VerifyTable
347 | StateKind::AcceptStash
348 | StateKind::HiddenCapture
349 | StateKind::PromptLookupIndex
350 | StateKind::TokenTreeMask
351 | StateKind::DraftTokens => {}
352 }
353 }
354 Ok(Self { terms })
355 }
356
357 /// 2026-09-30: Total bytes of the terms `keep` selects.
358 pub fn bytes_where(&self, keep: impl Fn(&StateTerm) -> bool) -> u64 {
359 self.terms.iter().filter(|t| keep(t)).map(|t| t.bytes).sum()
360 }
361
362 /// 2026-09-30: Total bytes.
363 pub fn bytes(&self) -> u64 {
364 self.bytes_where(|_| true)
365 }
366}
367
368#[cfg(test)]
369#[path = "state_tests.rs"]
370mod state_tests;