metrale_cache/
radix_tree.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The radix-tree prefix cache (`PrefixCache` implementation):
4//! KV block reuse plus the SSM snapshot index.
5//!
6//! Token sequences are chunked at `block_size`; each tree node stores one KV
7//! block. A lookup walks block-aligned chunks and returns the cached block
8//! indices, then asks the snapshot index for the deepest SSM snapshot within
9//! the matched prefix.
10//!
11//! Owner: cache.
12//! Invariants:
13//! - The tree and the snapshot index are behind separate mutexes, and no
14//!   method holds both: `inner` is released before `snapshot_index` is taken.
15//! - One radix root per `adapter_id`, so no lookup returns another adapter's
16//!   blocks.
17
18use parking_lot::Mutex;
19
20use metrale_telemetry::prefix_cache::{EvictedBlocks, PrefixCache, PrefixMatch};
21
22mod inner;
23mod partial_tail;
24mod snapshot;
25mod snapshot_insert;
26mod snapshot_session;
27mod snapshot_stats;
28mod snapshot_tier;
29
30#[cfg(test)]
31mod tests;
32
33use inner::RadixTreeInner;
34use snapshot::SsmSnapshotIndex;
35
36/// 2026-09-25: FNV-1a hash of the first `count` tokens, the key of an SSM
37/// snapshot. A nonzero `adapter_id` is folded in first, so adapters sharing a
38/// token prefix get different keys; `adapter_id == 0` (the base model) is not
39/// folded in.
40pub(crate) fn hash_token_prefix(tokens: &[u32], count: usize, adapter_id: u64) -> u64 {
41    let mut h: u64 = 0xcbf29ce484222325;
42    if adapter_id != 0 {
43        h ^= adapter_id;
44        h = h.wrapping_mul(0x100000001b3);
45    }
46    for &t in &tokens[..count] {
47        h ^= t as u64;
48        h = h.wrapping_mul(0x100000001b3);
49    }
50    h
51}
52
53/// 2026-09-25: The radix-tree prefix cache. SSM snapshots live in a separate
54/// `SsmSnapshotIndex`, so evicting tree nodes does not remove them.
55pub struct RadixTree {
56    inner: Mutex<RadixTreeInner>,
57    snapshot_index: Mutex<SsmSnapshotIndex>,
58}
59
60impl Default for RadixTree {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl RadixTree {
67    pub fn new() -> Self {
68        Self {
69            inner: Mutex::new(RadixTreeInner::new()),
70            snapshot_index: Mutex::new(SsmSnapshotIndex::new()),
71        }
72    }
73}
74
75impl PrefixCache for RadixTree {
76    fn lookup(
77        &self,
78        tokens: &[u32],
79        block_size: usize,
80        session_hash: u64,
81        adapter_id: u64,
82    ) -> PrefixMatch {
83        // 2026-09-25: Walk the tree under `inner`, then release it.
84        let (matched_blocks, matched_disk_block_ids, matched_tokens) = {
85            let mut inner = self.inner.lock();
86            let (blocks, disk, matched) = inner.walk(tokens, block_size, adapter_id);
87            if matched > 0 {
88                inner.inc_refs(tokens, block_size, matched, adapter_id);
89                metrale_telemetry::prefix_cache::record_cache_hit(matched);
90            } else {
91                metrale_telemetry::prefix_cache::record_cache_miss();
92            }
93            (blocks, disk, matched)
94        };
95        // 2026-09-25: Then the snapshot index. `lookup_tiered` returns the
96        // deepest anchor among resident and spilled entries: a resident hit
97        // fills `ssm_snapshot`, a spilled one `ssm_snapshot_tier_key`, which
98        // the caller faults in.
99        let mut ssm_snapshot = None;
100        let mut ssm_snapshot_tokens = 0;
101        let mut ssm_snapshot_tier_key = None;
102        let mut ssm_snapshot_tier_tokens = 0;
103        let mut ssm_snapshot_is_tail = false;
104        if matched_tokens > 0 {
105            let mut idx = self.snapshot_index.lock();
106            if let Some(m) = idx.lookup_tiered(tokens, matched_tokens, session_hash, adapter_id) {
107                ssm_snapshot_is_tail = m.is_tail;
108                match m.loc {
109                    snapshot::SnapLoc::Hbm(slot) => {
110                        ssm_snapshot = Some(slot);
111                        ssm_snapshot_tokens = m.token_count;
112                    }
113                    snapshot::SnapLoc::Tier(key) => {
114                        ssm_snapshot_tier_key = Some(key);
115                        ssm_snapshot_tier_tokens = m.token_count;
116                    }
117                }
118            }
119        }
120        // 2026-09-25: A list of only `u32::MAX` (no node carries a disk id)
121        // becomes empty, so a caller can test `is_empty()`. A list with any
122        // real id is passed through whole, `u32::MAX` entries included.
123        let matched_disk_block_ids = if matched_disk_block_ids.iter().all(|&id| id == u32::MAX) {
124            Vec::new()
125        } else {
126            matched_disk_block_ids
127        };
128        PrefixMatch {
129            matched_blocks,
130            matched_disk_block_ids,
131            matched_tokens,
132            ssm_snapshot,
133            ssm_snapshot_tokens,
134            ssm_snapshot_tier_key,
135            ssm_snapshot_tier_tokens,
136            ssm_snapshot_is_tail,
137        }
138    }
139
140    fn peek_matched_tokens(&self, tokens: &[u32], block_size: usize, adapter_id: u64) -> usize {
141        self.inner.lock().walk(tokens, block_size, adapter_id).2
142    }
143
144    fn pin_prefix(&self, tokens: &[u32], block_size: usize, adapter_id: u64) -> usize {
145        let mut inner = self.inner.lock();
146        let matched = inner.walk(tokens, block_size, adapter_id).2;
147        if matched > 0 {
148            inner.inc_refs(tokens, block_size, matched, adapter_id);
149        }
150        matched
151    }
152
153    fn insert(
154        &self,
155        tokens: &[u32],
156        block_table: &[u32],
157        disk_block_ids: &[u32],
158        block_size: usize,
159        matched_tokens: usize,
160        adapter_id: u64,
161    ) -> metrale_telemetry::prefix_cache::InsertAcquired {
162        self.inner.lock().insert(
163            tokens,
164            block_table,
165            disk_block_ids,
166            block_size,
167            matched_tokens,
168            adapter_id,
169        )
170    }
171
172    fn insert_with_snapshot(
173        &self,
174        tokens: &[u32],
175        block_table: &[u32],
176        disk_block_ids: &[u32],
177        block_size: usize,
178        snapshot_id: usize,
179        session_hash: u64,
180        matched_tokens: usize,
181        adapter_id: u64,
182    ) -> (
183        Option<usize>,
184        metrale_telemetry::prefix_cache::InsertAcquired,
185    ) {
186        // 2026-09-25: Tree nodes under `inner`, released before the snapshot
187        // index is taken.
188        let newly_acquired = self.inner.lock().insert(
189            tokens,
190            block_table,
191            disk_block_ids,
192            block_size,
193            matched_tokens,
194            adapter_id,
195        );
196        let prefix_hash = hash_token_prefix(tokens, tokens.len(), adapter_id);
197        let mut idx = self.snapshot_index.lock();
198        let displaced = idx.insert(prefix_hash, snapshot_id, session_hash, tokens.len());
199        (displaced, newly_acquired)
200    }
201
202    fn insert_tail_snapshot(
203        &self,
204        tokens: &[u32],
205        snapshot_id: usize,
206        session_hash: u64,
207        adapter_id: u64,
208    ) -> Vec<usize> {
209        // 2026-09-25: Index only; the tree is not touched.
210        let prefix_hash = hash_token_prefix(tokens, tokens.len(), adapter_id);
211        self.snapshot_index
212            .lock()
213            .insert_tail(prefix_hash, snapshot_id, session_hash, tokens.len())
214    }
215
216    fn insert_tail_sibling_snapshot(
217        &self,
218        tokens: &[u32],
219        snapshot_id: usize,
220        session_hash: u64,
221        adapter_id: u64,
222    ) -> Option<usize> {
223        // 2026-09-25: Index only; the tree is not touched.
224        let prefix_hash = hash_token_prefix(tokens, tokens.len(), adapter_id);
225        self.snapshot_index.lock().insert_tail_sibling(
226            prefix_hash,
227            snapshot_id,
228            session_hash,
229            tokens.len(),
230        )
231    }
232
233    fn insert_intermediate_snapshot(
234        &self,
235        tokens: &[u32],
236        _block_table: &[u32],
237        _disk_block_ids: &[u32],
238        _block_size: usize,
239        snapshot_id: usize,
240        session_hash: u64,
241        _matched_tokens: usize,
242        adapter_id: u64,
243    ) -> Option<usize> {
244        // 2026-09-25: Index only, at `tokens.len()`; the tree and its ref
245        // counts are not touched, and the block arguments are unused.
246        let prefix_hash = hash_token_prefix(tokens, tokens.len(), adapter_id);
247        let mut idx = self.snapshot_index.lock();
248        idx.insert(prefix_hash, snapshot_id, session_hash, tokens.len())
249    }
250
251    fn release(&self, tokens: &[u32], block_size: usize, adapter_id: u64) {
252        self.inner
253            .lock()
254            .dec_refs(tokens, block_size, tokens.len(), adapter_id);
255    }
256
257    fn release_matched(
258        &self,
259        tokens: &[u32],
260        block_size: usize,
261        matched_tokens: usize,
262        adapter_id: u64,
263    ) {
264        self.inner
265            .lock()
266            .dec_refs(tokens, block_size, matched_tokens, adapter_id);
267    }
268
269    fn evict(&self, num_blocks: usize) -> EvictedBlocks {
270        let (physical, disk) = self.inner.lock().evict(num_blocks);
271        // 2026-09-25: `u32::MAX` means no disk id, so only real ids are
272        // returned for the caller to `dec_disk_ref`.
273        let disk_block_ids: Vec<u32> = disk.into_iter().filter(|&id| id != u32::MAX).collect();
274        EvictedBlocks {
275            physical,
276            disk_block_ids,
277        }
278    }
279
280    fn evict_snapshot_lru(&self) -> Option<usize> {
281        self.snapshot_index.lock().evict_lru()
282    }
283
284    fn evict_snapshot_to_tier(
285        &self,
286        min_tokens: usize,
287    ) -> Option<metrale_telemetry::prefix_cache::TierEvict> {
288        self.snapshot_index.lock().evict_to_tier(min_tokens)
289    }
290
291    fn promote_snapshot(&self, key: u64, new_slot: usize) -> bool {
292        self.snapshot_index.lock().promote(key, new_slot)
293    }
294
295    fn forget_snapshot_tier_key(&self, key: u64) -> bool {
296        self.snapshot_index.lock().forget_tiered(key)
297    }
298
299    fn snapshot_count(&self) -> usize {
300        self.snapshot_index.lock().len()
301    }
302
303    fn stats(&self) -> (usize, usize) {
304        let inner = self.inner.lock();
305        let entries = inner.num_entries();
306        (entries, entries)
307    }
308}