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