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::{MemoryLogLevel, PersistentMemory},
9    },
10    logging::ServerLogger,
11    memory_management::{BytesFormat, memory_pool::Slice},
12    server::IoError,
13    storage::{ComputeStorage, StorageHandle},
14};
15
16use alloc::format;
17use alloc::string::{String, ToString};
18#[cfg(not(exclusive_memory_only))]
19use alloc::vec;
20use alloc::vec::Vec;
21use cubecl_common::{backtrace::BackTrace, stub::Arc};
22use cubecl_ir::MemoryDeviceProperties;
23
24pub use super::memory_pool::{ManagedMemoryBinding, handle::*};
25
26// These are 288 bytes vs 64 bytes. Adding boxing isn't really worth
27// saving the 200 bytes.
28#[allow(clippy::large_enum_variant)]
29enum DynamicPool {
30    Sliced(SlicedPool),
31    Exclusive(ExclusiveMemoryPool),
32}
33
34impl MemoryPool for DynamicPool {
35    fn accept(&self, size: u64) -> bool {
36        match self {
37            DynamicPool::Sliced(pool) => pool.accept(size),
38            DynamicPool::Exclusive(pool) => pool.accept(size),
39        }
40    }
41
42    fn find(&self, binding: &ManagedMemoryBinding) -> Result<&Slice, IoError> {
43        match self {
44            DynamicPool::Sliced(m) => m.find(binding),
45            DynamicPool::Exclusive(m) => m.find(binding),
46        }
47    }
48
49    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
50    fn try_reserve(&mut self, size: u64) -> Option<ManagedMemoryHandle> {
51        match self {
52            DynamicPool::Sliced(m) => m.try_reserve(size),
53            DynamicPool::Exclusive(m) => m.try_reserve(size),
54        }
55    }
56
57    #[cfg_attr(
58        feature = "tracing",
59        tracing::instrument(level = "trace", skip(self, storage))
60    )]
61    fn alloc<Storage: ComputeStorage>(
62        &mut self,
63        storage: &mut Storage,
64        size: u64,
65    ) -> Result<ManagedMemoryHandle, IoError> {
66        match self {
67            DynamicPool::Sliced(m) => m.alloc(storage, size),
68            DynamicPool::Exclusive(m) => m.alloc(storage, size),
69        }
70    }
71
72    fn get_memory_usage(&self) -> MemoryUsage {
73        match self {
74            DynamicPool::Sliced(m) => m.get_memory_usage(),
75            DynamicPool::Exclusive(m) => m.get_memory_usage(),
76        }
77    }
78
79    fn cleanup<Storage: ComputeStorage>(
80        &mut self,
81        storage: &mut Storage,
82        alloc_nr: u64,
83        explicit: bool,
84    ) {
85        match self {
86            DynamicPool::Sliced(m) => m.cleanup(storage, alloc_nr, explicit),
87            DynamicPool::Exclusive(m) => m.cleanup(storage, alloc_nr, explicit),
88        };
89        storage.flush();
90    }
91
92    fn bind(
93        &mut self,
94        reserved: ManagedMemoryHandle,
95        assigned: ManagedMemoryHandle,
96        cursor: u64,
97    ) -> Result<(), IoError> {
98        match self {
99            DynamicPool::Sliced(m) => m.bind(reserved, assigned, cursor),
100            DynamicPool::Exclusive(m) => m.bind(reserved, assigned, cursor),
101        }
102    }
103}
104
105#[derive(Default, Clone, Copy, Debug)]
106/// The mode of allocation used.
107pub enum MemoryAllocationMode {
108    /// Use the automatic memory management strategy for allocation.
109    #[default]
110    Auto,
111    /// Use a persistent memory management strategy, meaning that all allocations are for data that is
112    /// likely never going to be freed.
113    Persistent,
114}
115
116/// Reserves and keeps track of chunks of memory in the storage, and slices upon these chunks.
117pub struct MemoryManagement<Storage> {
118    name: String,
119    persistent: PersistentPool,
120    pools: Vec<DynamicPool>,
121    storage: Storage,
122    alloc_reserve_count: u64,
123    mode: MemoryAllocationMode,
124    config: PersistentMemory,
125    logger: Arc<ServerLogger>,
126}
127
128fn generate_bucket_sizes(
129    start_size: u64,
130    end_size: u64,
131    max_buckets: usize,
132    alignment: u64,
133) -> Vec<u64> {
134    let mut buckets = Vec::with_capacity(max_buckets);
135    let log_min = (start_size as f64).ln();
136    let log_max = (end_size as f64).ln();
137    let log_range = log_max - log_min;
138
139    // Pure exponential performed best, but let's try slightly denser in lower-mid range
140    for i in 0..max_buckets {
141        let p = i as f64 / (max_buckets - 1) as f64;
142        // Slight bias toward lower-mid range with less aggressive curve than sigmoid
143        let log_size = log_min + log_range * p;
144        let size = log_size.exp() as u64;
145        let aligned_size = size.next_multiple_of(alignment);
146        buckets.push(aligned_size);
147    }
148
149    buckets.dedup();
150    buckets
151}
152
153const DEALLOC_SCALE_MB: u64 = 1024 * 1024 * 1024;
154const BASE_DEALLOC_PERIOD: u64 = 5000;
155
156/// The options for creating a new [`MemoryManagement`] instance.
157#[derive(Debug)]
158pub struct MemoryManagementOptions {
159    /// The name of the memory management.
160    name: String,
161    /// The [`MemoryAllocationOption`] used by this instance.
162    memory: MemoryAllocationOption,
163}
164
165impl MemoryManagementOptions {
166    /// Creates a new [`MemoryManagementOptions`].
167    pub fn new<S: Into<String>>(name: S) -> Self {
168        Self {
169            name: name.into(),
170            memory: MemoryAllocationOption::FromConfig,
171        }
172    }
173
174    /// Forces the [`MemoryAllocationMode`] during execution to always be the provided one.
175    pub fn mode(mut self, mode: MemoryAllocationMode) -> Self {
176        self.memory = MemoryAllocationOption::Provided(mode);
177        self
178    }
179}
180
181#[derive(Default, Debug)]
182/// Determines which [`MemoryAllocationMode`] is used during allocations.
183enum MemoryAllocationOption {
184    #[default]
185    /// Uses the [`GlobalConfig`] to determine the mode of allocation.
186    FromConfig,
187    /// Use the provided [`MemoryAllocationMode`].
188    Provided(MemoryAllocationMode),
189}
190
191impl<Storage: ComputeStorage> MemoryManagement<Storage> {
192    /// Creates the options from device limits.
193    pub fn from_configuration(
194        storage: Storage,
195        properties: &MemoryDeviceProperties,
196        config: MemoryConfiguration,
197        logger: Arc<ServerLogger>,
198        options: MemoryManagementOptions,
199    ) -> Self {
200        let pool_options = match config {
201            #[cfg(not(exclusive_memory_only))]
202            MemoryConfiguration::SubSlices => {
203                // Round chunk size to be aligned.
204                let memory_alignment = properties.alignment;
205                let max_page = properties.max_page_size;
206                let mut pools = Vec::new();
207
208                const MB: u64 = 1024 * 1024;
209
210                // Add in a pool for allocations that are smaller than the min alignment,
211                // as they can't use offsets at all (on wgpu at least).
212                pools.push(MemoryPoolOptions {
213                    pool_type: PoolType::ExclusivePages { max_alloc_size: 0 },
214                    dealloc_period: None,
215                });
216
217                let mut current = max_page;
218                let mut max_sizes = vec![];
219                let mut page_sizes = vec![];
220                let mut base = pools.len() as u32;
221
222                while current >= 32 * MB {
223                    current /= 4;
224
225                    // Make sure every pool has an aligned size.
226                    current = current.next_multiple_of(memory_alignment);
227
228                    max_sizes.push(current / 2u64.pow(base));
229                    page_sizes.push(current);
230                    base += 1;
231                }
232
233                max_sizes.reverse();
234                page_sizes.reverse();
235
236                for i in 0..max_sizes.len() {
237                    let max = max_sizes[i];
238                    let page_size = page_sizes[i];
239
240                    pools.push(MemoryPoolOptions {
241                        // Creating max slices lower than the chunk size reduces fragmentation.
242                        pool_type: PoolType::SlicedPages {
243                            page_size,
244                            max_slice_size: max,
245                        },
246                        dealloc_period: None,
247                    });
248                }
249
250                // Add pools from big to small.
251                pools.push(MemoryPoolOptions {
252                    // Large allocations vary substantially in tensor-network
253                    // workloads; whole-device slabs strand too much memory.
254                    pool_type: PoolType::ExclusivePages {
255                        max_alloc_size: max_page / memory_alignment * memory_alignment,
256                    },
257                    dealloc_period: None,
258                });
259                pools
260            }
261            MemoryConfiguration::ExclusivePages => {
262                // Add all bin sizes. Nb: because of alignment some buckets
263                // end up as the same size, so only want unique ones,
264                // but also keep the order, so a BTree will do.
265                const MIN_BUCKET_SIZE: u64 = 1024 * 32;
266                const NUM_POOLS: usize = 24;
267
268                let sizes = generate_bucket_sizes(
269                    MIN_BUCKET_SIZE,
270                    properties.max_page_size,
271                    NUM_POOLS,
272                    properties.alignment,
273                );
274
275                sizes
276                    .iter()
277                    .map(|&size| {
278                        let dealloc_period = (BASE_DEALLOC_PERIOD as f64
279                            * (1.0 + size as f64 / (DEALLOC_SCALE_MB as f64)).round())
280                            as u64;
281
282                        MemoryPoolOptions {
283                            pool_type: PoolType::ExclusivePages {
284                                max_alloc_size: size,
285                            },
286                            dealloc_period: Some(dealloc_period),
287                        }
288                    })
289                    .collect()
290            }
291            MemoryConfiguration::Custom { pool_options } => pool_options,
292        };
293
294        logger.log_memory(
295            |level| !matches!(level, MemoryLogLevel::Disabled),
296            || {
297                let mut msg = String::new();
298                for pool in pool_options.iter() {
299                    msg += &format!("[{}] Using memory pool: \n {pool:?}\n", options.name);
300                }
301                msg
302            },
303        );
304
305        let pools: Vec<_> = pool_options
306            .iter()
307            .enumerate()
308            .map(|(pool_pos, options)| {
309                let pool_pos = pool_pos as u8;
310
311                match options.pool_type {
312                    PoolType::SlicedPages {
313                        page_size,
314                        max_slice_size,
315                    } => DynamicPool::Sliced(SlicedPool::new(
316                        page_size,
317                        max_slice_size,
318                        properties.alignment,
319                        pool_pos,
320                    )),
321                    PoolType::ExclusivePages { max_alloc_size } => {
322                        DynamicPool::Exclusive(ExclusiveMemoryPool::new(
323                            max_alloc_size,
324                            properties.alignment,
325                            options.dealloc_period.unwrap_or(u64::MAX),
326                            pool_pos,
327                        ))
328                    }
329                }
330            })
331            .collect();
332
333        let config = CubeClRuntimeConfig::get().memory.persistent_memory.clone();
334
335        let mode = match options.memory {
336            MemoryAllocationOption::Provided(mode) => mode,
337            MemoryAllocationOption::FromConfig => match config {
338                PersistentMemory::Enabled => MemoryAllocationMode::Auto,
339                PersistentMemory::Disabled => MemoryAllocationMode::Auto,
340                PersistentMemory::Enforced => MemoryAllocationMode::Persistent,
341            },
342        };
343
344        Self {
345            name: options.name,
346            persistent: PersistentPool::new(
347                properties.max_page_size,
348                properties.alignment,
349                pools.len() as u8,
350            ),
351            pools,
352            storage,
353            alloc_reserve_count: 0,
354            mode,
355            config,
356            logger,
357        }
358    }
359
360    /// Change the mode of allocation.
361    pub fn mode(&mut self, mode: MemoryAllocationMode) {
362        // We override the mode based on the cubecl config.
363        let mode = match self.config {
364            PersistentMemory::Enabled => mode,
365            PersistentMemory::Disabled | PersistentMemory::Enforced => return,
366        };
367
368        self.logger.log_memory(
369            |level| !matches!(level, MemoryLogLevel::Disabled),
370            || {
371                format!(
372                    "[{}] Setting memory allocation mode: from {:?} => {mode:?}",
373                    self.name, self.mode
374                )
375            },
376        );
377        self.mode = mode;
378    }
379
380    /// Cleanup allocations in pools that are deemed unnecessary.
381    pub fn cleanup(&mut self, explicit: bool) {
382        self.logger.log_memory(
383            |level| !matches!(level, MemoryLogLevel::Disabled) && explicit,
384            || "Manual memory cleanup ...".to_string(),
385        );
386
387        self.persistent
388            .cleanup(&mut self.storage, self.alloc_reserve_count, explicit);
389
390        for pool in self.pools.iter_mut() {
391            pool.cleanup(&mut self.storage, self.alloc_reserve_count, explicit);
392        }
393    }
394
395    /// Returns the storage from the specified binding
396    pub fn get_cursor(&self, binding: ManagedMemoryBinding) -> Result<u64, IoError> {
397        let slice = self.find(binding)?;
398        Ok(slice.cursor)
399    }
400
401    /// Returns the storage from the specified binding
402    fn find(&self, binding: ManagedMemoryBinding) -> Result<&Slice, IoError> {
403        let id = binding.descriptor();
404
405        if id.location().pool >= self.pools.len() as u8 {
406            return self.persistent.find(&binding);
407        }
408
409        let pool =
410            self.pools
411                .get(id.location().pool as usize)
412                .ok_or_else(|| IoError::NotFound {
413                    backtrace: BackTrace::capture(),
414                    reason: format!("Pool {} doesn't exist", id.location().pool).into(),
415                })?;
416
417        let slice = pool.find(&binding)?;
418
419        assert_eq!(slice.handle.descriptor(), binding.descriptor());
420
421        Ok(slice)
422    }
423
424    /// Returns the storage from the specified binding
425    pub fn get_storage(&mut self, binding: ManagedMemoryBinding) -> Result<StorageHandle, IoError> {
426        let slice = self.find(binding)?;
427        Ok(slice.storage.clone())
428    }
429
430    /// Returns the resource from the storage at the specified handle
431    pub fn get_resource(
432        &mut self,
433        binding: ManagedMemoryBinding,
434        offset_start: Option<u64>,
435        offset_end: Option<u64>,
436    ) -> Result<Storage::Resource, IoError> {
437        let handle = self.get_storage(binding)?;
438
439        let handle = match offset_start {
440            Some(offset) => handle.offset_start(offset),
441            None => handle,
442        };
443        let handle = match offset_end {
444            Some(offset) => handle.offset_end(offset),
445            None => handle,
446        };
447        Ok(self.storage().get(&handle))
448    }
449
450    /// Finds a spot in memory for a resource with the given size in bytes, and returns a handle to it
451    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
452    pub fn reserve(&mut self, size: u64) -> Result<ManagedMemoryHandle, IoError> {
453        // If this happens every nanosecond, counts overflows after 585 years, so not worth thinking too
454        // hard about overflow here.
455        self.alloc_reserve_count += 1;
456
457        if let Some(val) = self.persistent.try_reserve(size) {
458            self.logger.log_memory(
459                |level| matches!(level, MemoryLogLevel::Full),
460                || {
461                    format!(
462                        "[{}] Reserved memory {size} using persistent memory",
463                        self.name
464                    )
465                },
466            );
467            return Ok(val);
468        }
469
470        if matches!(self.mode, MemoryAllocationMode::Persistent) || self.persistent.has_size(size) {
471            let allocated = self.persistent.alloc(&mut self.storage, size);
472
473            self.logger.log_memory(
474                |level| !matches!(level, MemoryLogLevel::Disabled),
475                || {
476                    format!(
477                        "[{}] Allocated a new memory page using persistent memory, \n{}",
478                        self.name, self,
479                    )
480                },
481            );
482            return allocated;
483        }
484
485        self.logger.log_memory(
486            |level| matches!(level, MemoryLogLevel::Full),
487            || {
488                format!(
489                    "[{}] Reserved memory {} using dynamic pool",
490                    self.name,
491                    BytesFormat::new(size)
492                )
493            },
494        );
495
496        // Find first pool that fits this allocation
497        let pool = self
498            .pools
499            .iter_mut()
500            .find(|p| p.accept(size))
501            .ok_or(IoError::BufferTooBig {
502                size,
503                backtrace: BackTrace::capture(),
504            })?;
505
506        if let Some(slice) = pool.try_reserve(size) {
507            return Ok(slice);
508        }
509
510        let allocated = pool.alloc(&mut self.storage, size);
511
512        self.logger.log_memory(
513            |level| matches!(level, MemoryLogLevel::Full),
514            || {
515                format!(
516                    "[{}], Allocated a new memory page, current usage: \n{}",
517                    self.name, self
518                )
519            },
520        );
521
522        allocated
523    }
524
525    /// Fetch the storage used by the memory manager.
526    ///
527    /// # Notes
528    ///
529    /// The storage should probably not be used for allocations since the handles won't be
530    /// compatible with the ones provided by the current trait. Prefer using the
531    /// [alloc](ComputeStorage::alloc) and [dealloc](ComputeStorage::dealloc) functions.
532    ///
533    /// This is useful if you need to time the deallocations based on async computation, or to
534    /// change the mode of storage for different reasons.
535    pub fn storage(&mut self) -> &mut Storage {
536        &mut self.storage
537    }
538
539    /// Get the current memory usage.
540    pub fn memory_usage(&self) -> MemoryUsage {
541        let memory_usage = self.pools.iter().map(|x| x.get_memory_usage()).fold(
542            MemoryUsage {
543                number_allocs: 0,
544                bytes_in_use: 0,
545                bytes_padding: 0,
546                bytes_reserved: 0,
547            },
548            |m1, m2| m1.combine(m2),
549        );
550        memory_usage.combine(self.persistent.get_memory_usage())
551    }
552
553    /// Print out a report of the current memory usage.
554    pub fn print_memory_usage(&self) {
555        #[cfg(feature = "std")]
556        log::info!("{}", self.memory_usage());
557    }
558
559    /// Binds the given [handle](HandleId) to a [`MemorySlot`].
560    pub fn bind(
561        &mut self,
562        reserved: ManagedMemoryHandle,
563        assigned: ManagedMemoryHandle,
564        cursor: u64,
565    ) -> Result<(), IoError> {
566        let descriptor = reserved.descriptor();
567
568        if descriptor.location().init == 0 {
569            return Err(IoError::NotFound {
570                backtrace: BackTrace::capture(),
571                reason: "Reserved memory isn't initialized".into(),
572            });
573        }
574
575        let pool_index = descriptor.location().pool as usize;
576        if pool_index >= self.pools.len() {
577            return self.persistent.bind(reserved, assigned, cursor);
578        }
579
580        self.pools
581            .get_mut(pool_index)
582            .map(|p| p.bind(reserved, assigned, cursor))
583            .ok_or_else(|| IoError::NotFound {
584                backtrace: BackTrace::capture(),
585                reason: format!("Memory pool {} doesn't exist", pool_index).into(),
586            })?
587    }
588}
589
590impl<Storage: ComputeStorage> core::fmt::Display for MemoryManagement<Storage> {
591    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
592        f.write_str("\n# MemoryManagement\n\n")?;
593        f.write_fmt(format_args!(" - name: {:?}\n", self.name))?;
594        f.write_fmt(format_args!("\n## Persistent\n\n{}", self.persistent))?;
595        f.write_str("\n## Dynamic\n\n")?;
596
597        for pool in self.pools.iter() {
598            match pool {
599                DynamicPool::Sliced(pool) => f.write_fmt(format_args!("{pool}\n"))?,
600                DynamicPool::Exclusive(pool) => f.write_fmt(format_args!("{pool}\n"))?,
601            }
602        }
603        let memory_usage = self.memory_usage();
604        f.write_fmt(format_args!("\n## Summary\n\n{memory_usage}"))?;
605
606        Ok(())
607    }
608}
609
610impl<Storage> core::fmt::Debug for MemoryManagement<Storage> {
611    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
612        f.write_str(
613            alloc::format!(
614                "DynamicMemoryManagement {:?}",
615                core::any::type_name::<Storage>(),
616            )
617            .as_str(),
618        )
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use crate::{
626        memory_management::MemoryManagement,
627        storage::{BytesStorage, ComputeStorage, StorageHandle, StorageId},
628    };
629    use alloc::vec;
630
631    struct LimitedStorage {
632        storage: BytesStorage,
633        max_alloc_size: u64,
634        alloc_calls: u64,
635    }
636
637    impl ComputeStorage for LimitedStorage {
638        type Resource = <BytesStorage as ComputeStorage>::Resource;
639
640        fn alignment(&self) -> usize {
641            self.storage.alignment()
642        }
643
644        fn get(&mut self, handle: &StorageHandle) -> Self::Resource {
645            self.storage.get(handle)
646        }
647
648        fn alloc(&mut self, size: u64) -> Result<StorageHandle, IoError> {
649            self.alloc_calls += 1;
650            if size > self.max_alloc_size {
651                return Err(IoError::OutOfMemory {
652                    size,
653                    backtrace: BackTrace::capture(),
654                });
655            }
656            self.storage.alloc(size)
657        }
658
659        fn dealloc(&mut self, id: StorageId) {
660            self.storage.dealloc(id)
661        }
662
663        fn flush(&mut self) {
664            self.storage.flush()
665        }
666    }
667
668    const DUMMY_MEM_PROPS: MemoryDeviceProperties = MemoryDeviceProperties {
669        max_page_size: 128 * 1024 * 1024,
670        alignment: 32,
671    };
672
673    fn options() -> MemoryManagementOptions {
674        MemoryManagementOptions {
675            name: "test".into(),
676            memory: MemoryAllocationOption::FromConfig,
677        }
678    }
679
680    // Test pools with slices.
681    #[test_log::test]
682    #[cfg(not(exclusive_memory_only))]
683    fn test_handle_mutability() {
684        let mut memory_management = MemoryManagement::from_configuration(
685            BytesStorage::default(),
686            &DUMMY_MEM_PROPS,
687            MemoryConfiguration::SubSlices,
688            Arc::new(ServerLogger::default()),
689            options(),
690        );
691        let handle = memory_management.reserve(10).unwrap();
692        let other_ref = handle.clone();
693        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
694        drop(other_ref);
695        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
696    }
697
698    // Test pools with slices.
699    #[test_log::test]
700    #[cfg(not(exclusive_memory_only))]
701    fn test_memory_usage() {
702        let max_page_size = 512;
703
704        let mut memory_management = MemoryManagement::from_configuration(
705            BytesStorage::default(),
706            &DUMMY_MEM_PROPS,
707            MemoryConfiguration::Custom {
708                pool_options: vec![MemoryPoolOptions {
709                    pool_type: PoolType::ExclusivePages {
710                        max_alloc_size: max_page_size,
711                    },
712                    dealloc_period: None,
713                }],
714            },
715            Arc::new(ServerLogger::default()),
716            options(),
717        );
718        let handle = memory_management.reserve(100);
719        let usage = memory_management.memory_usage();
720
721        assert_eq!(usage.bytes_in_use, 100);
722        assert!(usage.bytes_reserved >= 100 && usage.bytes_reserved <= max_page_size);
723
724        // Drop and re-alloc.
725        drop(handle);
726        let _handle = memory_management.reserve(100);
727        let usage_new = memory_management.memory_usage();
728        assert_eq!(usage, usage_new);
729    }
730
731    #[test_log::test]
732    fn alloc_two_chunks_on_one_page() {
733        let page_size = 2048;
734
735        let mut memory_management = MemoryManagement::from_configuration(
736            BytesStorage::default(),
737            &DUMMY_MEM_PROPS,
738            MemoryConfiguration::Custom {
739                pool_options: vec![MemoryPoolOptions {
740                    pool_type: PoolType::SlicedPages {
741                        page_size,
742                        max_slice_size: page_size,
743                    },
744                    dealloc_period: None,
745                }],
746            },
747            Arc::new(ServerLogger::default()),
748            options(),
749        );
750
751        let alloc_size = 512;
752        let _handle = memory_management.reserve(alloc_size);
753        let _new_handle = memory_management.reserve(alloc_size);
754
755        let usage = memory_management.memory_usage();
756        assert_eq!(usage.number_allocs, 2);
757        assert_eq!(usage.bytes_in_use, alloc_size * 2);
758        assert_eq!(usage.bytes_reserved, page_size);
759    }
760
761    #[test_log::test]
762    fn sliced_pool_allocates_large_slices_exactly() {
763        let mut memory_management = MemoryManagement::from_configuration(
764            BytesStorage::default(),
765            &DUMMY_MEM_PROPS,
766            MemoryConfiguration::Custom {
767                pool_options: vec![MemoryPoolOptions {
768                    pool_type: PoolType::SlicedPages {
769                        page_size: 1024,
770                        max_slice_size: 1024,
771                    },
772                    dealloc_period: None,
773                }],
774            },
775            Arc::new(ServerLogger::default()),
776            options(),
777        );
778
779        let _handle = memory_management.reserve(400).unwrap();
780        let usage = memory_management.memory_usage();
781        assert_eq!(usage.bytes_in_use, 400);
782        assert_eq!(usage.bytes_reserved, 416);
783    }
784
785    #[test_log::test]
786    fn sliced_pool_falls_back_to_an_exact_page_after_oom() {
787        let mut memory_management = MemoryManagement::from_configuration(
788            LimitedStorage {
789                storage: BytesStorage::default(),
790                max_alloc_size: 512,
791                alloc_calls: 0,
792            },
793            &DUMMY_MEM_PROPS,
794            MemoryConfiguration::Custom {
795                pool_options: vec![MemoryPoolOptions {
796                    pool_type: PoolType::SlicedPages {
797                        page_size: 1024,
798                        max_slice_size: 1024,
799                    },
800                    dealloc_period: None,
801                }],
802            },
803            Arc::new(ServerLogger::default()),
804            options(),
805        );
806
807        let _handle = memory_management.reserve(200).unwrap();
808        let usage = memory_management.memory_usage();
809        assert_eq!(usage.bytes_in_use, 200);
810        assert_eq!(usage.bytes_reserved, 224);
811    }
812
813    #[test_log::test]
814    fn sliced_pool_oom_fallback_tries_an_exact_page_at_most_once() {
815        let mut memory_management = MemoryManagement::from_configuration(
816            LimitedStorage {
817                storage: BytesStorage::default(),
818                max_alloc_size: 100,
819                alloc_calls: 0,
820            },
821            &DUMMY_MEM_PROPS,
822            MemoryConfiguration::Custom {
823                pool_options: vec![MemoryPoolOptions {
824                    pool_type: PoolType::SlicedPages {
825                        page_size: 1024,
826                        max_slice_size: 1024,
827                    },
828                    dealloc_period: None,
829                }],
830            },
831            Arc::new(ServerLogger::default()),
832            options(),
833        );
834
835        // Small slice: the full page fails, then the exact 224 B page also fails.
836        let result = memory_management.reserve(200);
837        assert!(matches!(result, Err(IoError::OutOfMemory { .. })));
838        assert_eq!(memory_management.storage().alloc_calls, 2);
839
840        // Large slice (416 B > page_size / 4) is already exact: no second attempt.
841        let result = memory_management.reserve(400);
842        assert!(matches!(result, Err(IoError::OutOfMemory { .. })));
843        assert_eq!(memory_management.storage().alloc_calls, 3);
844        assert_eq!(memory_management.memory_usage().bytes_reserved, 0);
845    }
846
847    #[test_log::test]
848    #[cfg(not(exclusive_memory_only))]
849    fn subslices_reserves_large_allocations_exactly() {
850        const MB: u64 = 1024 * 1024;
851        let mut memory_management = MemoryManagement::from_configuration(
852            BytesStorage::default(),
853            &DUMMY_MEM_PROPS,
854            MemoryConfiguration::SubSlices,
855            Arc::new(ServerLogger::default()),
856            options(),
857        );
858
859        // With a 128 MB device page the largest sliced pool caps slices at 16 MB,
860        // so 20 MB lands in the top exclusive pool as an exact page, not a slab.
861        let _handle = memory_management.reserve(20 * MB).unwrap();
862        let usage = memory_management.memory_usage();
863        assert_eq!(usage.bytes_in_use, 20 * MB);
864        assert_eq!(usage.bytes_reserved, 20 * MB);
865    }
866
867    #[test_log::test]
868    fn exclusive_pool_reuses_the_tightest_free_page() {
869        let mut memory_management = MemoryManagement::from_configuration(
870            BytesStorage::default(),
871            &DUMMY_MEM_PROPS,
872            MemoryConfiguration::Custom {
873                pool_options: vec![MemoryPoolOptions {
874                    pool_type: PoolType::ExclusivePages {
875                        max_alloc_size: 4096,
876                    },
877                    dealloc_period: None,
878                }],
879            },
880            Arc::new(ServerLogger::default()),
881            options(),
882        );
883
884        let large = memory_management.reserve(1024).unwrap();
885        let small = memory_management.reserve(512).unwrap();
886        drop(large);
887        drop(small);
888
889        // 400 B must take the 512 B page so the 1024 B page stays free for 1000 B.
890        let _a = memory_management.reserve(400).unwrap();
891        let _b = memory_management.reserve(1000).unwrap();
892        let usage = memory_management.memory_usage();
893        assert_eq!(usage.number_allocs, 2);
894        assert_eq!(usage.bytes_reserved, 1024 + 512);
895    }
896
897    #[test_log::test]
898    fn alloc_reuses_storage() {
899        // If no storage is re-used, this will allocate two pages.
900        let page_size = 512;
901
902        let mut memory_management = MemoryManagement::from_configuration(
903            BytesStorage::default(),
904            &DUMMY_MEM_PROPS,
905            MemoryConfiguration::Custom {
906                pool_options: vec![MemoryPoolOptions {
907                    pool_type: PoolType::SlicedPages {
908                        page_size,
909                        max_slice_size: page_size,
910                    },
911                    dealloc_period: None,
912                }],
913            },
914            Arc::new(ServerLogger::default()),
915            options(),
916        );
917
918        let alloc_size = 512;
919        let _handle = memory_management.reserve(alloc_size);
920        drop(_handle);
921        let _new_handle = memory_management.reserve(alloc_size);
922
923        let usage = memory_management.memory_usage();
924        assert_eq!(usage.number_allocs, 1);
925        assert_eq!(usage.bytes_in_use, alloc_size);
926        assert_eq!(usage.bytes_reserved, page_size);
927    }
928
929    #[test_log::test]
930    fn alloc_allocs_new_storage() {
931        let page_size = 1024;
932
933        let mut memory_management = MemoryManagement::from_configuration(
934            BytesStorage::default(),
935            &DUMMY_MEM_PROPS,
936            MemoryConfiguration::Custom {
937                pool_options: vec![MemoryPoolOptions {
938                    pool_type: PoolType::SlicedPages {
939                        page_size,
940                        max_slice_size: page_size,
941                    },
942                    dealloc_period: None,
943                }],
944            },
945            Arc::new(ServerLogger::default()),
946            options(),
947        );
948
949        let alloc_size = 768;
950        let _handle = memory_management.reserve(alloc_size);
951        let _new_handle = memory_management.reserve(alloc_size);
952
953        let usage = memory_management.memory_usage();
954        assert_eq!(usage.number_allocs, 2);
955        assert_eq!(usage.bytes_in_use, alloc_size * 2);
956        assert_eq!(usage.bytes_reserved, alloc_size * 2);
957    }
958
959    #[test_log::test]
960    fn alloc_respects_alignment_size() {
961        let page_size = 500;
962        let mut memory_management = MemoryManagement::from_configuration(
963            BytesStorage::default(),
964            &MemoryDeviceProperties {
965                max_page_size: page_size,
966                alignment: 50,
967            },
968            MemoryConfiguration::Custom {
969                pool_options: vec![MemoryPoolOptions {
970                    pool_type: PoolType::SlicedPages {
971                        page_size,
972                        max_slice_size: page_size,
973                    },
974                    dealloc_period: None,
975                }],
976            },
977            Arc::new(ServerLogger::default()),
978            options(),
979        );
980        let alloc_size = 40;
981        let _handle = memory_management.reserve(alloc_size);
982        let _new_handle = memory_management.reserve(alloc_size);
983        let usage = memory_management.memory_usage();
984        // Each slice should be aligned to 50 bytes, so 20 padding bytes.
985        assert_eq!(usage.bytes_padding, 10 * 2);
986    }
987
988    #[test_log::test]
989    fn allocs_on_correct_page() {
990        let sizes = [100, 200, 300, 400];
991
992        let pools = sizes
993            .iter()
994            .map(|size| MemoryPoolOptions {
995                pool_type: PoolType::SlicedPages {
996                    page_size: *size,
997                    max_slice_size: *size,
998                },
999                dealloc_period: None,
1000            })
1001            .collect();
1002        let mut memory_management = MemoryManagement::from_configuration(
1003            BytesStorage::default(),
1004            &MemoryDeviceProperties {
1005                max_page_size: 128 * 1024 * 1024,
1006                alignment: 10,
1007            },
1008            MemoryConfiguration::Custom {
1009                pool_options: pools,
1010            },
1011            Arc::new(ServerLogger::default()),
1012            options(),
1013        );
1014        // Allocate one thing on each page.
1015        let alloc_sizes = [50, 150, 250, 350];
1016        let _handles = alloc_sizes.map(|s| memory_management.reserve(s));
1017
1018        let usage = memory_management.memory_usage();
1019
1020        // These large slices use exact pages in their selected pools.
1021        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
1022        assert_eq!(usage.bytes_reserved, alloc_sizes.iter().sum::<u64>());
1023    }
1024
1025    #[test_log::test]
1026    #[cfg(not(exclusive_memory_only))]
1027    fn allocate_deallocate_reallocate() {
1028        let mut memory_management = MemoryManagement::from_configuration(
1029            BytesStorage::default(),
1030            &MemoryDeviceProperties {
1031                max_page_size: 128 * 1024 * 1024,
1032                alignment: 32,
1033            },
1034            MemoryConfiguration::SubSlices,
1035            Arc::new(ServerLogger::default()),
1036            options(),
1037        );
1038        // Allocate a bunch
1039        let handles: Vec<_> = (0..5)
1040            .map(|i| memory_management.reserve(1000 * (i + 1)))
1041            .collect();
1042        let usage_before = memory_management.memory_usage();
1043        // Deallocate
1044        drop(handles);
1045        // Reallocate
1046        let _new_handles: Vec<_> = (0..5)
1047            .map(|i| memory_management.reserve(1000 * (i + 1)))
1048            .collect();
1049        let usage_after = memory_management.memory_usage();
1050        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
1051        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
1052        // Usage after can actually be _less_ because of defragging.
1053        assert!(usage_before.bytes_reserved >= usage_after.bytes_reserved);
1054    }
1055
1056    #[test_log::test]
1057    #[cfg(not(exclusive_memory_only))]
1058    fn test_fragmentation_resistance() {
1059        let mut memory_management = MemoryManagement::from_configuration(
1060            BytesStorage::default(),
1061            &MemoryDeviceProperties {
1062                max_page_size: 128 * 1024 * 1024,
1063                alignment: 32,
1064            },
1065            MemoryConfiguration::SubSlices,
1066            Arc::new(ServerLogger::default()),
1067            options(),
1068        );
1069        // Allocate a mix of small and large chunks
1070        let sizes = [50, 1000, 100, 5000, 200, 10000, 300];
1071        let handles: Vec<_> = sizes
1072            .iter()
1073            .map(|&size| memory_management.reserve(size).unwrap())
1074            .collect();
1075        let usage_before = memory_management.memory_usage();
1076        // Deallocate every other allocation
1077        for i in (0..handles.len()).step_by(2) {
1078            drop(handles[i].clone());
1079        }
1080        // Reallocate similar sizes
1081        for &size in &sizes[0..sizes.len() / 2] {
1082            memory_management.reserve(size).unwrap();
1083        }
1084        let usage_after = memory_management.memory_usage();
1085        // Check that we haven't increased our memory usage significantly
1086        assert!(usage_after.bytes_reserved <= (usage_before.bytes_reserved as f64 * 1.1) as u64);
1087    }
1088
1089    // Test pools without slices. More or less same as tests above.
1090    #[test_log::test]
1091    fn noslice_test_handle_mutability() {
1092        let mut memory_management = MemoryManagement::from_configuration(
1093            BytesStorage::default(),
1094            &(MemoryDeviceProperties {
1095                max_page_size: 128 * 1024 * 1024,
1096                alignment: 32,
1097            }),
1098            MemoryConfiguration::ExclusivePages,
1099            Arc::new(ServerLogger::default()),
1100            options(),
1101        );
1102        let handle = memory_management.reserve(10).unwrap();
1103        let other_ref = handle.clone();
1104        assert!(!handle.can_mut(), "Handle can't be mut when multiple ref.");
1105        drop(other_ref);
1106        assert!(handle.can_mut(), "Handle should be mut when only one ref.");
1107    }
1108
1109    #[test_log::test]
1110    fn noslice_alloc_two_chunk() {
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: 1024,
1118                    },
1119                    dealloc_period: None,
1120                }],
1121            },
1122            Arc::new(ServerLogger::default()),
1123            options(),
1124        );
1125
1126        let alloc_size = 512;
1127        let _handle = memory_management.reserve(alloc_size);
1128        let _new_handle = memory_management.reserve(alloc_size);
1129
1130        let usage = memory_management.memory_usage();
1131        assert_eq!(usage.number_allocs, 2);
1132        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1133        assert!(usage.bytes_reserved >= alloc_size * 2);
1134    }
1135
1136    #[test_log::test]
1137    fn noslice_alloc_reuses_storage() {
1138        // If no storage is re-used, this will allocate two pages.
1139        let mut memory_management = MemoryManagement::from_configuration(
1140            BytesStorage::default(),
1141            &DUMMY_MEM_PROPS,
1142            MemoryConfiguration::Custom {
1143                pool_options: vec![MemoryPoolOptions {
1144                    pool_type: PoolType::ExclusivePages {
1145                        max_alloc_size: 1024,
1146                    },
1147                    dealloc_period: None,
1148                }],
1149            },
1150            Arc::new(ServerLogger::default()),
1151            options(),
1152        );
1153
1154        let alloc_size = 512;
1155        let _handle = memory_management.reserve(alloc_size);
1156        drop(_handle);
1157        let _new_handle = memory_management.reserve(alloc_size);
1158
1159        let usage = memory_management.memory_usage();
1160        assert_eq!(usage.number_allocs, 1);
1161        assert_eq!(usage.bytes_in_use, alloc_size);
1162        assert!(usage.bytes_reserved >= alloc_size);
1163    }
1164
1165    #[test_log::test]
1166    fn noslice_alloc_allocs_new_storage() {
1167        let mut memory_management = MemoryManagement::from_configuration(
1168            BytesStorage::default(),
1169            &DUMMY_MEM_PROPS,
1170            MemoryConfiguration::Custom {
1171                pool_options: vec![MemoryPoolOptions {
1172                    pool_type: PoolType::ExclusivePages {
1173                        max_alloc_size: 1024,
1174                    },
1175                    dealloc_period: None,
1176                }],
1177            },
1178            Arc::new(ServerLogger::default()),
1179            options(),
1180        );
1181
1182        let alloc_size = 768;
1183        let _handle = memory_management.reserve(alloc_size);
1184        let _new_handle = memory_management.reserve(alloc_size);
1185        let usage = memory_management.memory_usage();
1186        assert_eq!(usage.number_allocs, 2);
1187        assert_eq!(usage.bytes_in_use, alloc_size * 2);
1188        assert!(usage.bytes_reserved >= alloc_size * 2);
1189    }
1190
1191    #[test_log::test]
1192    fn noslice_alloc_respects_alignment_size() {
1193        let mut memory_management = MemoryManagement::from_configuration(
1194            BytesStorage::default(),
1195            &MemoryDeviceProperties {
1196                max_page_size: DUMMY_MEM_PROPS.max_page_size,
1197                alignment: 50,
1198            },
1199            MemoryConfiguration::Custom {
1200                pool_options: vec![MemoryPoolOptions {
1201                    pool_type: PoolType::ExclusivePages {
1202                        max_alloc_size: 50 * 20,
1203                    },
1204                    dealloc_period: None,
1205                }],
1206            },
1207            Arc::new(ServerLogger::default()),
1208            options(),
1209        );
1210        let alloc_size = 40;
1211        let _handle = memory_management.reserve(alloc_size);
1212        let _new_handle = memory_management.reserve(alloc_size);
1213        let usage = memory_management.memory_usage();
1214        // Each allocation is exact apart from its 10 alignment bytes.
1215        assert_eq!(usage.bytes_padding, 10 * 2);
1216        assert_eq!(usage.bytes_reserved, 50 * 2);
1217    }
1218
1219    #[test_log::test]
1220    fn noslice_allocs_on_correct_page() {
1221        let pools = [100, 200, 300, 400]
1222            .iter()
1223            .map(|&size| MemoryPoolOptions {
1224                pool_type: PoolType::SlicedPages {
1225                    page_size: size,
1226                    max_slice_size: size,
1227                },
1228                dealloc_period: None,
1229            })
1230            .collect();
1231        let mut memory_management = MemoryManagement::from_configuration(
1232            BytesStorage::default(),
1233            &MemoryDeviceProperties {
1234                max_page_size: DUMMY_MEM_PROPS.max_page_size,
1235                alignment: 10,
1236            },
1237            MemoryConfiguration::Custom {
1238                pool_options: pools,
1239            },
1240            Arc::new(ServerLogger::default()),
1241            options(),
1242        );
1243        // Allocate one thing on each page.
1244        let alloc_sizes = [50, 150, 250, 350];
1245        let _handles = alloc_sizes.map(|s| memory_management.reserve(s));
1246        let usage = memory_management.memory_usage();
1247        // Total memory should be size of all pages, and no more.
1248        assert_eq!(usage.bytes_in_use, alloc_sizes.iter().sum::<u64>());
1249    }
1250
1251    #[test_log::test]
1252    fn noslice_allocate_deallocate_reallocate() {
1253        let mut memory_management = MemoryManagement::from_configuration(
1254            BytesStorage::default(),
1255            &MemoryDeviceProperties {
1256                max_page_size: 128 * 1024 * 1024,
1257                alignment: 32,
1258            },
1259            MemoryConfiguration::ExclusivePages,
1260            Arc::new(ServerLogger::default()),
1261            options(),
1262        );
1263        // Allocate a bunch
1264        let handles: Vec<_> = (0..5)
1265            .map(|i| memory_management.reserve(1000 * (i + 1)))
1266            .collect();
1267        let usage_before = memory_management.memory_usage();
1268        // Deallocate
1269        drop(handles);
1270        // Reallocate
1271        let _new_handles: Vec<_> = (0..5)
1272            .map(|i| memory_management.reserve(1000 * (i + 1)))
1273            .collect();
1274        let usage_after = memory_management.memory_usage();
1275        assert_eq!(usage_before.number_allocs, usage_after.number_allocs);
1276        assert_eq!(usage_before.bytes_in_use, usage_after.bytes_in_use);
1277        assert_eq!(usage_before.bytes_reserved, usage_after.bytes_reserved);
1278    }
1279}