use std::collections::HashMap;
use crate::error::{MlxError, Result};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PoolRootCapacity {
pub roots: usize,
pub bytes: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PoolMemoryUnavailable {
PhysicalResidency,
OwnersOutsidePool,
LifetimeHighWater,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PoolMemorySnapshot {
pub retained: PoolRootCapacity,
pub free: PoolRootCapacity,
pub issued: PoolRootCapacity,
pub residency_hint: PoolRootCapacity,
pub labels_overlap: bool,
pub physical_residency: PoolMemoryUnavailable,
pub owners_outside_pool: PoolMemoryUnavailable,
pub lifetime_high_water: PoolMemoryUnavailable,
}
#[derive(Clone, Copy)]
pub(super) enum Label {
Free = 1,
Issued = 2,
Residency = 4,
}
fn error(message: &str) -> MlxError {
MlxError::InvalidArgument(format!("pool memory census: {message}"))
}
fn add(total: &mut PoolRootCapacity, bytes: usize) -> Result<()> {
total.roots = total
.roots
.checked_add(1)
.ok_or_else(|| error("root count overflow"))?;
total.bytes = total
.bytes
.checked_add(bytes)
.ok_or_else(|| error("capacity overflow"))?;
Ok(())
}
pub(super) fn census(
records: impl IntoIterator<Item = (usize, usize, Label)>,
record_limit: usize,
) -> Result<PoolMemorySnapshot> {
let mut roots = HashMap::<usize, (usize, u8)>::new();
roots
.try_reserve(record_limit)
.map_err(|_| error("metadata reservation failed"))?;
let mut seen = 0usize;
for (identity, bytes, label) in records {
seen = seen
.checked_add(1)
.ok_or_else(|| error("record count overflow"))?;
if seen > record_limit {
return Err(error("record limit exceeded"));
}
match roots.get_mut(&identity) {
Some((length, labels)) => {
if *length != bytes {
return Err(error("one Metal object has inconsistent lengths"));
}
*labels |= label as u8;
}
None => {
roots.insert(identity, (bytes, label as u8));
}
}
}
let mut out = PoolMemorySnapshot {
retained: PoolRootCapacity::default(),
free: PoolRootCapacity::default(),
issued: PoolRootCapacity::default(),
residency_hint: PoolRootCapacity::default(),
labels_overlap: false,
physical_residency: PoolMemoryUnavailable::PhysicalResidency,
owners_outside_pool: PoolMemoryUnavailable::OwnersOutsidePool,
lifetime_high_water: PoolMemoryUnavailable::LifetimeHighWater,
};
for (bytes, labels) in roots.into_values() {
add(&mut out.retained, bytes)?;
if labels & Label::Free as u8 != 0 {
add(&mut out.free, bytes)?;
}
if labels & Label::Issued as u8 != 0 {
add(&mut out.issued, bytes)?;
}
if labels & Label::Residency as u8 != 0 {
add(&mut out.residency_hint, bytes)?;
}
out.labels_overlap |= labels.count_ones() > 1;
}
Ok(out)
}
impl super::MlxBufferPool {
pub fn memory_snapshot(&self) -> Result<PoolMemorySnapshot> {
let free_records = self
.free
.values()
.try_fold(0usize, |n, v| n.checked_add(v.len()))
.ok_or_else(|| error("free record count overflow"))?;
let limit = free_records
.checked_add(self.in_use.len())
.and_then(|n| n.checked_add(self.resident_buffers.len()))
.ok_or_else(|| error("record count overflow"))?;
let records = self
.free
.values()
.flatten()
.map(|r| (r, Label::Free))
.chain(self.in_use.iter().map(|(_, r)| (r, Label::Issued)))
.chain(
self.resident_buffers
.values()
.map(|r| (r, Label::Residency)),
)
.map(|(r, label)| {
let identity = (&**r.metal() as *const metal::BufferRef) as usize;
(identity, r.metal().length() as usize, label)
});
census(records, limit)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_memory_census_deduplicates_aliases_and_overlapping_labels() {
let s = census(
[
(1, 128, Label::Issued),
(1, 128, Label::Issued),
(1, 128, Label::Residency),
(2, 64, Label::Free),
],
4,
)
.unwrap();
assert_eq!(
s.retained,
PoolRootCapacity {
roots: 2,
bytes: 192
}
);
assert_eq!(s.issued.bytes, 128);
assert_eq!(s.free.bytes, 64);
assert_eq!(s.residency_hint.bytes, 128);
assert!(s.labels_overlap);
}
#[test]
fn pool_memory_census_counts_residency_only_and_distinct_objects() {
let s = census([(1, 64, Label::Residency), (2, 64, Label::Issued)], 2).unwrap();
assert_eq!(s.retained.bytes, 128); assert_eq!(s.residency_hint.roots, 1);
assert!(!s.labels_overlap);
}
#[test]
fn pool_memory_census_empty_is_not_physical_or_retirement_proof() {
let s = census([], 0).unwrap();
assert_eq!(s.retained.bytes, 0);
assert_eq!(
s.physical_residency,
PoolMemoryUnavailable::PhysicalResidency
);
assert_eq!(
s.owners_outside_pool,
PoolMemoryUnavailable::OwnersOutsidePool
);
assert_eq!(
s.lifetime_high_water,
PoolMemoryUnavailable::LifetimeHighWater
);
}
#[test]
fn pool_memory_census_rejects_inconsistent_identity_overflow_and_bad_limit() {
assert!(census([(1, 64, Label::Free), (1, 128, Label::Issued)], 2).is_err());
assert!(census([(1, usize::MAX, Label::Free), (2, 1, Label::Issued)], 2).is_err());
assert!(census([(1, 64, Label::Free)], 0).is_err());
}
#[test]
fn pool_memory_census_reset_moves_labels_without_changing_union() {
let before = census([(1, 64, Label::Issued), (1, 64, Label::Residency)], 2).unwrap();
let after = census([(1, 64, Label::Free), (1, 64, Label::Residency)], 2).unwrap();
assert_eq!(before.retained, after.retained);
assert_eq!(after.issued.bytes, 0);
assert_eq!(after.free.bytes, before.issued.bytes);
assert_eq!(
census([], 0).unwrap().owners_outside_pool,
PoolMemoryUnavailable::OwnersOutsidePool
);
}
#[test]
#[ignore = "opt-in tiny Metal allocation/residency census; no GPU submission"]
fn pool_memory_actual_roots_views_reset_and_clear() {
use crate::{DType, MlxBufferPool, MlxDevice};
let device = MlxDevice::new().expect("Metal device");
assert!(
device.residency_set().is_some(),
"control requires active residency hints"
);
let mut pool = MlxBufferPool::new();
let a = pool.alloc(&device, 8, DType::F32, vec![2]).unwrap();
let b = pool.alloc(&device, 16, DType::F32, vec![4]).unwrap();
let external = a.clone();
let view = a.slice_view(4, 1);
let identity = |buffer: &crate::MlxBuffer| {
(&**buffer.metal_buffer() as *const metal::BufferRef) as usize
};
assert_eq!(identity(&a), identity(&external));
assert_eq!(identity(&a), identity(&view));
assert_ne!(identity(&a), identity(&b));
assert_eq!(view.data_byte_len(), 4);
assert_eq!(view.byte_len(), 8);
pool.register_existing(&device, &view).unwrap();
let issued = pool.memory_snapshot().unwrap();
assert_eq!(
issued.retained,
PoolRootCapacity {
roots: 2,
bytes: 24
}
);
assert_eq!(issued.issued, issued.retained);
assert_eq!(issued.free, PoolRootCapacity::default());
assert_eq!(issued.residency_hint, issued.retained);
assert!(issued.labels_overlap);
drop(a);
drop(b);
pool.reset();
let free = pool.memory_snapshot().unwrap();
assert_eq!(free.retained, issued.retained);
assert_eq!(free.free, free.retained);
assert_eq!(free.issued, PoolRootCapacity::default());
assert_eq!(free.residency_hint, free.retained);
pool.clear();
let cleared = pool.memory_snapshot().unwrap();
assert_eq!(cleared.retained, PoolRootCapacity::default());
assert_eq!(cleared.residency_hint, PoolRootCapacity::default());
assert_eq!(external.byte_len(), 8);
assert_eq!(identity(&external), identity(&view));
assert_eq!(
cleared.owners_outside_pool,
PoolMemoryUnavailable::OwnersOutsidePool
);
assert_eq!(
cleared.physical_residency,
PoolMemoryUnavailable::PhysicalResidency
);
assert_eq!(
cleared.lifetime_high_water,
PoolMemoryUnavailable::LifetimeHighWater
);
drop(view);
drop(external);
println!("POOL_MEMORY_ACTUAL_COMPLETE roots=2 requested_bytes=24 gpu_submissions=0 outside_owners=Unavailable");
}
}