metrale_circuit/memory/
caches.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-10-02: The caches a circuit declares ([`StateKind::is_cache`]), sized: how many units
4//! each kind holds is a plan input ([`CacheInputs`]) the engine decides; the bytes of one unit
5//! are the declaration's elements times its element size. A cache whose input is absent holds
6//! nothing; nothing here guesses a count.
7//!
8//! Owner: metrale-circuit (memory).
9//! Invariants:
10//! - One unit rule per kind ([`units_of`]); every byte count but the prompt-lookup index is
11//!   `units x elements x element size`.
12//! - The prompt-lookup index is a host hash table per sequence: its bytes follow the table's
13//!   growth law ([`hash_table_bytes`]), evaluated at the sequence's history length.
14//! - A keyed format must be given ([`crate::state::StateInputs::formats`]); a missing key is
15//!   an error, as in [`crate::state::StatePlan`].
16
17use std::collections::BTreeMap;
18
19use crate::state::{StateDecl, StateDtype, StateError, StateFormat, StateKind};
20
21/// 2026-10-02: The prompt-lookup index of the sequences in flight.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct LookupInputs {
24    /// 2026-10-02: Sequences holding an index (the concurrency).
25    pub sequences: u64,
26    /// 2026-10-02: Tokens of history each one indexes (prompt plus generated).
27    pub history_tokens: u64,
28}
29
30/// 2026-10-02: How many units each cache kind holds; `None` or 0 for a cache the serve does not
31/// keep.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct CacheInputs {
34    /// 2026-10-02: Prefix-cache (Marconi) snapshot slots.
35    pub prefix_snapshot_slots: u64,
36    /// 2026-10-02: Decode-rollback ring slots per sequence slot, and the sequence slots.
37    pub ring: (u64, u64),
38    /// 2026-10-02: Carried-state verify slots (the dummy included) and carry-table rows; 0
39    /// without the carried-state verify.
40    pub carry: (u64, u64),
41    /// 2026-10-02: Verify-table rows (the WY tables, the accepted-hidden stash); 0 without a
42    /// proposer.
43    pub verify_table_rows: u64,
44    /// 2026-10-02: Hidden rows captured for the draft head.
45    pub capture_rows: u64,
46    /// 2026-10-02: The prompt-lookup index; `None` without prompt-lookup decoding.
47    pub lookup: Option<LookupInputs>,
48    /// 2026-10-02: Token-tree verify slots and nodes per tree; `None` without a token tree.
49    pub tree: Option<(u64, u64)>,
50    /// 2026-10-02: Verify slots and draft positions per slot whose drafted tokens are kept.
51    pub drafts: Option<(u64, u64)>,
52}
53
54/// 2026-10-02: One sized cache declaration.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CacheTerm {
57    /// 2026-10-02: The declaration's id (`l0.gdn.prefix_h`).
58    pub state: String,
59    /// 2026-10-02: Its kind.
60    pub kind: StateKind,
61    /// 2026-10-02: Its element type.
62    pub dtype: StateDtype,
63    /// 2026-10-02: Units held.
64    pub units: u64,
65    /// 2026-10-02: Bytes held.
66    pub bytes: u64,
67    /// 2026-10-02: Held in host memory.
68    pub host: bool,
69}
70
71/// 2026-10-02: The units `inputs` give a cache of `kind`; `None` for a non-cache kind, for the
72/// prompt-lookup index (not linear in its units) and on overflow.
73pub fn units_of(kind: StateKind, inputs: &CacheInputs) -> Option<u64> {
74    match kind {
75        StateKind::Recurrent | StateKind::PagedKv | StateKind::PromptLookupIndex => None,
76        StateKind::PrefixSnapshot => Some(inputs.prefix_snapshot_slots),
77        StateKind::RingSnapshot => inputs.ring.0.checked_mul(inputs.ring.1),
78        StateKind::CarryStash => Some(inputs.carry.0),
79        StateKind::CarryTable => Some(inputs.carry.1),
80        StateKind::VerifyTable | StateKind::AcceptStash => Some(inputs.verify_table_rows),
81        StateKind::HiddenCapture => Some(inputs.capture_rows),
82        StateKind::TokenTreeMask => inputs.tree.map_or(Some(0), |(slots, nodes)| {
83            slots.checked_mul(nodes)?.checked_mul(nodes)
84        }),
85        StateKind::DraftTokens => inputs
86            .drafts
87            .map_or(Some(0), |(slots, pos)| slots.checked_mul(pos)),
88    }
89}
90
91/// 2026-10-02: Bytes of a Rust `HashMap` (hashbrown) holding `entries` entries of
92/// `entry_bytes` each (a multiple of 16), as it grows by inserting them: buckets are the next
93/// power of two of `entries * 8 / 7` (4 or 8 below 8 entries; hashbrown `capacity_to_buckets`),
94/// each bucket one entry plus one control byte, plus one 16-byte control group. 0 entries hold no
95/// allocation.
96pub fn hash_table_bytes(entries: u64, entry_bytes: u64) -> Option<u64> {
97    if entries == 0 {
98        return Some(0);
99    }
100    let buckets = if entries < 4 {
101        4
102    } else if entries < 8 {
103        8
104    } else {
105        (entries.checked_mul(8)? / 7).checked_next_power_of_two()?
106    };
107    buckets
108        .checked_mul(entry_bytes.checked_add(1)?)?
109        .checked_add(16)
110}
111
112/// 2026-10-02: Size every cache declaration of `states` under `inputs`; `formats` resolves keyed
113/// formats.
114pub fn cache_terms(
115    states: &[StateDecl],
116    formats: &BTreeMap<String, StateDtype>,
117    inputs: &CacheInputs,
118) -> Result<Vec<CacheTerm>, StateError> {
119    let mut out = Vec::new();
120    for s in states.iter().filter(|s| s.kind.is_cache()) {
121        let dtype = match &s.format {
122            StateFormat::Fixed(d) => *d,
123            StateFormat::Keyed(k) => *formats.get(k).ok_or_else(|| StateError::MissingFormat {
124                state: s.id.clone(),
125                key: k.clone(),
126            })?,
127        };
128        let over = || StateError::Overflow(s.id.clone());
129        let unit = s.elements.checked_mul(dtype.size()).ok_or_else(over)?;
130        let (units, bytes) = match s.kind {
131            StateKind::PromptLookupIndex => match inputs.lookup {
132                Some(l) => {
133                    let one = hash_table_bytes(l.history_tokens, unit).ok_or_else(over)?;
134                    (
135                        l.sequences.checked_mul(l.history_tokens).ok_or_else(over)?,
136                        l.sequences.checked_mul(one).ok_or_else(over)?,
137                    )
138                }
139                None => (0, 0),
140            },
141            k => {
142                let units = units_of(k, inputs).ok_or_else(over)?;
143                (units, units.checked_mul(unit).ok_or_else(over)?)
144            }
145        };
146        out.push(CacheTerm {
147            state: s.id.clone(),
148            kind: s.kind,
149            dtype,
150            units,
151            bytes,
152            host: s.kind.is_host(),
153        });
154    }
155    Ok(out)
156}
157
158#[cfg(test)]
159#[path = "caches_tests.rs"]
160mod caches_tests;