Skip to main content

trueno/brick/tracing/
kv_cache.rs

1// ============================================================================
2// E.11.6: KvCacheStateTrace (MLT-05)
3// ============================================================================
4
5/// KV cache state at a single generation step.
6#[derive(Debug, Clone, Default)]
7pub struct KvCacheStateTrace {
8    /// Generation step (0-indexed)
9    pub step: usize,
10    /// Total cache size in bytes
11    pub cache_size_bytes: usize,
12    /// Number of valid (filled) positions in cache
13    pub valid_positions: usize,
14    /// Maximum positions (context window size)
15    pub max_positions: usize,
16    /// Evictions performed this step
17    pub evictions_this_step: usize,
18    /// Cache hit rate (reused positions / total lookups)
19    pub cache_hit_rate: f32,
20    /// Oldest position still in cache
21    pub oldest_position: usize,
22    /// Memory fragmentation (0.0 = compact, 1.0 = fully scattered)
23    pub fragmentation: f32,
24    /// Positions accessed this step (for locality analysis)
25    pub accessed_positions: Vec<usize>,
26}
27
28impl KvCacheStateTrace {
29    /// Create a new trace for a step.
30    pub fn new(step: usize, max_positions: usize) -> Self {
31        Self { step, max_positions, ..Default::default() }
32    }
33
34    /// Check if context window is exhausted.
35    pub fn is_window_exhausted(&self) -> bool {
36        self.valid_positions >= self.max_positions
37    }
38
39    /// Get cache utilization ratio.
40    pub fn utilization(&self) -> f32 {
41        if self.max_positions == 0 {
42            return 0.0;
43        }
44        self.valid_positions as f32 / self.max_positions as f32
45    }
46}
47
48/// Full KV cache trace for a generation session.
49#[derive(Debug, Clone, Default)]
50pub struct KvCacheSessionTrace {
51    /// Per-step traces
52    pub steps: Vec<KvCacheStateTrace>,
53    /// Total evictions across the session
54    pub total_evictions: usize,
55    /// Average cache hit rate
56    pub avg_hit_rate: f32,
57    /// Peak memory usage in bytes
58    pub peak_memory_bytes: usize,
59}
60
61impl KvCacheSessionTrace {
62    /// Add a step trace.
63    pub fn add_step(&mut self, trace: KvCacheStateTrace) {
64        self.total_evictions += trace.evictions_this_step;
65        self.peak_memory_bytes = self.peak_memory_bytes.max(trace.cache_size_bytes);
66
67        // Update rolling average
68        let n = self.steps.len() as f32 + 1.0;
69        self.avg_hit_rate = (self.avg_hit_rate * (n - 1.0) + trace.cache_hit_rate) / n;
70
71        self.steps.push(trace);
72    }
73
74    /// Check if eviction rate is concerning (>10% of steps).
75    pub fn has_high_eviction_rate(&self) -> bool {
76        if self.steps.is_empty() {
77            return false;
78        }
79        let eviction_steps = self.steps.iter().filter(|s| s.evictions_this_step > 0).count();
80        eviction_steps as f32 / self.steps.len() as f32 > 0.1
81    }
82
83    /// Check if KV cache is thrashing (high evictions + low hit rate).
84    ///
85    /// Returns true if the recent window shows both high eviction rate and low hit rate.
86    /// Uses all available steps if fewer than `window` steps exist.
87    ///
88    /// # Arguments
89    /// - `window`: Number of recent steps to consider (uses available if fewer)
90    /// - `min_hit_rate`: Minimum acceptable hit rate (0.0-1.0)
91    pub fn has_thrashing(&self, window: usize, min_hit_rate: f32) -> bool {
92        if self.steps.is_empty() {
93            return false;
94        }
95
96        // Use all steps if fewer than window
97        let actual_window = std::cmp::min(window, self.steps.len());
98        let recent_steps = &self.steps[self.steps.len() - actual_window..];
99        let recent_evictions: usize = recent_steps.iter().map(|s| s.evictions_this_step).sum();
100        let recent_hit_rate: f32 =
101            recent_steps.iter().map(|s| s.cache_hit_rate).sum::<f32>() / actual_window as f32;
102
103        // Thrashing: more than half the steps have evictions AND hit rate below threshold
104        recent_evictions > actual_window / 2 && recent_hit_rate < min_hit_rate
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_kv_cache_state_trace() {
114        let trace = KvCacheStateTrace::new(0, 2048);
115        assert_eq!(trace.step, 0);
116        assert_eq!(trace.max_positions, 2048);
117        assert!(!trace.is_window_exhausted());
118    }
119
120    #[test]
121    fn test_kv_cache_state_utilization() {
122        let mut trace = KvCacheStateTrace::new(0, 1000);
123        trace.valid_positions = 500;
124        assert!((trace.utilization() - 0.5).abs() < 0.01);
125    }
126
127    #[test]
128    fn test_kv_cache_session_trace() {
129        let mut session = KvCacheSessionTrace::default();
130        session.add_step(KvCacheStateTrace {
131            step: 0,
132            cache_hit_rate: 0.9,
133            evictions_this_step: 0,
134            cache_size_bytes: 1000,
135            ..Default::default()
136        });
137        session.add_step(KvCacheStateTrace {
138            step: 1,
139            cache_hit_rate: 0.8,
140            evictions_this_step: 5,
141            cache_size_bytes: 2000,
142            ..Default::default()
143        });
144
145        assert_eq!(session.steps.len(), 2);
146        assert_eq!(session.total_evictions, 5);
147        assert_eq!(session.peak_memory_bytes, 2000);
148        assert!((session.avg_hit_rate - 0.85).abs() < 0.01);
149    }
150
151    // ================================================================
152    // Coverage tests for has_high_eviction_rate (0% → 100%)
153    // ================================================================
154
155    #[test]
156    fn test_high_eviction_rate_empty_session() {
157        let session = KvCacheSessionTrace::default();
158        // Empty steps → early return false
159        assert!(!session.has_high_eviction_rate());
160    }
161
162    #[test]
163    fn test_high_eviction_rate_no_evictions() {
164        let mut session = KvCacheSessionTrace::default();
165        for i in 0..10 {
166            session.add_step(KvCacheStateTrace {
167                step: i,
168                evictions_this_step: 0,
169                ..Default::default()
170            });
171        }
172        // 0/10 = 0% eviction rate → false
173        assert!(!session.has_high_eviction_rate());
174    }
175
176    #[test]
177    fn test_high_eviction_rate_at_boundary() {
178        // Exactly 10% eviction rate: 1 out of 10 steps has evictions.
179        // 1/10 = 0.1, but the check is strictly > 0.1, so this is false.
180        let mut session = KvCacheSessionTrace::default();
181        for i in 0..10 {
182            session.add_step(KvCacheStateTrace {
183                step: i,
184                evictions_this_step: if i == 0 { 1 } else { 0 },
185                ..Default::default()
186            });
187        }
188        assert!(!session.has_high_eviction_rate());
189    }
190
191    #[test]
192    fn test_high_eviction_rate_just_above_boundary() {
193        // 2 out of 10 steps have evictions = 20% > 10% → true
194        let mut session = KvCacheSessionTrace::default();
195        for i in 0..10 {
196            session.add_step(KvCacheStateTrace {
197                step: i,
198                evictions_this_step: if i < 2 { 1 } else { 0 },
199                ..Default::default()
200            });
201        }
202        assert!(session.has_high_eviction_rate());
203    }
204
205    #[test]
206    fn test_high_eviction_rate_all_evictions() {
207        // Every step has evictions → 100% > 10% → true
208        let mut session = KvCacheSessionTrace::default();
209        for i in 0..5 {
210            session.add_step(KvCacheStateTrace {
211                step: i,
212                evictions_this_step: 3,
213                ..Default::default()
214            });
215        }
216        assert!(session.has_high_eviction_rate());
217    }
218
219    #[test]
220    fn test_high_eviction_rate_single_step_with_eviction() {
221        // 1 step with evictions out of 1 total = 100% → true
222        let mut session = KvCacheSessionTrace::default();
223        session.add_step(KvCacheStateTrace {
224            step: 0,
225            evictions_this_step: 1,
226            ..Default::default()
227        });
228        assert!(session.has_high_eviction_rate());
229    }
230
231    #[test]
232    fn test_high_eviction_rate_single_step_without_eviction() {
233        // 1 step with 0 evictions out of 1 total = 0% → false
234        let mut session = KvCacheSessionTrace::default();
235        session.add_step(KvCacheStateTrace {
236            step: 0,
237            evictions_this_step: 0,
238            ..Default::default()
239        });
240        assert!(!session.has_high_eviction_rate());
241    }
242}