1use crate::primitives::Seq;
5
6pub const RESIDENT_PREFIX_CLASS: u64 = 1;
7pub const SHARED_PREFIX_CLASS: u64 = 2;
10pub const HOST_EXPORT_CLASS: u64 = 4;
14
15pub const ENTRY_SHARE: u64 = 8;
18
19pub struct CacheEntry {
20 pub seq: Seq,
23 pub tokens: Vec<u32>,
24 pub pins: u32,
25 pub last_used: u64,
26 pub bytes: u64,
27 pub seeded: u32,
29 pub shared: bool,
32 pub exported: Option<Vec<u8>>,
35 pub prompt: u64,
38}
39
40impl CacheEntry {
41 pub fn share(&self, len: u64) -> bool {
43 len * ENTRY_SHARE >= self.tokens.len() as u64
44 }
45
46 pub fn serves(&self, len: u64) -> bool {
49 self.exported.is_none() || self.share(len)
50 }
51
52 pub fn holds_shared_prefix(&self) -> bool {
56 self.shared || self.seeded >= 2
57 }
58
59 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 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 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 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 pub fn resident(&self) -> Vec<Seq> {
101 self.entries.iter().filter(|e| e.exported.is_none()).map(|e| e.seq).collect()
102 }
103
104 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 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 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 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 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 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 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 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 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}