metrale_cache/kv_cache/
paged_impl.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `PagedKvCache` methods: pool allocation, the block free list
4//! and reference counts, block pointers and strides, host block copies, and
5//! BF16 fingerprint diagnostics. The struct is defined in `kv_cache.rs`.
6//!
7//! Owner: cache.
8//! Invariants: see the parent module.
9
10use anyhow::{Result, bail};
11
12use super::block_trace::BlockTrace;
13use super::{KvCacheConfig, KvCacheDtype, LayerPool, PagedKvCache};
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
15
16mod diagnostics;
17
18impl PagedKvCache {
19    /// 2026-09-25: Allocate a K pool and a V pool of `num_blocks` blocks for
20    /// every attention layer. All blocks start free with count 0.
21    pub fn new(config: KvCacheConfig, num_blocks: usize, gpu: &dyn GpuBackend) -> Result<Self> {
22        let mut layers = Vec::with_capacity(config.num_layers);
23        let mut total_bytes: usize = 0;
24        for i in 0..config.num_layers {
25            // 2026-09-25: K and V are sized separately: for `Bf16KTurbo3V` the
26            // K pool is BF16-sized and the V pool Turbo3-sized.
27            let k_block_bytes = config.k_block_bytes_for_layer(i);
28            let v_block_bytes = config.v_block_bytes_for_layer(i);
29            let k_pool_bytes = num_blocks * k_block_bytes;
30            let v_pool_bytes = num_blocks * v_block_bytes;
31            let k_pool = gpu.alloc(k_pool_bytes)?;
32            let v_pool = gpu.alloc(v_pool_bytes)?;
33            total_bytes += k_pool_bytes + v_pool_bytes;
34            layers.push(LayerPool {
35                k_pool,
36                v_pool,
37                k_block_stride: k_block_bytes,
38                v_block_stride: v_block_bytes,
39                dtype: config.dtype_for_layer(i),
40            });
41        }
42
43        let free_blocks: Vec<u32> = (0..num_blocks as u32).rev().collect();
44        let block_ref_counts = vec![0u32; num_blocks];
45
46        let has_mixed = !config.layer_dtypes.is_empty()
47            && config.layer_dtypes.iter().any(|d| *d != config.dtype);
48        if has_mixed {
49            let hp_count = config
50                .layer_dtypes
51                .iter()
52                .filter(|d| **d != config.dtype)
53                .count();
54            tracing::info!(
55                "KV cache: {} blocks × {} layers ({} high-precision) = {:.1} GB total (mixed dtype)",
56                num_blocks,
57                config.num_layers,
58                hp_count,
59                total_bytes as f64 / (1024.0 * 1024.0 * 1024.0),
60            );
61        } else {
62            tracing::info!(
63                "KV cache: {} blocks × {} layers × {} bytes/block = {:.1} GB total",
64                num_blocks,
65                config.num_layers,
66                config.block_bytes_kv(),
67                (num_blocks * config.num_layers * config.block_bytes_kv()) as f64
68                    / (1024.0 * 1024.0 * 1024.0),
69            );
70        }
71
72        Ok(Self {
73            layers,
74            num_blocks,
75            free_blocks,
76            block_ref_counts,
77            config,
78            trace: BlockTrace::new(num_blocks),
79        })
80    }
81
82    /// 2026-09-25: Pop a free block and set its count to 1. Errors when the
83    /// free list is empty.
84    #[track_caller]
85    pub fn alloc_block(&mut self) -> Result<u32> {
86        let idx = self
87            .free_blocks
88            .pop()
89            .ok_or_else(|| anyhow::anyhow!("KV cache exhausted: no free blocks"))?;
90        self.block_ref_counts[idx as usize] = 1;
91        if self.trace.is_on() {
92            self.trace
93                .record(idx as usize, "alloc", 1, std::panic::Location::caller());
94        }
95        Ok(idx)
96    }
97
98    /// 2026-09-25: Zero the block's K and V bytes in every layer, enqueued on
99    /// `stream`.
100    pub fn zero_block(
101        &self,
102        block_idx: u32,
103        gpu: &dyn metrale_gpu_runtime::gpu::GpuBackend,
104        stream: u64,
105    ) -> anyhow::Result<()> {
106        for layer in &self.layers {
107            let k_offset = block_idx as usize * layer.k_block_stride;
108            let v_offset = block_idx as usize * layer.v_block_stride;
109            gpu.memset_async(
110                layer.k_pool.offset(k_offset),
111                0,
112                layer.k_block_stride,
113                stream,
114            )?;
115            gpu.memset_async(
116                layer.v_pool.offset(v_offset),
117                0,
118                layer.v_block_stride,
119                stream,
120            )?;
121        }
122        Ok(())
123    }
124
125    /// 2026-09-25: Diagnostic twin of `zero_block` that fills with 0xFF.
126    /// `0xFFFF` is a BF16 NaN and `0xFF` an FP8 E4M3 NaN, so attention over a
127    /// slot nothing wrote yields NaN instead of zeros. The model engine
128    /// calls it instead of `zero_block` under `METRALE_KV_POISON`.
129    pub fn poison_block(
130        &self,
131        block_idx: u32,
132        gpu: &dyn metrale_gpu_runtime::gpu::GpuBackend,
133        stream: u64,
134    ) -> anyhow::Result<()> {
135        for layer in &self.layers {
136            let k_offset = block_idx as usize * layer.k_block_stride;
137            let v_offset = block_idx as usize * layer.v_block_stride;
138            gpu.memset_async(
139                layer.k_pool.offset(k_offset),
140                0xFF,
141                layer.k_block_stride,
142                stream,
143            )?;
144            gpu.memset_async(
145                layer.v_pool.offset(v_offset),
146                0xFF,
147                layer.v_block_stride,
148                stream,
149            )?;
150        }
151        Ok(())
152    }
153
154    /// 2026-09-25: `alloc_block`, returning `None` when the free list is empty.
155    #[track_caller]
156    pub fn try_alloc_block(&mut self) -> Option<u32> {
157        let idx = self.free_blocks.pop()?;
158        self.block_ref_counts[idx as usize] = 1;
159        if self.trace.is_on() {
160            self.trace
161                .record(idx as usize, "try_alloc", 1, std::panic::Location::caller());
162        }
163        Some(idx)
164    }
165
166    /// 2026-09-25: Add one reference to a block. There is no check that the
167    /// block is allocated.
168    #[track_caller]
169    pub fn inc_ref(&mut self, block_idx: u32) {
170        debug_assert!((block_idx as usize) < self.num_blocks);
171        self.block_ref_counts[block_idx as usize] += 1;
172        if self.trace.is_on() {
173            let after = self.block_ref_counts[block_idx as usize];
174            self.trace.record(
175                block_idx as usize,
176                "inc",
177                after,
178                std::panic::Location::caller(),
179            );
180        }
181    }
182
183    /// 2026-09-25: Drop one reference. Returns true if the count reached 0 and
184    /// the block went back on the free list. At 0 refs a debug build panics;
185    /// a release build logs an error and returns false.
186    #[track_caller]
187    pub fn dec_ref(&mut self, block_idx: u32) -> bool {
188        let idx = block_idx as usize;
189        debug_assert!(idx < self.num_blocks);
190        // 2026-09-25: Refused rather than decremented: the `debug_assert` is
191        // compiled out in release, and the workspace sets no `overflow-checks`,
192        // so the subtraction would wrap to u32::MAX and the block would never
193        // return to the free list.
194        debug_assert!(
195            self.block_ref_counts[idx] > 0,
196            "dec_ref on block with 0 refs"
197        );
198        if self.block_ref_counts[idx] == 0 {
199            let caller = std::panic::Location::caller();
200            tracing::error!(
201                "dec_ref on block {block_idx} with 0 refs (from {caller}) — refcount bug \
202                 (ignoring; would otherwise wrap to u32::MAX and pin the block){}",
203                if self.trace.is_on() {
204                    format!("\n  history: {}", self.trace.dump(idx))
205                } else {
206                    String::from(" [set METRALE_KV_TRACE=1 for this block's ref history]")
207                }
208            );
209            return false;
210        }
211        self.block_ref_counts[idx] -= 1;
212        if self.trace.is_on() {
213            let after = self.block_ref_counts[idx];
214            self.trace
215                .record(idx, "dec", after, std::panic::Location::caller());
216        }
217        if self.block_ref_counts[idx] == 0 {
218            self.free_blocks.push(block_idx);
219            true
220        } else {
221            false
222        }
223    }
224
225    /// 2026-09-25: `dec_ref`, discarding the result.
226    #[track_caller]
227    pub fn free_block(&mut self, block_idx: u32) {
228        self.dec_ref(block_idx);
229    }
230
231    /// 2026-09-25: `free_block` on every entry of `block_table`.
232    #[track_caller]
233    pub fn free_blocks(&mut self, block_table: &[u32]) {
234        for &idx in block_table {
235            self.free_block(idx);
236        }
237    }
238
239    /// 2026-09-25: Release the prefix cache's own reference on a block its
240    /// eviction returned: one decrement, and a push onto the free list only on
241    /// a 1 to 0 transition. At 0 refs it logs a warning and does nothing.
242    #[track_caller]
243    pub fn return_evicted_block(&mut self, block_idx: u32) {
244        let idx = block_idx as usize;
245        debug_assert!(idx < self.num_blocks);
246        // 2026-09-25: One decrement, not a reset to 0: a sequence can still
247        // hold the block in its block table, and freeing it would hand a live
248        // block to the next `alloc_block`. The cache's reference is the one
249        // `cache_acquires_refs` (model-engine `block_mgmt.rs`) took on insert.
250        //
251        // At 0 refs the block is already on the free list; a second push would
252        // let two `alloc_block` calls hand the same block to two sequences.
253        if self.block_ref_counts[idx] == 0 {
254            tracing::warn!(
255                "return_evicted_block({block_idx}) with 0 refs (from {}) — the prefix cache \
256                 returned a block it holds no reference on; ignoring (re-pushing it would \
257                 duplicate a free-list entry and alias the block across sequences){}",
258                std::panic::Location::caller(),
259                if self.trace.is_on() {
260                    format!("\n  history: {}", self.trace.dump(idx))
261                } else {
262                    String::new()
263                }
264            );
265            return;
266        }
267        self.block_ref_counts[idx] -= 1;
268        if self.trace.is_on() {
269            let after = self.block_ref_counts[idx];
270            self.trace
271                .record(idx, "evict_return", after, std::panic::Location::caller());
272        }
273        if self.block_ref_counts[idx] == 0 {
274            self.free_blocks.push(idx as u32);
275        }
276    }
277
278    /// 2026-09-25: The block's reference count.
279    pub fn ref_count(&self, block_idx: u32) -> u32 {
280        self.block_ref_counts[block_idx as usize]
281    }
282
283    /// 2026-09-25: Length of the free list.
284    pub fn num_free_blocks(&self) -> usize {
285        self.free_blocks.len()
286    }
287
288    /// 2026-09-25: Device address of a block in a layer's K pool.
289    pub fn k_cache_ptr(&self, layer_idx: usize, block_idx: u32) -> DevicePtr {
290        let layer = &self.layers[layer_idx];
291        layer
292            .k_pool
293            .offset(block_idx as usize * layer.k_block_stride)
294    }
295
296    /// 2026-09-25: Device address of a block in a layer's V pool.
297    pub fn v_cache_ptr(&self, layer_idx: usize, block_idx: u32) -> DevicePtr {
298        let layer = &self.layers[layer_idx];
299        layer
300            .v_pool
301            .offset(block_idx as usize * layer.v_block_stride)
302    }
303
304    /// 2026-09-25: Base address of a layer's K pool.
305    pub fn k_pool_ptr(&self, layer_idx: usize) -> DevicePtr {
306        self.layers[layer_idx].k_pool
307    }
308
309    /// 2026-09-25: Base address of a layer's V pool.
310    pub fn v_pool_ptr(&self, layer_idx: usize) -> DevicePtr {
311        self.layers[layer_idx].v_pool
312    }
313
314    /// 2026-09-25: `KvCacheConfig::cache_stride_elements`: elements per block
315    /// and side from the global dims, not from `layer_dims`.
316    pub fn cache_stride(&self) -> usize {
317        self.config.cache_stride_elements()
318    }
319
320    /// 2026-09-25: `KvCacheConfig::block_bytes`: uniform dtype, global dims.
321    pub fn block_stride_bytes(&self) -> usize {
322        self.config.block_bytes()
323    }
324
325    /// 2026-09-25: The K-side block stride of attention layer `layer_idx`,
326    /// which is also the V-side stride for a symmetric format.
327    pub fn block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
328        self.layers[layer_idx].k_block_stride
329    }
330
331    /// 2026-09-25: The K-side block stride of attention layer `layer_idx`.
332    pub fn k_block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
333        self.layers[layer_idx].k_block_stride
334    }
335
336    /// 2026-09-25: The V-side block stride of attention layer `layer_idx`.
337    pub fn v_block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
338        self.layers[layer_idx].v_block_stride
339    }
340
341    /// 2026-09-25: `KvCacheConfig::nvfp4_data_bytes`.
342    pub fn nvfp4_data_bytes(&self) -> usize {
343        self.config.nvfp4_data_bytes()
344    }
345
346    /// 2026-09-25: `KvCacheConfig::turbo4_data_bytes`.
347    pub fn turbo4_data_bytes(&self) -> usize {
348        self.config.turbo4_data_bytes()
349    }
350
351    /// 2026-09-25: `KvCacheConfig::turbo3_data_bytes`.
352    pub fn turbo3_data_bytes(&self) -> usize {
353        self.config.turbo3_data_bytes()
354    }
355
356    /// 2026-09-25: `KvCacheConfig::turbo2_data_bytes`.
357    pub fn turbo2_data_bytes(&self) -> usize {
358        self.config.turbo2_data_bytes()
359    }
360
361    /// 2026-09-25: `KvCacheConfig::turbo8_data_bytes`.
362    pub fn turbo8_data_bytes(&self) -> usize {
363        self.config.turbo8_data_bytes()
364    }
365
366    /// 2026-09-25: `KvCacheConfig::turbo4_scale_bytes`.
367    pub fn turbo4_scale_bytes(&self) -> usize {
368        self.config.turbo4_scale_bytes()
369    }
370
371    /// 2026-09-25: The configuration the cache was built with.
372    pub fn config(&self) -> &KvCacheConfig {
373        &self.config
374    }
375
376    /// 2026-09-25: The format of attention layer `layer_idx`.
377    pub fn dtype_for_layer(&self, layer_idx: usize) -> KvCacheDtype {
378        self.layers[layer_idx].dtype
379    }
380
381    pub fn block_size(&self) -> usize {
382        self.config.block_size
383    }
384
385    pub fn num_blocks(&self) -> usize {
386        self.num_blocks
387    }
388
389    pub fn dtype(&self) -> KvCacheDtype {
390        self.config.dtype
391    }
392
393    /// 2026-09-25: `KvCacheConfig::num_layers`.
394    pub fn num_layers(&self) -> usize {
395        self.config.num_layers
396    }
397
398    /// 2026-09-25: Copy one block of one layer to the host, as `(k, v)`, each
399    /// the length of its side's block stride.
400    pub fn read_block(
401        &self,
402        layer_idx: usize,
403        block_idx: u32,
404        gpu: &dyn GpuBackend,
405    ) -> Result<(Vec<u8>, Vec<u8>)> {
406        let k_stride = self.layers[layer_idx].k_block_stride;
407        let v_stride = self.layers[layer_idx].v_block_stride;
408        let k_ptr = self.k_cache_ptr(layer_idx, block_idx);
409        let v_ptr = self.v_cache_ptr(layer_idx, block_idx);
410
411        let mut k_data = vec![0u8; k_stride];
412        let mut v_data = vec![0u8; v_stride];
413        gpu.copy_d2h(k_ptr, &mut k_data)?;
414        gpu.copy_d2h(v_ptr, &mut v_data)?;
415
416        Ok((k_data, v_data))
417    }
418
419    /// 2026-09-25: Copy `k_data` and `v_data` from the host to the start of one
420    /// block of one layer.
421    pub fn write_block(
422        &self,
423        layer_idx: usize,
424        block_idx: u32,
425        k_data: &[u8],
426        v_data: &[u8],
427        gpu: &dyn GpuBackend,
428    ) -> Result<()> {
429        let k_ptr = self.k_cache_ptr(layer_idx, block_idx);
430        let v_ptr = self.v_cache_ptr(layer_idx, block_idx);
431        gpu.copy_h2d(k_data, k_ptr)?;
432        gpu.copy_h2d(v_data, v_ptr)?;
433        Ok(())
434    }
435
436    /// 2026-09-25: How many block indices fit in `available_bytes`, at
437    /// `block_bytes_kv_all_layers` each. Errors if that is zero.
438    pub fn compute_num_blocks(config: &KvCacheConfig, available_bytes: usize) -> Result<usize> {
439        let bytes_per_block = config.block_bytes_kv_all_layers();
440        if bytes_per_block == 0 {
441            bail!("KV cache block size is zero");
442        }
443        Ok(available_bytes / bytes_per_block)
444    }
445}