use crate::cgroup_memory::detect_cgroup_memory;
pub use crate::cgroup_memory::{
CgroupMemoryAvailability, CgroupMemoryLimit, CgroupMemoryObservation, CgroupMemoryProbeFailure,
CgroupMemoryProbeFailureKind,
};
pub const LIBRARY_ROW_CHUNK_TARGET_BYTES: usize = 8 * 1024 * 1024;
pub fn byte_balanced_row_chunk(cols: usize, n_rows: usize) -> usize {
const MIN_CHUNK_ROWS: usize = 512;
let bytes_per_row = cols.max(1) * std::mem::size_of::<f64>();
(LIBRARY_ROW_CHUNK_TARGET_BYTES / bytes_per_row)
.max(MIN_CHUNK_ROWS)
.min(n_rows.max(1))
}
#[derive(Clone, Debug)]
pub struct ResourcePolicy {
pub max_single_materialization_bytes: usize,
pub max_operator_cache_bytes: usize,
pub max_spatial_distance_cache_bytes: usize,
pub max_owned_data_cache_bytes: usize,
pub row_chunk_target_bytes: usize,
pub derivative_storage_mode: DerivativeStorageMode,
}
pub const OWNED_DATA_CACHE_MAX_ENTRIES: usize = 2;
const GOVERNOR_BUDGET_NUMERATOR: u128 = 3;
const GOVERNOR_BUDGET_DENOMINATOR: u128 = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemoryAvailabilitySource {
Host,
Cgroup,
HostAndCgroup,
CgroupProbeFailure,
}
impl std::fmt::Display for MemoryAvailabilitySource {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Host => formatter.write_str("host"),
Self::Cgroup => formatter.write_str("cgroup"),
Self::HostAndCgroup => formatter.write_str("host and cgroup equally"),
Self::CgroupProbeFailure => formatter.write_str("cgroup probe failure"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MemoryAvailability {
host_available_bytes: u64,
host_total_bytes: u64,
cgroup: CgroupMemoryObservation,
available_bytes: u64,
capacity_bytes: u64,
limiting_source: MemoryAvailabilitySource,
}
impl MemoryAvailability {
pub(crate) fn from_observation(
host_available_bytes: u64,
host_total_bytes: u64,
cgroup: CgroupMemoryObservation,
) -> Self {
use std::cmp::Ordering;
let capacity_bytes = match &cgroup {
CgroupMemoryObservation::NotPresent | CgroupMemoryObservation::V2Unbounded { .. } => {
host_total_bytes.max(host_available_bytes)
}
CgroupMemoryObservation::V2Limited(observation)
| CgroupMemoryObservation::V1Limited(observation) => host_total_bytes
.max(host_available_bytes)
.min(observation.limit_bytes()),
CgroupMemoryObservation::ProbeFailed(_) => 0,
};
let (available_bytes, limiting_source) = match &cgroup {
CgroupMemoryObservation::NotPresent | CgroupMemoryObservation::V2Unbounded { .. } => {
(host_available_bytes, MemoryAvailabilitySource::Host)
}
CgroupMemoryObservation::V2Limited(observation)
| CgroupMemoryObservation::V1Limited(observation) => {
match observation.available_bytes().cmp(&host_available_bytes) {
Ordering::Less => (
observation.available_bytes(),
MemoryAvailabilitySource::Cgroup,
),
Ordering::Equal => (
host_available_bytes,
MemoryAvailabilitySource::HostAndCgroup,
),
Ordering::Greater => (host_available_bytes, MemoryAvailabilitySource::Host),
}
}
CgroupMemoryObservation::ProbeFailed(_) => {
(0, MemoryAvailabilitySource::CgroupProbeFailure)
}
};
Self {
host_available_bytes,
host_total_bytes,
cgroup,
available_bytes,
capacity_bytes,
limiting_source,
}
}
pub const fn capacity_bytes(&self) -> u64 {
self.capacity_bytes
}
pub const fn available_bytes(&self) -> u64 {
self.available_bytes
}
pub fn available_bytes_usize(&self) -> usize {
usize::try_from(self.available_bytes).unwrap_or(usize::MAX)
}
}
impl std::fmt::Display for MemoryAvailability {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.cgroup {
CgroupMemoryObservation::ProbeFailed(failure) => write!(
formatter,
"0 bytes admitted because the active cgroup probe failed closed (host_available={}, failure={})",
self.host_available_bytes, failure,
),
observation => write!(
formatter,
"{} bytes limited by {} (capacity={}, host_available={}, host_total={}, {})",
self.available_bytes,
self.limiting_source,
self.capacity_bytes,
self.host_available_bytes,
self.host_total_bytes,
observation,
),
}
}
}
static MEMORY_AVAILABILITY_PROBES: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
pub fn resample_memory_availability() -> MemoryAvailability {
static SYSTEM: OnceLock<Mutex<sysinfo::System>> = OnceLock::new();
let system = SYSTEM.get_or_init(|| Mutex::new(sysinfo::System::new()));
let mut system = system.lock().expect("sysinfo system mutex poisoned");
system.refresh_memory();
let cgroup = detect_cgroup_memory();
MEMORY_AVAILABILITY_PROBES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
MemoryAvailability::from_observation(
system.available_memory(),
system.total_memory(),
cgroup,
)
}
pub fn process_memory_availability() -> &'static MemoryAvailability {
&MemoryGovernor::global().ledger.availability
}
pub fn process_available_memory_bytes() -> usize {
process_memory_availability().available_bytes_usize()
}
fn governor_budget_from_availability(availability: &MemoryAvailability) -> usize {
stationary_headroom_of_capacity(availability)
}
fn governor_materialization_cap_from_availability(availability: &MemoryAvailability) -> usize {
stationary_headroom_of_capacity(availability)
}
fn stationary_headroom_of_capacity(availability: &MemoryAvailability) -> usize {
let scaled = u128::from(availability.capacity_bytes()) * GOVERNOR_BUDGET_NUMERATOR
/ GOVERNOR_BUDGET_DENOMINATOR;
usize::try_from(scaled).unwrap_or(usize::MAX)
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum MemoryReservationError {
#[error(
"{context}: cannot reserve {requested_bytes} bytes; {reserved_bytes} of {budget_bytes} bytes already reserved process-wide; detected availability: {availability}"
)]
BudgetExceeded {
context: Box<str>,
requested_bytes: usize,
reserved_bytes: usize,
budget_bytes: usize,
availability: MemoryAvailability,
},
#[error(
"{context}: dense allocation size overflow for {copies} copies of a {nrows}x{ncols} f64 matrix"
)]
SizeOverflow {
context: Box<str>,
nrows: usize,
ncols: usize,
copies: usize,
},
}
#[derive(Debug)]
struct GovernorLedger {
budget_bytes: usize,
materialization_cap_bytes: usize,
availability: MemoryAvailability,
reserved_bytes: std::sync::atomic::AtomicUsize,
}
#[derive(Debug, Clone)]
pub struct MemoryGovernor {
ledger: Arc<GovernorLedger>,
}
impl MemoryGovernor {
pub fn global() -> &'static MemoryGovernor {
static GLOBAL: OnceLock<MemoryGovernor> = OnceLock::new();
GLOBAL.get_or_init(|| {
let availability = resample_memory_availability();
MemoryGovernor::with_detected_availability(availability)
})
}
fn with_detected_availability(availability: MemoryAvailability) -> Self {
let budget_bytes = governor_budget_from_availability(&availability);
let materialization_cap_bytes = governor_materialization_cap_from_availability(&availability);
Self {
ledger: Arc::new(GovernorLedger {
budget_bytes,
materialization_cap_bytes,
availability,
reserved_bytes: std::sync::atomic::AtomicUsize::new(0),
}),
}
}
pub fn reserved_bytes(&self) -> usize {
self.ledger
.reserved_bytes
.load(std::sync::atomic::Ordering::Acquire)
}
pub fn remaining_bytes(&self) -> usize {
self.ledger
.budget_bytes
.saturating_sub(self.reserved_bytes())
}
pub fn single_materialization_cap_bytes(&self) -> usize {
self.ledger.materialization_cap_bytes
}
pub fn try_reserve(
&self,
bytes: usize,
context: &str,
) -> Result<MemoryReservation, MemoryReservationError> {
use std::sync::atomic::Ordering;
let mut current = self.ledger.reserved_bytes.load(Ordering::Relaxed);
loop {
let next = match current.checked_add(bytes) {
Some(next) if next <= self.ledger.budget_bytes => next,
_ => {
return Err(MemoryReservationError::BudgetExceeded {
context: context.into(),
requested_bytes: bytes,
reserved_bytes: current,
budget_bytes: self.ledger.budget_bytes,
availability: self.ledger.availability.clone(),
});
}
};
match self.ledger.reserved_bytes.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => {
return Ok(MemoryReservation {
ledger: Arc::clone(&self.ledger),
bytes,
});
}
Err(observed) => current = observed,
}
}
}
pub fn try_reserve_dense_f64(
&self,
nrows: usize,
ncols: usize,
context: &str,
) -> Result<MemoryReservation, MemoryReservationError> {
self.try_reserve_dense_f64_copies(nrows, ncols, 1, context)
}
pub fn try_reserve_dense_f64_copies(
&self,
nrows: usize,
ncols: usize,
copies: usize,
context: &str,
) -> Result<MemoryReservation, MemoryReservationError> {
let bytes = dense_f64_bytes(nrows, ncols)
.and_then(|one| one.checked_mul(copies))
.ok_or_else(|| MemoryReservationError::SizeOverflow {
context: context.into(),
nrows,
ncols,
copies,
})?;
self.try_reserve(bytes, context)
}
}
pub const fn dense_f64_bytes(nrows: usize, ncols: usize) -> Option<usize> {
match nrows.checked_mul(ncols) {
Some(cells) => cells.checked_mul(std::mem::size_of::<f64>()),
None => None,
}
}
#[derive(Debug)]
#[must_use = "dropping a memory reservation immediately releases its ledger charge"]
pub struct MemoryReservation {
ledger: Arc<GovernorLedger>,
bytes: usize,
}
impl MemoryReservation {
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn bind<T>(self, value: T) -> Governed<T> {
Governed {
value,
}
}
}
#[derive(Debug)]
#[must_use = "the governed value owns a live process-wide memory reservation"]
pub struct Governed<T> {
value: T,
}
impl<T> std::ops::Deref for Governed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> std::ops::DerefMut for Governed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl<T> AsRef<T> for Governed<T> {
fn as_ref(&self) -> &T {
&self.value
}
}
impl<T> AsMut<T> for Governed<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.value
}
}
impl Drop for MemoryReservation {
fn drop(&mut self) {
self.ledger
.reserved_bytes
.fetch_sub(self.bytes, std::sync::atomic::Ordering::AcqRel);
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ProblemHints {
pub marginal_slope_large_scale_active: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DerivativeStorageMode {
AnalyticOperatorRequired,
MaterializeIfSmall,
DiagnosticsOnly,
}
#[derive(Clone, Debug)]
pub struct MaterializationPolicy {
pub max_single_dense_bytes: usize,
pub max_cached_dense_bytes: usize,
pub row_chunk_target_bytes: usize,
pub allow_operator_materialization: bool,
pub allow_diagnostic_materialization: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum MatrixMaterializationError {
#[error(
"{context}: dense materialization of {nrows}x{ncols} requires {bytes} bytes (limit {limit_bytes})"
)]
TooLarge {
context: &'static str,
nrows: usize,
ncols: usize,
bytes: usize,
limit_bytes: usize,
},
#[error("{context}: operator does not implement chunked row access")]
MissingRowChunk { context: &'static str },
#[error("{context}: row materialization failed: {reason}")]
RowMaterializationFailed {
context: &'static str,
reason: String,
},
#[error("{context}: materialization forbidden by policy (mode={mode:?})")]
Forbidden {
context: &'static str,
mode: DerivativeStorageMode,
},
#[error(transparent)]
Reservation(#[from] MemoryReservationError),
}
pub trait ResidentBytes {
fn resident_bytes(&self) -> usize;
}
impl ResourcePolicy {
pub fn default_library() -> Self {
Self::for_observed_memory(process_memory_availability())
}
pub fn for_observed_memory(availability: &MemoryAvailability) -> Self {
let single_cap = governor_materialization_cap_from_availability(availability);
Self {
max_single_materialization_bytes: single_cap,
max_operator_cache_bytes: single_cap,
max_spatial_distance_cache_bytes: single_cap,
max_owned_data_cache_bytes: single_cap,
row_chunk_target_bytes: LIBRARY_ROW_CHUNK_TARGET_BYTES,
derivative_storage_mode: DerivativeStorageMode::MaterializeIfSmall,
}
}
pub fn analytic_operator_required() -> Self {
let base = Self::default_library();
Self {
derivative_storage_mode: DerivativeStorageMode::AnalyticOperatorRequired,
..base
}
}
pub fn for_problem(hints: ProblemHints) -> Self {
if hints.marginal_slope_large_scale_active {
return Self::analytic_operator_required();
}
Self::default_library()
}
pub const fn material_policy(&self) -> MaterializationPolicy {
MaterializationPolicy {
max_single_dense_bytes: self.max_single_materialization_bytes,
max_cached_dense_bytes: self.max_operator_cache_bytes,
row_chunk_target_bytes: self.row_chunk_target_bytes,
allow_operator_materialization: matches!(
self.derivative_storage_mode,
DerivativeStorageMode::MaterializeIfSmall
),
allow_diagnostic_materialization: !matches!(
self.derivative_storage_mode,
DerivativeStorageMode::AnalyticOperatorRequired
),
}
}
}
pub const fn rows_for_target_bytes(target_bytes: usize, cols: usize) -> usize {
let raw_bytes_per_row = cols.saturating_mul(std::mem::size_of::<f64>());
let bytes_per_row = if raw_bytes_per_row == 0 {
1
} else {
raw_bytes_per_row
};
let rows = target_bytes / bytes_per_row;
if rows == 0 { 1 } else { rows }
}
pub fn prediction_chunk_rows(parameter_dim: usize, local_dim: usize, total_rows: usize) -> usize {
const MIN_ROWS: usize = 16;
const MAX_ROWS: usize = 4096;
if total_rows == 0 {
return 1;
}
let live_f64_values_per_row = parameter_dim
.max(1)
.saturating_mul(local_dim.max(1))
.saturating_mul(4);
rows_for_target_bytes(
ResourcePolicy::default_library().row_chunk_target_bytes,
live_f64_values_per_row,
)
.clamp(MIN_ROWS, MAX_ROWS)
.min(total_rows)
}
use std::collections::{HashMap, VecDeque};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex, OnceLock};
pub struct ByteLruCache<K: Eq + Hash + Clone, V> {
shards: Box<[Mutex<ByteLruInner<K, V>>]>,
shard_bytes: usize,
shard_entries: Option<usize>,
max_bytes: usize,
governor: MemoryGovernor,
}
struct ByteLruInner<K, V> {
map: HashMap<K, (V, usize, MemoryReservation)>,
order: VecDeque<K>,
resident_bytes: usize,
}
impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> ByteLruCache<K, V> {
pub fn new(max_bytes: usize) -> Self {
Self::build(max_bytes, None, 1)
}
pub fn with_max_entries(max_bytes: usize, max_entries: usize) -> Self {
Self::build(max_bytes, Some(max_entries), 1)
}
pub fn new_sharded(max_bytes: usize, shard_count: usize) -> Self {
Self::build(max_bytes, None, shard_count)
}
pub fn with_max_entries_sharded(
max_bytes: usize,
max_entries: usize,
shard_count: usize,
) -> Self {
Self::build(max_bytes, Some(max_entries), shard_count)
}
fn build(max_bytes: usize, max_entries: Option<usize>, shard_count: usize) -> Self {
Self::build_with_governor(
max_bytes,
max_entries,
shard_count,
MemoryGovernor::global().clone(),
)
}
fn build_with_governor(
max_bytes: usize,
max_entries: Option<usize>,
shard_count: usize,
governor: MemoryGovernor,
) -> Self {
let shard_count = shard_count.max(1);
let shard_bytes = max_bytes.div_ceil(shard_count);
let shard_entries = max_entries.map(|m| {
if m == 0 {
0
} else {
m.div_ceil(shard_count).max(1)
}
});
let shards = (0..shard_count)
.map(|_| {
Mutex::new(ByteLruInner {
map: HashMap::new(),
order: VecDeque::new(),
resident_bytes: 0,
})
})
.collect::<Vec<_>>()
.into_boxed_slice();
Self {
shards,
shard_bytes,
shard_entries,
max_bytes,
governor,
}
}
#[inline]
fn shard(&self, key: &K) -> &Mutex<ByteLruInner<K, V>> {
if self.shards.len() == 1 {
return &self.shards[0];
}
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
let shard_count =
u64::try_from(self.shards.len()).expect("shard count must fit in u64");
let shard = usize::try_from(hasher.finish() % shard_count)
.expect("shard index is bounded by the usize shard count");
&self.shards[shard]
}
pub fn get(&self, key: &K) -> Option<V> {
let mut g = self.shard(key).lock().unwrap_or_else(|p| p.into_inner());
let v = g.map.get(key)?.0.clone();
if let Some(pos) = g.order.iter().position(|k| k == key) {
let k = g
.order
.remove(pos)
.expect("position() returned an in-bounds index into this same deque");
g.order.push_back(k);
}
Some(v)
}
pub fn insert(&self, key: K, value: V) {
let charge = value.resident_bytes();
let mut g = self.shard(&key).lock().unwrap_or_else(|p| p.into_inner());
if let Some((_old, old_charge, _reservation)) = g.map.remove(&key) {
g.resident_bytes = g.resident_bytes.saturating_sub(old_charge);
if let Some(pos) = g.order.iter().position(|k| k == &key) {
g.order.remove(pos);
}
}
if charge > self.shard_bytes {
return;
}
if let Some(max_entries) = self.shard_entries {
if max_entries == 0 {
return;
}
while g.map.len() >= max_entries {
if let Some(evict_key) = g.order.pop_front() {
if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
g.resident_bytes = g.resident_bytes.saturating_sub(c);
}
} else {
break;
}
}
}
while g.resident_bytes + charge > self.shard_bytes {
if let Some(evict_key) = g.order.pop_front() {
if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
g.resident_bytes = g.resident_bytes.saturating_sub(c);
}
} else {
break;
}
}
let reservation = match self
.governor
.try_reserve(charge, "ByteLruCache resident entry")
{
Ok(reservation) => reservation,
Err(_) => return,
};
g.map.insert(key.clone(), (value, charge, reservation));
g.order.push_back(key);
g.resident_bytes = g.resident_bytes.saturating_add(charge);
}
pub fn resident_bytes(&self) -> usize {
self.shards
.iter()
.map(|shard| {
shard
.lock()
.unwrap_or_else(|p| p.into_inner())
.resident_bytes
})
.sum()
}
pub const fn max_bytes(&self) -> usize {
self.max_bytes
}
pub fn len(&self) -> usize {
self.shards
.iter()
.map(|shard| shard.lock().unwrap_or_else(|p| p.into_inner()).map.len())
.sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
for shard in self.shards.iter() {
let mut g = shard.lock().unwrap_or_else(|p| p.into_inner());
g.map.clear();
g.order.clear();
g.resident_bytes = 0;
}
}
}
impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> std::fmt::Debug for ByteLruCache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ByteLruCache")
.field("resident_bytes", &self.resident_bytes())
.field("max_bytes", &self.max_bytes)
.field("shard_count", &self.shards.len())
.field("shard_bytes", &self.shard_bytes)
.field("shard_entries", &self.shard_entries)
.finish()
}
}
impl ResidentBytes for Arc<ndarray::Array2<f64>> {
fn resident_bytes(&self) -> usize {
std::mem::size_of::<f64>()
.saturating_mul(self.nrows())
.saturating_mul(self.ncols())
}
}
pub struct RayonSafeOnce<T> {
slot: std::sync::OnceLock<T>,
}
impl<T> RayonSafeOnce<T> {
pub const fn new() -> Self {
Self {
slot: std::sync::OnceLock::new(),
}
}
#[inline]
pub fn get(&self) -> Option<&T> {
self.slot.get()
}
pub fn get_or_compute<F>(&self, init: F) -> &T
where
F: FnOnce() -> T,
{
if let Some(v) = self.slot.get() {
return v;
}
let candidate = init();
if self.slot.set(candidate).is_err() {
log::trace!(
"RayonSafeOnce: a concurrent initializer won the race; \
keeping its value and discarding this candidate"
);
}
self.slot
.get()
.expect("RayonSafeOnce slot populated by set() above")
}
}
impl<T> Default for RayonSafeOnce<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone> Clone for RayonSafeOnce<T> {
fn clone(&self) -> Self {
let cloned = Self::new();
if let Some(value) = self.slot.get() {
cloned.slot.get_or_init(|| value.clone());
}
cloned
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for RayonSafeOnce<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RayonSafeOnce")
.field("slot", &self.slot.get())
.finish()
}
}
#[cfg(test)]
mod byte_lru_tests {
use super::*;
fn cache_test_governor(budget_bytes: usize) -> MemoryGovernor {
let available_bytes = (budget_bytes as u128 * GOVERNOR_BUDGET_DENOMINATOR)
.div_ceil(GOVERNOR_BUDGET_NUMERATOR);
let available_bytes =
u64::try_from(available_bytes).expect("test cache budget must fit in u64");
MemoryGovernor::with_detected_availability(MemoryAvailability::from_observation(
available_bytes,
available_bytes,
CgroupMemoryObservation::NotPresent,
))
}
#[derive(Clone, PartialEq, Debug)]
struct Payload(u64);
impl ResidentBytes for Payload {
fn resident_bytes(&self) -> usize {
8
}
}
#[test]
fn single_shard_round_trips_and_evicts_by_bytes() {
let cache: ByteLruCache<u64, Payload> =
ByteLruCache::build_with_governor(24, None, 1, cache_test_governor(24));
for k in 0..3 {
cache.insert(k, Payload(k));
}
assert_eq!(cache.len(), 3);
assert_eq!(cache.resident_bytes(), 24);
assert_eq!(cache.get(&0), Some(Payload(0)));
cache.insert(3, Payload(3));
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(&1), None);
assert_eq!(cache.get(&0), Some(Payload(0)));
assert_eq!(cache.get(&3), Some(Payload(3)));
}
#[test]
fn zero_entry_budget_disables_caching_in_every_shard() {
let single: ByteLruCache<u64, Payload> = ByteLruCache::with_max_entries(1 << 20, 0);
single.insert(7, Payload(7));
assert_eq!(single.get(&7), None);
let sharded: ByteLruCache<u64, Payload> =
ByteLruCache::with_max_entries_sharded(1 << 20, 0, 16);
sharded.insert(7, Payload(7));
assert_eq!(sharded.get(&7), None);
}
#[test]
fn sharded_cache_retrieves_all_keys_and_respects_aggregate_budget() {
let shard_count = 8usize;
let max_bytes = 8 * 64; let cache: ByteLruCache<u64, Payload> = ByteLruCache::build_with_governor(
max_bytes,
None,
shard_count,
cache_test_governor(max_bytes),
);
for k in 0..64u64 {
cache.insert(k, Payload(k));
}
assert!(cache.resident_bytes() <= max_bytes.div_ceil(shard_count) * shard_count);
cache.insert(123, Payload(123));
assert_eq!(cache.get(&123), Some(Payload(123)));
assert!(!cache.is_empty());
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.resident_bytes(), 0);
}
}
#[cfg(test)]
mod resource_policy_tests {
use super::*;
#[test]
fn rows_for_target_bytes_exact_fit() {
assert_eq!(rows_for_target_bytes(8, 1), 1);
}
#[test]
fn rows_for_target_bytes_multiple_rows() {
assert_eq!(rows_for_target_bytes(80, 1), 10);
}
#[test]
fn rows_for_target_bytes_multiple_cols() {
assert_eq!(rows_for_target_bytes(128, 4), 4);
}
#[test]
fn rows_for_target_bytes_zero_target_returns_one() {
assert_eq!(rows_for_target_bytes(0, 1), 1);
}
#[test]
fn rows_for_target_bytes_zero_cols_returns_non_zero() {
assert_eq!(rows_for_target_bytes(100, 0), 100);
}
#[test]
fn rows_for_target_bytes_large_target() {
let target = LIBRARY_ROW_CHUNK_TARGET_BYTES;
let cols = 1024_usize;
let expected = target / (cols * std::mem::size_of::<f64>());
assert_eq!(rows_for_target_bytes(target, cols), expected);
}
#[test]
fn prediction_chunks_share_the_runtime_byte_budget() {
assert_eq!(prediction_chunk_rows(1024, 1, 100_000), 256);
assert_eq!(prediction_chunk_rows(32, 2, 100_000), 4096);
}
#[test]
fn prediction_chunks_respect_dataset_bounds() {
assert_eq!(prediction_chunk_rows(1, 1, 7), 7);
assert_eq!(prediction_chunk_rows(1, 1, 0), 1);
}
#[test]
fn for_problem_small_data_uses_materialize_if_small() {
let p = ResourcePolicy::for_problem(ProblemHints::default());
assert_eq!(
p.derivative_storage_mode,
DerivativeStorageMode::MaterializeIfSmall
);
}
#[test]
fn for_problem_has_no_row_or_column_cliff() {
let narrow = ResourcePolicy::for_problem(ProblemHints::default());
let wide = ResourcePolicy::for_problem(ProblemHints::default());
assert_eq!(
narrow.derivative_storage_mode,
DerivativeStorageMode::MaterializeIfSmall
);
assert_eq!(
wide.derivative_storage_mode,
DerivativeStorageMode::MaterializeIfSmall
);
}
#[test]
fn for_problem_dimension_overflow_defers_to_typed_reservation() {
let policy = ResourcePolicy::for_problem(ProblemHints::default());
assert_eq!(
policy.derivative_storage_mode,
DerivativeStorageMode::MaterializeIfSmall
);
}
#[test]
fn for_problem_marginal_slope_hint_is_strict() {
let p = ResourcePolicy::for_problem(ProblemHints {
marginal_slope_large_scale_active: true,
});
assert_eq!(
p.derivative_storage_mode,
DerivativeStorageMode::AnalyticOperatorRequired
);
}
#[test]
fn material_policy_default_library_allows_operator_and_diagnostics() {
let mp = ResourcePolicy::default_library().material_policy();
assert!(mp.allow_operator_materialization);
assert!(mp.allow_diagnostic_materialization);
}
#[test]
fn material_policy_analytic_operator_required_blocks_both() {
let mp = ResourcePolicy::analytic_operator_required().material_policy();
assert!(!mp.allow_operator_materialization);
assert!(!mp.allow_diagnostic_materialization);
}
#[test]
fn material_policy_propagates_byte_limits() {
let policy = ResourcePolicy::default_library();
let mp = policy.material_policy();
assert_eq!(
mp.max_single_dense_bytes,
policy.max_single_materialization_bytes
);
assert_eq!(mp.max_cached_dense_bytes, policy.max_operator_cache_bytes);
assert_eq!(mp.row_chunk_target_bytes, policy.row_chunk_target_bytes);
}
#[test]
fn compressed_macos_observation_keeps_xnu_available_memory_positive() {
let xnu_available = (75_514_u64 + 69_056 + 3_802) * 16_384;
assert_eq!(xnu_available, 2_430_926_848);
let availability = MemoryAvailability::from_observation(
xnu_available,
8 * 1024 * 1024 * 1024,
CgroupMemoryObservation::NotPresent,
);
assert_eq!(availability.available_bytes(), xnu_available);
assert_eq!(availability.capacity_bytes(), 8 * 1024 * 1024 * 1024);
assert_eq!(
governor_budget_from_availability(&availability),
6 * 1024 * 1024 * 1024
);
}
#[test]
fn literal_unlimited_cgroup_defers_to_host_available_memory_2317() {
let availability = MemoryAvailability::from_observation(
2_430_926_848,
4_000_000_000,
CgroupMemoryObservation::V2Unbounded {
cgroup_path: "/fixture/leaf".into(),
inspected_levels: 3,
},
);
assert_eq!(availability.available_bytes(), 2_430_926_848);
assert_eq!(availability.capacity_bytes(), 4_000_000_000);
assert_eq!(availability.limiting_source, MemoryAvailabilitySource::Host);
assert!(format!("{availability}").contains("unbounded cgroup-v2"));
let governor = MemoryGovernor::with_detected_availability(availability);
assert_eq!(governor.remaining_bytes(), 3_000_000_000);
assert_eq!(governor.single_materialization_cap_bytes(), 3_000_000_000);
let reservation = governor
.try_reserve(3_000_000_000, "host budget")
.unwrap();
assert_eq!(governor.remaining_bytes(), 0);
assert!(matches!(
governor.try_reserve(1, "one byte beyond the host budget"),
Err(MemoryReservationError::BudgetExceeded { .. })
));
drop(reservation);
assert_eq!(governor.remaining_bytes(), 3_000_000_000);
}
}
#[cfg(test)]
mod governor_budget_is_capacity_determined_2702_tests {
use super::*;
const HOST_AVAILABLE_BYTES: u64 = 448_648_040_448;
const HOST_TOTAL_BYTES: u64 = 527_799_400 * 1024;
const JOB_LIMIT_BYTES: u64 = 8 * 1024 * 1024 * 1024;
const SE_TRANSFORMED_ROWS: usize = 512;
const SE_CHUNK_BYTES: usize = SE_TRANSFORMED_ROWS * 8 * 2;
fn one_job_cgroup_at_load(charged: u64) -> MemoryAvailability {
crate::test_support::simulated_cgroup_memory_environment(
HOST_AVAILABLE_BYTES,
HOST_TOTAL_BYTES,
JOB_LIMIT_BYTES,
charged,
)
}
#[test]
fn a_cgroup_at_its_limit_moves_neither_the_budget_nor_the_materialization_cap_2684_2702() {
const LIMIT: u64 = 6 * 1024 * 1024 * 1024;
const DESIGN_BYTES: usize = 300 * 12 * 8;
let budget = LIMIT as usize / 4 * 3;
for charged in [0, LIMIT - 53_248, LIMIT] {
let availability = crate::test_support::simulated_cgroup_memory_environment(
HOST_AVAILABLE_BYTES,
HOST_TOTAL_BYTES,
LIMIT,
charged,
);
assert_eq!(availability.available_bytes(), LIMIT - charged);
assert_eq!(availability.capacity_bytes(), LIMIT);
let governor = MemoryGovernor::with_detected_availability(availability);
assert_eq!(governor.remaining_bytes(), budget);
assert_eq!(governor.single_materialization_cap_bytes(), budget);
assert!(governor.single_materialization_cap_bytes() > DESIGN_BYTES);
assert!(matches!(
governor.try_reserve(8 << 30, "larger than the job"),
Err(MemoryReservationError::BudgetExceeded { .. })
));
let chunk = governor
.try_reserve_dense_f64_copies(64, 1, 2, "coefficient-SE solve chunk")
.expect("the incident's 1,024-byte chunk fits at every ambient load");
assert_eq!(chunk.bytes(), 1_024);
assert_eq!(governor.remaining_bytes(), budget - 1_024);
assert_eq!(governor.single_materialization_cap_bytes(), budget);
drop(chunk);
assert_eq!(governor.remaining_bytes(), budget);
}
let tiny = crate::test_support::simulated_cgroup_memory_environment(
HOST_AVAILABLE_BYTES,
HOST_TOTAL_BYTES,
1_024,
0,
);
let governor = MemoryGovernor::with_detected_availability(tiny);
assert_eq!(governor.single_materialization_cap_bytes(), 768);
assert!(matches!(
governor.try_reserve(DESIGN_BYTES, "300 by 12 design"),
Err(MemoryReservationError::BudgetExceeded { .. })
));
}
#[test]
fn a_request_larger_than_the_job_is_still_refused_at_every_load() {
for charged in [0, JOB_LIMIT_BYTES / 2, JOB_LIMIT_BYTES - 4_096] {
let governor = MemoryGovernor::with_detected_availability(one_job_cgroup_at_load(charged));
let refusal = governor
.try_reserve(16 * 1024 * 1024 * 1024, "twice the job's ceiling")
.expect_err("16 GiB cannot be admitted in an 8 GiB job");
match refusal {
MemoryReservationError::BudgetExceeded { budget_bytes, .. } => {
assert_eq!(budget_bytes, (JOB_LIMIT_BYTES as usize) / 4 * 3);
}
other => panic!("expected a budget refusal naming the ceiling, got {other:?}"),
}
}
}
#[test]
fn the_verdict_does_not_depend_on_what_this_process_did_earlier() {
let governor = MemoryGovernor::with_detected_availability(one_job_cgroup_at_load(0));
let request = || {
governor
.try_reserve_dense_f64_copies(
SE_TRANSFORMED_ROWS,
1,
2,
"factorized coefficient-SE solve chunk",
)
.map(|reservation| reservation.bytes())
};
let before = request().expect("admissible on a fresh ledger");
{
let bulk = governor
.try_reserve(
governor.remaining_bytes() - SE_CHUNK_BYTES,
"prior work in this process",
)
.expect("the bulk reservation is exactly the remaining budget");
assert_eq!(governor.remaining_bytes(), SE_CHUNK_BYTES);
assert!(request().is_ok());
drop(bulk);
}
assert_eq!(governor.reserved_bytes(), 0);
assert_eq!(request().expect("admissible again"), before);
}
}