Skip to main content

superfluid_executor/
cache.rs

1//! The executor's prefix / reuse cache: entries in the runtime's pool, and
2//! entries held out of it as the runtime's lossless exports.
3
4use crate::primitives::Seq;
5
6pub const RESIDENT_PREFIX_CLASS: u64 = 1;
7/// The resident entries that have started two lanes or more: a prefix
8/// requests share. Evicted only when this class is named too.
9pub const SHARED_PREFIX_CLASS: u64 = 2;
10/// The entries held out of the pool, as exports in host memory. Named when
11/// memory is short on the machine: they go, and for a while no entry that
12/// leaves the pool is kept as one.
13pub const HOST_EXPORT_CLASS: u64 = 4;
14
15/// One part in eight of an entry: what a seed must take of it to count as a use, and what
16/// two entries may differ by and still be one.
17pub const ENTRY_SHARE: u64 = 8;
18
19pub struct CacheEntry {
20    /// The entry's sequence while it is in the pool, and its name either way
21    /// (a runtime never hands a handle out twice).
22    pub seq: Seq,
23    pub tokens: Vec<u32>,
24    pub pins: u32,
25    pub last_used: u64,
26    pub bytes: u64,
27    /// Lanes started from this entry.
28    pub seeded: u32,
29    /// A copy of a live lane's state, published for the requests that share
30    /// its prefix: a lane seeded from it copies, never takes it over.
31    pub shared: bool,
32    /// `Some`: the entry is out of the pool, held as the runtime's lossless
33    /// export of its `tokens.len()` tokens of state; its sequence is freed.
34    pub exported: Option<Vec<u8>>,
35    /// How much of `tokens` was the prompt of the lane that left the entry
36    /// (the rest it generated); 0 where none is known.
37    pub prompt: u64,
38}
39
40impl CacheEntry {
41    /// Whether `len` tokens are a real part of this entry.
42    pub fn share(&self, len: u64) -> bool {
43        len * ENTRY_SHARE >= self.tokens.len() as u64
44    }
45
46    /// Whether a seed of `len` tokens is served from this entry: any, from
47    /// one in the pool (a copy); a real part of it, from an export.
48    pub fn serves(&self, len: u64) -> bool {
49        self.exported.is_none() || self.share(len)
50    }
51
52    /// An entry holds a prefix requests share when it is a lane's state
53    /// published for them, or has started two lanes or more: it goes after
54    /// every entry that does not.
55    pub fn holds_shared_prefix(&self) -> bool {
56        self.shared || self.seeded >= 2
57    }
58
59    /// Whether `other` serves what this entry does: its tokens begin with all of this
60    /// entry's, or all but a share of them when it was used later.
61    pub fn covered_by(&self, other: &CacheEntry) -> bool {
62        let common = self.tokens.iter().zip(&other.tokens).take_while(|(a, b)| a == b).count();
63        let (n, later) = (self.tokens.len(), (other.last_used, other.seq) > (self.last_used, self.seq));
64        let serves = other.share(common as u64);
65        if common == n {
66            // Of two entries with the same tokens, the later one stays.
67            serves && (other.tokens.len() > n || later)
68        } else {
69            serves && later && (n - common) as u64 * ENTRY_SHARE <= n as u64
70        }
71    }
72}
73
74#[derive(Default)]
75pub struct PrefixCache {
76    pub entries: Vec<CacheEntry>,
77    /// An entry came in or was used since the cache last settled, so an
78    /// export may have come to be covered.
79    pub changed: bool,
80}
81
82impl PrefixCache {
83    pub fn insert(&mut self, seq: Seq, tokens: Vec<u32>, prompt: u64, bytes: u64, now: u64) {
84        let prompt = prompt.min(tokens.len() as u64);
85        self.entries.push(CacheEntry { seq, tokens, pins: 0, last_used: now, bytes, seeded: 0, shared: false, exported: None, prompt });
86        self.changed = true;
87    }
88
89    pub fn get(&self, seq: Seq) -> Option<&CacheEntry> {
90        self.entries.iter().find(|e| e.seq == seq)
91    }
92
93    /// Take the entry out of the cache.
94    pub fn remove(&mut self, seq: Seq) -> Option<CacheEntry> {
95        let at = self.entries.iter().position(|e| e.seq == seq)?;
96        Some(self.entries.remove(at))
97    }
98
99    /// The sequences of the entries in the pool.
100    pub fn resident(&self) -> Vec<Seq> {
101        self.entries.iter().filter(|e| e.exported.is_none()).map(|e| e.seq).collect()
102    }
103
104    /// Tokens of state held out of the pool.
105    pub fn exported_tokens(&self) -> u64 {
106        self.entries.iter().filter(|e| e.exported.is_some()).map(|e| e.tokens.len() as u64).sum()
107    }
108
109    /// Host bytes the exports hold.
110    pub fn exported_bytes(&self) -> u64 {
111        self.entries.iter().filter_map(|e| e.exported.as_ref()).map(|p| p.len() as u64).sum()
112    }
113
114    /// The entries in the pool, then those out of it: of two that serve the
115    /// same seed, the one in the pool copies it, the other imports.
116    fn by_tier(&self) -> impl Iterator<Item = (usize, &CacheEntry)> {
117        let in_pool = self.entries.iter().enumerate().filter(|(_, e)| e.exported.is_none());
118        in_pool.chain(self.entries.iter().enumerate().filter(|(_, e)| e.exported.is_some()))
119    }
120
121    pub fn best_prefix(&self, span: &[u32], page: u64, exact_only: bool) -> Option<(usize, u64)> {
122        let mut best: Option<(usize, u64)> = None;
123        for (i, e) in self.by_tier() {
124            let common = e.tokens.iter().zip(span.iter()).take_while(|(a, b)| a == b).count() as u64;
125            let len = if exact_only {
126                if common == e.tokens.len() as u64 { common } else { 0 }
127            } else {
128                common / page * page
129            };
130            if len >= page && len < span.len() as u64 && e.serves(len) && best.is_none_or(|(_, b)| len > b) {
131                best = Some((i, len));
132            }
133        }
134        best
135    }
136
137    pub fn candidates(&self, span: &[u32], page: u64, exact_only: bool) -> Vec<(u64, Seq)> {
138        let mut out: Vec<(u64, Seq)> = Vec::new();
139        for (_, e) in self.by_tier() {
140            let common = e.tokens.iter().zip(span.iter()).take_while(|(a, b)| a == b).count() as u64;
141            let len = if exact_only {
142                if common == e.tokens.len() as u64 && common < span.len() as u64 { common } else { 0 }
143            } else {
144                common / page * page
145            };
146            if len >= page && len.is_multiple_of(page) && e.serves(len) && !out.iter().any(|(l, _)| *l == len) {
147                out.push((len, e.seq));
148            }
149        }
150        out.sort_by_key(|(l, _)| std::cmp::Reverse(*l));
151        out
152    }
153
154    /// Pool bytes an eviction could free: the entries in the pool no lease
155    /// pins.
156    pub fn unpinned_bytes(&self) -> u64 {
157        self.entries.iter().filter(|e| e.pins == 0 && e.exported.is_none()).map(|e| e.bytes).sum()
158    }
159
160    /// Pool bytes the cache accounts for.
161    pub fn resident_bytes(&self) -> u64 {
162        self.entries.iter().filter(|e| e.exported.is_none()).map(|e| e.bytes).sum()
163    }
164
165    pub fn plan_evict(&self, target: u64, cap: u64, shared_too: bool) -> (u64, Vec<usize>) {
166        let (picked, _) = self.plan_evict_where(cap, shared_too, |p| self.bytes_of(p) >= target);
167        (self.bytes_of(&picked), picked)
168    }
169
170    pub fn plan_evict_until(&self, cap: u64, enough: impl FnMut(&[usize]) -> bool) -> (Vec<usize>, bool) {
171        self.plan_evict_where(cap, true, enough)
172    }
173
174    /// The entries in the pool one eviction takes out of it.
175    fn plan_evict_where(&self, cap: u64, shared_too: bool, mut enough: impl FnMut(&[usize]) -> bool) -> (Vec<usize>, bool) {
176        let mut order: Vec<usize> = (0..self.entries.len())
177            .filter(|&i| {
178                let e = &self.entries[i];
179                e.pins == 0 && e.exported.is_none() && (shared_too || !e.holds_shared_prefix())
180            })
181            .collect();
182        // An entry that has started two lanes or more holds a prefix that
183        // requests share (a system prompt, a document, a burst's common
184        // part): it goes after every entry that has not, oldest first in each.
185        order.sort_by_key(|&i| (self.entries[i].holds_shared_prefix(), self.entries[i].last_used));
186        let mut used = 0u64;
187        let mut picked = Vec::new();
188        if enough(&picked) {
189            return (picked, true);
190        }
191        for i in order {
192            let bytes = self.entries[i].bytes;
193            if used + bytes > cap {
194                continue;
195            }
196            used += bytes;
197            picked.push(i);
198            if enough(&picked) {
199                return (picked, true);
200            }
201        }
202        (picked, false)
203    }
204
205    pub fn bytes_of(&self, indices: &[usize]) -> u64 {
206        indices.iter().map(|&i| self.entries[i].bytes).sum()
207    }
208
209    /// Takes the unpinned entries whose tokens carry one of `digests` out of
210    /// the cache, and names them with whether each was in the pool.
211    pub fn evict_digests(&mut self, digests: &[[u8; 32]]) -> Vec<(Seq, bool)> {
212        let mut gone = Vec::new();
213        self.entries.retain(|e| {
214            let d = superfluid_fingerprint::content_digest(&e.tokens);
215            if e.pins == 0 && digests.contains(&d) {
216                gone.push((e.seq, e.exported.is_none()));
217                false
218            } else {
219                true
220            }
221        });
222        gone
223    }
224
225    /// Removes the entries keyed by exactly `tokens` that no seed holds.
226    pub fn evict_exact(&mut self, tokens: &[u32]) -> Vec<(Seq, bool)> {
227        let mut gone = Vec::new();
228        self.entries.retain(|e| {
229            if e.pins == 0 && e.tokens[..] == *tokens {
230                gone.push((e.seq, e.exported.is_none()));
231                false
232            } else {
233                true
234            }
235        });
236        gone
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::{CacheEntry, PrefixCache};
243
244    fn evict(c: &mut PrefixCache, target: u64, shared_too: bool) -> (u64, Vec<u64>) {
245        let (freed, picked) = c.plan_evict(target, u64::MAX, shared_too);
246        let seqs: Vec<u64> = picked.iter().map(|&i| c.entries[i].seq).collect();
247        for &s in &seqs {
248            c.remove(s);
249        }
250        (freed, seqs)
251    }
252
253    #[test]
254    fn an_entry_that_seeded_several_lanes_outlives_newer_ones_that_seeded_none() {
255        let mut c = PrefixCache::default();
256        c.insert(1, vec![1, 2, 3], 0, 10, 1);
257        c.entries[0].seeded = 7;
258        c.insert(2, vec![4, 5, 6], 0, 10, 5);
259        c.insert(3, vec![7, 8, 9], 0, 10, 6);
260        let (_, gone) = evict(&mut c, 20, true);
261        assert_eq!(gone, vec![2, 3], "the two that seeded nothing go first");
262        c.insert(4, vec![1], 0, 10, 9);
263        c.entries.last_mut().unwrap().seeded = 1;
264        let (freed, gone) = evict(&mut c, 100, false);
265        assert_eq!((freed, gone), (10, vec![4]), "one lane is not a shared prefix; the shared one is spared");
266        let (_, gone) = evict(&mut c, 100, true);
267        assert_eq!(gone, vec![1], "unless shared prefixes may go too");
268    }
269
270    #[test]
271    fn an_entry_out_of_the_pool_is_offered_after_one_in_it_and_only_for_a_real_part_of_it() {
272        let mut c = PrefixCache::default();
273        c.insert(1, (10..26).collect(), 0, 16, 1);
274        c.insert(2, (10..18).collect(), 0, 8, 2);
275        c.entries[0].exported = Some(vec![0; 64]);
276        let span: Vec<u32> = (10..30).collect();
277        assert_eq!(c.candidates(&span, 1, false), vec![(16, 1), (8, 2)]);
278        let branch: Vec<u32> = (10..18).chain([99]).collect();
279        assert_eq!(c.candidates(&branch, 1, false), vec![(8, 2)], "the eight both hold are offered from the pool");
280        let short: Vec<u32> = vec![10, 900];
281        assert_eq!(c.candidates(&short, 1, false), vec![(1, 2)], "one token is no real part of the sixteen exported");
282        assert_eq!((c.resident(), c.exported_tokens(), c.exported_bytes(), c.resident_bytes()), (vec![2], 16, 64, 8));
283        let (_, gone) = evict(&mut c, 100, true);
284        assert_eq!(gone, vec![2], "an eviction takes from the pool only");
285    }
286
287    #[test]
288    fn a_later_turn_covers_an_earlier_one_and_a_different_conversation_does_not() {
289        let entry = |seq: u64, tokens: Vec<u32>, used: u64| CacheEntry {
290            seq,
291            tokens,
292            pins: 0,
293            last_used: used,
294            bytes: 0,
295            seeded: 0,
296            shared: false,
297            exported: None,
298            prompt: 0,
299        };
300        let turn1 = entry(1, (10..26).collect(), 1);
301        let turn2 = entry(2, (10..26).chain([40, 41]).collect(), 2);
302        assert!(turn1.covered_by(&turn2) && !turn2.covered_by(&turn1));
303        let reworded = entry(3, (10..25).chain([70, 71, 72]).collect(), 3);
304        assert!(turn1.covered_by(&reworded), "all but its last token, used later");
305        assert!(!reworded.covered_by(&turn1), "used earlier, and its own last three tokens");
306        let other = entry(4, (10..18).chain(200..208).collect(), 4);
307        assert!(!turn1.covered_by(&other), "half of it is another conversation's");
308        let long = entry(5, (10..300).collect(), 5);
309        assert!(!turn1.covered_by(&long), "sixteen tokens are no real part of 290: a seed of them is not served from its export");
310    }
311}