Skip to main content

ghostlink_core/
kv_cache.rs

1//! KV cache primitives for a single sequence within a single transformer layer.
2//!
3//! Stores one contiguous, pre-allocated buffer for all tokens' keys and one
4//! for all values (shape: `max_tokens_per_seq * num_heads * hidden_dim_per_head`
5//! floats each), instead of one small heap allocation per token. The previous
6//! design called `resize_with` on a `Vec<KVCacheEntry>` where every entry
7//! independently allocated its own `keys`/`values` `Vec<f32>` — for the
8//! default 8192-token max sequence length, a full-length `initialize()`
9//! performed up to 16,384 separate small heap allocations. A single upfront
10//! allocation eliminates that, keeps a sequence's KV data in one contiguous
11//! cache-friendly region, and enables `read_range` — a single batched read
12//! over a token span, the shape attention actually needs, instead of N
13//! separate per-token reads.
14//!
15//! Reads and writes are guarded by an `RwLock` rather than a `Mutex`: real
16//! attention reads happen far more often than writes (every position reads
17//! all previous positions' KV every decode step; only one write happens per
18//! step), so letting reads proceed concurrently with each other matters.
19//!
20//! Zero-copy read paths (`with_read_kv`, `with_read_range`, `read_range_into`)
21//! allow inspecting or copying cached keys and values directly without owned
22//! heap allocations.
23//!
24//! ## Read API: prefer the zero-copy closure forms
25//!
26//! `with_read_kv` / `with_read_range` are the intended read API — they hand
27//! out borrowed slices under the read lock and allocate nothing. The owned
28//! forms `read_kv` / `read_range` exist only as a reference implementation and
29//! as the equivalence oracle for the zero-copy paths' tests; they are
30//! `#[doc(hidden)]` and deliberately **not** part of the public surface.
31//!
32//! Measured on the reference host (`cargo bench -p ghostlink-core --bench
33//! kv_cache`, `wide` config), the difference is not a micro-optimization:
34//!
35//! | operation | owned | zero-copy |
36//! |---|---|---|
37//! | `read_range_1024` | 4.57 ms | 12.8 ns |
38//! | `decode_step_256` | 141.9 ms | 1.26 ms |
39//! | `concurrent_4_readers_256` | 1.63 ms | 221 ns |
40//!
41//! A naive wiring of the owned paths into a decode loop costs 100x-350,000x.
42//! Use the closure forms.
43//!
44//! This module has no current caller in `runtime.rs` — Ghostlink delegates
45//! actual model execution to an external inference engine (llama-server /
46//! Ollama, see `ghost-link::native_engine`) rather than computing attention
47//! itself. It stays self-contained as a ready-to-use primitive for a future
48//! local execution path, so it can evolve without affecting the stable
49//! public API exported from `lib.rs`.
50
51use 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    /// Number of `f32` elements one token's keys (or values) occupy.
66    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/// Owned copy of one token's cached key/value vectors, returned by `read_kv`.
82/// Small and fixed-size (`config.token_width()` floats each) — not a copy of
83/// the whole cache.
84#[derive(Clone, Debug, PartialEq)]
85pub struct KVCacheEntry {
86    pub keys: Vec<f32>,
87    pub values: Vec<f32>,
88}
89
90/// Borrowed slice view over a contiguous token range's cached keys and values.
91#[derive(Debug, PartialEq)]
92pub struct KVSpan<'a> {
93    pub keys: &'a [f32],
94    pub values: &'a [f32],
95}
96
97struct KvCacheState {
98    /// Empty until the first `initialize()` call; allocated exactly once
99    /// after that, sized for `config.max_tokens_per_seq` tokens.
100    keys: Vec<f32>,
101    values: Vec<f32>,
102    /// Number of tokens actually written so far (<= config.max_tokens_per_seq).
103    current_len: usize,
104}
105
106/// Cheap to clone (an `Arc` around the shared, lock-guarded buffer) — matches
107/// the shared-state pattern used elsewhere in this crate (e.g. `ClusterState`).
108#[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        // Deliberately does not print the backing buffers — they can be
117        // megabytes (max_tokens_per_seq * token_width * 2 floats).
118        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    /// Creates an empty cache. Does not allocate the backing buffer yet —
133    /// `initialize()` performs the single upfront allocation, sized once for
134    /// `config.max_tokens_per_seq`.
135    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    /// Ensures the backing buffer exists (allocating it on the first call
147    /// only) and marks the first `seq_len` tokens (clamped to
148    /// `config.max_tokens_per_seq`) as valid.
149    ///
150    /// Safe to call again on the same cache — e.g. to grow `current_len` as
151    /// more tokens are generated — without losing previously written data:
152    /// the buffer is allocated once and never reallocated by this method.
153    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    /// Writes one token's key/value vectors into the cache at `token_idx`.
176    /// Copies directly into the pre-allocated buffer slice — no allocation.
177    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        // Pre-compute slice range bounds once upfront to reduce arithmetic
196        // calculations inside the lock-holding section.
197        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    /// Writes multiple tokens' key/value vectors under a single write-lock
215    /// acquisition, validating every entry upfront (before taking the lock)
216    /// so a bad entry anywhere in the batch fails the whole call without
217    /// partially writing it. Same per-token semantics as calling `write_kv`
218    /// in a loop — this only changes how many times the lock is acquired.
219    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        // OPTIMIZATION: Validate entries and compute max_token_idx upfront before acquiring
228        // the write lock. This eliminates conditional branch tracking inside the lock-holding loop.
229        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        // Single write lock acquisition
245        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        // Copy batch entries into backing slice buffer
254        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    /// Inspects one token's cached key/value slices directly under a read lock via a closure.
266    /// Zero heap allocation on this path.
267    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    /// Reads one token's cached key/value vectors as an owned, fixed-size
290    /// copy (`config.token_width()` floats each — not the whole cache).
291    ///
292    /// **Prefer [`Self::with_read_kv`]** — this allocates two `Vec`s per call
293    /// and is kept only as a reference implementation / test oracle (see the
294    /// module docs for the measured cost). Not part of the public API.
295    #[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    /// Inspects keys/values for a contiguous token range (`[start_token, end_token)`)
304    /// directly under a read lock via a closure that accepts a `KVSpan`.
305    /// Zero heap allocation on this path.
306    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    /// Reads keys/values for a contiguous token range (`[start_token,
341    /// end_token)`) in one call — the shape attention actually wants (all
342    /// prior positions at once), instead of `end_token - start_token`
343    /// separate lock acquisitions and small allocations.
344    ///
345    /// **Prefer [`Self::with_read_range`] or [`Self::read_range_into`]** —
346    /// this allocates two `Vec`s sized to the whole span on every call, which
347    /// in a decode loop is 100x-350,000x slower than the zero-copy forms (see
348    /// the module docs). Kept only as a reference implementation / test
349    /// oracle. Not part of the public API.
350    #[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    /// Copies keys and values for a contiguous token range into pre-allocated
362    /// caller slices `keys_out` and `values_out`.
363    /// Rejects output buffers if they are too small for the requested token range.
364    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        // Small width keeps test assertions easy to read; 2 heads * 4 dims = 8.
413        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        // Growing current_len must not touch the already-allocated buffer,
455        // so token 1's data must survive.
456        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        // First token in the range is token_idx 1.
505        assert_eq!(keys[0], 1.0);
506        assert_eq!(values[0], 10.0);
507        // Second token in the range is token_idx 2.
508        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        // Written through the clone, visible through the original — same
564        // underlying Arc<RwLock<..>>, not a deep copy.
565        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        // Upfront validation must reject the whole batch before the write
630        // lock is even taken, so entry 0 must still hold its zero-initialized
631        // default rather than the batch's (never-applied) value.
632        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        // Writer thread appends tokens
731        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        // 4 Reader threads
746        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}