1use std::sync::{Arc, Mutex};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct KvPoolExhausted;
29
30impl std::fmt::Display for KvPoolExhausted {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "KV cache block pool exhausted: no free blocks remain")
33 }
34}
35
36impl std::error::Error for KvPoolExhausted {}
37
38pub struct KvBlockPool {
46 block_size: usize,
47 total_blocks: usize,
48 free_blocks: usize,
49}
50
51impl KvBlockPool {
52 pub fn new(block_size: usize, total_blocks: usize) -> Self {
56 assert!(block_size > 0, "block_size must be positive");
57 KvBlockPool {
58 block_size,
59 total_blocks,
60 free_blocks: total_blocks,
61 }
62 }
63
64 pub fn block_size(&self) -> usize {
65 self.block_size
66 }
67
68 pub fn total_blocks(&self) -> usize {
69 self.total_blocks
70 }
71
72 pub fn free_blocks(&self) -> usize {
73 self.free_blocks
74 }
75
76 fn try_acquire(&mut self, n: usize) -> bool {
77 if n <= self.free_blocks {
78 self.free_blocks -= n;
79 true
80 } else {
81 false
82 }
83 }
84
85 fn release(&mut self, n: usize) {
86 self.free_blocks = (self.free_blocks + n).min(self.total_blocks);
87 }
88}
89
90struct PooledState {
91 pool: Arc<Mutex<KvBlockPool>>,
92 block_size: usize,
93 blocks_held: usize,
94}
95
96pub struct KvCache {
97 pub n_kv_heads: usize,
98 pub head_dim: usize,
99 pub k: Vec<f32>, pub v: Vec<f32>,
101 pub seq_len: usize,
102 planned_capacity: Option<usize>,
105 pool_state: Option<PooledState>,
109}
110
111impl Clone for KvCache {
118 fn clone(&self) -> Self {
119 KvCache {
120 n_kv_heads: self.n_kv_heads,
121 head_dim: self.head_dim,
122 k: self.k.clone(),
123 v: self.v.clone(),
124 seq_len: self.seq_len,
125 planned_capacity: self.planned_capacity,
126 pool_state: None,
127 }
128 }
129}
130
131impl Drop for KvCache {
132 fn drop(&mut self) {
133 if let Some(state) = &self.pool_state {
134 if let Ok(mut pool) = state.pool.lock() {
135 pool.release(state.blocks_held);
136 }
137 }
138 }
139}
140
141impl KvCache {
142 pub fn new(n_kv_heads: usize, head_dim: usize) -> Self {
143 KvCache {
144 n_kv_heads,
145 head_dim,
146 k: Vec::new(),
147 v: Vec::new(),
148 seq_len: 0,
149 planned_capacity: None,
150 pool_state: None,
151 }
152 }
153
154 pub fn with_capacity(n_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
158 let elems_per_position = n_kv_heads * head_dim;
159 KvCache {
160 n_kv_heads,
161 head_dim,
162 k: Vec::with_capacity(max_seq_len * elems_per_position),
163 v: Vec::with_capacity(max_seq_len * elems_per_position),
164 seq_len: 0,
165 planned_capacity: Some(max_seq_len),
166 pool_state: None,
167 }
168 }
169
170 pub fn with_pool(
189 n_kv_heads: usize,
190 head_dim: usize,
191 pool: Arc<Mutex<KvBlockPool>>,
192 max_seq_len: usize,
193 ) -> Result<Self, KvPoolExhausted> {
194 let block_size = pool.lock().unwrap().block_size();
195 let blocks_needed = max_seq_len.div_ceil(block_size).max(1);
196 if !pool.lock().unwrap().try_acquire(blocks_needed) {
197 return Err(KvPoolExhausted);
198 }
199 let elems_per_position = n_kv_heads * head_dim;
200 Ok(KvCache {
201 n_kv_heads,
202 head_dim,
203 k: Vec::with_capacity(blocks_needed * block_size * elems_per_position),
204 v: Vec::with_capacity(blocks_needed * block_size * elems_per_position),
205 seq_len: 0,
206 planned_capacity: None,
207 pool_state: Some(PooledState {
208 pool,
209 block_size,
210 blocks_held: blocks_needed,
211 }),
212 })
213 }
214
215 pub fn push(&mut self, k_step: &[f32], v_step: &[f32]) -> Result<(), KvPoolExhausted> {
222 assert_eq!(k_step.len(), self.n_kv_heads * self.head_dim);
223 assert_eq!(v_step.len(), self.n_kv_heads * self.head_dim);
224
225 let elems_per_position = self.n_kv_heads * self.head_dim;
226 if let Some(state) = &mut self.pool_state {
227 let capacity_positions = self.k.capacity() / elems_per_position;
228 if self.seq_len == capacity_positions {
229 if !state.pool.lock().unwrap().try_acquire(1) {
230 return Err(KvPoolExhausted);
231 }
232 state.blocks_held += 1;
233 self.k.reserve_exact(state.block_size * elems_per_position);
234 self.v.reserve_exact(state.block_size * elems_per_position);
235 }
236 }
237
238 self.k.extend_from_slice(k_step);
239 self.v.extend_from_slice(v_step);
240 self.seq_len += 1;
241 Ok(())
242 }
243
244 pub fn advance_len(&mut self, n: usize) -> Result<(), KvPoolExhausted> {
248 if n == 0 {
249 return Ok(());
250 }
251 let elems_per_position = self.n_kv_heads * self.head_dim;
252 let zeros = vec![0f32; elems_per_position];
253 for _ in 0..n {
254 self.push(&zeros, &zeros)?;
255 }
256 Ok(())
257 }
258
259 pub fn release_to_pool(&mut self) {
264 if let Some(state) = self.pool_state.take() {
265 if let Ok(mut pool) = state.pool.lock() {
266 pool.release(state.blocks_held);
267 }
268 }
269 }
270
271 pub fn clear(&mut self) {
272 self.k.clear();
273 self.v.clear();
274 self.seq_len = 0;
275 }
276
277 pub fn truncate(&mut self, new_seq_len: usize) {
284 assert!(
285 new_seq_len <= self.seq_len,
286 "truncate target {new_seq_len} must not exceed current seq_len {}",
287 self.seq_len
288 );
289 let elems_per_position = self.n_kv_heads * self.head_dim;
290 self.k.truncate(new_seq_len * elems_per_position);
291 self.v.truncate(new_seq_len * elems_per_position);
292 self.seq_len = new_seq_len;
293 }
294
295 pub fn allocated_bytes(&self) -> usize {
300 (self.k.capacity() + self.v.capacity()) * std::mem::size_of::<f32>()
301 }
302
303 pub fn is_within_planned_capacity(&self) -> bool {
308 match self.planned_capacity {
309 Some(cap) => {
310 self.seq_len <= cap
311 && self.k.capacity() >= self.seq_len * self.n_kv_heads * self.head_dim
312 }
313 None => false,
314 }
315 }
316}
317
318pub struct PagedKvStore {
332 block_size: usize,
333 n_kv_heads: usize,
334 head_dim: usize,
335 k: Vec<f32>, v: Vec<f32>,
337 free_block_ids: Vec<usize>,
338}
339
340impl PagedKvStore {
341 pub fn new(block_size: usize, total_blocks: usize, n_kv_heads: usize, head_dim: usize) -> Self {
342 assert!(block_size > 0, "block_size must be positive");
343 let elems_per_block = block_size * n_kv_heads * head_dim;
344 PagedKvStore {
345 block_size,
346 n_kv_heads,
347 head_dim,
348 k: vec![0.0; total_blocks * elems_per_block],
349 v: vec![0.0; total_blocks * elems_per_block],
350 free_block_ids: (0..total_blocks).rev().collect(),
354 }
355 }
356
357 pub fn block_size(&self) -> usize {
358 self.block_size
359 }
360
361 pub fn free_block_count(&self) -> usize {
362 self.free_block_ids.len()
363 }
364
365 fn acquire_block(&mut self) -> Option<usize> {
366 self.free_block_ids.pop()
367 }
368
369 fn release_block(&mut self, id: usize) {
370 self.free_block_ids.push(id);
371 }
372
373 fn elems_per_block(&self) -> usize {
374 self.block_size * self.n_kv_heads * self.head_dim
375 }
376
377 pub fn k_row(&self, id: usize, offset: usize) -> &[f32] {
383 let elems_per_position = self.n_kv_heads * self.head_dim;
384 let start = id * self.elems_per_block() + offset * elems_per_position;
385 &self.k[start..start + elems_per_position]
386 }
387
388 pub fn v_row(&self, id: usize, offset: usize) -> &[f32] {
389 let elems_per_position = self.n_kv_heads * self.head_dim;
390 let start = id * self.elems_per_block() + offset * elems_per_position;
391 &self.v[start..start + elems_per_position]
392 }
393
394 fn k_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
395 let elems_per_position = self.n_kv_heads * self.head_dim;
396 let start = id * self.elems_per_block() + offset * elems_per_position;
397 &mut self.k[start..start + elems_per_position]
398 }
399
400 fn v_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
401 let elems_per_position = self.n_kv_heads * self.head_dim;
402 let start = id * self.elems_per_block() + offset * elems_per_position;
403 &mut self.v[start..start + elems_per_position]
404 }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub struct PagedStoreExhausted;
412
413impl std::fmt::Display for PagedStoreExhausted {
414 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415 write!(f, "paged KV store exhausted: no free blocks remain")
416 }
417}
418
419impl std::error::Error for PagedStoreExhausted {}
420
421#[derive(Debug, Clone, Default)]
427pub struct PagedKvCache {
428 block_table: Vec<usize>,
429 seq_len: usize,
430}
431
432impl PagedKvCache {
433 pub fn new() -> Self {
434 PagedKvCache {
435 block_table: Vec::new(),
436 seq_len: 0,
437 }
438 }
439
440 pub fn seq_len(&self) -> usize {
441 self.seq_len
442 }
443
444 pub fn block_table(&self) -> &[usize] {
445 &self.block_table
446 }
447
448 pub fn push(
453 &mut self,
454 store: &mut PagedKvStore,
455 k_step: &[f32],
456 v_step: &[f32],
457 ) -> Result<(), PagedStoreExhausted> {
458 let block_size = store.block_size();
459 let offset_in_block = self.seq_len % block_size;
460 if offset_in_block == 0 {
461 let id = store.acquire_block().ok_or(PagedStoreExhausted)?;
462 self.block_table.push(id);
463 }
464 let block_id = *self
465 .block_table
466 .last()
467 .expect("offset_in_block == 0 branch above always pushes one first");
468 store
469 .k_row_mut(block_id, offset_in_block)
470 .copy_from_slice(k_step);
471 store
472 .v_row_mut(block_id, offset_in_block)
473 .copy_from_slice(v_step);
474 self.seq_len += 1;
475 Ok(())
476 }
477
478 pub fn release(&mut self, store: &mut PagedKvStore) {
483 for id in self.block_table.drain(..) {
484 store.release_block(id);
485 }
486 self.seq_len = 0;
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn push_grows_seq_len_and_stores_values() {
496 let mut cache = KvCache::new(2, 2);
497 cache
498 .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
499 .unwrap();
500 assert_eq!(cache.seq_len, 1);
501 cache
502 .push(&[9.0, 10.0, 11.0, 12.0], &[13.0, 14.0, 15.0, 16.0])
503 .unwrap();
504 assert_eq!(cache.seq_len, 2);
505 assert_eq!(cache.k.len(), 2 * 2 * 2);
506 assert_eq!(cache.k[4], 9.0);
507 }
508
509 #[test]
510 #[should_panic]
511 fn push_wrong_size_panics() {
512 let mut cache = KvCache::new(2, 2);
513 let _ = cache.push(&[1.0, 2.0], &[1.0, 2.0]); }
515
516 #[test]
517 fn clear_resets_state() {
518 let mut cache = KvCache::new(1, 1);
519 cache.push(&[1.0], &[2.0]).unwrap();
520 cache.clear();
521 assert_eq!(cache.seq_len, 0);
522 assert!(cache.k.is_empty());
523 }
524
525 #[test]
526 fn truncate_rolls_back_to_exact_length_preserving_earlier_data() {
527 let mut cache = KvCache::new(2, 2);
528 cache
529 .push(&[1.0, 2.0, 3.0, 4.0], &[10.0, 20.0, 30.0, 40.0])
530 .unwrap();
531 cache
532 .push(&[5.0, 6.0, 7.0, 8.0], &[50.0, 60.0, 70.0, 80.0])
533 .unwrap();
534 cache
535 .push(&[9.0, 9.0, 9.0, 9.0], &[90.0, 90.0, 90.0, 90.0])
536 .unwrap();
537 assert_eq!(cache.seq_len, 3);
538
539 cache.truncate(1);
540 assert_eq!(cache.seq_len, 1);
541 assert_eq!(cache.k, vec![1.0, 2.0, 3.0, 4.0]);
542 assert_eq!(cache.v, vec![10.0, 20.0, 30.0, 40.0]);
543 }
544
545 #[test]
546 fn truncate_to_current_length_is_a_no_op() {
547 let mut cache = KvCache::new(1, 2);
548 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
549 cache.truncate(1);
550 assert_eq!(cache.seq_len, 1);
551 assert_eq!(cache.k, vec![1.0, 2.0]);
552 }
553
554 #[test]
555 fn truncate_to_zero_empties_the_cache() {
556 let mut cache = KvCache::new(1, 2);
557 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
558 cache.truncate(0);
559 assert_eq!(cache.seq_len, 0);
560 assert!(cache.k.is_empty());
561 assert!(cache.v.is_empty());
562 }
563
564 #[test]
565 #[should_panic]
566 fn truncate_beyond_current_length_panics() {
567 let mut cache = KvCache::new(1, 2);
568 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
569 cache.truncate(5);
570 }
571
572 #[test]
573 fn push_after_truncate_continues_correctly() {
574 let mut cache = KvCache::new(1, 1);
575 cache.push(&[1.0], &[10.0]).unwrap();
576 cache.push(&[2.0], &[20.0]).unwrap();
577 cache.push(&[3.0], &[30.0]).unwrap(); cache.truncate(2);
579 cache.push(&[99.0], &[990.0]).unwrap(); assert_eq!(cache.seq_len, 3);
581 assert_eq!(cache.k, vec![1.0, 2.0, 99.0]);
582 assert_eq!(cache.v, vec![10.0, 20.0, 990.0]);
583 }
584
585 #[test]
586 fn with_capacity_preallocates_and_never_reallocates_within_plan() {
587 let n_kv_heads = 4;
588 let head_dim = 8;
589 let max_seq_len = 16;
590 let mut cache = KvCache::with_capacity(n_kv_heads, head_dim, max_seq_len);
591
592 let expected_elems = max_seq_len * n_kv_heads * head_dim;
593 assert!(cache.k.capacity() >= expected_elems);
594 assert!(cache.v.capacity() >= expected_elems);
595
596 let step = vec![0.5f32; n_kv_heads * head_dim];
597 let k_ptr_before = cache.k.as_ptr();
598 for _ in 0..max_seq_len {
599 cache.push(&step, &step).unwrap();
600 }
601 let k_ptr_after = cache.k.as_ptr();
602 assert_eq!(
603 k_ptr_before, k_ptr_after,
604 "pushing exactly up to the planned capacity must not reallocate"
605 );
606 assert!(cache.is_within_planned_capacity());
607 }
608
609 #[test]
610 fn allocated_bytes_reflects_preallocated_capacity_not_just_used_length() {
611 let cache = KvCache::with_capacity(4, 8, 100);
612 let expected_min = 100 * 4 * 8 * 2 * 4;
614 assert!(
615 cache.allocated_bytes() >= expected_min,
616 "allocated_bytes={} expected_min={expected_min}",
617 cache.allocated_bytes()
618 );
619 assert_eq!(cache.seq_len, 0);
621 }
622
623 #[test]
624 fn grow_as_you_go_cache_reports_not_within_planned_capacity() {
625 let mut cache = KvCache::new(2, 2);
626 cache
627 .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
628 .unwrap();
629 assert!(
630 !cache.is_within_planned_capacity(),
631 "a cache built with `new` has no plan to be within"
632 );
633 }
634
635 #[test]
636 fn with_pool_acquires_one_block_and_reports_it_in_free_blocks() {
637 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 10)));
638 let cache = KvCache::with_pool(2, 2, pool.clone(), 0).unwrap();
639 assert_eq!(pool.lock().unwrap().free_blocks(), 9);
640 assert_eq!(cache.seq_len, 0);
641 }
642
643 #[test]
644 fn with_pool_fails_without_mutating_the_pool_when_exhausted() {
645 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 0)));
646 let result = KvCache::with_pool(2, 2, pool.clone(), 0);
647 assert!(result.is_err());
648 assert_eq!(pool.lock().unwrap().free_blocks(), 0);
649 }
650
651 #[test]
652 fn push_acquires_additional_blocks_as_the_cache_crosses_block_boundaries() {
653 let block_size = 2;
654 let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 10)));
655 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
656 assert_eq!(pool.lock().unwrap().free_blocks(), 9);
657
658 cache.push(&[1.0], &[1.0]).unwrap();
661 cache.push(&[2.0], &[2.0]).unwrap();
662 assert_eq!(
663 pool.lock().unwrap().free_blocks(),
664 9,
665 "filling exactly the first block must not acquire a second one"
666 );
667
668 cache.push(&[3.0], &[3.0]).unwrap();
670 assert_eq!(pool.lock().unwrap().free_blocks(), 8);
671 assert_eq!(cache.seq_len, 3);
672 assert_eq!(cache.k, vec![1.0, 2.0, 3.0]);
673 }
674
675 #[test]
676 fn push_returns_pool_exhausted_and_leaves_state_unchanged_when_no_blocks_remain() {
677 let block_size = 1;
678 let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 1)));
679 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
680 assert_eq!(pool.lock().unwrap().free_blocks(), 0);
681
682 cache.push(&[1.0], &[1.0]).unwrap(); let before_k = cache.k.clone();
685 let result = cache.push(&[2.0], &[2.0]);
686 assert_eq!(result, Err(KvPoolExhausted));
687 assert_eq!(cache.seq_len, 1, "a failed push must not change seq_len");
688 assert_eq!(cache.k, before_k, "a failed push must not append data");
689 }
690
691 #[test]
692 fn dropping_a_pooled_cache_returns_its_blocks_to_the_pool() {
693 let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 2)));
694 {
695 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
696 cache.push(&[1.0], &[1.0]).unwrap(); cache.push(&[2.0], &[2.0]).unwrap(); assert_eq!(pool.lock().unwrap().free_blocks(), 0);
699 }
700 assert_eq!(
701 pool.lock().unwrap().free_blocks(),
702 2,
703 "both blocks held by the dropped cache must return to the pool"
704 );
705 }
706
707 #[test]
708 fn release_to_pool_is_explicit_and_idempotent() {
709 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 5)));
710 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
711 assert_eq!(pool.lock().unwrap().free_blocks(), 4);
712
713 cache.release_to_pool();
714 assert_eq!(pool.lock().unwrap().free_blocks(), 5);
715
716 cache.release_to_pool(); assert_eq!(pool.lock().unwrap().free_blocks(), 5);
718
719 drop(cache); assert_eq!(pool.lock().unwrap().free_blocks(), 5);
721 }
722
723 #[test]
724 fn two_pooled_caches_share_one_bounded_budget() {
725 let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 1)));
726 let cache_a = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
727 let cache_b = KvCache::with_pool(1, 1, pool.clone(), 0);
728 assert!(
729 cache_b.is_err(),
730 "a second concurrent request must not be admitted when the shared budget is full"
731 );
732
733 drop(cache_a);
734 let cache_c = KvCache::with_pool(1, 1, pool, 0);
735 assert!(
736 cache_c.is_ok(),
737 "once the first request's cache is dropped, its budget must become available again"
738 );
739 }
740
741 #[test]
742 fn cloning_a_pooled_cache_detaches_the_clone_from_pool_accounting() {
743 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 3)));
744 let original = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
745 assert_eq!(pool.lock().unwrap().free_blocks(), 2);
746
747 let clone = original.clone();
748 assert_eq!(
749 pool.lock().unwrap().free_blocks(),
750 2,
751 "cloning must not acquire additional blocks"
752 );
753 assert_eq!(clone.k, original.k);
754
755 drop(clone);
756 assert_eq!(
757 pool.lock().unwrap().free_blocks(),
758 2,
759 "dropping a detached clone must not release the original's blocks"
760 );
761
762 drop(original);
763 assert_eq!(
764 pool.lock().unwrap().free_blocks(),
765 3,
766 "dropping the original must release its blocks exactly once"
767 );
768 }
769}