1use std::sync::{Arc, RwLock};
52
53pub const DEFAULT_MAX_TOKENS_PER_SEQ: usize = 8192;
54pub const DEFAULT_NUM_HEADS: usize = 4;
55pub const DEFAULT_HIDDEN_DIM_PER_HEAD: usize = 64;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct KVCacheConfig {
59 pub max_tokens_per_seq: usize,
60 pub num_heads: usize,
61 pub hidden_dim_per_head: usize,
62}
63
64impl KVCacheConfig {
65 pub const fn token_width(&self) -> usize {
67 self.num_heads * self.hidden_dim_per_head
68 }
69}
70
71impl Default for KVCacheConfig {
72 fn default() -> Self {
73 Self {
74 max_tokens_per_seq: DEFAULT_MAX_TOKENS_PER_SEQ,
75 num_heads: DEFAULT_NUM_HEADS,
76 hidden_dim_per_head: DEFAULT_HIDDEN_DIM_PER_HEAD,
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq)]
85pub struct KVCacheEntry {
86 pub keys: Vec<f32>,
87 pub values: Vec<f32>,
88}
89
90#[derive(Debug, PartialEq)]
92pub struct KVSpan<'a> {
93 pub keys: &'a [f32],
94 pub values: &'a [f32],
95}
96
97struct KvCacheState {
98 keys: Vec<f32>,
101 values: Vec<f32>,
102 current_len: usize,
104}
105
106#[derive(Clone)]
109pub struct LayerKvCache {
110 pub config: KVCacheConfig,
111 state: Arc<RwLock<KvCacheState>>,
112}
113
114impl std::fmt::Debug for LayerKvCache {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("LayerKvCache")
119 .field("config", &self.config)
120 .field("current_len", &self.len())
121 .finish()
122 }
123}
124
125impl Default for LayerKvCache {
126 fn default() -> Self {
127 Self::new(KVCacheConfig::default())
128 }
129}
130
131impl LayerKvCache {
132 pub fn new(config: KVCacheConfig) -> Self {
136 Self {
137 config,
138 state: Arc::new(RwLock::new(KvCacheState {
139 keys: Vec::new(),
140 values: Vec::new(),
141 current_len: 0,
142 })),
143 }
144 }
145
146 pub fn initialize(&self, seq_len: usize) -> Result<(), String> {
154 if seq_len == 0 {
155 return Err("sequence length must be greater than zero".to_string());
156 }
157
158 let bounded_len = seq_len.min(self.config.max_tokens_per_seq);
159 let width = self.config.token_width();
160 let mut state = self
161 .state
162 .write()
163 .map_err(|_| "kv cache lock poisoned".to_string())?;
164
165 if state.keys.is_empty() {
166 let capacity = self.config.max_tokens_per_seq * width;
167 state.keys = vec![0.0; capacity];
168 state.values = vec![0.0; capacity];
169 }
170
171 state.current_len = state.current_len.max(bounded_len);
172 Ok(())
173 }
174
175 pub fn write_kv(&self, token_idx: usize, keys: &[f32], values: &[f32]) -> Result<(), String> {
178 if keys.len() != values.len() {
179 return Err("keys/values length mismatch".to_string());
180 }
181 let width = self.config.token_width();
182 if keys.len() != width {
183 return Err(format!(
184 "expected {width} elements per token (num_heads * hidden_dim_per_head), got {}",
185 keys.len()
186 ));
187 }
188 if token_idx >= self.config.max_tokens_per_seq {
189 return Err(format!(
190 "token index {token_idx} out of bounds (max {})",
191 self.config.max_tokens_per_seq
192 ));
193 }
194
195 let start = token_idx * width;
198 let end = start + width;
199
200 let mut state = self
201 .state
202 .write()
203 .map_err(|_| "kv cache lock poisoned".to_string())?;
204 if state.keys.is_empty() {
205 return Err("cache not initialized — call initialize() first".to_string());
206 }
207
208 state.keys[start..end].copy_from_slice(keys);
209 state.values[start..end].copy_from_slice(values);
210 state.current_len = state.current_len.max(token_idx + 1);
211 Ok(())
212 }
213
214 pub fn write_kv_batch(&self, entries: &[(usize, &[f32], &[f32])]) -> Result<(), String> {
220 if entries.is_empty() {
221 return Ok(());
222 }
223
224 let width = self.config.token_width();
225 let mut max_token_idx = 0;
226
227 for &(token_idx, keys, values) in entries {
230 if keys.len() != values.len() {
231 return Err("keys/values length mismatch".to_string());
232 }
233 if keys.len() != width {
234 return Err(format!("expected {width} elements, got {}", keys.len()));
235 }
236 if token_idx >= self.config.max_tokens_per_seq {
237 return Err(format!("token index {token_idx} out of bounds"));
238 }
239 if token_idx > max_token_idx {
240 max_token_idx = token_idx;
241 }
242 }
243
244 let mut state = self
246 .state
247 .write()
248 .map_err(|_| "kv cache lock poisoned".to_string())?;
249 if state.keys.is_empty() {
250 return Err("cache not initialized".to_string());
251 }
252
253 for &(token_idx, keys, values) in entries {
255 let start = token_idx * width;
256 let end = start + width;
257 state.keys[start..end].copy_from_slice(keys);
258 state.values[start..end].copy_from_slice(values);
259 }
260
261 state.current_len = state.current_len.max(max_token_idx + 1);
262 Ok(())
263 }
264
265 pub fn with_read_kv<F, R>(&self, token_idx: usize, f: F) -> Result<R, String>
268 where
269 F: FnOnce(&[f32], &[f32]) -> R,
270 {
271 let width = self.config.token_width();
272 let state = self
273 .state
274 .read()
275 .map_err(|_| "kv cache lock poisoned".to_string())?;
276
277 if token_idx >= state.current_len {
278 return Err(format!(
279 "token index {token_idx} out of bounds (current length {})",
280 state.current_len
281 ));
282 }
283
284 let start = token_idx * width;
285 let end = start + width;
286 Ok(f(&state.keys[start..end], &state.values[start..end]))
287 }
288
289 #[doc(hidden)]
296 pub fn read_kv(&self, token_idx: usize) -> Result<KVCacheEntry, String> {
297 self.with_read_kv(token_idx, |keys, values| KVCacheEntry {
298 keys: keys.to_vec(),
299 values: values.to_vec(),
300 })
301 }
302
303 pub fn with_read_range<F, R>(
307 &self,
308 start_token: usize,
309 end_token: usize,
310 f: F,
311 ) -> Result<R, String>
312 where
313 F: FnOnce(KVSpan<'_>) -> R,
314 {
315 if start_token > end_token {
316 return Err("start_token must be <= end_token".to_string());
317 }
318 let width = self.config.token_width();
319 let state = self
320 .state
321 .read()
322 .map_err(|_| "kv cache lock poisoned".to_string())?;
323
324 if end_token > state.current_len {
325 return Err(format!(
326 "end_token {end_token} exceeds current length {}",
327 state.current_len
328 ));
329 }
330
331 let start = start_token * width;
332 let end = end_token * width;
333 let span = KVSpan {
334 keys: &state.keys[start..end],
335 values: &state.values[start..end],
336 };
337 Ok(f(span))
338 }
339
340 #[doc(hidden)]
351 pub fn read_range(
352 &self,
353 start_token: usize,
354 end_token: usize,
355 ) -> Result<(Vec<f32>, Vec<f32>), String> {
356 self.with_read_range(start_token, end_token, |span| {
357 (span.keys.to_vec(), span.values.to_vec())
358 })
359 }
360
361 pub fn read_range_into(
365 &self,
366 start_token: usize,
367 end_token: usize,
368 keys_out: &mut [f32],
369 values_out: &mut [f32],
370 ) -> Result<(), String> {
371 if start_token > end_token {
372 return Err("start_token must be <= end_token".to_string());
373 }
374 let num_tokens = end_token - start_token;
375 let width = self.config.token_width();
376 let required_len = num_tokens * width;
377
378 if keys_out.len() < required_len {
379 return Err(format!(
380 "keys_out buffer too short: expected at least {required_len} elements, got {}",
381 keys_out.len()
382 ));
383 }
384 if values_out.len() < required_len {
385 return Err(format!(
386 "values_out buffer too short: expected at least {required_len} elements, got {}",
387 values_out.len()
388 ));
389 }
390
391 self.with_read_range(start_token, end_token, |span| {
392 keys_out[..required_len].copy_from_slice(span.keys);
393 values_out[..required_len].copy_from_slice(span.values);
394 })
395 }
396
397 pub fn len(&self) -> usize {
398 self.state.read().map(|s| s.current_len).unwrap_or(0)
399 }
400
401 pub fn is_empty(&self) -> bool {
402 self.len() == 0
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use proptest::prelude::*;
410
411 fn small_config() -> KVCacheConfig {
412 KVCacheConfig {
414 max_tokens_per_seq: 16,
415 num_heads: 2,
416 hidden_dim_per_head: 4,
417 }
418 }
419
420 #[test]
421 fn initialize_rejects_zero_len() {
422 let cache = LayerKvCache::default();
423 assert!(cache.initialize(0).is_err());
424 }
425
426 #[test]
427 fn initialize_and_round_trip() {
428 let cache = LayerKvCache::new(small_config());
429 cache.initialize(8).expect("cache should initialize");
430 assert_eq!(cache.len(), 8);
431
432 let width = cache.config.token_width();
433 let keys = vec![1.0_f32; width];
434 let values = vec![2.0_f32; width];
435 cache
436 .write_kv(2, &keys, &values)
437 .expect("write should succeed");
438
439 let entry = cache.read_kv(2).expect("entry should exist");
440 assert_eq!(entry.keys, keys);
441 assert_eq!(entry.values, values);
442 }
443
444 #[test]
445 fn growing_seq_len_preserves_prior_writes() {
446 let cache = LayerKvCache::new(small_config());
447 cache.initialize(4).unwrap();
448
449 let width = cache.config.token_width();
450 let keys = vec![7.0_f32; width];
451 let values = vec![9.0_f32; width];
452 cache.write_kv(1, &keys, &values).unwrap();
453
454 cache.initialize(10).unwrap();
457 assert_eq!(cache.len(), 10);
458
459 let entry = cache.read_kv(1).unwrap();
460 assert_eq!(entry.keys, keys);
461 assert_eq!(entry.values, values);
462 }
463
464 #[test]
465 fn write_kv_rejects_wrong_width() {
466 let cache = LayerKvCache::new(small_config());
467 cache.initialize(4).unwrap();
468 let wrong_width = vec![0.0_f32; cache.config.token_width() + 1];
469 assert!(cache.write_kv(0, &wrong_width, &wrong_width).is_err());
470 }
471
472 #[test]
473 fn write_kv_rejects_out_of_bounds_token() {
474 let cache = LayerKvCache::new(small_config());
475 cache.initialize(4).unwrap();
476 let width = cache.config.token_width();
477 let data = vec![0.0_f32; width];
478 assert!(cache.write_kv(999, &data, &data).is_err());
479 }
480
481 #[test]
482 fn write_kv_before_initialize_is_rejected() {
483 let cache = LayerKvCache::new(small_config());
484 let width = cache.config.token_width();
485 let data = vec![0.0_f32; width];
486 assert!(cache.write_kv(0, &data, &data).is_err());
487 }
488
489 #[test]
490 fn read_range_returns_contiguous_data() {
491 let cache = LayerKvCache::new(small_config());
492 cache.initialize(4).unwrap();
493 let width = cache.config.token_width();
494
495 for token_idx in 0..4 {
496 let keys = vec![token_idx as f32; width];
497 let values = vec![(token_idx * 10) as f32; width];
498 cache.write_kv(token_idx, &keys, &values).unwrap();
499 }
500
501 let (keys, values) = cache.read_range(1, 3).unwrap();
502 assert_eq!(keys.len(), 2 * width);
503 assert_eq!(values.len(), 2 * width);
504 assert_eq!(keys[0], 1.0);
506 assert_eq!(values[0], 10.0);
507 assert_eq!(keys[width], 2.0);
509 assert_eq!(values[width], 20.0);
510 }
511
512 #[test]
513 fn read_range_rejects_end_beyond_current_len() {
514 let cache = LayerKvCache::new(small_config());
515 cache.initialize(4).unwrap();
516 assert!(cache.read_range(0, 100).is_err());
517 }
518
519 #[test]
520 fn is_empty_and_len_track_initialization() {
521 let cache = LayerKvCache::new(small_config());
522 assert!(cache.is_empty());
523 cache.initialize(5).unwrap();
524 assert!(!cache.is_empty());
525 assert_eq!(cache.len(), 5);
526 }
527
528 #[test]
529 fn concurrent_reads_do_not_corrupt_or_deadlock() {
530 let cache = LayerKvCache::new(small_config());
531 cache.initialize(8).unwrap();
532 let width = cache.config.token_width();
533 for token_idx in 0..8 {
534 let v = vec![token_idx as f32; width];
535 cache.write_kv(token_idx, &v, &v).unwrap();
536 }
537
538 let mut handles = Vec::new();
539 for _ in 0..8 {
540 let cache = cache.clone();
541 handles.push(std::thread::spawn(move || {
542 for token_idx in 0..8 {
543 let entry = cache.read_kv(token_idx).unwrap();
544 assert_eq!(entry.keys[0], token_idx as f32);
545 }
546 }));
547 }
548 for h in handles {
549 h.join().unwrap();
550 }
551 }
552
553 #[test]
554 fn clone_shares_the_same_underlying_cache() {
555 let cache = LayerKvCache::new(small_config());
556 cache.initialize(4).unwrap();
557 let width = cache.config.token_width();
558 let data = vec![3.0_f32; width];
559
560 let cache_clone = cache.clone();
561 cache_clone.write_kv(0, &data, &data).unwrap();
562
563 assert_eq!(cache.read_kv(0).unwrap().keys, data);
566 }
567
568 #[test]
569 fn write_kv_batch_multiple() {
570 let cache = LayerKvCache::new(small_config());
571 cache.initialize(8).unwrap();
572 let width = cache.config.token_width();
573
574 let k0 = vec![0.0_f32; width];
575 let v0 = vec![0.0_f32; width];
576 let k1 = vec![1.0_f32; width];
577 let v1 = vec![10.0_f32; width];
578
579 let entries = [
580 (0, k0.as_slice(), v0.as_slice()),
581 (1, k1.as_slice(), v1.as_slice()),
582 ];
583
584 cache.write_kv_batch(&entries).unwrap();
585 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
586 assert_eq!(cache.read_kv(1).unwrap().values[0], 10.0);
587 }
588
589 #[test]
590 fn write_kv_batch_empty() {
591 let cache = LayerKvCache::new(small_config());
592 cache.initialize(4).unwrap();
593 assert!(cache.write_kv_batch(&[]).is_ok());
594 }
595
596 #[test]
597 fn write_kv_batch_validation_fails() {
598 let cache = LayerKvCache::new(small_config());
599 cache.initialize(4).unwrap();
600 let width = cache.config.token_width();
601
602 let k0 = vec![1.0_f32; width];
603 let v0 = vec![2.0_f32; width];
604 let bad_k = vec![0.0_f32; width + 1];
605
606 let entries = [
607 (0, k0.as_slice(), v0.as_slice()),
608 (1, bad_k.as_slice(), v0.as_slice()),
609 ];
610
611 assert!(cache.write_kv_batch(&entries).is_err());
612 }
613
614 #[test]
615 fn write_kv_batch_is_atomic_on_validation_failure() {
616 let cache = LayerKvCache::new(small_config());
617 cache.initialize(4).unwrap();
618 let width = cache.config.token_width();
619
620 let good_keys = vec![5.0_f32; width];
621 let bad_keys = vec![0.0_f32; width + 1];
622
623 let entries = [
624 (0, good_keys.as_slice(), good_keys.as_slice()),
625 (1, bad_keys.as_slice(), good_keys.as_slice()),
626 ];
627
628 assert!(cache.write_kv_batch(&entries).is_err());
629 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
633 }
634
635 #[test]
636 fn with_read_kv_matches_read_kv() {
637 let cache = LayerKvCache::new(small_config());
638 cache.initialize(4).unwrap();
639 let width = cache.config.token_width();
640 let keys = vec![1.5_f32; width];
641 let values = vec![2.5_f32; width];
642 cache.write_kv(1, &keys, &values).unwrap();
643
644 let owned = cache.read_kv(1).unwrap();
645 cache
646 .with_read_kv(1, |k, v| {
647 assert_eq!(k, owned.keys.as_slice());
648 assert_eq!(v, owned.values.as_slice());
649 })
650 .unwrap();
651 }
652
653 #[test]
654 fn with_read_range_matches_read_range() {
655 let cache = LayerKvCache::new(small_config());
656 cache.initialize(4).unwrap();
657 let width = cache.config.token_width();
658 for t in 0..4 {
659 let k = vec![t as f32 + 0.1; width];
660 let v = vec![t as f32 + 0.2; width];
661 cache.write_kv(t, &k, &v).unwrap();
662 }
663
664 let (owned_keys, owned_values) = cache.read_range(1, 3).unwrap();
665 cache
666 .with_read_range(1, 3, |span| {
667 assert_eq!(span.keys, owned_keys.as_slice());
668 assert_eq!(span.values, owned_values.as_slice());
669 })
670 .unwrap();
671 }
672
673 #[test]
674 fn read_range_into_matches_read_range() {
675 let cache = LayerKvCache::new(small_config());
676 cache.initialize(4).unwrap();
677 let width = cache.config.token_width();
678 for t in 0..4 {
679 let k = vec![t as f32 + 0.1; width];
680 let v = vec![t as f32 + 0.2; width];
681 cache.write_kv(t, &k, &v).unwrap();
682 }
683
684 let (owned_keys, owned_values) = cache.read_range(0, 4).unwrap();
685 let mut keys_out = vec![0.0_f32; 4 * width];
686 let mut values_out = vec![0.0_f32; 4 * width];
687
688 cache
689 .read_range_into(0, 4, &mut keys_out, &mut values_out)
690 .unwrap();
691
692 assert_eq!(keys_out, owned_keys);
693 assert_eq!(values_out, owned_values);
694 }
695
696 #[test]
697 fn read_range_into_rejects_short_output_buffers() {
698 let cache = LayerKvCache::new(small_config());
699 cache.initialize(4).unwrap();
700 let width = cache.config.token_width();
701 let mut short_buf = vec![0.0_f32; width - 1];
702 let mut ok_buf = vec![0.0_f32; width];
703
704 assert!(cache
705 .read_range_into(0, 1, &mut short_buf, &mut ok_buf)
706 .is_err());
707 assert!(cache
708 .read_range_into(0, 1, &mut ok_buf, &mut short_buf)
709 .is_err());
710 }
711
712 #[test]
713 fn concurrent_mixed_readers_and_one_writer_no_corruption() {
714 let config = KVCacheConfig {
715 max_tokens_per_seq: 256,
716 num_heads: 2,
717 hidden_dim_per_head: 4,
718 };
719 let cache = LayerKvCache::new(config);
720 let width = config.token_width();
721 cache.initialize(1).unwrap();
722
723 let initial_keys = vec![1.0_f32; width];
724 let initial_vals = vec![2.0_f32; width];
725 cache.write_kv(0, &initial_keys, &initial_vals).unwrap();
726
727 let stop_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
728 let mut handles = Vec::new();
729
730 let writer_cache = cache.clone();
732 let writer_stop = stop_flag.clone();
733 handles.push(std::thread::spawn(move || {
734 let mut t = 1;
735 while !writer_stop.load(std::sync::atomic::Ordering::Relaxed) && t < 256 {
736 let k = vec![t as f32; width];
737 let v = vec![(t * 10) as f32; width];
738 if writer_cache.write_kv(t, &k, &v).is_ok() {
739 t += 1;
740 }
741 std::thread::yield_now();
742 }
743 }));
744
745 for _ in 0..4 {
747 let reader_cache = cache.clone();
748 let reader_stop = stop_flag.clone();
749 handles.push(std::thread::spawn(move || {
750 while !reader_stop.load(std::sync::atomic::Ordering::Relaxed) {
751 let len = reader_cache.len();
752 if len > 0 {
753 reader_cache
754 .with_read_range(0, len, |span| {
755 for i in 0..len {
756 let token_k = &span.keys[i * width..(i + 1) * width];
757 let token_v = &span.values[i * width..(i + 1) * width];
758 if i == 0 {
759 assert_eq!(token_k[0], 1.0);
760 assert_eq!(token_v[0], 2.0);
761 } else {
762 assert_eq!(token_k[0], i as f32);
763 assert_eq!(token_v[0], (i * 10) as f32);
764 }
765 }
766 })
767 .unwrap();
768 }
769 std::thread::yield_now();
770 }
771 }));
772 }
773
774 std::thread::sleep(std::time::Duration::from_millis(50));
775 stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);
776
777 for h in handles {
778 h.join().unwrap();
779 }
780 }
781
782 proptest! {
783 #[test]
784 fn proptest_random_writes_and_read_range_equals_concatenation(
785 token_count in 1usize..16,
786 seed in 0u64..1000
787 ) {
788 let config = KVCacheConfig {
789 max_tokens_per_seq: 16,
790 num_heads: 2,
791 hidden_dim_per_head: 4,
792 };
793 let cache = LayerKvCache::new(config);
794 let width = config.token_width();
795 cache.initialize(token_count).unwrap();
796
797 let mut expected_keys = Vec::new();
798 let mut expected_values = Vec::new();
799
800 for t in 0..token_count {
801 let k: Vec<f32> = (0..width).map(|i| (seed + t as u64 * 10 + i as u64) as f32).collect();
802 let v: Vec<f32> = (0..width).map(|i| (seed + t as u64 * 100 + i as u64) as f32).collect();
803 cache.write_kv(t, &k, &v).unwrap();
804 expected_keys.extend_from_slice(&k);
805 expected_values.extend_from_slice(&v);
806 }
807
808 let (read_k, read_v) = cache.read_range(0, token_count).unwrap();
809 prop_assert_eq!(read_k, expected_keys.clone());
810 prop_assert_eq!(read_v, expected_values.clone());
811
812 cache.with_read_range(0, token_count, |span| {
813 assert_eq!(span.keys, expected_keys.as_slice());
814 assert_eq!(span.values, expected_values.as_slice());
815 }).unwrap();
816 }
817 }
818}