Skip to main content

cubecl_runtime/memory_management/
memory_manage.rs

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