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 read_kv(&self, token_idx: usize) -> Result<KVCacheEntry, String> {
182 let width = self.config.token_width();
183 let state = self
184 .state
185 .read()
186 .map_err(|_| "kv cache lock poisoned".to_string())?;
187
188 if token_idx >= state.current_len {
189 return Err(format!(
190 "token index {token_idx} out of bounds (current length {})",
191 state.current_len
192 ));
193 }
194
195 let start = token_idx * width;
196 Ok(KVCacheEntry {
197 keys: state.keys[start..start + width].to_vec(),
198 values: state.values[start..start + width].to_vec(),
199 })
200 }
201
202 pub fn read_range(
207 &self,
208 start_token: usize,
209 end_token: usize,
210 ) -> Result<(Vec<f32>, Vec<f32>), String> {
211 if start_token > end_token {
212 return Err("start_token must be <= end_token".to_string());
213 }
214 let width = self.config.token_width();
215 let state = self
216 .state
217 .read()
218 .map_err(|_| "kv cache lock poisoned".to_string())?;
219
220 if end_token > state.current_len {
221 return Err(format!(
222 "end_token {end_token} exceeds current length {}",
223 state.current_len
224 ));
225 }
226
227 let start = start_token * width;
228 let end = end_token * width;
229 Ok((
230 state.keys[start..end].to_vec(),
231 state.values[start..end].to_vec(),
232 ))
233 }
234
235 pub fn len(&self) -> usize {
236 self.state.read().map(|s| s.current_len).unwrap_or(0)
237 }
238
239 pub fn is_empty(&self) -> bool {
240 self.len() == 0
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn small_config() -> KVCacheConfig {
249 KVCacheConfig {
251 max_tokens_per_seq: 16,
252 num_heads: 2,
253 hidden_dim_per_head: 4,
254 }
255 }
256
257 #[test]
258 fn initialize_rejects_zero_len() {
259 let cache = LayerKvCache::default();
260 assert!(cache.initialize(0).is_err());
261 }
262
263 #[test]
264 fn initialize_and_round_trip() {
265 let cache = LayerKvCache::new(small_config());
266 cache.initialize(8).expect("cache should initialize");
267 assert_eq!(cache.len(), 8);
268
269 let width = cache.config.token_width();
270 let keys = vec![1.0_f32; width];
271 let values = vec![2.0_f32; width];
272 cache
273 .write_kv(2, &keys, &values)
274 .expect("write should succeed");
275
276 let entry = cache.read_kv(2).expect("entry should exist");
277 assert_eq!(entry.keys, keys);
278 assert_eq!(entry.values, values);
279 }
280
281 #[test]
282 fn growing_seq_len_preserves_prior_writes() {
283 let cache = LayerKvCache::new(small_config());
284 cache.initialize(4).unwrap();
285
286 let width = cache.config.token_width();
287 let keys = vec![7.0_f32; width];
288 let values = vec![9.0_f32; width];
289 cache.write_kv(1, &keys, &values).unwrap();
290
291 cache.initialize(10).unwrap();
294 assert_eq!(cache.len(), 10);
295
296 let entry = cache.read_kv(1).unwrap();
297 assert_eq!(entry.keys, keys);
298 assert_eq!(entry.values, values);
299 }
300
301 #[test]
302 fn write_kv_rejects_wrong_width() {
303 let cache = LayerKvCache::new(small_config());
304 cache.initialize(4).unwrap();
305 let wrong_width = vec![0.0_f32; cache.config.token_width() + 1];
306 assert!(cache.write_kv(0, &wrong_width, &wrong_width).is_err());
307 }
308
309 #[test]
310 fn write_kv_rejects_out_of_bounds_token() {
311 let cache = LayerKvCache::new(small_config());
312 cache.initialize(4).unwrap();
313 let width = cache.config.token_width();
314 let data = vec![0.0_f32; width];
315 assert!(cache.write_kv(999, &data, &data).is_err());
316 }
317
318 #[test]
319 fn write_kv_before_initialize_is_rejected() {
320 let cache = LayerKvCache::new(small_config());
321 let width = cache.config.token_width();
322 let data = vec![0.0_f32; width];
323 assert!(cache.write_kv(0, &data, &data).is_err());
324 }
325
326 #[test]
327 fn read_range_returns_contiguous_data() {
328 let cache = LayerKvCache::new(small_config());
329 cache.initialize(4).unwrap();
330 let width = cache.config.token_width();
331
332 for token_idx in 0..4 {
333 let keys = vec![token_idx as f32; width];
334 let values = vec![(token_idx * 10) as f32; width];
335 cache.write_kv(token_idx, &keys, &values).unwrap();
336 }
337
338 let (keys, values) = cache.read_range(1, 3).unwrap();
339 assert_eq!(keys.len(), 2 * width);
340 assert_eq!(values.len(), 2 * width);
341 assert_eq!(keys[0], 1.0);
343 assert_eq!(values[0], 10.0);
344 assert_eq!(keys[width], 2.0);
346 assert_eq!(values[width], 20.0);
347 }
348
349 #[test]
350 fn read_range_rejects_end_beyond_current_len() {
351 let cache = LayerKvCache::new(small_config());
352 cache.initialize(4).unwrap();
353 assert!(cache.read_range(0, 100).is_err());
354 }
355
356 #[test]
357 fn is_empty_and_len_track_initialization() {
358 let cache = LayerKvCache::new(small_config());
359 assert!(cache.is_empty());
360 cache.initialize(5).unwrap();
361 assert!(!cache.is_empty());
362 assert_eq!(cache.len(), 5);
363 }
364
365 #[test]
366 fn concurrent_reads_do_not_corrupt_or_deadlock() {
367 let cache = LayerKvCache::new(small_config());
368 cache.initialize(8).unwrap();
369 let width = cache.config.token_width();
370 for token_idx in 0..8 {
371 let v = vec![token_idx as f32; width];
372 cache.write_kv(token_idx, &v, &v).unwrap();
373 }
374
375 let mut handles = Vec::new();
376 for _ in 0..8 {
377 let cache = cache.clone();
378 handles.push(std::thread::spawn(move || {
379 for token_idx in 0..8 {
380 let entry = cache.read_kv(token_idx).unwrap();
381 assert_eq!(entry.keys[0], token_idx as f32);
382 }
383 }));
384 }
385 for h in handles {
386 h.join().unwrap();
387 }
388 }
389
390 #[test]
391 fn clone_shares_the_same_underlying_cache() {
392 let cache = LayerKvCache::new(small_config());
393 cache.initialize(4).unwrap();
394 let width = cache.config.token_width();
395 let data = vec![3.0_f32; width];
396
397 let cache_clone = cache.clone();
398 cache_clone.write_kv(0, &data, &data).unwrap();
399
400 assert_eq!(cache.read_kv(0).unwrap().keys, data);
403 }
404}