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