Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}