Skip to main content

vyre_driver/
cache_eviction.rs

1//! Backend-neutral cache eviction policy.
2//!
3//! Concrete drivers use this for pipeline/module caches without depending
4//! on domain or self-substrate crates. Inputs are caller-owned marginal
5//! gains; output is a 0/1 retention vector where `1` means keep.
6
7/// Compute the retention set: `k` entries to keep from `n` gains.
8///
9/// The algorithm is greedy argmax over caller-provided marginal gains.
10/// The selected entry's gain is zeroed after each pick so it cannot be
11/// selected twice. Callers that model correlated entries should update
12/// `gains` between calls before invoking this helper.
13#[must_use]
14pub fn select_retention_set(gains: &mut [u32], n: u32, k: u32) -> Vec<u32> {
15    match try_select_retention_set(gains, n, k) {
16        Ok(picked) => picked,
17        Err(error) => {
18            tracing::error!("{error}");
19            Vec::new()
20        }
21    }
22}
23
24/// Compute the retention set with fallible result storage allocation.
25pub fn try_select_retention_set(
26    gains: &mut [u32],
27    n: u32,
28    k: u32,
29) -> Result<Vec<u32>, CacheEvictionAllocationError> {
30    let effective_n = effective_len(gains, n);
31    let mut picked = Vec::new();
32    reserve_picked(&mut picked, effective_n)?;
33    select_retention_set_into(gains, n, k, &mut picked);
34    Ok(picked)
35}
36
37/// Compute the retention set into caller-owned storage.
38pub fn select_retention_set_into(gains: &mut [u32], n: u32, k: u32, picked: &mut Vec<u32>) {
39    let effective_n = effective_len(gains, n);
40    let keep_limit = (k as usize).min(effective_n);
41    picked.clear();
42    picked.resize(effective_n, 0);
43    let mut keep_count = 0usize;
44    while keep_count < keep_limit {
45        let Some(winner) = argmax_unpicked(&gains[..effective_n], picked) else {
46            break;
47        };
48        picked[winner] = 1;
49        gains[winner] = 0;
50        keep_count += 1;
51    }
52}
53
54/// Compute the retention set into caller-owned fallible storage.
55pub fn try_select_retention_set_into(
56    gains: &mut [u32],
57    n: u32,
58    k: u32,
59    picked: &mut Vec<u32>,
60) -> Result<(), CacheEvictionAllocationError> {
61    let effective_n = effective_len(gains, n);
62    reserve_picked(picked, effective_n)?;
63    select_retention_set_into(gains, n, k, picked);
64    Ok(())
65}
66
67/// Retention-vector storage allocation failure.
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct CacheEvictionAllocationError {
70    /// Requested retention-vector entries.
71    pub requested: usize,
72    /// Allocator failure details.
73    pub message: String,
74}
75
76impl std::fmt::Display for CacheEvictionAllocationError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(
79            f,
80            "cache eviction failed to reserve {} retention entries: {}. Fix: shard cache eviction or lower cache soft caps before eviction planning.",
81            self.requested, self.message
82        )
83    }
84}
85
86impl std::error::Error for CacheEvictionAllocationError {}
87
88/// Record one eviction decision in the driver metrics/log stream.
89pub fn record_eviction(dropped_fraction: f64) {
90    let dropped_fraction = if dropped_fraction.is_finite() {
91        dropped_fraction.clamp(0.0, 1.0)
92    } else {
93        0.0
94    };
95    tracing::trace!(
96        target: "vyre.driver.eviction",
97        dropped_fraction,
98        "cache eviction decision",
99    );
100}
101
102/// Record one eviction decision from exact entry counts.
103pub fn record_eviction_counts(dropped_entries: usize, total_entries: usize) {
104    let dropped_basis_points = eviction_basis_points(dropped_entries, total_entries);
105    tracing::trace!(
106        target: "vyre.driver.eviction",
107        dropped_entries,
108        total_entries,
109        dropped_basis_points,
110        dropped_fraction = f64::from(dropped_basis_points) / 10_000.0,
111        "cache eviction decision",
112    );
113}
114
115/// Compute exact floor basis points for an eviction decision.
116#[must_use]
117pub fn eviction_basis_points(dropped_entries: usize, total_entries: usize) -> u32 {
118    if total_entries == 0 {
119        return 0;
120    }
121    let bounded_dropped = dropped_entries.min(total_entries);
122    let dropped = u64::try_from(bounded_dropped).unwrap_or(u64::MAX);
123    let total = u64::try_from(total_entries).unwrap_or(u64::MAX);
124    crate::numeric::ratio_basis_points_u64(
125        dropped,
126        total,
127        0,
128        "cache eviction dropped entries",
129        "driver",
130    )
131    .min(10_000)
132}
133
134fn effective_len(gains: &[u32], n: u32) -> usize {
135    gains.len().min(n as usize)
136}
137
138fn reserve_picked(
139    picked: &mut Vec<u32>,
140    effective_n: usize,
141) -> Result<(), CacheEvictionAllocationError> {
142    crate::allocation::try_reserve_vec_to_capacity(picked, effective_n).map_err(|error| {
143        CacheEvictionAllocationError {
144            requested: effective_n,
145            message: error.to_string(),
146        }
147    })
148}
149
150fn argmax_unpicked(gains: &[u32], picked: &[u32]) -> Option<usize> {
151    let mut best: Option<(usize, u32)> = None;
152    for (idx, gain) in gains.iter().copied().enumerate() {
153        if picked.get(idx).copied().unwrap_or(0) != 0 || gain == 0 {
154            continue;
155        }
156        match best {
157            Some((_, current)) if gain <= current => {}
158            _ => best = Some((idx, gain)),
159        }
160    }
161    best.map(|(idx, _)| idx)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn retains_top_k_gains() {
170        let mut gains = vec![3, 10, 2, 8, 1];
171        let picked = select_retention_set(&mut gains, 5, 2);
172        assert_eq!(picked, vec![0, 1, 0, 1, 0]);
173    }
174
175    #[test]
176    fn zero_k_evicts_all() {
177        let mut gains = vec![3, 10, 2];
178        let picked = select_retention_set(&mut gains, 3, 0);
179        assert_eq!(picked, vec![0, 0, 0]);
180    }
181
182    #[test]
183    fn k_equal_n_keeps_positive_gain_entries() {
184        let mut gains = vec![3, 0, 2];
185        let picked = select_retention_set(&mut gains, 3, 3);
186        assert_eq!(picked, vec![1, 0, 1]);
187    }
188
189    #[test]
190    fn into_reuses_storage() {
191        let mut gains = vec![1, 9, 4];
192        let mut picked = Vec::with_capacity(8);
193        let ptr = picked.as_ptr();
194        select_retention_set_into(&mut gains, 3, 2, &mut picked);
195        assert_eq!(picked, vec![0, 1, 1]);
196        assert_eq!(picked.as_ptr(), ptr);
197    }
198
199    #[test]
200    fn try_into_reuses_storage() {
201        let mut gains = vec![1, 9, 4];
202        let mut picked = Vec::with_capacity(8);
203        let ptr = picked.as_ptr();
204        try_select_retention_set_into(&mut gains, 3, 2, &mut picked)
205            .expect("Fix: retention scratch should be reusable");
206        assert_eq!(picked, vec![0, 1, 1]);
207        assert_eq!(picked.as_ptr(), ptr);
208    }
209
210    #[test]
211    fn invalid_sizing_is_clamped_not_panicked() {
212        let mut gains = vec![5, 1];
213        let picked = select_retention_set(&mut gains, 99, 99);
214        assert_eq!(picked, vec![1, 1]);
215    }
216
217    #[test]
218    fn eviction_basis_points_are_exact_and_bounded() {
219        assert_eq!(eviction_basis_points(0, 0), 0);
220        assert_eq!(eviction_basis_points(1, 2), 5_000);
221        assert_eq!(eviction_basis_points(476, 512), 9_296);
222        assert_eq!(eviction_basis_points(9, 3), 10_000);
223        assert_eq!(eviction_basis_points(usize::MAX, usize::MAX), 10_000);
224    }
225
226    #[test]
227    fn eviction_recording_accepts_hostile_ratios() {
228        record_eviction(f64::NAN);
229        record_eviction(f64::INFINITY);
230        record_eviction(f64::NEG_INFINITY);
231        record_eviction_counts(usize::MAX, 1);
232    }
233
234    #[test]
235    fn release_eviction_selector_exposes_fallible_allocation_path() {
236        let source = include_str!("cache_eviction.rs");
237        assert!(
238            source.contains("pub fn try_select_retention_set")
239                && source.contains("pub fn try_select_retention_set_into")
240                && source.contains("try_reserve_vec_to_capacity"),
241            "Fix: release cache eviction callers need a fallible selector path instead of infallible Vec allocation."
242        );
243        assert!(
244            !source.contains(concat!("Vec::with_capacity", "(effective_len")),
245            "Fix: cache eviction selector must not allocate retention vectors infallibly on release paths."
246        );
247    }
248}