Skip to main content

cubecl_runtime/memory_management/
layout.rs

1//! How each [`MemoryConfiguration`] lays its pools out on a device.
2//!
3//! Here rather than in the server that builds the pools, because the presets
4//! are only what they are under this crate's `exclusive_memory_only` cfg: a
5//! crate deciding that for itself disagrees with this one as soon as the
6//! feature is turned on here directly, and names a preset that is not there.
7
8use super::{MemoryConfiguration, MemoryPoolOptions, PoolType};
9#[cfg(not(exclusive_memory_only))]
10use alloc::vec;
11use alloc::vec::Vec;
12use cubecl_ir::MemoryDeviceProperties;
13
14/// Whether this build refuses pools that share a page between allocations —
15/// the `exclusive-memory-only` feature, or a wasm target.
16///
17/// Decided by this crate alone, for the reason the module gives; a crate that
18/// needs to know reads it here instead of deciding again.
19pub const EXCLUSIVE_MEMORY_ONLY: bool = cfg!(exclusive_memory_only);
20
21impl MemoryConfiguration {
22    /// The pools this configuration lays out on a device with `properties`,
23    /// in the order an allocation tries them.
24    pub fn pool_options(self, properties: &MemoryDeviceProperties) -> Vec<MemoryPoolOptions> {
25        match self {
26            #[cfg(not(exclusive_memory_only))]
27            MemoryConfiguration::SubSlices => {
28                // Round chunk size to be aligned.
29                let memory_alignment = properties.alignment;
30                let max_page = properties.max_page_size;
31                let mut pools = Vec::new();
32
33                const MB: u64 = 1024 * 1024;
34
35                // Add in a pool for allocations that are smaller than the min alignment,
36                // as they can't use offsets at all (on wgpu at least).
37                pools.push(MemoryPoolOptions {
38                    pool_type: PoolType::ExclusivePages { max_alloc_size: 0 },
39                    dealloc_period: None,
40                });
41
42                let mut current = max_page;
43                let mut max_sizes = vec![];
44                let mut page_sizes = vec![];
45                let mut base = pools.len() as u32;
46
47                while current >= 32 * MB {
48                    current /= 4;
49
50                    // Make sure every pool has an aligned size.
51                    current = current.next_multiple_of(memory_alignment);
52
53                    max_sizes.push(current / 2u64.pow(base));
54                    page_sizes.push(current);
55                    base += 1;
56                }
57
58                max_sizes.reverse();
59                page_sizes.reverse();
60
61                for i in 0..max_sizes.len() {
62                    let max = max_sizes[i];
63                    let page_size = page_sizes[i];
64
65                    pools.push(MemoryPoolOptions {
66                        // Creating max slices lower than the chunk size reduces fragmentation.
67                        pool_type: PoolType::SlicedPages {
68                            page_size,
69                            max_slice_size: max,
70                            max_pool_size: None,
71                        },
72                        dealloc_period: None,
73                    });
74                }
75
76                // Allocations bigger than the sliced ladder get exact-size
77                // exclusive pages. A sliced tail pool here would materialize a
78                // whole `max_page` page (a quarter of device memory) for the
79                // first allocation that lands in it — on unified-memory devices
80                // that alone can consume a large share of host RAM. Exclusive
81                // pages allocate exactly what is requested and are released once
82                // they sit unused for a full dealloc period.
83                let max_alloc = max_page / memory_alignment * memory_alignment;
84                let dealloc_period = (BASE_DEALLOC_PERIOD as f64
85                    * (1.0 + max_alloc as f64 / (DEALLOC_SCALE_MB as f64)).round())
86                    as u64;
87                pools.push(MemoryPoolOptions {
88                    pool_type: PoolType::ExclusivePages {
89                        max_alloc_size: max_alloc,
90                    },
91                    dealloc_period: Some(dealloc_period),
92                });
93                pools
94            }
95            MemoryConfiguration::ExclusivePages => {
96                // Add all bin sizes. Nb: because of alignment some buckets
97                // end up as the same size, so only want unique ones,
98                // but also keep the order, so a BTree will do.
99                const MIN_BUCKET_SIZE: u64 = 1024 * 32;
100                const NUM_POOLS: usize = 24;
101
102                let sizes = generate_bucket_sizes(
103                    MIN_BUCKET_SIZE,
104                    properties.max_page_size,
105                    NUM_POOLS,
106                    properties.alignment,
107                );
108
109                sizes
110                    .iter()
111                    .map(|&size| {
112                        let dealloc_period = (BASE_DEALLOC_PERIOD as f64
113                            * (1.0 + size as f64 / (DEALLOC_SCALE_MB as f64)).round())
114                            as u64;
115
116                        MemoryPoolOptions {
117                            pool_type: PoolType::ExclusivePages {
118                                max_alloc_size: size,
119                            },
120                            dealloc_period: Some(dealloc_period),
121                        }
122                    })
123                    .collect()
124            }
125            MemoryConfiguration::Custom { pool_options } => pool_options,
126        }
127    }
128
129    /// Whether this is the [`SubSlices`](Self::SubSlices) preset — never, in
130    /// a build that has none.
131    pub fn is_sub_slices(&self) -> bool {
132        match self {
133            #[cfg(not(exclusive_memory_only))]
134            Self::SubSlices => true,
135            _ => false,
136        }
137    }
138}
139
140fn generate_bucket_sizes(
141    start_size: u64,
142    end_size: u64,
143    max_buckets: usize,
144    alignment: u64,
145) -> Vec<u64> {
146    let mut buckets = Vec::with_capacity(max_buckets);
147    let log_min = (start_size as f64).ln();
148    let log_max = (end_size as f64).ln();
149    let log_range = log_max - log_min;
150
151    // Pure exponential performed best, but let's try slightly denser in lower-mid range
152    for i in 0..max_buckets {
153        let p = i as f64 / (max_buckets - 1) as f64;
154        // Slight bias toward lower-mid range with less aggressive curve than sigmoid
155        let log_size = log_min + log_range * p;
156        let size = log_size.exp() as u64;
157        let aligned_size = size.next_multiple_of(alignment);
158        buckets.push(aligned_size);
159    }
160
161    buckets.dedup();
162    buckets
163}
164
165const DEALLOC_SCALE_MB: u64 = 1024 * 1024 * 1024;
166const BASE_DEALLOC_PERIOD: u64 = 5000;