1use crate::cgroup_memory::detect_cgroup_memory;
2pub use crate::cgroup_memory::{
3 CgroupMemoryAvailability, CgroupMemoryLimit, CgroupMemoryObservation,
4 CgroupMemoryProbeFailure, CgroupMemoryProbeFailureKind,
5};
6
7pub 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
24const GOVERNOR_BUDGET_NUMERATOR: u128 = 3;
40const GOVERNOR_BUDGET_DENOMINATOR: u128 = 4;
41
42#[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#[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
158pub 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
170fn 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#[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#[derive(Debug, Clone)]
234pub struct MemoryGovernor {
235 ledger: Arc<GovernorLedger>,
236}
237
238impl MemoryGovernor {
239 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 pub fn single_materialization_cap_bytes(&self) -> usize {
283 self.ledger.budget_bytes
284 }
285
286 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 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 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
362pub 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#[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 pub fn bind<T>(self, value: T) -> Governed<T> {
387 Governed {
388 value,
389 reservation: self,
390 }
391 }
392}
393
394#[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#[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 AnalyticOperatorRequired,
457 MaterializeIfSmall,
459 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 #[error(transparent)]
504 Reservation(#[from] MemoryReservationError),
505}
506
507pub trait ResidentBytes {
508 fn resident_bytes(&self) -> usize;
509}
510
511impl ResourcePolicy {
512 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 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 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 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
597pub 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
610pub 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
639pub struct ByteLruCache<K: Eq + Hash + Clone, V> {
650 shards: Box<[Mutex<ByteLruInner<K, V>>]>,
661 shard_bytes: usize,
663 shard_entries: Option<usize>,
665 max_bytes: usize,
666 governor: MemoryGovernor,
667}
668
669struct ByteLruInner<K, V> {
670 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 pub fn new_sharded(max_bytes: usize, shard_count: usize) -> Self {
691 Self::build(max_bytes, None, shard_count)
692 }
693
694 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 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 let mut g = self.shard(key).lock().unwrap_or_else(|p| p.into_inner());
764 let v = g.map.get(key)?.0.clone();
765 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 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 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
877impl 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
891pub 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 #[inline]
924 pub fn get(&self) -> Option<&T> {
925 self.slot.get()
926 }
927
928 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 #[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 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 assert_eq!(cache.get(&0), Some(Payload(0)));
1012 cache.insert(3, Payload(3));
1013 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 let shard_count = 8usize;
1037 let max_bytes = 8 * 64; 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 assert!(cache.resident_bytes() <= max_bytes.div_ceil(shard_count) * shard_count);
1049 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 #[test]
1078 fn rows_for_target_bytes_exact_fit() {
1079 assert_eq!(rows_for_target_bytes(8, 1), 1);
1081 }
1082
1083 #[test]
1084 fn rows_for_target_bytes_multiple_rows() {
1085 assert_eq!(rows_for_target_bytes(80, 1), 10);
1087 }
1088
1089 #[test]
1090 fn rows_for_target_bytes_multiple_cols() {
1091 assert_eq!(rows_for_target_bytes(128, 4), 4);
1093 }
1094
1095 #[test]
1096 fn rows_for_target_bytes_zero_target_returns_one() {
1097 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 assert_eq!(rows_for_target_bytes(100, 0), 100);
1105 }
1106
1107 #[test]
1108 fn rows_for_target_bytes_large_target() {
1109 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 #[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 #[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 #[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 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 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 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 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 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 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}