metrale_cache/
kv_cache.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The paged KV cache: the `KvCacheDtype` storage formats, the
4//! per-layer block geometry in `KvCacheConfig`, and the `PagedKvCache` block
5//! pool with its per-block reference counts.
6//!
7//! Owner: cache.
8//! Invariants:
9//! - Until `release`, every attention layer `i < num_layers` has its own K pool
10//!   and V pool of `num_blocks` blocks, sized by that layer's K-side and V-side
11//!   block bytes.
12//! - A block index is pushed back onto the free list only when a decrement
13//!   (`dec_ref`, `return_evicted_block`) takes its count from 1 to 0. A
14//!   decrement at 0 never pushes: it is logged, and `dec_ref` also panics in
15//!   a debug build.
16
17use anyhow::{Result, bail};
18use metrale_gpu_runtime::gpu::DevicePtr;
19
20pub(crate) const NVFP4_GROUP_SIZE: usize = 16;
21
22/// 2026-09-25: KV cache storage format. A symmetric variant uses one format
23/// for K and V; a `*K*V` variant names the K format, then the V format
24/// (`kv_pair`).
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum KvCacheDtype {
27    /// 2026-09-25: BF16, 2 bytes per element.
28    Bf16,
29    /// 2026-09-25: FP8 E4M3, 1 byte per element, with one scale per tensor
30    /// (the `k_scale`/`v_scale` arguments of `reshape_and_cache_flash_fp8`).
31    Fp8,
32    /// 2026-09-25: E2M1 values, two per byte, plus one FP8 E4M3 scale byte per
33    /// 16-element group: 4.5 bits per element.
34    Nvfp4,
35    /// 2026-09-25: WHT-rotated 16-level Lloyd-Max codebook (`TURBO4_LUT`) in
36    /// the `Nvfp4` byte layout.
37    Turbo4,
38    /// 2026-09-25: WHT-rotated 8-level Lloyd-Max codebook (`TURBO3_LUT`), 8
39    /// values in 3 bytes plus one FP8 scale byte per group: 3.5 bits per
40    /// element, 22% smaller than `Turbo4`.
41    Turbo3,
42    /// 2026-09-25: WHT-rotated 4-level codebook, 4 values per byte plus one FP8
43    /// scale byte per group: 2.5 bits per element, 6.4x smaller than `Bf16`.
44    /// The server's `auto_high_precision_layers` gives this dtype a larger
45    /// high-precision boundary than the other symmetric turbo dtypes.
46    Turbo2,
47    /// 2026-09-25: WHT-rotated FP8 E4M3 data plus one BF16 scale per group: 9
48    /// bits per element, 12.5% more than `Fp8`.
49    Turbo8,
50    /// 2026-09-25: K `Turbo4`, V `Turbo3`: 8 bits per K+V element pair, against
51    /// 9 for `Turbo4`.
52    Turbo4KTurbo3V,
53    /// 2026-09-25: K `Turbo4`, V `Turbo8`: 13.5 bits per K+V element pair,
54    /// against 18 for `Turbo8`.
55    Turbo4KTurbo8V,
56    /// 2026-09-25: K `Turbo3`, V `Turbo8`.
57    Turbo3KTurbo8V,
58    /// 2026-09-25: K `Bf16`, V `Turbo4`.
59    Bf16KTurbo4V,
60    /// 2026-09-25: K `Bf16`, V `Turbo3`.
61    Bf16KTurbo3V,
62    /// 2026-09-25: K `Fp8`, V `Turbo4`.
63    Fp8KTurbo4V,
64    /// 2026-09-25: K `Fp8`, V `Turbo3`.
65    Fp8KTurbo3V,
66    /// 2026-09-25: K `Bf16`, V `Turbo2`.
67    Bf16KTurbo2V,
68    /// 2026-09-25: K `Fp8`, V `Turbo2`.
69    Fp8KTurbo2V,
70}
71
72impl std::fmt::Display for KvCacheDtype {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        // 2026-09-25: The spelling lives only in `name` (catalog.rs), whose
75        // match has no wildcard arm.
76        f.write_str(self.name())
77    }
78}
79
80impl KvCacheDtype {
81    /// 2026-09-25: The (K, V) formats. A symmetric variant returns
82    /// `(self, self)`.
83    pub fn kv_pair(self) -> (KvCacheDtype, KvCacheDtype) {
84        match self {
85            KvCacheDtype::Turbo4KTurbo3V => (KvCacheDtype::Turbo4, KvCacheDtype::Turbo3),
86            KvCacheDtype::Turbo4KTurbo8V => (KvCacheDtype::Turbo4, KvCacheDtype::Turbo8),
87            KvCacheDtype::Turbo3KTurbo8V => (KvCacheDtype::Turbo3, KvCacheDtype::Turbo8),
88            KvCacheDtype::Bf16KTurbo4V => (KvCacheDtype::Bf16, KvCacheDtype::Turbo4),
89            KvCacheDtype::Bf16KTurbo3V => (KvCacheDtype::Bf16, KvCacheDtype::Turbo3),
90            KvCacheDtype::Fp8KTurbo4V => (KvCacheDtype::Fp8, KvCacheDtype::Turbo4),
91            KvCacheDtype::Fp8KTurbo3V => (KvCacheDtype::Fp8, KvCacheDtype::Turbo3),
92            KvCacheDtype::Bf16KTurbo2V => (KvCacheDtype::Bf16, KvCacheDtype::Turbo2),
93            KvCacheDtype::Fp8KTurbo2V => (KvCacheDtype::Fp8, KvCacheDtype::Turbo2),
94            other => (other, other),
95        }
96    }
97
98    /// 2026-09-25: True for the symmetric turbo formats, whose cache contents
99    /// are in the WHT-rotated basis: `write_kv_cache.rs` runs
100    /// `wht_bf16_inplace` on K and V before the turbo write. The decode
101    /// attention calls it on each side of `kv_pair()`, applying the WHT to Q
102    /// when K is rotated and the inverse WHT to the output when V is rotated.
103    /// A `*K*V` variant itself returns false, so call it on a side.
104    pub fn is_wht_rotated(self) -> bool {
105        matches!(
106            self,
107            KvCacheDtype::Turbo2
108                | KvCacheDtype::Turbo3
109                | KvCacheDtype::Turbo4
110                | KvCacheDtype::Turbo8
111        )
112    }
113
114    /// 2026-09-25: True if K and V use different formats.
115    pub fn is_asymmetric(self) -> bool {
116        matches!(
117            self,
118            KvCacheDtype::Turbo4KTurbo3V
119                | KvCacheDtype::Turbo4KTurbo8V
120                | KvCacheDtype::Turbo3KTurbo8V
121                | KvCacheDtype::Bf16KTurbo4V
122                | KvCacheDtype::Bf16KTurbo3V
123                | KvCacheDtype::Fp8KTurbo4V
124                | KvCacheDtype::Fp8KTurbo3V
125                | KvCacheDtype::Bf16KTurbo2V
126                | KvCacheDtype::Fp8KTurbo2V
127        )
128    }
129}
130
131impl std::str::FromStr for KvCacheDtype {
132    type Err = anyhow::Error;
133    fn from_str(s: &str) -> Result<Self> {
134        match s {
135            "bf16" => Ok(KvCacheDtype::Bf16),
136            "fp8" => Ok(KvCacheDtype::Fp8),
137            "nvfp4" => Ok(KvCacheDtype::Nvfp4),
138            "turbo4" => Ok(KvCacheDtype::Turbo4),
139            "turbo3" => Ok(KvCacheDtype::Turbo3),
140            "turbo2" => Ok(KvCacheDtype::Turbo2),
141            "turbo8" => Ok(KvCacheDtype::Turbo8),
142            "turbo4k_turbo3v" | "turbo4k3v" => Ok(KvCacheDtype::Turbo4KTurbo3V),
143            "turbo4k_turbo8v" | "turbo4k8v" => Ok(KvCacheDtype::Turbo4KTurbo8V),
144            "turbo3k_turbo8v" | "turbo3k8v" => Ok(KvCacheDtype::Turbo3KTurbo8V),
145            "bf16k_turbo4v" | "bf16k4v" => Ok(KvCacheDtype::Bf16KTurbo4V),
146            "bf16k_turbo3v" | "bf16k3v" => Ok(KvCacheDtype::Bf16KTurbo3V),
147            "fp8k_turbo4v" | "fp8k4v" => Ok(KvCacheDtype::Fp8KTurbo4V),
148            "fp8k_turbo3v" | "fp8k3v" => Ok(KvCacheDtype::Fp8KTurbo3V),
149            "bf16k_turbo2v" | "bf16k2v" => Ok(KvCacheDtype::Bf16KTurbo2V),
150            "fp8k_turbo2v" | "fp8k2v" => Ok(KvCacheDtype::Fp8KTurbo2V),
151            other => bail!(
152                "Unsupported --kv-cache-dtype '{other}'. Symmetric: 'bf16', 'fp8', 'nvfp4', 'turbo4', 'turbo3', 'turbo8'. \
153                Asymmetric (TQ+): turbo*_turbo*v, bf16k_turbo[34]v (safer asym: K baseline, V compressed), fp8k_turbo[34]v."
154            ),
155        }
156    }
157}
158
159/// 2026-09-25: Geometry and formats of the paged KV cache.
160pub struct KvCacheConfig {
161    /// 2026-09-25: Tokens per block.
162    pub block_size: usize,
163    /// 2026-09-25: KV heads per layer, where `layer_dims` does not override it.
164    pub num_kv_heads: usize,
165    /// 2026-09-25: Head dimension, where `layer_dims` does not override it.
166    pub head_dim: usize,
167    /// 2026-09-25: Attention layers that get a K pool and a V pool.
168    pub num_layers: usize,
169    /// 2026-09-25: Format of every layer that `layer_dtypes` does not cover.
170    pub dtype: KvCacheDtype,
171    /// 2026-09-25: Per-layer format: layer `i` uses `layer_dtypes[i]` when
172    /// `i < layer_dtypes.len()`, else `dtype`.
173    pub layer_dtypes: Vec<KvCacheDtype>,
174    /// 2026-09-25: Per-layer `(num_kv_heads, head_dim)`, by the same rule as
175    /// `layer_dtypes`, for models whose attention layers differ in shape (the
176    /// Gemma-4 loader's `kv_layer_dims`). Pool sizes and block strides use it.
177    pub layer_dims: Vec<(usize, usize)>,
178    /// 2026-09-25: Per-sequence cap on HBM-resident blocks under
179    /// `--high-speed-swap`. With `Some(N)` the model engine slides a
180    /// sequence's window at `N` blocks (`ensure_blocks_through_decode`);
181    /// `None` means no cap.
182    pub cache_blocks_per_seq: Option<u32>,
183}
184
185impl KvCacheConfig {
186    /// 2026-09-25: The format of attention layer `layer_idx`.
187    pub fn dtype_for_layer(&self, layer_idx: usize) -> KvCacheDtype {
188        if layer_idx < self.layer_dtypes.len() {
189            self.layer_dtypes[layer_idx]
190        } else {
191            self.dtype
192        }
193    }
194
195    /// 2026-09-25: Bytes of one side of one block for `dtype` and
196    /// `(nkv, hd)`. A `*K*V` variant gives its K-side size.
197    fn block_bytes_dims(&self, dtype: KvCacheDtype, nkv: usize, hd: usize) -> usize {
198        let elems = self.block_size * nkv * hd;
199        match dtype {
200            KvCacheDtype::Bf16
201            | KvCacheDtype::Bf16KTurbo4V
202            | KvCacheDtype::Bf16KTurbo3V
203            | KvCacheDtype::Bf16KTurbo2V => elems * 2,
204            KvCacheDtype::Fp8
205            | KvCacheDtype::Fp8KTurbo4V
206            | KvCacheDtype::Fp8KTurbo3V
207            | KvCacheDtype::Fp8KTurbo2V => elems,
208            KvCacheDtype::Nvfp4
209            | KvCacheDtype::Turbo4
210            | KvCacheDtype::Turbo4KTurbo3V
211            | KvCacheDtype::Turbo4KTurbo8V => {
212                // 2026-09-25: Two 4-bit values per byte, then one FP8 scale
213                // byte per group. NVFP4 and Turbo4 differ only in codebook.
214                let data = elems / 2;
215                let num_groups = elems / NVFP4_GROUP_SIZE;
216                data + num_groups
217            }
218            KvCacheDtype::Turbo3 | KvCacheDtype::Turbo3KTurbo8V => {
219                // 2026-09-25: 8 values in 3 bytes, then one FP8 scale byte per
220                // group.
221                let data = elems * 3 / 8;
222                let num_groups = elems / NVFP4_GROUP_SIZE;
223                data + num_groups
224            }
225            KvCacheDtype::Turbo2 => {
226                // 2026-09-25: 4 values per byte, then one FP8 scale byte per
227                // group.
228                let data = elems / 4;
229                let num_groups = elems / NVFP4_GROUP_SIZE;
230                data + num_groups
231            }
232            KvCacheDtype::Turbo8 => {
233                // 2026-09-25: One FP8 byte per element, then one 2-byte BF16
234                // scale per group, where the layouts above use a 1-byte FP8
235                // scale.
236                let num_groups = elems / NVFP4_GROUP_SIZE;
237                elems + num_groups * 2
238            }
239        }
240    }
241
242    /// 2026-09-25: V-side bytes per block: `block_bytes_dims` of
243    /// `dtype.kv_pair().1`.
244    #[allow(dead_code)]
245    pub fn v_block_bytes_dims(&self, dtype: KvCacheDtype, nkv: usize, hd: usize) -> usize {
246        let (_, v) = dtype.kv_pair();
247        self.block_bytes_dims(v, nkv, hd)
248    }
249
250    /// 2026-09-25: K-side bytes per block: `block_bytes_dims` of
251    /// `dtype.kv_pair().0` (for `Bf16KTurbo3V`, the BF16 size).
252    #[allow(dead_code)]
253    pub fn k_block_bytes_dims(&self, dtype: KvCacheDtype, nkv: usize, hd: usize) -> usize {
254        let (k, _) = dtype.kv_pair();
255        self.block_bytes_dims(k, nkv, hd)
256    }
257
258    /// 2026-09-25: K-side bytes per block of attention layer `layer_idx`, from
259    /// its dims and the K side of its format. `PagedKvCache::new` sizes the K
260    /// pool with it.
261    pub fn k_block_bytes_for_layer(&self, layer_idx: usize) -> usize {
262        let (nkv, hd) = self.dims_for_layer(layer_idx);
263        let (k, _) = self.dtype_for_layer(layer_idx).kv_pair();
264        self.block_bytes_dims(k, nkv, hd)
265    }
266
267    /// 2026-09-25: V-side bytes per block of attention layer `layer_idx`.
268    /// Equals `k_block_bytes_for_layer` for a symmetric format.
269    pub fn v_block_bytes_for_layer(&self, layer_idx: usize) -> usize {
270        let (nkv, hd) = self.dims_for_layer(layer_idx);
271        let (_, v) = self.dtype_for_layer(layer_idx).kv_pair();
272        self.block_bytes_dims(v, nkv, hd)
273    }
274
275    /// 2026-09-25: `block_bytes_dims` with the global `num_kv_heads` and
276    /// `head_dim`, ignoring `layer_dims`.
277    fn block_bytes_for_dtype(&self, dtype: KvCacheDtype) -> usize {
278        self.block_bytes_dims(dtype, self.num_kv_heads, self.head_dim)
279    }
280
281    /// 2026-09-25: Bytes of one side of one block with the uniform `dtype` and
282    /// the global dims. For a `*K*V` dtype this is the K side.
283    pub fn block_bytes(&self) -> usize {
284        self.block_bytes_for_dtype(self.dtype)
285    }
286
287    /// 2026-09-25: `(num_kv_heads, head_dim)` of attention layer `layer_idx`:
288    /// `layer_dims[layer_idx]` when present, else the global values.
289    pub fn dims_for_layer(&self, layer_idx: usize) -> (usize, usize) {
290        if layer_idx < self.layer_dims.len() {
291            self.layer_dims[layer_idx]
292        } else {
293            (self.num_kv_heads, self.head_dim)
294        }
295    }
296
297    /// 2026-09-25: Bytes of one side of one block of attention layer
298    /// `layer_idx`, from its own dims and format. For a `*K*V` format this is
299    /// the K side.
300    pub fn block_bytes_for_layer(&self, layer_idx: usize) -> usize {
301        let (nkv, hd) = self.dims_for_layer(layer_idx);
302        self.block_bytes_dims(self.dtype_for_layer(layer_idx), nkv, hd)
303    }
304
305    /// 2026-09-25: Twice `block_bytes()`. That is K + V for a symmetric
306    /// uniform dtype, but twice the K side for a `*K*V` dtype.
307    pub fn block_bytes_kv(&self) -> usize {
308        self.block_bytes() * 2
309    }
310
311    /// 2026-09-25: K plus V bytes of one block index summed over all
312    /// layers, per layer's own dims and K and V formats.
313    pub fn block_bytes_kv_all_layers(&self) -> usize {
314        (0..self.num_layers)
315            .map(|i| self.k_block_bytes_for_layer(i) + self.v_block_bytes_for_layer(i))
316            .sum()
317    }
318
319    /// 2026-09-25: Elements per block and side, from the global dims:
320    /// `block_size * num_kv_heads * head_dim`.
321    pub fn cache_stride_elements(&self) -> usize {
322        self.block_size * self.num_kv_heads * self.head_dim
323    }
324
325    /// 2026-09-25: NVFP4 data bytes per block (two E2M1 values per byte),
326    /// from the global dims.
327    pub fn nvfp4_data_bytes(&self) -> usize {
328        self.block_size * self.num_kv_heads * self.head_dim / 2
329    }
330
331    /// 2026-09-25: NVFP4 scale bytes per block (one FP8 byte per group), from
332    /// the global dims.
333    pub fn nvfp4_scale_bytes(&self) -> usize {
334        self.block_size * self.num_kv_heads * self.head_dim / NVFP4_GROUP_SIZE
335    }
336
337    /// 2026-09-25: Turbo4 data bytes per block, equal to the NVFP4 value.
338    pub fn turbo4_data_bytes(&self) -> usize {
339        self.nvfp4_data_bytes()
340    }
341
342    /// 2026-09-25: Turbo4 scale bytes per block, equal to the NVFP4 value.
343    pub fn turbo4_scale_bytes(&self) -> usize {
344        self.nvfp4_scale_bytes()
345    }
346
347    /// 2026-09-25: Turbo3 data bytes per block (8 values in 3 bytes).
348    pub fn turbo3_data_bytes(&self) -> usize {
349        let elems = self.block_size * self.num_kv_heads * self.head_dim;
350        elems * 3 / 8
351    }
352
353    /// 2026-09-25: Turbo3 scale bytes per block, equal to the NVFP4 value.
354    pub fn turbo3_scale_bytes(&self) -> usize {
355        self.nvfp4_scale_bytes()
356    }
357
358    /// 2026-09-25: Turbo2 data bytes per block (4 values per byte).
359    pub fn turbo2_data_bytes(&self) -> usize {
360        let elems = self.block_size * self.num_kv_heads * self.head_dim;
361        elems / 4
362    }
363
364    /// 2026-09-25: Turbo2 scale bytes per block, equal to the NVFP4 value.
365    pub fn turbo2_scale_bytes(&self) -> usize {
366        self.nvfp4_scale_bytes()
367    }
368
369    /// 2026-09-25: Turbo8 data bytes per block (one FP8 byte per element).
370    pub fn turbo8_data_bytes(&self) -> usize {
371        self.block_size * self.num_kv_heads * self.head_dim
372    }
373
374    /// 2026-09-25: Turbo8 scale bytes per block: one 2-byte BF16 scale per
375    /// group, twice the NVFP4 value.
376    pub fn turbo8_scale_bytes(&self) -> usize {
377        self.nvfp4_scale_bytes() * 2
378    }
379}
380
381/// 2026-09-25: One attention layer's K and V pools.
382struct LayerPool {
383    k_pool: DevicePtr,
384    v_pool: DevicePtr,
385    /// 2026-09-25: Bytes between K blocks; differs from V for a `*K*V` format.
386    k_block_stride: usize,
387    /// 2026-09-25: Bytes between V blocks.
388    v_block_stride: usize,
389    /// 2026-09-25: This layer's format.
390    dtype: KvCacheDtype,
391}
392
393/// 2026-09-25: Paged KV cache across all attention layers: the pools and
394/// the block free list with per-block reference counts.
395pub struct PagedKvCache {
396    layers: Vec<LayerPool>,
397    num_blocks: usize,
398    free_blocks: Vec<u32>,
399    /// 2026-09-25: Per-block reference count, so a block can be shared (the
400    /// prefix cache holds one). Set to 1 on alloc; the block returns to the
401    /// free list when a decrement reaches 0.
402    block_ref_counts: Vec<u32>,
403    config: KvCacheConfig,
404    /// 2026-09-25: Per-block refcount event history (`METRALE_KV_TRACE=1`;
405    /// empty otherwise).
406    trace: block_trace::BlockTrace,
407}
408
409mod block_trace;
410mod catalog;
411mod paged_impl;
412/// 2026-09-25: Release both pools of every layer, and clear the free list and
413/// ref counts with them, so a released cache cannot hand out a block of freed
414/// memory: `alloc_block` then fails. Every pool is freed even after an error;
415/// the first error is returned.
416impl metrale_core::scope::ModelResource<dyn metrale_gpu_runtime::gpu::GpuBackend> for PagedKvCache {
417    fn label(&self) -> &'static str {
418        "kv cache"
419    }
420
421    fn release(&mut self, gpu: &dyn metrale_gpu_runtime::gpu::GpuBackend) -> anyhow::Result<()> {
422        let mut first_error = None;
423        for layer in self.layers.drain(..) {
424            for ptr in [layer.k_pool, layer.v_pool] {
425                if let Err(e) = gpu.free(ptr)
426                    && first_error.is_none()
427                {
428                    first_error = Some(e);
429                }
430            }
431        }
432        self.free_blocks.clear();
433        self.block_ref_counts.clear();
434        self.num_blocks = 0;
435        match first_error {
436            Some(e) => Err(e),
437            None => Ok(()),
438        }
439    }
440}
441
442#[cfg(test)]
443mod tests;
444
445#[cfg(test)]
446mod tests_tq_plus;