Skip to main content

ferrox_core/
cache.rs

1//! A per-layer KV cache, growable one position at a time during decode.
2//! Two growth strategies exist:
3//!
4//! - `with_pool`: PagedAttention-style block allocation. Many caches
5//!   (one per concurrent request, typically) draw fixed-size blocks
6//!   from one shared, bounded `KvBlockPool` instead of each
7//!   independently pre-committing to a worst-case context length.
8//!   Growth happens in fixed block-sized quanta, and a cache's blocks
9//!   return to the shared pool when it's dropped, so the pool's free
10//!   count is a real, live admission-control signal a caller can check
11//!   before accepting a new request. This is the block-*allocation*
12//!   half of PagedAttention; it does not (yet) change how attention
13//!   reads a cache -- `k`/`v` are still read as one contiguous slice
14//!   per sequence (see `Decoder::forward_token`/`forward_batch`), just
15//!   backed by capacity that grows in block-sized steps instead of
16//!   Rust's default exponential `Vec` growth. Wiring this into
17//!   `ferrox-server` as live per-request admission control via
18//!   `FERROX_KV_POOL_BLOCKS`/`FERROX_KV_POOL_BLOCK_SIZE`.
19
20use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
21
22use crate::kv_swa::KvWindow;
23
24/// Returned by `KvCache::push` (and `with_pool`) when a pool-backed
25/// cache needs another block but its shared `KvBlockPool` has none
26/// free. Caches built with `new`/`with_capacity` never return this --
27/// their growth is unconditional, matching their pre-paging behavior
28/// exactly.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct KvPoolExhausted;
31
32impl std::fmt::Display for KvPoolExhausted {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "KV cache block pool exhausted: no free blocks remain")
35    }
36}
37
38impl std::error::Error for KvPoolExhausted {}
39
40/// A bounded pool of fixed-size KV-cache blocks (in positions) shared
41/// across many `KvCache` instances, typically one pool per server
42/// process. Each `KvCache::with_pool` acquires one block up front and
43/// one more each time it grows past its currently held capacity;
44/// `free_blocks` is therefore a live, accurate admission-control
45/// signal -- a caller can check it before accepting a new request
46/// rather than discovering exhaustion only after committing memory.
47pub struct KvBlockPool {
48    block_size: usize,
49    total_blocks: usize,
50    free_blocks: usize,
51}
52
53impl KvBlockPool {
54    /// `block_size` positions per block, `total_blocks` blocks in the
55    /// whole shared budget (so `block_size * total_blocks` positions
56    /// total, across however many caches draw from this pool at once).
57    pub fn new(block_size: usize, total_blocks: usize) -> Self {
58        assert!(block_size > 0, "block_size must be positive");
59        KvBlockPool {
60            block_size,
61            total_blocks,
62            free_blocks: total_blocks,
63        }
64    }
65
66    pub fn block_size(&self) -> usize {
67        self.block_size
68    }
69
70    pub fn total_blocks(&self) -> usize {
71        self.total_blocks
72    }
73
74    pub fn free_blocks(&self) -> usize {
75        self.free_blocks
76    }
77
78    /// Re-budget the pool.
79    ///
80    /// The pool is an *accounting* budget, not an allocator: each
81    /// `KvCache` owns its own buffer and this counts how many blocks
82    /// the deployment has promised. So a resize is arithmetic, with one
83    /// rule that is not.
84    ///
85    /// Shrinking below what is currently held is REFUSED and the pool
86    /// is left exactly as it was. `free_blocks` would have to go
87    /// negative to represent it, and the alternative -- clamping it to
88    /// zero -- silently over-promises: the caches already holding those
89    /// blocks do not give them back, so every later `try_acquire`
90    /// would be deciding against a budget that does not describe the
91    /// memory in use.
92    ///
93    /// Returns the number of blocks currently held when it refuses, so
94    /// the caller can say what the floor actually is rather than making
95    /// the operator find it by being rejected.
96    pub fn resize(&mut self, total_blocks: usize) -> Result<(), usize> {
97        let in_use = self.total_blocks - self.free_blocks;
98        if total_blocks < in_use {
99            return Err(in_use);
100        }
101        self.free_blocks = total_blocks - in_use;
102        self.total_blocks = total_blocks;
103        Ok(())
104    }
105
106    fn try_acquire(&mut self, n: usize) -> bool {
107        if n <= self.free_blocks {
108            self.free_blocks -= n;
109            true
110        } else {
111            false
112        }
113    }
114
115    fn release(&mut self, n: usize) {
116        self.free_blocks = (self.free_blocks + n).min(self.total_blocks);
117    }
118}
119
120struct PooledState {
121    pool: Arc<Mutex<KvBlockPool>>,
122    block_size: usize,
123    blocks_held: usize,
124}
125
126pub struct KvCache {
127    pub n_kv_heads: usize,
128    /// The K head width. Also the Q head width and the width RoPE
129    /// rotates within (`n_embd_head_k`).
130    pub head_dim: usize,
131    /// The V head width (`n_embd_head_v`). Equal to `head_dim` for every
132    /// architecture but MiMo-V2 (`head_dim: 192, v_head_dim: 128`);
133    /// `ferrox_models::kv_head_dims` is the seam. Every row of `k` is
134    /// `n_kv_heads * head_dim` wide and every row of `v` is
135    /// `n_kv_heads * v_head_dim`, so the two buffers are sized and
136    /// indexed by their OWN width throughout this file.
137    pub v_head_dim: usize,
138    pub k: Vec<f32>, // [rows, n_kv_heads, head_dim], flattened
139    pub v: Vec<f32>, // [rows, n_kv_heads, v_head_dim], flattened
140    /// Positions this sequence has consumed.
141    ///
142    /// **Not the same thing as the number of rows in `k`/`v`**, and the
143    /// distinction is the whole reason this field is private. They are
144    /// equal today because nothing evicts, and they stop being equal
145    /// the moment a windowed layer drops a position behind its window
146    /// (#61): `positions` keeps counting, `rows` does not.
147    ///
148    /// Every reader has to say which one it meant, so there is no
149    /// `seq_len` any more. [`Self::positions`] is what RoPE, a resume
150    /// point and a truncate target mean; [`Self::rows`] is what
151    /// attention iterates and what the bytes cost.
152    ///
153    /// Two bugs have already been caused by the two being one field.
154    /// `PrefillState` read the KV's length as the position to resume at
155    /// (#37), and `DraftModelSpeculator::sync` trusted a counter beside
156    /// a cache that a device-resident backend leaves empty. Both were
157    /// right to read the store rather than keep a copy; both would be
158    /// wrong the day the store evicts.
159    positions: usize,
160    /// The capacity (in positions) this cache was pre-allocated for,
161    /// if any. `None` for caches built with `new` or `with_pool`.
162    planned_capacity: Option<usize>,
163    /// `Some` for caches built with `with_pool`; tracks the shared
164    /// pool and how many blocks this cache currently holds, so its
165    /// blocks can be returned on drop.
166    pool_state: Option<PooledState>,
167    /// `Some` once a windowed layer's cache has been told it may drop
168    /// rows behind its window (#61). `None` -- the default, and what
169    /// every constructor produces -- means this cache keeps every
170    /// position it was ever pushed, which is what every store in this
171    /// engine did before.
172    ///
173    /// Armed by the decoder, never by a constructor, because the fact
174    /// that a layer is windowed lives in `ModelConfig` and the decision
175    /// that eviction is *safe for this run* lives in
176    /// `ferrox_models::decoder::kv_window`. See [`Self::arm_window`].
177    window: Option<KvWindow>,
178    /// A recurrent layer's state (`crate::recurrent_state`), `None` for
179    /// every attention layer and for a recurrent layer's sequence that
180    /// has not run a token yet. Created by the layer's block at the
181    /// size its weights name; cloned with the cache; cleared with it;
182    /// and the reason [`Self::truncate`] can refuse
183    /// ([`Self::can_truncate_to`]). Public because the block that owns
184    /// the geometry lives in another crate.
185    pub recurrent: Option<crate::recurrent_state::RecurrentState>,
186}
187
188/// Cloning a pool-backed cache detaches the clone from pool accounting
189/// (its `k`/`v`/`seq_len` data is copied normally, but the clone does
190/// not hold or later release any blocks itself) -- mirroring how
191/// `ferrox-models::prefix_cache` already uses `KvCache::clone` to fork
192/// a cached prefix into a new, independent request's cache. Only the
193/// original cache's blocks are released, exactly once, when it drops.
194impl Clone for KvCache {
195    fn clone(&self) -> Self {
196        KvCache {
197            n_kv_heads: self.n_kv_heads,
198            head_dim: self.head_dim,
199            v_head_dim: self.v_head_dim,
200            k: self.k.clone(),
201            v: self.v.clone(),
202            positions: self.positions,
203            planned_capacity: self.planned_capacity,
204            pool_state: None,
205            // Carried, not reset: a clone of a cache that has already
206            // dropped rows is a cache that has already dropped rows,
207            // and pretending otherwise would let the clone's
208            // `positions` be read as a row count again.
209            window: self.window,
210            recurrent: self.recurrent.clone(),
211        }
212    }
213}
214
215impl Drop for KvCache {
216    fn drop(&mut self) {
217        if let Some(state) = &self.pool_state {
218            if let Ok(mut pool) = state.pool.lock() {
219                pool.release(state.blocks_held);
220            }
221        }
222    }
223}
224
225impl KvCache {
226    /// Positions this sequence has consumed: what RoPE means, what a
227    /// resume point means, and what a truncate target is measured in.
228    ///
229    /// Monotonic except through [`Self::truncate`] and [`Self::clear`].
230    /// Equal to [`Self::rows`] today, and deliberately a different
231    /// method so it stops being equal safely (#61).
232    #[inline]
233    pub fn positions(&self) -> usize {
234        self.positions
235    }
236
237    /// Sets the position counter directly, for a constructor that
238    /// filled `k`/`v` by hand rather than through `push`.
239    ///
240    /// Deliberately narrow and deliberately not `pub`: the only honest
241    /// caller is one that has just written exactly this many rows, and
242    /// a public setter on a counter the buffer should imply is how the
243    /// two drift apart again.
244    pub(crate) fn set_positions(&mut self, positions: usize) {
245        debug_assert_eq!(
246            positions,
247            self.rows(),
248            "set_positions must agree with the rows just written"
249        );
250        self.positions = positions;
251    }
252
253    /// Test-only: sets the position counter WITHOUT the agreement check
254    /// [`Self::set_positions`] makes.
255    ///
256    /// Exists for one caller: `kv_signature`'s test that a serialized
257    /// payload whose declared count contradicts its buffers is
258    /// rejected. That contradiction is the thing under test, so it has
259    /// to be constructible, and it must not be constructible anywhere
260    /// else.
261    #[cfg(test)]
262    pub(crate) fn force_positions_for_test(&mut self, positions: usize) {
263        self.positions = positions;
264    }
265
266    /// Rows of K/V actually resident: what attention iterates over and
267    /// what the memory costs.
268    ///
269    /// Derived from the buffer rather than counted alongside it, so it
270    /// cannot drift from what is really there. That is the same rule
271    /// the batched prefill learned in #37: read the cursor, do not keep
272    /// a copy of it.
273    #[inline]
274    pub fn rows(&self) -> usize {
275        let k_width = self.k_width();
276        if k_width == 0 {
277            return 0;
278        }
279        self.k.len() / k_width
280    }
281
282    /// One position's K row: `n_kv_heads * head_dim` elements.
283    #[inline]
284    pub fn k_width(&self) -> usize {
285        self.n_kv_heads * self.head_dim
286    }
287
288    /// One position's V row: `n_kv_heads * v_head_dim` elements.
289    #[inline]
290    pub fn v_width(&self) -> usize {
291        self.n_kv_heads * self.v_head_dim
292    }
293
294    /// A cache whose K and V heads share one width -- every architecture
295    /// but MiMo-V2. [`Self::new_split`] is the general form.
296    pub fn new(n_kv_heads: usize, head_dim: usize) -> Self {
297        Self::new_split(n_kv_heads, head_dim, head_dim)
298    }
299
300    pub fn new_split(n_kv_heads: usize, head_dim: usize, v_head_dim: usize) -> Self {
301        KvCache {
302            n_kv_heads,
303            head_dim,
304            v_head_dim,
305            k: Vec::new(),
306            v: Vec::new(),
307            positions: 0,
308            planned_capacity: None,
309            pool_state: None,
310            window: None,
311            recurrent: None,
312        }
313    }
314
315    /// Pre-allocates storage for up to `max_seq_len` positions, so
316    /// `push` never triggers a reallocation-and-copy during decode.
317    /// Use this when the maximum context length is known ahead of time
318    pub fn with_capacity(n_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
319        Self::with_capacity_split(n_kv_heads, head_dim, head_dim, max_seq_len)
320    }
321
322    /// [`Self::with_capacity`] with a V head width of its own.
323    pub fn with_capacity_split(
324        n_kv_heads: usize,
325        head_dim: usize,
326        v_head_dim: usize,
327        max_seq_len: usize,
328    ) -> Self {
329        KvCache {
330            n_kv_heads,
331            head_dim,
332            v_head_dim,
333            k: Vec::with_capacity(max_seq_len * n_kv_heads * head_dim),
334            v: Vec::with_capacity(max_seq_len * n_kv_heads * v_head_dim),
335            positions: 0,
336            planned_capacity: Some(max_seq_len),
337            pool_state: None,
338            window: None,
339            recurrent: None,
340        }
341    }
342
343    /// Acquires up front however many blocks from `pool` are needed to
344    /// cover `max_seq_len` positions (at least one, even if
345    /// `max_seq_len` is `0`), so a caller that knows its worst-case
346    /// sequence length ahead of time (as `ferrox-server` does: prompt
347    /// length + `max_tokens`) never needs to acquire another block
348    /// mid-decode. This matters beyond performance: `push` growing past
349    /// its currently held capacity can fail if the pool is exhausted by
350    /// *other* requests by then, and callers like
351    /// `ferrox_models::Decoder::forward_token` treat `push` as
352    /// infallible for non-pooled caches -- a pooled cache that
353    /// under-reserves at construction and then fails to grow later
354    /// would violate that assumption and panic mid-decode. Sizing to
355    /// `max_seq_len` up front turns that into an admission-control
356    /// decision made once, honestly, before any generation work starts,
357    /// exactly mirroring `with_capacity`'s worst-case pre-allocation --
358    /// just drawn from a shared pool instead of a private allocation.
359    /// Returns `Err(KvPoolExhausted)` without mutating anything if the
360    /// pool doesn't have that many blocks free.
361    pub fn with_pool(
362        n_kv_heads: usize,
363        head_dim: usize,
364        pool: Arc<Mutex<KvBlockPool>>,
365        max_seq_len: usize,
366    ) -> Result<Self, KvPoolExhausted> {
367        Self::with_pool_split(n_kv_heads, head_dim, head_dim, pool, max_seq_len)
368    }
369
370    /// [`Self::with_pool`] with a V head width of its own.
371    pub fn with_pool_split(
372        n_kv_heads: usize,
373        head_dim: usize,
374        v_head_dim: usize,
375        pool: Arc<Mutex<KvBlockPool>>,
376        max_seq_len: usize,
377    ) -> Result<Self, KvPoolExhausted> {
378        let block_size = pool.lock().unwrap().block_size();
379        let blocks_needed = max_seq_len.div_ceil(block_size).max(1);
380        if !pool.lock().unwrap().try_acquire(blocks_needed) {
381            return Err(KvPoolExhausted);
382        }
383        Ok(KvCache {
384            n_kv_heads,
385            head_dim,
386            v_head_dim,
387            k: Vec::with_capacity(blocks_needed * block_size * n_kv_heads * head_dim),
388            v: Vec::with_capacity(blocks_needed * block_size * n_kv_heads * v_head_dim),
389            positions: 0,
390            planned_capacity: None,
391            pool_state: Some(PooledState {
392                pool,
393                block_size,
394                blocks_held: blocks_needed,
395            }),
396            window: None,
397            recurrent: None,
398        })
399    }
400
401    /// Appends one position's key/value vectors (each
402    /// `n_kv_heads * head_dim` long) to the cache. For pool-backed
403    /// caches, this may need to acquire another block first; if the
404    /// shared pool has none free, no data is appended and
405    /// `Err(KvPoolExhausted)` is returned. Caches built with `new` or
406    /// `with_capacity` always return `Ok`.
407    pub fn push(&mut self, k_step: &[f32], v_step: &[f32]) -> Result<(), KvPoolExhausted> {
408        assert_eq!(k_step.len(), self.k_width());
409        assert_eq!(v_step.len(), self.v_width());
410
411        let (k_width, v_width) = (self.k_width(), self.v_width());
412        if let Some(state) = &mut self.pool_state {
413            let capacity_positions = self.k.capacity() / k_width;
414            // ROWS, not positions: this asks whether the buffer is
415            // full, and an evicting cache's buffer is shorter than its
416            // position count. Equal for a cache that never evicts.
417            let rows = self.k.len() / k_width;
418            if rows == capacity_positions {
419                if !state.pool.lock().unwrap().try_acquire(1) {
420                    return Err(KvPoolExhausted);
421                }
422                state.blocks_held += 1;
423                self.k.reserve_exact(state.block_size * k_width);
424                self.v.reserve_exact(state.block_size * v_width);
425            }
426        }
427
428        self.k.extend_from_slice(k_step);
429        self.v.extend_from_slice(v_step);
430        self.positions += 1;
431        Ok(())
432    }
433
434    /// Advance length by `n` positions without storing real K/V values
435    /// (zero-fill). Used when Metal owns the KV plane and the host cache
436    /// only needs matching `seq_len` for sync checks.
437    pub fn advance_len(&mut self, n: usize) -> Result<(), KvPoolExhausted> {
438        if n == 0 {
439            return Ok(());
440        }
441        let k_zeros = vec![0f32; self.k_width()];
442        let v_zeros = vec![0f32; self.v_width()];
443        for _ in 0..n {
444            self.push(&k_zeros, &v_zeros)?;
445        }
446        Ok(())
447    }
448
449    /// Returns this cache's blocks to its shared pool immediately
450    /// (rather than waiting for `Drop`) and detaches it from pool
451    /// accounting; a no-op for caches that aren't pool-backed, and
452    /// idempotent if called more than once.
453    pub fn release_to_pool(&mut self) {
454        if let Some(state) = self.pool_state.take() {
455            if let Ok(mut pool) = state.pool.lock() {
456                pool.release(state.blocks_held);
457            }
458        }
459    }
460
461    pub fn clear(&mut self) {
462        self.k.clear();
463        self.v.clear();
464        self.positions = 0;
465        self.recurrent = None;
466    }
467
468    /// Whether [`Self::truncate`] to `new_seq_len` is possible.
469    ///
470    /// Always, for an attention layer's cache: its rows are a history.
471    /// For a cache holding a recurrent state only to zero (the state
472    /// resets) or to where it already is: the state is a reduction
473    /// over the whole prefix and has no "one token ago"
474    /// (`crate::recurrent_state`). Every caller that rolls a cache back
475    /// to a middle position asks this first, or is fenced off the model.
476    pub fn can_truncate_to(&self, new_seq_len: usize) -> bool {
477        self.recurrent.is_none() || new_seq_len == 0 || new_seq_len == self.positions
478    }
479
480    /// Rolls the cache back to exactly `new_seq_len` positions,
481    /// discarding everything after. Used to reject speculatively
482    /// decoded draft tokens that turned out wrong: their K/V were
483    /// already pushed during batched verification, and rejection means
484    /// removing them so the next real decode step continues from the
485    /// last *accepted* position, not the last *attempted* one.
486    /// Rolls the cache back to exactly `new_seq_len` POSITIONS.
487    ///
488    /// A windowed cache can only roll back into rows it still holds. A
489    /// target further back than [`Self::rows`] names a position this
490    /// cache dropped behind its window, and there is no honest thing to
491    /// return for it -- so it stops, rather than silently rolling back
492    /// to the oldest row it happens to have and answering the next
493    /// token out of a history with a hole in it.
494    ///
495    /// Unreachable for a cache that has not been armed by
496    /// [`Self::arm_window`], which is every cache unless
497    /// `FERROX_KV_WINDOW` is on: `rows == positions` there, so the
498    /// second precondition is implied by the first.
499    pub fn truncate(&mut self, new_seq_len: usize) {
500        assert!(
501            new_seq_len <= self.positions,
502            "truncate target {new_seq_len} must not exceed current seq_len {}",
503            self.positions
504        );
505        let rows = self.rows();
506        let dropped = self.positions - new_seq_len;
507        assert!(
508            dropped <= rows,
509            "truncate target {new_seq_len} is {dropped} positions back but only {rows} rows \
510             are resident: this cache evicted behind a {:?} window (#61). Turn off \
511             FERROX_KV_WINDOW for a workload that rolls the KV cache back this far.",
512            self.window.map(|w| w.window())
513        );
514        assert!(
515            self.can_truncate_to(new_seq_len),
516            "truncate target {new_seq_len} on a cache holding a recurrent state at {} positions: \
517             a Mamba state is a reduction over the whole prefix and cannot be rolled back to a \
518             middle position (`crate::recurrent_state`); the caller should have asked \
519             `can_truncate_to` or been fenced off a recurrent model",
520            self.positions
521        );
522        if new_seq_len == 0 {
523            self.recurrent = None;
524        }
525        let keep_rows = rows - dropped;
526        self.k.truncate(keep_rows * self.k_width());
527        self.v.truncate(keep_rows * self.v_width());
528        self.positions = new_seq_len;
529    }
530
531    /// Tells this cache it may drop rows that have fallen behind
532    /// `window` (#61 step 2).
533    ///
534    /// Arming alone drops nothing: [`Self::evict_behind_window`] is what
535    /// drops, and the holder calls it at a point where it knows nothing
536    /// is mid-read. That split is deliberate. `push` is the obvious
537    /// place to evict and it is the wrong one: `Decoder::forward_batch`
538    /// writes a whole prefill batch into the cache and only then reads
539    /// it back, against a row offset it captured BEFORE the writes.
540    /// Evicting inside `push` would move every row out from under that
541    /// offset, and the prompt would be attended over shifted keys. So
542    /// eviction is something the holder asks for, and asking for it too
543    /// rarely only costs memory -- over-retention is always correct,
544    /// under-retention is wrong logits.
545    ///
546    /// Idempotent; a second call with a different window replaces the
547    /// first, and rows already dropped stay dropped.
548    pub fn arm_window(&mut self, window: KvWindow) {
549        self.window = Some(window);
550    }
551
552    /// The window this cache evicts behind, or `None` if it keeps
553    /// everything.
554    pub fn window(&self) -> Option<KvWindow> {
555        self.window
556    }
557
558    /// Drops rows that have fallen behind the armed window, returning
559    /// how many rows went. Zero, always, for an unarmed cache.
560    ///
561    /// The rows dropped are the OLDEST ones, so what remains is still a
562    /// contiguous suffix of the sequence: row `i` of `rows` holds
563    /// absolute position `positions - rows + i`. Every windowed
564    /// attention kernel reads the last `window` rows and nothing else,
565    /// and [`KvWindow::rows_after`] guarantees at least that many
566    /// survive, so the set of rows a kernel reads is byte-for-byte the
567    /// set it would have read with no eviction at all.
568    pub fn evict_behind_window(&mut self) -> usize {
569        let Some(window) = self.window else {
570            return 0;
571        };
572        let (k_width, v_width) = (self.k_width(), self.v_width());
573        if k_width == 0 {
574            return 0;
575        }
576        let rows = self.k.len() / k_width;
577        let keep = window.rows_after(self.positions).min(rows);
578        let drop_rows = rows - keep;
579        if drop_rows == 0 {
580            return 0;
581        }
582        // One `drain` per eviction, not one per token: the whole reason
583        // `KvWindow` carries slack. This moves `keep` rows down, and it
584        // happens once every `slack + 1` positions.
585        self.k.drain(..drop_rows * k_width);
586        self.v.drain(..drop_rows * v_width);
587        // `drain` frees no memory, and the saving this exists for is
588        // memory. A cache that just absorbed a 32k-token prefill holds
589        // 32k rows of capacity behind `window + slack` rows of data
590        // until something hands it back. Only when the excess is large
591        // enough to be worth a realloc-and-copy: shrinking on every
592        // eviction would trade the block drain for a full copy.
593        //
594        // **Never for a pool-backed cache**, and the reason is a bug
595        // rather than a preference. `push` asks whether the buffer is
596        // full by comparing rows against `k.capacity()`, and takes
597        // another block from the shared pool when they meet. Shrinking
598        // the buffer to a window's worth would make that condition true
599        // every `slack + 1` tokens forever, so a windowed pooled cache
600        // would draw a fresh block from the pool on a cadence, hold
601        // every one of them until it drops, and exhaust the pool
602        // mid-answer -- where `push` is documented infallible and the
603        // caller panics. The pool has already promised this cache its
604        // blocks; handing the capacity back without handing the blocks
605        // back saves nothing and costs that.
606        if self.pool_state.is_none() {
607            let want_k = window.max_rows() * k_width;
608            if self.k.capacity() > want_k.saturating_mul(2) {
609                self.k.shrink_to(want_k);
610                self.v.shrink_to(window.max_rows() * v_width);
611            }
612        }
613        drop_rows
614    }
615
616    /// Bytes currently resident for this cache's K and V buffers
617    /// combined (actual allocated capacity, not just used length) --
618    /// the number that matters for "does this fit in the context
619    /// budget,"
620    pub fn allocated_bytes(&self) -> usize {
621        (self.k.capacity() + self.v.capacity()) * std::mem::size_of::<f32>()
622    }
623
624    /// True if this cache was pre-allocated via `with_capacity` and
625    /// has not yet grown past that planned capacity (i.e. `push` has
626    /// never had to reallocate). Useful for tests/diagnostics
627    /// confirming the pre-allocation path actually avoided reallocs.
628    pub fn is_within_planned_capacity(&self) -> bool {
629        match self.planned_capacity {
630            Some(cap) => {
631                // POSITIONS against the plan (the plan was made in
632                // positions), ROWS against the buffer (the buffer holds
633                // rows). Equal unless this cache evicts.
634                self.positions <= cap && self.k.capacity() >= self.rows() * self.k_width()
635            }
636            None => false,
637        }
638    }
639}
640
641/// The other half of PagedAttention that `KvBlockPool`/`KvCache::with_pool`
642/// deliberately don't implement (see this module's doc comment): real,
643/// *shared* physical block storage that many sequences' block tables can
644/// address into, instead of each `KvCache` still owning its own private,
645/// contiguous `Vec`. `KvBlockPool` only ever bounds a *count* of blocks
646/// each cache may grow to; `PagedKvStore` is the actual backing memory,
647/// and a sequence's `PagedKvCache` holds a block table (an ordered list
648/// of block IDs into this shared store) instead of owning K/V data
649/// directly. This is what makes non-contiguous-block reads during
650/// attention (`causal_gqa_attention_paged`, in `attention.rs`) possible
651/// at all -- `causal_gqa_attention`'s existing contiguous-slice read
652/// pattern has no way to express "position 37 lives in block 12, cached
653/// out of order relative to block 5."
654pub struct PagedKvStore {
655    block_size: usize,
656    n_kv_heads: usize,
657    /// K head width; see [`KvCache::head_dim`].
658    head_dim: usize,
659    /// V head width; see [`KvCache::v_head_dim`].
660    v_head_dim: usize,
661    k: Vec<f32>, // [total_blocks * block_size, n_kv_heads, head_dim], flattened
662    v: Vec<f32>, // [total_blocks * block_size, n_kv_heads, v_head_dim], flattened
663    free_block_ids: Vec<usize>,
664}
665
666impl PagedKvStore {
667    /// One width for K and V; [`Self::new_split`] is the general form.
668    pub fn new(block_size: usize, total_blocks: usize, n_kv_heads: usize, head_dim: usize) -> Self {
669        Self::new_split(block_size, total_blocks, n_kv_heads, head_dim, head_dim)
670    }
671
672    pub fn new_split(
673        block_size: usize,
674        total_blocks: usize,
675        n_kv_heads: usize,
676        head_dim: usize,
677        v_head_dim: usize,
678    ) -> Self {
679        assert!(block_size > 0, "block_size must be positive");
680        PagedKvStore {
681            block_size,
682            n_kv_heads,
683            head_dim,
684            v_head_dim,
685            k: vec![0.0; total_blocks * block_size * n_kv_heads * head_dim],
686            v: vec![0.0; total_blocks * block_size * n_kv_heads * v_head_dim],
687            // Pushed in descending order so `pop()` hands out ascending
688            // block IDs -- not load-bearing for correctness (any free ID
689            // works), just makes manual debugging/inspection saner.
690            free_block_ids: (0..total_blocks).rev().collect(),
691        }
692    }
693
694    pub fn block_size(&self) -> usize {
695        self.block_size
696    }
697
698    pub fn free_block_count(&self) -> usize {
699        self.free_block_ids.len()
700    }
701
702    pub fn n_kv_heads(&self) -> usize {
703        self.n_kv_heads
704    }
705
706    pub fn head_dim(&self) -> usize {
707        self.head_dim
708    }
709
710    pub fn v_head_dim(&self) -> usize {
711        self.v_head_dim
712    }
713
714    /// One position's K row: `n_kv_heads * head_dim` elements.
715    pub fn k_width(&self) -> usize {
716        self.n_kv_heads * self.head_dim
717    }
718
719    /// One position's V row: `n_kv_heads * v_head_dim` elements.
720    pub fn v_width(&self) -> usize {
721        self.n_kv_heads * self.v_head_dim
722    }
723
724    fn acquire_block(&mut self) -> Option<usize> {
725        self.free_block_ids.pop()
726    }
727
728    fn release_block(&mut self, id: usize) {
729        self.free_block_ids.push(id);
730    }
731
732    fn k_elems_per_block(&self) -> usize {
733        self.block_size * self.k_width()
734    }
735
736    fn v_elems_per_block(&self) -> usize {
737        self.block_size * self.v_width()
738    }
739
740    /// One position's K (or V) row within block `id` at `offset` (0-based
741    /// within the block) -- `[n_kv_heads * head_dim]` long. Used by
742    /// `causal_gqa_attention_paged` to read attention inputs directly out
743    /// of shared physical storage via a block table, and by
744    /// `PagedKvCache::push` to write a new position into it.
745    pub fn k_row(&self, id: usize, offset: usize) -> &[f32] {
746        let width = self.k_width();
747        let start = id * self.k_elems_per_block() + offset * width;
748        &self.k[start..start + width]
749    }
750
751    pub fn v_row(&self, id: usize, offset: usize) -> &[f32] {
752        let width = self.v_width();
753        let start = id * self.v_elems_per_block() + offset * width;
754        &self.v[start..start + width]
755    }
756
757    fn k_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
758        let width = self.k_width();
759        let start = id * self.k_elems_per_block() + offset * width;
760        &mut self.k[start..start + width]
761    }
762
763    fn v_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
764        let width = self.v_width();
765        let start = id * self.v_elems_per_block() + offset * width;
766        &mut self.v[start..start + width]
767    }
768}
769
770/// Returned when a `PagedKvCache` needs another block but its
771/// `PagedKvStore` has none free -- the paged-storage analog of
772/// `KvPoolExhausted`.
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub struct PagedStoreExhausted;
775
776impl std::fmt::Display for PagedStoreExhausted {
777    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
778        write!(f, "paged KV store exhausted: no free blocks remain")
779    }
780}
781
782impl std::error::Error for PagedStoreExhausted {}
783
784/// One sequence's view into a shared `PagedKvStore`: a block table
785/// (which physical blocks this sequence's positions live in, in order)
786/// plus how many positions have been written so far. Unlike `KvCache`,
787/// this holds no K/V data itself -- every read and write goes through
788/// the shared store.
789#[derive(Debug, Clone, Default)]
790pub struct PagedKvCache {
791    block_table: Vec<usize>,
792    seq_len: usize,
793    /// A recurrent layer's state, per sequence and NOT paged: the store
794    /// holds rows and a Mamba layer has none. Same rules as
795    /// `KvCache::recurrent`.
796    pub recurrent: Option<crate::recurrent_state::RecurrentState>,
797}
798
799impl PagedKvCache {
800    pub fn new() -> Self {
801        PagedKvCache {
802            block_table: Vec::new(),
803            seq_len: 0,
804            recurrent: None,
805        }
806    }
807
808    /// See `KvCache::can_truncate_to`.
809    pub fn can_truncate_to(&self, new_seq_len: usize) -> bool {
810        self.recurrent.is_none() || new_seq_len == 0 || new_seq_len == self.seq_len
811    }
812
813    pub fn seq_len(&self) -> usize {
814        self.seq_len
815    }
816
817    pub fn block_table(&self) -> &[usize] {
818        &self.block_table
819    }
820
821    /// Appends one position's key/value vectors, acquiring a new block
822    /// from `store` first if the current tail block is full (or none
823    /// held yet). Mirrors `KvCache::push`'s signature/semantics exactly,
824    /// just against shared storage instead of a private buffer.
825    pub fn push(
826        &mut self,
827        store: &mut PagedKvStore,
828        k_step: &[f32],
829        v_step: &[f32],
830    ) -> Result<(), PagedStoreExhausted> {
831        let block_size = store.block_size();
832        let offset_in_block = self.seq_len % block_size;
833        // Index by position rather than taking the tail block: a
834        // sequence that pre-reserved (see `reserve`) already holds the
835        // block this position belongs in, and appending another would
836        // both leak a block and write the row in the wrong place.
837        let block_index = self.seq_len / block_size;
838        if block_index >= self.block_table.len() {
839            let id = store.acquire_block().ok_or(PagedStoreExhausted)?;
840            self.block_table.push(id);
841        }
842        let block_id = self.block_table[block_index];
843        store
844            .k_row_mut(block_id, offset_in_block)
845            .copy_from_slice(k_step);
846        store
847            .v_row_mut(block_id, offset_in_block)
848            .copy_from_slice(v_step);
849        self.seq_len += 1;
850        Ok(())
851    }
852
853    /// Appends a block the caller already owns, without taking one from
854    /// the store.
855    ///
856    /// This is how a sliding window recycles. A block whose positions
857    /// have fallen behind the window is never read again -- the paged
858    /// attention kernel indexes `block_table[t / block_size]` only for
859    /// `t >= seq_len - window` -- so its storage can back a *later*
860    /// position instead of being handed back and re-acquired. The table
861    /// keeps its absolute-position indexing and simply names the same
862    /// physical block at two indices: the stale one, which nothing
863    /// reads, and the live one.
864    ///
865    /// That aliasing is the reason this is a separate method rather than
866    /// a flag on [`Self::reserve`]. A caller that recycles owns the
867    /// obligation to release each distinct block exactly once, and to
868    /// have established that the donor index really is out of window --
869    /// neither of which this type can check for itself.
870    pub fn append_block(&mut self, block_id: usize) {
871        self.block_table.push(block_id);
872    }
873
874    /// Releases every block this sequence holds back to `store`. Must be
875    /// called explicitly (there's no `Drop` here, since dropping needs a
876    /// `&mut PagedKvStore` this type doesn't own a reference to) --
877    /// mirrors `KvCache::release_to_pool`, just not automatic.
878    ///
879    /// Each *distinct* block once: a table that has recycled through
880    /// [`Self::append_block`] names one block at more than one index, and
881    /// releasing per index would put the same id on the free list twice,
882    /// after which two sequences are handed the same memory.
883    pub fn release(&mut self, store: &mut PagedKvStore) {
884        let mut seen: Vec<usize> = Vec::new();
885        for id in self.block_table.drain(..) {
886            if !seen.contains(&id) {
887                seen.push(id);
888                store.release_block(id);
889            }
890        }
891        self.seq_len = 0;
892        self.recurrent = None;
893    }
894
895    /// How many *additional* blocks appending `n_new` positions would
896    /// take from `store`, given what this sequence already holds.
897    ///
898    /// Counted against held CAPACITY rather than against `seq_len`, so
899    /// it is right in both cases. The tail block is usually part-full,
900    /// so the answer is never simply `n_new / block_size`: positions
901    /// that land in a block already held cost nothing. And a sequence
902    /// that pre-reserved (see [`Self::reserve`]) holds blocks beyond
903    /// its length, which a `seq_len`-only sum would ask for twice.
904    ///
905    /// Callers that must not fail part-way through a write check this
906    /// against [`PagedKvStore::free_block_count`] before touching
907    /// anything.
908    pub fn blocks_needed_for(&self, store: &PagedKvStore, n_new: usize) -> usize {
909        let held_capacity = self.block_table.len() * store.block_size();
910        let unused = held_capacity.saturating_sub(self.seq_len);
911        n_new.saturating_sub(unused).div_ceil(store.block_size())
912    }
913
914    /// Takes the blocks `n_new` more positions will need, without
915    /// advancing `seq_len`.
916    ///
917    /// This is what makes a multi-layer append all-or-nothing. The
918    /// check and the taking happen together, so every later
919    /// [`Self::push`] writes into a block this sequence already owns
920    /// and cannot fail. Reserving and then not filling is harmless: the
921    /// blocks are this sequence's until it releases, and `seq_len`
922    /// still says how far it really got.
923    pub fn reserve(
924        &mut self,
925        store: &mut PagedKvStore,
926        n_new: usize,
927    ) -> Result<(), PagedStoreExhausted> {
928        let need = self.blocks_needed_for(store, n_new);
929        if need > store.free_block_count() {
930            return Err(PagedStoreExhausted);
931        }
932        for _ in 0..need {
933            let id = store
934                .acquire_block()
935                .expect("checked against free_block_count immediately above");
936            self.block_table.push(id);
937        }
938        Ok(())
939    }
940
941    /// Installs a block table the caller allocated, with `seq_len`
942    /// positions already computed in it.
943    ///
944    /// This is how a sequence starts life on top of a cached prefix:
945    /// the blocks are somebody else's, already full, and this sequence
946    /// appends past them.
947    ///
948    /// The `seq_len` installed here is therefore also the POSITION the
949    /// caller's next forward pass must run at, and the caller has no
950    /// second source for that number: [`Self::push`] writes at `seq_len`
951    /// and ignores whatever position its caller believes it is at. A
952    /// prefill that started from zero over an adopted prefix put the
953    /// prompt in the rows *after* the prefix while carrying positions
954    /// `0..n`, which is a wrong answer served with a 200.
955    ///
956    /// `seq_len` MUST be a whole number of blocks,
957    /// because the first append writes at `seq_len` and a shared block
958    /// must never be written -- another sequence is attending over it.
959    /// A ragged length would put that write inside the last shared
960    /// block, corrupting a prefix every other holder is reading.
961    pub fn adopt_blocks(&mut self, block_table: Vec<usize>, seq_len: usize, block_size: usize) {
962        assert_eq!(
963            seq_len % block_size,
964            0,
965            "an adopted prefix must end on a block boundary, or the first \
966             append writes into a block another sequence is reading"
967        );
968        assert!(
969            seq_len / block_size <= block_table.len(),
970            "block table too short for the adopted length"
971        );
972        self.block_table = block_table;
973        self.seq_len = seq_len;
974    }
975
976    /// Copies this sequence's KV out of the shared store into a plain
977    /// contiguous [`KvCache`].
978    ///
979    /// This is what lets the batched prefill path run *unchanged* over
980    /// paged storage. Its fast arm hands `cache.k` / `cache.v` to a
981    /// blocked kernel that reads them as flat slices, and a block table
982    /// cannot be expressed that way. Rather than maintain a second
983    /// prefill kernel that reads through the table -- a copy that could
984    /// drift from the one every other model path uses -- the pages are
985    /// materialised once per layer, the existing kernel runs, and the
986    /// new rows go back with [`Self::append_contiguous`].
987    ///
988    /// The cost is one `seq_len * n_kv_heads * head_dim` copy per layer
989    /// per prefill call, against matmuls that dominate prefill. Decode
990    /// still reads through the block table and copies nothing, which is
991    /// where page sharing actually pays.
992    pub fn to_contiguous(&self, store: &PagedKvStore) -> KvCache {
993        let mut cache = KvCache::with_capacity_split(
994            store.n_kv_heads,
995            store.head_dim,
996            store.v_head_dim,
997            self.seq_len,
998        );
999        cache.k.reserve_exact(self.seq_len * store.k_width());
1000        cache.v.reserve_exact(self.seq_len * store.v_width());
1001        for pos in 0..self.seq_len {
1002            let block_id = self.block_table[pos / store.block_size];
1003            let offset = pos % store.block_size;
1004            cache.k.extend_from_slice(store.k_row(block_id, offset));
1005            cache.v.extend_from_slice(store.v_row(block_id, offset));
1006        }
1007        // The paged store does not evict either, so its positions and
1008        // its rows agree and this one assignment is both.
1009        cache.set_positions(self.seq_len);
1010        cache.recurrent = self.recurrent.clone();
1011        cache
1012    }
1013
1014    /// Appends `count` positions' worth of contiguous K/V rows, the
1015    /// inverse of [`Self::to_contiguous`].
1016    ///
1017    /// Blocks are reserved for the whole append *before* the first row
1018    /// is written, so a store that cannot hold the request refuses it
1019    /// having changed nothing. Writing rows until the store runs dry
1020    /// would leave the sequence with a `seq_len` that disagrees with
1021    /// the model's own idea of how far it has got, which is not a
1022    /// recoverable state.
1023    pub fn append_contiguous(
1024        &mut self,
1025        store: &mut PagedKvStore,
1026        k: &[f32],
1027        v: &[f32],
1028        count: usize,
1029    ) -> Result<(), PagedStoreExhausted> {
1030        let (k_width, v_width) = (store.k_width(), store.v_width());
1031        assert_eq!(k.len(), count * k_width, "k row count");
1032        assert_eq!(v.len(), count * v_width, "v row count");
1033        if self.blocks_needed_for(store, count) > store.free_block_count() {
1034            return Err(PagedStoreExhausted);
1035        }
1036        for i in 0..count {
1037            self.push(
1038                store,
1039                &k[i * k_width..(i + 1) * k_width],
1040                &v[i * v_width..(i + 1) * v_width],
1041            )
1042            .expect("blocks reserved above, so no push here can exhaust the store");
1043        }
1044        Ok(())
1045    }
1046}
1047
1048/// Per-layer [`PagedKvStore`]s that many concurrent requests share.
1049///
1050/// # Why a lock per layer, and why two phases
1051///
1052/// `ferrox-server` runs generation on `spawn_blocking` with, in its own
1053/// words, "no I/O and no shared lock". `KvBlockPool` survives that
1054/// because it only bounds a *count*: each `KvCache` owns a private
1055/// `Vec`, and the pool mutex is taken briefly at acquire and release,
1056/// never during a forward. A `PagedKvStore` is the opposite -- it IS
1057/// the backing memory -- so sharing one across concurrent requests
1058/// needs an answer to "who may touch these bytes when".
1059///
1060/// The answer the API already implies: attention takes
1061/// `&PagedKvStore` and only `push` takes `&mut`. So the accesses split
1062/// cleanly into many concurrent readers and one short exclusive write
1063/// per position, which is exactly an `RwLock` -- and one per LAYER
1064/// rather than one for the whole model, so two requests contend only
1065/// when both are writing the same layer at the same instant.
1066///
1067/// A caller must therefore take the write guard for the push alone and
1068/// drop it before attending under a read guard. Holding the write
1069/// guard across attention would serialise the expensive half and give
1070/// back a global lock with extra steps. Nothing breaks in the gap: a
1071/// sequence's block table and length are its own, and another
1072/// request's push in between only touches blocks it exclusively holds.
1073///
1074/// # Deadlock
1075///
1076/// [`Self::write_all`] is the one place several layers are held at
1077/// once, and it takes them in ascending layer order. Every caller
1078/// getting the same order is what makes that safe; there is no other
1079/// multi-layer acquisition in the codebase, and a new one must follow
1080/// the same rule.
1081///
1082/// # Poisoning
1083///
1084/// A panic while holding a store leaves the KV mid-write, which is not
1085/// recoverable state, but it is also not *unsound* -- the bytes are
1086/// plain `f32`. Poison is stepped over with `into_inner`, matching how
1087/// `ferrox-server` already treats its pool mutex: a poisoned lock
1088/// should not turn one request's panic into a permanently dead server.
1089pub struct SharedPagedKv {
1090    layers: Vec<RwLock<PagedKvStore>>,
1091    /// Guarded separately from the layers, and always taken BEFORE
1092    /// them, never while a layer guard is held. That one-way order is
1093    /// what keeps group allocation and the per-layer push paths from
1094    /// deadlocking against each other.
1095    groups: Mutex<GroupTable>,
1096}
1097
1098impl SharedPagedKv {
1099    /// One store per layer, each with `blocks_per_layer` blocks.
1100    pub fn new(
1101        n_layers: usize,
1102        block_size: usize,
1103        blocks_per_layer: usize,
1104        n_kv_heads: usize,
1105        head_dim: usize,
1106    ) -> Self {
1107        SharedPagedKv {
1108            layers: (0..n_layers)
1109                .map(|_| {
1110                    RwLock::new(PagedKvStore::new(
1111                        block_size,
1112                        blocks_per_layer,
1113                        n_kv_heads,
1114                        head_dim,
1115                    ))
1116                })
1117                .collect(),
1118            groups: Mutex::new(GroupTable::default()),
1119        }
1120    }
1121
1122    /// Wraps stores the caller built, for tests and for callers that
1123    /// size layers differently.
1124    pub fn from_stores(stores: Vec<PagedKvStore>) -> Self {
1125        SharedPagedKv {
1126            layers: stores.into_iter().map(RwLock::new).collect(),
1127            groups: Mutex::new(GroupTable::default()),
1128        }
1129    }
1130
1131    pub fn layer_count(&self) -> usize {
1132        self.layers.len()
1133    }
1134
1135    /// Shared access to one layer, for attention.
1136    pub fn read(&self, layer: usize) -> RwLockReadGuard<'_, PagedKvStore> {
1137        self.layers[layer]
1138            .read()
1139            .unwrap_or_else(|poisoned| poisoned.into_inner())
1140    }
1141
1142    /// Exclusive access to one layer, for a push. Hold it for the push
1143    /// and nothing else -- see the type docs.
1144    pub fn write(&self, layer: usize) -> RwLockWriteGuard<'_, PagedKvStore> {
1145        self.layers[layer]
1146            .write()
1147            .unwrap_or_else(|poisoned| poisoned.into_inner())
1148    }
1149
1150    /// Every layer at once, in ascending order, so a multi-layer append
1151    /// is atomic against other requests.
1152    ///
1153    /// This is what makes "all layers advance or none do" hold under
1154    /// concurrency rather than only single-threaded: checking free
1155    /// space and then appending are separate steps, and without the
1156    /// guards spanning both, another request can take the blocks in
1157    /// between and leave this one half-written.
1158    ///
1159    /// Ascending order is the deadlock rule; see the type docs.
1160    pub fn write_all(&self) -> Vec<RwLockWriteGuard<'_, PagedKvStore>> {
1161        self.layers
1162            .iter()
1163            .map(|l| l.write().unwrap_or_else(|poisoned| poisoned.into_inner()))
1164            .collect()
1165    }
1166
1167    /// Free blocks in one layer, for admission control. A snapshot: by
1168    /// the time a caller acts on it another request may have taken
1169    /// them, which is why the append itself re-checks under the guard.
1170    pub fn free_blocks(&self, layer: usize) -> usize {
1171        self.read(layer).free_block_count()
1172    }
1173
1174    /// Takes one block from EVERY layer as a single group, refcount 1.
1175    ///
1176    /// All layers or none: a group that existed in some layers and not
1177    /// others could not answer "which block holds position p in layer
1178    /// l", which is the only question it exists to answer.
1179    pub fn acquire_group(&self) -> Option<PageGroup> {
1180        let mut guards = self.write_all();
1181        if guards.iter().any(|s| s.free_block_count() == 0) {
1182            return None;
1183        }
1184        let blocks: Vec<usize> = guards
1185            .iter_mut()
1186            .map(|s| {
1187                s.acquire_block()
1188                    .expect("checked every layer under these same guards")
1189            })
1190            .collect();
1191        let mut groups = self
1192            .groups
1193            .lock()
1194            .unwrap_or_else(|poisoned| poisoned.into_inner());
1195        Some(PageGroup(groups.insert(blocks)))
1196    }
1197
1198    /// One more holder of `group`.
1199    ///
1200    /// Called when a second sequence adopts a cached prefix. Without
1201    /// it, the first sequence to finish frees pages the second is
1202    /// still attending over -- a use-after-free that shows up as
1203    /// another conversation's tokens rather than as a crash.
1204    pub fn retain_group(&self, group: PageGroup) {
1205        let mut groups = self
1206            .groups
1207            .lock()
1208            .unwrap_or_else(|poisoned| poisoned.into_inner());
1209        groups.retain(group.0);
1210    }
1211
1212    /// One fewer holder. At zero the blocks go back to their layers.
1213    ///
1214    /// Returns whether this was the last holder, so a caller can assert
1215    /// on it rather than guess.
1216    pub fn release_group(&self, group: PageGroup) -> bool {
1217        let blocks = {
1218            let mut groups = self
1219                .groups
1220                .lock()
1221                .unwrap_or_else(|poisoned| poisoned.into_inner());
1222            match groups.release(group.0) {
1223                Some(blocks) => blocks,
1224                None => return false,
1225            }
1226        };
1227        // The groups lock is dropped before the layer guards are taken,
1228        // so the lock order is always groups-then-layers and never the
1229        // reverse. See the type docs on deadlock.
1230        let mut guards = self.write_all();
1231        for (store, block) in guards.iter_mut().zip(blocks) {
1232            store.release_block(block);
1233        }
1234        true
1235    }
1236
1237    /// Which block in each layer this group owns, indexed by layer.
1238    pub fn group_blocks(&self, group: PageGroup) -> Vec<usize> {
1239        let groups = self
1240            .groups
1241            .lock()
1242            .unwrap_or_else(|poisoned| poisoned.into_inner());
1243        groups.blocks(group.0).to_vec()
1244    }
1245
1246    /// How many holders `group` has. Zero means it does not exist.
1247    pub fn group_refs(&self, group: PageGroup) -> u32 {
1248        let groups = self
1249            .groups
1250            .lock()
1251            .unwrap_or_else(|poisoned| poisoned.into_inner());
1252        groups.refs(group.0)
1253    }
1254
1255    /// Groups that could still be allocated, bounded by the layer with
1256    /// the fewest free blocks: a group needs one from each.
1257    pub fn free_groups(&self) -> usize {
1258        (0..self.layers.len())
1259            .map(|l| self.free_blocks(l))
1260            .min()
1261            .unwrap_or(0)
1262    }
1263}
1264
1265/// A handle to one block in every layer.
1266///
1267/// The unit of sharing between sequences, and the only thing small
1268/// enough to be what a radix prefix cache stores: that cache maps a
1269/// token prefix to ONE index per token, while a position's KV lives in
1270/// `n_layers` different blocks. A group is the name for all of them.
1271#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1272pub struct PageGroup(pub u32);
1273
1274/// Group ids, their per-layer blocks, and how many holders each has.
1275#[derive(Debug, Default)]
1276struct GroupTable {
1277    /// Indexed by group id. `None` for an id currently on the free list.
1278    blocks: Vec<Option<Vec<usize>>>,
1279    refs: Vec<u32>,
1280    free_ids: Vec<u32>,
1281}
1282
1283impl GroupTable {
1284    fn insert(&mut self, blocks: Vec<usize>) -> u32 {
1285        if let Some(id) = self.free_ids.pop() {
1286            self.blocks[id as usize] = Some(blocks);
1287            self.refs[id as usize] = 1;
1288            return id;
1289        }
1290        self.blocks.push(Some(blocks));
1291        self.refs.push(1);
1292        (self.blocks.len() - 1) as u32
1293    }
1294
1295    fn retain(&mut self, id: u32) {
1296        let refs = &mut self.refs[id as usize];
1297        assert!(*refs > 0, "cannot retain group {id}, which has no holders");
1298        *refs += 1;
1299    }
1300
1301    /// Drops one holder, returning the blocks to free only when the
1302    /// last one goes.
1303    fn release(&mut self, id: u32) -> Option<Vec<usize>> {
1304        let refs = &mut self.refs[id as usize];
1305        assert!(*refs > 0, "double free of group {id}");
1306        *refs -= 1;
1307        if *refs > 0 {
1308            return None;
1309        }
1310        // The id is reusable now, but only after the blocks are out:
1311        // handing the id back while it still named blocks would let a
1312        // later `acquire_group` believe it owns them too.
1313        let blocks = self.blocks[id as usize]
1314            .take()
1315            .expect("a group with holders always has blocks");
1316        self.free_ids.push(id);
1317        Some(blocks)
1318    }
1319
1320    fn blocks(&self, id: u32) -> &[usize] {
1321        self.blocks[id as usize]
1322            .as_deref()
1323            .expect("group has no blocks; it was already released")
1324    }
1325
1326    fn refs(&self, id: u32) -> u32 {
1327        self.refs.get(id as usize).copied().unwrap_or(0)
1328    }
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333    use super::*;
1334
1335    /// `blocks_needed_for` is the reservation the whole no-partial-write
1336    /// guarantee rests on, and it is wrong in two opposite directions
1337    /// that fail very differently.
1338    ///
1339    /// UNDER-counting is the dangerous one: `append_contiguous` reserves
1340    /// on this answer and then pushes with an `expect`, so too small a
1341    /// number panics part-way through a layer -- exactly the corrupted
1342    /// state the reservation exists to prevent. Over-counting merely
1343    /// refuses a request that would have fitted.
1344    ///
1345    /// Both mistakes are one edit away. Flooring instead of ceiling
1346    /// under-counts whenever the append does not land on a block
1347    /// boundary; ignoring the part-full tail over-counts whenever a
1348    /// sequence is mid-block, which after the first token is almost
1349    /// always. Neither shows up when the numbers happen to divide
1350    /// evenly, so the cases here are chosen so that they do not.
1351    #[test]
1352    fn blocks_needed_for_accounts_for_the_part_full_tail_block() {
1353        let mut store = PagedKvStore::new(/* block_size = */ 4, 64, 1, 1);
1354        let mut cache = PagedKvCache::new();
1355        let row = [1.0f32];
1356        // Real pushes rather than poking `seq_len`: the count is
1357        // against blocks this sequence HOLDS, so a length with no
1358        // blocks behind it is a state that cannot occur and would only
1359        // let the test agree with an arithmetic nothing produces.
1360        let advance = |cache: &mut PagedKvCache, store: &mut PagedKvStore, n: usize| {
1361            for _ in 0..n {
1362                cache.push(store, &row, &row).unwrap();
1363            }
1364        };
1365
1366        // Empty: a whole-block boundary, and a remainder that a floor
1367        // would round away.
1368        assert_eq!(cache.blocks_needed_for(&store, 0), 0);
1369        assert_eq!(cache.blocks_needed_for(&store, 1), 1);
1370        assert_eq!(cache.blocks_needed_for(&store, 4), 1);
1371        assert_eq!(cache.blocks_needed_for(&store, 5), 2, "5 into 4s needs 2");
1372
1373        // One position in: three slots free in the tail, so appending up
1374        // to three costs NOTHING. Ignoring the tail would say 1.
1375        advance(&mut cache, &mut store, 1);
1376        assert_eq!(cache.blocks_needed_for(&store, 3), 0, "fits in the tail");
1377        assert_eq!(cache.blocks_needed_for(&store, 4), 1);
1378        assert_eq!(cache.blocks_needed_for(&store, 8), 2);
1379
1380        // The awkward case: 1 free in the tail, 6 to append. 5 spill
1381        // over 4-wide blocks, so 2. A floor gives 1 and a tail-blind
1382        // ceil gives 2 for the wrong reason, so this pins the shape.
1383        advance(&mut cache, &mut store, 2); // seq_len = 3
1384        assert_eq!(cache.blocks_needed_for(&store, 6), 2);
1385        assert_eq!(cache.blocks_needed_for(&store, 5), 1);
1386
1387        // Tail exactly full: no free slots, so this behaves like empty.
1388        advance(&mut cache, &mut store, 1); // seq_len = 4
1389        assert_eq!(cache.blocks_needed_for(&store, 1), 1);
1390        assert_eq!(cache.blocks_needed_for(&store, 4), 1);
1391
1392        // A RESERVED block is capacity this sequence already holds, so
1393        // it must not be asked for twice. Counting from `seq_len` alone
1394        // would say 1 here and take a second block for positions the
1395        // reservation already covers.
1396        cache.reserve(&mut store, 4).unwrap();
1397        assert_eq!(
1398            cache.blocks_needed_for(&store, 4),
1399            0,
1400            "a reserved block is already held"
1401        );
1402        assert_eq!(cache.blocks_needed_for(&store, 5), 1);
1403    }
1404
1405    /// A group takes one block from every layer, and gives them all
1406    /// back together.
1407    ///
1408    /// All-or-nothing is the point: a group holding blocks in some
1409    /// layers and not others cannot answer "which block holds position
1410    /// p in layer l", which is the only question it exists for.
1411    #[test]
1412    fn a_group_takes_one_block_from_every_layer_and_returns_them_together() {
1413        let kv = SharedPagedKv::new(3, 2, 4, 1, 1);
1414        assert_eq!(kv.free_groups(), 4);
1415
1416        let g = kv.acquire_group().expect("4 groups available");
1417        let blocks = kv.group_blocks(g);
1418        assert_eq!(blocks.len(), 3, "one block per layer");
1419        for l in 0..3 {
1420            assert_eq!(kv.free_blocks(l), 3, "layer {l} gave up exactly one");
1421        }
1422        assert_eq!(kv.free_groups(), 3);
1423
1424        assert!(kv.release_group(g), "sole holder, so this frees it");
1425        for l in 0..3 {
1426            assert_eq!(kv.free_blocks(l), 4, "layer {l} got its block back");
1427        }
1428        assert_eq!(kv.free_groups(), 4);
1429    }
1430
1431    /// A group survives until its LAST holder releases it.
1432    ///
1433    /// This is what makes prefix sharing safe. Two sequences off one
1434    /// system prompt hold the same pages; if the first to finish freed
1435    /// them, the second would keep attending over blocks the store had
1436    /// already handed to somebody else -- surfacing as another
1437    /// conversation's tokens, not as a crash.
1438    #[test]
1439    fn a_group_shared_by_two_holders_survives_the_first_release() {
1440        let kv = SharedPagedKv::new(2, 2, 2, 1, 1);
1441        let g = kv.acquire_group().unwrap();
1442        let blocks = kv.group_blocks(g);
1443        kv.retain_group(g);
1444        assert_eq!(kv.group_refs(g), 2);
1445
1446        assert!(
1447            !kv.release_group(g),
1448            "one holder remains, so nothing is freed"
1449        );
1450        assert_eq!(kv.group_refs(g), 1);
1451        assert_eq!(kv.free_blocks(0), 1, "the blocks are still held");
1452        assert_eq!(kv.group_blocks(g), blocks, "and still name the same blocks");
1453
1454        assert!(kv.release_group(g), "last holder frees it");
1455        assert_eq!(kv.group_refs(g), 0);
1456        assert_eq!(kv.free_blocks(0), 2);
1457    }
1458
1459    /// Exhaustion is per group, bounded by the tightest layer.
1460    ///
1461    /// A layer with one block left caps the whole pool at one more
1462    /// group however much room the others have, because a group needs
1463    /// one block from each.
1464    #[test]
1465    fn group_capacity_is_bounded_by_the_layer_with_the_fewest_blocks() {
1466        let kv = SharedPagedKv::from_stores(vec![
1467            PagedKvStore::new(2, 5, 1, 1),
1468            PagedKvStore::new(2, 1, 1, 1),
1469        ]);
1470        assert_eq!(kv.free_groups(), 1, "layer 1 has only one block");
1471
1472        let g = kv.acquire_group().expect("one group fits");
1473        assert_eq!(kv.free_groups(), 0);
1474        assert!(
1475            kv.acquire_group().is_none(),
1476            "layer 1 is empty, so no group can be formed"
1477        );
1478        // The refused attempt must not have taken layer 0's block.
1479        assert_eq!(kv.free_blocks(0), 4, "a refused group leaks nothing");
1480        kv.release_group(g);
1481        assert_eq!(kv.free_blocks(0), 5);
1482    }
1483
1484    /// A released id is reused, with a refcount that starts over.
1485    #[test]
1486    fn a_released_group_id_is_reused_with_a_fresh_refcount() {
1487        let kv = SharedPagedKv::new(1, 2, 2, 1, 1);
1488        let first = kv.acquire_group().unwrap();
1489        kv.retain_group(first);
1490        assert_eq!(kv.group_refs(first), 2);
1491        kv.release_group(first);
1492        kv.release_group(first);
1493        assert_eq!(kv.group_refs(first), 0, "gone, not merely decremented");
1494
1495        let second = kv.acquire_group().unwrap();
1496        assert_eq!(second, first, "the id is reused");
1497        assert_eq!(
1498            kv.group_refs(second),
1499            1,
1500            "a reused id must not inherit the old count"
1501        );
1502        assert_eq!(kv.group_blocks(second).len(), 1);
1503        assert_eq!(kv.free_blocks(0), 1);
1504    }
1505
1506    /// Reading a group after its last holder released it PANICS rather
1507    /// than answering with stale blocks.
1508    ///
1509    /// This is the observable half of clearing the entry on release,
1510    /// and the reason it is `take` rather than `clone`: a caller still
1511    /// holding a `PageGroup` after releasing it is exactly the bug
1512    /// refcounting exists to prevent, and blocks that now belong to
1513    /// somebody else are the worst possible answer -- the caller reads
1514    /// another sequence's KV and nothing says so.
1515    ///
1516    /// Written after sabotage showed the previous test here passed with
1517    /// `clone` in place of `take`: `insert` overwrites the entry on
1518    /// reuse, so a stale entry was never reachable through the path
1519    /// that test took. This one reaches it.
1520    #[test]
1521    #[should_panic(expected = "already released")]
1522    fn reading_a_released_group_panics_rather_than_returning_stale_blocks() {
1523        let kv = SharedPagedKv::new(2, 2, 2, 1, 1);
1524        let g = kv.acquire_group().unwrap();
1525        assert!(kv.release_group(g));
1526        let _ = kv.group_blocks(g);
1527    }
1528
1529    /// Releasing a group nobody holds is a bug, not a no-op.
1530    ///
1531    /// Silently ignoring it would let a double release return the same
1532    /// blocks to the store twice, after which two sequences are handed
1533    /// the same page and both write it.
1534    #[test]
1535    #[should_panic(expected = "double free of group")]
1536    fn releasing_a_group_twice_panics_rather_than_freeing_it_twice() {
1537        let kv = SharedPagedKv::new(1, 2, 2, 1, 1);
1538        let g = kv.acquire_group().unwrap();
1539        assert!(kv.release_group(g));
1540        kv.release_group(g);
1541    }
1542
1543    /// A recycled block backs a later position without the store ever
1544    /// being asked for another one, and the later position's writes are
1545    /// what a read at that position returns.
1546    ///
1547    /// This is the whole sliding-window mechanism in miniature. Blocks
1548    /// of two, four positions, and only two blocks in the store: without
1549    /// recycling, position 2 has nowhere to go.
1550    #[test]
1551    fn a_recycled_block_backs_a_later_position_without_touching_the_store() {
1552        let mut store = PagedKvStore::new(2, 2, 1, 2);
1553        let mut cache = PagedKvCache::new();
1554        cache.push(&mut store, &[1.0, 1.0], &[1.0, 1.0]).unwrap();
1555        cache.push(&mut store, &[2.0, 2.0], &[2.0, 2.0]).unwrap();
1556        assert_eq!(store.free_block_count(), 1, "one block per position pair");
1557
1558        // Positions 0..2 have fallen behind a window of two. Their block
1559        // backs positions 2..4 instead, and the store is untouched.
1560        let recycled = cache.block_table()[0];
1561        cache.append_block(recycled);
1562        assert_eq!(
1563            store.free_block_count(),
1564            1,
1565            "recycling must not take a block from the store"
1566        );
1567        cache.push(&mut store, &[3.0, 3.0], &[3.0, 3.0]).unwrap();
1568        assert_eq!(cache.seq_len(), 3);
1569        assert_eq!(
1570            cache.block_table(),
1571            &[recycled, recycled],
1572            "the same block at the stale index and the live one"
1573        );
1574
1575        // Reading position 2 sees the new row. Position 0's row is gone,
1576        // which is exactly what "behind the window" means -- the kernel
1577        // never indexes it.
1578        let flat = cache.to_contiguous(&store);
1579        assert_eq!(&flat.k[4..6], &[3.0, 3.0], "position 2 reads its own row");
1580        assert_eq!(
1581            &flat.k[0..2],
1582            &[3.0, 3.0],
1583            "position 0 now reads the recycled row, and nothing may read it"
1584        );
1585    }
1586
1587    /// Releasing an aliased table hands each block back ONCE.
1588    ///
1589    /// Per index instead of per distinct block would put the recycled id
1590    /// on the free list twice, and the next two acquisitions would hand
1591    /// two sequences the same memory -- which does not fail, it
1592    /// interleaves two conversations' KV.
1593    #[test]
1594    fn releasing_a_recycled_table_gives_each_block_back_once() {
1595        // Exactly one block in the store, so "handed back twice" is
1596        // observable as a second acquisition succeeding.
1597        let mut store = PagedKvStore::new(2, 1, 1, 2);
1598        let mut cache = PagedKvCache::new();
1599        cache.push(&mut store, &[1.0, 1.0], &[1.0, 1.0]).unwrap();
1600        let held = cache.block_table()[0];
1601        cache.append_block(held);
1602        cache.append_block(held);
1603
1604        let free_before = store.free_block_count();
1605        cache.release(&mut store);
1606        assert_eq!(
1607            store.free_block_count(),
1608            free_before + 1,
1609            "three table entries naming one block are one block back"
1610        );
1611        // And the store agrees: it can hand out that block once.
1612        assert!(store.acquire_block().is_some());
1613        assert!(store.acquire_block().is_none());
1614    }
1615
1616    #[test]
1617    fn a_gathered_sequence_round_trips_through_the_store() {
1618        let mut store = PagedKvStore::new(2, 8, 2, 2);
1619        let mut cache = PagedKvCache::new();
1620        // Five positions over blocks of two: the tail block is half
1621        // full, which is where an off-by-one in the gather shows up.
1622        let rows: Vec<[f32; 4]> = (0..5)
1623            .map(|i| {
1624                let b = i as f32 * 10.0;
1625                [b + 1.0, b + 2.0, b + 3.0, b + 4.0]
1626            })
1627            .collect();
1628        for r in &rows {
1629            cache.push(&mut store, r, r).unwrap();
1630        }
1631
1632        let flat = cache.to_contiguous(&store);
1633        assert_eq!(flat.positions(), 5);
1634        assert_eq!(flat.k.len(), 5 * 4);
1635        for (i, r) in rows.iter().enumerate() {
1636            assert_eq!(&flat.k[i * 4..(i + 1) * 4], r, "position {i} k");
1637            assert_eq!(&flat.v[i * 4..(i + 1) * 4], r, "position {i} v");
1638        }
1639
1640        // And appending those same rows back onto a fresh sequence
1641        // reproduces the store's view of them exactly.
1642        let mut rebuilt = PagedKvCache::new();
1643        let mut store2 = PagedKvStore::new(2, 8, 2, 2);
1644        rebuilt
1645            .append_contiguous(&mut store2, &flat.k, &flat.v, 5)
1646            .unwrap();
1647        let again = rebuilt.to_contiguous(&store2);
1648        assert_eq!(again.k, flat.k);
1649        assert_eq!(again.v, flat.v);
1650        assert_eq!(again.positions(), flat.positions());
1651    }
1652
1653    #[test]
1654    fn push_grows_seq_len_and_stores_values() {
1655        let mut cache = KvCache::new(2, 2);
1656        cache
1657            .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
1658            .unwrap();
1659        assert_eq!(cache.positions(), 1);
1660        cache
1661            .push(&[9.0, 10.0, 11.0, 12.0], &[13.0, 14.0, 15.0, 16.0])
1662            .unwrap();
1663        assert_eq!(cache.positions(), 2);
1664        assert_eq!(cache.k.len(), 2 * 2 * 2);
1665        assert_eq!(cache.k[4], 9.0);
1666    }
1667
1668    #[test]
1669    #[should_panic]
1670    fn push_wrong_size_panics() {
1671        let mut cache = KvCache::new(2, 2);
1672        let _ = cache.push(&[1.0, 2.0], &[1.0, 2.0]); // too short
1673    }
1674
1675    #[test]
1676    fn clear_resets_state() {
1677        let mut cache = KvCache::new(1, 1);
1678        cache.push(&[1.0], &[2.0]).unwrap();
1679        cache.clear();
1680        assert_eq!(cache.positions(), 0);
1681        assert!(cache.k.is_empty());
1682    }
1683
1684    #[test]
1685    fn truncate_rolls_back_to_exact_length_preserving_earlier_data() {
1686        let mut cache = KvCache::new(2, 2);
1687        cache
1688            .push(&[1.0, 2.0, 3.0, 4.0], &[10.0, 20.0, 30.0, 40.0])
1689            .unwrap();
1690        cache
1691            .push(&[5.0, 6.0, 7.0, 8.0], &[50.0, 60.0, 70.0, 80.0])
1692            .unwrap();
1693        cache
1694            .push(&[9.0, 9.0, 9.0, 9.0], &[90.0, 90.0, 90.0, 90.0])
1695            .unwrap();
1696        assert_eq!(cache.positions(), 3);
1697
1698        cache.truncate(1);
1699        assert_eq!(cache.positions(), 1);
1700        assert_eq!(cache.k, vec![1.0, 2.0, 3.0, 4.0]);
1701        assert_eq!(cache.v, vec![10.0, 20.0, 30.0, 40.0]);
1702    }
1703
1704    #[test]
1705    fn truncate_to_current_length_is_a_no_op() {
1706        let mut cache = KvCache::new(1, 2);
1707        cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1708        cache.truncate(1);
1709        assert_eq!(cache.positions(), 1);
1710        assert_eq!(cache.k, vec![1.0, 2.0]);
1711    }
1712
1713    #[test]
1714    fn truncate_to_zero_empties_the_cache() {
1715        let mut cache = KvCache::new(1, 2);
1716        cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1717        cache.truncate(0);
1718        assert_eq!(cache.positions(), 0);
1719        assert!(cache.k.is_empty());
1720        assert!(cache.v.is_empty());
1721    }
1722
1723    #[test]
1724    #[should_panic]
1725    fn truncate_beyond_current_length_panics() {
1726        let mut cache = KvCache::new(1, 2);
1727        cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1728        cache.truncate(5);
1729    }
1730
1731    #[test]
1732    fn push_after_truncate_continues_correctly() {
1733        let mut cache = KvCache::new(1, 1);
1734        cache.push(&[1.0], &[10.0]).unwrap();
1735        cache.push(&[2.0], &[20.0]).unwrap();
1736        cache.push(&[3.0], &[30.0]).unwrap(); // this one will be "rejected"
1737        cache.truncate(2);
1738        cache.push(&[99.0], &[990.0]).unwrap(); // real continuation after rejection
1739        assert_eq!(cache.positions(), 3);
1740        assert_eq!(cache.k, vec![1.0, 2.0, 99.0]);
1741        assert_eq!(cache.v, vec![10.0, 20.0, 990.0]);
1742    }
1743
1744    #[test]
1745    fn with_capacity_preallocates_and_never_reallocates_within_plan() {
1746        let n_kv_heads = 4;
1747        let head_dim = 8;
1748        let max_seq_len = 16;
1749        let mut cache = KvCache::with_capacity(n_kv_heads, head_dim, max_seq_len);
1750
1751        let expected_elems = max_seq_len * n_kv_heads * head_dim;
1752        assert!(cache.k.capacity() >= expected_elems);
1753        assert!(cache.v.capacity() >= expected_elems);
1754
1755        let step = vec![0.5f32; n_kv_heads * head_dim];
1756        let k_ptr_before = cache.k.as_ptr();
1757        for _ in 0..max_seq_len {
1758            cache.push(&step, &step).unwrap();
1759        }
1760        let k_ptr_after = cache.k.as_ptr();
1761        assert_eq!(
1762            k_ptr_before, k_ptr_after,
1763            "pushing exactly up to the planned capacity must not reallocate"
1764        );
1765        assert!(cache.is_within_planned_capacity());
1766    }
1767
1768    #[test]
1769    fn allocated_bytes_reflects_preallocated_capacity_not_just_used_length() {
1770        let cache = KvCache::with_capacity(4, 8, 100);
1771        // 100 positions * 4 kv_heads * 8 head_dim * 2 (k+v) * 4 bytes/f32
1772        let expected_min = 100 * 4 * 8 * 2 * 4;
1773        assert!(
1774            cache.allocated_bytes() >= expected_min,
1775            "allocated_bytes={} expected_min={expected_min}",
1776            cache.allocated_bytes()
1777        );
1778        // Nothing has been pushed yet, but the memory is already reserved.
1779        assert_eq!(cache.positions(), 0);
1780    }
1781
1782    #[test]
1783    fn grow_as_you_go_cache_reports_not_within_planned_capacity() {
1784        let mut cache = KvCache::new(2, 2);
1785        cache
1786            .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
1787            .unwrap();
1788        assert!(
1789            !cache.is_within_planned_capacity(),
1790            "a cache built with `new` has no plan to be within"
1791        );
1792    }
1793
1794    #[test]
1795    fn with_pool_acquires_one_block_and_reports_it_in_free_blocks() {
1796        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 10)));
1797        let cache = KvCache::with_pool(2, 2, pool.clone(), 0).unwrap();
1798        assert_eq!(pool.lock().unwrap().free_blocks(), 9);
1799        assert_eq!(cache.positions(), 0);
1800    }
1801
1802    #[test]
1803    fn with_pool_fails_without_mutating_the_pool_when_exhausted() {
1804        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 0)));
1805        let result = KvCache::with_pool(2, 2, pool.clone(), 0);
1806        assert!(result.is_err());
1807        assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1808    }
1809
1810    #[test]
1811    fn push_acquires_additional_blocks_as_the_cache_crosses_block_boundaries() {
1812        let block_size = 2;
1813        let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 10)));
1814        let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1815        assert_eq!(pool.lock().unwrap().free_blocks(), 9);
1816
1817        // First block holds `block_size` = 2 positions; pushing them
1818        // must not need a second block.
1819        cache.push(&[1.0], &[1.0]).unwrap();
1820        cache.push(&[2.0], &[2.0]).unwrap();
1821        assert_eq!(
1822            pool.lock().unwrap().free_blocks(),
1823            9,
1824            "filling exactly the first block must not acquire a second one"
1825        );
1826
1827        // The third position crosses into a second block.
1828        cache.push(&[3.0], &[3.0]).unwrap();
1829        assert_eq!(pool.lock().unwrap().free_blocks(), 8);
1830        assert_eq!(cache.positions(), 3);
1831        assert_eq!(cache.k, vec![1.0, 2.0, 3.0]);
1832    }
1833
1834    #[test]
1835    fn push_returns_pool_exhausted_and_leaves_state_unchanged_when_no_blocks_remain() {
1836        let block_size = 1;
1837        let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 1)));
1838        let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1839        assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1840
1841        cache.push(&[1.0], &[1.0]).unwrap(); // fills the one held block
1842
1843        let before_k = cache.k.clone();
1844        let result = cache.push(&[2.0], &[2.0]);
1845        assert_eq!(result, Err(KvPoolExhausted));
1846        assert_eq!(
1847            cache.positions(),
1848            1,
1849            "a failed push must not change seq_len"
1850        );
1851        assert_eq!(cache.k, before_k, "a failed push must not append data");
1852    }
1853
1854    #[test]
1855    fn dropping_a_pooled_cache_returns_its_blocks_to_the_pool() {
1856        let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 2)));
1857        {
1858            let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1859            cache.push(&[1.0], &[1.0]).unwrap(); // fills the first (only held) block
1860            cache.push(&[2.0], &[2.0]).unwrap(); // crosses into a second block
1861            assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1862        }
1863        assert_eq!(
1864            pool.lock().unwrap().free_blocks(),
1865            2,
1866            "both blocks held by the dropped cache must return to the pool"
1867        );
1868    }
1869
1870    #[test]
1871    fn release_to_pool_is_explicit_and_idempotent() {
1872        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 5)));
1873        let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1874        assert_eq!(pool.lock().unwrap().free_blocks(), 4);
1875
1876        cache.release_to_pool();
1877        assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1878
1879        cache.release_to_pool(); // no-op, must not over-release
1880        assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1881
1882        drop(cache); // must not release again either
1883        assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1884    }
1885
1886    #[test]
1887    fn two_pooled_caches_share_one_bounded_budget() {
1888        let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 1)));
1889        let cache_a = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1890        let cache_b = KvCache::with_pool(1, 1, pool.clone(), 0);
1891        assert!(
1892            cache_b.is_err(),
1893            "a second concurrent request must not be admitted when the shared budget is full"
1894        );
1895
1896        drop(cache_a);
1897        let cache_c = KvCache::with_pool(1, 1, pool, 0);
1898        assert!(
1899            cache_c.is_ok(),
1900            "once the first request's cache is dropped, its budget must become available again"
1901        );
1902    }
1903
1904    #[test]
1905    fn cloning_a_pooled_cache_detaches_the_clone_from_pool_accounting() {
1906        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 3)));
1907        let original = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1908        assert_eq!(pool.lock().unwrap().free_blocks(), 2);
1909
1910        let clone = original.clone();
1911        assert_eq!(
1912            pool.lock().unwrap().free_blocks(),
1913            2,
1914            "cloning must not acquire additional blocks"
1915        );
1916        assert_eq!(clone.k, original.k);
1917
1918        drop(clone);
1919        assert_eq!(
1920            pool.lock().unwrap().free_blocks(),
1921            2,
1922            "dropping a detached clone must not release the original's blocks"
1923        );
1924
1925        drop(original);
1926        assert_eq!(
1927            pool.lock().unwrap().free_blocks(),
1928            3,
1929            "dropping the original must release its blocks exactly once"
1930        );
1931    }
1932
1933    /// A resize is arithmetic, and the one rule that is not: shrinking
1934    /// past what is held is refused, and the pool is left exactly as it
1935    /// was.
1936    ///
1937    /// Clamping to zero instead would silently over-promise -- the
1938    /// caches holding those blocks do not give them back, so every
1939    /// later acquire would decide against a budget that does not
1940    /// describe the memory in use. This test fails under that clamp.
1941    #[test]
1942    fn a_pool_refuses_to_shrink_below_what_is_already_held() {
1943        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 10)));
1944        let held = KvCache::with_pool(2, 4, Arc::clone(&pool), 24).expect("blocks");
1945        let in_use = {
1946            let p = pool.lock().unwrap();
1947            p.total_blocks() - p.free_blocks()
1948        };
1949        assert!(in_use > 0, "the fixture must actually hold blocks");
1950
1951        let mut p = pool.lock().unwrap();
1952        assert_eq!(p.resize(in_use - 1), Err(in_use));
1953        assert_eq!(p.total_blocks(), 10, "a refused resize changes nothing");
1954        assert_eq!(p.free_blocks(), 10 - in_use);
1955
1956        // Down to exactly what is held is legal, and leaves nothing free.
1957        assert_eq!(p.resize(in_use), Ok(()));
1958        assert_eq!(p.free_blocks(), 0);
1959        drop(p);
1960        drop(held);
1961    }
1962
1963    /// Growing hands the new blocks to the free list without disturbing
1964    /// what is held, which is the whole point of a live re-split.
1965    #[test]
1966    fn growing_a_pool_adds_to_what_is_free_and_not_to_what_is_held() {
1967        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 8)));
1968        let held = KvCache::with_pool(2, 4, Arc::clone(&pool), 16).expect("blocks");
1969        let mut p = pool.lock().unwrap();
1970        let in_use = p.total_blocks() - p.free_blocks();
1971
1972        assert_eq!(p.resize(32), Ok(()));
1973        assert_eq!(p.total_blocks(), 32);
1974        assert_eq!(p.free_blocks(), 32 - in_use);
1975        drop(p);
1976        drop(held);
1977    }
1978
1979    /// Positions and rows agree for every store that does not evict,
1980    /// and this pins that they are DERIVED separately rather than one
1981    /// being an alias for the other.
1982    ///
1983    /// The point of the split is #61: a windowed layer will drop rows
1984    /// behind its window while its position count keeps climbing. Until
1985    /// then the two are equal, so this test cannot prove much about
1986    /// eviction. What it CAN prove, and what matters, is that `rows`
1987    /// reads the buffer rather than the counter: set the counter to a
1988    /// lie and `rows` still reports the truth.
1989    #[test]
1990    fn rows_is_read_from_the_buffer_and_positions_from_the_counter() {
1991        let mut cache = KvCache::new(2, 4);
1992        for i in 0..5 {
1993            let step = vec![i as f32; 8];
1994            cache.push(&step, &step).expect("unbounded growth");
1995        }
1996        assert_eq!(cache.positions(), 5);
1997        assert_eq!(cache.rows(), 5, "nothing evicts, so they agree");
1998
1999        // The counter lies; the buffer does not.
2000        cache.force_positions_for_test(99);
2001        assert_eq!(cache.positions(), 99);
2002        assert_eq!(
2003            cache.rows(),
2004            5,
2005            "rows must come from k/v, or it is just a second name for the counter"
2006        );
2007    }
2008
2009    /// `truncate` is measured in POSITIONS, and it moves both, because
2010    /// nothing evicts yet. Written down because it is the first thing
2011    /// eviction changes: a truncate target will stop being a row index.
2012    #[test]
2013    fn truncate_moves_positions_and_rows_together_while_nothing_evicts() {
2014        let mut cache = KvCache::new(1, 2);
2015        for i in 0..4 {
2016            let step = vec![i as f32; 2];
2017            cache.push(&step, &step).expect("unbounded growth");
2018        }
2019        cache.truncate(2);
2020        assert_eq!(cache.positions(), 2);
2021        assert_eq!(cache.rows(), 2);
2022        assert_eq!(cache.k.len(), 4, "two positions of two elements each");
2023    }
2024
2025    /// The store's rule must be *the* rule, not a second copy of it.
2026    ///
2027    /// `KvWindow::rows_after` is what `ferrox_models::kv_budget` prices
2028    /// against. If the store drained to anything else the budget would
2029    /// be describing a cache that does not exist, which is #33 again.
2030    #[test]
2031    fn a_windowed_cache_holds_exactly_what_the_rule_says_it_holds() {
2032        let window = KvWindow::new(8, 3).expect("positive window");
2033        let mut cache = KvCache::new(2, 4);
2034        cache.arm_window(window);
2035        let step = vec![1.0f32; 8];
2036        for p in 1..=200usize {
2037            cache.push(&step, &step).expect("unbounded growth");
2038            cache.evict_behind_window();
2039            assert_eq!(cache.positions(), p);
2040            assert_eq!(
2041                cache.rows(),
2042                window.rows_after(p),
2043                "at {p} positions the store and the rule disagree"
2044            );
2045        }
2046    }
2047
2048    /// The point of the issue: positions keep counting, rows do not.
2049    #[test]
2050    fn a_windowed_layers_resident_rows_stop_growing() {
2051        let window = KvWindow::with_default_slack(16).expect("positive window");
2052        let mut cache = KvCache::new(2, 4);
2053        cache.arm_window(window);
2054        let step = vec![0.5f32; 8];
2055        for _ in 0..2000 {
2056            cache.push(&step, &step).expect("unbounded growth");
2057            cache.evict_behind_window();
2058        }
2059        assert_eq!(cache.positions(), 2000);
2060        assert!(
2061            cache.rows() <= window.max_rows(),
2062            "{} rows resident after 2000 positions",
2063            cache.rows()
2064        );
2065        // And the bytes really went back, not just the length: `drain`
2066        // frees nothing, and memory is the whole point.
2067        assert!(
2068            cache.allocated_bytes() <= window.max_rows() * 8 * 2 * 4 * 2,
2069            "capacity was never handed back: {} bytes",
2070            cache.allocated_bytes()
2071        );
2072    }
2073
2074    /// **The correctness argument, measured.**
2075    ///
2076    /// Eviction is only allowed to be token-identical because the rows a
2077    /// windowed kernel reads -- the last `window` of them -- are the
2078    /// same bytes whether or not anything behind them was dropped. This
2079    /// pushes distinguishable rows into an evicting cache and a plain
2080    /// one and compares exactly that slice.
2081    #[test]
2082    fn the_rows_a_windowed_kernel_reads_are_identical_with_and_without_eviction() {
2083        let window = KvWindow::new(6, 2).expect("positive window");
2084        let (n_kv_heads, head_dim) = (2usize, 4usize);
2085        let per = n_kv_heads * head_dim;
2086        let mut evicting = KvCache::new(n_kv_heads, head_dim);
2087        evicting.arm_window(window);
2088        let mut plain = KvCache::new(n_kv_heads, head_dim);
2089
2090        for p in 0..120usize {
2091            // A row nothing else could produce, so a misplaced row is
2092            // not merely a different number but an identifiable one.
2093            let k: Vec<f32> = (0..per).map(|i| (p * 100 + i) as f32).collect();
2094            let v: Vec<f32> = k.iter().map(|x| -x).collect();
2095            evicting.push(&k, &v).expect("unbounded growth");
2096            evicting.evict_behind_window();
2097            plain.push(&k, &v).expect("unbounded growth");
2098
2099            let read = window.window().min(p + 1);
2100            let e_start = (evicting.rows() - read) * per;
2101            let p_start = (plain.rows() - read) * per;
2102            assert_eq!(
2103                &evicting.k[e_start..],
2104                &plain.k[p_start..],
2105                "K read set diverged at position {p}"
2106            );
2107            assert_eq!(
2108                &evicting.v[e_start..],
2109                &plain.v[p_start..],
2110                "V read set diverged at position {p}"
2111            );
2112            assert_eq!(evicting.positions(), plain.positions());
2113        }
2114    }
2115
2116    /// An unarmed cache is the cache this engine has always had.
2117    #[test]
2118    fn an_unarmed_cache_never_drops_a_row() {
2119        let mut cache = KvCache::new(2, 4);
2120        let step = vec![1.0f32; 8];
2121        for _ in 0..64 {
2122            cache.push(&step, &step).expect("unbounded growth");
2123            assert_eq!(cache.evict_behind_window(), 0);
2124        }
2125        assert_eq!(cache.rows(), 64);
2126        assert_eq!(cache.positions(), 64);
2127        assert!(cache.window().is_none());
2128    }
2129
2130    /// Speculative decoding rolls the cache back by the number of
2131    /// rejected draft tokens. That is representable while the target is
2132    /// still resident.
2133    #[test]
2134    fn truncate_still_works_inside_the_resident_window() {
2135        let window = KvWindow::new(8, 3).expect("positive window");
2136        let mut cache = KvCache::new(1, 2);
2137        cache.arm_window(window);
2138        let step = vec![1.0f32; 2];
2139        for _ in 0..50 {
2140            cache.push(&step, &step).expect("unbounded growth");
2141            cache.evict_behind_window();
2142        }
2143        let rows_before = cache.rows();
2144        cache.truncate(46);
2145        assert_eq!(cache.positions(), 46);
2146        assert_eq!(cache.rows(), rows_before - 4);
2147    }
2148
2149    /// ...and stops, loudly, when it is not. A cache that rolled back to
2150    /// the oldest row it happened to have would answer the next token
2151    /// out of a history with a hole in it.
2152    #[test]
2153    #[should_panic(expected = "rows are resident")]
2154    fn truncate_refuses_to_roll_back_past_what_the_window_kept() {
2155        let window = KvWindow::new(4, 1).expect("positive window");
2156        let mut cache = KvCache::new(1, 2);
2157        cache.arm_window(window);
2158        let step = vec![1.0f32; 2];
2159        for _ in 0..50 {
2160            cache.push(&step, &step).expect("unbounded growth");
2161            cache.evict_behind_window();
2162        }
2163        cache.truncate(3);
2164    }
2165
2166    /// A clone of an evicting cache is still an evicting cache. Reset
2167    /// the window on clone and `positions` becomes a row count again in
2168    /// whatever reads the clone.
2169    #[test]
2170    fn a_clone_carries_the_window() {
2171        let window = KvWindow::new(4, 1).expect("positive window");
2172        let mut cache = KvCache::new(1, 2);
2173        cache.arm_window(window);
2174        let step = vec![1.0f32; 2];
2175        for _ in 0..20 {
2176            cache.push(&step, &step).expect("unbounded growth");
2177            cache.evict_behind_window();
2178        }
2179        let copy = cache.clone();
2180        assert_eq!(copy.window(), Some(window));
2181        assert_eq!(copy.rows(), cache.rows());
2182        assert_eq!(copy.positions(), cache.positions());
2183    }
2184
2185    /// A pool-backed cache grows when its BUFFER is full, not when its
2186    /// position counter reaches the buffer's size. Those are the same
2187    /// number until something evicts, and an evicting pooled cache keyed
2188    /// on positions would acquire a block per token forever and exhaust
2189    /// the pool.
2190    #[test]
2191    fn a_pooled_windowed_cache_stops_acquiring_blocks() {
2192        let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 8)));
2193        let mut cache =
2194            KvCache::with_pool(1, 2, Arc::clone(&pool), 8).expect("the pool was sized for this");
2195        cache.arm_window(KvWindow::new(4, 1).expect("positive window"));
2196        let step = vec![1.0f32; 2];
2197        for _ in 0..64 {
2198            cache
2199                .push(&step, &step)
2200                .expect("the buffer never fills again");
2201            cache.evict_behind_window();
2202        }
2203        assert_eq!(cache.positions(), 64);
2204        assert!(cache.rows() <= 5);
2205    }
2206
2207    /// **The sibling above passes for the wrong reason on its own
2208    /// sizing, so this is the same property where the trap is armed.**
2209    ///
2210    /// Eviction hands surplus CAPACITY back with `shrink_to`, which is
2211    /// where its memory saving actually comes from. For a pool-backed
2212    /// cache that is a bug rather than a saving: the pool has already
2213    /// promised these blocks and does not take them back, while `push`
2214    /// decides it needs another block by comparing rows against
2215    /// `k.capacity()`. Shrink that capacity to a window and the
2216    /// condition is true again every `slack + 1` tokens, forever -- so
2217    /// the cache draws a fresh block from the shared pool on a cadence,
2218    /// never releases one, and runs the pool dry underneath every OTHER
2219    /// request in the process. It surfaces where `push` is documented
2220    /// infallible, so the caller panics mid-answer.
2221    ///
2222    /// The sibling's cache is 8 positions against a 5-row ceiling,
2223    /// which is under the 2x threshold `shrink_to` is gated on, so it
2224    /// never reaches the shrink at all. This one is 64 positions
2225    /// against the same ceiling, and the pool is given spare blocks so
2226    /// the leak shows up as blocks quietly gone rather than only as the
2227    /// error at the end of them.
2228    #[test]
2229    fn an_evicting_pooled_cache_never_hands_its_capacity_back_to_reacquire_it() {
2230        let pool = Arc::new(Mutex::new(KvBlockPool::new(8, 24)));
2231        let mut cache =
2232            KvCache::with_pool(1, 2, Arc::clone(&pool), 64).expect("8 of the 24 blocks");
2233        let free_after_construction = pool.lock().unwrap().free_blocks();
2234        assert_eq!(free_after_construction, 16, "8 blocks cover 64 positions");
2235
2236        cache.arm_window(KvWindow::new(4, 1).expect("positive window"));
2237        let step = vec![1.0f32; 2];
2238        for _ in 0..512 {
2239            cache
2240                .push(&step, &step)
2241                .expect("a reservation made up front must not be re-made per token");
2242            cache.evict_behind_window();
2243        }
2244
2245        assert_eq!(
2246            pool.lock().unwrap().free_blocks(),
2247            free_after_construction,
2248            "the cache drew more blocks from the shared pool while evicting"
2249        );
2250        assert_eq!(cache.positions(), 512);
2251        assert!(cache.rows() <= 5);
2252    }
2253}