1use crate::runtime::cache::lru::{AccessTracker, IntrusiveLru};
2use rustc_hash::FxHashMap;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub struct CacheEntry {
8 pub key: u64,
10 pub size: u64,
12 pub tier: usize,
14}
15
16#[non_exhaustive]
24pub struct CacheTier {
25 pub name: String,
27 pub capacity: u64,
29 pub used: u64,
31 pub(crate) entries: FxHashMap<u64, CacheEntry>,
32 pub(crate) lru: IntrusiveLru<u64, ()>,
33}
34
35impl CacheTier {
36 #[inline]
38 pub fn new(name: impl Into<String>, capacity: u64) -> Self {
39 let name = name.into();
40 match Self::try_new(name.clone(), capacity) {
41 Ok(tier) => tier,
42 Err(error) => {
43 tracing::error!(
44 tier = %name,
45 capacity,
46 error = %error,
47 "wgpu cache tier LRU reservation failed; continuing with grow-on-use metadata"
48 );
49 Self {
50 name,
51 capacity,
52 used: 0,
53 entries: FxHashMap::default(),
54 lru: IntrusiveLru::with_reserved_capacity(0),
55 }
56 }
57 }
58 }
59
60 #[inline]
67 pub fn try_new(
68 name: impl Into<String>,
69 capacity: u64,
70 ) -> Result<Self, vyre_driver::BackendError> {
71 Ok(Self {
72 name: name.into(),
73 capacity,
74 used: 0,
75 entries: FxHashMap::default(),
76 lru: IntrusiveLru::try_with_reserved_capacity(1024)?,
77 })
78 }
79}
80
81#[non_exhaustive]
83pub struct AccessStats {
84 pub frequency: u32,
86 pub last_access: u64,
89 pub size: u64,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct LruPolicy {
97 pub promote_threshold: u32,
99}
100
101impl LruPolicy {
102 pub const DEFAULT_THRESHOLD: u32 = 3;
104
105 #[inline]
107 pub fn new(promote_threshold: u32) -> Self {
108 Self { promote_threshold }
109 }
110}
111
112impl Default for LruPolicy {
113 fn default() -> Self {
114 Self::new(Self::DEFAULT_THRESHOLD)
115 }
116}
117
118impl LruPolicy {
119 fn should_promote(&self, _key: u64, stats: &AccessStats) -> bool {
120 stats.frequency >= self.promote_threshold
121 }
122
123 fn eviction_candidate_per_tier(
124 &self,
125 _tier: usize,
126 entries: &FxHashMap<u64, CacheEntry>,
127 _tracker: &AccessTracker,
128 tier_lru: &IntrusiveLru<u64, ()>,
129 ) -> Option<u64> {
130 for (key, _) in tier_lru.iter_coldest() {
137 if entries.contains_key(key) {
138 return Some(*key);
139 }
140 }
141 entries.keys().copied().next()
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[non_exhaustive]
148pub enum CacheError {
149 KeyNotFound,
151 EntryTooLarge,
153 CapacityAccountingOverflow,
155}
156
157impl std::fmt::Display for CacheError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 Self::KeyNotFound => write!(
161 f,
162 "Key not found in cache. Fix: verify the key was inserted before operating on it."
163 ),
164 Self::EntryTooLarge => write!(
165 f,
166 "Entry size exceeds the capacity of the largest tier. Fix: reduce the buffer size or increase the tier capacity."
167 ),
168 Self::CapacityAccountingOverflow => write!(
169 f,
170 "Tiered cache byte accounting overflowed. Fix: rebuild the cache or shard entries before continuing."
171 ),
172 }
173 }
174}
175
176impl std::error::Error for CacheError {}
177
178#[non_exhaustive]
183pub struct TieredCache {
184 pub(crate) tiers: Vec<CacheTier>,
185 pub(crate) tracker: AccessTracker,
186 pub(crate) policy: LruPolicy,
187 index: FxHashMap<u64, usize>,
189}
190
191impl TieredCache {
192 #[inline]
194 pub fn new(tiers: Vec<CacheTier>) -> Self {
195 match Self::try_new(tiers) {
196 Ok(cache) => cache,
197 Err(error) => {
198 tracing::error!(
199 error = %error,
200 "wgpu tiered cache tracker reservation failed; continuing with grow-on-use metadata"
201 );
202 Self::with_policy(Vec::new(), LruPolicy::default())
203 }
204 }
205 }
206
207 #[inline]
214 pub fn try_new(tiers: Vec<CacheTier>) -> Result<Self, vyre_driver::BackendError> {
215 Self::try_with_policy(tiers, LruPolicy::default())
216 }
217}
218
219impl TieredCache {
220 #[inline]
222 pub fn with_policy(tiers: Vec<CacheTier>, policy: LruPolicy) -> Self {
223 match Self::try_with_policy(tiers, policy) {
224 Ok(cache) => cache,
225 Err(error) => {
226 tracing::error!(
227 error = %error,
228 "wgpu tiered cache tracker reservation failed; continuing with grow-on-use metadata"
229 );
230 Self {
231 tiers: Vec::new(),
232 tracker: AccessTracker::new(),
233 policy,
234 index: FxHashMap::default(),
235 }
236 }
237 }
238 }
239
240 #[inline]
247 pub fn try_with_policy(
248 tiers: Vec<CacheTier>,
249 policy: LruPolicy,
250 ) -> Result<Self, vyre_driver::BackendError> {
251 Ok(Self {
252 tiers,
253 tracker: AccessTracker::try_new()?,
254 policy,
255 index: FxHashMap::default(),
256 })
257 }
258
259 #[inline]
261 pub fn get(&self, key: u64) -> Option<&CacheEntry> {
262 let &tier = self.index.get(&key)?;
263 self.tiers[tier].entries.get(&key)
264 }
265
266 #[inline]
272 pub fn insert(&mut self, key: u64, size: u64) -> Result<(), CacheError> {
273 if self.get(key).is_some() {
274 self.evict(key);
275 }
276 self.tracker.set_size(key, size);
277 self.insert_into_tier(key, size, 0)
278 }
279
280 #[inline]
282 pub fn record_access(&mut self, key: u64) {
283 if let Some(&tier_id) = self.index.get(&key) {
284 self.tracker.record(key);
285 self.tiers[tier_id].lru.touch(key);
288 }
289 }
290
291 #[inline]
297 pub fn promote(&mut self, key: u64) -> Result<(), CacheError> {
298 let entry = self.get(key).copied().ok_or(CacheError::KeyNotFound)?;
299 let stats = self.tracker.stats(key).ok_or(CacheError::KeyNotFound)?;
300 if !self.policy.should_promote(key, &stats) {
301 return Ok(());
302 }
303 let target = entry
304 .tier
305 .checked_add(1)
306 .ok_or(CacheError::CapacityAccountingOverflow)?;
307 if target >= self.tiers.len() {
308 return Ok(());
309 }
310 let size = entry.size;
311 self.remove_entry(key);
312 self.move_into_tier(key, size, target, entry.tier)
313 }
314
315 #[inline]
321 pub fn demote(&mut self, key: u64) -> Result<(), CacheError> {
322 let entry = self.get(key).copied().ok_or(CacheError::KeyNotFound)?;
323 if entry.tier == 0 {
324 return Ok(());
325 }
326 let target = entry.tier - 1;
327 let size = entry.size;
328 self.remove_entry(key);
329 self.move_into_tier(key, size, target, entry.tier)
330 }
331
332 fn insert_into_tier(
333 &mut self,
334 key: u64,
335 size: u64,
336 mut start: usize,
337 ) -> Result<(), CacheError> {
338 while start < self.tiers.len() {
339 if size > self.tiers[start].capacity {
340 start += 1;
341 continue;
342 }
343 if self.make_room(start, size) {
344 self.tiers[start].used = checked_tier_used_add(self.tiers[start].used, size)?;
345 self.tiers[start].entries.insert(
346 key,
347 CacheEntry {
348 key,
349 size,
350 tier: start,
351 },
352 );
353 self.tiers[start].lru.ensure(key);
356 self.tiers[start].lru.touch(key);
357 self.index.insert(key, start);
358 return Ok(());
359 }
360 start += 1;
361 }
362 Err(CacheError::EntryTooLarge)
363 }
364
365 fn move_into_tier(
366 &mut self,
367 key: u64,
368 size: u64,
369 target: usize,
370 fallback: usize,
371 ) -> Result<(), CacheError> {
372 if self.make_room(target, size) {
373 self.tiers[target].used = checked_tier_used_add(self.tiers[target].used, size)?;
374 self.tiers[target].entries.insert(
375 key,
376 CacheEntry {
377 key,
378 size,
379 tier: target,
380 },
381 );
382 self.tiers[target].lru.ensure(key);
383 self.tiers[target].lru.touch(key);
384 self.index.insert(key, target);
385 Ok(())
386 } else {
387 self.insert_into_tier(key, size, fallback)
388 }
389 }
390
391 fn make_room(&mut self, tier: usize, size: u64) -> bool {
392 loop {
393 let used = self.tiers[tier].used;
394 let cap = self.tiers[tier].capacity;
395 if used.checked_add(size).is_some_and(|total| total <= cap) {
396 return true;
397 }
398 let candidate = {
404 let tier_ref = &self.tiers[tier];
405 self.policy.eviction_candidate_per_tier(
406 tier,
407 &tier_ref.entries,
408 &self.tracker,
409 &tier_ref.lru,
410 )
411 };
412 if let Some(key) = candidate {
413 self.evict_from_tier(key, tier);
414 } else {
415 return false;
416 }
417 }
418 }
419
420 fn remove_entry(&mut self, key: u64) -> Option<CacheEntry> {
421 let &tier_id = self.index.get(&key)?;
422 let tier = &mut self.tiers[tier_id];
423 let entry = tier.entries.remove(&key)?;
424 tier.lru.remove(&key);
425 debit_tier_used(tier, entry.size);
426 self.index.remove(&key);
427 Some(entry)
428 }
429
430 fn evict(&mut self, key: u64) -> Option<CacheEntry> {
431 let &tier_id = self.index.get(&key)?;
432 let tier = &mut self.tiers[tier_id];
433 let entry = tier.entries.remove(&key)?;
434 tier.lru.remove(&key);
435 debit_tier_used(tier, entry.size);
436 self.index.remove(&key);
437 self.tracker.remove(key);
438 Some(entry)
439 }
440
441 pub fn evict_coldest(&mut self) -> Option<u64> {
446 for (tier_idx, tier) in self.tiers.iter().enumerate() {
447 if let Some(key) = self.policy.eviction_candidate_per_tier(
448 tier_idx,
449 &tier.entries,
450 &self.tracker,
451 &tier.lru,
452 ) {
453 self.evict_from_tier(key, tier_idx);
454 return Some(key);
455 }
456 }
457 None
458 }
459
460 fn evict_from_tier(&mut self, key: u64, tier: usize) -> Option<CacheEntry> {
461 let tier = &mut self.tiers[tier];
462 let entry = tier.entries.remove(&key)?;
463 tier.lru.remove(&key);
464 debit_tier_used(tier, entry.size);
465 self.index.remove(&key);
466 self.tracker.remove(key);
467 Some(entry)
468 }
469}
470
471fn checked_tier_used_add(used: u64, size: u64) -> Result<u64, CacheError> {
472 used.checked_add(size)
473 .ok_or(CacheError::CapacityAccountingOverflow)
474}
475
476fn debit_tier_used(tier: &mut CacheTier, size: u64) {
477 match tier.used.checked_sub(size) {
478 Some(used) => {
479 tier.used = used;
480 }
481 None => {
482 tracing::error!(
483 tier = %tier.name,
484 used = tier.used,
485 removed_size = size,
486 "tiered cache byte accounting underflowed; repairing from live entries. Fix: investigate mismatched cache tier metadata."
487 );
488 tier.used = recompute_tier_used(tier);
489 }
490 }
491}
492
493fn recompute_tier_used(tier: &CacheTier) -> u64 {
494 let mut total = 0_u64;
495 for entry in tier.entries.values() {
496 total = match total.checked_add(entry.size) {
497 Some(next) => next,
498 None => {
499 tracing::error!(
500 tier = %tier.name,
501 "tiered cache byte accounting overflowed while repairing from live entries; pinning used bytes to u64::MAX."
502 );
503 return u64::MAX;
504 }
505 };
506 }
507 total
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn tiered_cache_repairs_used_bytes_after_underflow_instead_of_panicking() {
516 let mut cache = TieredCache::new(vec![CacheTier::new("gpu", 128)]);
517 cache.insert(1, 64).expect("Fix: test insert must fit");
518 cache.tiers[0].used = 0;
519
520 let removed = cache
521 .evict(1)
522 .expect("Fix: corrupted entry should still evict");
523
524 assert_eq!(removed.size, 64);
525 assert_eq!(cache.tiers[0].used, 0);
526 assert!(cache.get(1).is_none());
527 }
528
529 #[test]
530 fn tiered_cache_source_has_no_release_path_panic_accounting() {
531 let source = include_str!("tiered_cache.rs");
532 let production = source
533 .split("#[cfg(test)]")
534 .next()
535 .expect("Fix: tiered cache production source must precede tests");
536 assert!(
537 !production.contains(concat!("panic", "!("))
538 && !production.contains(".unwrap_or_else("),
539 "Fix: tiered cache accounting must repair or return typed errors instead of aborting."
540 );
541 assert!(
542 production.contains("debit_tier_used")
543 && production.contains("recompute_tier_used")
544 && production.contains("repairing from live entries"),
545 "Fix: tiered cache underflow must be repaired from live entry metadata with a loud diagnostic."
546 );
547 }
548}