Skip to main content

cubecl_server/memory_management/
memory_manage.rs

1use super::{
2    InstallMemoryPoolsError, ManagedMemoryBinding, ManagedMemoryHandle, ManagedMemoryId,
3    MemoryAllocationMode, MemoryConfiguration, MemoryReport, MemoryUsage, PERSISTENT_POOL_POS,
4    PoolType,
5    memory_pool::{
6        DirectPool, ExclusiveMemoryPool, MemoryPool, PageMapping, PersistentPool, SlicedPool,
7    },
8};
9use crate::{
10    config::{
11        CubeClRuntimeConfig, RuntimeConfig,
12        memory::{MemoryLogLevel, PersistentMemory},
13    },
14    logging::ServerLogger,
15    memory_management::{BytesFormat, ErrorGraph, FailureId, memory_pool::Slice},
16    server::IoError,
17    storage::{ComputeStorage, StorageHandle},
18};
19
20use alloc::format;
21use alloc::string::{String, ToString};
22use alloc::vec::Vec;
23use core::ops::Range;
24use cubecl_environment::backtrace::BackTrace;
25use cubecl_environment::collections::HashSet;
26use cubecl_environment::sync::Arc;
27use cubecl_ir::MemoryDeviceProperties;
28
29// These are 288 bytes vs 64 bytes. Adding boxing isn't really worth
30// saving the 200 bytes.
31#[allow(clippy::large_enum_variant)]
32enum DynamicPool {
33    Sliced(SlicedPool),
34    Exclusive(ExclusiveMemoryPool),
35    Direct(DirectPool),
36}
37
38impl MemoryPool for DynamicPool {
39    fn accept(&self, size: u64) -> bool {
40        match self {
41            DynamicPool::Sliced(pool) => pool.accept(size),
42            DynamicPool::Exclusive(pool) => pool.accept(size),
43            DynamicPool::Direct(pool) => pool.accept(size),
44        }
45    }
46
47    fn find(&self, binding: &ManagedMemoryBinding) -> Result<&Slice, IoError> {
48        match self {
49            DynamicPool::Sliced(m) => m.find(binding),
50            DynamicPool::Exclusive(m) => m.find(binding),
51            DynamicPool::Direct(m) => m.find(binding),
52        }
53    }
54
55    fn find_mut(&mut self, binding: &ManagedMemoryBinding) -> Result<&mut Slice, IoError> {
56        match self {
57            DynamicPool::Sliced(m) => m.find_mut(binding),
58            DynamicPool::Exclusive(m) => m.find_mut(binding),
59            DynamicPool::Direct(m) => m.find_mut(binding),
60        }
61    }
62
63    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
64    fn try_reserve(&mut self, size: u64, failures: &mut ErrorGraph) -> Option<ManagedMemoryHandle> {
65        match self {
66            DynamicPool::Sliced(m) => m.try_reserve(size, failures),
67            DynamicPool::Exclusive(m) => m.try_reserve(size, failures),
68            DynamicPool::Direct(m) => m.try_reserve(size, failures),
69        }
70    }
71
72    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
73    fn alloc<Storage: ComputeStorage>(
74        &mut self,
75        storage: &mut Storage,
76        size: u64,
77        mapping: PageMapping,
78        failures: &mut ErrorGraph,
79    ) -> Result<ManagedMemoryHandle, IoError> {
80        match self {
81            DynamicPool::Sliced(m) => m.alloc(storage, size, mapping, failures),
82            DynamicPool::Exclusive(m) => m.alloc(storage, size, mapping, failures),
83            DynamicPool::Direct(m) => m.alloc(storage, size, mapping, failures),
84        }
85    }
86
87    fn materialize<Storage: ComputeStorage>(
88        &mut self,
89        storage: &mut Storage,
90        binding: &ManagedMemoryBinding,
91    ) -> Result<(), IoError> {
92        match self {
93            DynamicPool::Sliced(m) => m.materialize(storage, binding),
94            DynamicPool::Exclusive(m) => m.materialize(storage, binding),
95            DynamicPool::Direct(m) => m.materialize(storage, binding),
96        }
97    }
98
99    fn get_memory_usage(&self) -> MemoryUsage {
100        match self {
101            DynamicPool::Sliced(m) => m.get_memory_usage(),
102            DynamicPool::Exclusive(m) => m.get_memory_usage(),
103            DynamicPool::Direct(m) => m.get_memory_usage(),
104        }
105    }
106
107    fn cleanup<Storage: ComputeStorage>(
108        &mut self,
109        storage: &mut Storage,
110        alloc_nr: u64,
111        explicit: bool,
112        failures: &mut ErrorGraph,
113    ) {
114        match self {
115            DynamicPool::Sliced(m) => m.cleanup(storage, alloc_nr, explicit, failures),
116            DynamicPool::Exclusive(m) => m.cleanup(storage, alloc_nr, explicit, failures),
117            DynamicPool::Direct(m) => m.cleanup(storage, alloc_nr, explicit, failures),
118        };
119        storage.flush();
120    }
121
122    fn bind(
123        &mut self,
124        reserved: ManagedMemoryHandle,
125        assigned: ManagedMemoryHandle,
126        cursor: u64,
127        failures: &mut ErrorGraph,
128    ) -> Result<(), IoError> {
129        match self {
130            DynamicPool::Sliced(m) => m.bind(reserved, assigned, cursor, failures),
131            DynamicPool::Exclusive(m) => m.bind(reserved, assigned, cursor, failures),
132            DynamicPool::Direct(m) => m.bind(reserved, assigned, cursor, failures),
133        }
134    }
135}
136
137impl core::fmt::Display for DynamicPool {
138    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139        match self {
140            DynamicPool::Sliced(pool) => write!(f, "{pool}"),
141            DynamicPool::Exclusive(pool) => write!(f, "{pool}"),
142            DynamicPool::Direct(pool) => write!(f, "{pool}"),
143        }
144    }
145}
146
147impl DynamicPool {
148    fn report(&self) -> super::MemoryPoolReport {
149        match self {
150            DynamicPool::Sliced(m) => m.report(),
151            DynamicPool::Exclusive(m) => m.report(),
152            DynamicPool::Direct(m) => m.report(),
153        }
154    }
155}
156
157/// Reserves and keeps track of chunks of memory in the storage, and slices upon these chunks.
158pub struct MemoryManagement<Storage> {
159    name: String,
160    persistent: PersistentPool,
161    pools: Vec<DynamicPool>,
162    /// Dynamic pools that have already reported hitting their cap, so the
163    /// warning stays one per pool per layout rather than one per allocation.
164    /// Cleared by [`install_pools`](Self::install_pools).
165    capacity_warned: HashSet<usize>,
166    storage: Storage,
167    alloc_reserve_count: u64,
168    mode: MemoryAllocationMode,
169    /// Open persistent windows (see [`mode`](Self::mode)): the effective mode
170    /// stays `Persistent` until every nested window has closed.
171    persistent_windows: u64,
172    config: PersistentMemory,
173    logger: Arc<ServerLogger>,
174    /// State of the active graph capture, if any.
175    capture: Option<CaptureState>,
176}
177
178/// While a graph capture is active, allocations are forced into the persistent
179/// pool; slices there stay freely reusable during the window (warmup populates
180/// them, the capture run reuses them), and `capture_end` hands the graph exactly
181/// the slices the window touched.
182struct CaptureState {
183    /// The mode to restore at `capture_end`. Mid-capture [`mode`] changes land
184    /// here instead of taking effect, so they can't reroute capture allocations
185    /// away from the persistent pool.
186    restore_mode: MemoryAllocationMode,
187    /// Ids of every persistent slice handed out (reserved or freshly allocated)
188    /// while the window was open — exactly the slices the graph's recorded
189    /// kernels may replay against. `capture_end` retains these and nothing else,
190    /// so a slice the window never touched is not over-retained, and a
191    /// pre-existing slice freed and reused mid-window is still pinned.
192    touched: HashSet<ManagedMemoryId>,
193    /// Whether the warmup (priming) phase is still running, i.e. the capture window has not opened
194    /// yet. While set, every slice handed out is retained in `primed` instead of being recycled.
195    priming: bool,
196    /// Slices retained during priming, released by
197    /// [`capture_priming_end`](MemoryManagement::capture_priming_end).
198    ///
199    /// Warmup exists to leave the pool able to serve the recorded run without allocating — an
200    /// allocation inside the window is recorded as a memory node, and CUDA refuses to relaunch a
201    /// graph holding one. Letting warmup recycle its own slices defeats that: the pool only ever
202    /// grows to a warmup pass's transient *peak*, which depends on how far the host runs ahead of
203    /// the device and can land below what the recorded run asks for. Holding every slice instead
204    /// forces the pool up to the pass's full distinct working set — an upper bound on any peak —
205    /// so once these are released the recorded run cannot ask for a slice the pool lacks.
206    primed: Vec<ManagedMemoryHandle>,
207}
208
209/// The options for creating a new [`MemoryManagement`] instance.
210#[derive(Debug)]
211pub struct MemoryManagementOptions {
212    /// The name of the memory management.
213    name: String,
214    /// The [`MemoryAllocationOption`] used by this instance.
215    memory: MemoryAllocationOption,
216}
217
218impl MemoryManagementOptions {
219    /// Creates a new [`MemoryManagementOptions`].
220    pub fn new<S: Into<String>>(name: S) -> Self {
221        Self {
222            name: name.into(),
223            memory: MemoryAllocationOption::FromConfig,
224        }
225    }
226
227    /// Forces the [`MemoryAllocationMode`] during execution to always be the provided one.
228    pub fn mode(mut self, mode: MemoryAllocationMode) -> Self {
229        self.memory = MemoryAllocationOption::Provided(mode);
230        self
231    }
232}
233
234#[derive(Default, Debug)]
235/// Determines which [`MemoryAllocationMode`] is used during allocations.
236enum MemoryAllocationOption {
237    #[default]
238    /// Uses the [`GlobalConfig`] to determine the mode of allocation.
239    FromConfig,
240    /// Use the provided [`MemoryAllocationMode`].
241    Provided(MemoryAllocationMode),
242}
243
244impl<Storage: ComputeStorage> MemoryManagement<Storage> {
245    /// Creates the options from device limits.
246    pub fn from_configuration(
247        storage: Storage,
248        properties: &MemoryDeviceProperties,
249        config: MemoryConfiguration,
250        logger: Arc<ServerLogger>,
251        options: MemoryManagementOptions,
252    ) -> Self {
253        let pools = build_pools(properties, config, &logger, &options.name);
254
255        let config = CubeClRuntimeConfig::get().memory.persistent_memory.clone();
256
257        let mode = match options.memory {
258            MemoryAllocationOption::Provided(mode) => mode,
259            MemoryAllocationOption::FromConfig => match config {
260                PersistentMemory::Enabled | PersistentMemory::SizeMatch => {
261                    MemoryAllocationMode::Auto
262                }
263                PersistentMemory::Disabled => MemoryAllocationMode::Auto,
264                PersistentMemory::Enforced => MemoryAllocationMode::Persistent,
265            },
266        };
267
268        Self {
269            name: options.name,
270            persistent: PersistentPool::new(
271                properties.max_page_size,
272                properties.alignment,
273                PERSISTENT_POOL_POS,
274            ),
275            pools,
276            capacity_warned: HashSet::new(),
277            storage,
278            alloc_reserve_count: 0,
279            mode,
280            persistent_windows: 0,
281            config,
282            logger,
283            capture: None,
284        }
285    }
286
287    /// Replace the dynamic pools with ones built from a new layout.
288    ///
289    /// The old pools are cleaned up first (every currently-free page returned
290    /// to the driver) and then discarded — this installs new pools, it does
291    /// not re-tune the existing ones. That is why it only happens when no live
292    /// allocation remains in them: a live slice carries its pool position, so
293    /// swapping the pool list under it would leave that position pointing at a
294    /// different pool. The caller installs at a quiescent point (e.g. right
295    /// after unloading a model), so a refusal is the exceptional path, not the
296    /// normal one.
297    ///
298    /// Rebuilding resets each pool's high-water marks, which is what lets a
299    /// measured plan be read from a pass that follows a rebuild rather than
300    /// from the process's whole history.
301    ///
302    /// The persistent pool is untouched: its slices route through a fixed
303    /// sentinel position and its layout is model-agnostic.
304    ///
305    /// # Errors
306    ///
307    /// [`PoolsInUse`](InstallMemoryPoolsError::PoolsInUse) when something is
308    /// still live in the dynamic pools; the old layout is kept and nothing is
309    /// disturbed. Retry once the work holding them drains.
310    pub fn install_pools(
311        &mut self,
312        config: MemoryConfiguration,
313        properties: &MemoryDeviceProperties,
314        failures: &mut ErrorGraph,
315    ) -> Result<(), InstallMemoryPoolsError> {
316        self.cleanup(true, failures);
317
318        // Only the dynamic pools are rebuilt, so only their live slices block
319        // (persistent usage — weights of another workload — doesn't).
320        let dynamic_in_use: u64 = self
321            .pools
322            .iter()
323            .map(|pool| pool.get_memory_usage().bytes_in_use)
324            .sum();
325        if dynamic_in_use > 0 {
326            return Err(InstallMemoryPoolsError::PoolsInUse {
327                bytes_in_use: dynamic_in_use,
328            });
329        }
330
331        self.pools = build_pools(properties, config, &self.logger, &self.name);
332        // A new layout is a new plan, and whether it is short is a fresh
333        // question — every pool gets to report its first spill again.
334        self.capacity_warned.clear();
335        Ok(())
336    }
337
338    /// Begin a graph capture: force every allocation into the persistent pool
339    /// — exact-fit slices with no bucket padding, which is what a graph's
340    /// static shapes want — and start recording which slices the window hands
341    /// out (see [`reserve`](Self::reserve)). Every slice the window touches
342    /// belongs to the graph at [`capture_end`](Self::capture_end); anything it
343    /// never touches (pre-existing live buffers, idle free slices) does not.
344    /// Slices stay reusable *within* the window — warmup populates the pool, then
345    /// the capture run reuses those slices without a fresh device allocation
346    /// (illegal mid-capture). Sets the mode directly, overriding the config gate
347    /// that [`mode`](Self::mode) honors. If a capture is already active, only the
348    /// mode is re-forced — the original capture keeps its touched set and restore
349    /// state.
350    pub fn capture_begin(&mut self) {
351        if self.capture.is_none() {
352            self.capture = Some(CaptureState {
353                restore_mode: self.mode,
354                touched: HashSet::new(),
355                priming: true,
356                primed: Vec::new(),
357            });
358        }
359        self.mode = MemoryAllocationMode::Persistent;
360    }
361
362    /// End the priming phase and release the slices warmup retained, returning them to the pool as
363    /// free. Call immediately before the capture window opens.
364    ///
365    /// After this the pool holds every slice a warmup pass touched, all of them free, so the
366    /// recorded run reuses them instead of growing the pool (see [`CaptureState::primed`]). No-op
367    /// when no capture is active or priming already ended.
368    pub fn capture_priming_end(&mut self) {
369        if let Some(capture) = &mut self.capture {
370            capture.priming = false;
371            // Dropping the handles makes the slices free again; the slices themselves stay in the
372            // pool, which is the point.
373            capture.primed.clear();
374        }
375    }
376
377    /// End a graph capture: restore the previous allocation mode and return a
378    /// retained handle to every persistent slice the window touched — exactly
379    /// the memory the graph's recorded kernels replay against. The caller pins
380    /// these on the graph so the pool never reuses graph memory (which a replay
381    /// would corrupt); dropping the graph drops the handles and releases the
382    /// slices. Slices the window never touched are left alone, so a pre-existing
383    /// live buffer keeps its reuse and in-place (`can_mut`) semantics. Empty if
384    /// no capture was active.
385    pub fn capture_end(&mut self) -> Vec<ManagedMemoryHandle> {
386        match self.capture.take() {
387            Some(capture) => {
388                self.mode = capture.restore_mode;
389                self.persistent.retain_touched(&capture.touched)
390            }
391            None => Vec::new(),
392        }
393    }
394
395    /// Change the mode of allocation.
396    ///
397    /// Persistent windows **nest**: a `Persistent` call opens one, an `Auto`
398    /// call closes one, and the effective mode stays `Persistent` while any
399    /// window is open. Callers routinely nest without knowing it — a module
400    /// load opens a window around the whole load while the parameter machinery
401    /// underneath opens one per parameter — and without the depth, the first
402    /// inner window's exit would flip the rest of the outer window back to
403    /// `Auto`: weights landing in the dynamic pools, which then refuse every
404    /// later rebuild ([`install_pools`](Self::install_pools)) for the model's whole
405    /// life.
406    pub fn mode(&mut self, mode: MemoryAllocationMode) {
407        // We override the mode based on the cubecl config.
408        let mode = match self.config {
409            PersistentMemory::Enabled | PersistentMemory::SizeMatch => mode,
410            PersistentMemory::Disabled | PersistentMemory::Enforced => return,
411        };
412
413        match mode {
414            MemoryAllocationMode::Persistent => self.persistent_windows += 1,
415            MemoryAllocationMode::Auto => {
416                self.persistent_windows = self.persistent_windows.saturating_sub(1)
417            }
418        }
419        let mode = match self.persistent_windows > 0 {
420            true => MemoryAllocationMode::Persistent,
421            false => MemoryAllocationMode::Auto,
422        };
423
424        self.logger.log_memory(
425            |level| !matches!(level, MemoryLogLevel::Disabled),
426            || {
427                format!(
428                    "[{}] Setting memory allocation mode: from {:?} => {mode:?}",
429                    self.name, self.mode
430                )
431            },
432        );
433
434        // A capture owns the effective mode until it ends: changing it now
435        // would route capture allocations away from the persistent pool. Defer
436        // the change to `capture_end`.
437        match &mut self.capture {
438            Some(capture) => capture.restore_mode = mode,
439            None => self.mode = mode,
440        }
441    }
442
443    /// Cleanup allocations in pools that are deemed unnecessary.
444    pub fn cleanup(&mut self, explicit: bool, failures: &mut ErrorGraph) {
445        self.logger.log_memory(
446            |level| !matches!(level, MemoryLogLevel::Disabled) && explicit,
447            || "Manual memory cleanup ...".to_string(),
448        );
449
450        // Nothing may be freed during a capture. The persistent window's free
451        // slices are exactly what the capture run reuses (deallocating one
452        // forces a fresh device allocation mid-capture, which faults), and
453        // the storage frees behind the dynamic pools can synchronize the
454        // device (e.g. `hipFree`), which invalidates the capture. Everything
455        // stays queued until the capture ends.
456        if self.capture.is_some() {
457            return;
458        }
459
460        self.persistent.cleanup(
461            &mut self.storage,
462            self.alloc_reserve_count,
463            explicit,
464            failures,
465        );
466
467        for pool in self.pools.iter_mut() {
468            pool.cleanup(
469                &mut self.storage,
470                self.alloc_reserve_count,
471                explicit,
472                failures,
473            );
474        }
475
476        // The pools only queue their page deallocations in the storage; an
477        // explicit cleanup means "release the memory now", so push them to the
478        // driver instead of leaving them pending.
479        if explicit {
480            self.storage.flush();
481        }
482    }
483
484    /// Returns the storage from the specified binding
485    pub fn get_cursor(&self, binding: ManagedMemoryBinding) -> Result<u64, IoError> {
486        let slice = self.find(&binding)?;
487        Ok(slice.cursor)
488    }
489
490    /// Returns the storage from the specified binding
491    fn find(&self, binding: &ManagedMemoryBinding) -> Result<&Slice, IoError> {
492        let id = binding.descriptor();
493
494        if !id.is_allocated() {
495            return Err(IoError::NotFound {
496                backtrace: BackTrace::capture(),
497                reason: "Memory location was never initialized".into(),
498            });
499        }
500
501        let slice = if id.location().pool == PERSISTENT_POOL_POS {
502            self.persistent.find(binding)?
503        } else {
504            let pool =
505                self.pools
506                    .get(id.location().pool as usize)
507                    .ok_or_else(|| IoError::NotFound {
508                        backtrace: BackTrace::capture(),
509                        reason: format!("Pool {} doesn't exist", id.location().pool).into(),
510                    })?;
511
512            pool.find(binding)?
513        };
514
515        // A stale location (e.g. a page that was deallocated and whose index a
516        // later cleanup reassigned) must surface as `NotFound`, never as another
517        // allocation's slice.
518        if slice.handle.descriptor() != binding.descriptor() {
519            return Err(IoError::NotFound {
520                backtrace: BackTrace::capture(),
521                reason: "Memory location points to a different allocation".into(),
522            });
523        }
524
525        Ok(slice)
526    }
527
528    /// [`find`](Self::find), mutably — the path [`taint`](Self::taint) and
529    /// [`written`](Self::written) take to reach the slice.
530    fn find_mut(&mut self, binding: &ManagedMemoryBinding) -> Result<&mut Slice, IoError> {
531        let id = binding.descriptor();
532
533        if !id.is_allocated() {
534            return Err(IoError::NotFound {
535                backtrace: BackTrace::capture(),
536                reason: "Memory location was never initialized".into(),
537            });
538        }
539
540        let slice = if id.location().pool == PERSISTENT_POOL_POS {
541            self.persistent.find_mut(binding)?
542        } else {
543            let pool = self
544                .pools
545                .get_mut(id.location().pool as usize)
546                .ok_or_else(|| IoError::NotFound {
547                    backtrace: BackTrace::capture(),
548                    reason: format!("Pool {} doesn't exist", id.location().pool).into(),
549                })?;
550
551            pool.find_mut(binding)?
552        };
553
554        // The same stale-location rule as `find`.
555        if slice.handle.descriptor() != binding.descriptor() {
556            return Err(IoError::NotFound {
557                backtrace: BackTrace::capture(),
558                reason: "Memory location points to a different allocation".into(),
559            });
560        }
561
562        Ok(slice)
563    }
564
565    /// Returns the storage from the specified binding.
566    ///
567    /// This is the funnel every buffer dereference passes through
568    /// ([`get_resource`](Self::get_resource) delegates here), so it is where
569    /// a lazily-carved allocation gets its real device backing: the handle
570    /// returned always refers to mapped memory.
571    pub fn get_storage(&mut self, binding: ManagedMemoryBinding) -> Result<StorageHandle, IoError> {
572        self.materialize(&binding)?;
573        let slice = self.find(&binding)?;
574        Ok(slice.storage.clone())
575    }
576
577    /// Install real backing behind `binding` when its allocation was carved
578    /// lazily under a dry run. Lookup errors are left for
579    /// [`find`](Self::find) to report with its usual diagnostics.
580    fn materialize(&mut self, binding: &ManagedMemoryBinding) -> Result<(), IoError> {
581        let location = binding.descriptor().location();
582        if location.init == 0 {
583            return Ok(());
584        }
585        match location.pool {
586            PERSISTENT_POOL_POS => self.persistent.materialize(&mut self.storage, binding),
587            pool => match self.pools.get_mut(pool as usize) {
588                Some(pool) => pool.materialize(&mut self.storage, binding),
589                None => Ok(()),
590            },
591        }
592    }
593
594    /// Returns the resource from the storage at the specified handle
595    pub fn get_resource(
596        &mut self,
597        binding: ManagedMemoryBinding,
598        offset_start: Option<u64>,
599        offset_end: Option<u64>,
600    ) -> Result<Storage::Resource, IoError> {
601        let handle = self.get_storage(binding)?;
602
603        let handle = match offset_start {
604            Some(offset) => handle.offset_start(offset),
605            None => handle,
606        };
607        let handle = match offset_end {
608            Some(offset) => handle.offset_end(offset),
609            None => handle,
610        };
611        self.storage().get(&handle)
612    }
613
614    /// Record a persistent slice as touched by the active capture window, so
615    /// [`capture_end`](Self::capture_end) retains exactly the slices the window
616    /// handed out. A no-op outside a capture.
617    ///
618    /// Called with a slice's **final** identity: from [`reserve`](Self::reserve)
619    /// for a handle used as-is (e.g. pinned staging), and from [`bind`](Self::bind)
620    /// for a buffer whose reserved handle is replaced by an assigned one. A
621    /// reserved id later superseded by `bind` also lands here but harmlessly —
622    /// ids are unique, so it matches no live slice at `capture_end`.
623    fn capture_touch(&mut self, handle: &ManagedMemoryHandle) {
624        if let Some(capture) = &mut self.capture {
625            capture.touched.insert(handle.descriptor().id);
626            if capture.priming {
627                // Retain it so warmup cannot recycle this slice, forcing the pool to grow to the
628                // pass's full working set rather than its transient peak.
629                capture.primed.push(handle.clone());
630            }
631        }
632    }
633
634    /// Finds a spot in memory for a resource with the given size in bytes, and returns a handle to it
635    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
636    pub fn reserve(
637        &mut self,
638        size: u64,
639        failures: &mut ErrorGraph,
640    ) -> Result<ManagedMemoryHandle, IoError> {
641        // If this happens every nanosecond, counts overflows after 585 years, so not worth thinking too
642        // hard about overflow here.
643        self.alloc_reserve_count += 1;
644
645        // Drive the pools' periodic deallocation. Each pool gates itself on
646        // its own `dealloc_period` (pools without one no-op), so this is a few
647        // comparisons per reservation — without it, pages freed long ago are
648        // never returned to the driver until an explicit cleanup, which on
649        // long-running processes lets every stream's pools grow monotonically.
650        self.cleanup(false, failures);
651
652        let mapping = PageMapping::current();
653
654        // In an explicit persistent window the pool always serves the
655        // allocation (reusing a freed same-size slice when one exists).
656        // Outside a window, the pool participates only under the `size-match`
657        // config: recurring weight-shaped allocations reuse the buckets — a
658        // training-friendly heuristic that would otherwise pull inference
659        // activations into exact-sized persistent slices.
660        let persistent_mode = matches!(self.mode, MemoryAllocationMode::Persistent);
661        let size_match = matches!(self.config, PersistentMemory::SizeMatch);
662
663        if (persistent_mode || size_match)
664            && let Some(val) = self.persistent.try_reserve(size, failures)
665        {
666            self.logger.log_memory(
667                |level| matches!(level, MemoryLogLevel::Full),
668                || {
669                    format!(
670                        "[{}] Reserved memory {size} using persistent memory",
671                        self.name
672                    )
673                },
674            );
675            self.capture_touch(&val);
676            return Ok(val);
677        }
678
679        if persistent_mode || (size_match && self.persistent.has_size(size)) {
680            let allocated = self
681                .persistent
682                .alloc(&mut self.storage, size, mapping, failures);
683
684            self.logger.log_memory(
685                |level| !matches!(level, MemoryLogLevel::Disabled),
686                || {
687                    format!(
688                        "[{}] Allocated a new memory page using persistent memory, \n{}",
689                        self.name, self,
690                    )
691                },
692            );
693            if let Ok(handle) = &allocated {
694                self.capture_touch(handle);
695            }
696            return allocated;
697        }
698
699        self.logger.log_memory(
700            |level| matches!(level, MemoryLogLevel::Full),
701            || {
702                format!(
703                    "[{}] Reserved memory {} using dynamic pool",
704                    self.name,
705                    BytesFormat::new(size)
706                )
707            },
708        );
709
710        // Serve from the first pool that accepts this size and has capacity. A
711        // hard-capped pool that is full falls through to the next accepting
712        // pool instead of failing outright, so a growable tail pool can act as
713        // an escape hatch behind a measured arena. Deliberate: a cap is a plan,
714        // and a plan that turns out to be short should cost memory, not kill
715        // the workload. Where the cap is a hard budget rather than a plan,
716        // configure no pool behind it — then a full pool still errors, which is
717        // what keeps the budget-vs-device-OOM distinction schedulers rely on.
718        let mut capacity_exceeded = None;
719        let mut reserved = None;
720
721        for (index, pool) in self
722            .pools
723            .iter_mut()
724            .enumerate()
725            .filter(|(_, pool)| pool.accept(size))
726        {
727            if let Some(slice) = pool.try_reserve(size, failures) {
728                return Ok(slice);
729            }
730
731            match pool.alloc(&mut self.storage, size, mapping, failures) {
732                Ok(handle) => {
733                    reserved = Some(handle);
734                    break;
735                }
736                Err(err @ IoError::PoolCapacityExceeded { .. }) => {
737                    // Loud on purpose: a spill means the cap was under-planned
738                    // (e.g. a workload the dry run never measured), and the
739                    // escape hatch serving it must not hide that. Once per pool
740                    // per layout, though — a workload that runs above its cap
741                    // spills on *every* reservation, and a warning per
742                    // allocation buries the one that mattered. `install_pools`
743                    // clears the latch: a new layout is a new plan to judge.
744                    if self.capacity_warned.insert(index) {
745                        log::warn!(
746                            "[{}] memory pool {index} is at capacity (first hit at an \
747                             allocation of {size} B); spilling to the next accepting pool. \
748                             The measured plan is short for this workload.",
749                            self.name
750                        );
751                    }
752                    capacity_exceeded = Some(err);
753                }
754                // A spill already in hand is the more useful diagnosis: it says
755                // the plan was short, where this one only says the pool behind
756                // it also failed.
757                Err(err) => return Err(capacity_exceeded.unwrap_or(err)),
758            }
759        }
760
761        let Some(reserved) = reserved else {
762            return Err(capacity_exceeded.unwrap_or_else(|| IoError::BufferTooBig {
763                size,
764                backtrace: BackTrace::capture(),
765            }));
766        };
767
768        self.logger.log_memory(
769            |level| matches!(level, MemoryLogLevel::Full),
770            || {
771                format!(
772                    "[{}], Allocated a new memory page, current usage: \n{}",
773                    self.name, self
774                )
775            },
776        );
777
778        Ok(reserved)
779    }
780
781    /// Fetch the storage used by the memory manager.
782    ///
783    /// # Notes
784    ///
785    /// The storage should probably not be used for allocations since the handles won't be
786    /// compatible with the ones provided by the current trait. Prefer using the
787    /// [alloc](ComputeStorage::alloc) and [dealloc](ComputeStorage::dealloc) functions.
788    ///
789    /// This is useful if you need to time the deallocations based on async computation, or to
790    /// change the mode of storage for different reasons.
791    pub fn storage(&mut self) -> &mut Storage {
792        &mut self.storage
793    }
794
795    /// Get the current memory usage.
796    pub fn memory_usage(&self) -> MemoryUsage {
797        let memory_usage = self.pools.iter().map(|x| x.get_memory_usage()).fold(
798            MemoryUsage {
799                number_allocs: 0,
800                bytes_in_use: 0,
801                bytes_padding: 0,
802                bytes_reserved: 0,
803            },
804            |m1, m2| m1.combine(m2),
805        );
806        memory_usage.combine(self.persistent.get_memory_usage())
807    }
808
809    /// A structured per-pool report: each pool's shape, usage, and high-water
810    /// marks, in allocation-routing order.
811    ///
812    /// The read side of a measured memory plan — the cycle, and what the
813    /// marks cover, is on [`MemoryReport`].
814    pub fn memory_report(&self) -> MemoryReport {
815        MemoryReport {
816            dynamic: self.pools.iter().map(|pool| pool.report()).collect(),
817            persistent: self.persistent.report(),
818        }
819    }
820
821    /// Print out a report of the current memory usage.
822    pub fn print_memory_usage(&self) {
823        #[cfg(feature = "std")]
824        log::info!("{}", self.memory_usage());
825    }
826
827    /// Binds the given [handle](HandleId) to a [`MemorySlot`].
828    pub fn bind(
829        &mut self,
830        reserved: ManagedMemoryHandle,
831        assigned: ManagedMemoryHandle,
832        cursor: u64,
833        failures: &mut ErrorGraph,
834    ) -> Result<(), IoError> {
835        let descriptor = reserved.descriptor();
836
837        if !descriptor.is_allocated() {
838            return Err(IoError::NotFound {
839                backtrace: BackTrace::capture(),
840                reason: "Reserved memory isn't initialized".into(),
841            });
842        }
843
844        let pool_index = descriptor.location().pool as usize;
845        if pool_index == PERSISTENT_POOL_POS as usize {
846            // `bind` sets the slice's final identity to `assigned` (replacing the
847            // throwaway reserved handle), so this — not the earlier `reserve` — is
848            // the id a capture must track for a bound persistent buffer.
849            self.capture_touch(&assigned);
850            return self.persistent.bind(reserved, assigned, cursor, failures);
851        }
852
853        self.pools
854            .get_mut(pool_index)
855            .map(|p| p.bind(reserved, assigned, cursor, failures))
856            .ok_or_else(|| IoError::NotFound {
857                backtrace: BackTrace::capture(),
858                reason: format!("Memory pool {} doesn't exist", pool_index).into(),
859            })?
860    }
861
862    /// The failure claiming any byte of `range` in the allocation behind
863    /// `binding`, if one does.
864    ///
865    /// This is the read path's whole check: a field on a slice the resolution
866    /// walks anyway. A binding this manager does not hold answers `None` —
867    /// lookup errors are [`find`](Self::find)'s to report, and a failed lookup
868    /// carries no failure to name.
869    pub fn failure(&self, binding: &ManagedMemoryBinding, range: Range<u64>) -> Option<FailureId> {
870        self.find(binding)
871            .ok()
872            .and_then(|slice| slice.tainted.failure(&range))
873    }
874
875    /// Point `range` of the allocation behind `binding` at `failure`.
876    ///
877    /// A binding this manager does not hold is left alone, for the same
878    /// reason [`failure`](Self::failure) answers `None` for one.
879    pub fn taint(
880        &mut self,
881        binding: &ManagedMemoryBinding,
882        range: Range<u64>,
883        failure: FailureId,
884        failures: &mut ErrorGraph,
885    ) {
886        if let Ok(slice) = self.find_mut(binding) {
887            slice.tainted.taint(range, failure, failures);
888        }
889    }
890
891    /// `range` of the allocation behind `binding` has a writer again: release
892    /// every claim on those bytes — and only those bytes, since a partial
893    /// write says nothing about the rest of the buffer.
894    pub fn written(
895        &mut self,
896        binding: &ManagedMemoryBinding,
897        range: Range<u64>,
898        failures: &mut ErrorGraph,
899    ) {
900        if let Ok(slice) = self.find_mut(binding) {
901            slice.tainted.written(range, failures);
902        }
903    }
904}
905
906impl<Storage: ComputeStorage> core::fmt::Display for MemoryManagement<Storage> {
907    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
908        f.write_str("\n# MemoryManagement\n\n")?;
909        f.write_fmt(format_args!(" - name: {:?}\n", self.name))?;
910        f.write_fmt(format_args!("\n## Persistent\n\n{}", self.persistent))?;
911        f.write_str("\n## Dynamic\n\n")?;
912
913        for pool in self.pools.iter() {
914            f.write_fmt(format_args!("{pool}\n"))?;
915        }
916        let memory_usage = self.memory_usage();
917        f.write_fmt(format_args!("\n## Summary\n\n{memory_usage}"))?;
918
919        Ok(())
920    }
921}
922
923impl<Storage> core::fmt::Debug for MemoryManagement<Storage> {
924    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
925        f.write_str(
926            alloc::format!(
927                "DynamicMemoryManagement {:?}",
928                core::any::type_name::<Storage>(),
929            )
930            .as_str(),
931        )
932    }
933}
934
935/// Build the dynamic pools for `config` — the shared core of
936/// [`MemoryManagement::from_configuration`] and
937/// [`MemoryManagement::install_pools`].
938fn build_pools(
939    properties: &MemoryDeviceProperties,
940    config: MemoryConfiguration,
941    logger: &Arc<ServerLogger>,
942    name: &str,
943) -> Vec<DynamicPool> {
944    let pool_options = config.pool_options(properties);
945
946    logger.log_memory(
947        |level| !matches!(level, MemoryLogLevel::Disabled),
948        || {
949            let mut msg = String::new();
950            for pool in pool_options.iter() {
951                msg += &format!("[{name}] Using memory pool: \n {pool:?}\n");
952            }
953            msg
954        },
955    );
956
957    assert!(
958        pool_options.len() < PERSISTENT_POOL_POS as usize,
959        "at most {} dynamic pools are supported",
960        PERSISTENT_POOL_POS - 1
961    );
962
963    pool_options
964        .iter()
965        .enumerate()
966        .map(|(pool_pos, pool)| {
967            let pool_pos = pool_pos as u8;
968
969            match pool.pool_type {
970                PoolType::SlicedPages {
971                    page_size,
972                    max_slice_size,
973                    max_pool_size,
974                } => DynamicPool::Sliced(SlicedPool::new(
975                    page_size,
976                    max_slice_size,
977                    properties.alignment,
978                    pool_pos,
979                    max_pool_size,
980                )),
981                PoolType::ExclusivePages { max_alloc_size } => {
982                    DynamicPool::Exclusive(ExclusiveMemoryPool::new(
983                        max_alloc_size,
984                        properties.alignment,
985                        pool.dealloc_period.unwrap_or(u64::MAX),
986                        pool_pos,
987                    ))
988                }
989                PoolType::Direct { reclaim_at } => {
990                    DynamicPool::Direct(DirectPool::new(properties.alignment, pool_pos, reclaim_at))
991                }
992            }
993        })
994        .collect()
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000    use crate::memory_management::MemoryPoolOptions;
1001    use crate::{
1002        config::memory::{MemoryPoolConfig, MemoryPoolsConfig, MemoryPoolsPreset},
1003        memory_management::{MemoryManagement, PoolConfigError},
1004        storage::BytesStorage,
1005    };
1006    use alloc::vec;
1007
1008    const DUMMY_MEM_PROPS: MemoryDeviceProperties =
1009        MemoryDeviceProperties::new(128 * 1024 * 1024, 32);
1010
1011    fn options() -> MemoryManagementOptions {
1012        MemoryManagementOptions {
1013            name: "test".into(),
1014            memory: MemoryAllocationOption::FromConfig,
1015        }
1016    }
1017
1018    // Test pools with slices.
1019    #[test_log::test]
1020    #[cfg(not(exclusive_memory_only))]
1021    fn test_handle_mutability() {
1022        let mut memory_management = MemoryManagement::from_configuration(
1023            BytesStorage::default(),
1024            &DUMMY_MEM_PROPS,
1025            MemoryConfiguration::SubSlices,
1026            Arc::new(ServerLogger::default()),
1027            options(),
1028        );
1029        let handle = memory_management
1030            .reserve(10, &mut ErrorGraph::default())
1031            .unwrap();
1032        let other_ref = handle.clone();
1033        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
1034        drop(other_ref);
1035        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
1036    }
1037
1038    // Test pools with slices.
1039    #[test_log::test]
1040    #[cfg(not(exclusive_memory_only))]
1041    fn test_memory_usage() {
1042        let max_page_size = 512;
1043
1044        let mut memory_management = MemoryManagement::from_configuration(
1045            BytesStorage::default(),
1046            &DUMMY_MEM_PROPS,
1047            MemoryConfiguration::Custom {
1048                pool_options: vec![MemoryPoolOptions {
1049                    pool_type: PoolType::ExclusivePages {
1050                        max_alloc_size: max_page_size,
1051                    },
1052                    dealloc_period: None,
1053                }],
1054            },
1055            Arc::new(ServerLogger::default()),
1056            options(),
1057        );
1058        let handle = memory_management.reserve(100, &mut ErrorGraph::default());
1059        let usage = memory_management.memory_usage();
1060
1061        assert_eq!(usage.bytes_in_use, 100);
1062        assert!(usage.bytes_reserved >= 100 && usage.bytes_reserved <= max_page_size);
1063
1064        // Drop and re-alloc.
1065        drop(handle);
1066        let _handle = memory_management.reserve(100, &mut ErrorGraph::default());
1067        let usage_new = memory_management.memory_usage();
1068        assert_eq!(usage, usage_new);
1069    }
1070
1071    #[test_log::test]
1072    fn find_uninit_binding_returns_not_found() {
1073        let mut memory_management = MemoryManagement::from_configuration(
1074            BytesStorage::default(),
1075            &DUMMY_MEM_PROPS,
1076            MemoryConfiguration::Custom {
1077                pool_options: vec![MemoryPoolOptions {
1078                    pool_type: PoolType::SlicedPages {
1079                        page_size: 2048,
1080                        max_slice_size: 2048,
1081                        max_pool_size: None,
1082                    },
1083                    dealloc_period: None,
1084                }],
1085            },
1086            Arc::new(ServerLogger::default()),
1087            options(),
1088        );
1089
1090        // Even with a live page at index 0, a never-initialized descriptor must
1091        // not resolve to it.
1092        let _live = memory_management
1093            .reserve(512, &mut ErrorGraph::default())
1094            .unwrap();
1095
1096        let binding = ManagedMemoryHandle::new().binding();
1097        assert!(matches!(
1098            memory_management.get_cursor(binding),
1099            Err(IoError::NotFound { .. })
1100        ));
1101    }
1102
1103    #[test_log::test]
1104    fn find_stale_descriptor_returns_not_found() {
1105        let mut memory_management = MemoryManagement::from_configuration(
1106            BytesStorage::default(),
1107            &DUMMY_MEM_PROPS,
1108            MemoryConfiguration::Custom {
1109                pool_options: vec![MemoryPoolOptions {
1110                    pool_type: PoolType::SlicedPages {
1111                        page_size: 2048,
1112                        max_slice_size: 2048,
1113                        max_pool_size: None,
1114                    },
1115                    dealloc_period: None,
1116                }],
1117            },
1118            Arc::new(ServerLogger::default()),
1119            options(),
1120        );
1121
1122        let reserved = memory_management
1123            .reserve(512, &mut ErrorGraph::default())
1124            .unwrap();
1125        let stale = reserved.clone();
1126        let assigned = ManagedMemoryHandle::new();
1127        let assigned_binding = assigned.clone().binding();
1128
1129        memory_management
1130            .bind(reserved, assigned, 0, &mut ErrorGraph::default())
1131            .unwrap();
1132
1133        // The slice's identity is now `assigned`; the stale reserved descriptor
1134        // must surface as `NotFound`, not as the new allocation's data.
1135        assert!(matches!(
1136            memory_management.get_cursor(stale.binding()),
1137            Err(IoError::NotFound { .. })
1138        ));
1139        assert!(memory_management.get_cursor(assigned_binding).is_ok());
1140    }
1141
1142    #[test_log::test]
1143    fn held_binding_survives_explicit_cleanup_renumber() {
1144        let mut memory_management = MemoryManagement::from_configuration(
1145            BytesStorage::default(),
1146            &DUMMY_MEM_PROPS,
1147            MemoryConfiguration::Custom {
1148                pool_options: vec![MemoryPoolOptions {
1149                    pool_type: PoolType::ExclusivePages {
1150                        max_alloc_size: 1024,
1151                    },
1152                    dealloc_period: None,
1153                }],
1154            },
1155            Arc::new(ServerLogger::default()),
1156            options(),
1157        );
1158
1159        let handle_a = memory_management
1160            .reserve(1024, &mut ErrorGraph::default())
1161            .unwrap();
1162        let handle_b = memory_management
1163            .reserve(1024, &mut ErrorGraph::default())
1164            .unwrap();
1165        let handle_c = memory_management
1166            .reserve(1024, &mut ErrorGraph::default())
1167            .unwrap();
1168
1169        let binding_b = handle_b.binding();
1170        drop(handle_a);
1171        drop(handle_c);
1172
1173        // Deallocates the two free pages and renumbers the surviving one.
1174        memory_management.cleanup(true, &mut ErrorGraph::default());
1175
1176        assert!(memory_management.get_cursor(binding_b.clone()).is_ok());
1177        assert!(memory_management.get_storage(binding_b).is_ok());
1178        assert_eq!(memory_management.memory_usage().bytes_reserved, 1024);
1179    }
1180
1181    fn capped_sliced_config(page_size: u64, max_pool_size: Option<u64>) -> MemoryConfiguration {
1182        MemoryConfiguration::Custom {
1183            pool_options: vec![MemoryPoolOptions {
1184                pool_type: PoolType::SlicedPages {
1185                    page_size,
1186                    max_slice_size: page_size,
1187                    max_pool_size,
1188                },
1189                dealloc_period: None,
1190            }],
1191        }
1192    }
1193
1194    #[test_log::test]
1195    fn capped_sliced_pool_errors_instead_of_growing() {
1196        let mut memory_management = MemoryManagement::from_configuration(
1197            BytesStorage::default(),
1198            &DUMMY_MEM_PROPS,
1199            capped_sliced_config(1024, Some(2048)),
1200            Arc::new(ServerLogger::default()),
1201            options(),
1202        );
1203
1204        let _a = memory_management
1205            .reserve(1024, &mut ErrorGraph::default())
1206            .unwrap();
1207        let _b = memory_management
1208            .reserve(1024, &mut ErrorGraph::default())
1209            .unwrap();
1210
1211        let result = memory_management.reserve(1024, &mut ErrorGraph::default());
1212        assert!(matches!(
1213            result,
1214            Err(IoError::PoolCapacityExceeded { capacity: 2048, .. })
1215        ));
1216        assert_eq!(
1217            memory_management.memory_usage().bytes_reserved,
1218            2048,
1219            "a failed reservation must not grow the pool"
1220        );
1221    }
1222
1223    /// What the throughput probes rely on: a persistent window serves buffers
1224    /// the installed layout refuses, however many are held at once, and leaves
1225    /// the layout's budget alone.
1226    #[test_log::test]
1227    fn persistent_window_serves_what_a_capped_layout_refuses() {
1228        let mut memory_management = MemoryManagement::from_configuration(
1229            BytesStorage::default(),
1230            &DUMMY_MEM_PROPS,
1231            capped_sliced_config(1024, Some(1024)),
1232            Arc::new(ServerLogger::default()),
1233            options(),
1234        );
1235
1236        let refused = memory_management.reserve(4096, &mut ErrorGraph::default());
1237        assert!(matches!(refused, Err(IoError::BufferTooBig { .. })));
1238
1239        memory_management.mode(MemoryAllocationMode::Persistent);
1240        let _input = memory_management
1241            .reserve(4096, &mut ErrorGraph::default())
1242            .unwrap();
1243        let _output = memory_management
1244            .reserve(4096, &mut ErrorGraph::default())
1245            .unwrap();
1246        let _line = memory_management
1247            .reserve(16, &mut ErrorGraph::default())
1248            .unwrap();
1249        memory_management.mode(MemoryAllocationMode::Auto);
1250
1251        let report = memory_management.memory_report();
1252        assert_eq!(report.persistent.usage.bytes_in_use, 2 * 4096 + 16);
1253        assert_eq!(report.dynamic[0].pages_peak, 0);
1254    }
1255
1256    #[test_log::test]
1257    fn capped_sliced_pool_reuses_freed_memory() {
1258        let mut memory_management = MemoryManagement::from_configuration(
1259            BytesStorage::default(),
1260            &DUMMY_MEM_PROPS,
1261            capped_sliced_config(1024, Some(2048)),
1262            Arc::new(ServerLogger::default()),
1263            options(),
1264        );
1265
1266        let handle_a = memory_management
1267            .reserve(1024, &mut ErrorGraph::default())
1268            .unwrap();
1269        let _b = memory_management
1270            .reserve(1024, &mut ErrorGraph::default())
1271            .unwrap();
1272        drop(handle_a);
1273
1274        // The capacity error is transient: freeing makes the reservation fit
1275        // again without growing the pool.
1276        let _c = memory_management
1277            .reserve(1024, &mut ErrorGraph::default())
1278            .unwrap();
1279        assert_eq!(memory_management.memory_usage().bytes_reserved, 2048);
1280    }
1281
1282    #[test_log::test]
1283    fn capped_lazy_pool_cleanup_still_frees() {
1284        let mut memory_management = MemoryManagement::from_configuration(
1285            BytesStorage::default(),
1286            &DUMMY_MEM_PROPS,
1287            capped_sliced_config(1024, Some(2048)),
1288            Arc::new(ServerLogger::default()),
1289            options(),
1290        );
1291
1292        let handle_a = memory_management
1293            .reserve(1024, &mut ErrorGraph::default())
1294            .unwrap();
1295        let handle_b = memory_management
1296            .reserve(1024, &mut ErrorGraph::default())
1297            .unwrap();
1298        drop(handle_a);
1299        drop(handle_b);
1300        memory_management.cleanup(true, &mut ErrorGraph::default());
1301        assert_eq!(memory_management.memory_usage().bytes_reserved, 0);
1302
1303        // The cap is still enforced after the pool shrank and regrew.
1304        let _a = memory_management
1305            .reserve(1024, &mut ErrorGraph::default())
1306            .unwrap();
1307        let _b = memory_management
1308            .reserve(1024, &mut ErrorGraph::default())
1309            .unwrap();
1310        assert!(matches!(
1311            memory_management.reserve(1024, &mut ErrorGraph::default()),
1312            Err(IoError::PoolCapacityExceeded { .. })
1313        ));
1314    }
1315
1316    #[test_log::test]
1317    fn max_pool_size_smaller_than_page_shrinks_page() {
1318        let mut memory_management = MemoryManagement::from_configuration(
1319            BytesStorage::default(),
1320            &DUMMY_MEM_PROPS,
1321            capped_sliced_config(2048, Some(512)),
1322            Arc::new(ServerLogger::default()),
1323            options(),
1324        );
1325
1326        let _small = memory_management
1327            .reserve(256, &mut ErrorGraph::default())
1328            .unwrap();
1329        assert!(memory_management.memory_usage().bytes_reserved <= 512);
1330
1331        // Larger than the (shrunk) page: rejected without growing the footprint.
1332        assert!(
1333            memory_management
1334                .reserve(1024, &mut ErrorGraph::default())
1335                .is_err()
1336        );
1337        assert!(memory_management.memory_usage().bytes_reserved <= 512);
1338    }
1339
1340    #[test_log::test]
1341    fn max_pool_size_below_alignment_never_overshoots() {
1342        // A cap below the device alignment (32 in `DUMMY_MEM_PROPS`) can't fit
1343        // even the smallest page, so every reservation must error rather than
1344        // exceed the budget.
1345        let mut memory_management = MemoryManagement::from_configuration(
1346            BytesStorage::default(),
1347            &DUMMY_MEM_PROPS,
1348            capped_sliced_config(1024, Some(16)),
1349            Arc::new(ServerLogger::default()),
1350            options(),
1351        );
1352
1353        assert!(matches!(
1354            memory_management.reserve(8, &mut ErrorGraph::default()),
1355            Err(IoError::PoolCapacityExceeded { .. })
1356        ));
1357        assert_eq!(memory_management.memory_usage().bytes_reserved, 0);
1358    }
1359
1360    /// Persistent windows nest: a module load arms one window around the whole
1361    /// load while the parameter machinery arms one per parameter inside it —
1362    /// an inner window closing must not flip the rest of the outer one back to
1363    /// `Auto`, or most of the load's weights land in the dynamic pools (and
1364    /// block every later `install_pools`).
1365    #[test_log::test]
1366    fn persistent_windows_nest() {
1367        let mut memory_management = MemoryManagement::from_configuration(
1368            BytesStorage::default(),
1369            &DUMMY_MEM_PROPS,
1370            MemoryConfiguration::Custom {
1371                pool_options: vec![MemoryPoolOptions {
1372                    pool_type: PoolType::SlicedPages {
1373                        page_size: 4096,
1374                        max_slice_size: 4096,
1375                        max_pool_size: None,
1376                    },
1377                    dealloc_period: None,
1378                }],
1379            },
1380            Arc::new(ServerLogger::default()),
1381            options(),
1382        );
1383
1384        memory_management.mode(MemoryAllocationMode::Persistent); // the load's window
1385        memory_management.mode(MemoryAllocationMode::Persistent); // one parameter's window
1386        memory_management.mode(MemoryAllocationMode::Auto); // that parameter is done
1387
1388        // Still inside the load's window: the allocation must be persistent.
1389        let weight = memory_management
1390            .reserve(1024, &mut ErrorGraph::default())
1391            .unwrap();
1392        let report = memory_management.memory_report();
1393        assert_eq!(
1394            report.persistent.usage.bytes_in_use, 1024,
1395            "an allocation inside the outer window is persistent"
1396        );
1397        assert_eq!(
1398            report.dynamic[0].pages_peak, 0,
1399            "nothing leaked into the dynamic pools"
1400        );
1401
1402        // The outer window closes; ordinary allocations are dynamic again.
1403        memory_management.mode(MemoryAllocationMode::Auto);
1404        let _transient = memory_management
1405            .reserve(1024, &mut ErrorGraph::default())
1406            .unwrap();
1407        assert_eq!(memory_management.memory_report().dynamic[0].pages_peak, 1);
1408
1409        drop(weight);
1410    }
1411
1412    /// A full capped pool spills to the next accepting pool — with a warning,
1413    /// never silently — instead of failing the allocation.
1414    ///
1415    /// The cap is a measured plan, and a short plan should cost memory rather
1416    /// than kill the workload. When no later pool accepts the size the
1417    /// capacity error still surfaces (see
1418    /// `capped_sliced_pool_errors_instead_of_growing`), which is what keeps
1419    /// the budget-vs-device-OOM distinction schedulers rely on.
1420    #[test_log::test]
1421    fn capacity_overflow_falls_through_to_later_accepting_pool() {
1422        let mut memory_management = MemoryManagement::from_configuration(
1423            BytesStorage::default(),
1424            &DUMMY_MEM_PROPS,
1425            MemoryConfiguration::Custom {
1426                pool_options: vec![
1427                    MemoryPoolOptions {
1428                        pool_type: PoolType::SlicedPages {
1429                            page_size: 1024,
1430                            max_slice_size: 1024,
1431                            max_pool_size: Some(1024),
1432                        },
1433                        dealloc_period: None,
1434                    },
1435                    MemoryPoolOptions {
1436                        pool_type: PoolType::SlicedPages {
1437                            page_size: 1024,
1438                            max_slice_size: 1024,
1439                            max_pool_size: None,
1440                        },
1441                        dealloc_period: None,
1442                    },
1443                ],
1444            },
1445            Arc::new(ServerLogger::default()),
1446            options(),
1447        );
1448
1449        let _fill = memory_management
1450            .reserve(1024, &mut ErrorGraph::default())
1451            .unwrap();
1452        let _overflow = memory_management
1453            .reserve(1024, &mut ErrorGraph::default())
1454            .unwrap();
1455        assert_eq!(
1456            memory_management.memory_usage().bytes_reserved,
1457            2048,
1458            "the overflow landed in the later pool, not a second arena page"
1459        );
1460
1461        let report = memory_management.memory_report();
1462        assert_eq!(report.dynamic[0].pages_peak, 1, "the cap held");
1463        assert_eq!(report.dynamic[1].pages_peak, 1, "the tail caught the spill");
1464    }
1465
1466    #[test_log::test]
1467    fn alloc_two_chunks_on_one_page() {
1468        let page_size = 2048;
1469
1470        let mut memory_management = MemoryManagement::from_configuration(
1471            BytesStorage::default(),
1472            &DUMMY_MEM_PROPS,
1473            MemoryConfiguration::Custom {
1474                pool_options: vec![MemoryPoolOptions {
1475                    pool_type: PoolType::SlicedPages {
1476                        page_size,
1477                        max_slice_size: page_size,
1478                        max_pool_size: None,
1479                    },
1480                    dealloc_period: None,
1481                }],
1482            },
1483            Arc::new(ServerLogger::default()),
1484            options(),
1485        );
1486
1487        let alloc_size = 512;
1488        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1489        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1490
1491        let usage = memory_management.memory_usage();
1492        assert_eq!(usage.number_allocs, 2);
1493        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1494        assert_eq!(usage.bytes_reserved, page_size);
1495    }
1496
1497    #[test_log::test]
1498    fn alloc_reuses_storage() {
1499        // If no storage is re-used, this will allocate two pages.
1500        let page_size = 512;
1501
1502        let mut memory_management = MemoryManagement::from_configuration(
1503            BytesStorage::default(),
1504            &DUMMY_MEM_PROPS,
1505            MemoryConfiguration::Custom {
1506                pool_options: vec![MemoryPoolOptions {
1507                    pool_type: PoolType::SlicedPages {
1508                        page_size,
1509                        max_slice_size: page_size,
1510                        max_pool_size: None,
1511                    },
1512                    dealloc_period: None,
1513                }],
1514            },
1515            Arc::new(ServerLogger::default()),
1516            options(),
1517        );
1518
1519        let alloc_size = 512;
1520        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1521        drop(_handle);
1522        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1523
1524        let usage = memory_management.memory_usage();
1525        assert_eq!(usage.number_allocs, 1);
1526        assert_eq!(usage.bytes_in_use, alloc_size);
1527        assert_eq!(usage.bytes_reserved, page_size);
1528    }
1529
1530    #[test_log::test]
1531    fn alloc_allocs_new_storage() {
1532        let page_size = 1024;
1533
1534        let mut memory_management = MemoryManagement::from_configuration(
1535            BytesStorage::default(),
1536            &DUMMY_MEM_PROPS,
1537            MemoryConfiguration::Custom {
1538                pool_options: vec![MemoryPoolOptions {
1539                    pool_type: PoolType::SlicedPages {
1540                        page_size,
1541                        max_slice_size: page_size,
1542                        max_pool_size: None,
1543                    },
1544                    dealloc_period: None,
1545                }],
1546            },
1547            Arc::new(ServerLogger::default()),
1548            options(),
1549        );
1550
1551        let alloc_size = 768;
1552        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1553        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1554
1555        let usage = memory_management.memory_usage();
1556        assert_eq!(usage.number_allocs, 2);
1557        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1558        assert_eq!(usage.bytes_reserved, page_size * 2);
1559    }
1560
1561    #[test_log::test]
1562    fn alloc_respects_alignment_size() {
1563        let page_size = 500;
1564        let mut memory_management = MemoryManagement::from_configuration(
1565            BytesStorage::default(),
1566            &MemoryDeviceProperties::new(page_size, 50),
1567            MemoryConfiguration::Custom {
1568                pool_options: vec![MemoryPoolOptions {
1569                    pool_type: PoolType::SlicedPages {
1570                        page_size,
1571                        max_slice_size: page_size,
1572                        max_pool_size: None,
1573                    },
1574                    dealloc_period: None,
1575                }],
1576            },
1577            Arc::new(ServerLogger::default()),
1578            options(),
1579        );
1580        let alloc_size = 40;
1581        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1582        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1583        let usage = memory_management.memory_usage();
1584        // Each slice should be aligned to 50 bytes, so 20 padding bytes.
1585        assert_eq!(usage.bytes_padding, 10 * 2);
1586    }
1587
1588    #[test_log::test]
1589    fn allocs_on_correct_page() {
1590        let sizes = [100, 200, 300, 400];
1591
1592        let pools = sizes
1593            .iter()
1594            .map(|size| MemoryPoolOptions {
1595                pool_type: PoolType::SlicedPages {
1596                    page_size: *size,
1597                    max_slice_size: *size,
1598                    max_pool_size: None,
1599                },
1600                dealloc_period: None,
1601            })
1602            .collect();
1603        let mut memory_management = MemoryManagement::from_configuration(
1604            BytesStorage::default(),
1605            &MemoryDeviceProperties::new(128 * 1024 * 1024, 10),
1606            MemoryConfiguration::Custom {
1607                pool_options: pools,
1608            },
1609            Arc::new(ServerLogger::default()),
1610            options(),
1611        );
1612        // Allocate one thing on each page.
1613        let alloc_sizes = [50, 150, 250, 350];
1614        let _handles =
1615            alloc_sizes.map(|s| memory_management.reserve(s, &mut ErrorGraph::default()));
1616
1617        let usage = memory_management.memory_usage();
1618
1619        // Total memory should be size of all pages, and no more.
1620        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
1621        assert!(usage.bytes_reserved >= sizes.iter().sum::<u64>());
1622    }
1623
1624    #[test_log::test]
1625    fn resolve_absent_pools_config_keeps_runtime_choice() {
1626        #[cfg(not(exclusive_memory_only))]
1627        assert!(matches!(
1628            MemoryConfiguration::SubSlices
1629                .resolve(None, &DUMMY_MEM_PROPS)
1630                .unwrap(),
1631            MemoryConfiguration::SubSlices
1632        ));
1633        assert!(matches!(
1634            MemoryConfiguration::ExclusivePages
1635                .resolve(None, &DUMMY_MEM_PROPS)
1636                .unwrap(),
1637            MemoryConfiguration::ExclusivePages
1638        ));
1639    }
1640
1641    #[test_log::test]
1642    fn resolve_preset_overrides_runtime_choice() {
1643        let preset = MemoryPoolsConfig::Preset(MemoryPoolsPreset::ExclusivePages);
1644        #[cfg(not(exclusive_memory_only))]
1645        let base = MemoryConfiguration::SubSlices;
1646        #[cfg(exclusive_memory_only)]
1647        let base = MemoryConfiguration::ExclusivePages;
1648
1649        assert!(matches!(
1650            base.resolve(Some(&preset), &DUMMY_MEM_PROPS).unwrap(),
1651            MemoryConfiguration::ExclusivePages
1652        ));
1653    }
1654
1655    #[test_log::test]
1656    #[cfg(exclusive_memory_only)]
1657    fn resolve_rejects_sliced_pools_when_exclusive_only() {
1658        use crate::config::size::MemorySize;
1659
1660        let pools = MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1661            page_size: MemorySize(1024),
1662            max_slice_size: None,
1663            max_pool_size: None,
1664            dealloc_period: None,
1665        }]);
1666        assert_eq!(
1667            MemoryConfiguration::default()
1668                .resolve(Some(&pools), &DUMMY_MEM_PROPS)
1669                .unwrap_err(),
1670            PoolConfigError::SlicedPoolsUnavailable
1671        );
1672    }
1673
1674    #[test_log::test]
1675    #[cfg(not(exclusive_memory_only))]
1676    fn resolve_explicit_list_aligns_and_defaults() {
1677        use crate::config::size::MemorySize;
1678
1679        let pools = MemoryPoolsConfig::Explicit(vec![
1680            MemoryPoolConfig::Exclusive {
1681                max_alloc_size: MemorySize(8 * 1024),
1682                dealloc_period: Some(10000),
1683            },
1684            MemoryPoolConfig::Sliced {
1685                // Rounded up to the 32-byte alignment.
1686                page_size: MemorySize(1000),
1687                max_slice_size: None,
1688                max_pool_size: Some(MemorySize(4096)),
1689                dealloc_period: None,
1690            },
1691        ]);
1692
1693        let resolved = MemoryConfiguration::default()
1694            .resolve(Some(&pools), &DUMMY_MEM_PROPS)
1695            .unwrap();
1696        let MemoryConfiguration::Custom { pool_options } = resolved else {
1697            panic!("expected a custom configuration");
1698        };
1699
1700        assert_eq!(pool_options.len(), 2);
1701        assert!(matches!(
1702            pool_options[0].pool_type,
1703            PoolType::ExclusivePages {
1704                max_alloc_size: 8192
1705            }
1706        ));
1707        assert_eq!(pool_options[0].dealloc_period, Some(10000));
1708        assert!(matches!(
1709            pool_options[1].pool_type,
1710            PoolType::SlicedPages {
1711                page_size: 1024,
1712                // Defaults to the aligned page size.
1713                max_slice_size: 1024,
1714                max_pool_size: Some(4096),
1715            }
1716        ));
1717    }
1718
1719    #[test_log::test]
1720    #[cfg(not(exclusive_memory_only))]
1721    fn resolve_invalid_pool_configs_fail() {
1722        use crate::config::size::MemorySize;
1723
1724        let cases = [
1725            (
1726                MemoryPoolsConfig::Explicit(vec![]),
1727                PoolConfigError::EmptyPoolList,
1728            ),
1729            (
1730                MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1731                    page_size: MemorySize(0),
1732                    max_slice_size: None,
1733                    max_pool_size: None,
1734                    dealloc_period: None,
1735                }]),
1736                PoolConfigError::ZeroSize { field: "page_size" },
1737            ),
1738            (
1739                MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1740                    page_size: MemorySize(1024),
1741                    max_slice_size: Some(MemorySize(2048)),
1742                    max_pool_size: None,
1743                    dealloc_period: None,
1744                }]),
1745                PoolConfigError::SliceLargerThanPage {
1746                    page_size: 1024,
1747                    max_slice_size: 2048,
1748                },
1749            ),
1750            (
1751                MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1752                    page_size: MemorySize(2048),
1753                    max_slice_size: None,
1754                    max_pool_size: Some(MemorySize(1024)),
1755                    dealloc_period: None,
1756                }]),
1757                PoolConfigError::CapSmallerThanPage {
1758                    page_size: 2048,
1759                    max_pool_size: 1024,
1760                },
1761            ),
1762            (
1763                MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1764                    page_size: MemorySize(1024),
1765                    max_slice_size: None,
1766                    // 2^26 pages of 1 KiB: far beyond the u16 page index.
1767                    max_pool_size: Some(MemorySize(64 * 1024 * 1024 * 1024)),
1768                    dealloc_period: None,
1769                }]),
1770                PoolConfigError::TooManyPages {
1771                    pages: 64 * 1024 * 1024,
1772                },
1773            ),
1774            (
1775                MemoryPoolsConfig::Explicit(vec![
1776                    MemoryPoolConfig::Exclusive {
1777                        max_alloc_size: MemorySize(1024),
1778                        dealloc_period: None,
1779                    };
1780                    PERSISTENT_POOL_POS as usize
1781                ]),
1782                PoolConfigError::TooManyPools {
1783                    count: PERSISTENT_POOL_POS as usize,
1784                },
1785            ),
1786        ];
1787
1788        for (pools, expected) in cases {
1789            let result = MemoryConfiguration::default().resolve(Some(&pools), &DUMMY_MEM_PROPS);
1790            assert_eq!(result.unwrap_err(), expected);
1791        }
1792    }
1793
1794    // The motivating use case: allocations from different "sequence-length
1795    // ranges" reuse the same arena instead of each landing in its own
1796    // size-bucketed pool that keeps a separate reservation.
1797    #[test_log::test]
1798    #[cfg(not(exclusive_memory_only))]
1799    fn resolved_single_arena_reuses_across_sizes() {
1800        use crate::config::size::MemorySize;
1801
1802        let page = 1024 * 1024; // 1 MiB arena.
1803        let pools = MemoryPoolsConfig::Explicit(vec![MemoryPoolConfig::Sliced {
1804            page_size: MemorySize(page),
1805            max_slice_size: None,
1806            max_pool_size: None,
1807            dealloc_period: None,
1808        }]);
1809        let config = MemoryConfiguration::default()
1810            .resolve(Some(&pools), &DUMMY_MEM_PROPS)
1811            .unwrap();
1812
1813        let mut memory_management = MemoryManagement::from_configuration(
1814            BytesStorage::default(),
1815            &DUMMY_MEM_PROPS,
1816            config,
1817            Arc::new(ServerLogger::default()),
1818            options(),
1819        );
1820
1821        // A "small seq" allocation, then freed.
1822        let small = memory_management
1823            .reserve(4 * 1024, &mut ErrorGraph::default())
1824            .unwrap();
1825        drop(small);
1826        // A "large seq" allocation must reuse the same arena page.
1827        let large = memory_management
1828            .reserve(512 * 1024, &mut ErrorGraph::default())
1829            .unwrap();
1830
1831        let usage = memory_management.memory_usage();
1832        assert_eq!(
1833            usage.bytes_reserved, page,
1834            "both sizes must share a single arena page"
1835        );
1836        assert_eq!(usage.number_allocs, 1);
1837        drop(large);
1838    }
1839
1840    #[test_log::test]
1841    #[cfg(not(exclusive_memory_only))]
1842    fn allocate_deallocate_reallocate() {
1843        let mut memory_management = MemoryManagement::from_configuration(
1844            BytesStorage::default(),
1845            &MemoryDeviceProperties::new(128 * 1024 * 1024, 32),
1846            MemoryConfiguration::SubSlices,
1847            Arc::new(ServerLogger::default()),
1848            options(),
1849        );
1850        // Allocate a bunch
1851        let handles: Vec<_> = (0..5)
1852            .map(|i| memory_management.reserve(1000 * (i + 1), &mut ErrorGraph::default()))
1853            .collect();
1854        let usage_before = memory_management.memory_usage();
1855        // Deallocate
1856        drop(handles);
1857        // Reallocate
1858        let _new_handles: Vec<_> = (0..5)
1859            .map(|i| memory_management.reserve(1000 * (i + 1), &mut ErrorGraph::default()))
1860            .collect();
1861        let usage_after = memory_management.memory_usage();
1862        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
1863        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
1864        // Usage after can actually be _less_ because of defragging.
1865        assert!(usage_before.bytes_reserved >= usage_after.bytes_reserved);
1866    }
1867
1868    #[test_log::test]
1869    #[cfg(not(exclusive_memory_only))]
1870    fn test_fragmentation_resistance() {
1871        let mut memory_management = MemoryManagement::from_configuration(
1872            BytesStorage::default(),
1873            &MemoryDeviceProperties::new(128 * 1024 * 1024, 32),
1874            MemoryConfiguration::SubSlices,
1875            Arc::new(ServerLogger::default()),
1876            options(),
1877        );
1878        // Allocate a mix of small and large chunks
1879        let sizes = [50, 1000, 100, 5000, 200, 10000, 300];
1880        let handles: Vec<_> = sizes
1881            .iter()
1882            .map(|&size| {
1883                memory_management
1884                    .reserve(size, &mut ErrorGraph::default())
1885                    .unwrap()
1886            })
1887            .collect();
1888        let usage_before = memory_management.memory_usage();
1889        // Deallocate every other allocation
1890        for i in (0..handles.len()).step_by(2) {
1891            drop(handles[i].clone());
1892        }
1893        // Reallocate similar sizes
1894        for &size in &sizes[0..sizes.len() / 2] {
1895            memory_management
1896                .reserve(size, &mut ErrorGraph::default())
1897                .unwrap();
1898        }
1899        let usage_after = memory_management.memory_usage();
1900        // Check that we haven't increased our memory usage significantly
1901        assert!(usage_after.bytes_reserved <= (usage_before.bytes_reserved as f64 * 1.1) as u64);
1902    }
1903
1904    // Test pools without slices. More or less same as tests above.
1905    #[test_log::test]
1906    fn noslice_test_handle_mutability() {
1907        let mut memory_management = MemoryManagement::from_configuration(
1908            BytesStorage::default(),
1909            &MemoryDeviceProperties::new(128 * 1024 * 1024, 32),
1910            MemoryConfiguration::ExclusivePages,
1911            Arc::new(ServerLogger::default()),
1912            options(),
1913        );
1914        let handle = memory_management
1915            .reserve(10, &mut ErrorGraph::default())
1916            .unwrap();
1917        let other_ref = handle.clone();
1918        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
1919        drop(other_ref);
1920        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
1921    }
1922
1923    #[test_log::test]
1924    fn noslice_alloc_two_chunk() {
1925        let mut memory_management = MemoryManagement::from_configuration(
1926            BytesStorage::default(),
1927            &DUMMY_MEM_PROPS,
1928            MemoryConfiguration::Custom {
1929                pool_options: vec![MemoryPoolOptions {
1930                    pool_type: PoolType::ExclusivePages {
1931                        max_alloc_size: 1024,
1932                    },
1933                    dealloc_period: None,
1934                }],
1935            },
1936            Arc::new(ServerLogger::default()),
1937            options(),
1938        );
1939
1940        let alloc_size = 512;
1941        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1942        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1943
1944        let usage = memory_management.memory_usage();
1945        assert_eq!(usage.number_allocs, 2);
1946        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1947        assert!(usage.bytes_reserved >= alloc_size * 2);
1948    }
1949
1950    #[test_log::test]
1951    fn noslice_alloc_reuses_storage() {
1952        // If no storage is re-used, this will allocate two pages.
1953        let mut memory_management = MemoryManagement::from_configuration(
1954            BytesStorage::default(),
1955            &DUMMY_MEM_PROPS,
1956            MemoryConfiguration::Custom {
1957                pool_options: vec![MemoryPoolOptions {
1958                    pool_type: PoolType::ExclusivePages {
1959                        max_alloc_size: 1024,
1960                    },
1961                    dealloc_period: None,
1962                }],
1963            },
1964            Arc::new(ServerLogger::default()),
1965            options(),
1966        );
1967
1968        let alloc_size = 512;
1969        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1970        drop(_handle);
1971        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1972
1973        let usage = memory_management.memory_usage();
1974        assert_eq!(usage.number_allocs, 1);
1975        assert_eq!(usage.bytes_in_use, alloc_size);
1976        assert!(usage.bytes_reserved >= alloc_size);
1977    }
1978
1979    #[test_log::test]
1980    fn noslice_alloc_allocs_new_storage() {
1981        let mut memory_management = MemoryManagement::from_configuration(
1982            BytesStorage::default(),
1983            &DUMMY_MEM_PROPS,
1984            MemoryConfiguration::Custom {
1985                pool_options: vec![MemoryPoolOptions {
1986                    pool_type: PoolType::ExclusivePages {
1987                        max_alloc_size: 1024,
1988                    },
1989                    dealloc_period: None,
1990                }],
1991            },
1992            Arc::new(ServerLogger::default()),
1993            options(),
1994        );
1995
1996        let alloc_size = 768;
1997        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1998        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
1999        let usage = memory_management.memory_usage();
2000        assert_eq!(usage.number_allocs, 2);
2001        assert_eq!(usage.bytes_in_use, alloc_size * 2);
2002        assert!(usage.bytes_reserved >= alloc_size * 2);
2003    }
2004
2005    #[test_log::test]
2006    fn noslice_alloc_respects_alignment_size() {
2007        let mut memory_management = MemoryManagement::from_configuration(
2008            BytesStorage::default(),
2009            &MemoryDeviceProperties::new(DUMMY_MEM_PROPS.max_page_size, 50),
2010            MemoryConfiguration::Custom {
2011                pool_options: vec![MemoryPoolOptions {
2012                    pool_type: PoolType::ExclusivePages {
2013                        max_alloc_size: 50 * 20,
2014                    },
2015                    dealloc_period: None,
2016                }],
2017            },
2018            Arc::new(ServerLogger::default()),
2019            options(),
2020        );
2021        let alloc_size = 40;
2022        let _handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
2023        let _new_handle = memory_management.reserve(alloc_size, &mut ErrorGraph::default());
2024        let usage = memory_management.memory_usage();
2025        // Each slice should be aligned to 60 bytes, so 20 padding bytes.
2026        assert_eq!(usage.bytes_padding, 10 * 2);
2027    }
2028
2029    #[test_log::test]
2030    fn noslice_allocs_on_correct_page() {
2031        let pools = [100, 200, 300, 400]
2032            .iter()
2033            .map(|&size| MemoryPoolOptions {
2034                pool_type: PoolType::SlicedPages {
2035                    page_size: size,
2036                    max_slice_size: size,
2037                    max_pool_size: None,
2038                },
2039                dealloc_period: None,
2040            })
2041            .collect();
2042        let mut memory_management = MemoryManagement::from_configuration(
2043            BytesStorage::default(),
2044            &MemoryDeviceProperties::new(DUMMY_MEM_PROPS.max_page_size, 10),
2045            MemoryConfiguration::Custom {
2046                pool_options: pools,
2047            },
2048            Arc::new(ServerLogger::default()),
2049            options(),
2050        );
2051        // Allocate one thing on each page.
2052        let alloc_sizes = [50, 150, 250, 350];
2053        let _handles =
2054            alloc_sizes.map(|s| memory_management.reserve(s, &mut ErrorGraph::default()));
2055        let usage = memory_management.memory_usage();
2056        // Total memory should be size of all pages, and no more.
2057        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
2058    }
2059
2060    #[test_log::test]
2061    fn capture_pins_reused_persistent_slice() {
2062        let mut memory_management = MemoryManagement::from_configuration(
2063            BytesStorage::default(),
2064            &DUMMY_MEM_PROPS,
2065            MemoryConfiguration::ExclusivePages,
2066            Arc::new(ServerLogger::default()),
2067            options(),
2068        );
2069
2070        // First capture allocates a persistent slice, then everything is freed.
2071        memory_management.capture_begin();
2072        let first = memory_management
2073            .reserve(1024, &mut ErrorGraph::default())
2074            .unwrap();
2075        drop(first);
2076        drop(memory_management.capture_end());
2077
2078        // A second capture reuses that now-free slice: the reuse must be pinned
2079        // even though the slice predates the capture.
2080        memory_management.capture_begin();
2081        let second = memory_management
2082            .reserve(1024, &mut ErrorGraph::default())
2083            .unwrap();
2084        drop(second);
2085        let pins = memory_management.capture_end();
2086        assert_eq!(pins.len(), 1, "the reused slice must be retained");
2087
2088        // While pinned, the pool must not hand the slice to a later allocation.
2089        let before = memory_management.memory_usage();
2090        let _other = memory_management
2091            .reserve(1024, &mut ErrorGraph::default())
2092            .unwrap();
2093        let after = memory_management.memory_usage();
2094        assert!(
2095            after.bytes_reserved > before.bytes_reserved,
2096            "a pinned slice was handed to a later allocation"
2097        );
2098    }
2099
2100    #[test_log::test]
2101    fn capture_pins_preexisting_slice_freed_and_reused_midwindow() {
2102        let mut memory_management = MemoryManagement::from_configuration(
2103            BytesStorage::default(),
2104            &DUMMY_MEM_PROPS,
2105            MemoryConfiguration::ExclusivePages,
2106            Arc::new(ServerLogger::default()),
2107            options(),
2108        );
2109
2110        // A persistent slice that is live (in use) when the next window opens.
2111        memory_management.capture_begin();
2112        let live = memory_management
2113            .reserve(1024, &mut ErrorGraph::default())
2114            .unwrap();
2115        drop(memory_management.capture_end()); // release the pin; `live` still holds the slice.
2116
2117        // The window opens with `live`'s slice in use, then frees it mid-window
2118        // and reuses that exact slice for a window allocation the graph records
2119        // against. The old snapshot-of-in-use heuristic excluded it (it was in
2120        // use at begin); reservation-tracking pins it because the window touched
2121        // it — the whole point of the redesign.
2122        memory_management.capture_begin();
2123        drop(live);
2124        let reused = memory_management
2125            .reserve(1024, &mut ErrorGraph::default())
2126            .unwrap();
2127        drop(reused);
2128        let pins = memory_management.capture_end();
2129        assert_eq!(
2130            pins.len(),
2131            1,
2132            "a pre-existing slice freed and reused mid-window must be pinned"
2133        );
2134    }
2135
2136    #[test_log::test]
2137    fn capture_does_not_retain_untouched_free_slices() {
2138        let mut memory_management = MemoryManagement::from_configuration(
2139            BytesStorage::default(),
2140            &DUMMY_MEM_PROPS,
2141            MemoryConfiguration::ExclusivePages,
2142            Arc::new(ServerLogger::default()),
2143            options(),
2144        );
2145
2146        // Leave an idle free slice in the pool from an earlier capture.
2147        memory_management.capture_begin();
2148        let earlier = memory_management
2149            .reserve(1024, &mut ErrorGraph::default())
2150            .unwrap();
2151        drop(earlier);
2152        drop(memory_management.capture_end());
2153
2154        // A new capture that only ever touches a different size must not retain
2155        // that leftover idle slice — reservation-tracking pins exactly what the
2156        // window used, so no free-slice cleanup at `capture_begin` is needed.
2157        memory_management.capture_begin();
2158        let window = memory_management
2159            .reserve(2048, &mut ErrorGraph::default())
2160            .unwrap();
2161        drop(window);
2162        let pins = memory_management.capture_end();
2163        assert_eq!(
2164            pins.len(),
2165            1,
2166            "only the touched slice is retained, not the idle leftover"
2167        );
2168    }
2169
2170    #[test_log::test]
2171    fn capture_survives_explicit_cleanup() {
2172        let mut memory_management = MemoryManagement::from_configuration(
2173            BytesStorage::default(),
2174            &DUMMY_MEM_PROPS,
2175            MemoryConfiguration::ExclusivePages,
2176            Arc::new(ServerLogger::default()),
2177            options(),
2178        );
2179
2180        memory_management.capture_begin();
2181        let handle = memory_management
2182            .reserve(1024, &mut ErrorGraph::default())
2183            .unwrap();
2184        drop(handle);
2185        // An explicit cleanup mid-capture compacts the persistent pool; the
2186        // capture must keep its pins through the rebuild.
2187        memory_management.cleanup(true, &mut ErrorGraph::default());
2188        let pins = memory_management.capture_end();
2189        assert_eq!(pins.len(), 1, "pin lost across an explicit cleanup");
2190    }
2191
2192    #[test_log::test]
2193    fn capture_begin_is_reentrant() {
2194        let mut memory_management = MemoryManagement::from_configuration(
2195            BytesStorage::default(),
2196            &DUMMY_MEM_PROPS,
2197            MemoryConfiguration::ExclusivePages,
2198            Arc::new(ServerLogger::default()),
2199            options(),
2200        );
2201
2202        memory_management.capture_begin();
2203        let first = memory_management
2204            .reserve(1024, &mut ErrorGraph::default())
2205            .unwrap();
2206        // A second begin (defensive: callers arm a capture exactly once) must
2207        // not discard the pins or the saved mode of the capture already in flight.
2208        memory_management.capture_begin();
2209        let second = memory_management
2210            .reserve(2048, &mut ErrorGraph::default())
2211            .unwrap();
2212        drop(first);
2213        drop(second);
2214        let pins = memory_management.capture_end();
2215        assert_eq!(pins.len(), 2, "pins from before the re-entrant begin lost");
2216        assert!(
2217            memory_management.capture_end().is_empty(),
2218            "capture must be fully disarmed"
2219        );
2220    }
2221
2222    #[test_log::test]
2223    fn capture_leaves_preexisting_buffers_alone() {
2224        let mut memory_management = MemoryManagement::from_configuration(
2225            BytesStorage::default(),
2226            &DUMMY_MEM_PROPS,
2227            MemoryConfiguration::ExclusivePages,
2228            Arc::new(ServerLogger::default()),
2229            options(),
2230        );
2231
2232        // A persistent buffer that predates the capture and stays alive
2233        // through it (weights, a graph input created earlier).
2234        memory_management.capture_begin();
2235        let preexisting = memory_management
2236            .reserve(1024, &mut ErrorGraph::default())
2237            .unwrap();
2238        drop(memory_management.capture_end());
2239
2240        memory_management.capture_begin();
2241        let window = memory_management
2242            .reserve(2048, &mut ErrorGraph::default())
2243            .unwrap();
2244        drop(window);
2245        let pins = memory_management.capture_end();
2246
2247        // Only the window's slice is claimed; the pre-existing buffer keeps a
2248        // single user reference, so in-place ops on it keep working.
2249        assert_eq!(
2250            pins.len(),
2251            1,
2252            "only the window's slice belongs to the graph"
2253        );
2254        assert!(
2255            preexisting.can_mut(),
2256            "a capture must not claim pre-existing live buffers"
2257        );
2258    }
2259
2260    /// Warmup must leave the pool holding its full *distinct working set*, not
2261    /// its transient peak.
2262    ///
2263    /// This is the property the whole priming phase exists for. If warmup is
2264    /// allowed to recycle its own slices, the pool only ever grows to the peak
2265    /// number of slices live *at any one instant* during the pass — and that
2266    /// peak depends on how far the host runs ahead of the device, so it can
2267    /// land below what the recorded run asks for. The window then has to
2268    /// allocate, which a capture records as a memory node, and CUDA refuses to
2269    /// relaunch a graph holding one: the first launch succeeds and every replay
2270    /// after it fails.
2271    ///
2272    /// Here warmup reserves the same size three times *sequentially*, so its
2273    /// instantaneous peak is one slice while its working set is three. The
2274    /// recorded run then holds three at once. Without retention the pool ends
2275    /// warmup with one slice and the window allocates two more.
2276    #[test_log::test]
2277    fn capture_priming_leaves_the_working_set_not_the_peak() {
2278        let mut memory_management = MemoryManagement::from_configuration(
2279            BytesStorage::default(),
2280            &DUMMY_MEM_PROPS,
2281            MemoryConfiguration::ExclusivePages,
2282            Arc::new(ServerLogger::default()),
2283            options(),
2284        );
2285
2286        // Warmup: three sequential reserve/drop cycles. Each drop would hand
2287        // the slice straight back to the next reserve if priming did not retain
2288        // it, leaving a one-slice pool.
2289        memory_management.capture_begin();
2290        for _ in 0..3 {
2291            let scratch = memory_management
2292                .reserve(1024, &mut ErrorGraph::default())
2293                .unwrap();
2294            drop(scratch);
2295        }
2296        // Warmup is over: release the retained slices. They stay in the pool,
2297        // now free, which is the entire point.
2298        memory_management.capture_priming_end();
2299        let after_warmup = memory_management.memory_usage();
2300
2301        // The recorded run holds three slices of that size simultaneously —
2302        // more than warmup's instantaneous peak of one. Every one of them must
2303        // come from the pool.
2304        let recorded: Vec<_> = (0..3)
2305            .map(|_| {
2306                memory_management
2307                    .reserve(1024, &mut ErrorGraph::default())
2308                    .unwrap()
2309            })
2310            .collect();
2311        let after_window = memory_management.memory_usage();
2312
2313        assert_eq!(
2314            after_window.bytes_reserved, after_warmup.bytes_reserved,
2315            "the capture window grew the pool: warmup left only its transient \
2316             peak, so the recorded run had to allocate — which a capture records \
2317             as a memory node and makes the graph un-relaunchable"
2318        );
2319
2320        drop(recorded);
2321        drop(memory_management.capture_end());
2322    }
2323
2324    /// The mechanism behind [`capture_priming_leaves_the_working_set_not_the_peak`]:
2325    /// a handle dropped *during* priming must not return its slice to the free
2326    /// list, and `capture_priming_end` must give every one of them back.
2327    #[test_log::test]
2328    fn capture_priming_holds_dropped_slices_until_priming_ends() {
2329        let mut memory_management = MemoryManagement::from_configuration(
2330            BytesStorage::default(),
2331            &DUMMY_MEM_PROPS,
2332            MemoryConfiguration::ExclusivePages,
2333            Arc::new(ServerLogger::default()),
2334            options(),
2335        );
2336
2337        memory_management.capture_begin();
2338        let first = memory_management
2339            .reserve(1024, &mut ErrorGraph::default())
2340            .unwrap();
2341        drop(first);
2342
2343        // Still priming: the dropped slice is retained, so this reserve cannot
2344        // recycle it and the pool has to grow.
2345        let before_second = memory_management.memory_usage();
2346        let second = memory_management
2347            .reserve(1024, &mut ErrorGraph::default())
2348            .unwrap();
2349        let after_second = memory_management.memory_usage();
2350        assert!(
2351            after_second.bytes_reserved > before_second.bytes_reserved,
2352            "priming must retain a dropped slice instead of recycling it"
2353        );
2354        drop(second);
2355
2356        // Priming over: both slices are free again and must now be reused.
2357        memory_management.capture_priming_end();
2358        let before_reuse = memory_management.memory_usage();
2359        let reused = memory_management
2360            .reserve(1024, &mut ErrorGraph::default())
2361            .unwrap();
2362        let after_reuse = memory_management.memory_usage();
2363        assert_eq!(
2364            after_reuse.bytes_reserved, before_reuse.bytes_reserved,
2365            "capture_priming_end must release the retained slices for reuse"
2366        );
2367
2368        drop(reused);
2369        drop(memory_management.capture_end());
2370    }
2371
2372    /// A backend that never calls `capture_priming_end` (HIP did not, before the
2373    /// call was added to both) must not leak warmup's slices past the capture.
2374    #[test_log::test]
2375    fn capture_end_releases_primed_slices_when_priming_never_ended() {
2376        let mut memory_management = MemoryManagement::from_configuration(
2377            BytesStorage::default(),
2378            &DUMMY_MEM_PROPS,
2379            MemoryConfiguration::ExclusivePages,
2380            Arc::new(ServerLogger::default()),
2381            options(),
2382        );
2383
2384        memory_management.capture_begin();
2385        let scratch = memory_management
2386            .reserve(1024, &mut ErrorGraph::default())
2387            .unwrap();
2388        drop(scratch);
2389        // The caller let its handle go, but priming is still holding the slice.
2390        assert!(
2391            memory_management.memory_usage().bytes_in_use > 0,
2392            "priming should still be retaining the dropped slice"
2393        );
2394
2395        // No `capture_priming_end` — `capture_end` drops the `CaptureState`,
2396        // and with it every handle priming retained.
2397        drop(memory_management.capture_end());
2398        assert_eq!(
2399            memory_management.memory_usage().bytes_in_use,
2400            0,
2401            "primed slices outlived the capture"
2402        );
2403    }
2404
2405    #[test_log::test]
2406    fn noslice_allocate_deallocate_reallocate() {
2407        let mut memory_management = MemoryManagement::from_configuration(
2408            BytesStorage::default(),
2409            &MemoryDeviceProperties::new(128 * 1024 * 1024, 32),
2410            MemoryConfiguration::ExclusivePages,
2411            Arc::new(ServerLogger::default()),
2412            options(),
2413        );
2414        // Allocate a bunch
2415        let handles: Vec<_> = (0..5)
2416            .map(|i| memory_management.reserve(1000 * (i + 1), &mut ErrorGraph::default()))
2417            .collect();
2418        let usage_before = memory_management.memory_usage();
2419        // Deallocate
2420        drop(handles);
2421        // Reallocate
2422        let _new_handles: Vec<_> = (0..5)
2423            .map(|i| memory_management.reserve(1000 * (i + 1), &mut ErrorGraph::default()))
2424            .collect();
2425        let usage_after = memory_management.memory_usage();
2426        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
2427        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
2428        assert_eq!(usage_before.bytes_reserved, usage_after.bytes_reserved);
2429    }
2430}