1use std::sync::{Arc, RwLock};
28
29pub const DEFAULT_MAX_TOKENS_PER_SEQ: usize = 8192;
30pub const DEFAULT_NUM_HEADS: usize = 4;
31pub const DEFAULT_HIDDEN_DIM_PER_HEAD: usize = 64;
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct KVCacheConfig {
35 pub max_tokens_per_seq: usize,
36 pub num_heads: usize,
37 pub hidden_dim_per_head: usize,
38}
39
40impl KVCacheConfig {
41 pub const fn token_width(&self) -> usize {
43 self.num_heads * self.hidden_dim_per_head
44 }
45}
46
47impl Default for KVCacheConfig {
48 fn default() -> Self {
49 Self {
50 max_tokens_per_seq: DEFAULT_MAX_TOKENS_PER_SEQ,
51 num_heads: DEFAULT_NUM_HEADS,
52 hidden_dim_per_head: DEFAULT_HIDDEN_DIM_PER_HEAD,
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq)]
61pub struct KVCacheEntry {
62 pub keys: Vec<f32>,
63 pub values: Vec<f32>,
64}
65
66struct KvCacheState {
67 keys: Vec<f32>,
70 values: Vec<f32>,
71 current_len: usize,
73}
74
75#[derive(Clone)]
78pub struct LayerKvCache {
79 pub config: KVCacheConfig,
80 state: Arc<RwLock<KvCacheState>>,
81}
82
83impl std::fmt::Debug for LayerKvCache {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("LayerKvCache")
88 .field("config", &self.config)
89 .field("current_len", &self.len())
90 .finish()
91 }
92}
93
94impl Default for LayerKvCache {
95 fn default() -> Self {
96 Self::new(KVCacheConfig::default())
97 }
98}
99
100impl LayerKvCache {
101 pub fn new(config: KVCacheConfig) -> Self {
105 Self {
106 config,
107 state: Arc::new(RwLock::new(KvCacheState {
108 keys: Vec::new(),
109 values: Vec::new(),
110 current_len: 0,
111 })),
112 }
113 }
114
115 pub fn initialize(&self, seq_len: usize) -> Result<(), String> {
123 if seq_len == 0 {
124 return Err("sequence length must be greater than zero".to_string());
125 }
126
127 let bounded_len = seq_len.min(self.config.max_tokens_per_seq);
128 let width = self.config.token_width();
129 let mut state = self
130 .state
131 .write()
132 .map_err(|_| "kv cache lock poisoned".to_string())?;
133
134 if state.keys.is_empty() {
135 let capacity = self.config.max_tokens_per_seq * width;
136 state.keys = vec![0.0; capacity];
137 state.values = vec![0.0; capacity];
138 }
139
140 state.current_len = state.current_len.max(bounded_len);
141 Ok(())
142 }
143
144 pub fn write_kv(&self, token_idx: usize, keys: &[f32], values: &[f32]) -> Result<(), String> {
147 if keys.len() != values.len() {
148 return Err("keys/values length mismatch".to_string());
149 }
150 let width = self.config.token_width();
151 if keys.len() != width {
152 return Err(format!(
153 "expected {width} elements per token (num_heads * hidden_dim_per_head), got {}",
154 keys.len()
155 ));
156 }
157 if token_idx >= self.config.max_tokens_per_seq {
158 return Err(format!(
159 "token index {token_idx} out of bounds (max {})",
160 self.config.max_tokens_per_seq
161 ));
162 }
163
164 let start = token_idx * width;
167 let end = start + width;
168
169 let mut state = self
170 .state
171 .write()
172 .map_err(|_| "kv cache lock poisoned".to_string())?;
173 if state.keys.is_empty() {
174 return Err("cache not initialized — call initialize() first".to_string());
175 }
176
177 state.keys[start..end].copy_from_slice(keys);
178 state.values[start..end].copy_from_slice(values);
179 state.current_len = state.current_len.max(token_idx + 1);
180 Ok(())
181 }
182
183 pub fn write_kv_batch(&self, entries: &[(usize, &[f32], &[f32])]) -> Result<(), String> {
189 if entries.is_empty() {
190 return Ok(());
191 }
192
193 let width = self.config.token_width();
194
195 for (token_idx, keys, values) in entries {
197 if keys.len() != values.len() {
198 return Err("keys/values length mismatch".to_string());
199 }
200 if keys.len() != width {
201 return Err(format!("expected {width} elements, got {}", keys.len()));
202 }
203 if *token_idx >= self.config.max_tokens_per_seq {
204 return Err(format!("token index {token_idx} out of bounds"));
205 }
206 }
207
208 let mut state = self
210 .state
211 .write()
212 .map_err(|_| "kv cache lock poisoned".to_string())?;
213 if state.keys.is_empty() {
214 return Err("cache not initialized".to_string());
215 }
216
217 let mut max_token_idx = 0;
218 for &(token_idx, keys, values) in entries {
221 let start = token_idx * width;
222 let end = start + width;
223 state.keys[start..end].copy_from_slice(keys);
224 state.values[start..end].copy_from_slice(values);
225 if token_idx > max_token_idx {
226 max_token_idx = token_idx;
227 }
228 }
229
230 state.current_len = state.current_len.max(max_token_idx + 1);
231 Ok(())
232 }
233
234 pub fn read_kv(&self, token_idx: usize) -> Result<KVCacheEntry, String> {
237 let width = self.config.token_width();
238 let state = self
239 .state
240 .read()
241 .map_err(|_| "kv cache lock poisoned".to_string())?;
242
243 if token_idx >= state.current_len {
244 return Err(format!(
245 "token index {token_idx} out of bounds (current length {})",
246 state.current_len
247 ));
248 }
249
250 let start = token_idx * width;
251 Ok(KVCacheEntry {
252 keys: state.keys[start..start + width].to_vec(),
253 values: state.values[start..start + width].to_vec(),
254 })
255 }
256
257 pub fn read_range(
262 &self,
263 start_token: usize,
264 end_token: usize,
265 ) -> Result<(Vec<f32>, Vec<f32>), String> {
266 if start_token > end_token {
267 return Err("start_token must be <= end_token".to_string());
268 }
269 let width = self.config.token_width();
270 let state = self
271 .state
272 .read()
273 .map_err(|_| "kv cache lock poisoned".to_string())?;
274
275 if end_token > state.current_len {
276 return Err(format!(
277 "end_token {end_token} exceeds current length {}",
278 state.current_len
279 ));
280 }
281
282 let start = start_token * width;
283 let end = end_token * width;
284 Ok((
285 state.keys[start..end].to_vec(),
286 state.values[start..end].to_vec(),
287 ))
288 }
289
290 pub fn len(&self) -> usize {
291 self.state.read().map(|s| s.current_len).unwrap_or(0)
292 }
293
294 pub fn is_empty(&self) -> bool {
295 self.len() == 0
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn small_config() -> KVCacheConfig {
304 KVCacheConfig {
306 max_tokens_per_seq: 16,
307 num_heads: 2,
308 hidden_dim_per_head: 4,
309 }
310 }
311
312 #[test]
313 fn initialize_rejects_zero_len() {
314 let cache = LayerKvCache::default();
315 assert!(cache.initialize(0).is_err());
316 }
317
318 #[test]
319 fn initialize_and_round_trip() {
320 let cache = LayerKvCache::new(small_config());
321 cache.initialize(8).expect("cache should initialize");
322 assert_eq!(cache.len(), 8);
323
324 let width = cache.config.token_width();
325 let keys = vec![1.0_f32; width];
326 let values = vec![2.0_f32; width];
327 cache
328 .write_kv(2, &keys, &values)
329 .expect("write should succeed");
330
331 let entry = cache.read_kv(2).expect("entry should exist");
332 assert_eq!(entry.keys, keys);
333 assert_eq!(entry.values, values);
334 }
335
336 #[test]
337 fn growing_seq_len_preserves_prior_writes() {
338 let cache = LayerKvCache::new(small_config());
339 cache.initialize(4).unwrap();
340
341 let width = cache.config.token_width();
342 let keys = vec![7.0_f32; width];
343 let values = vec![9.0_f32; width];
344 cache.write_kv(1, &keys, &values).unwrap();
345
346 cache.initialize(10).unwrap();
349 assert_eq!(cache.len(), 10);
350
351 let entry = cache.read_kv(1).unwrap();
352 assert_eq!(entry.keys, keys);
353 assert_eq!(entry.values, values);
354 }
355
356 #[test]
357 fn write_kv_rejects_wrong_width() {
358 let cache = LayerKvCache::new(small_config());
359 cache.initialize(4).unwrap();
360 let wrong_width = vec![0.0_f32; cache.config.token_width() + 1];
361 assert!(cache.write_kv(0, &wrong_width, &wrong_width).is_err());
362 }
363
364 #[test]
365 fn write_kv_rejects_out_of_bounds_token() {
366 let cache = LayerKvCache::new(small_config());
367 cache.initialize(4).unwrap();
368 let width = cache.config.token_width();
369 let data = vec![0.0_f32; width];
370 assert!(cache.write_kv(999, &data, &data).is_err());
371 }
372
373 #[test]
374 fn write_kv_before_initialize_is_rejected() {
375 let cache = LayerKvCache::new(small_config());
376 let width = cache.config.token_width();
377 let data = vec![0.0_f32; width];
378 assert!(cache.write_kv(0, &data, &data).is_err());
379 }
380
381 #[test]
382 fn read_range_returns_contiguous_data() {
383 let cache = LayerKvCache::new(small_config());
384 cache.initialize(4).unwrap();
385 let width = cache.config.token_width();
386
387 for token_idx in 0..4 {
388 let keys = vec![token_idx as f32; width];
389 let values = vec![(token_idx * 10) as f32; width];
390 cache.write_kv(token_idx, &keys, &values).unwrap();
391 }
392
393 let (keys, values) = cache.read_range(1, 3).unwrap();
394 assert_eq!(keys.len(), 2 * width);
395 assert_eq!(values.len(), 2 * width);
396 assert_eq!(keys[0], 1.0);
398 assert_eq!(values[0], 10.0);
399 assert_eq!(keys[width], 2.0);
401 assert_eq!(values[width], 20.0);
402 }
403
404 #[test]
405 fn read_range_rejects_end_beyond_current_len() {
406 let cache = LayerKvCache::new(small_config());
407 cache.initialize(4).unwrap();
408 assert!(cache.read_range(0, 100).is_err());
409 }
410
411 #[test]
412 fn is_empty_and_len_track_initialization() {
413 let cache = LayerKvCache::new(small_config());
414 assert!(cache.is_empty());
415 cache.initialize(5).unwrap();
416 assert!(!cache.is_empty());
417 assert_eq!(cache.len(), 5);
418 }
419
420 #[test]
421 fn concurrent_reads_do_not_corrupt_or_deadlock() {
422 let cache = LayerKvCache::new(small_config());
423 cache.initialize(8).unwrap();
424 let width = cache.config.token_width();
425 for token_idx in 0..8 {
426 let v = vec![token_idx as f32; width];
427 cache.write_kv(token_idx, &v, &v).unwrap();
428 }
429
430 let mut handles = Vec::new();
431 for _ in 0..8 {
432 let cache = cache.clone();
433 handles.push(std::thread::spawn(move || {
434 for token_idx in 0..8 {
435 let entry = cache.read_kv(token_idx).unwrap();
436 assert_eq!(entry.keys[0], token_idx as f32);
437 }
438 }));
439 }
440 for h in handles {
441 h.join().unwrap();
442 }
443 }
444
445 #[test]
446 fn clone_shares_the_same_underlying_cache() {
447 let cache = LayerKvCache::new(small_config());
448 cache.initialize(4).unwrap();
449 let width = cache.config.token_width();
450 let data = vec![3.0_f32; width];
451
452 let cache_clone = cache.clone();
453 cache_clone.write_kv(0, &data, &data).unwrap();
454
455 assert_eq!(cache.read_kv(0).unwrap().keys, data);
458 }
459
460 #[test]
461 fn write_kv_batch_multiple() {
462 let cache = LayerKvCache::new(small_config());
463 cache.initialize(8).unwrap();
464 let width = cache.config.token_width();
465
466 let k0 = vec![0.0_f32; width];
467 let v0 = vec![0.0_f32; width];
468 let k1 = vec![1.0_f32; width];
469 let v1 = vec![10.0_f32; width];
470
471 let entries = [
472 (0, k0.as_slice(), v0.as_slice()),
473 (1, k1.as_slice(), v1.as_slice()),
474 ];
475
476 cache.write_kv_batch(&entries).unwrap();
477 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
478 assert_eq!(cache.read_kv(1).unwrap().values[0], 10.0);
479 }
480
481 #[test]
482 fn write_kv_batch_empty() {
483 let cache = LayerKvCache::new(small_config());
484 cache.initialize(4).unwrap();
485 assert!(cache.write_kv_batch(&[]).is_ok());
486 }
487
488 #[test]
489 fn write_kv_batch_validation_fails() {
490 let cache = LayerKvCache::new(small_config());
491 cache.initialize(4).unwrap();
492 let width = cache.config.token_width();
493
494 let k0 = vec![1.0_f32; width];
495 let v0 = vec![2.0_f32; width];
496 let bad_k = vec![0.0_f32; width + 1];
497
498 let entries = [
499 (0, k0.as_slice(), v0.as_slice()),
500 (1, bad_k.as_slice(), v0.as_slice()),
501 ];
502
503 assert!(cache.write_kv_batch(&entries).is_err());
504 }
505
506 #[test]
507 fn write_kv_batch_is_atomic_on_validation_failure() {
508 let cache = LayerKvCache::new(small_config());
509 cache.initialize(4).unwrap();
510 let width = cache.config.token_width();
511
512 let good_keys = vec![5.0_f32; width];
513 let bad_keys = vec![0.0_f32; width + 1];
514
515 let entries = [
516 (0, good_keys.as_slice(), good_keys.as_slice()),
517 (1, bad_keys.as_slice(), good_keys.as_slice()),
518 ];
519
520 assert!(cache.write_kv_batch(&entries).is_err());
521 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
525 }
526}