use crate::memory_management::Cleanup;
use crate::{
config::memory::MemoryLogLevel,
logging::ServerLogger,
memory_management::{
BytesFormat, ErrorGraph, ManagedMemoryHandle, MemoryPoolKind, MemoryPoolReport,
memory_pool::{MemoryPool, PageMapping, SlicedLayout, SlicedPool, calculate_padding},
relocation::{
ArenaShape, ArenaState, Landed, LiveAllocation, Move, OutdatedPages, Relocation,
StorageCopy, TargetRoom,
},
},
server::IoError,
storage::ComputeStorage,
};
use alloc::{format, string::String, vec::Vec};
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::sync::Arc;
use cubecl_ir::MemoryDeviceProperties;
pub const ARENA_SLOTS: usize = 64;
const PAGE_SLACK: u64 = 1024 * 1024;
const PAGE_GRANULE: u64 = 1024 * 1024;
pub struct PoolArena {
slots: Vec<Option<SlicedPool>>,
first_index: u8,
current: usize,
outdated: Vec<usize>,
sizing: PageSizing,
logger: Arc<ServerLogger>,
name: String,
}
#[derive(Debug, Clone, Copy)]
pub struct PageSizing {
min_page_size: u64,
max_page_size: u64,
alignment: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fit {
Fits,
Grow {
page_size: u64,
},
TooLarge,
}
impl PageSizing {
pub fn new(min_page_size: u64, properties: &MemoryDeviceProperties) -> Self {
let alignment = properties.alignment;
let max_page_size = (properties.max_page_size / alignment * alignment).max(alignment);
Self {
min_page_size: min_page_size
.max(alignment)
.next_multiple_of(alignment)
.min(max_page_size),
max_page_size,
alignment,
}
}
pub fn fit(&self, size: u64, page_size: u64) -> Fit {
let needed = size + calculate_padding(size, self.alignment);
if needed <= page_size {
return Fit::Fits;
}
if needed > self.max_page_size {
return Fit::TooLarge;
}
Fit::Grow {
page_size: size
.saturating_add(PAGE_SLACK)
.next_multiple_of(PAGE_GRANULE)
.next_multiple_of(self.alignment)
.clamp(self.min_page_size, self.max_page_size),
}
}
fn pool(&self, page_size: u64, index: u8) -> SlicedPool {
SlicedPool::new(SlicedLayout {
page_size,
max_slice_size: page_size,
alignment: self.alignment,
pool: index,
})
}
}
impl PoolArena {
pub fn new(
sizing: PageSizing,
first_index: u8,
logger: Arc<ServerLogger>,
name: String,
) -> Self {
let mut slots = Vec::with_capacity(ARENA_SLOTS);
slots.push(Some(sizing.pool(sizing.min_page_size, first_index)));
slots.resize_with(ARENA_SLOTS, || None);
Self {
slots,
first_index,
current: 0,
outdated: Vec::with_capacity(ARENA_SLOTS),
sizing,
logger,
name,
}
}
pub fn pool(&self, index: u8) -> Option<&SlicedPool> {
let slot = index.checked_sub(self.first_index)?;
self.slots.get(slot as usize)?.as_ref()
}
pub fn pool_mut(&mut self, index: u8) -> Option<&mut SlicedPool> {
let slot = index.checked_sub(self.first_index)?;
self.slots.get_mut(slot as usize)?.as_mut()
}
pub fn has_outdated(&self) -> bool {
!self.outdated.is_empty()
}
pub fn state(&self) -> ArenaState {
ArenaState {
page_size: self.current().page_size(),
has_outdated: self.has_outdated(),
full: self.outdated.len() + 1 == ARENA_SLOTS,
shape: ArenaShape {
current_pages: self.current().pages_held(),
outdated_pools: self.outdated.len(),
outdated_pages: self.outdated().map(SlicedPool::pages_held).sum(),
outdated_guarded: self
.outdated()
.flat_map(SlicedPool::pages)
.filter(|page| page.is_guarded())
.count(),
},
}
}
fn current(&self) -> &SlicedPool {
self.slots[self.current]
.as_ref()
.expect("the current slot always holds a pool")
}
fn current_mut(&mut self) -> &mut SlicedPool {
self.slots[self.current]
.as_mut()
.expect("the current slot always holds a pool")
}
fn outdated(&self) -> impl Iterator<Item = &SlicedPool> {
self.outdated
.iter()
.filter_map(|slot| self.slots[*slot].as_ref())
}
fn pair_mut(&mut self, source: u8, target: u8) -> Option<(&mut SlicedPool, &mut SlicedPool)> {
let first = self.first_index;
let [source, target] = self
.slots
.get_disjoint_mut([
source.checked_sub(first)? as usize,
target.checked_sub(first)? as usize,
])
.ok()?;
Some((source.as_mut()?, target.as_mut()?))
}
pub fn reserve<Storage: ComputeStorage>(
&mut self,
storage: &mut Storage,
size: u64,
mapping: PageMapping,
failures: &mut ErrorGraph,
) -> Result<ManagedMemoryHandle, IoError> {
match self.sizing.fit(size, self.current().page_size()) {
Fit::Fits => self.current_mut().reserve(storage, size, mapping, failures),
Fit::Grow { page_size } => self.grow(storage, size, page_size, mapping, failures),
Fit::TooLarge => Err(IoError::BufferTooBig {
size,
backtrace: BackTrace::capture(),
}),
}
}
pub fn try_reserve(
&mut self,
size: u64,
failures: &mut ErrorGraph,
) -> Option<ManagedMemoryHandle> {
match self.sizing.fit(size, self.current().page_size()) {
Fit::Fits => self.current_mut().try_reserve(size, failures),
Fit::Grow { .. } | Fit::TooLarge => None,
}
}
pub fn cleanup<Storage: ComputeStorage>(
&mut self,
storage: &mut Storage,
alloc_nr: u64,
cleanup: Cleanup,
failures: &mut ErrorGraph,
) {
self.current_mut()
.cleanup(storage, alloc_nr, cleanup, failures);
self.drain(storage, failures);
}
fn drain<Storage: ComputeStorage>(&mut self, storage: &mut Storage, failures: &mut ErrorGraph) {
let slots = &mut self.slots;
self.outdated.retain(|&slot| {
let Some(pool) = slots[slot].as_mut() else {
return false;
};
pool.release_empty(storage, failures);
let drained = pool.is_empty();
if drained {
slots[slot] = None;
}
!drained
});
}
pub fn report(&self) -> impl Iterator<Item = MemoryPoolReport> + '_ {
let current = self.current();
let report = MemoryPoolReport {
kind: MemoryPoolKind::Adaptive {
page_size: current.page_size(),
outdated_pages: self.outdated().map(SlicedPool::pages_held).sum(),
},
..current.report()
};
core::iter::once(report).chain(self.outdated().map(SlicedPool::report))
}
pub fn plan<Storage: ComputeStorage>(
&mut self,
room: TargetRoom,
storage: &mut Storage,
failures: &mut ErrorGraph,
) -> Relocation {
let plan = OutdatedPages::new(self.outdated().flat_map(SlicedPool::pages)).plan();
let mut moves = Vec::new();
for page in plan {
let start = moves.len();
for allocation in page.live {
match self.plan_move(allocation, room, storage, failures) {
Some(relocated) => moves.push(relocated),
None => {
moves.truncate(start);
break;
}
}
}
}
Relocation::new(moves)
}
pub fn commit<Storage: ComputeStorage>(
&mut self,
landed: Landed,
storage: &mut Storage,
failures: &mut ErrorGraph,
) {
for Move {
allocation, target, ..
} in landed.into_moves()
{
let source = allocation.descriptor().location();
let destination = target.descriptor().location();
drop(target);
drop(allocation);
if let Some((source_pool, target_pool)) = self.pair_mut(source.pool, destination.pool) {
source_pool
.slice_at(source)
.hand_over(target_pool.slice_at(destination), failures);
}
}
self.drain(storage, failures);
}
fn plan_move<Storage: ComputeStorage>(
&mut self,
allocation: LiveAllocation,
room: TargetRoom,
storage: &mut Storage,
failures: &mut ErrorGraph,
) -> Option<Move> {
let source = allocation.handle.descriptor().location();
let source_storage = self.pool_mut(source.pool)?.storage_at(source);
let pool = self.current_mut();
let target = match (pool.try_reserve(allocation.size, failures), room) {
(Some(target), _) => target,
(None, TargetRoom::Held) => return None,
(None, TargetRoom::MayAllocate) => pool
.alloc(storage, allocation.size, PageMapping::Eager, failures)
.ok()?,
};
let copy = match source_storage {
Some(source) => Some(StorageCopy {
source,
target: pool
.mapped_storage_at(storage, target.descriptor().location())
.ok()?,
}),
None => None,
};
Some(Move {
allocation: allocation.handle,
target,
copy,
})
}
fn grow<Storage: ComputeStorage>(
&mut self,
storage: &mut Storage,
size: u64,
page_size: u64,
mapping: PageMapping,
failures: &mut ErrorGraph,
) -> Result<ManagedMemoryHandle, IoError> {
let Some(slot) = self.slots.iter().position(Option::is_none) else {
return Err(IoError::PageSizesExhausted {
size,
backtrace: BackTrace::capture(),
});
};
let mut pool = self.sizing.pool(page_size, self.first_index + slot as u8);
let handle = pool.alloc(storage, size, mapping, failures)?;
self.slots[slot] = Some(pool);
self.outdated.push(self.current);
self.current = slot;
self.logger.log_memory(
|level| !matches!(level, MemoryLogLevel::Disabled),
|| {
format!(
"[{}] Carving pages of {} from here on:\n{self}",
self.name,
BytesFormat::new(page_size)
)
},
);
Ok(handle)
}
}
impl core::fmt::Display for PoolArena {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for pool in self.slots.iter().flatten() {
write!(f, "{pool}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_management::relocation::CopyQueue;
use crate::server::ServerError;
use crate::storage::{StorageHandle, StorageId, StorageUtilization};
const MIB: u64 = 1024 * 1024;
fn sizing() -> PageSizing {
PageSizing::new(2 * MIB, &MemoryDeviceProperties::new(16 * MIB, 32))
}
struct Bookkeeping {
limit: u64,
}
impl ComputeStorage for Bookkeeping {
type Resource = ();
fn alignment(&self) -> usize {
32
}
fn get(&mut self, _handle: &StorageHandle) -> Result<(), IoError> {
Ok(())
}
fn alloc(&mut self, size: u64) -> Result<StorageHandle, IoError> {
if size > self.limit {
return Err(IoError::OutOfMemory {
size,
backtrace: BackTrace::capture(),
});
}
Ok(StorageHandle::new(
StorageId::new(),
StorageUtilization { offset: 0, size },
))
}
fn dealloc(&mut self, _id: StorageId) {}
fn flush(&mut self) {}
fn bytes_allocated(&self) -> u64 {
0
}
}
struct Landing;
impl CopyQueue<Bookkeeping> for Landing {
fn wait_device(&mut self) -> Result<(), ServerError> {
Ok(())
}
fn copy(&mut self, _storage: &mut Bookkeeping, _copy: &StorageCopy) -> Result<(), IoError> {
Ok(())
}
fn wait_copies(&mut self) -> Result<(), ServerError> {
Ok(())
}
}
fn reserve(
arena: &mut PoolArena,
storage: &mut Bookkeeping,
size: u64,
) -> Result<ManagedMemoryHandle, IoError> {
arena.reserve(
storage,
size,
PageMapping::Eager,
&mut ErrorGraph::default(),
)
}
#[test]
fn a_refused_growth_leaves_the_page_size_where_it_was() {
let mut storage = Bookkeeping { limit: 8 * MIB };
let mut arena = PoolArena::new(sizing(), 2, Default::default(), String::new());
let _small = reserve(&mut arena, &mut storage, MIB).unwrap();
let refused = reserve(&mut arena, &mut storage, 10 * MIB);
assert!(
matches!(refused, Err(IoError::OutOfMemory { .. })),
"{refused:?}"
);
assert_eq!(arena.state().page_size, 2 * MIB);
assert!(!arena.state().has_outdated);
let _served = reserve(&mut arena, &mut storage, MIB).unwrap();
assert_eq!(arena.current().pages_held(), 1, "served from the page held");
}
#[test]
fn a_full_arena_refuses_to_grow_until_a_relocation_frees_it() {
let mut storage = Bookkeeping { limit: u64::MAX };
let sizing = PageSizing::new(2 * MIB, &MemoryDeviceProperties::new(1024 * MIB, 32));
let mut arena = PoolArena::new(sizing, 2, Default::default(), String::new());
let failures = &mut ErrorGraph::default();
let mut live = Vec::new();
for step in 0..ARENA_SLOTS as u64 - 1 {
live.push(reserve(&mut arena, &mut storage, (3 + 2 * step) * MIB).unwrap());
}
assert!(arena.state().full);
let refused = reserve(&mut arena, &mut storage, 200 * MIB);
assert!(
matches!(refused, Err(IoError::PageSizesExhausted { .. })),
"{refused:?}"
);
let relocation = arena.plan(TargetRoom::MayAllocate, &mut storage, failures);
assert_eq!(
relocation.len(),
ARENA_SLOTS - 2,
"one allocation per outdated pool that carved a page"
);
let landed = relocation.copy(&mut storage, &mut Landing).unwrap();
arena.commit(landed, &mut storage, failures);
assert!(!arena.state().has_outdated);
reserve(&mut arena, &mut storage, 200 * MIB).expect("a slot is free again");
}
#[test]
fn an_allocation_that_fits_the_pages_grows_nothing() {
assert_eq!(sizing().fit(MIB, 2 * MIB), Fit::Fits);
}
#[test]
fn a_growth_sizes_pages_to_the_allocation_with_slack() {
assert_eq!(
sizing().fit(10 * MIB, 2 * MIB),
Fit::Grow {
page_size: 11 * MIB
}
);
}
#[test]
fn a_growth_stops_at_the_devices_largest_page() {
assert_eq!(
sizing().fit(16 * MIB, 2 * MIB),
Fit::Grow {
page_size: 16 * MIB
}
);
assert_eq!(sizing().fit(16 * MIB + 1, 2 * MIB), Fit::TooLarge);
}
}