Skip to main content

gam_runtime/
resource.rs

1use crate::cgroup_memory::detect_cgroup_memory;
2pub use crate::cgroup_memory::{
3    CgroupMemoryAvailability, CgroupMemoryLimit, CgroupMemoryObservation,
4    CgroupMemoryProbeFailure, CgroupMemoryProbeFailureKind,
5};
6
7/// The library-default streamed row-chunk target (8 MiB), shared as a `const`
8/// so compile-time consumers (e.g. device tile geometry) stay in lockstep with
9/// [`ResourcePolicy::default_library`] without a runtime policy query.
10pub const LIBRARY_ROW_CHUNK_TARGET_BYTES: usize = 8 * 1024 * 1024;
11
12#[derive(Clone, Debug)]
13pub struct ResourcePolicy {
14    pub max_single_materialization_bytes: usize,
15    pub max_operator_cache_bytes: usize,
16    pub max_spatial_distance_cache_bytes: usize,
17    pub max_owned_data_cache_bytes: usize,
18    pub row_chunk_target_bytes: usize,
19    pub derivative_storage_mode: DerivativeStorageMode,
20}
21
22pub const OWNED_DATA_CACHE_MAX_ENTRIES: usize = 2;
23
24// ─────────────────────────────────────────────────────────────────────────────
25// Process-wide memory governor
26// ─────────────────────────────────────────────────────────────────────────────
27
28/// Fraction of the *detected available* memory the governor is allowed to hand
29/// out as reservations: 3/4.
30///
31/// The ledger only accounts for the large, planned allocations that route
32/// through [`MemoryGovernor::try_reserve`] (dense design materializations,
33/// covariance blocks, sampler design assemblies). Everything else — allocator
34/// slack, thread stacks, code, and small per-iteration temporaries, plus
35/// whatever the rest of the machine does concurrently — lives in the remaining
36/// quarter. The base quantity is *available* (not total) memory, so memory
37/// already committed by other processes is excluded before the fraction is
38/// applied.
39const GOVERNOR_BUDGET_NUMERATOR: u128 = 3;
40const GOVERNOR_BUDGET_DENOMINATOR: u128 = 4;
41
42/// Which observation is the binding ceiling on memory available to this
43/// process.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum MemoryAvailabilitySource {
46    Host,
47    Cgroup,
48    HostAndCgroup,
49    CgroupProbeFailure,
50}
51
52impl std::fmt::Display for MemoryAvailabilitySource {
53    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::Host => formatter.write_str("host"),
56            Self::Cgroup => formatter.write_str("cgroup"),
57            Self::HostAndCgroup => formatter.write_str("host and cgroup equally"),
58            Self::CgroupProbeFailure => formatter.write_str("cgroup probe failure"),
59        }
60    }
61}
62
63/// Provenance-preserving memory availability for the current process.
64///
65/// A finite cgroup-v1 or cgroup-v2 ceiling admits exactly `min(host available,
66/// reclaim-aware cgroup available)`. A v2 literal `memory.max = max` remains
67/// typed as unbounded and therefore defers to the host; v1's numeric unlimited
68/// sentinel participates exactly and likewise loses to a tighter host value.
69/// If an active controller cannot be parsed exactly, admission fails closed
70/// with zero bytes while retaining the typed probe failure; it never silently
71/// inherits host capacity.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct MemoryAvailability {
74    host_available_bytes: u64,
75    cgroup: CgroupMemoryObservation,
76    available_bytes: u64,
77    limiting_source: MemoryAvailabilitySource,
78}
79
80impl MemoryAvailability {
81    fn from_observation(
82        host_available_bytes: u64,
83        cgroup: CgroupMemoryObservation,
84    ) -> Self {
85        use std::cmp::Ordering;
86
87        let (available_bytes, limiting_source) = match &cgroup {
88            CgroupMemoryObservation::NotPresent
89            | CgroupMemoryObservation::V2Unbounded { .. } => {
90                (host_available_bytes, MemoryAvailabilitySource::Host)
91            }
92            CgroupMemoryObservation::V2Limited(observation)
93            | CgroupMemoryObservation::V1Limited(observation) => {
94                match observation.available_bytes().cmp(&host_available_bytes) {
95                    Ordering::Less => (
96                        observation.available_bytes(),
97                        MemoryAvailabilitySource::Cgroup,
98                    ),
99                    Ordering::Equal => (
100                        host_available_bytes,
101                        MemoryAvailabilitySource::HostAndCgroup,
102                    ),
103                    Ordering::Greater => {
104                        (host_available_bytes, MemoryAvailabilitySource::Host)
105                    }
106                }
107            }
108            CgroupMemoryObservation::ProbeFailed(_) => {
109                (0, MemoryAvailabilitySource::CgroupProbeFailure)
110            }
111        };
112        Self {
113            host_available_bytes,
114            cgroup,
115            available_bytes,
116            limiting_source,
117        }
118    }
119
120    pub const fn host_available_bytes(&self) -> u64 {
121        self.host_available_bytes
122    }
123
124    pub const fn cgroup(&self) -> &CgroupMemoryObservation {
125        &self.cgroup
126    }
127
128    pub const fn available_bytes(&self) -> u64 {
129        self.available_bytes
130    }
131
132    pub fn available_bytes_usize(&self) -> usize {
133        usize::try_from(self.available_bytes).unwrap_or(usize::MAX)
134    }
135
136    pub const fn limiting_source(&self) -> MemoryAvailabilitySource {
137        self.limiting_source
138    }
139}
140
141impl std::fmt::Display for MemoryAvailability {
142    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match &self.cgroup {
144            CgroupMemoryObservation::ProbeFailed(failure) => write!(
145                formatter,
146                "0 bytes admitted because the active cgroup probe failed closed (host_available={}, failure={})",
147                self.host_available_bytes, failure,
148            ),
149            observation => write!(
150                formatter,
151                "{} bytes limited by {} (host_available={}, {})",
152                self.available_bytes, self.limiting_source, self.host_available_bytes, observation,
153            ),
154        }
155    }
156}
157
158/// Refresh and return the OS and cgroup observations that govern process
159/// memory admission. This is the single system-memory authority shared by the
160/// global governor and specialized runtime planners.
161pub fn detect_memory_availability() -> MemoryAvailability {
162    static SYSTEM: OnceLock<Mutex<sysinfo::System>> = OnceLock::new();
163    let system = SYSTEM.get_or_init(|| Mutex::new(sysinfo::System::new()));
164    let mut system = system.lock().expect("sysinfo system mutex poisoned");
165    system.refresh_memory();
166    let cgroup = detect_cgroup_memory();
167    MemoryAvailability::from_observation(system.available_memory(), cgroup)
168}
169
170/// Convert one provenance-preserving availability observation to the process
171/// budget. Zero is an authoritative exhausted-memory signal and deliberately
172/// yields a zero budget.
173fn governor_budget_from_availability(availability: &MemoryAvailability) -> usize {
174    let scaled = u128::from(availability.available_bytes()) * GOVERNOR_BUDGET_NUMERATOR
175        / GOVERNOR_BUDGET_DENOMINATOR;
176    usize::try_from(scaled).unwrap_or(usize::MAX)
177}
178
179/// Typed refusal from [`MemoryGovernor::try_reserve`].
180///
181/// Carries the full ledger evidence so callers can route to a chunked or
182/// matrix-free strategy (and so error messages explain *why* dense was
183/// refused). This is a routing signal, never an abort: the process still has
184/// its unreserved headroom, the requested allocation just does not fit the
185/// joint budget.
186#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
187pub enum MemoryReservationError {
188    #[error(
189        "{context}: cannot reserve {requested_bytes} bytes; {reserved_bytes} of {budget_bytes} bytes already reserved process-wide; detected availability: {availability}"
190    )]
191    BudgetExceeded {
192        context: Box<str>,
193        requested_bytes: usize,
194        reserved_bytes: usize,
195        budget_bytes: usize,
196        availability: MemoryAvailability,
197    },
198
199    #[error(
200        "{context}: dense allocation size overflow for {copies} copies of a {nrows}x{ncols} f64 matrix"
201    )]
202    SizeOverflow {
203        context: Box<str>,
204        nrows: usize,
205        ncols: usize,
206        copies: usize,
207    },
208}
209
210#[derive(Debug)]
211struct GovernorLedger {
212    budget_bytes: usize,
213    availability: MemoryAvailability,
214    reserved_bytes: std::sync::atomic::AtomicUsize,
215}
216
217/// Process-wide byte-accounting governor for large allocations.
218///
219/// Every large planned allocation (dense design materialization, covariance
220/// block, sampler design assembly) reserves its byte footprint against one
221/// shared ledger via [`try_reserve`](Self::try_reserve) and holds the returned
222/// RAII [`MemoryReservation`] for as long as the allocation is live. Because
223/// the ledger is shared, allocations that are each individually acceptable can
224/// no longer *jointly* exceed memory: whichever request would tip the ledger
225/// past the budget gets a typed [`MemoryReservationError`] and routes to a
226/// chunked or matrix-free strategy instead. Strategy selection is thereby a
227/// continuous function of predicted live bytes vs remaining budget, not of
228/// row/column thresholds.
229///
230/// The global budget is sized once from actually-available memory (host
231/// `available_memory`, clamped by cgroup limits inside containers) — see
232/// [`GOVERNOR_BUDGET_NUMERATOR`] for the headroom rationale.
233#[derive(Debug, Clone)]
234pub struct MemoryGovernor {
235    ledger: Arc<GovernorLedger>,
236}
237
238impl MemoryGovernor {
239    /// The process-wide governor. Budget detection runs once, on first use.
240    pub fn global() -> &'static MemoryGovernor {
241        static GLOBAL: OnceLock<MemoryGovernor> = OnceLock::new();
242        GLOBAL.get_or_init(|| {
243            let availability = detect_memory_availability();
244            MemoryGovernor::with_detected_availability(availability)
245        })
246    }
247
248    fn with_detected_availability(availability: MemoryAvailability) -> Self {
249        let budget_bytes = governor_budget_from_availability(&availability);
250        Self {
251            ledger: Arc::new(GovernorLedger {
252                budget_bytes,
253                availability,
254                reserved_bytes: std::sync::atomic::AtomicUsize::new(0),
255            }),
256        }
257    }
258
259    pub fn budget_bytes(&self) -> usize {
260        self.ledger.budget_bytes
261    }
262
263    pub fn availability(&self) -> MemoryAvailability {
264        self.ledger.availability.clone()
265    }
266
267    pub fn reserved_bytes(&self) -> usize {
268        self.ledger
269            .reserved_bytes
270            .load(std::sync::atomic::Ordering::Acquire)
271    }
272
273    pub fn remaining_bytes(&self) -> usize {
274        self.ledger
275            .budget_bytes
276            .saturating_sub(self.reserved_bytes())
277    }
278
279    /// Absolute ceiling for one governed operation. Consumers reserve the
280    /// operation's complete predicted live set (matrix plus simultaneous
281    /// workspaces/copies), so the shared ledger itself is the policy.
282    pub fn single_materialization_cap_bytes(&self) -> usize {
283        self.ledger.budget_bytes
284    }
285
286    /// Reserve `bytes` against the joint ledger.
287    ///
288    /// On success the returned [`MemoryReservation`] must be held for as long
289    /// as the allocation it accounts for is live; dropping it releases the
290    /// bytes. On failure the caller receives the ledger evidence and is
291    /// expected to fall back to a chunked or matrix-free strategy.
292    pub fn try_reserve(
293        &self,
294        bytes: usize,
295        context: &str,
296    ) -> Result<MemoryReservation, MemoryReservationError> {
297        use std::sync::atomic::Ordering;
298        let mut current = self.ledger.reserved_bytes.load(Ordering::Relaxed);
299        loop {
300            let next = match current.checked_add(bytes) {
301                Some(next) if next <= self.ledger.budget_bytes => next,
302                _ => {
303                    return Err(MemoryReservationError::BudgetExceeded {
304                        context: context.into(),
305                        requested_bytes: bytes,
306                        reserved_bytes: current,
307                        budget_bytes: self.ledger.budget_bytes,
308                        availability: self.ledger.availability.clone(),
309                    });
310                }
311            };
312            match self.ledger.reserved_bytes.compare_exchange_weak(
313                current,
314                next,
315                Ordering::AcqRel,
316                Ordering::Relaxed,
317            ) {
318                Ok(_) => {
319                    return Ok(MemoryReservation {
320                        ledger: Arc::clone(&self.ledger),
321                        bytes,
322                    });
323                }
324                Err(observed) => current = observed,
325            }
326        }
327    }
328
329    /// Reserve the footprint of a dense `nrows × ncols` `f64` matrix.
330    /// Dimension-product overflow is reported as a budget refusal (an
331    /// allocation whose size cannot even be computed certainly does not fit).
332    pub fn try_reserve_dense_f64(
333        &self,
334        nrows: usize,
335        ncols: usize,
336        context: &str,
337    ) -> Result<MemoryReservation, MemoryReservationError> {
338        self.try_reserve_dense_f64_copies(nrows, ncols, 1, context)
339    }
340
341    /// Reserve the predicted live footprint of `copies` simultaneous dense
342    /// matrices with one atomic ledger charge.
343    pub fn try_reserve_dense_f64_copies(
344        &self,
345        nrows: usize,
346        ncols: usize,
347        copies: usize,
348        context: &str,
349    ) -> Result<MemoryReservation, MemoryReservationError> {
350        let bytes = dense_f64_bytes(nrows, ncols)
351            .and_then(|one| one.checked_mul(copies))
352            .ok_or_else(|| MemoryReservationError::SizeOverflow {
353                context: context.into(),
354                nrows,
355                ncols,
356                copies,
357            })?;
358        self.try_reserve(bytes, context)
359    }
360}
361
362/// Checked byte footprint of a dense `nrows × ncols` `f64` matrix.
363pub const fn dense_f64_bytes(nrows: usize, ncols: usize) -> Option<usize> {
364    match nrows.checked_mul(ncols) {
365        Some(cells) => cells.checked_mul(std::mem::size_of::<f64>()),
366        None => None,
367    }
368}
369
370/// RAII guard for bytes reserved on a [`MemoryGovernor`] ledger; dropping it
371/// releases the reservation. Hold it exactly as long as the accounted
372/// allocation is live.
373#[derive(Debug)]
374#[must_use = "dropping a memory reservation immediately releases its ledger charge"]
375pub struct MemoryReservation {
376    ledger: Arc<GovernorLedger>,
377    bytes: usize,
378}
379
380impl MemoryReservation {
381    pub fn bytes(&self) -> usize {
382        self.bytes
383    }
384
385    /// Couple this reservation to the value whose memory it accounts for.
386    pub fn bind<T>(self, value: T) -> Governed<T> {
387        Governed {
388            value,
389            reservation: self,
390        }
391    }
392}
393
394/// A value whose live memory is coupled to a process-wide reservation.
395///
396/// Large fallible materializations return this owner so the allocation cannot
397/// outlive its ledger charge. It dereferences to the wrapped value for normal
398/// ndarray and collection operations.
399#[derive(Debug)]
400#[must_use = "the governed value owns a live process-wide memory reservation"]
401pub struct Governed<T> {
402    value: T,
403    reservation: MemoryReservation,
404}
405
406impl<T> Governed<T> {
407    pub fn reserved_bytes(&self) -> usize {
408        self.reservation.bytes()
409    }
410}
411
412impl<T> std::ops::Deref for Governed<T> {
413    type Target = T;
414
415    fn deref(&self) -> &Self::Target {
416        &self.value
417    }
418}
419
420impl<T> std::ops::DerefMut for Governed<T> {
421    fn deref_mut(&mut self) -> &mut Self::Target {
422        &mut self.value
423    }
424}
425
426impl<T> AsRef<T> for Governed<T> {
427    fn as_ref(&self) -> &T {
428        &self.value
429    }
430}
431
432impl<T> AsMut<T> for Governed<T> {
433    fn as_mut(&mut self) -> &mut T {
434        &mut self.value
435    }
436}
437
438impl Drop for MemoryReservation {
439    fn drop(&mut self) {
440        self.ledger
441            .reserved_bytes
442            .fetch_sub(self.bytes, std::sync::atomic::Ordering::AcqRel);
443    }
444}
445
446/// Hints that flip strict mode on regardless of n/p — used when a code path
447/// is structurally operator-only and any dense fallback would be a bug.
448#[derive(Clone, Copy, Debug, Default)]
449pub struct ProblemHints {
450    pub marginal_slope_large_scale_active: bool,
451}
452
453#[derive(Clone, Copy, Debug, PartialEq, Eq)]
454pub enum DerivativeStorageMode {
455    /// Production exact-math: operator-backed, no dense fallback.
456    AnalyticOperatorRequired,
457    /// Allow dense materialization if under the single-materialization budget.
458    MaterializeIfSmall,
459    /// Dense materialization only permitted for diagnostic code paths.
460    DiagnosticsOnly,
461}
462
463#[derive(Clone, Debug)]
464pub struct MaterializationPolicy {
465    pub max_single_dense_bytes: usize,
466    pub max_cached_dense_bytes: usize,
467    pub row_chunk_target_bytes: usize,
468    pub allow_operator_materialization: bool,
469    pub allow_diagnostic_materialization: bool,
470}
471
472#[derive(Debug, thiserror::Error)]
473pub enum MatrixMaterializationError {
474    #[error(
475        "{context}: dense materialization of {nrows}x{ncols} requires {bytes} bytes (limit {limit_bytes})"
476    )]
477    TooLarge {
478        context: &'static str,
479        nrows: usize,
480        ncols: usize,
481        bytes: usize,
482        limit_bytes: usize,
483    },
484
485    #[error("{context}: operator does not implement chunked row access")]
486    MissingRowChunk { context: &'static str },
487
488    #[error("{context}: row materialization failed: {reason}")]
489    RowMaterializationFailed {
490        context: &'static str,
491        reason: String,
492    },
493
494    #[error("{context}: materialization forbidden by policy (mode={mode:?})")]
495    Forbidden {
496        context: &'static str,
497        mode: DerivativeStorageMode,
498    },
499
500    /// The process-wide [`MemoryGovernor`] could not reserve the requested live
501    /// footprint (or its checked byte size overflowed). Callers route to a
502    /// chunked or matrix-free strategy.
503    #[error(transparent)]
504    Reservation(#[from] MemoryReservationError),
505}
506
507pub trait ResidentBytes {
508    fn resident_bytes(&self) -> usize;
509}
510
511impl ResourcePolicy {
512    /// Conservative default suitable for general-purpose use.
513    ///
514    /// Uses `MaterializeIfSmall`: dense materialization is allowed only when
515    /// the matrix fits under `max_single_materialization_bytes`. This lets
516    /// small-data families that lack an implicit operator work out of the box,
517    /// while problems whose dense footprint does not fit real memory get a
518    /// typed refusal that forces the analytic-operator path. Set
519    /// `derivative_storage_mode = AnalyticOperatorRequired` explicitly to
520    /// reject all dense fallback.
521    ///
522    /// Scalar caps expose the governor's full allowance; they are admission
523    /// hints, not independent budgets. Actual materializations and caches must
524    /// reserve their complete live footprint against the shared governor, so
525    /// any combination of categories is bounded by one ledger.
526    pub fn default_library() -> Self {
527        let governor = MemoryGovernor::global();
528        let single_cap = governor.single_materialization_cap_bytes();
529        Self {
530            max_single_materialization_bytes: single_cap,
531            max_operator_cache_bytes: single_cap,
532            max_spatial_distance_cache_bytes: single_cap,
533            max_owned_data_cache_bytes: single_cap,
534            row_chunk_target_bytes: LIBRARY_ROW_CHUNK_TARGET_BYTES,
535            derivative_storage_mode: DerivativeStorageMode::MaterializeIfSmall,
536        }
537    }
538
539    /// Strict mode that rejects every dense fallback. Use when you intend to
540    /// run only on operator-backed bases (large-scale Duchon/TPS, exact
541    /// GAMLSS marginal slope, CTN, etc.). The byte caps only govern the
542    /// residual diagnostic surfaces (materialization itself is forbidden by
543    /// the mode).
544    pub fn analytic_operator_required() -> Self {
545        let base = Self::default_library();
546        Self {
547            derivative_storage_mode: DerivativeStorageMode::AnalyticOperatorRequired,
548            ..base
549        }
550    }
551
552    /// Auto-derive the resource policy from the shape of the problem rather
553    /// than from an explicit CLI flag.
554    ///
555    /// Shape alone never flips a policy mode: doing so merely moves the old
556    /// row/column cliff to a byte threshold. Every non-structural path starts
557    /// permissive and makes its strategy decision from the operation's checked
558    /// predicted live bytes versus the governor's current remaining budget.
559    ///
560    /// `hints.marginal_slope_large_scale_active` forces strict mode regardless
561    /// of shape: that path is structurally operator-only and any dense
562    /// fallback would be a bug, not a memory question.
563    pub fn for_problem(hints: ProblemHints) -> Self {
564        if hints.marginal_slope_large_scale_active {
565            return Self::analytic_operator_required();
566        }
567        Self::default_library()
568    }
569
570    /// Permissive mode for small-data usage and tests. Admission still uses
571    /// the same process ledger; only the streaming chunk geometry differs.
572    pub fn permissive_small_data() -> Self {
573        let base = Self::default_library();
574        Self {
575            row_chunk_target_bytes: 64 * 1024 * 1024,
576            ..base
577        }
578    }
579
580    pub const fn material_policy(&self) -> MaterializationPolicy {
581        MaterializationPolicy {
582            max_single_dense_bytes: self.max_single_materialization_bytes,
583            max_cached_dense_bytes: self.max_operator_cache_bytes,
584            row_chunk_target_bytes: self.row_chunk_target_bytes,
585            allow_operator_materialization: matches!(
586                self.derivative_storage_mode,
587                DerivativeStorageMode::MaterializeIfSmall
588            ),
589            allow_diagnostic_materialization: !matches!(
590                self.derivative_storage_mode,
591                DerivativeStorageMode::AnalyticOperatorRequired
592            ),
593        }
594    }
595}
596
597/// Returns how many rows to stream per chunk so that each chunk uses approximately
598/// `target_bytes` given a row width of `cols` f64 entries.
599pub const fn rows_for_target_bytes(target_bytes: usize, cols: usize) -> usize {
600    let raw_bytes_per_row = cols.saturating_mul(std::mem::size_of::<f64>());
601    let bytes_per_row = if raw_bytes_per_row == 0 {
602        1
603    } else {
604        raw_bytes_per_row
605    };
606    let rows = target_bytes / bytes_per_row;
607    if rows == 0 { 1 } else { rows }
608}
609
610/// Select the row count for prediction-time covariance work.
611///
612/// A prediction row can keep roughly four `parameter_dim × local_dim`
613/// `f64` workspaces live while gradients and covariance solves are assembled.
614/// This central policy prevents predictors from drifting to different memory
615/// budgets and chunk bounds for the same operation.
616pub fn prediction_chunk_rows(parameter_dim: usize, local_dim: usize, total_rows: usize) -> usize {
617    const MIN_ROWS: usize = 16;
618    const MAX_ROWS: usize = 4096;
619
620    if total_rows == 0 {
621        return 1;
622    }
623    let live_f64_values_per_row = parameter_dim
624        .max(1)
625        .saturating_mul(local_dim.max(1))
626        .saturating_mul(4);
627    rows_for_target_bytes(
628        ResourcePolicy::default_library().row_chunk_target_bytes,
629        live_f64_values_per_row,
630    )
631    .clamp(MIN_ROWS, MAX_ROWS)
632    .min(total_rows)
633}
634
635use std::collections::{HashMap, VecDeque};
636use std::hash::{Hash, Hasher};
637use std::sync::{Arc, Mutex, OnceLock};
638
639/// Byte-limited LRU cache with an optional entry cap.
640///
641/// Unlike an entry-count-limited LRU, this cache tracks the resident byte cost
642/// of each value (via [`ResidentBytes`]) and evicts the least-recently-used
643/// entries until the total resident bytes fit under `max_bytes`. This is the
644/// correct policy for large-scale payloads where a single cache entry (e.g.
645/// an n*K distance matrix) can itself be multiple gigabytes and an entry-count
646/// cap would silently blow the memory budget. Small entry caps are still useful
647/// for payloads with known shape, such as owned PC data matrices shared across
648/// model blocks.
649pub struct ByteLruCache<K: Eq + Hash + Clone, V> {
650    /// One independent LRU partition per shard. A single shard (the default)
651    /// is byte-for-byte equivalent to the original single-`Mutex` cache; with
652    /// `shard_count > 1` the key hash selects the shard, so concurrent traffic
653    /// on distinct keys contends `1/shard_count` as often and each shard's
654    /// recency `VecDeque` is `1/shard_count` as long (the hit-path rescan is a
655    /// linear `position` lookup, so shrinking the per-shard order also cuts
656    /// per-access cost). Sharding is opt-in (`new_sharded`) precisely because
657    /// the byte budget is split across shards — that is correct for caches of
658    /// many small entries (e.g. cell-moment memos) but wrong for caches of a
659    /// few multi-GiB entries (distance matrices), which keep `shard_count == 1`.
660    shards: Box<[Mutex<ByteLruInner<K, V>>]>,
661    /// Per-shard byte budget. `shard_bytes * shards.len() >= max_bytes`.
662    shard_bytes: usize,
663    /// Per-shard entry budget, if any (`0` disables caching, as before).
664    shard_entries: Option<usize>,
665    max_bytes: usize,
666    governor: MemoryGovernor,
667}
668
669struct ByteLruInner<K, V> {
670    // The reservation is stored beside the value, so eviction and clear drop
671    // the process-wide charge at exactly the same time as cache ownership.
672    map: HashMap<K, (V, usize, MemoryReservation)>,
673    order: VecDeque<K>,
674    resident_bytes: usize,
675}
676
677impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> ByteLruCache<K, V> {
678    pub fn new(max_bytes: usize) -> Self {
679        Self::build(max_bytes, None, 1)
680    }
681
682    pub fn with_max_entries(max_bytes: usize, max_entries: usize) -> Self {
683        Self::build(max_bytes, Some(max_entries), 1)
684    }
685
686    /// Like [`new`](Self::new) but partitions the cache across `shard_count`
687    /// independently-locked LRU shards to cut lock contention under heavy
688    /// concurrent access. The byte budget is divided evenly across shards, so
689    /// this is only appropriate for caches holding many small entries.
690    pub fn new_sharded(max_bytes: usize, shard_count: usize) -> Self {
691        Self::build(max_bytes, None, shard_count)
692    }
693
694    /// Like [`with_max_entries`](Self::with_max_entries) but sharded; see
695    /// [`new_sharded`](Self::new_sharded).
696    pub fn with_max_entries_sharded(
697        max_bytes: usize,
698        max_entries: usize,
699        shard_count: usize,
700    ) -> Self {
701        Self::build(max_bytes, Some(max_entries), shard_count)
702    }
703
704    fn build(max_bytes: usize, max_entries: Option<usize>, shard_count: usize) -> Self {
705        Self::build_with_governor(
706            max_bytes,
707            max_entries,
708            shard_count,
709            MemoryGovernor::global().clone(),
710        )
711    }
712
713    fn build_with_governor(
714        max_bytes: usize,
715        max_entries: Option<usize>,
716        shard_count: usize,
717        governor: MemoryGovernor,
718    ) -> Self {
719        let shard_count = shard_count.max(1);
720        // Split the global budgets across shards, rounding up so the aggregate
721        // capacity never falls below the requested budget. With a single shard
722        // these equal the global budgets exactly (legacy behavior). A `0`
723        // entry budget still disables caching and must not be rounded up to 1.
724        let shard_bytes = max_bytes.div_ceil(shard_count);
725        let shard_entries = max_entries.map(|m| {
726            if m == 0 {
727                0
728            } else {
729                m.div_ceil(shard_count).max(1)
730            }
731        });
732        let shards = (0..shard_count)
733            .map(|_| {
734                Mutex::new(ByteLruInner {
735                    map: HashMap::new(),
736                    order: VecDeque::new(),
737                    resident_bytes: 0,
738                })
739            })
740            .collect::<Vec<_>>()
741            .into_boxed_slice();
742        Self {
743            shards,
744            shard_bytes,
745            shard_entries,
746            max_bytes,
747            governor,
748        }
749    }
750
751    #[inline]
752    fn shard(&self, key: &K) -> &Mutex<ByteLruInner<K, V>> {
753        if self.shards.len() == 1 {
754            return &self.shards[0];
755        }
756        let mut hasher = std::collections::hash_map::DefaultHasher::new();
757        key.hash(&mut hasher);
758        &self.shards[(hasher.finish() as usize) % self.shards.len()]
759    }
760
761    pub fn get(&self, key: &K) -> Option<V> {
762        // recover from poison
763        let mut g = self.shard(key).lock().unwrap_or_else(|p| p.into_inner());
764        let v = g.map.get(key)?.0.clone();
765        // move to back (most-recently-used)
766        if let Some(pos) = g.order.iter().position(|k| k == key) {
767            let k = g.order.remove(pos).unwrap();
768            g.order.push_back(k);
769        }
770        Some(v)
771    }
772
773    pub fn insert(&self, key: K, value: V) {
774        let charge = value.resident_bytes();
775        let mut g = self.shard(&key).lock().unwrap_or_else(|p| p.into_inner());
776
777        // If already present, remove the old entry first so resident bytes stay
778        // accurate and the LRU ordering reflects this insertion.
779        if let Some((_old, old_charge, _reservation)) = g.map.remove(&key) {
780            g.resident_bytes = g.resident_bytes.saturating_sub(old_charge);
781            if let Some(pos) = g.order.iter().position(|k| k == &key) {
782                g.order.remove(pos);
783            }
784        }
785
786        if charge > self.shard_bytes {
787            // Too large to cache; skip insertion.
788            return;
789        }
790
791        if let Some(max_entries) = self.shard_entries {
792            if max_entries == 0 {
793                return;
794            }
795            while g.map.len() >= max_entries {
796                if let Some(evict_key) = g.order.pop_front() {
797                    if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
798                        g.resident_bytes = g.resident_bytes.saturating_sub(c);
799                    }
800                } else {
801                    break;
802                }
803            }
804        }
805
806        while g.resident_bytes + charge > self.shard_bytes {
807            if let Some(evict_key) = g.order.pop_front() {
808                if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
809                    g.resident_bytes = g.resident_bytes.saturating_sub(c);
810                }
811            } else {
812                break;
813            }
814        }
815
816        let reservation = match self
817            .governor
818            .try_reserve(charge, "ByteLruCache resident entry")
819        {
820            Ok(reservation) => reservation,
821            Err(_) => return,
822        };
823        g.map.insert(key.clone(), (value, charge, reservation));
824        g.order.push_back(key);
825        g.resident_bytes = g.resident_bytes.saturating_add(charge);
826    }
827
828    pub fn resident_bytes(&self) -> usize {
829        self.shards
830            .iter()
831            .map(|shard| {
832                shard
833                    .lock()
834                    .unwrap_or_else(|p| p.into_inner())
835                    .resident_bytes
836            })
837            .sum()
838    }
839
840    pub const fn max_bytes(&self) -> usize {
841        self.max_bytes
842    }
843
844    pub fn len(&self) -> usize {
845        self.shards
846            .iter()
847            .map(|shard| shard.lock().unwrap_or_else(|p| p.into_inner()).map.len())
848            .sum()
849    }
850
851    pub fn is_empty(&self) -> bool {
852        self.len() == 0
853    }
854
855    pub fn clear(&self) {
856        for shard in self.shards.iter() {
857            let mut g = shard.lock().unwrap_or_else(|p| p.into_inner());
858            g.map.clear();
859            g.order.clear();
860            g.resident_bytes = 0;
861        }
862    }
863}
864
865impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> std::fmt::Debug for ByteLruCache<K, V> {
866    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867        f.debug_struct("ByteLruCache")
868            .field("resident_bytes", &self.resident_bytes())
869            .field("max_bytes", &self.max_bytes)
870            .field("shard_count", &self.shards.len())
871            .field("shard_bytes", &self.shard_bytes)
872            .field("shard_entries", &self.shard_entries)
873            .finish()
874    }
875}
876
877/// Byte-accounting for `Arc<Array2<f64>>`.
878///
879/// Reports the full dense footprint of the owned array. Multiple `Arc`s
880/// pointing to the same allocation will each report the full size; this is
881/// the conservative accounting the caches want because a single residency in
882/// the cache is what we are budgeting for.
883impl ResidentBytes for Arc<ndarray::Array2<f64>> {
884    fn resident_bytes(&self) -> usize {
885        std::mem::size_of::<f64>()
886            .saturating_mul(self.nrows())
887            .saturating_mul(self.ncols())
888    }
889}
890
891/// Lazy-init cache safe to call from inside rayon par_iter.
892///
893/// `std::sync::OnceLock::get_or_init` parks racing threads on an OS
894/// condition variable until the leader's init closure finishes. If the
895/// leader's init closure itself dispatches a nested `into_par_iter`, the
896/// parked threads are now unavailable as rayon workers, and the leader
897/// blocks waiting for chunks that no one can service. Classic deadlock.
898///
899/// `RayonSafeOnce` removes the trap by computing the value *outside* any
900/// lock. Concurrent racers may produce duplicate values; the first to
901/// publish wins, the rest drop their result. No thread ever parks waiting
902/// for another thread's init to finish, so nested rayon par_iter inside
903/// the init closure is safe.
904///
905/// Use this in place of `OnceLock` whenever the init closure transitively
906/// runs rayon work *and* the cache may be entered concurrently from
907/// inside another rayon par_iter. The redundant-work cost on first race
908/// is the price for never deadlocking; in practice the loser threads
909/// throw away one round of work and steady-state is identical to
910/// `OnceLock`.
911pub struct RayonSafeOnce<T> {
912    slot: std::sync::OnceLock<T>,
913}
914
915impl<T> RayonSafeOnce<T> {
916    pub const fn new() -> Self {
917        Self {
918            slot: std::sync::OnceLock::new(),
919        }
920    }
921
922    /// Returns the cached value if already populated.
923    #[inline]
924    pub fn get(&self) -> Option<&T> {
925        self.slot.get()
926    }
927
928    /// Returns the cached value, computing it if absent.
929    ///
930    /// The init closure runs WITHOUT holding any lock — calls from
931    /// concurrent rayon workers may all run it, and all but the first
932    /// to call `set` discard their result. This is the contract that
933    /// keeps nested `into_par_iter` inside `init` from deadlocking on
934    /// other workers parked on a `OnceLock`.
935    ///
936    /// Named `get_or_compute` (not `get_or_init`) so the codebase-level
937    /// lint that bans `OnceLock::get_or_init` near rayon `par_iter` does
938    /// not flag this safe-by-construction path.
939    pub fn get_or_compute<F>(&self, init: F) -> &T
940    where
941        F: FnOnce() -> T,
942    {
943        if let Some(v) = self.slot.get() {
944            return v;
945        }
946        let candidate = init();
947        self.slot.set(candidate).ok();
948        self.slot
949            .get()
950            .expect("RayonSafeOnce slot populated by set() above")
951    }
952}
953
954impl<T> Default for RayonSafeOnce<T> {
955    fn default() -> Self {
956        Self::new()
957    }
958}
959
960impl<T: Clone> Clone for RayonSafeOnce<T> {
961    fn clone(&self) -> Self {
962        let cloned = Self::new();
963        if let Some(value) = self.slot.get() {
964            cloned.slot.set(value.clone()).ok();
965        }
966        cloned
967    }
968}
969
970impl<T: std::fmt::Debug> std::fmt::Debug for RayonSafeOnce<T> {
971    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
972        f.debug_struct("RayonSafeOnce")
973            .field("slot", &self.slot.get())
974            .finish()
975    }
976}
977
978#[cfg(test)]
979mod byte_lru_tests {
980    use super::*;
981
982    fn cache_test_governor(budget_bytes: usize) -> MemoryGovernor {
983        let available_bytes = (budget_bytes as u128 * GOVERNOR_BUDGET_DENOMINATOR)
984            .div_ceil(GOVERNOR_BUDGET_NUMERATOR);
985        MemoryGovernor::with_detected_availability(MemoryAvailability::from_observation(
986            u64::try_from(available_bytes).expect("test cache budget must fit in u64"),
987            CgroupMemoryObservation::NotPresent,
988        ))
989    }
990
991    /// Fixed-charge value so byte-budget arithmetic in the tests is exact.
992    #[derive(Clone, PartialEq, Debug)]
993    struct Payload(u64);
994    impl ResidentBytes for Payload {
995        fn resident_bytes(&self) -> usize {
996            8
997        }
998    }
999
1000    #[test]
1001    fn single_shard_round_trips_and_evicts_by_bytes() {
1002        // 3 entries' worth of budget; a single shard preserves strict global LRU.
1003        let cache: ByteLruCache<u64, Payload> =
1004            ByteLruCache::build_with_governor(24, None, 1, cache_test_governor(24));
1005        for k in 0..3 {
1006            cache.insert(k, Payload(k));
1007        }
1008        assert_eq!(cache.len(), 3);
1009        assert_eq!(cache.resident_bytes(), 24);
1010        // Touch key 0 so it is most-recently-used, then overflow by one.
1011        assert_eq!(cache.get(&0), Some(Payload(0)));
1012        cache.insert(3, Payload(3));
1013        // Key 1 (now least-recently-used) is evicted; 0 survives the touch.
1014        assert_eq!(cache.len(), 3);
1015        assert_eq!(cache.get(&1), None);
1016        assert_eq!(cache.get(&0), Some(Payload(0)));
1017        assert_eq!(cache.get(&3), Some(Payload(3)));
1018    }
1019
1020    #[test]
1021    fn zero_entry_budget_disables_caching_in_every_shard() {
1022        let single: ByteLruCache<u64, Payload> = ByteLruCache::with_max_entries(1 << 20, 0);
1023        single.insert(7, Payload(7));
1024        assert_eq!(single.get(&7), None);
1025        let sharded: ByteLruCache<u64, Payload> =
1026            ByteLruCache::with_max_entries_sharded(1 << 20, 0, 16);
1027        sharded.insert(7, Payload(7));
1028        assert_eq!(sharded.get(&7), None);
1029    }
1030
1031    #[test]
1032    fn sharded_cache_retrieves_all_keys_and_respects_aggregate_budget() {
1033        // Generous budget split across 8 shards; every inserted key must be
1034        // retrievable and the aggregate residency must never exceed the global
1035        // budget (shard_bytes * shard_count, rounded up).
1036        let shard_count = 8usize;
1037        let max_bytes = 8 * 64; // 64 entries' worth, 8 per shard on average.
1038        let cache: ByteLruCache<u64, Payload> = ByteLruCache::build_with_governor(
1039            max_bytes,
1040            None,
1041            shard_count,
1042            cache_test_governor(max_bytes),
1043        );
1044        for k in 0..64u64 {
1045            cache.insert(k, Payload(k));
1046        }
1047        // Per-shard budgets sum to >= the requested global budget.
1048        assert!(cache.resident_bytes() <= max_bytes.div_ceil(shard_count) * shard_count);
1049        // Re-inserting then reading back a key returns the stored payload.
1050        cache.insert(123, Payload(123));
1051        assert_eq!(cache.get(&123), Some(Payload(123)));
1052        assert!(!cache.is_empty());
1053        cache.clear();
1054        assert_eq!(cache.len(), 0);
1055        assert_eq!(cache.resident_bytes(), 0);
1056    }
1057}
1058
1059#[cfg(test)]
1060mod resource_policy_tests {
1061    use super::*;
1062
1063    fn test_governor(budget_bytes: usize) -> MemoryGovernor {
1064        let available_bytes = (budget_bytes as u128 * GOVERNOR_BUDGET_DENOMINATOR)
1065            .div_ceil(GOVERNOR_BUDGET_NUMERATOR);
1066        let availability = MemoryAvailability::from_observation(
1067            u64::try_from(available_bytes).expect("test budget has a representable observation"),
1068            CgroupMemoryObservation::NotPresent,
1069        );
1070        let governor = MemoryGovernor::with_detected_availability(availability);
1071        assert_eq!(governor.budget_bytes(), budget_bytes);
1072        governor
1073    }
1074
1075    // ── rows_for_target_bytes ─────────────────────────────────────────────────
1076
1077    #[test]
1078    fn rows_for_target_bytes_exact_fit() {
1079        // 1 col × 8 bytes/f64; target 8 bytes → 1 row
1080        assert_eq!(rows_for_target_bytes(8, 1), 1);
1081    }
1082
1083    #[test]
1084    fn rows_for_target_bytes_multiple_rows() {
1085        // 1 col × 8 bytes/f64; target 80 bytes → 10 rows
1086        assert_eq!(rows_for_target_bytes(80, 1), 10);
1087    }
1088
1089    #[test]
1090    fn rows_for_target_bytes_multiple_cols() {
1091        // 4 cols × 8 = 32 bytes/row; target 128 → 4 rows
1092        assert_eq!(rows_for_target_bytes(128, 4), 4);
1093    }
1094
1095    #[test]
1096    fn rows_for_target_bytes_zero_target_returns_one() {
1097        // Zero target cannot give 0 rows — floor to 1
1098        assert_eq!(rows_for_target_bytes(0, 1), 1);
1099    }
1100
1101    #[test]
1102    fn rows_for_target_bytes_zero_cols_returns_non_zero() {
1103        // Zero cols → bytes_per_row falls back to 1 → rows = target
1104        assert_eq!(rows_for_target_bytes(100, 0), 100);
1105    }
1106
1107    #[test]
1108    fn rows_for_target_bytes_large_target() {
1109        // 8 MiB target, 1024 cols → 1024 bytes/row → 8192 rows
1110        let target = 8 * 1024 * 1024;
1111        let cols = 1024_usize;
1112        let expected = target / (cols * std::mem::size_of::<f64>());
1113        assert_eq!(rows_for_target_bytes(target, cols), expected);
1114    }
1115
1116    #[test]
1117    fn prediction_chunks_share_the_runtime_byte_budget() {
1118        assert_eq!(prediction_chunk_rows(1024, 1, 100_000), 256);
1119        assert_eq!(prediction_chunk_rows(32, 2, 100_000), 4096);
1120    }
1121
1122    #[test]
1123    fn prediction_chunks_respect_dataset_bounds() {
1124        assert_eq!(prediction_chunk_rows(1, 1, 7), 7);
1125        assert_eq!(prediction_chunk_rows(1, 1, 0), 1);
1126    }
1127
1128    // ── ResourcePolicy::for_problem ──────────────────────────────────────────
1129
1130    #[test]
1131    fn for_problem_small_data_uses_materialize_if_small() {
1132        let p = ResourcePolicy::for_problem(ProblemHints::default());
1133        assert_eq!(
1134            p.derivative_storage_mode,
1135            DerivativeStorageMode::MaterializeIfSmall
1136        );
1137    }
1138
1139    #[test]
1140    fn for_problem_has_no_row_or_column_cliff() {
1141        let narrow = ResourcePolicy::for_problem(ProblemHints::default());
1142        let wide = ResourcePolicy::for_problem(ProblemHints::default());
1143        assert_eq!(
1144            narrow.derivative_storage_mode,
1145            DerivativeStorageMode::MaterializeIfSmall
1146        );
1147        assert_eq!(
1148            wide.derivative_storage_mode,
1149            DerivativeStorageMode::MaterializeIfSmall
1150        );
1151    }
1152
1153    #[test]
1154    fn for_problem_dimension_overflow_defers_to_typed_reservation() {
1155        let policy = ResourcePolicy::for_problem(ProblemHints::default());
1156        assert_eq!(
1157            policy.derivative_storage_mode,
1158            DerivativeStorageMode::MaterializeIfSmall
1159        );
1160    }
1161
1162    #[test]
1163    fn for_problem_marginal_slope_hint_is_strict() {
1164        let p = ResourcePolicy::for_problem(ProblemHints {
1165            marginal_slope_large_scale_active: true,
1166        });
1167        assert_eq!(
1168            p.derivative_storage_mode,
1169            DerivativeStorageMode::AnalyticOperatorRequired
1170        );
1171    }
1172
1173    // ── ResourcePolicy::material_policy ─────────────────────────────────────
1174
1175    #[test]
1176    fn material_policy_default_library_allows_operator_and_diagnostics() {
1177        let mp = ResourcePolicy::default_library().material_policy();
1178        assert!(mp.allow_operator_materialization);
1179        assert!(mp.allow_diagnostic_materialization);
1180    }
1181
1182    #[test]
1183    fn material_policy_analytic_operator_required_blocks_both() {
1184        let mp = ResourcePolicy::analytic_operator_required().material_policy();
1185        assert!(!mp.allow_operator_materialization);
1186        assert!(!mp.allow_diagnostic_materialization);
1187    }
1188
1189    #[test]
1190    fn material_policy_propagates_byte_limits() {
1191        let policy = ResourcePolicy::default_library();
1192        let mp = policy.material_policy();
1193        assert_eq!(
1194            mp.max_single_dense_bytes,
1195            policy.max_single_materialization_bytes
1196        );
1197        assert_eq!(mp.max_cached_dense_bytes, policy.max_operator_cache_bytes);
1198        assert_eq!(mp.row_chunk_target_bytes, policy.row_chunk_target_bytes);
1199    }
1200
1201    // ── MemoryGovernor ledger ────────────────────────────────────────────────
1202
1203    #[test]
1204    fn reservations_account_and_release_on_drop() {
1205        let governor = test_governor(1_000);
1206        assert_eq!(governor.remaining_bytes(), 1_000);
1207        let first = governor.try_reserve(600, "test-first").expect("fits");
1208        assert_eq!(governor.reserved_bytes(), 600);
1209        assert_eq!(governor.remaining_bytes(), 400);
1210        assert_eq!(first.bytes(), 600);
1211        drop(first);
1212        assert_eq!(governor.reserved_bytes(), 0);
1213        assert_eq!(governor.remaining_bytes(), 1_000);
1214    }
1215
1216    #[test]
1217    fn jointly_excessive_reservations_are_refused_with_evidence() {
1218        // Two allocations that each fit alone must not be jointly grantable —
1219        // this is exactly the independent-budgets failure the ledger exists
1220        // to prevent.
1221        let governor = test_governor(1_000);
1222        let availability = governor.availability();
1223        let held = governor.try_reserve(600, "test-held").expect("fits alone");
1224        let refusal = governor
1225            .try_reserve(600, "test-joint")
1226            .expect_err("600 + 600 exceeds the 1000-byte budget");
1227        assert_eq!(
1228            refusal,
1229            MemoryReservationError::BudgetExceeded {
1230                context: "test-joint".into(),
1231                requested_bytes: 600,
1232                reserved_bytes: 600,
1233                budget_bytes: 1_000,
1234                availability,
1235            }
1236        );
1237        // After releasing the holder, the same request succeeds: refusal is a
1238        // routing signal, not a terminal state.
1239        drop(held);
1240        let refreshed = governor
1241            .try_reserve(600, "test-joint")
1242            .expect("fits after release");
1243        assert_eq!(refreshed.bytes(), 600);
1244    }
1245
1246    #[test]
1247    fn dense_reservation_uses_checked_footprint() {
1248        let governor = test_governor(1 << 20);
1249        let ok = governor
1250            .try_reserve_dense_f64(1024, 64, "test-dense")
1251            .expect("512 KiB fits in 1 MiB");
1252        assert_eq!(ok.bytes(), 1024 * 64 * 8);
1253        drop(ok);
1254        // Dimension-product overflow must refuse, never wrap into a tiny
1255        // spurious reservation.
1256        governor
1257            .try_reserve_dense_f64(usize::MAX, 2, "test-overflow")
1258            .expect_err("overflowing footprint cannot be reserved");
1259        assert!(matches!(
1260            governor.try_reserve_dense_f64(usize::MAX, 2, "test-overflow"),
1261            Err(MemoryReservationError::SizeOverflow { .. })
1262        ));
1263    }
1264
1265    #[test]
1266    fn concurrent_reservations_never_oversubscribe() {
1267        let governor = std::sync::Arc::new(test_governor(1_000));
1268        let granted = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1269        let barrier = std::sync::Arc::new(std::sync::Barrier::new(9));
1270        std::thread::scope(|scope| {
1271            for _ in 0..8 {
1272                let governor = std::sync::Arc::clone(&governor);
1273                let granted = std::sync::Arc::clone(&granted);
1274                let barrier = std::sync::Arc::clone(&barrier);
1275                scope.spawn(move || {
1276                    let held = governor.try_reserve(200, "test-race").ok();
1277                    if held.is_some() {
1278                        granted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1279                    }
1280                    barrier.wait();
1281                    assert!(governor.reserved_bytes() <= governor.budget_bytes());
1282                    barrier.wait();
1283                    drop(held);
1284                });
1285            }
1286            barrier.wait();
1287            assert_eq!(granted.load(std::sync::atomic::Ordering::SeqCst), 5);
1288            assert_eq!(governor.reserved_bytes(), 1_000);
1289            barrier.wait();
1290        });
1291        assert_eq!(governor.reserved_bytes(), 0);
1292    }
1293
1294    #[test]
1295    fn global_policy_caps_are_one_shared_admission_ceiling() {
1296        let governor = MemoryGovernor::global();
1297        assert_eq!(
1298            governor.single_materialization_cap_bytes(),
1299            governor.budget_bytes()
1300        );
1301        let policy = ResourcePolicy::default_library();
1302        assert_eq!(
1303            policy.max_single_materialization_bytes,
1304            governor.single_materialization_cap_bytes()
1305        );
1306        let strict = ResourcePolicy::analytic_operator_required();
1307        assert_eq!(
1308            strict.max_single_materialization_bytes,
1309            policy.max_single_materialization_bytes
1310        );
1311    }
1312
1313    #[test]
1314    fn governed_value_holds_and_releases_its_charge() {
1315        let governor = test_governor(64);
1316        let governed = governor
1317            .try_reserve(32, "governed-value")
1318            .expect("reservation fits")
1319            .bind(vec![0_u8; 32]);
1320        assert_eq!(governed.len(), 32);
1321        assert_eq!(governed.reserved_bytes(), 32);
1322        assert_eq!(governor.reserved_bytes(), 32);
1323        drop(governed);
1324        assert_eq!(governor.reserved_bytes(), 0);
1325    }
1326
1327    #[test]
1328    fn memory_availability_distinguishes_host_cgroup_and_exhaustion() {
1329        let host_only = MemoryAvailability::from_observation(
1330            1_000,
1331            CgroupMemoryObservation::NotPresent,
1332        );
1333        assert_eq!(host_only.available_bytes(), 1_000);
1334        assert_eq!(host_only.limiting_source(), MemoryAvailabilitySource::Host);
1335        assert_eq!(governor_budget_from_availability(&host_only), 750);
1336
1337        let exhausted_host =
1338            MemoryAvailability::from_observation(0, CgroupMemoryObservation::NotPresent);
1339        assert_eq!(exhausted_host.available_bytes(), 0);
1340        assert_eq!(governor_budget_from_availability(&exhausted_host), 0);
1341
1342        let finite_cgroup = MemoryAvailability::from_observation(
1343            1_000,
1344            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1345                "/fixture/leaf",
1346                600,
1347                200,
1348                0,
1349                1,
1350            )),
1351        );
1352        assert_eq!(finite_cgroup.available_bytes(), 400);
1353        assert_eq!(
1354            finite_cgroup.limiting_source(),
1355            MemoryAvailabilitySource::Cgroup
1356        );
1357        assert_eq!(governor_budget_from_availability(&finite_cgroup), 300);
1358
1359        let exhausted_cgroup = MemoryAvailability::from_observation(
1360            1_000,
1361            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1362                "/fixture/leaf",
1363                600,
1364                600,
1365                0,
1366                1,
1367            )),
1368        );
1369        assert_eq!(exhausted_cgroup.available_bytes(), 0);
1370        assert_eq!(
1371            exhausted_cgroup.limiting_source(),
1372            MemoryAvailabilitySource::Cgroup
1373        );
1374        assert_eq!(governor_budget_from_availability(&exhausted_cgroup), 0);
1375
1376        // A finite cgroup with more headroom than the host remains visible as
1377        // provenance, but the host is the binding observation.
1378        let host_is_tighter = MemoryAvailability::from_observation(
1379            1_000,
1380            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1381                "/fixture/leaf",
1382                8_000,
1383                2_000,
1384                0,
1385                1,
1386            )),
1387        );
1388        assert_eq!(host_is_tighter.available_bytes(), 1_000);
1389        assert_eq!(
1390            host_is_tighter.limiting_source(),
1391            MemoryAvailabilitySource::Host
1392        );
1393
1394        let equal_cgroup_ceiling = MemoryAvailability::from_observation(
1395            1_000,
1396            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1397                "/fixture/leaf",
1398                1_200,
1399                200,
1400                0,
1401                1,
1402            )),
1403        );
1404        assert_eq!(equal_cgroup_ceiling.available_bytes(), 1_000);
1405        assert_eq!(
1406            equal_cgroup_ceiling.limiting_source(),
1407            MemoryAvailabilitySource::HostAndCgroup
1408        );
1409
1410        let both_exhausted = MemoryAvailability::from_observation(
1411            0,
1412            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1413                "/fixture/leaf",
1414                600,
1415                600,
1416                0,
1417                1,
1418            )),
1419        );
1420        assert_eq!(
1421            both_exhausted.limiting_source(),
1422            MemoryAvailabilitySource::HostAndCgroup
1423        );
1424
1425        // A numeric cgroup-v2 `memory.max = 0` is distinct from the literal
1426        // `max` token and remains an authoritative hard-zero ceiling.
1427        let zero_ceiling = MemoryAvailability::from_observation(
1428            8_000,
1429            CgroupMemoryObservation::V2Limited(CgroupMemoryAvailability::fixture(
1430                "/fixture/leaf",
1431                0,
1432                0,
1433                0,
1434                1,
1435            )),
1436        );
1437        assert_eq!(zero_ceiling.available_bytes(), 0);
1438        assert_eq!(
1439            zero_ceiling.limiting_source(),
1440            MemoryAvailabilitySource::Cgroup
1441        );
1442        assert_eq!(governor_budget_from_availability(&zero_ceiling), 0);
1443    }
1444
1445    #[test]
1446    fn literal_unlimited_cgroup_defers_to_host_available_memory_2317() {
1447        let unlimited = MemoryAvailability::from_observation(
1448            2_430_926_848,
1449            CgroupMemoryObservation::V2Unbounded {
1450                cgroup_path: "/fixture/leaf".into(),
1451                inspected_levels: 3,
1452            },
1453        );
1454        assert_eq!(unlimited.available_bytes(), 2_430_926_848);
1455        assert_eq!(unlimited.limiting_source(), MemoryAvailabilitySource::Host);
1456        assert_eq!(
1457            governor_budget_from_availability(&unlimited),
1458            1_823_195_136
1459        );
1460        assert!(format!("{unlimited}").contains("unbounded cgroup-v2"));
1461    }
1462
1463    #[test]
1464    fn finite_cgroup_v1_headroom_participates_in_the_same_exact_minimum() {
1465        let availability = MemoryAvailability::from_observation(
1466            8_000,
1467            CgroupMemoryObservation::V1Limited(CgroupMemoryAvailability::fixture(
1468                "/sys/fs/cgroup/memory/slurm/job",
1469                4_000,
1470                1_500,
1471                500,
1472                4,
1473            )),
1474        );
1475        assert_eq!(availability.available_bytes(), 3_000);
1476        assert_eq!(
1477            availability.limiting_source(),
1478            MemoryAvailabilitySource::Cgroup
1479        );
1480        assert_eq!(governor_budget_from_availability(&availability), 2_250);
1481        let evidence = format!("{availability}");
1482        assert!(evidence.contains("cgroup-v1"));
1483        assert!(evidence.contains("available=3000"));
1484    }
1485
1486    #[test]
1487    fn malformed_active_cgroup_fails_closed_with_typed_evidence() {
1488        let availability = MemoryAvailability::from_observation(
1489            8_000,
1490            CgroupMemoryObservation::ProbeFailed(CgroupMemoryProbeFailure::fixture(
1491                CgroupMemoryProbeFailureKind::InvalidCounter,
1492                "/fixture/leaf/memory.current",
1493                "expected an unsigned byte count",
1494            )),
1495        );
1496        assert_eq!(availability.available_bytes(), 0);
1497        assert_eq!(
1498            availability.limiting_source(),
1499            MemoryAvailabilitySource::CgroupProbeFailure
1500        );
1501        assert_eq!(governor_budget_from_availability(&availability), 0);
1502        let evidence = format!("{availability}");
1503        assert!(evidence.contains("failed closed"));
1504        assert!(evidence.contains("invalid-counter"));
1505    }
1506
1507    #[test]
1508    fn compressed_macos_observation_keeps_xnu_available_memory_positive() {
1509        // #2316's healthy 8 GiB macOS host had more compressed than
1510        // free+inactive pages. sysinfo 0.33 subtracted compressor pages and
1511        // saturated to zero; 0.38 follows XNU and reports
1512        // (active + inactive + free) * page_size.
1513        let xnu_available = (75_514_u64 + 69_056 + 3_802) * 16_384;
1514        assert_eq!(xnu_available, 2_430_926_848);
1515        let availability = MemoryAvailability::from_observation(
1516            xnu_available,
1517            CgroupMemoryObservation::NotPresent,
1518        );
1519        assert_eq!(availability.available_bytes(), xnu_available);
1520        assert_eq!(
1521            governor_budget_from_availability(&availability),
1522            1_823_195_136
1523        );
1524    }
1525}