1mod executor;
11mod lifecycle;
12mod persistence;
13mod policy;
14mod storage;
15mod telemetry;
16mod worker;
17
18pub use executor::{
19 CacheIoAdmission, CacheIoCompletionDisposition, CacheIoExecutionState,
20 CacheIoExecutionStateError, CacheIoPreparation, CacheIoStartDisposition,
21};
22pub use lifecycle::{CacheBlockLifecycle, CacheLifecycleError, MutableCacheTail};
23pub use persistence::{
24 finalize_prompt_cache_shard, hash_prompt_cache_shard_payload, inspect_prompt_cache,
25 prompt_cache_rank_path, resolve_prompt_cache_root, safe_prompt_cache_shard_path,
26 validate_prompt_cache_manifest, LiveCacheBlockPublication, LiveCachePublicationError,
27 PromptCachePersistenceError, PromptCachePublication, ReversiblePromptCachePublication,
28 MAX_PROMPT_CACHE_SHARD_HEADER_BYTES, PROMPT_CACHE_CURRENT_FILE,
29 PROMPT_CACHE_GENERATIONS_DIRECTORY,
30};
31pub use policy::{
32 CacheResidencyConfigurationError, CacheResidencyPolicy, LiveCacheDiskPolicy, PagedCacheOptions,
33};
34pub use storage::{
35 CacheBlockStorage, CacheHostDemotionOperation, CacheHostPromotion, CacheIoOperation,
36 CacheIoOperationKey, CacheIoOperationKind, CacheStorageError, CacheStoragePhase,
37};
38pub use telemetry::{
39 CacheLayerResidencyReport, CacheLayerResidencyStats, CacheResidencyReport,
40 CacheResidencyTelemetry, CACHE_RESIDENCY_LAYER_REPORT_LIMIT,
41};
42pub use worker::{
43 CacheIoSubmission, CacheIoSubmissionOutcome, CacheIoTicket, CacheIoWorker, CacheIoWorkerError,
44};
45
46use serde::{Deserialize, Serialize};
47use std::{
48 collections::BTreeMap,
49 sync::{
50 atomic::{AtomicU64, Ordering},
51 Arc, Mutex,
52 },
53};
54
55static NEXT_CACHE_POOL_ID: AtomicU64 = AtomicU64::new(1);
56static NEXT_CACHE_POOL_RESERVATION_ID: AtomicU64 = AtomicU64::new(1);
57
58#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
60pub struct CachePoolLimits {
61 device_bytes: u64,
62 host_bytes: u64,
63 transfer_in_flight_bytes: u64,
64 disk_bytes: u64,
65}
66
67impl CachePoolLimits {
68 pub fn new(
73 device_bytes: u64,
74 host_bytes: u64,
75 transfer_in_flight_bytes: u64,
76 disk_bytes: u64,
77 ) -> Result<Self, CachePoolError> {
78 if device_bytes == 0 {
79 return Err(CachePoolError::InvalidLimits(
80 "cache pool device budget must be nonzero",
81 ));
82 }
83 if transfer_in_flight_bytes == 0 {
84 return Err(CachePoolError::InvalidLimits(
85 "cache pool transfer-in-flight budget must be nonzero",
86 ));
87 }
88 Ok(Self {
89 device_bytes,
90 host_bytes,
91 transfer_in_flight_bytes,
92 disk_bytes,
93 })
94 }
95
96 pub const fn device_bytes(self) -> u64 {
98 self.device_bytes
99 }
100
101 pub const fn host_bytes(self) -> u64 {
103 self.host_bytes
104 }
105
106 pub const fn transfer_in_flight_bytes(self) -> u64 {
108 self.transfer_in_flight_bytes
109 }
110
111 pub const fn disk_bytes(self) -> u64 {
113 self.disk_bytes
114 }
115}
116
117#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
119pub struct CachePoolUsage {
120 pub device_bytes: u64,
122 pub host_bytes: u64,
124 pub transfer_in_flight_bytes: u64,
126 pub disk_bytes: u64,
128}
129
130impl CachePoolUsage {
131 fn checked_add(self, other: Self) -> Option<Self> {
132 Some(Self {
133 device_bytes: self.device_bytes.checked_add(other.device_bytes)?,
134 host_bytes: self.host_bytes.checked_add(other.host_bytes)?,
135 transfer_in_flight_bytes: self
136 .transfer_in_flight_bytes
137 .checked_add(other.transfer_in_flight_bytes)?,
138 disk_bytes: self.disk_bytes.checked_add(other.disk_bytes)?,
139 })
140 }
141
142 fn checked_sub(self, other: Self) -> Option<Self> {
143 Some(Self {
144 device_bytes: self.device_bytes.checked_sub(other.device_bytes)?,
145 host_bytes: self.host_bytes.checked_sub(other.host_bytes)?,
146 transfer_in_flight_bytes: self
147 .transfer_in_flight_bytes
148 .checked_sub(other.transfer_in_flight_bytes)?,
149 disk_bytes: self.disk_bytes.checked_sub(other.disk_bytes)?,
150 })
151 }
152}
153
154#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum CachePoolResource {
158 Device,
160 Host,
162 TransferInFlight,
164 Disk,
166}
167
168#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
170pub struct CachePoolReport {
171 pub pool_id: u64,
173 pub managers: usize,
175 pub current_device_bytes: u64,
177 pub peak_device_bytes: u64,
179 pub current_host_bytes: u64,
181 pub peak_host_bytes: u64,
183 pub current_transfer_in_flight_bytes: u64,
185 pub peak_transfer_in_flight_bytes: u64,
187 pub current_disk_bytes: u64,
189 pub peak_disk_bytes: u64,
191 pub limits: CachePoolLimits,
193}
194
195#[derive(Debug)]
196struct CachePoolState {
197 managers: BTreeMap<u64, CachePoolUsage>,
198 reservations: BTreeMap<u64, CachePoolUsage>,
199 current: CachePoolUsage,
200 peak: CachePoolUsage,
201}
202
203#[derive(Clone)]
205pub struct CacheResidencyPool {
206 id: u64,
207 limits: CachePoolLimits,
208 state: Arc<Mutex<CachePoolState>>,
209}
210
211impl std::fmt::Debug for CacheResidencyPool {
212 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 formatter
214 .debug_struct("CacheResidencyPool")
215 .field("id", &self.id)
216 .field("limits", &self.limits)
217 .finish_non_exhaustive()
218 }
219}
220
221impl PartialEq for CacheResidencyPool {
222 fn eq(&self, other: &Self) -> bool {
223 Arc::ptr_eq(&self.state, &other.state)
224 }
225}
226
227impl Eq for CacheResidencyPool {}
228
229impl CacheResidencyPool {
230 pub fn new(limits: CachePoolLimits) -> Self {
232 Self {
233 id: NEXT_CACHE_POOL_ID.fetch_add(1, Ordering::Relaxed),
234 limits,
235 state: Arc::new(Mutex::new(CachePoolState {
236 managers: BTreeMap::new(),
237 reservations: BTreeMap::new(),
238 current: CachePoolUsage::default(),
239 peak: CachePoolUsage::default(),
240 })),
241 }
242 }
243
244 pub const fn id(&self) -> u64 {
246 self.id
247 }
248
249 pub const fn limits(&self) -> CachePoolLimits {
251 self.limits
252 }
253
254 pub fn register_manager(&self, manager: u64) -> Result<CachePoolMembership, CachePoolError> {
256 let mut state = self.state.lock().map_err(|_| CachePoolError::Poisoned)?;
257 if state.managers.contains_key(&manager) {
258 return Err(CachePoolError::DuplicateManager { manager });
259 }
260 state.managers.insert(manager, CachePoolUsage::default());
261 Ok(CachePoolMembership {
262 manager,
263 pool: self.clone(),
264 })
265 }
266
267 pub fn update_manager(
275 &self,
276 manager: u64,
277 usage: CachePoolUsage,
278 ) -> Result<CachePoolUsage, CachePoolError> {
279 let mut state = self.state.lock().map_err(|_| CachePoolError::Poisoned)?;
280 let old = *state
281 .managers
282 .get(&manager)
283 .ok_or(CachePoolError::UnknownManager { manager })?;
284 let current = state
285 .current
286 .checked_sub(old)
287 .and_then(|current| current.checked_add(usage))
288 .ok_or(CachePoolError::AccountingOverflow {
289 operation: "manager occupancy publication",
290 })?;
291 state.managers.insert(manager, usage);
292 state.current = current;
293 update_peaks(&mut state, self.limits);
294 Ok(state.current)
295 }
296
297 pub fn reserve(&self, usage: CachePoolUsage) -> Result<CachePoolReservation, CachePoolError> {
299 let mut state = self.state.lock().map_err(|_| CachePoolError::Poisoned)?;
300 validate_additional(state.current, usage, self.limits)?;
301 let required =
302 state
303 .current
304 .checked_add(usage)
305 .ok_or(CachePoolError::AccountingOverflow {
306 operation: "temporary admission",
307 })?;
308 let reservation = NEXT_CACHE_POOL_RESERVATION_ID.fetch_add(1, Ordering::Relaxed);
309 state.reservations.insert(reservation, usage);
310 state.current = required;
311 update_peaks(&mut state, self.limits);
312 Ok(CachePoolReservation {
313 reservation,
314 pool: self.clone(),
315 })
316 }
317
318 pub fn reserve_transfer(&self, bytes: u64) -> Result<CachePoolReservation, CachePoolError> {
320 self.reserve(CachePoolUsage {
321 transfer_in_flight_bytes: bytes,
322 ..CachePoolUsage::default()
323 })
324 }
325
326 pub fn report(&self) -> Result<CachePoolReport, CachePoolError> {
328 let state = self.state.lock().map_err(|_| CachePoolError::Poisoned)?;
329 Ok(CachePoolReport {
330 pool_id: self.id,
331 managers: state.managers.len(),
332 current_device_bytes: state.current.device_bytes,
333 peak_device_bytes: state.peak.device_bytes,
334 current_host_bytes: state.current.host_bytes,
335 peak_host_bytes: state.peak.host_bytes,
336 current_transfer_in_flight_bytes: state.current.transfer_in_flight_bytes,
337 peak_transfer_in_flight_bytes: state.peak.transfer_in_flight_bytes,
338 current_disk_bytes: state.current.disk_bytes,
339 peak_disk_bytes: state.peak.disk_bytes,
340 limits: self.limits,
341 })
342 }
343
344 fn remove_manager(&self, manager: u64) {
345 if let Ok(mut state) = self.state.lock() {
346 if let Some(previous) = state.managers.get(&manager).copied() {
347 if let Some(current) = state.current.checked_sub(previous) {
348 state.managers.remove(&manager);
349 state.current = current;
350 }
351 }
352 }
353 }
354}
355
356#[derive(Debug)]
358pub struct CachePoolReservation {
359 reservation: u64,
360 pool: CacheResidencyPool,
361}
362
363impl Drop for CachePoolReservation {
364 fn drop(&mut self) {
365 if let Ok(mut state) = self.pool.state.lock() {
366 if let Some(usage) = state.reservations.get(&self.reservation).copied() {
367 if let Some(current) = state.current.checked_sub(usage) {
368 state.reservations.remove(&self.reservation);
369 state.current = current;
370 }
371 }
372 }
373 }
374}
375
376#[derive(Debug)]
378pub struct CachePoolMembership {
379 manager: u64,
380 pool: CacheResidencyPool,
381}
382
383impl CachePoolMembership {
384 pub const fn pool(&self) -> &CacheResidencyPool {
386 &self.pool
387 }
388}
389
390impl Drop for CachePoolMembership {
391 fn drop(&mut self) {
392 self.pool.remove_manager(self.manager);
393 }
394}
395
396fn validate_additional(
397 current: CachePoolUsage,
398 usage: CachePoolUsage,
399 limits: CachePoolLimits,
400) -> Result<(), CachePoolError> {
401 let required = current
402 .checked_add(usage)
403 .ok_or(CachePoolError::AccountingOverflow {
404 operation: "admission validation",
405 })?;
406 for (resource, additional, required, budget) in [
407 (
408 CachePoolResource::Device,
409 usage.device_bytes,
410 required.device_bytes,
411 limits.device_bytes,
412 ),
413 (
414 CachePoolResource::Host,
415 usage.host_bytes,
416 required.host_bytes,
417 limits.host_bytes,
418 ),
419 (
420 CachePoolResource::TransferInFlight,
421 usage.transfer_in_flight_bytes,
422 required.transfer_in_flight_bytes,
423 limits.transfer_in_flight_bytes,
424 ),
425 (
426 CachePoolResource::Disk,
427 usage.disk_bytes,
428 required.disk_bytes,
429 limits.disk_bytes,
430 ),
431 ] {
432 if additional != 0 && required > budget {
433 return Err(CachePoolError::BudgetExceeded {
434 resource,
435 required,
436 budget,
437 });
438 }
439 }
440 Ok(())
441}
442
443fn update_peaks(state: &mut CachePoolState, limits: CachePoolLimits) {
444 if state.current.device_bytes <= limits.device_bytes {
445 state.peak.device_bytes = state.peak.device_bytes.max(state.current.device_bytes);
446 }
447 if state.current.host_bytes <= limits.host_bytes {
448 state.peak.host_bytes = state.peak.host_bytes.max(state.current.host_bytes);
449 }
450 if state.current.transfer_in_flight_bytes <= limits.transfer_in_flight_bytes {
451 state.peak.transfer_in_flight_bytes = state
452 .peak
453 .transfer_in_flight_bytes
454 .max(state.current.transfer_in_flight_bytes);
455 }
456 if state.current.disk_bytes <= limits.disk_bytes {
457 state.peak.disk_bytes = state.peak.disk_bytes.max(state.current.disk_bytes);
458 }
459}
460
461#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
463pub enum CachePoolError {
464 #[error("invalid cache pool limits: {0}")]
466 InvalidLimits(&'static str),
467 #[error("cache pool manager {manager} is already registered")]
469 DuplicateManager {
470 manager: u64,
472 },
473 #[error("cache pool manager {manager} is not registered")]
475 UnknownManager {
476 manager: u64,
478 },
479 #[error(
481 "cache pool {resource:?} budget exceeded: required {required} bytes, budget {budget} bytes"
482 )]
483 BudgetExceeded {
484 resource: CachePoolResource,
486 required: u64,
488 budget: u64,
490 },
491 #[error("cache pool accounting overflow during {operation}")]
493 AccountingOverflow {
494 operation: &'static str,
496 },
497 #[error("cache residency pool state is poisoned")]
499 Poisoned,
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use std::sync::{mpsc, Barrier};
506
507 fn pool() -> CacheResidencyPool {
508 CacheResidencyPool::new(CachePoolLimits::new(16, 12, 10, 8).unwrap())
509 }
510
511 #[test]
512 fn manager_membership_owns_published_occupancy() {
513 let pool = pool();
514 let membership = pool.register_manager(7).unwrap();
515 pool.update_manager(
516 7,
517 CachePoolUsage {
518 device_bytes: 8,
519 host_bytes: 4,
520 ..CachePoolUsage::default()
521 },
522 )
523 .unwrap();
524 let report = pool.report().unwrap();
525 assert_eq!(report.managers, 1);
526 assert_eq!(report.current_device_bytes, 8);
527 assert_eq!(report.current_host_bytes, 4);
528
529 drop(membership);
530 let report = pool.report().unwrap();
531 assert_eq!(report.managers, 0);
532 assert_eq!(report.current_device_bytes, 0);
533 assert_eq!(report.current_host_bytes, 0);
534 }
535
536 #[test]
537 fn reservation_is_atomic_and_released_by_exact_owner() {
538 let pool = pool();
539 let first = pool.reserve_transfer(6).unwrap();
540 assert_eq!(pool.report().unwrap().current_transfer_in_flight_bytes, 6);
541 assert_eq!(
542 pool.reserve_transfer(5).unwrap_err(),
543 CachePoolError::BudgetExceeded {
544 resource: CachePoolResource::TransferInFlight,
545 required: 11,
546 budget: 10,
547 }
548 );
549 assert_eq!(pool.report().unwrap().current_transfer_in_flight_bytes, 6);
550 drop(first);
551 assert_eq!(pool.report().unwrap().current_transfer_in_flight_bytes, 0);
552 }
553
554 #[test]
555 fn independent_resource_axes_fail_closed() {
556 let pool = pool();
557 for (usage, resource, required, budget) in [
558 (
559 CachePoolUsage {
560 device_bytes: 17,
561 ..CachePoolUsage::default()
562 },
563 CachePoolResource::Device,
564 17,
565 16,
566 ),
567 (
568 CachePoolUsage {
569 host_bytes: 13,
570 ..CachePoolUsage::default()
571 },
572 CachePoolResource::Host,
573 13,
574 12,
575 ),
576 (
577 CachePoolUsage {
578 disk_bytes: 9,
579 ..CachePoolUsage::default()
580 },
581 CachePoolResource::Disk,
582 9,
583 8,
584 ),
585 ] {
586 assert_eq!(
587 pool.reserve(usage).unwrap_err(),
588 CachePoolError::BudgetExceeded {
589 resource,
590 required,
591 budget,
592 }
593 );
594 }
595 assert_eq!(pool.report().unwrap().current_device_bytes, 0);
596 assert_eq!(pool.report().unwrap().current_host_bytes, 0);
597 assert_eq!(pool.report().unwrap().current_disk_bytes, 0);
598 }
599
600 #[test]
601 fn reports_and_limits_round_trip_without_backend_types() {
602 let pool = pool();
603 let _membership = pool.register_manager(1).unwrap();
604 pool.update_manager(
605 1,
606 CachePoolUsage {
607 device_bytes: 8,
608 disk_bytes: 4,
609 ..CachePoolUsage::default()
610 },
611 )
612 .unwrap();
613 let report = pool.report().unwrap();
614 let encoded = serde_json::to_string(&report).unwrap();
615 assert_eq!(
616 serde_json::from_str::<CachePoolReport>(&encoded).unwrap(),
617 report
618 );
619 }
620
621 #[test]
622 fn concurrent_admission_has_one_atomic_winner() {
623 let pool = CacheResidencyPool::new(CachePoolLimits::new(64, 10, 64, 0).unwrap());
624 let start = Arc::new(Barrier::new(3));
625 let finish = Arc::new(Barrier::new(3));
626 let (sender, receiver) = mpsc::channel();
627 let handles = (0..2)
628 .map(|_| {
629 let pool = pool.clone();
630 let start = Arc::clone(&start);
631 let finish = Arc::clone(&finish);
632 let sender = sender.clone();
633 std::thread::spawn(move || {
634 start.wait();
635 let admission = pool.reserve(CachePoolUsage {
636 host_bytes: 6,
637 ..CachePoolUsage::default()
638 });
639 sender.send(admission.is_ok()).unwrap();
640 finish.wait();
641 drop(admission);
642 })
643 })
644 .collect::<Vec<_>>();
645 drop(sender);
646 start.wait();
647 let admitted = [receiver.recv().unwrap(), receiver.recv().unwrap()];
648 assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
649 assert_eq!(pool.report().unwrap().current_host_bytes, 6);
650 finish.wait();
651 for handle in handles {
652 handle.join().unwrap();
653 }
654 assert_eq!(pool.report().unwrap().current_host_bytes, 0);
655 }
656
657 #[test]
658 fn manager_identity_and_accounting_fail_closed() {
659 let pool = pool();
660 let _membership = pool.register_manager(9).unwrap();
661 assert_eq!(
662 pool.register_manager(9).unwrap_err(),
663 CachePoolError::DuplicateManager { manager: 9 }
664 );
665 assert_eq!(
666 pool.update_manager(10, CachePoolUsage::default())
667 .unwrap_err(),
668 CachePoolError::UnknownManager { manager: 10 }
669 );
670 let overflow = CacheResidencyPool::new(CachePoolLimits::new(u64::MAX, 1, 1, 0).unwrap());
671 let _reservation = overflow
672 .reserve(CachePoolUsage {
673 device_bytes: u64::MAX,
674 ..CachePoolUsage::default()
675 })
676 .unwrap();
677 assert!(matches!(
678 overflow.reserve(CachePoolUsage {
679 device_bytes: 1,
680 ..CachePoolUsage::default()
681 }),
682 Err(CachePoolError::AccountingOverflow { .. })
683 ));
684 }
685}