1use 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 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 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 #[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 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 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 #[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 #[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 #[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 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 #[track_caller]
227 pub fn free_block(&mut self, block_idx: u32) {
228 self.dec_ref(block_idx);
229 }
230
231 #[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 #[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 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 pub fn ref_count(&self, block_idx: u32) -> u32 {
280 self.block_ref_counts[block_idx as usize]
281 }
282
283 pub fn num_free_blocks(&self) -> usize {
285 self.free_blocks.len()
286 }
287
288 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 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 pub fn k_pool_ptr(&self, layer_idx: usize) -> DevicePtr {
306 self.layers[layer_idx].k_pool
307 }
308
309 pub fn v_pool_ptr(&self, layer_idx: usize) -> DevicePtr {
311 self.layers[layer_idx].v_pool
312 }
313
314 pub fn cache_stride(&self) -> usize {
317 self.config.cache_stride_elements()
318 }
319
320 pub fn block_stride_bytes(&self) -> usize {
322 self.config.block_bytes()
323 }
324
325 pub fn block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
328 self.layers[layer_idx].k_block_stride
329 }
330
331 pub fn k_block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
333 self.layers[layer_idx].k_block_stride
334 }
335
336 pub fn v_block_stride_bytes_for_layer(&self, layer_idx: usize) -> usize {
338 self.layers[layer_idx].v_block_stride
339 }
340
341 pub fn nvfp4_data_bytes(&self) -> usize {
343 self.config.nvfp4_data_bytes()
344 }
345
346 pub fn turbo4_data_bytes(&self) -> usize {
348 self.config.turbo4_data_bytes()
349 }
350
351 pub fn turbo3_data_bytes(&self) -> usize {
353 self.config.turbo3_data_bytes()
354 }
355
356 pub fn turbo2_data_bytes(&self) -> usize {
358 self.config.turbo2_data_bytes()
359 }
360
361 pub fn turbo8_data_bytes(&self) -> usize {
363 self.config.turbo8_data_bytes()
364 }
365
366 pub fn turbo4_scale_bytes(&self) -> usize {
368 self.config.turbo4_scale_bytes()
369 }
370
371 pub fn config(&self) -> &KvCacheConfig {
373 &self.config
374 }
375
376 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 pub fn num_layers(&self) -> usize {
395 self.config.num_layers
396 }
397
398 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 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 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}