weir/fixed_point_resident_cache/
cache.rs1use super::{
2 entry::FixedPointResidentGraphCacheEntry, retained_graph_bytes,
3 stats::FixedPointResidentGraphCacheStats,
4};
5use crate::fixed_point_graph::FixedPointForwardGraph;
6use crate::fixed_point_resident::FixedPointResidentGraph;
7use crate::resident_cache_identity::{
8 ResidentGraphCacheIdentity, ResidentGraphCacheMissEvidence, ResidentGraphCacheMissReason,
9};
10use crate::resident_cache_lru::ResidentCacheLru;
11use std::collections::HashMap;
12use vyre::ResidentGraphReuseTelemetry;
13use vyre_primitives::bitset::bitset_words;
14
15#[derive(Clone, Debug, Default, PartialEq)]
18pub struct FixedPointResidentGraphCache {
19 entries: HashMap<ResidentGraphCacheIdentity, FixedPointResidentGraphCacheEntry>,
20 lru: ResidentCacheLru<ResidentGraphCacheIdentity>,
21 max_retained_bytes: Option<usize>,
22 retained_bytes: usize,
23 clock: u64,
24 hits: u64,
25 misses: u64,
26 resident_uploads: u64,
27 resident_graph_reuse: ResidentGraphReuseTelemetry,
28 evictions: u64,
29 last_miss_reason: Option<ResidentGraphCacheMissReason>,
30}
31
32impl FixedPointResidentGraphCache {
33 #[must_use]
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
44 pub fn with_max_retained_bytes(max_retained_bytes: usize) -> Self {
45 Self {
46 max_retained_bytes: Some(max_retained_bytes),
47 ..Self::default()
48 }
49 }
50
51 pub fn get_or_upload(
53 &mut self,
54 backend: &dyn vyre::VyreBackend,
55 graph: &FixedPointForwardGraph,
56 ) -> Result<&FixedPointResidentGraph, vyre::BackendError> {
57 let backend_id = backend.id();
58 let backend_version = backend.version();
59 let layout_hash = graph.stable_layout_hash();
60 let identity = ResidentGraphCacheIdentity::fixed_point(
61 backend_id,
62 backend_version,
63 graph.kind(),
64 layout_hash,
65 graph.node_count(),
66 graph.edge_count(),
67 bitset_words(graph.node_count()),
68 );
69 if let Some(entry) = self.entries.get(&identity) {
70 let retained_bytes = entry.retained_bytes;
71 if entry.graph.node_count() != graph.node_count()
72 || entry.graph.edge_count() != graph.edge_count()
73 || entry.graph.kind() != graph.kind()
74 || entry.graph.stable_layout_hash() != layout_hash
75 {
76 return Err(vyre::BackendError::InvalidProgram {
77 fix: format!(
78 "Fix: resident fixed-point graph cache entry for backend `{}` version `{}` has layout_hash={} nodes={} edges={} kind={:?} but requested layout_hash={} nodes={} edges={} kind={:?}; rebuild the resident graph cache before dispatch.",
79 backend_id,
80 backend_version,
81 entry.graph.stable_layout_hash(),
82 entry.graph.node_count(),
83 entry.graph.edge_count(),
84 entry.graph.kind(),
85 layout_hash,
86 graph.node_count(),
87 graph.edge_count(),
88 graph.kind()
89 ),
90 });
91 }
92 self.last_miss_reason = None;
93 self.hits = self
94 .hits
95 .checked_add(1)
96 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache hit counter overflowed u64; rebuild the resident graph cache before reuse."))?;
97 let retained_bytes_u64 = u64::try_from(retained_bytes).map_err(|error| {
98 resident_graph_cache_error(format!(
99 "Fix: resident fixed-point graph retained byte count does not fit u64 during hit accounting: {error}; shard the graph before resident reuse."
100 ))
101 })?;
102 self.resident_graph_reuse
103 .record_warm_reuse(retained_bytes_u64)
104 .map_err(|error| resident_graph_cache_error(error.to_string()))?;
105 let last_seen = self.next_cache_tick()?;
106 self.record_lru(identity.clone(), last_seen)?;
107 let entry = self.entries.get_mut(&identity).ok_or_else(|| {
108 resident_graph_cache_error("Fix: resident graph cache key disappeared after LRU refresh; rebuild the resident graph cache because this indicates internal cache state corruption.")
109 })?;
110 entry.last_seen = last_seen;
111 return Ok(&entry.graph);
112 }
113 self.last_miss_reason = Some(self.miss_reason_for_identity(&identity));
114 self.misses = self
115 .misses
116 .checked_add(1)
117 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache miss counter overflowed u64; rebuild the resident graph cache before reuse."))?;
118 let retained_bytes = retained_graph_bytes(graph)?;
119 self.evict_until_room(backend, retained_bytes)?;
120 let resident = FixedPointResidentGraph::upload(backend, graph)?;
121 self.resident_uploads = self
122 .resident_uploads
123 .checked_add(1)
124 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache upload counter overflowed u64; rebuild the resident graph cache before reuse."))?;
125 let retained_bytes_u64 = u64::try_from(retained_bytes).map_err(|error| {
126 resident_graph_cache_error(format!(
127 "Fix: resident fixed-point graph retained byte count does not fit u64: {error}; shard the graph before resident upload."
128 ))
129 })?;
130 self.resident_graph_reuse
131 .record_cold_upload(retained_bytes_u64)
132 .map_err(|error| resident_graph_cache_error(error.to_string()))?;
133 let last_seen = self.next_cache_tick()?;
134 self.entries.insert(
135 identity.clone(),
136 FixedPointResidentGraphCacheEntry {
137 retained_bytes,
138 last_seen,
139 graph: resident,
140 },
141 );
142 self.record_lru(identity.clone(), last_seen)?;
143 self.retained_bytes = self
144 .retained_bytes
145 .checked_add(retained_bytes)
146 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache retained byte accounting overflowed usize; reduce retained graph budget or rebuild the resident graph cache."))?;
147 Ok(&self
148 .entries
149 .get(&identity)
150 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache key disappeared after upload; rebuild the resident graph cache because this indicates internal cache state corruption."))?
151 .graph)
152 }
153
154 #[must_use]
156 pub fn len(&self) -> usize {
157 self.entries.len()
158 }
159
160 #[must_use]
162 pub fn is_empty(&self) -> bool {
163 self.entries.is_empty()
164 }
165
166 #[must_use]
168 pub fn stats(&self) -> FixedPointResidentGraphCacheStats {
169 FixedPointResidentGraphCacheStats {
170 hits: self.hits,
171 misses: self.misses,
172 resident_uploads: self.resident_uploads,
173 resident_upload_bytes: self.resident_graph_reuse.upload_bytes,
174 resident_avoided_upload_bytes: self.resident_graph_reuse.avoided_upload_bytes,
175 evictions: self.evictions,
176 retained_bytes: self.retained_bytes,
177 entries: self.entries.len(),
178 }
179 }
180
181 #[must_use]
184 pub fn resident_identities(&self) -> Vec<ResidentGraphCacheIdentity> {
185 let mut identities = self.entries.keys().cloned().collect::<Vec<_>>();
186 identities.sort_by(ResidentGraphCacheIdentity::cmp_stable);
187 identities
188 }
189
190 #[must_use]
192 pub fn miss_reason_for_identity(
193 &self,
194 requested: &ResidentGraphCacheIdentity,
195 ) -> ResidentGraphCacheMissReason {
196 ResidentGraphCacheMissReason::classify(ResidentGraphCacheMissEvidence::from_identities(
197 self.entries.keys(),
198 requested,
199 ))
200 }
201
202 #[must_use]
204 pub const fn last_miss_reason(&self) -> Option<ResidentGraphCacheMissReason> {
205 self.last_miss_reason
206 }
207
208 fn evict_until_room(
209 &mut self,
210 backend: &dyn vyre::VyreBackend,
211 incoming_bytes: usize,
212 ) -> Result<(), vyre::BackendError> {
213 let Some(max_retained_bytes) = self.max_retained_bytes else {
214 return Ok(());
215 };
216 while !self.entries.is_empty()
217 && self
218 .retained_bytes
219 .checked_add(incoming_bytes)
220 .is_none_or(|total| total > max_retained_bytes)
221 {
222 let Some(key) = self.pop_lru_key() else {
223 return Ok(());
224 };
225 let entry = self
226 .entries
227 .remove(&key)
228 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache eviction key disappeared before removal; rebuild the resident graph cache because this indicates internal cache state corruption."))?;
229 self.retained_bytes = self
230 .retained_bytes
231 .checked_sub(entry.retained_bytes)
232 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache retained byte accounting underflowed during eviction; rebuild the resident graph cache before reuse."))?;
233 self.evictions = self
234 .evictions
235 .checked_add(1)
236 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache eviction counter overflowed u64; rebuild the resident graph cache before reuse."))?;
237 entry.graph.free(backend)?;
238 }
239 Ok(())
240 }
241
242 pub fn free_all(&mut self, backend: &dyn vyre::VyreBackend) -> Result<(), vyre::BackendError> {
244 let mut first_error = None;
245 for (_, entry) in self.entries.drain() {
246 self.retained_bytes =
247 self.retained_bytes
248 .checked_sub(entry.retained_bytes)
249 .ok_or_else(|| vyre::BackendError::InvalidProgram {
250 fix: "Fix: resident graph cache retained byte accounting underflowed while freeing all entries; rebuild the cache before reuse.".to_string(),
251 })?;
252 if let Err(error) = entry.graph.free(backend) {
253 first_error.get_or_insert(error);
254 }
255 }
256 self.lru.clear();
257 if let Some(error) = first_error {
258 Err(error)
259 } else {
260 Ok(())
261 }
262 }
263
264 fn next_cache_tick(&mut self) -> Result<u64, vyre::BackendError> {
265 self.clock = self
266 .clock
267 .checked_add(1)
268 .ok_or_else(|| resident_graph_cache_error("Fix: resident graph cache logical clock overflowed u64; rebuild the resident graph cache before reuse."))?;
269 Ok(self.clock)
270 }
271
272 fn pop_lru_key(&mut self) -> Option<ResidentGraphCacheIdentity> {
273 self.lru.pop_valid(
274 |key| self.entries.get(key).map(|entry| entry.last_seen),
275 || self.entries.keys().next().cloned(),
276 )
277 }
278
279 fn record_lru(
280 &mut self,
281 key: ResidentGraphCacheIdentity,
282 last_seen: u64,
283 ) -> Result<(), vyre::BackendError> {
284 self.lru
285 .record(
286 key,
287 last_seen,
288 self.entries
289 .iter()
290 .map(|(key, entry)| (key.clone(), entry.last_seen)),
291 "resident graph cache",
292 )
293 .map_err(resident_graph_cache_error)
294 }
295
296 #[cfg(test)]
297 pub(super) fn lru_len_for_tests(&self) -> usize {
298 self.lru.len()
299 }
300}
301
302fn resident_graph_cache_error(fix: impl Into<String>) -> vyre::BackendError {
303 vyre::BackendError::InvalidProgram { fix: fix.into() }
304}