/home/noah/src/trueno/src/brick/kv_cache.rs
Line | Count | Source |
1 | | //! KV Cache Management and Batch Ordering |
2 | | //! |
3 | | //! LCP-10: KV Cache Slot tracking for transformer inference. |
4 | | //! LCP-14: Sequential Batch Ordering for cache-friendly processing. |
5 | | |
6 | | // ---------------------------------------------------------------------------- |
7 | | // LCP-10: KV Cache Slot Info |
8 | | // ---------------------------------------------------------------------------- |
9 | | |
10 | | /// Metadata for a KV cache slot in transformer inference. |
11 | | /// |
12 | | /// Tracks position, token info, and usage for cache management. |
13 | | #[derive(Debug, Clone, Default)] |
14 | | pub struct KvCacheSlotInfo { |
15 | | /// Sequence position this slot represents |
16 | | pub position: u32, |
17 | | /// Token ID stored in this slot |
18 | | pub token_id: u32, |
19 | | /// Layer index |
20 | | pub layer: u16, |
21 | | /// Head index |
22 | | pub head: u16, |
23 | | /// Whether this slot is valid/filled |
24 | | pub valid: bool, |
25 | | /// Last access time (in steps) |
26 | | pub last_access: u64, |
27 | | } |
28 | | |
29 | | impl KvCacheSlotInfo { |
30 | | /// Create a new slot info. |
31 | 0 | pub fn new(position: u32, token_id: u32, layer: u16, head: u16) -> Self { |
32 | 0 | Self { |
33 | 0 | position, |
34 | 0 | token_id, |
35 | 0 | layer, |
36 | 0 | head, |
37 | 0 | valid: true, |
38 | 0 | last_access: 0, |
39 | 0 | } |
40 | 0 | } |
41 | | |
42 | | /// Mark slot as accessed. |
43 | 0 | pub fn touch(&mut self, step: u64) { |
44 | 0 | self.last_access = step; |
45 | 0 | } |
46 | | |
47 | | /// Invalidate the slot. |
48 | 0 | pub fn invalidate(&mut self) { |
49 | 0 | self.valid = false; |
50 | 0 | } |
51 | | |
52 | | /// Check if slot can be evicted (LRU policy). |
53 | | #[must_use] |
54 | 0 | pub fn eviction_priority(&self, current_step: u64) -> u64 { |
55 | 0 | if !self.valid { |
56 | 0 | return u64::MAX; // Invalid slots have highest eviction priority |
57 | 0 | } |
58 | 0 | current_step.saturating_sub(self.last_access) |
59 | 0 | } |
60 | | } |
61 | | |
62 | | /// KV cache manager with slot tracking. |
63 | | #[derive(Debug)] |
64 | | pub struct KvCacheManager { |
65 | | /// Slot metadata |
66 | | slots: Vec<KvCacheSlotInfo>, |
67 | | /// Current step counter |
68 | | current_step: u64, |
69 | | /// Number of valid slots |
70 | | valid_count: usize, |
71 | | } |
72 | | |
73 | | impl KvCacheManager { |
74 | | /// Create manager with given capacity. |
75 | 0 | pub fn new(capacity: usize) -> Self { |
76 | 0 | Self { |
77 | 0 | slots: vec![KvCacheSlotInfo::default(); capacity], |
78 | 0 | current_step: 0, |
79 | 0 | valid_count: 0, |
80 | 0 | } |
81 | 0 | } |
82 | | |
83 | | /// Allocate a slot. |
84 | 0 | pub fn allocate( |
85 | 0 | &mut self, |
86 | 0 | position: u32, |
87 | 0 | token_id: u32, |
88 | 0 | layer: u16, |
89 | 0 | head: u16, |
90 | 0 | ) -> Option<usize> { |
91 | | // Find first invalid slot |
92 | 0 | for (i, slot) in self.slots.iter_mut().enumerate() { |
93 | 0 | if !slot.valid { |
94 | 0 | *slot = KvCacheSlotInfo::new(position, token_id, layer, head); |
95 | 0 | slot.touch(self.current_step); |
96 | 0 | self.valid_count += 1; |
97 | 0 | return Some(i); |
98 | 0 | } |
99 | | } |
100 | 0 | None // No free slots |
101 | 0 | } |
102 | | |
103 | | /// Access a slot. |
104 | 0 | pub fn access(&mut self, index: usize) -> Option<&KvCacheSlotInfo> { |
105 | 0 | if index < self.slots.len() { |
106 | 0 | self.slots[index].touch(self.current_step); |
107 | 0 | Some(&self.slots[index]) |
108 | | } else { |
109 | 0 | None |
110 | | } |
111 | 0 | } |
112 | | |
113 | | /// Evict LRU slot. |
114 | 0 | pub fn evict_lru(&mut self) -> Option<usize> { |
115 | 0 | let mut best_idx = None; |
116 | 0 | let mut best_priority = 0u64; |
117 | | |
118 | 0 | for (i, slot) in self.slots.iter().enumerate() { |
119 | 0 | if slot.valid { |
120 | 0 | let priority = slot.eviction_priority(self.current_step); |
121 | | // Use >= for first found slot (best_idx.is_none()), then > for ties |
122 | 0 | if best_idx.is_none() || priority > best_priority { |
123 | 0 | best_priority = priority; |
124 | 0 | best_idx = Some(i); |
125 | 0 | } |
126 | 0 | } |
127 | | } |
128 | | |
129 | 0 | if let Some(idx) = best_idx { |
130 | 0 | self.slots[idx].invalidate(); |
131 | 0 | self.valid_count -= 1; |
132 | 0 | } |
133 | 0 | best_idx |
134 | 0 | } |
135 | | |
136 | | /// Advance step counter. |
137 | 0 | pub fn step(&mut self) { |
138 | 0 | self.current_step += 1; |
139 | 0 | } |
140 | | |
141 | | /// Get number of valid slots. |
142 | | #[must_use] |
143 | 0 | pub fn valid_count(&self) -> usize { |
144 | 0 | self.valid_count |
145 | 0 | } |
146 | | |
147 | | /// Get capacity. |
148 | | #[must_use] |
149 | 0 | pub fn capacity(&self) -> usize { |
150 | 0 | self.slots.len() |
151 | 0 | } |
152 | | } |
153 | | |
154 | | // ---------------------------------------------------------------------------- |
155 | | // LCP-14: Sequential Batch Ordering |
156 | | // ---------------------------------------------------------------------------- |
157 | | |
158 | | /// Sequential batch orderer for cache-friendly processing. |
159 | | /// |
160 | | /// Ensures batches are processed in optimal order for memory access patterns. |
161 | | #[derive(Debug, Clone)] |
162 | | pub struct SequentialBatchOrderer { |
163 | | /// Batch indices in processing order |
164 | | order: Vec<usize>, |
165 | | /// Current position in order |
166 | | position: usize, |
167 | | } |
168 | | |
169 | | impl SequentialBatchOrderer { |
170 | | /// Create orderer for n batches. |
171 | 0 | pub fn new(n_batches: usize) -> Self { |
172 | 0 | Self { |
173 | 0 | order: (0..n_batches).collect(), |
174 | 0 | position: 0, |
175 | 0 | } |
176 | 0 | } |
177 | | |
178 | | /// Create orderer with reverse order (sometimes better for certain patterns). |
179 | 0 | pub fn reversed(n_batches: usize) -> Self { |
180 | 0 | Self { |
181 | 0 | order: (0..n_batches).rev().collect(), |
182 | 0 | position: 0, |
183 | 0 | } |
184 | 0 | } |
185 | | |
186 | | /// Create orderer with interleaved order (for better cache utilization). |
187 | 0 | pub fn interleaved(n_batches: usize) -> Self { |
188 | 0 | let mut order = Vec::with_capacity(n_batches); |
189 | 0 | let mid = n_batches / 2; |
190 | | |
191 | | // Interleave: 0, mid, 1, mid+1, 2, mid+2, ... |
192 | 0 | for i in 0..mid { |
193 | 0 | order.push(i); |
194 | 0 | if mid + i < n_batches { |
195 | 0 | order.push(mid + i); |
196 | 0 | } |
197 | | } |
198 | | // Handle odd number of batches |
199 | 0 | if !n_batches.is_multiple_of(2) { |
200 | 0 | order.push(n_batches - 1); |
201 | 0 | } |
202 | | |
203 | 0 | Self { order, position: 0 } |
204 | 0 | } |
205 | | |
206 | | /// Get next batch index. |
207 | 0 | pub fn next_batch(&mut self) -> Option<usize> { |
208 | 0 | if self.position < self.order.len() { |
209 | 0 | let idx = self.order[self.position]; |
210 | 0 | self.position += 1; |
211 | 0 | Some(idx) |
212 | | } else { |
213 | 0 | None |
214 | | } |
215 | 0 | } |
216 | | |
217 | | /// Reset to beginning. |
218 | 0 | pub fn reset(&mut self) { |
219 | 0 | self.position = 0; |
220 | 0 | } |
221 | | |
222 | | /// Check if all batches have been processed. |
223 | | #[must_use] |
224 | 0 | pub fn is_done(&self) -> bool { |
225 | 0 | self.position >= self.order.len() |
226 | 0 | } |
227 | | |
228 | | /// Get remaining count. |
229 | | #[must_use] |
230 | 0 | pub fn remaining(&self) -> usize { |
231 | 0 | self.order.len().saturating_sub(self.position) |
232 | 0 | } |
233 | | } |
234 | | |
235 | | impl Iterator for SequentialBatchOrderer { |
236 | | type Item = usize; |
237 | | |
238 | 0 | fn next(&mut self) -> Option<Self::Item> { |
239 | 0 | self.next_batch() |
240 | 0 | } |
241 | | } |
242 | | |
243 | | #[cfg(test)] |
244 | | mod tests { |
245 | | use super::*; |
246 | | |
247 | | // ========================================================================= |
248 | | // KvCacheSlotInfo Tests |
249 | | // ========================================================================= |
250 | | |
251 | | #[test] |
252 | | fn test_kv_cache_slot_info_new() { |
253 | | let slot = KvCacheSlotInfo::new(5, 42, 2, 8); |
254 | | |
255 | | assert_eq!(slot.position, 5); |
256 | | assert_eq!(slot.token_id, 42); |
257 | | assert_eq!(slot.layer, 2); |
258 | | assert_eq!(slot.head, 8); |
259 | | assert!(slot.valid); |
260 | | assert_eq!(slot.last_access, 0); |
261 | | } |
262 | | |
263 | | #[test] |
264 | | fn test_kv_cache_slot_info_default() { |
265 | | let slot = KvCacheSlotInfo::default(); |
266 | | |
267 | | assert_eq!(slot.position, 0); |
268 | | assert_eq!(slot.token_id, 0); |
269 | | assert_eq!(slot.layer, 0); |
270 | | assert_eq!(slot.head, 0); |
271 | | assert!(!slot.valid); |
272 | | assert_eq!(slot.last_access, 0); |
273 | | } |
274 | | |
275 | | #[test] |
276 | | fn test_kv_cache_slot_info_touch() { |
277 | | let mut slot = KvCacheSlotInfo::new(0, 0, 0, 0); |
278 | | |
279 | | slot.touch(10); |
280 | | assert_eq!(slot.last_access, 10); |
281 | | |
282 | | slot.touch(50); |
283 | | assert_eq!(slot.last_access, 50); |
284 | | } |
285 | | |
286 | | #[test] |
287 | | fn test_kv_cache_slot_info_invalidate() { |
288 | | let mut slot = KvCacheSlotInfo::new(0, 0, 0, 0); |
289 | | assert!(slot.valid); |
290 | | |
291 | | slot.invalidate(); |
292 | | assert!(!slot.valid); |
293 | | } |
294 | | |
295 | | #[test] |
296 | | fn test_kv_cache_slot_info_eviction_priority_valid() { |
297 | | let mut slot = KvCacheSlotInfo::new(0, 0, 0, 0); |
298 | | slot.touch(10); |
299 | | |
300 | | // Same step as last access = priority 0 |
301 | | assert_eq!(slot.eviction_priority(10), 0); |
302 | | |
303 | | // Steps since last access |
304 | | assert_eq!(slot.eviction_priority(20), 10); |
305 | | assert_eq!(slot.eviction_priority(100), 90); |
306 | | } |
307 | | |
308 | | #[test] |
309 | | fn test_kv_cache_slot_info_eviction_priority_invalid() { |
310 | | let mut slot = KvCacheSlotInfo::new(0, 0, 0, 0); |
311 | | slot.invalidate(); |
312 | | |
313 | | // Invalid slots have MAX priority (should be evicted first) |
314 | | assert_eq!(slot.eviction_priority(0), u64::MAX); |
315 | | assert_eq!(slot.eviction_priority(100), u64::MAX); |
316 | | } |
317 | | |
318 | | // ========================================================================= |
319 | | // KvCacheManager Tests |
320 | | // ========================================================================= |
321 | | |
322 | | #[test] |
323 | | fn test_kv_cache_manager_new() { |
324 | | let mgr = KvCacheManager::new(10); |
325 | | |
326 | | assert_eq!(mgr.capacity(), 10); |
327 | | assert_eq!(mgr.valid_count(), 0); |
328 | | } |
329 | | |
330 | | #[test] |
331 | | fn test_kv_cache_manager_allocate() { |
332 | | let mut mgr = KvCacheManager::new(3); |
333 | | |
334 | | let idx0 = mgr.allocate(0, 100, 0, 0); |
335 | | assert_eq!(idx0, Some(0)); |
336 | | assert_eq!(mgr.valid_count(), 1); |
337 | | |
338 | | let idx1 = mgr.allocate(1, 101, 0, 0); |
339 | | assert_eq!(idx1, Some(1)); |
340 | | assert_eq!(mgr.valid_count(), 2); |
341 | | |
342 | | let idx2 = mgr.allocate(2, 102, 0, 0); |
343 | | assert_eq!(idx2, Some(2)); |
344 | | assert_eq!(mgr.valid_count(), 3); |
345 | | |
346 | | // Full - no more slots |
347 | | let idx3 = mgr.allocate(3, 103, 0, 0); |
348 | | assert_eq!(idx3, None); |
349 | | assert_eq!(mgr.valid_count(), 3); |
350 | | } |
351 | | |
352 | | #[test] |
353 | | fn test_kv_cache_manager_access() { |
354 | | let mut mgr = KvCacheManager::new(3); |
355 | | |
356 | | let idx = mgr.allocate(0, 42, 1, 2).unwrap(); |
357 | | |
358 | | let slot = mgr.access(idx).unwrap(); |
359 | | assert_eq!(slot.token_id, 42); |
360 | | assert_eq!(slot.layer, 1); |
361 | | assert_eq!(slot.head, 2); |
362 | | |
363 | | // Out of bounds access |
364 | | assert!(mgr.access(100).is_none()); |
365 | | } |
366 | | |
367 | | #[test] |
368 | | fn test_kv_cache_manager_step() { |
369 | | let mut mgr = KvCacheManager::new(2); |
370 | | let idx = mgr.allocate(0, 0, 0, 0).unwrap(); |
371 | | |
372 | | // Step advances counter |
373 | | mgr.step(); |
374 | | mgr.step(); |
375 | | |
376 | | // Access updates last_access to current step |
377 | | let slot = mgr.access(idx).unwrap(); |
378 | | assert_eq!(slot.last_access, 2); |
379 | | } |
380 | | |
381 | | #[test] |
382 | | fn test_kv_cache_manager_evict_lru() { |
383 | | let mut mgr = KvCacheManager::new(3); |
384 | | |
385 | | // Allocate with steps between to create LRU ordering |
386 | | let idx0 = mgr.allocate(0, 100, 0, 0).unwrap(); |
387 | | mgr.step(); |
388 | | let idx1 = mgr.allocate(1, 101, 0, 0).unwrap(); |
389 | | mgr.step(); |
390 | | let _idx2 = mgr.allocate(2, 102, 0, 0).unwrap(); |
391 | | mgr.step(); |
392 | | |
393 | | // Access idx0 to make it recently used |
394 | | mgr.access(idx0); |
395 | | |
396 | | // LRU should be idx1 (oldest access without recent touch) |
397 | | let evicted = mgr.evict_lru(); |
398 | | assert_eq!(evicted, Some(idx1)); |
399 | | assert_eq!(mgr.valid_count(), 2); |
400 | | } |
401 | | |
402 | | #[test] |
403 | | fn test_kv_cache_manager_evict_empty() { |
404 | | let mut mgr = KvCacheManager::new(3); |
405 | | |
406 | | // No valid slots - nothing to evict |
407 | | let evicted = mgr.evict_lru(); |
408 | | assert_eq!(evicted, None); |
409 | | } |
410 | | |
411 | | #[test] |
412 | | fn test_kv_cache_manager_allocate_after_evict() { |
413 | | let mut mgr = KvCacheManager::new(2); |
414 | | |
415 | | // Fill up |
416 | | mgr.allocate(0, 100, 0, 0).unwrap(); |
417 | | mgr.step(); |
418 | | mgr.allocate(1, 101, 0, 0).unwrap(); |
419 | | assert_eq!(mgr.valid_count(), 2); |
420 | | |
421 | | // Full |
422 | | assert!(mgr.allocate(2, 102, 0, 0).is_none()); |
423 | | |
424 | | // Evict one |
425 | | mgr.evict_lru(); |
426 | | assert_eq!(mgr.valid_count(), 1); |
427 | | |
428 | | // Now can allocate |
429 | | let idx = mgr.allocate(2, 102, 0, 0); |
430 | | assert!(idx.is_some()); |
431 | | assert_eq!(mgr.valid_count(), 2); |
432 | | } |
433 | | |
434 | | // ========================================================================= |
435 | | // SequentialBatchOrderer Tests |
436 | | // ========================================================================= |
437 | | |
438 | | #[test] |
439 | | fn test_sequential_batch_orderer_new() { |
440 | | let orderer = SequentialBatchOrderer::new(5); |
441 | | |
442 | | assert_eq!(orderer.remaining(), 5); |
443 | | assert!(!orderer.is_done()); |
444 | | } |
445 | | |
446 | | #[test] |
447 | | fn test_sequential_batch_orderer_sequential() { |
448 | | let mut orderer = SequentialBatchOrderer::new(4); |
449 | | |
450 | | assert_eq!(orderer.next_batch(), Some(0)); |
451 | | assert_eq!(orderer.next_batch(), Some(1)); |
452 | | assert_eq!(orderer.next_batch(), Some(2)); |
453 | | assert_eq!(orderer.next_batch(), Some(3)); |
454 | | assert_eq!(orderer.next_batch(), None); |
455 | | assert!(orderer.is_done()); |
456 | | } |
457 | | |
458 | | #[test] |
459 | | fn test_sequential_batch_orderer_reversed() { |
460 | | let mut orderer = SequentialBatchOrderer::reversed(4); |
461 | | |
462 | | assert_eq!(orderer.next_batch(), Some(3)); |
463 | | assert_eq!(orderer.next_batch(), Some(2)); |
464 | | assert_eq!(orderer.next_batch(), Some(1)); |
465 | | assert_eq!(orderer.next_batch(), Some(0)); |
466 | | assert_eq!(orderer.next_batch(), None); |
467 | | } |
468 | | |
469 | | #[test] |
470 | | fn test_sequential_batch_orderer_interleaved_even() { |
471 | | let orderer = SequentialBatchOrderer::interleaved(4); |
472 | | let order: Vec<_> = orderer.collect(); |
473 | | |
474 | | // 4 batches: 0, 2, 1, 3 |
475 | | assert_eq!(order, vec![0, 2, 1, 3]); |
476 | | } |
477 | | |
478 | | #[test] |
479 | | fn test_sequential_batch_orderer_interleaved_odd() { |
480 | | let orderer = SequentialBatchOrderer::interleaved(5); |
481 | | let order: Vec<_> = orderer.collect(); |
482 | | |
483 | | // All indices should be present |
484 | | assert_eq!(order.len(), 5); |
485 | | let mut sorted = order.clone(); |
486 | | sorted.sort(); |
487 | | assert_eq!(sorted, vec![0, 1, 2, 3, 4]); |
488 | | } |
489 | | |
490 | | #[test] |
491 | | fn test_sequential_batch_orderer_reset() { |
492 | | let mut orderer = SequentialBatchOrderer::new(3); |
493 | | |
494 | | orderer.next_batch(); |
495 | | orderer.next_batch(); |
496 | | assert_eq!(orderer.remaining(), 1); |
497 | | |
498 | | orderer.reset(); |
499 | | assert_eq!(orderer.remaining(), 3); |
500 | | assert!(!orderer.is_done()); |
501 | | } |
502 | | |
503 | | #[test] |
504 | | fn test_sequential_batch_orderer_remaining() { |
505 | | let mut orderer = SequentialBatchOrderer::new(5); |
506 | | |
507 | | assert_eq!(orderer.remaining(), 5); |
508 | | orderer.next_batch(); |
509 | | assert_eq!(orderer.remaining(), 4); |
510 | | orderer.next_batch(); |
511 | | assert_eq!(orderer.remaining(), 3); |
512 | | } |
513 | | |
514 | | #[test] |
515 | | fn test_sequential_batch_orderer_iterator() { |
516 | | let orderer = SequentialBatchOrderer::new(3); |
517 | | let batches: Vec<_> = orderer.collect(); |
518 | | |
519 | | assert_eq!(batches, vec![0, 1, 2]); |
520 | | } |
521 | | |
522 | | #[test] |
523 | | fn test_sequential_batch_orderer_empty() { |
524 | | let mut orderer = SequentialBatchOrderer::new(0); |
525 | | |
526 | | assert!(orderer.is_done()); |
527 | | assert_eq!(orderer.remaining(), 0); |
528 | | assert_eq!(orderer.next_batch(), None); |
529 | | } |
530 | | |
531 | | // ========================================================================= |
532 | | // FALSIFICATION TESTS |
533 | | // ========================================================================= |
534 | | |
535 | | /// FALSIFICATION TEST: KvCacheManager must maintain valid_count invariant |
536 | | /// |
537 | | /// valid_count must always equal the number of slots with valid=true |
538 | | #[test] |
539 | | fn test_falsify_kv_cache_valid_count_invariant() { |
540 | | let mut mgr = KvCacheManager::new(5); |
541 | | |
542 | | // Allocate some |
543 | | mgr.allocate(0, 0, 0, 0); |
544 | | mgr.allocate(1, 0, 0, 0); |
545 | | mgr.allocate(2, 0, 0, 0); |
546 | | |
547 | | assert_eq!( |
548 | | mgr.valid_count(), |
549 | | 3, |
550 | | "FALSIFICATION FAILED: valid_count should be 3 after 3 allocations" |
551 | | ); |
552 | | |
553 | | // Evict some |
554 | | mgr.evict_lru(); |
555 | | assert_eq!( |
556 | | mgr.valid_count(), |
557 | | 2, |
558 | | "FALSIFICATION FAILED: valid_count should be 2 after 1 eviction" |
559 | | ); |
560 | | |
561 | | mgr.evict_lru(); |
562 | | mgr.evict_lru(); |
563 | | assert_eq!( |
564 | | mgr.valid_count(), |
565 | | 0, |
566 | | "FALSIFICATION FAILED: valid_count should be 0 after all evictions" |
567 | | ); |
568 | | |
569 | | // Can't go negative |
570 | | mgr.evict_lru(); |
571 | | assert_eq!( |
572 | | mgr.valid_count(), |
573 | | 0, |
574 | | "FALSIFICATION FAILED: valid_count should not go negative" |
575 | | ); |
576 | | } |
577 | | |
578 | | /// FALSIFICATION TEST: LRU eviction must evict oldest-accessed slot |
579 | | /// |
580 | | /// Given slots with different access times, evict_lru must always |
581 | | /// return the slot with the oldest (smallest) last_access. |
582 | | #[test] |
583 | | fn test_falsify_lru_eviction_order() { |
584 | | let mut mgr = KvCacheManager::new(4); |
585 | | |
586 | | // Allocate 4 slots with step advances |
587 | | let idx0 = mgr.allocate(0, 0, 0, 0).unwrap(); |
588 | | mgr.step(); |
589 | | let idx1 = mgr.allocate(1, 0, 0, 0).unwrap(); |
590 | | mgr.step(); |
591 | | let idx2 = mgr.allocate(2, 0, 0, 0).unwrap(); |
592 | | mgr.step(); |
593 | | let idx3 = mgr.allocate(3, 0, 0, 0).unwrap(); |
594 | | mgr.step(); |
595 | | |
596 | | // Access slots in specific order to set LRU |
597 | | // Touch idx2, then idx0 - making idx1, idx3 the oldest |
598 | | mgr.access(idx2); |
599 | | mgr.step(); |
600 | | mgr.access(idx0); |
601 | | mgr.step(); |
602 | | |
603 | | // First eviction should be idx1 (oldest not accessed) |
604 | | let evicted1 = mgr.evict_lru().unwrap(); |
605 | | assert_eq!( |
606 | | evicted1, idx1, |
607 | | "FALSIFICATION FAILED: First eviction should be idx1 (step 1)" |
608 | | ); |
609 | | |
610 | | // Second eviction should be idx3 (next oldest) |
611 | | let evicted2 = mgr.evict_lru().unwrap(); |
612 | | assert_eq!( |
613 | | evicted2, idx3, |
614 | | "FALSIFICATION FAILED: Second eviction should be idx3 (step 3)" |
615 | | ); |
616 | | } |
617 | | |
618 | | /// FALSIFICATION TEST: SequentialBatchOrderer must cover all indices |
619 | | /// |
620 | | /// After iteration completes, all indices 0..n must have been returned |
621 | | /// exactly once. |
622 | | #[test] |
623 | | fn test_falsify_batch_orderer_covers_all_indices() { |
624 | | for n_batches in [1, 2, 3, 5, 10, 100] { |
625 | | for orderer in [ |
626 | | SequentialBatchOrderer::new(n_batches), |
627 | | SequentialBatchOrderer::reversed(n_batches), |
628 | | SequentialBatchOrderer::interleaved(n_batches), |
629 | | ] { |
630 | | let batches: Vec<_> = orderer.collect(); |
631 | | |
632 | | // Must have exactly n_batches elements |
633 | | assert_eq!( |
634 | | batches.len(), |
635 | | n_batches, |
636 | | "FALSIFICATION FAILED: orderer returned {} batches, expected {}", |
637 | | batches.len(), |
638 | | n_batches |
639 | | ); |
640 | | |
641 | | // Sort and verify all indices present |
642 | | let mut sorted = batches.clone(); |
643 | | sorted.sort(); |
644 | | let expected: Vec<_> = (0..n_batches).collect(); |
645 | | assert_eq!( |
646 | | sorted, expected, |
647 | | "FALSIFICATION FAILED: orderer did not return all indices 0..{}", |
648 | | n_batches |
649 | | ); |
650 | | } |
651 | | } |
652 | | } |
653 | | |
654 | | /// FALSIFICATION TEST: Eviction priority must be monotonic with age |
655 | | /// |
656 | | /// For valid slots, older access time = higher eviction priority. |
657 | | #[test] |
658 | | fn test_falsify_eviction_priority_monotonic() { |
659 | | let mut slot = KvCacheSlotInfo::new(0, 0, 0, 0); |
660 | | slot.touch(10); |
661 | | |
662 | | let mut prev_priority = slot.eviction_priority(10); |
663 | | for current_step in 11..=100 { |
664 | | let priority = slot.eviction_priority(current_step); |
665 | | assert!( |
666 | | priority >= prev_priority, |
667 | | "FALSIFICATION FAILED: eviction priority decreased from {} to {} at step {}", |
668 | | prev_priority, |
669 | | priority, |
670 | | current_step |
671 | | ); |
672 | | prev_priority = priority; |
673 | | } |
674 | | } |
675 | | } |