cubecl_runtime/memory_management/config.rs
1//! Resolving a memory configuration into the pools it asks for: value types
2//! only, so the client can validate a layout before shipping it to a server.
3
4use super::{MemoryConfiguration, MemoryPoolOptions, PoolType};
5use crate::config::memory::{MemoryPoolConfig, MemoryPoolsConfig, MemoryPoolsPreset};
6use alloc::vec::Vec;
7use cubecl_ir::MemoryDeviceProperties;
8
9/// Why a `memory.pools` config could not be turned into a pool layout.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum PoolConfigError {
12 /// `memory.pools` was an empty list.
13 EmptyPoolList,
14 /// A size field that must be non-zero was zero.
15 ZeroSize {
16 /// The offending field.
17 field: &'static str,
18 },
19 /// `max_slice_size` exceeds `page_size` (a slice can never span pages).
20 SliceLargerThanPage {
21 /// The page size in bytes (after alignment).
22 page_size: u64,
23 /// The maximum slice size in bytes (after alignment).
24 max_slice_size: u64,
25 },
26 /// `max_pool_size` is smaller than `page_size` (the cap can't fit one page).
27 CapSmallerThanPage {
28 /// The page size in bytes (after alignment).
29 page_size: u64,
30 /// The pool capacity in bytes.
31 max_pool_size: u64,
32 },
33 /// `max_pool_size` spans more pages of `page_size` than a pool can hold.
34 TooManyPages {
35 /// The number of pages the configuration asks for.
36 pages: u64,
37 },
38 /// The pool list has more entries than the pool routing can address.
39 TooManyPools {
40 /// The number of entries in the configuration.
41 count: usize,
42 },
43 /// The preset is not available in this build.
44 PresetUnavailable {
45 /// The preset name.
46 preset: &'static str,
47 },
48 /// Sliced pools are not available in this build (`exclusive_memory_only`).
49 SlicedPoolsUnavailable,
50}
51
52impl core::fmt::Display for PoolConfigError {
53 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
54 match self {
55 PoolConfigError::EmptyPoolList => write!(f, "the pool list is empty"),
56 PoolConfigError::ZeroSize { field } => write!(f, "`{field}` must be non-zero"),
57 PoolConfigError::SliceLargerThanPage {
58 page_size,
59 max_slice_size,
60 } => write!(
61 f,
62 "`max_slice_size` ({max_slice_size}) exceeds `page_size` ({page_size}); a slice can never span pages"
63 ),
64 PoolConfigError::CapSmallerThanPage {
65 page_size,
66 max_pool_size,
67 } => write!(
68 f,
69 "`max_pool_size` ({max_pool_size}) is smaller than `page_size` ({page_size}); the cap can't fit a single page"
70 ),
71 PoolConfigError::TooManyPages { pages } => write!(
72 f,
73 "`max_pool_size` spans {pages} pages of `page_size`, exceeding the maximum of {}; increase `page_size` or lower the cap",
74 u16::MAX
75 ),
76 PoolConfigError::TooManyPools { count } => write!(
77 f,
78 "the pool list has {count} entries, exceeding the maximum of {} dynamic pools",
79 PERSISTENT_POOL_POS - 1
80 ),
81 PoolConfigError::PresetUnavailable { preset } => {
82 write!(f, "the `{preset}` preset is not available in this build")
83 }
84 PoolConfigError::SlicedPoolsUnavailable => {
85 write!(
86 f,
87 "sliced pools are not available in this build (exclusive memory only)"
88 )
89 }
90 }
91 }
92}
93
94impl core::error::Error for PoolConfigError {}
95
96impl MemoryConfiguration {
97 /// Resolve a programmatic [`MemoryPoolsConfig`] override against the
98 /// runtime-chosen configuration for the **main GPU** pool.
99 ///
100 /// When `pools` is `None`, the runtime's own `self` is kept unchanged;
101 /// when present, it wins. There is deliberately no config-file pathway for
102 /// pool layouts — they are dynamic (set per model just before a load) and
103 /// must not freeze at startup; the override reaches the server through
104 /// [`install_memory_pools`](crate::client::Client::install_memory_pools).
105 ///
106 /// `page_size` is deliberately not validated against
107 /// [`MemoryDeviceProperties::max_page_size`]: that value is a sizing
108 /// heuristic for the default layouts (CUDA/HIP report a quarter of the
109 /// device memory), not an allocation limit, and a large arena is exactly
110 /// what an explicit pool override is for. An unallocatable page fails at
111 /// allocation time.
112 pub fn resolve(
113 self,
114 pools: Option<&MemoryPoolsConfig>,
115 properties: &MemoryDeviceProperties,
116 ) -> Result<Self, PoolConfigError> {
117 let Some(pools) = pools else {
118 return Ok(self);
119 };
120
121 match pools {
122 MemoryPoolsConfig::Preset(MemoryPoolsPreset::SubSlices) => {
123 #[cfg(exclusive_memory_only)]
124 {
125 Err(PoolConfigError::PresetUnavailable {
126 preset: "sub-slices",
127 })
128 }
129 #[cfg(not(exclusive_memory_only))]
130 {
131 Ok(MemoryConfiguration::SubSlices)
132 }
133 }
134 MemoryPoolsConfig::Preset(MemoryPoolsPreset::ExclusivePages) => {
135 Ok(MemoryConfiguration::ExclusivePages)
136 }
137 MemoryPoolsConfig::Explicit(entries) => {
138 if entries.is_empty() {
139 return Err(PoolConfigError::EmptyPoolList);
140 }
141 // Slices route through their pool's position, and the
142 // persistent pool owns the sentinel position, so the list must
143 // stay addressable below it — checked here so the caller gets
144 // the error instead of a panic on the device thread.
145 if entries.len() >= PERSISTENT_POOL_POS as usize {
146 return Err(PoolConfigError::TooManyPools {
147 count: entries.len(),
148 });
149 }
150 let pool_options = entries
151 .iter()
152 .map(|entry| pool_options_from_entry(entry, properties))
153 .collect::<Result<Vec<_>, _>>()?;
154 Ok(MemoryConfiguration::Custom { pool_options })
155 }
156 }
157 }
158}
159
160/// Convert one config entry into runtime pool options, aligning sizes up to
161/// the device alignment (a device constraint, not a user error).
162fn pool_options_from_entry(
163 entry: &MemoryPoolConfig,
164 properties: &MemoryDeviceProperties,
165) -> Result<MemoryPoolOptions, PoolConfigError> {
166 let alignment = properties.alignment.max(1);
167 match entry {
168 MemoryPoolConfig::Exclusive {
169 max_alloc_size,
170 dealloc_period,
171 } => {
172 // 0 stays 0: a pool dedicated to zero-sized allocations, as used by
173 // the `SubSlices` preset.
174 let max_alloc_size = max_alloc_size.bytes().next_multiple_of(alignment);
175 Ok(MemoryPoolOptions {
176 pool_type: PoolType::ExclusivePages { max_alloc_size },
177 dealloc_period: *dealloc_period,
178 })
179 }
180 MemoryPoolConfig::Direct { reclaim_at } => Ok(MemoryPoolOptions {
181 pool_type: PoolType::Direct {
182 reclaim_at: reclaim_at.map(|size| size.bytes()),
183 },
184 // `dealloc_period` has no meaning here: the pool reclaims on
185 // memory pressure, not on an allocation count.
186 dealloc_period: None,
187 }),
188 // Sliced pools break the invariant `exclusive_memory_only` builds rely
189 // on (e.g. wgpu on wasm assumes a buffer is never shared between
190 // slices), so an explicit list must be rejected just like the
191 // `sub-slices` preset is.
192 #[cfg(exclusive_memory_only)]
193 MemoryPoolConfig::Sliced { .. } => Err(PoolConfigError::SlicedPoolsUnavailable),
194 #[cfg(not(exclusive_memory_only))]
195 MemoryPoolConfig::Sliced {
196 page_size,
197 max_slice_size,
198 max_pool_size,
199 dealloc_period,
200 } => {
201 if page_size.bytes() == 0 {
202 return Err(PoolConfigError::ZeroSize { field: "page_size" });
203 }
204
205 let page_size = page_size.bytes().next_multiple_of(alignment);
206 let max_slice_size = match max_slice_size {
207 Some(size) if size.bytes() == 0 => {
208 return Err(PoolConfigError::ZeroSize {
209 field: "max_slice_size",
210 });
211 }
212 Some(size) => size.bytes().next_multiple_of(alignment),
213 None => page_size,
214 };
215 if max_slice_size > page_size {
216 return Err(PoolConfigError::SliceLargerThanPage {
217 page_size,
218 max_slice_size,
219 });
220 }
221 if let Some(cap) = max_pool_size {
222 let cap = cap.bytes();
223 if cap == 0 {
224 return Err(PoolConfigError::ZeroSize {
225 field: "max_pool_size",
226 });
227 }
228 if cap < page_size {
229 return Err(PoolConfigError::CapSmallerThanPage {
230 page_size,
231 max_pool_size: cap,
232 });
233 }
234 let pages = cap / page_size;
235 if pages > u16::MAX as u64 {
236 return Err(PoolConfigError::TooManyPages { pages });
237 }
238 }
239
240 Ok(MemoryPoolOptions {
241 pool_type: PoolType::SlicedPages {
242 page_size,
243 max_slice_size,
244 max_pool_size: max_pool_size.map(|size| size.bytes()),
245 },
246 dealloc_period: *dealloc_period,
247 })
248 }
249 }
250}
251
252/// The pool position stamped on persistent-pool slices, routing their binds
253/// and lookups to the persistent pool. A fixed sentinel (rather than "one past
254/// the dynamic pools") so live persistent slices stay routable when
255/// [`MemoryManagement::install_pools`] rebuilds the dynamic pools with a
256/// different count.
257#[doc(hidden)]
258pub const PERSISTENT_POOL_POS: u8 = u8::MAX;