/home/noah/src/realizar/src/inference/kv_cache.rs
Line | Count | Source |
1 | | //! Key-Value cache for efficient autoregressive generation |
2 | | //! |
3 | | //! Stores past key and value tensors to avoid recomputation during generation. |
4 | | //! Enables O(1) per-token computation instead of O(n) for sequence of length n. |
5 | | //! |
6 | | //! ## Cache Types |
7 | | //! |
8 | | //! - [`KVCache`] - Basic KV cache with row-major storage |
9 | | //! - [`OptimizedKVCache`] - Optimized cache with transposed V for better memory access |
10 | | //! |
11 | | //! ## Attention Functions |
12 | | //! |
13 | | //! - [`attention_with_cache`] - Scaled dot-product attention using cached K/V |
14 | | //! - [`attention_with_transposed_v`] - Attention with transposed V storage |
15 | | |
16 | | use super::{simd_dot, simd_softmax}; |
17 | | |
18 | | /// Key-Value cache for autoregressive generation |
19 | | /// |
20 | | /// Stores past key and value tensors to avoid recomputation during generation. |
21 | | /// Enables O(1) per-token computation instead of O(n) for sequence of length n. |
22 | | /// |
23 | | /// # Example |
24 | | /// |
25 | | /// ``` |
26 | | /// use realizar::inference::KVCache; |
27 | | /// |
28 | | /// let mut cache = KVCache::new(12, 768, 2048); // 12 layers, 768 hidden, 2048 max seq |
29 | | /// assert!(cache.is_empty()); |
30 | | /// |
31 | | /// // Store KV for position 0 |
32 | | /// let k = vec![0.1; 768]; |
33 | | /// let v = vec![0.2; 768]; |
34 | | /// cache.store(0, &k, &v); |
35 | | /// cache.advance(); |
36 | | /// |
37 | | /// assert_eq!(cache.len(), 1); |
38 | | /// ``` |
39 | | #[derive(Clone)] |
40 | | pub struct KVCache { |
41 | | /// Key cache: [num_layers, max_seq_len, kv_dim] |
42 | | k_cache: Vec<Vec<f32>>, |
43 | | /// Value cache: [num_layers, max_seq_len, kv_dim] |
44 | | v_cache: Vec<Vec<f32>>, |
45 | | /// Current sequence length |
46 | | seq_len: usize, |
47 | | /// Hidden dimension per head |
48 | | hidden_dim: usize, |
49 | | } |
50 | | |
51 | | impl KVCache { |
52 | | /// Create a new KV cache |
53 | | /// |
54 | | /// # Arguments |
55 | | /// |
56 | | /// * `num_layers` - Number of transformer layers |
57 | | /// * `hidden_dim` - Hidden dimension (total across all heads) |
58 | | /// * `max_seq_len` - Maximum sequence length to cache |
59 | | #[must_use] |
60 | 14 | pub fn new(num_layers: usize, hidden_dim: usize, max_seq_len: usize) -> Self { |
61 | 14 | let k_cache = vec![vec![0.0; max_seq_len * hidden_dim]; num_layers]; |
62 | 14 | let v_cache = vec![vec![0.0; max_seq_len * hidden_dim]; num_layers]; |
63 | 14 | Self { |
64 | 14 | k_cache, |
65 | 14 | v_cache, |
66 | 14 | seq_len: 0, |
67 | 14 | hidden_dim, |
68 | 14 | } |
69 | 14 | } |
70 | | |
71 | | /// Store a new KV pair for a layer |
72 | | /// |
73 | | /// Stores the key and value vectors at the current sequence position. |
74 | | /// Does nothing if the cache is full. |
75 | | /// |
76 | | /// # Arguments |
77 | | /// |
78 | | /// * `layer` - Layer index (0 to num_layers-1) |
79 | | /// * `k` - Key vector of length `hidden_dim` |
80 | | /// * `v` - Value vector of length `hidden_dim` |
81 | 151 | pub fn store(&mut self, layer: usize, k: &[f32], v: &[f32]) { |
82 | 151 | let start = self.seq_len * self.hidden_dim; |
83 | 151 | let end = start + self.hidden_dim; |
84 | | |
85 | 151 | if end <= self.k_cache[layer].len() { |
86 | 150 | self.k_cache[layer][start..end].copy_from_slice(k); |
87 | 150 | self.v_cache[layer][start..end].copy_from_slice(v); |
88 | 150 | }1 |
89 | 151 | } |
90 | | |
91 | | /// Advance the sequence position |
92 | | /// |
93 | | /// Call after storing all layers for the current position. |
94 | 50 | pub fn advance(&mut self) { |
95 | 50 | self.seq_len += 1; |
96 | 50 | } |
97 | | |
98 | | /// Get cached keys for a layer |
99 | | /// |
100 | | /// Returns all keys from position 0 to current seq_len. |
101 | | #[must_use] |
102 | 14 | pub fn get_k(&self, layer: usize) -> &[f32] { |
103 | 14 | &self.k_cache[layer][..self.seq_len * self.hidden_dim] |
104 | 14 | } |
105 | | |
106 | | /// Get cached values for a layer |
107 | | /// |
108 | | /// Returns all values from position 0 to current seq_len. |
109 | | #[must_use] |
110 | 12 | pub fn get_v(&self, layer: usize) -> &[f32] { |
111 | 12 | &self.v_cache[layer][..self.seq_len * self.hidden_dim] |
112 | 12 | } |
113 | | |
114 | | /// Get current sequence length |
115 | | #[must_use] |
116 | 10 | pub fn len(&self) -> usize { |
117 | 10 | self.seq_len |
118 | 10 | } |
119 | | |
120 | | /// Check if cache is empty |
121 | | #[must_use] |
122 | 6 | pub fn is_empty(&self) -> bool { |
123 | 6 | self.seq_len == 0 |
124 | 6 | } |
125 | | |
126 | | /// Reset the cache for a new sequence |
127 | | /// |
128 | | /// Clears the sequence position but keeps allocated memory. |
129 | 3 | pub fn reset(&mut self) { |
130 | 3 | self.seq_len = 0; |
131 | 3 | } |
132 | | } |
133 | | |
134 | | /// Compute attention with KV cache |
135 | | /// |
136 | | /// Computes scaled dot-product attention: softmax(QK^T / sqrt(d)) V |
137 | | /// Uses cached K and V from previous positions. |
138 | | /// |
139 | | /// # Arguments |
140 | | /// |
141 | | /// * `q` - Query vector [hidden_dim] |
142 | | /// * `k_cache` - Cached keys [seq_len × hidden_dim] |
143 | | /// * `v_cache` - Cached values [seq_len × hidden_dim] |
144 | | /// * `current_k` - Key for current position [hidden_dim] |
145 | | /// * `current_v` - Value for current position [hidden_dim] |
146 | | /// * `num_heads` - Number of attention heads |
147 | | /// |
148 | | /// # Returns |
149 | | /// |
150 | | /// Attention output [hidden_dim] |
151 | | /// |
152 | | /// # Example |
153 | | /// |
154 | | /// ``` |
155 | | /// use realizar::inference::attention_with_cache; |
156 | | /// |
157 | | /// let hidden_dim = 64; |
158 | | /// let num_heads = 2; |
159 | | /// |
160 | | /// let q = vec![0.1; hidden_dim]; |
161 | | /// let k_cache: Vec<f32> = vec![]; // No cached positions |
162 | | /// let v_cache: Vec<f32> = vec![]; |
163 | | /// let current_k = vec![0.1; hidden_dim]; |
164 | | /// let current_v = vec![0.2; hidden_dim]; |
165 | | /// |
166 | | /// let output = attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
167 | | /// assert_eq!(output.len(), hidden_dim); |
168 | | /// ``` |
169 | | #[must_use] |
170 | 22 | pub fn attention_with_cache( |
171 | 22 | q: &[f32], |
172 | 22 | k_cache: &[f32], |
173 | 22 | v_cache: &[f32], |
174 | 22 | current_k: &[f32], |
175 | 22 | current_v: &[f32], |
176 | 22 | num_heads: usize, |
177 | 22 | ) -> Vec<f32> { |
178 | 22 | let hidden_dim = q.len(); |
179 | 22 | let head_dim = hidden_dim / num_heads; |
180 | 22 | let cache_len = if hidden_dim > 0 { |
181 | 22 | k_cache.len() / hidden_dim |
182 | | } else { |
183 | 0 | 0 |
184 | | }; |
185 | 22 | let total_len = cache_len + 1; |
186 | 22 | let scale = 1.0 / (head_dim as f32).sqrt(); |
187 | | |
188 | 22 | let mut output = vec![0.0; hidden_dim]; |
189 | | |
190 | | // Process each head independently |
191 | 87 | for h in 0..num_heads22 { |
192 | 87 | let head_offset = h * head_dim; |
193 | 87 | let q_head = &q[head_offset..head_offset + head_dim]; |
194 | | |
195 | | // Compute attention scores for all positions |
196 | 87 | let mut scores = Vec::with_capacity(total_len); |
197 | | |
198 | | // Scores against cached K |
199 | 128 | for pos in 0..cache_len87 { |
200 | 128 | let k_start = pos * hidden_dim + head_offset; |
201 | 128 | let k_head = &k_cache[k_start..k_start + head_dim]; |
202 | 128 | let score = simd_dot(q_head, k_head) * scale; |
203 | 128 | scores.push(score); |
204 | 128 | } |
205 | | |
206 | | // Score against current K |
207 | 87 | let current_k_head = ¤t_k[head_offset..head_offset + head_dim]; |
208 | 87 | scores.push(simd_dot(q_head, current_k_head) * scale); |
209 | | |
210 | | // Softmax |
211 | 87 | simd_softmax(&mut scores); |
212 | | |
213 | | // Weighted sum of V |
214 | 87 | let out_head = &mut output[head_offset..head_offset + head_dim]; |
215 | | |
216 | 128 | for (pos, &weight) in scores.iter()87 .enumerate87 ().take87 (cache_len87 ) { |
217 | 128 | let v_start = pos * hidden_dim + head_offset; |
218 | 128 | let v_head = &v_cache[v_start..v_start + head_dim]; |
219 | 7.69k | for (i, &v) in v_head128 .iter128 ().enumerate128 () { |
220 | 7.69k | out_head[i] += weight * v; |
221 | 7.69k | } |
222 | | } |
223 | | |
224 | | // Add contribution from current V |
225 | 87 | let current_v_head = ¤t_v[head_offset..head_offset + head_dim]; |
226 | 87 | let current_weight = scores[cache_len]; |
227 | 3.92k | for (i, &v) in current_v_head87 .iter87 ().enumerate87 () { |
228 | 3.92k | out_head[i] += current_weight * v; |
229 | 3.92k | } |
230 | | } |
231 | | |
232 | 22 | output |
233 | 22 | } |
234 | | |
235 | | /// Optimized KV cache with contiguous storage |
236 | | /// |
237 | | /// Uses transposed V storage for better memory access during attention. |
238 | | /// The value cache is stored as [hidden_dim × max_seq_len] instead of |
239 | | /// [max_seq_len × hidden_dim], enabling better cache locality when |
240 | | /// computing attention. |
241 | | #[derive(Clone)] |
242 | | pub struct OptimizedKVCache { |
243 | | /// Key cache: [num_layers][seq_len × hidden_dim] |
244 | | k_cache: Vec<Vec<f32>>, |
245 | | /// Value cache (transposed): [num_layers][hidden_dim × seq_len] |
246 | | v_cache: Vec<Vec<f32>>, |
247 | | /// Current sequence length |
248 | | seq_len: usize, |
249 | | /// Hidden dimension |
250 | | hidden_dim: usize, |
251 | | /// Maximum sequence length |
252 | | max_seq_len: usize, |
253 | | } |
254 | | |
255 | | impl OptimizedKVCache { |
256 | | /// Create a new optimized KV cache |
257 | | #[must_use] |
258 | 6 | pub fn new(num_layers: usize, hidden_dim: usize, max_seq_len: usize) -> Self { |
259 | 6 | let k_cache = vec![vec![0.0; max_seq_len * hidden_dim]; num_layers]; |
260 | 6 | let v_cache = vec![vec![0.0; hidden_dim * max_seq_len]; num_layers]; |
261 | 6 | Self { |
262 | 6 | k_cache, |
263 | 6 | v_cache, |
264 | 6 | seq_len: 0, |
265 | 6 | hidden_dim, |
266 | 6 | max_seq_len, |
267 | 6 | } |
268 | 6 | } |
269 | | |
270 | | /// Store a new KV pair with transposed V storage |
271 | | /// |
272 | | /// Does nothing if the cache is at maximum capacity. |
273 | 10 | pub fn store(&mut self, layer: usize, k: &[f32], v: &[f32]) { |
274 | 10 | if self.seq_len >= self.max_seq_len { |
275 | 1 | return; |
276 | 9 | } |
277 | | |
278 | | // Store K in normal format |
279 | 9 | let k_start = self.seq_len * self.hidden_dim; |
280 | 9 | let k_end = k_start + self.hidden_dim; |
281 | 9 | self.k_cache[layer][k_start..k_end].copy_from_slice(k); |
282 | | |
283 | | // Store V transposed: v[i] goes to v_cache[i * max_seq_len + seq_len] |
284 | 26 | for (i, &val) in v9 .iter9 ().enumerate9 () { |
285 | 26 | self.v_cache[layer][i * self.max_seq_len + self.seq_len] = val; |
286 | 26 | } |
287 | 10 | } |
288 | | |
289 | | /// Advance the sequence position |
290 | 10 | pub fn advance(&mut self) { |
291 | 10 | if self.seq_len < self.max_seq_len { |
292 | 9 | self.seq_len += 1; |
293 | 9 | }1 |
294 | 10 | } |
295 | | |
296 | | /// Get cached keys for a layer |
297 | | #[must_use] |
298 | 2 | pub fn get_k(&self, layer: usize) -> &[f32] { |
299 | 2 | &self.k_cache[layer][..self.seq_len * self.hidden_dim] |
300 | 2 | } |
301 | | |
302 | | /// Get cached values (transposed) for a layer |
303 | | #[must_use] |
304 | 3 | pub fn get_v_transposed(&self, layer: usize) -> &[f32] { |
305 | 3 | &self.v_cache[layer] |
306 | 3 | } |
307 | | |
308 | | /// Get current sequence length |
309 | | #[must_use] |
310 | 4 | pub fn len(&self) -> usize { |
311 | 4 | self.seq_len |
312 | 4 | } |
313 | | |
314 | | /// Check if cache is empty |
315 | | #[must_use] |
316 | 2 | pub fn is_empty(&self) -> bool { |
317 | 2 | self.seq_len == 0 |
318 | 2 | } |
319 | | |
320 | | /// Reset the cache |
321 | 1 | pub fn reset(&mut self) { |
322 | 1 | self.seq_len = 0; |
323 | 1 | } |
324 | | |
325 | | /// Get maximum sequence length |
326 | | #[must_use] |
327 | 1 | pub fn max_len(&self) -> usize { |
328 | 1 | self.max_seq_len |
329 | 1 | } |
330 | | } |
331 | | |
332 | | /// Attention with transposed V cache for better memory access |
333 | | /// |
334 | | /// Uses transposed V storage for improved cache locality during |
335 | | /// the weighted sum computation. |
336 | | #[must_use] |
337 | 3 | pub fn attention_with_transposed_v( |
338 | 3 | q: &[f32], |
339 | 3 | k_cache: &[f32], |
340 | 3 | v_cache_transposed: &[f32], |
341 | 3 | current_k: &[f32], |
342 | 3 | current_v: &[f32], |
343 | 3 | num_heads: usize, |
344 | 3 | max_seq_len: usize, |
345 | 3 | ) -> Vec<f32> { |
346 | 3 | let hidden_dim = q.len(); |
347 | 3 | let head_dim = hidden_dim / num_heads; |
348 | 3 | let cache_len = if hidden_dim > 0 { |
349 | 3 | k_cache.len() / hidden_dim |
350 | | } else { |
351 | 0 | 0 |
352 | | }; |
353 | 3 | let total_len = cache_len + 1; |
354 | 3 | let scale = 1.0 / (head_dim as f32).sqrt(); |
355 | | |
356 | 3 | let mut output = vec![0.0; hidden_dim]; |
357 | | |
358 | 6 | for h in 0..num_heads3 { |
359 | 6 | let head_offset = h * head_dim; |
360 | 6 | let q_head = &q[head_offset..head_offset + head_dim]; |
361 | | |
362 | | // Compute attention scores |
363 | 6 | let mut scores = Vec::with_capacity(total_len); |
364 | | |
365 | 6 | for pos in 0..cache_len { |
366 | 6 | let k_start = pos * hidden_dim + head_offset; |
367 | 6 | let k_head = &k_cache[k_start..k_start + head_dim]; |
368 | 6 | scores.push(simd_dot(q_head, k_head) * scale); |
369 | 6 | } |
370 | | |
371 | 6 | let current_k_head = ¤t_k[head_offset..head_offset + head_dim]; |
372 | 6 | scores.push(simd_dot(q_head, current_k_head) * scale); |
373 | | |
374 | 6 | simd_softmax(&mut scores); |
375 | | |
376 | | // Weighted sum with transposed V (better cache locality) |
377 | 6 | let out_head = &mut output[head_offset..head_offset + head_dim]; |
378 | | |
379 | 12 | for i in 0..head_dim6 { |
380 | 12 | let v_idx = (head_offset + i) * max_seq_len; |
381 | 12 | let mut sum = 0.0; |
382 | 12 | for (pos, &weight) in scores.iter().enumerate().take(cache_len) { |
383 | 12 | sum += weight * v_cache_transposed[v_idx + pos]; |
384 | 12 | } |
385 | 12 | sum += scores[cache_len] * current_v[head_offset + i]; |
386 | 12 | out_head[i] = sum; |
387 | | } |
388 | | } |
389 | | |
390 | 3 | output |
391 | 3 | } |
392 | | |
393 | | // ============================================================================ |
394 | | // EXTREME TDD: Comprehensive Tests |
395 | | // ============================================================================ |
396 | | |
397 | | #[cfg(test)] |
398 | | mod tests { |
399 | | use super::*; |
400 | | |
401 | | // ------------------------------------------------------------------------ |
402 | | // KVCache Tests |
403 | | // ------------------------------------------------------------------------ |
404 | | |
405 | | #[test] |
406 | 1 | fn test_kv_cache_new() { |
407 | 1 | let cache = KVCache::new(12, 768, 2048); |
408 | 1 | assert!(cache.is_empty()); |
409 | 1 | assert_eq!(cache.len(), 0); |
410 | 1 | } |
411 | | |
412 | | #[test] |
413 | 1 | fn test_kv_cache_store_and_retrieve() { |
414 | 1 | let mut cache = KVCache::new(2, 4, 10); |
415 | | |
416 | 1 | let k = vec![1.0, 2.0, 3.0, 4.0]; |
417 | 1 | let v = vec![5.0, 6.0, 7.0, 8.0]; |
418 | | |
419 | 1 | cache.store(0, &k, &v); |
420 | 1 | cache.advance(); |
421 | | |
422 | 1 | assert_eq!(cache.len(), 1); |
423 | 1 | assert_eq!(cache.get_k(0), &[1.0, 2.0, 3.0, 4.0]); |
424 | 1 | assert_eq!(cache.get_v(0), &[5.0, 6.0, 7.0, 8.0]); |
425 | 1 | } |
426 | | |
427 | | #[test] |
428 | 1 | fn test_kv_cache_multiple_positions() { |
429 | 1 | let mut cache = KVCache::new(1, 2, 10); |
430 | | |
431 | | // Store position 0 |
432 | 1 | cache.store(0, &[1.0, 2.0], &[3.0, 4.0]); |
433 | 1 | cache.advance(); |
434 | | |
435 | | // Store position 1 |
436 | 1 | cache.store(0, &[5.0, 6.0], &[7.0, 8.0]); |
437 | 1 | cache.advance(); |
438 | | |
439 | 1 | assert_eq!(cache.len(), 2); |
440 | 1 | assert_eq!(cache.get_k(0), &[1.0, 2.0, 5.0, 6.0]); |
441 | 1 | assert_eq!(cache.get_v(0), &[3.0, 4.0, 7.0, 8.0]); |
442 | 1 | } |
443 | | |
444 | | #[test] |
445 | 1 | fn test_kv_cache_multiple_layers() { |
446 | 1 | let mut cache = KVCache::new(2, 2, 10); |
447 | | |
448 | 1 | cache.store(0, &[1.0, 2.0], &[3.0, 4.0]); |
449 | 1 | cache.store(1, &[5.0, 6.0], &[7.0, 8.0]); |
450 | 1 | cache.advance(); |
451 | | |
452 | 1 | assert_eq!(cache.get_k(0), &[1.0, 2.0]); |
453 | 1 | assert_eq!(cache.get_k(1), &[5.0, 6.0]); |
454 | 1 | assert_eq!(cache.get_v(0), &[3.0, 4.0]); |
455 | 1 | assert_eq!(cache.get_v(1), &[7.0, 8.0]); |
456 | 1 | } |
457 | | |
458 | | #[test] |
459 | 1 | fn test_kv_cache_reset() { |
460 | 1 | let mut cache = KVCache::new(1, 4, 10); |
461 | | |
462 | 1 | cache.store(0, &[1.0; 4], &[2.0; 4]); |
463 | 1 | cache.advance(); |
464 | 1 | assert_eq!(cache.len(), 1); |
465 | | |
466 | 1 | cache.reset(); |
467 | 1 | assert!(cache.is_empty()); |
468 | 1 | assert_eq!(cache.len(), 0); |
469 | 1 | } |
470 | | |
471 | | #[test] |
472 | 1 | fn test_kv_cache_is_empty() { |
473 | 1 | let mut cache = KVCache::new(1, 4, 10); |
474 | 1 | assert!(cache.is_empty()); |
475 | | |
476 | 1 | cache.store(0, &[1.0; 4], &[1.0; 4]); |
477 | 1 | cache.advance(); |
478 | 1 | assert!(!cache.is_empty()); |
479 | 1 | } |
480 | | |
481 | | #[test] |
482 | 1 | fn test_kv_cache_clone() { |
483 | 1 | let mut cache = KVCache::new(1, 2, 10); |
484 | 1 | cache.store(0, &[1.0, 2.0], &[3.0, 4.0]); |
485 | 1 | cache.advance(); |
486 | | |
487 | 1 | let cloned = cache.clone(); |
488 | 1 | assert_eq!(cloned.len(), 1); |
489 | 1 | assert_eq!(cloned.get_k(0), &[1.0, 2.0]); |
490 | 1 | } |
491 | | |
492 | | // ------------------------------------------------------------------------ |
493 | | // OptimizedKVCache Tests |
494 | | // ------------------------------------------------------------------------ |
495 | | |
496 | | #[test] |
497 | 1 | fn test_optimized_cache_new() { |
498 | 1 | let cache = OptimizedKVCache::new(12, 768, 2048); |
499 | 1 | assert!(cache.is_empty()); |
500 | 1 | assert_eq!(cache.len(), 0); |
501 | 1 | assert_eq!(cache.max_len(), 2048); |
502 | 1 | } |
503 | | |
504 | | #[test] |
505 | 1 | fn test_optimized_cache_store_and_retrieve() { |
506 | 1 | let mut cache = OptimizedKVCache::new(1, 4, 10); |
507 | | |
508 | 1 | let k = vec![1.0, 2.0, 3.0, 4.0]; |
509 | 1 | let v = vec![5.0, 6.0, 7.0, 8.0]; |
510 | | |
511 | 1 | cache.store(0, &k, &v); |
512 | 1 | cache.advance(); |
513 | | |
514 | 1 | assert_eq!(cache.len(), 1); |
515 | 1 | assert_eq!(cache.get_k(0), &[1.0, 2.0, 3.0, 4.0]); |
516 | | |
517 | | // V is transposed: v[i] at position 0 is at index i * max_seq_len |
518 | 1 | let v_transposed = cache.get_v_transposed(0); |
519 | 1 | assert_eq!(v_transposed[0], 5.0); // v[0] at pos 0 |
520 | 1 | assert_eq!(v_transposed[10], 6.0); // v[1] at pos 0 (stride = max_seq_len = 10) |
521 | 1 | assert_eq!(v_transposed[20], 7.0); // v[2] at pos 0 |
522 | 1 | assert_eq!(v_transposed[30], 8.0); // v[3] at pos 0 |
523 | 1 | } |
524 | | |
525 | | #[test] |
526 | 1 | fn test_optimized_cache_transposed_v_layout() { |
527 | 1 | let mut cache = OptimizedKVCache::new(1, 2, 5); |
528 | | |
529 | | // Store position 0 |
530 | 1 | cache.store(0, &[1.0, 2.0], &[10.0, 20.0]); |
531 | 1 | cache.advance(); |
532 | | |
533 | | // Store position 1 |
534 | 1 | cache.store(0, &[3.0, 4.0], &[30.0, 40.0]); |
535 | 1 | cache.advance(); |
536 | | |
537 | 1 | let v_transposed = cache.get_v_transposed(0); |
538 | | // v[0] positions: indices 0, 1, 2, ... (stride 1) |
539 | | // v[1] positions: indices 5, 6, 7, ... (stride 1, offset = hidden_dim * max_seq_len / hidden_dim = max_seq_len) |
540 | 1 | assert_eq!(v_transposed[0], 10.0); // v[0] at pos 0 |
541 | 1 | assert_eq!(v_transposed[1], 30.0); // v[0] at pos 1 |
542 | 1 | assert_eq!(v_transposed[5], 20.0); // v[1] at pos 0 |
543 | 1 | assert_eq!(v_transposed[6], 40.0); // v[1] at pos 1 |
544 | 1 | } |
545 | | |
546 | | #[test] |
547 | 1 | fn test_optimized_cache_max_capacity() { |
548 | 1 | let mut cache = OptimizedKVCache::new(1, 2, 3); |
549 | | |
550 | | // Fill to capacity |
551 | 4 | for i3 in 0..3 { |
552 | 3 | cache.store(0, &[i as f32; 2], &[i as f32; 2]); |
553 | 3 | cache.advance(); |
554 | 3 | } |
555 | 1 | assert_eq!(cache.len(), 3); |
556 | | |
557 | | // Should not advance beyond max |
558 | 1 | cache.store(0, &[99.0; 2], &[99.0; 2]); |
559 | 1 | cache.advance(); |
560 | 1 | assert_eq!(cache.len(), 3); // Still at max |
561 | 1 | } |
562 | | |
563 | | #[test] |
564 | 1 | fn test_optimized_cache_reset() { |
565 | 1 | let mut cache = OptimizedKVCache::new(1, 4, 10); |
566 | | |
567 | 1 | cache.store(0, &[1.0; 4], &[2.0; 4]); |
568 | 1 | cache.advance(); |
569 | 1 | cache.reset(); |
570 | | |
571 | 1 | assert!(cache.is_empty()); |
572 | 1 | } |
573 | | |
574 | | // ------------------------------------------------------------------------ |
575 | | // attention_with_cache Tests |
576 | | // ------------------------------------------------------------------------ |
577 | | |
578 | | #[test] |
579 | 1 | fn test_attention_with_cache_no_history() { |
580 | 1 | let hidden_dim = 4; |
581 | 1 | let num_heads = 2; |
582 | | |
583 | 1 | let q = vec![1.0; hidden_dim]; |
584 | 1 | let k_cache: Vec<f32> = vec![]; |
585 | 1 | let v_cache: Vec<f32> = vec![]; |
586 | 1 | let current_k = vec![1.0; hidden_dim]; |
587 | 1 | let current_v = vec![2.0; hidden_dim]; |
588 | | |
589 | 1 | let output = |
590 | 1 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
591 | | |
592 | 1 | assert_eq!(output.len(), hidden_dim); |
593 | | // With no history and uniform attention, output should equal current_v |
594 | 5 | for &v4 in &output { |
595 | 4 | assert!((v - 2.0).abs() < 1e-5); |
596 | | } |
597 | 1 | } |
598 | | |
599 | | #[test] |
600 | 1 | fn test_attention_with_cache_one_cached() { |
601 | 1 | let hidden_dim = 4; |
602 | 1 | let num_heads = 2; |
603 | | |
604 | 1 | let q = vec![1.0; hidden_dim]; |
605 | | // One cached position |
606 | 1 | let k_cache = vec![1.0; hidden_dim]; |
607 | 1 | let v_cache = vec![1.0; hidden_dim]; |
608 | 1 | let current_k = vec![1.0; hidden_dim]; |
609 | 1 | let current_v = vec![3.0; hidden_dim]; |
610 | | |
611 | 1 | let output = |
612 | 1 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
613 | | |
614 | 1 | assert_eq!(output.len(), hidden_dim); |
615 | | // With uniform K, attention is 0.5 to each position |
616 | | // output = 0.5 * 1.0 + 0.5 * 3.0 = 2.0 |
617 | 5 | for &v4 in &output { |
618 | 4 | assert!((v - 2.0).abs() < 1e-5); |
619 | | } |
620 | 1 | } |
621 | | |
622 | | #[test] |
623 | 1 | fn test_attention_with_cache_multi_head() { |
624 | 1 | let hidden_dim = 8; |
625 | 1 | let num_heads = 4; |
626 | 1 | let head_dim = hidden_dim / num_heads; |
627 | | |
628 | | // Each head has different Q |
629 | 1 | let mut q = vec![0.0; hidden_dim]; |
630 | 4 | for h in 0..num_heads1 { |
631 | 8 | for i in 0..head_dim4 { |
632 | 8 | q[h * head_dim + i] = (h + 1) as f32; |
633 | 8 | } |
634 | | } |
635 | | |
636 | 1 | let k_cache: Vec<f32> = vec![]; |
637 | 1 | let v_cache: Vec<f32> = vec![]; |
638 | 1 | let current_k = vec![1.0; hidden_dim]; |
639 | 1 | let current_v = vec![1.0; hidden_dim]; |
640 | | |
641 | 1 | let output = |
642 | 1 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
643 | | |
644 | 1 | assert_eq!(output.len(), hidden_dim); |
645 | | // All outputs should be 1.0 (current_v with softmax weight 1.0) |
646 | 9 | for &v8 in &output { |
647 | 8 | assert!((v - 1.0).abs() < 1e-5); |
648 | | } |
649 | 1 | } |
650 | | |
651 | | #[test] |
652 | 1 | fn test_attention_preserves_dimension() { |
653 | 5 | for hidden_dim4 in [64, 128, 256, 512] { |
654 | 20 | for num_heads16 in [1, 2, 4, 8] { |
655 | 16 | if hidden_dim % num_heads != 0 { |
656 | 0 | continue; |
657 | 16 | } |
658 | | |
659 | 16 | let q = vec![0.1; hidden_dim]; |
660 | 16 | let k_cache = vec![0.1; hidden_dim * 2]; |
661 | 16 | let v_cache = vec![0.2; hidden_dim * 2]; |
662 | 16 | let current_k = vec![0.1; hidden_dim]; |
663 | 16 | let current_v = vec![0.3; hidden_dim]; |
664 | | |
665 | 16 | let output = |
666 | 16 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
667 | | |
668 | 16 | assert_eq!(output.len(), hidden_dim); |
669 | | } |
670 | | } |
671 | 1 | } |
672 | | |
673 | | // ------------------------------------------------------------------------ |
674 | | // attention_with_transposed_v Tests |
675 | | // ------------------------------------------------------------------------ |
676 | | |
677 | | #[test] |
678 | 1 | fn test_transposed_attention_no_history() { |
679 | 1 | let hidden_dim = 4; |
680 | 1 | let num_heads = 2; |
681 | 1 | let max_seq_len = 10; |
682 | | |
683 | 1 | let q = vec![1.0; hidden_dim]; |
684 | 1 | let k_cache: Vec<f32> = vec![]; |
685 | 1 | let v_cache_transposed = vec![0.0; hidden_dim * max_seq_len]; |
686 | 1 | let current_k = vec![1.0; hidden_dim]; |
687 | 1 | let current_v = vec![2.0; hidden_dim]; |
688 | | |
689 | 1 | let output = attention_with_transposed_v( |
690 | 1 | &q, |
691 | 1 | &k_cache, |
692 | 1 | &v_cache_transposed, |
693 | 1 | ¤t_k, |
694 | 1 | ¤t_v, |
695 | 1 | num_heads, |
696 | 1 | max_seq_len, |
697 | | ); |
698 | | |
699 | 1 | assert_eq!(output.len(), hidden_dim); |
700 | | // Output should equal current_v when no history |
701 | 5 | for &v4 in &output { |
702 | 4 | assert!((v - 2.0).abs() < 1e-5); |
703 | | } |
704 | 1 | } |
705 | | |
706 | | #[test] |
707 | 1 | fn test_transposed_attention_one_cached() { |
708 | 1 | let hidden_dim = 4; |
709 | 1 | let num_heads = 2; |
710 | 1 | let max_seq_len = 10; |
711 | | |
712 | 1 | let q = vec![1.0; hidden_dim]; |
713 | 1 | let k_cache = vec![1.0; hidden_dim]; // 1 cached position |
714 | | |
715 | | // Transposed V: v[i] at pos j is at index i * max_seq_len + j |
716 | 1 | let mut v_cache_transposed = vec![0.0; hidden_dim * max_seq_len]; |
717 | 4 | for i in 0..hidden_dim1 { |
718 | 4 | v_cache_transposed[i * max_seq_len] = 1.0; // v[i] = 1.0 at pos 0 |
719 | 4 | } |
720 | | |
721 | 1 | let current_k = vec![1.0; hidden_dim]; |
722 | 1 | let current_v = vec![3.0; hidden_dim]; |
723 | | |
724 | 1 | let output = attention_with_transposed_v( |
725 | 1 | &q, |
726 | 1 | &k_cache, |
727 | 1 | &v_cache_transposed, |
728 | 1 | ¤t_k, |
729 | 1 | ¤t_v, |
730 | 1 | num_heads, |
731 | 1 | max_seq_len, |
732 | | ); |
733 | | |
734 | | // Uniform attention: 0.5 * 1.0 + 0.5 * 3.0 = 2.0 |
735 | 5 | for &v4 in &output { |
736 | 4 | assert!((v - 2.0).abs() < 1e-5); |
737 | | } |
738 | 1 | } |
739 | | |
740 | | // ------------------------------------------------------------------------ |
741 | | // Integration Tests |
742 | | // ------------------------------------------------------------------------ |
743 | | |
744 | | #[test] |
745 | 1 | fn test_cache_and_attention_integration() { |
746 | 1 | let num_layers = 2; |
747 | 1 | let hidden_dim = 4; |
748 | 1 | let max_seq_len = 10; |
749 | 1 | let num_heads = 2; |
750 | | |
751 | 1 | let mut cache = KVCache::new(num_layers, hidden_dim, max_seq_len); |
752 | | |
753 | | // Simulate 3 tokens |
754 | 4 | for pos3 in 0..3 { |
755 | 3 | let k = vec![pos as f32; hidden_dim]; |
756 | 3 | let v = vec![(pos * 2) as f32; hidden_dim]; |
757 | | |
758 | 6 | for layer in 0..num_layers3 { |
759 | 6 | cache.store(layer, &k, &v); |
760 | 6 | } |
761 | 3 | cache.advance(); |
762 | | } |
763 | | |
764 | | // Now compute attention for token 4 |
765 | 1 | let q = vec![1.0; hidden_dim]; |
766 | 1 | let current_k = vec![3.0; hidden_dim]; |
767 | 1 | let current_v = vec![6.0; hidden_dim]; |
768 | | |
769 | 1 | let k_cached = cache.get_k(0); |
770 | 1 | let v_cached = cache.get_v(0); |
771 | | |
772 | 1 | let output = |
773 | 1 | attention_with_cache(&q, k_cached, v_cached, ¤t_k, ¤t_v, num_heads); |
774 | | |
775 | 1 | assert_eq!(output.len(), hidden_dim); |
776 | | // Output should be some weighted combination |
777 | 4 | assert!1 (output.iter()1 .all1 (|&x| x.is_finite())); |
778 | 1 | } |
779 | | |
780 | | #[test] |
781 | 1 | fn test_optimized_cache_and_attention_integration() { |
782 | 1 | let num_layers = 1; |
783 | 1 | let hidden_dim = 4; |
784 | 1 | let max_seq_len = 10; |
785 | 1 | let num_heads = 2; |
786 | | |
787 | 1 | let mut cache = OptimizedKVCache::new(num_layers, hidden_dim, max_seq_len); |
788 | | |
789 | | // Store 2 positions |
790 | 1 | cache.store(0, &[1.0; 4], &[2.0; 4]); |
791 | 1 | cache.advance(); |
792 | 1 | cache.store(0, &[1.0; 4], &[4.0; 4]); |
793 | 1 | cache.advance(); |
794 | | |
795 | 1 | let q = vec![1.0; hidden_dim]; |
796 | 1 | let current_k = vec![1.0; hidden_dim]; |
797 | 1 | let current_v = vec![6.0; hidden_dim]; |
798 | | |
799 | 1 | let output = attention_with_transposed_v( |
800 | 1 | &q, |
801 | 1 | cache.get_k(0), |
802 | 1 | cache.get_v_transposed(0), |
803 | 1 | ¤t_k, |
804 | 1 | ¤t_v, |
805 | 1 | num_heads, |
806 | 1 | max_seq_len, |
807 | | ); |
808 | | |
809 | 1 | assert_eq!(output.len(), hidden_dim); |
810 | | // Uniform attention: (2 + 4 + 6) / 3 = 4.0 |
811 | 5 | for &v4 in &output { |
812 | 4 | assert!((v - 4.0).abs() < 1e-5); |
813 | | } |
814 | 1 | } |
815 | | |
816 | | // ------------------------------------------------------------------------ |
817 | | // Edge Cases |
818 | | // ------------------------------------------------------------------------ |
819 | | |
820 | | #[test] |
821 | 1 | fn test_single_head_attention() { |
822 | 1 | let hidden_dim = 4; |
823 | 1 | let num_heads = 1; |
824 | | |
825 | 1 | let q = vec![1.0; hidden_dim]; |
826 | 1 | let k_cache: Vec<f32> = vec![]; |
827 | 1 | let v_cache: Vec<f32> = vec![]; |
828 | 1 | let current_k = vec![1.0; hidden_dim]; |
829 | 1 | let current_v = vec![5.0; hidden_dim]; |
830 | | |
831 | 1 | let output = |
832 | 1 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
833 | | |
834 | 5 | for &v4 in &output { |
835 | 4 | assert!((v - 5.0).abs() < 1e-5); |
836 | | } |
837 | 1 | } |
838 | | |
839 | | #[test] |
840 | 1 | fn test_many_heads_attention() { |
841 | 1 | let hidden_dim = 64; |
842 | 1 | let num_heads = 16; |
843 | | |
844 | 1 | let q = vec![0.1; hidden_dim]; |
845 | 1 | let k_cache: Vec<f32> = vec![]; |
846 | 1 | let v_cache: Vec<f32> = vec![]; |
847 | 1 | let current_k = vec![0.1; hidden_dim]; |
848 | 1 | let current_v = vec![1.0; hidden_dim]; |
849 | | |
850 | 1 | let output = |
851 | 1 | attention_with_cache(&q, &k_cache, &v_cache, ¤t_k, ¤t_v, num_heads); |
852 | | |
853 | 1 | assert_eq!(output.len(), hidden_dim); |
854 | 65 | for &v64 in &output { |
855 | 64 | assert!((v - 1.0).abs() < 1e-5); |
856 | | } |
857 | 1 | } |
858 | | } |