Skip to main content

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;