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 mut state = self
165 .state
166 .write()
167 .map_err(|_| "kv cache lock poisoned".to_string())?;
168 if state.keys.is_empty() {
169 return Err("cache not initialized — call initialize() first".to_string());
170 }
171
172 let start = token_idx * width;
173 state.keys[start..start + width].copy_from_slice(keys);
174 state.values[start..start + width].copy_from_slice(values);
175 state.current_len = state.current_len.max(token_idx + 1);
176 Ok(())
177 }
178
179 pub fn write_kv_batch(&self, entries: &[(usize, &[f32], &[f32])]) -> Result<(), String> {
185 if entries.is_empty() {
186 return Ok(());
187 }
188
189 let width = self.config.token_width();
190
191 for (token_idx, keys, values) in entries {
193 if keys.len() != values.len() {
194 return Err("keys/values length mismatch".to_string());
195 }
196 if keys.len() != width {
197 return Err(format!("expected {width} elements, got {}", keys.len()));
198 }
199 if *token_idx >= self.config.max_tokens_per_seq {
200 return Err(format!("token index {token_idx} out of bounds"));
201 }
202 }
203
204 let mut state = self
206 .state
207 .write()
208 .map_err(|_| "kv cache lock poisoned".to_string())?;
209 if state.keys.is_empty() {
210 return Err("cache not initialized".to_string());
211 }
212
213 let mut max_token_idx = 0;
214 for (token_idx, keys, values) in entries {
215 let start = token_idx * width;
216 state.keys[start..start + width].copy_from_slice(keys);
217 state.values[start..start + width].copy_from_slice(values);
218 max_token_idx = max_token_idx.max(*token_idx);
219 }
220
221 state.current_len = state.current_len.max(max_token_idx + 1);
222 Ok(())
223 }
224
225 pub fn read_kv(&self, token_idx: usize) -> Result<KVCacheEntry, String> {
228 let width = self.config.token_width();
229 let state = self
230 .state
231 .read()
232 .map_err(|_| "kv cache lock poisoned".to_string())?;
233
234 if token_idx >= state.current_len {
235 return Err(format!(
236 "token index {token_idx} out of bounds (current length {})",
237 state.current_len
238 ));
239 }
240
241 let start = token_idx * width;
242 Ok(KVCacheEntry {
243 keys: state.keys[start..start + width].to_vec(),
244 values: state.values[start..start + width].to_vec(),
245 })
246 }
247
248 pub fn read_range(
253 &self,
254 start_token: usize,
255 end_token: usize,
256 ) -> Result<(Vec<f32>, Vec<f32>), String> {
257 if start_token > end_token {
258 return Err("start_token must be <= end_token".to_string());
259 }
260 let width = self.config.token_width();
261 let state = self
262 .state
263 .read()
264 .map_err(|_| "kv cache lock poisoned".to_string())?;
265
266 if end_token > state.current_len {
267 return Err(format!(
268 "end_token {end_token} exceeds current length {}",
269 state.current_len
270 ));
271 }
272
273 let start = start_token * width;
274 let end = end_token * width;
275 Ok((
276 state.keys[start..end].to_vec(),
277 state.values[start..end].to_vec(),
278 ))
279 }
280
281 pub fn len(&self) -> usize {
282 self.state.read().map(|s| s.current_len).unwrap_or(0)
283 }
284
285 pub fn is_empty(&self) -> bool {
286 self.len() == 0
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn small_config() -> KVCacheConfig {
295 KVCacheConfig {
297 max_tokens_per_seq: 16,
298 num_heads: 2,
299 hidden_dim_per_head: 4,
300 }
301 }
302
303 #[test]
304 fn initialize_rejects_zero_len() {
305 let cache = LayerKvCache::default();
306 assert!(cache.initialize(0).is_err());
307 }
308
309 #[test]
310 fn initialize_and_round_trip() {
311 let cache = LayerKvCache::new(small_config());
312 cache.initialize(8).expect("cache should initialize");
313 assert_eq!(cache.len(), 8);
314
315 let width = cache.config.token_width();
316 let keys = vec![1.0_f32; width];
317 let values = vec![2.0_f32; width];
318 cache
319 .write_kv(2, &keys, &values)
320 .expect("write should succeed");
321
322 let entry = cache.read_kv(2).expect("entry should exist");
323 assert_eq!(entry.keys, keys);
324 assert_eq!(entry.values, values);
325 }
326
327 #[test]
328 fn growing_seq_len_preserves_prior_writes() {
329 let cache = LayerKvCache::new(small_config());
330 cache.initialize(4).unwrap();
331
332 let width = cache.config.token_width();
333 let keys = vec![7.0_f32; width];
334 let values = vec![9.0_f32; width];
335 cache.write_kv(1, &keys, &values).unwrap();
336
337 cache.initialize(10).unwrap();
340 assert_eq!(cache.len(), 10);
341
342 let entry = cache.read_kv(1).unwrap();
343 assert_eq!(entry.keys, keys);
344 assert_eq!(entry.values, values);
345 }
346
347 #[test]
348 fn write_kv_rejects_wrong_width() {
349 let cache = LayerKvCache::new(small_config());
350 cache.initialize(4).unwrap();
351 let wrong_width = vec![0.0_f32; cache.config.token_width() + 1];
352 assert!(cache.write_kv(0, &wrong_width, &wrong_width).is_err());
353 }
354
355 #[test]
356 fn write_kv_rejects_out_of_bounds_token() {
357 let cache = LayerKvCache::new(small_config());
358 cache.initialize(4).unwrap();
359 let width = cache.config.token_width();
360 let data = vec![0.0_f32; width];
361 assert!(cache.write_kv(999, &data, &data).is_err());
362 }
363
364 #[test]
365 fn write_kv_before_initialize_is_rejected() {
366 let cache = LayerKvCache::new(small_config());
367 let width = cache.config.token_width();
368 let data = vec![0.0_f32; width];
369 assert!(cache.write_kv(0, &data, &data).is_err());
370 }
371
372 #[test]
373 fn read_range_returns_contiguous_data() {
374 let cache = LayerKvCache::new(small_config());
375 cache.initialize(4).unwrap();
376 let width = cache.config.token_width();
377
378 for token_idx in 0..4 {
379 let keys = vec![token_idx as f32; width];
380 let values = vec![(token_idx * 10) as f32; width];
381 cache.write_kv(token_idx, &keys, &values).unwrap();
382 }
383
384 let (keys, values) = cache.read_range(1, 3).unwrap();
385 assert_eq!(keys.len(), 2 * width);
386 assert_eq!(values.len(), 2 * width);
387 assert_eq!(keys[0], 1.0);
389 assert_eq!(values[0], 10.0);
390 assert_eq!(keys[width], 2.0);
392 assert_eq!(values[width], 20.0);
393 }
394
395 #[test]
396 fn read_range_rejects_end_beyond_current_len() {
397 let cache = LayerKvCache::new(small_config());
398 cache.initialize(4).unwrap();
399 assert!(cache.read_range(0, 100).is_err());
400 }
401
402 #[test]
403 fn is_empty_and_len_track_initialization() {
404 let cache = LayerKvCache::new(small_config());
405 assert!(cache.is_empty());
406 cache.initialize(5).unwrap();
407 assert!(!cache.is_empty());
408 assert_eq!(cache.len(), 5);
409 }
410
411 #[test]
412 fn concurrent_reads_do_not_corrupt_or_deadlock() {
413 let cache = LayerKvCache::new(small_config());
414 cache.initialize(8).unwrap();
415 let width = cache.config.token_width();
416 for token_idx in 0..8 {
417 let v = vec![token_idx as f32; width];
418 cache.write_kv(token_idx, &v, &v).unwrap();
419 }
420
421 let mut handles = Vec::new();
422 for _ in 0..8 {
423 let cache = cache.clone();
424 handles.push(std::thread::spawn(move || {
425 for token_idx in 0..8 {
426 let entry = cache.read_kv(token_idx).unwrap();
427 assert_eq!(entry.keys[0], token_idx as f32);
428 }
429 }));
430 }
431 for h in handles {
432 h.join().unwrap();
433 }
434 }
435
436 #[test]
437 fn clone_shares_the_same_underlying_cache() {
438 let cache = LayerKvCache::new(small_config());
439 cache.initialize(4).unwrap();
440 let width = cache.config.token_width();
441 let data = vec![3.0_f32; width];
442
443 let cache_clone = cache.clone();
444 cache_clone.write_kv(0, &data, &data).unwrap();
445
446 assert_eq!(cache.read_kv(0).unwrap().keys, data);
449 }
450
451 #[test]
452 fn write_kv_batch_multiple() {
453 let cache = LayerKvCache::new(small_config());
454 cache.initialize(8).unwrap();
455 let width = cache.config.token_width();
456
457 let k0 = vec![0.0_f32; width];
458 let v0 = vec![0.0_f32; width];
459 let k1 = vec![1.0_f32; width];
460 let v1 = vec![10.0_f32; width];
461
462 let entries = [
463 (0, k0.as_slice(), v0.as_slice()),
464 (1, k1.as_slice(), v1.as_slice()),
465 ];
466
467 cache.write_kv_batch(&entries).unwrap();
468 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
469 assert_eq!(cache.read_kv(1).unwrap().values[0], 10.0);
470 }
471
472 #[test]
473 fn write_kv_batch_empty() {
474 let cache = LayerKvCache::new(small_config());
475 cache.initialize(4).unwrap();
476 assert!(cache.write_kv_batch(&[]).is_ok());
477 }
478
479 #[test]
480 fn write_kv_batch_validation_fails() {
481 let cache = LayerKvCache::new(small_config());
482 cache.initialize(4).unwrap();
483 let width = cache.config.token_width();
484
485 let k0 = vec![1.0_f32; width];
486 let v0 = vec![2.0_f32; width];
487 let bad_k = vec![0.0_f32; width + 1];
488
489 let entries = [
490 (0, k0.as_slice(), v0.as_slice()),
491 (1, bad_k.as_slice(), v0.as_slice()),
492 ];
493
494 assert!(cache.write_kv_batch(&entries).is_err());
495 }
496
497 #[test]
498 fn write_kv_batch_is_atomic_on_validation_failure() {
499 let cache = LayerKvCache::new(small_config());
500 cache.initialize(4).unwrap();
501 let width = cache.config.token_width();
502
503 let good_keys = vec![5.0_f32; width];
504 let bad_keys = vec![0.0_f32; width + 1];
505
506 let entries = [
507 (0, good_keys.as_slice(), good_keys.as_slice()),
508 (1, bad_keys.as_slice(), good_keys.as_slice()),
509 ];
510
511 assert!(cache.write_kv_batch(&entries).is_err());
512 assert_eq!(cache.read_kv(0).unwrap().keys[0], 0.0);
516 }
517}