1use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
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 pub fn resize(&mut self, total_blocks: usize) -> Result<(), usize> {
95 let in_use = self.total_blocks - self.free_blocks;
96 if total_blocks < in_use {
97 return Err(in_use);
98 }
99 self.free_blocks = total_blocks - in_use;
100 self.total_blocks = total_blocks;
101 Ok(())
102 }
103
104 fn try_acquire(&mut self, n: usize) -> bool {
105 if n <= self.free_blocks {
106 self.free_blocks -= n;
107 true
108 } else {
109 false
110 }
111 }
112
113 fn release(&mut self, n: usize) {
114 self.free_blocks = (self.free_blocks + n).min(self.total_blocks);
115 }
116}
117
118struct PooledState {
119 pool: Arc<Mutex<KvBlockPool>>,
120 block_size: usize,
121 blocks_held: usize,
122}
123
124pub struct KvCache {
125 pub n_kv_heads: usize,
126 pub head_dim: usize,
127 pub k: Vec<f32>, pub v: Vec<f32>,
129 pub seq_len: usize,
130 planned_capacity: Option<usize>,
133 pool_state: Option<PooledState>,
137}
138
139impl Clone for KvCache {
146 fn clone(&self) -> Self {
147 KvCache {
148 n_kv_heads: self.n_kv_heads,
149 head_dim: self.head_dim,
150 k: self.k.clone(),
151 v: self.v.clone(),
152 seq_len: self.seq_len,
153 planned_capacity: self.planned_capacity,
154 pool_state: None,
155 }
156 }
157}
158
159impl Drop for KvCache {
160 fn drop(&mut self) {
161 if let Some(state) = &self.pool_state {
162 if let Ok(mut pool) = state.pool.lock() {
163 pool.release(state.blocks_held);
164 }
165 }
166 }
167}
168
169impl KvCache {
170 pub fn new(n_kv_heads: usize, head_dim: usize) -> Self {
171 KvCache {
172 n_kv_heads,
173 head_dim,
174 k: Vec::new(),
175 v: Vec::new(),
176 seq_len: 0,
177 planned_capacity: None,
178 pool_state: None,
179 }
180 }
181
182 pub fn with_capacity(n_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
186 let elems_per_position = n_kv_heads * head_dim;
187 KvCache {
188 n_kv_heads,
189 head_dim,
190 k: Vec::with_capacity(max_seq_len * elems_per_position),
191 v: Vec::with_capacity(max_seq_len * elems_per_position),
192 seq_len: 0,
193 planned_capacity: Some(max_seq_len),
194 pool_state: None,
195 }
196 }
197
198 pub fn with_pool(
217 n_kv_heads: usize,
218 head_dim: usize,
219 pool: Arc<Mutex<KvBlockPool>>,
220 max_seq_len: usize,
221 ) -> Result<Self, KvPoolExhausted> {
222 let block_size = pool.lock().unwrap().block_size();
223 let blocks_needed = max_seq_len.div_ceil(block_size).max(1);
224 if !pool.lock().unwrap().try_acquire(blocks_needed) {
225 return Err(KvPoolExhausted);
226 }
227 let elems_per_position = n_kv_heads * head_dim;
228 Ok(KvCache {
229 n_kv_heads,
230 head_dim,
231 k: Vec::with_capacity(blocks_needed * block_size * elems_per_position),
232 v: Vec::with_capacity(blocks_needed * block_size * elems_per_position),
233 seq_len: 0,
234 planned_capacity: None,
235 pool_state: Some(PooledState {
236 pool,
237 block_size,
238 blocks_held: blocks_needed,
239 }),
240 })
241 }
242
243 pub fn push(&mut self, k_step: &[f32], v_step: &[f32]) -> Result<(), KvPoolExhausted> {
250 assert_eq!(k_step.len(), self.n_kv_heads * self.head_dim);
251 assert_eq!(v_step.len(), self.n_kv_heads * self.head_dim);
252
253 let elems_per_position = self.n_kv_heads * self.head_dim;
254 if let Some(state) = &mut self.pool_state {
255 let capacity_positions = self.k.capacity() / elems_per_position;
256 if self.seq_len == capacity_positions {
257 if !state.pool.lock().unwrap().try_acquire(1) {
258 return Err(KvPoolExhausted);
259 }
260 state.blocks_held += 1;
261 self.k.reserve_exact(state.block_size * elems_per_position);
262 self.v.reserve_exact(state.block_size * elems_per_position);
263 }
264 }
265
266 self.k.extend_from_slice(k_step);
267 self.v.extend_from_slice(v_step);
268 self.seq_len += 1;
269 Ok(())
270 }
271
272 pub fn advance_len(&mut self, n: usize) -> Result<(), KvPoolExhausted> {
276 if n == 0 {
277 return Ok(());
278 }
279 let elems_per_position = self.n_kv_heads * self.head_dim;
280 let zeros = vec![0f32; elems_per_position];
281 for _ in 0..n {
282 self.push(&zeros, &zeros)?;
283 }
284 Ok(())
285 }
286
287 pub fn release_to_pool(&mut self) {
292 if let Some(state) = self.pool_state.take() {
293 if let Ok(mut pool) = state.pool.lock() {
294 pool.release(state.blocks_held);
295 }
296 }
297 }
298
299 pub fn clear(&mut self) {
300 self.k.clear();
301 self.v.clear();
302 self.seq_len = 0;
303 }
304
305 pub fn truncate(&mut self, new_seq_len: usize) {
312 assert!(
313 new_seq_len <= self.seq_len,
314 "truncate target {new_seq_len} must not exceed current seq_len {}",
315 self.seq_len
316 );
317 let elems_per_position = self.n_kv_heads * self.head_dim;
318 self.k.truncate(new_seq_len * elems_per_position);
319 self.v.truncate(new_seq_len * elems_per_position);
320 self.seq_len = new_seq_len;
321 }
322
323 pub fn allocated_bytes(&self) -> usize {
328 (self.k.capacity() + self.v.capacity()) * std::mem::size_of::<f32>()
329 }
330
331 pub fn is_within_planned_capacity(&self) -> bool {
336 match self.planned_capacity {
337 Some(cap) => {
338 self.seq_len <= cap
339 && self.k.capacity() >= self.seq_len * self.n_kv_heads * self.head_dim
340 }
341 None => false,
342 }
343 }
344}
345
346pub struct PagedKvStore {
360 block_size: usize,
361 n_kv_heads: usize,
362 head_dim: usize,
363 k: Vec<f32>, v: Vec<f32>,
365 free_block_ids: Vec<usize>,
366}
367
368impl PagedKvStore {
369 pub fn new(block_size: usize, total_blocks: usize, n_kv_heads: usize, head_dim: usize) -> Self {
370 assert!(block_size > 0, "block_size must be positive");
371 let elems_per_block = block_size * n_kv_heads * head_dim;
372 PagedKvStore {
373 block_size,
374 n_kv_heads,
375 head_dim,
376 k: vec![0.0; total_blocks * elems_per_block],
377 v: vec![0.0; total_blocks * elems_per_block],
378 free_block_ids: (0..total_blocks).rev().collect(),
382 }
383 }
384
385 pub fn block_size(&self) -> usize {
386 self.block_size
387 }
388
389 pub fn free_block_count(&self) -> usize {
390 self.free_block_ids.len()
391 }
392
393 pub fn n_kv_heads(&self) -> usize {
394 self.n_kv_heads
395 }
396
397 pub fn head_dim(&self) -> usize {
398 self.head_dim
399 }
400
401 fn acquire_block(&mut self) -> Option<usize> {
402 self.free_block_ids.pop()
403 }
404
405 fn release_block(&mut self, id: usize) {
406 self.free_block_ids.push(id);
407 }
408
409 fn elems_per_block(&self) -> usize {
410 self.block_size * self.n_kv_heads * self.head_dim
411 }
412
413 pub fn k_row(&self, id: usize, offset: usize) -> &[f32] {
419 let elems_per_position = self.n_kv_heads * self.head_dim;
420 let start = id * self.elems_per_block() + offset * elems_per_position;
421 &self.k[start..start + elems_per_position]
422 }
423
424 pub fn v_row(&self, id: usize, offset: usize) -> &[f32] {
425 let elems_per_position = self.n_kv_heads * self.head_dim;
426 let start = id * self.elems_per_block() + offset * elems_per_position;
427 &self.v[start..start + elems_per_position]
428 }
429
430 fn k_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
431 let elems_per_position = self.n_kv_heads * self.head_dim;
432 let start = id * self.elems_per_block() + offset * elems_per_position;
433 &mut self.k[start..start + elems_per_position]
434 }
435
436 fn v_row_mut(&mut self, id: usize, offset: usize) -> &mut [f32] {
437 let elems_per_position = self.n_kv_heads * self.head_dim;
438 let start = id * self.elems_per_block() + offset * elems_per_position;
439 &mut self.v[start..start + elems_per_position]
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub struct PagedStoreExhausted;
448
449impl std::fmt::Display for PagedStoreExhausted {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 write!(f, "paged KV store exhausted: no free blocks remain")
452 }
453}
454
455impl std::error::Error for PagedStoreExhausted {}
456
457#[derive(Debug, Clone, Default)]
463pub struct PagedKvCache {
464 block_table: Vec<usize>,
465 seq_len: usize,
466}
467
468impl PagedKvCache {
469 pub fn new() -> Self {
470 PagedKvCache {
471 block_table: Vec::new(),
472 seq_len: 0,
473 }
474 }
475
476 pub fn seq_len(&self) -> usize {
477 self.seq_len
478 }
479
480 pub fn block_table(&self) -> &[usize] {
481 &self.block_table
482 }
483
484 pub fn push(
489 &mut self,
490 store: &mut PagedKvStore,
491 k_step: &[f32],
492 v_step: &[f32],
493 ) -> Result<(), PagedStoreExhausted> {
494 let block_size = store.block_size();
495 let offset_in_block = self.seq_len % block_size;
496 let block_index = self.seq_len / block_size;
501 if block_index >= self.block_table.len() {
502 let id = store.acquire_block().ok_or(PagedStoreExhausted)?;
503 self.block_table.push(id);
504 }
505 let block_id = self.block_table[block_index];
506 store
507 .k_row_mut(block_id, offset_in_block)
508 .copy_from_slice(k_step);
509 store
510 .v_row_mut(block_id, offset_in_block)
511 .copy_from_slice(v_step);
512 self.seq_len += 1;
513 Ok(())
514 }
515
516 pub fn release(&mut self, store: &mut PagedKvStore) {
521 for id in self.block_table.drain(..) {
522 store.release_block(id);
523 }
524 self.seq_len = 0;
525 }
526
527 pub fn blocks_needed_for(&self, store: &PagedKvStore, n_new: usize) -> usize {
541 let held_capacity = self.block_table.len() * store.block_size();
542 let unused = held_capacity.saturating_sub(self.seq_len);
543 n_new.saturating_sub(unused).div_ceil(store.block_size())
544 }
545
546 pub fn reserve(
556 &mut self,
557 store: &mut PagedKvStore,
558 n_new: usize,
559 ) -> Result<(), PagedStoreExhausted> {
560 let need = self.blocks_needed_for(store, n_new);
561 if need > store.free_block_count() {
562 return Err(PagedStoreExhausted);
563 }
564 for _ in 0..need {
565 let id = store
566 .acquire_block()
567 .expect("checked against free_block_count immediately above");
568 self.block_table.push(id);
569 }
570 Ok(())
571 }
572
573 pub fn adopt_blocks(&mut self, block_table: Vec<usize>, seq_len: usize, block_size: usize) {
584 assert_eq!(
585 seq_len % block_size,
586 0,
587 "an adopted prefix must end on a block boundary, or the first \
588 append writes into a block another sequence is reading"
589 );
590 assert!(
591 seq_len / block_size <= block_table.len(),
592 "block table too short for the adopted length"
593 );
594 self.block_table = block_table;
595 self.seq_len = seq_len;
596 }
597
598 pub fn to_contiguous(&self, store: &PagedKvStore) -> KvCache {
615 let elems_per_position = store.n_kv_heads * store.head_dim;
616 let mut cache = KvCache::with_capacity(store.n_kv_heads, store.head_dim, self.seq_len);
617 cache.k.reserve_exact(self.seq_len * elems_per_position);
618 cache.v.reserve_exact(self.seq_len * elems_per_position);
619 for pos in 0..self.seq_len {
620 let block_id = self.block_table[pos / store.block_size];
621 let offset = pos % store.block_size;
622 cache.k.extend_from_slice(store.k_row(block_id, offset));
623 cache.v.extend_from_slice(store.v_row(block_id, offset));
624 }
625 cache.seq_len = self.seq_len;
626 cache
627 }
628
629 pub fn append_contiguous(
639 &mut self,
640 store: &mut PagedKvStore,
641 k: &[f32],
642 v: &[f32],
643 count: usize,
644 ) -> Result<(), PagedStoreExhausted> {
645 let elems_per_position = store.n_kv_heads * store.head_dim;
646 assert_eq!(k.len(), count * elems_per_position, "k row count");
647 assert_eq!(v.len(), count * elems_per_position, "v row count");
648 if self.blocks_needed_for(store, count) > store.free_block_count() {
649 return Err(PagedStoreExhausted);
650 }
651 for i in 0..count {
652 let lo = i * elems_per_position;
653 let hi = lo + elems_per_position;
654 self.push(store, &k[lo..hi], &v[lo..hi])
655 .expect("blocks reserved above, so no push here can exhaust the store");
656 }
657 Ok(())
658 }
659}
660
661pub struct SharedPagedKv {
703 layers: Vec<RwLock<PagedKvStore>>,
704 groups: Mutex<GroupTable>,
709}
710
711impl SharedPagedKv {
712 pub fn new(
714 n_layers: usize,
715 block_size: usize,
716 blocks_per_layer: usize,
717 n_kv_heads: usize,
718 head_dim: usize,
719 ) -> Self {
720 SharedPagedKv {
721 layers: (0..n_layers)
722 .map(|_| {
723 RwLock::new(PagedKvStore::new(
724 block_size,
725 blocks_per_layer,
726 n_kv_heads,
727 head_dim,
728 ))
729 })
730 .collect(),
731 groups: Mutex::new(GroupTable::default()),
732 }
733 }
734
735 pub fn from_stores(stores: Vec<PagedKvStore>) -> Self {
738 SharedPagedKv {
739 layers: stores.into_iter().map(RwLock::new).collect(),
740 groups: Mutex::new(GroupTable::default()),
741 }
742 }
743
744 pub fn layer_count(&self) -> usize {
745 self.layers.len()
746 }
747
748 pub fn read(&self, layer: usize) -> RwLockReadGuard<'_, PagedKvStore> {
750 self.layers[layer]
751 .read()
752 .unwrap_or_else(|poisoned| poisoned.into_inner())
753 }
754
755 pub fn write(&self, layer: usize) -> RwLockWriteGuard<'_, PagedKvStore> {
758 self.layers[layer]
759 .write()
760 .unwrap_or_else(|poisoned| poisoned.into_inner())
761 }
762
763 pub fn write_all(&self) -> Vec<RwLockWriteGuard<'_, PagedKvStore>> {
774 self.layers
775 .iter()
776 .map(|l| l.write().unwrap_or_else(|poisoned| poisoned.into_inner()))
777 .collect()
778 }
779
780 pub fn free_blocks(&self, layer: usize) -> usize {
784 self.read(layer).free_block_count()
785 }
786
787 pub fn acquire_group(&self) -> Option<PageGroup> {
793 let mut guards = self.write_all();
794 if guards.iter().any(|s| s.free_block_count() == 0) {
795 return None;
796 }
797 let blocks: Vec<usize> = guards
798 .iter_mut()
799 .map(|s| {
800 s.acquire_block()
801 .expect("checked every layer under these same guards")
802 })
803 .collect();
804 let mut groups = self
805 .groups
806 .lock()
807 .unwrap_or_else(|poisoned| poisoned.into_inner());
808 Some(PageGroup(groups.insert(blocks)))
809 }
810
811 pub fn retain_group(&self, group: PageGroup) {
818 let mut groups = self
819 .groups
820 .lock()
821 .unwrap_or_else(|poisoned| poisoned.into_inner());
822 groups.retain(group.0);
823 }
824
825 pub fn release_group(&self, group: PageGroup) -> bool {
830 let blocks = {
831 let mut groups = self
832 .groups
833 .lock()
834 .unwrap_or_else(|poisoned| poisoned.into_inner());
835 match groups.release(group.0) {
836 Some(blocks) => blocks,
837 None => return false,
838 }
839 };
840 let mut guards = self.write_all();
844 for (store, block) in guards.iter_mut().zip(blocks) {
845 store.release_block(block);
846 }
847 true
848 }
849
850 pub fn group_blocks(&self, group: PageGroup) -> Vec<usize> {
852 let groups = self
853 .groups
854 .lock()
855 .unwrap_or_else(|poisoned| poisoned.into_inner());
856 groups.blocks(group.0).to_vec()
857 }
858
859 pub fn group_refs(&self, group: PageGroup) -> u32 {
861 let groups = self
862 .groups
863 .lock()
864 .unwrap_or_else(|poisoned| poisoned.into_inner());
865 groups.refs(group.0)
866 }
867
868 pub fn free_groups(&self) -> usize {
871 (0..self.layers.len())
872 .map(|l| self.free_blocks(l))
873 .min()
874 .unwrap_or(0)
875 }
876}
877
878#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
885pub struct PageGroup(pub u32);
886
887#[derive(Debug, Default)]
889struct GroupTable {
890 blocks: Vec<Option<Vec<usize>>>,
892 refs: Vec<u32>,
893 free_ids: Vec<u32>,
894}
895
896impl GroupTable {
897 fn insert(&mut self, blocks: Vec<usize>) -> u32 {
898 if let Some(id) = self.free_ids.pop() {
899 self.blocks[id as usize] = Some(blocks);
900 self.refs[id as usize] = 1;
901 return id;
902 }
903 self.blocks.push(Some(blocks));
904 self.refs.push(1);
905 (self.blocks.len() - 1) as u32
906 }
907
908 fn retain(&mut self, id: u32) {
909 let refs = &mut self.refs[id as usize];
910 assert!(*refs > 0, "cannot retain group {id}, which has no holders");
911 *refs += 1;
912 }
913
914 fn release(&mut self, id: u32) -> Option<Vec<usize>> {
917 let refs = &mut self.refs[id as usize];
918 assert!(*refs > 0, "double free of group {id}");
919 *refs -= 1;
920 if *refs > 0 {
921 return None;
922 }
923 let blocks = self.blocks[id as usize]
927 .take()
928 .expect("a group with holders always has blocks");
929 self.free_ids.push(id);
930 Some(blocks)
931 }
932
933 fn blocks(&self, id: u32) -> &[usize] {
934 self.blocks[id as usize]
935 .as_deref()
936 .expect("group has no blocks; it was already released")
937 }
938
939 fn refs(&self, id: u32) -> u32 {
940 self.refs.get(id as usize).copied().unwrap_or(0)
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
965 fn blocks_needed_for_accounts_for_the_part_full_tail_block() {
966 let mut store = PagedKvStore::new(4, 64, 1, 1);
967 let mut cache = PagedKvCache::new();
968 let row = [1.0f32];
969 let advance = |cache: &mut PagedKvCache, store: &mut PagedKvStore, n: usize| {
974 for _ in 0..n {
975 cache.push(store, &row, &row).unwrap();
976 }
977 };
978
979 assert_eq!(cache.blocks_needed_for(&store, 0), 0);
982 assert_eq!(cache.blocks_needed_for(&store, 1), 1);
983 assert_eq!(cache.blocks_needed_for(&store, 4), 1);
984 assert_eq!(cache.blocks_needed_for(&store, 5), 2, "5 into 4s needs 2");
985
986 advance(&mut cache, &mut store, 1);
989 assert_eq!(cache.blocks_needed_for(&store, 3), 0, "fits in the tail");
990 assert_eq!(cache.blocks_needed_for(&store, 4), 1);
991 assert_eq!(cache.blocks_needed_for(&store, 8), 2);
992
993 advance(&mut cache, &mut store, 2); assert_eq!(cache.blocks_needed_for(&store, 6), 2);
998 assert_eq!(cache.blocks_needed_for(&store, 5), 1);
999
1000 advance(&mut cache, &mut store, 1); assert_eq!(cache.blocks_needed_for(&store, 1), 1);
1003 assert_eq!(cache.blocks_needed_for(&store, 4), 1);
1004
1005 cache.reserve(&mut store, 4).unwrap();
1010 assert_eq!(
1011 cache.blocks_needed_for(&store, 4),
1012 0,
1013 "a reserved block is already held"
1014 );
1015 assert_eq!(cache.blocks_needed_for(&store, 5), 1);
1016 }
1017
1018 #[test]
1025 fn a_group_takes_one_block_from_every_layer_and_returns_them_together() {
1026 let kv = SharedPagedKv::new(3, 2, 4, 1, 1);
1027 assert_eq!(kv.free_groups(), 4);
1028
1029 let g = kv.acquire_group().expect("4 groups available");
1030 let blocks = kv.group_blocks(g);
1031 assert_eq!(blocks.len(), 3, "one block per layer");
1032 for l in 0..3 {
1033 assert_eq!(kv.free_blocks(l), 3, "layer {l} gave up exactly one");
1034 }
1035 assert_eq!(kv.free_groups(), 3);
1036
1037 assert!(kv.release_group(g), "sole holder, so this frees it");
1038 for l in 0..3 {
1039 assert_eq!(kv.free_blocks(l), 4, "layer {l} got its block back");
1040 }
1041 assert_eq!(kv.free_groups(), 4);
1042 }
1043
1044 #[test]
1052 fn a_group_shared_by_two_holders_survives_the_first_release() {
1053 let kv = SharedPagedKv::new(2, 2, 2, 1, 1);
1054 let g = kv.acquire_group().unwrap();
1055 let blocks = kv.group_blocks(g);
1056 kv.retain_group(g);
1057 assert_eq!(kv.group_refs(g), 2);
1058
1059 assert!(
1060 !kv.release_group(g),
1061 "one holder remains, so nothing is freed"
1062 );
1063 assert_eq!(kv.group_refs(g), 1);
1064 assert_eq!(kv.free_blocks(0), 1, "the blocks are still held");
1065 assert_eq!(kv.group_blocks(g), blocks, "and still name the same blocks");
1066
1067 assert!(kv.release_group(g), "last holder frees it");
1068 assert_eq!(kv.group_refs(g), 0);
1069 assert_eq!(kv.free_blocks(0), 2);
1070 }
1071
1072 #[test]
1078 fn group_capacity_is_bounded_by_the_layer_with_the_fewest_blocks() {
1079 let kv = SharedPagedKv::from_stores(vec![
1080 PagedKvStore::new(2, 5, 1, 1),
1081 PagedKvStore::new(2, 1, 1, 1),
1082 ]);
1083 assert_eq!(kv.free_groups(), 1, "layer 1 has only one block");
1084
1085 let g = kv.acquire_group().expect("one group fits");
1086 assert_eq!(kv.free_groups(), 0);
1087 assert!(
1088 kv.acquire_group().is_none(),
1089 "layer 1 is empty, so no group can be formed"
1090 );
1091 assert_eq!(kv.free_blocks(0), 4, "a refused group leaks nothing");
1093 kv.release_group(g);
1094 assert_eq!(kv.free_blocks(0), 5);
1095 }
1096
1097 #[test]
1099 fn a_released_group_id_is_reused_with_a_fresh_refcount() {
1100 let kv = SharedPagedKv::new(1, 2, 2, 1, 1);
1101 let first = kv.acquire_group().unwrap();
1102 kv.retain_group(first);
1103 assert_eq!(kv.group_refs(first), 2);
1104 kv.release_group(first);
1105 kv.release_group(first);
1106 assert_eq!(kv.group_refs(first), 0, "gone, not merely decremented");
1107
1108 let second = kv.acquire_group().unwrap();
1109 assert_eq!(second, first, "the id is reused");
1110 assert_eq!(
1111 kv.group_refs(second),
1112 1,
1113 "a reused id must not inherit the old count"
1114 );
1115 assert_eq!(kv.group_blocks(second).len(), 1);
1116 assert_eq!(kv.free_blocks(0), 1);
1117 }
1118
1119 #[test]
1134 #[should_panic(expected = "already released")]
1135 fn reading_a_released_group_panics_rather_than_returning_stale_blocks() {
1136 let kv = SharedPagedKv::new(2, 2, 2, 1, 1);
1137 let g = kv.acquire_group().unwrap();
1138 assert!(kv.release_group(g));
1139 let _ = kv.group_blocks(g);
1140 }
1141
1142 #[test]
1148 #[should_panic(expected = "double free of group")]
1149 fn releasing_a_group_twice_panics_rather_than_freeing_it_twice() {
1150 let kv = SharedPagedKv::new(1, 2, 2, 1, 1);
1151 let g = kv.acquire_group().unwrap();
1152 assert!(kv.release_group(g));
1153 kv.release_group(g);
1154 }
1155
1156 #[test]
1157 fn a_gathered_sequence_round_trips_through_the_store() {
1158 let mut store = PagedKvStore::new(2, 8, 2, 2);
1159 let mut cache = PagedKvCache::new();
1160 let rows: Vec<[f32; 4]> = (0..5)
1163 .map(|i| {
1164 let b = i as f32 * 10.0;
1165 [b + 1.0, b + 2.0, b + 3.0, b + 4.0]
1166 })
1167 .collect();
1168 for r in &rows {
1169 cache.push(&mut store, r, r).unwrap();
1170 }
1171
1172 let flat = cache.to_contiguous(&store);
1173 assert_eq!(flat.seq_len, 5);
1174 assert_eq!(flat.k.len(), 5 * 4);
1175 for (i, r) in rows.iter().enumerate() {
1176 assert_eq!(&flat.k[i * 4..(i + 1) * 4], r, "position {i} k");
1177 assert_eq!(&flat.v[i * 4..(i + 1) * 4], r, "position {i} v");
1178 }
1179
1180 let mut rebuilt = PagedKvCache::new();
1183 let mut store2 = PagedKvStore::new(2, 8, 2, 2);
1184 rebuilt
1185 .append_contiguous(&mut store2, &flat.k, &flat.v, 5)
1186 .unwrap();
1187 let again = rebuilt.to_contiguous(&store2);
1188 assert_eq!(again.k, flat.k);
1189 assert_eq!(again.v, flat.v);
1190 assert_eq!(again.seq_len, flat.seq_len);
1191 }
1192
1193 #[test]
1194 fn push_grows_seq_len_and_stores_values() {
1195 let mut cache = KvCache::new(2, 2);
1196 cache
1197 .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
1198 .unwrap();
1199 assert_eq!(cache.seq_len, 1);
1200 cache
1201 .push(&[9.0, 10.0, 11.0, 12.0], &[13.0, 14.0, 15.0, 16.0])
1202 .unwrap();
1203 assert_eq!(cache.seq_len, 2);
1204 assert_eq!(cache.k.len(), 2 * 2 * 2);
1205 assert_eq!(cache.k[4], 9.0);
1206 }
1207
1208 #[test]
1209 #[should_panic]
1210 fn push_wrong_size_panics() {
1211 let mut cache = KvCache::new(2, 2);
1212 let _ = cache.push(&[1.0, 2.0], &[1.0, 2.0]); }
1214
1215 #[test]
1216 fn clear_resets_state() {
1217 let mut cache = KvCache::new(1, 1);
1218 cache.push(&[1.0], &[2.0]).unwrap();
1219 cache.clear();
1220 assert_eq!(cache.seq_len, 0);
1221 assert!(cache.k.is_empty());
1222 }
1223
1224 #[test]
1225 fn truncate_rolls_back_to_exact_length_preserving_earlier_data() {
1226 let mut cache = KvCache::new(2, 2);
1227 cache
1228 .push(&[1.0, 2.0, 3.0, 4.0], &[10.0, 20.0, 30.0, 40.0])
1229 .unwrap();
1230 cache
1231 .push(&[5.0, 6.0, 7.0, 8.0], &[50.0, 60.0, 70.0, 80.0])
1232 .unwrap();
1233 cache
1234 .push(&[9.0, 9.0, 9.0, 9.0], &[90.0, 90.0, 90.0, 90.0])
1235 .unwrap();
1236 assert_eq!(cache.seq_len, 3);
1237
1238 cache.truncate(1);
1239 assert_eq!(cache.seq_len, 1);
1240 assert_eq!(cache.k, vec![1.0, 2.0, 3.0, 4.0]);
1241 assert_eq!(cache.v, vec![10.0, 20.0, 30.0, 40.0]);
1242 }
1243
1244 #[test]
1245 fn truncate_to_current_length_is_a_no_op() {
1246 let mut cache = KvCache::new(1, 2);
1247 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1248 cache.truncate(1);
1249 assert_eq!(cache.seq_len, 1);
1250 assert_eq!(cache.k, vec![1.0, 2.0]);
1251 }
1252
1253 #[test]
1254 fn truncate_to_zero_empties_the_cache() {
1255 let mut cache = KvCache::new(1, 2);
1256 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1257 cache.truncate(0);
1258 assert_eq!(cache.seq_len, 0);
1259 assert!(cache.k.is_empty());
1260 assert!(cache.v.is_empty());
1261 }
1262
1263 #[test]
1264 #[should_panic]
1265 fn truncate_beyond_current_length_panics() {
1266 let mut cache = KvCache::new(1, 2);
1267 cache.push(&[1.0, 2.0], &[3.0, 4.0]).unwrap();
1268 cache.truncate(5);
1269 }
1270
1271 #[test]
1272 fn push_after_truncate_continues_correctly() {
1273 let mut cache = KvCache::new(1, 1);
1274 cache.push(&[1.0], &[10.0]).unwrap();
1275 cache.push(&[2.0], &[20.0]).unwrap();
1276 cache.push(&[3.0], &[30.0]).unwrap(); cache.truncate(2);
1278 cache.push(&[99.0], &[990.0]).unwrap(); assert_eq!(cache.seq_len, 3);
1280 assert_eq!(cache.k, vec![1.0, 2.0, 99.0]);
1281 assert_eq!(cache.v, vec![10.0, 20.0, 990.0]);
1282 }
1283
1284 #[test]
1285 fn with_capacity_preallocates_and_never_reallocates_within_plan() {
1286 let n_kv_heads = 4;
1287 let head_dim = 8;
1288 let max_seq_len = 16;
1289 let mut cache = KvCache::with_capacity(n_kv_heads, head_dim, max_seq_len);
1290
1291 let expected_elems = max_seq_len * n_kv_heads * head_dim;
1292 assert!(cache.k.capacity() >= expected_elems);
1293 assert!(cache.v.capacity() >= expected_elems);
1294
1295 let step = vec![0.5f32; n_kv_heads * head_dim];
1296 let k_ptr_before = cache.k.as_ptr();
1297 for _ in 0..max_seq_len {
1298 cache.push(&step, &step).unwrap();
1299 }
1300 let k_ptr_after = cache.k.as_ptr();
1301 assert_eq!(
1302 k_ptr_before, k_ptr_after,
1303 "pushing exactly up to the planned capacity must not reallocate"
1304 );
1305 assert!(cache.is_within_planned_capacity());
1306 }
1307
1308 #[test]
1309 fn allocated_bytes_reflects_preallocated_capacity_not_just_used_length() {
1310 let cache = KvCache::with_capacity(4, 8, 100);
1311 let expected_min = 100 * 4 * 8 * 2 * 4;
1313 assert!(
1314 cache.allocated_bytes() >= expected_min,
1315 "allocated_bytes={} expected_min={expected_min}",
1316 cache.allocated_bytes()
1317 );
1318 assert_eq!(cache.seq_len, 0);
1320 }
1321
1322 #[test]
1323 fn grow_as_you_go_cache_reports_not_within_planned_capacity() {
1324 let mut cache = KvCache::new(2, 2);
1325 cache
1326 .push(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0])
1327 .unwrap();
1328 assert!(
1329 !cache.is_within_planned_capacity(),
1330 "a cache built with `new` has no plan to be within"
1331 );
1332 }
1333
1334 #[test]
1335 fn with_pool_acquires_one_block_and_reports_it_in_free_blocks() {
1336 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 10)));
1337 let cache = KvCache::with_pool(2, 2, pool.clone(), 0).unwrap();
1338 assert_eq!(pool.lock().unwrap().free_blocks(), 9);
1339 assert_eq!(cache.seq_len, 0);
1340 }
1341
1342 #[test]
1343 fn with_pool_fails_without_mutating_the_pool_when_exhausted() {
1344 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 0)));
1345 let result = KvCache::with_pool(2, 2, pool.clone(), 0);
1346 assert!(result.is_err());
1347 assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1348 }
1349
1350 #[test]
1351 fn push_acquires_additional_blocks_as_the_cache_crosses_block_boundaries() {
1352 let block_size = 2;
1353 let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 10)));
1354 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1355 assert_eq!(pool.lock().unwrap().free_blocks(), 9);
1356
1357 cache.push(&[1.0], &[1.0]).unwrap();
1360 cache.push(&[2.0], &[2.0]).unwrap();
1361 assert_eq!(
1362 pool.lock().unwrap().free_blocks(),
1363 9,
1364 "filling exactly the first block must not acquire a second one"
1365 );
1366
1367 cache.push(&[3.0], &[3.0]).unwrap();
1369 assert_eq!(pool.lock().unwrap().free_blocks(), 8);
1370 assert_eq!(cache.seq_len, 3);
1371 assert_eq!(cache.k, vec![1.0, 2.0, 3.0]);
1372 }
1373
1374 #[test]
1375 fn push_returns_pool_exhausted_and_leaves_state_unchanged_when_no_blocks_remain() {
1376 let block_size = 1;
1377 let pool = Arc::new(Mutex::new(KvBlockPool::new(block_size, 1)));
1378 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1379 assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1380
1381 cache.push(&[1.0], &[1.0]).unwrap(); let before_k = cache.k.clone();
1384 let result = cache.push(&[2.0], &[2.0]);
1385 assert_eq!(result, Err(KvPoolExhausted));
1386 assert_eq!(cache.seq_len, 1, "a failed push must not change seq_len");
1387 assert_eq!(cache.k, before_k, "a failed push must not append data");
1388 }
1389
1390 #[test]
1391 fn dropping_a_pooled_cache_returns_its_blocks_to_the_pool() {
1392 let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 2)));
1393 {
1394 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1395 cache.push(&[1.0], &[1.0]).unwrap(); cache.push(&[2.0], &[2.0]).unwrap(); assert_eq!(pool.lock().unwrap().free_blocks(), 0);
1398 }
1399 assert_eq!(
1400 pool.lock().unwrap().free_blocks(),
1401 2,
1402 "both blocks held by the dropped cache must return to the pool"
1403 );
1404 }
1405
1406 #[test]
1407 fn release_to_pool_is_explicit_and_idempotent() {
1408 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 5)));
1409 let mut cache = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1410 assert_eq!(pool.lock().unwrap().free_blocks(), 4);
1411
1412 cache.release_to_pool();
1413 assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1414
1415 cache.release_to_pool(); assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1417
1418 drop(cache); assert_eq!(pool.lock().unwrap().free_blocks(), 5);
1420 }
1421
1422 #[test]
1423 fn two_pooled_caches_share_one_bounded_budget() {
1424 let pool = Arc::new(Mutex::new(KvBlockPool::new(1, 1)));
1425 let cache_a = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1426 let cache_b = KvCache::with_pool(1, 1, pool.clone(), 0);
1427 assert!(
1428 cache_b.is_err(),
1429 "a second concurrent request must not be admitted when the shared budget is full"
1430 );
1431
1432 drop(cache_a);
1433 let cache_c = KvCache::with_pool(1, 1, pool, 0);
1434 assert!(
1435 cache_c.is_ok(),
1436 "once the first request's cache is dropped, its budget must become available again"
1437 );
1438 }
1439
1440 #[test]
1441 fn cloning_a_pooled_cache_detaches_the_clone_from_pool_accounting() {
1442 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 3)));
1443 let original = KvCache::with_pool(1, 1, pool.clone(), 0).unwrap();
1444 assert_eq!(pool.lock().unwrap().free_blocks(), 2);
1445
1446 let clone = original.clone();
1447 assert_eq!(
1448 pool.lock().unwrap().free_blocks(),
1449 2,
1450 "cloning must not acquire additional blocks"
1451 );
1452 assert_eq!(clone.k, original.k);
1453
1454 drop(clone);
1455 assert_eq!(
1456 pool.lock().unwrap().free_blocks(),
1457 2,
1458 "dropping a detached clone must not release the original's blocks"
1459 );
1460
1461 drop(original);
1462 assert_eq!(
1463 pool.lock().unwrap().free_blocks(),
1464 3,
1465 "dropping the original must release its blocks exactly once"
1466 );
1467 }
1468
1469 #[test]
1478 fn a_pool_refuses_to_shrink_below_what_is_already_held() {
1479 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 10)));
1480 let held = KvCache::with_pool(2, 4, Arc::clone(&pool), 24).expect("blocks");
1481 let in_use = {
1482 let p = pool.lock().unwrap();
1483 p.total_blocks() - p.free_blocks()
1484 };
1485 assert!(in_use > 0, "the fixture must actually hold blocks");
1486
1487 let mut p = pool.lock().unwrap();
1488 assert_eq!(p.resize(in_use - 1), Err(in_use));
1489 assert_eq!(p.total_blocks(), 10, "a refused resize changes nothing");
1490 assert_eq!(p.free_blocks(), 10 - in_use);
1491
1492 assert_eq!(p.resize(in_use), Ok(()));
1494 assert_eq!(p.free_blocks(), 0);
1495 drop(p);
1496 drop(held);
1497 }
1498
1499 #[test]
1502 fn growing_a_pool_adds_to_what_is_free_and_not_to_what_is_held() {
1503 let pool = Arc::new(Mutex::new(KvBlockPool::new(4, 8)));
1504 let held = KvCache::with_pool(2, 4, Arc::clone(&pool), 16).expect("blocks");
1505 let mut p = pool.lock().unwrap();
1506 let in_use = p.total_blocks() - p.free_blocks();
1507
1508 assert_eq!(p.resize(32), Ok(()));
1509 assert_eq!(p.total_blocks(), 32);
1510 assert_eq!(p.free_blocks(), 32 - in_use);
1511 drop(p);
1512 drop(held);
1513 }
1514}