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;