onnx_runtime_memory_api/allocator.rs
1//! Primitive memory types and the ordinary allocator contract.
2//!
3//! [`DeviceAllocator`] deliberately covers only device identity plus ordinary
4//! allocation and terminal release. Lazy virtual backing and shared physical
5//! mappings are independent optional capabilities in [`crate::capability`].
6
7use std::any::Any;
8use std::fmt::Debug;
9use std::ptr::NonNull;
10
11use crate::capability::{SharedMapping, VirtualBacking};
12use crate::deferred::{AllocationReleaseOutcome, ReleaseAccounting};
13use crate::{MemoryError, Tier};
14
15#[derive(Clone, Copy, Debug)]
16pub struct AllocationCommitRange {
17 pub ptr: NonNull<u8>,
18 pub allocation_bytes: usize,
19 pub align: usize,
20 pub offset: usize,
21 pub bytes: usize,
22}
23
24#[derive(Debug)]
25pub struct MappedAllocation<T> {
26 pub allocation: T,
27 /// Newly created physical ownership. This is independent of mapping:
28 /// retained-pool/shared handles may map bytes while adding zero ownership.
29 pub additional_owned_bytes: u64,
30 pub newly_mapped_bytes: u64,
31}
32
33/// Which physical device memory comes from.
34///
35/// A [`Tier`] says how far away memory is; this says which device within that
36/// tier. Two CUDA devices are the same tier and different allocators.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub struct DeviceKey {
39 pub tier: Tier,
40 pub index: u32,
41}
42
43impl DeviceKey {
44 pub const HOST: Self = Self {
45 tier: Tier::Host,
46 index: 0,
47 };
48
49 pub const fn device(index: u32) -> Self {
50 Self {
51 tier: Tier::Device,
52 index,
53 }
54 }
55}
56
57/// A pinned, read-only shared prefix whose physical bytes are owned once and
58/// may be mapped into multiple allocations.
59pub trait SharedDevicePrefix: Send + Sync + Debug {
60 fn device_ptr(&self) -> u64;
61 fn committed_physical_bytes(&self) -> u64;
62 fn mapped_bytes(&self) -> usize;
63 fn requested_bytes(&self) -> usize;
64
65 /// Concrete-prefix recovery for the mapping implementation that created
66 /// this opaque handle.
67 ///
68 /// This is not an allocator/capability identity proof. Capability coherence
69 /// is the trusted contract documented on [`DeviceAllocator`].
70 fn as_any(&self) -> &dyn Any;
71}
72
73/// The accounting outcome of mapping a [`SharedDevicePrefix`] into one
74/// allocation.
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct SharedPrefixCommitInfo {
77 /// Newly owned physical bytes. A valid additional shared mapping reports
78 /// zero because the prefix was charged once when created.
79 pub additional_owned_bytes: u64,
80 /// Newly mapped bytes on the mapped-attribution axis.
81 pub newly_mapped_bytes: u64,
82 pub granules: usize,
83}
84
85/// Somewhere ordinary memory comes from.
86///
87/// An eager allocator implements only these three required methods. It does
88/// not implement degenerate commit/decommit or sharing methods.
89///
90/// # Safety and coherence contract
91///
92/// This raw-pointer boundary is trusted. Implementations must return unique,
93/// suitably aligned live allocations and must release only their own
94/// allocations. A capability returned by [`as_virtual_backing`](Self::as_virtual_backing)
95/// or [`as_shared_mapping`](Self::as_shared_mapping) must operate on the same
96/// selected mechanism and [`DeviceKey`] as this allocator, and that answer must
97/// remain stable for the allocator's lifetime. Transparent wrappers may
98/// forward all three interfaces to one coherent inner mechanism.
99///
100/// Rust does not structurally prove that a hostile wrapper delegates its
101/// ordinary allocation, optional capabilities, and release to one inner
102/// object. Runtime identity heuristics are not a security boundary here.
103/// In-tree unsafe implementations are responsible for satisfying this contract
104/// and are tested as coherent units.
105///
106/// Whole-allocation terminal release always comes back through this trait,
107/// including allocations reserved through [`VirtualBacking`]. Optional
108/// capability traits never take ownership of terminal release.
109pub trait DeviceAllocator: Send + Sync + Debug {
110 /// Take `bytes` aligned to `align`.
111 fn allocate(&self, bytes: usize, align: usize) -> Result<NonNull<u8>, MemoryError>;
112
113 /// Give back a whole allocation returned by this allocator or by the
114 /// [`VirtualBacking`] capability discovered from this allocator.
115 ///
116 /// This is the pre-Phase-4 **migration adapter**. It cannot report partial
117 /// failure, so new code should implement and call
118 /// [`release`](Self::release) instead; the default `release` forwards here
119 /// so existing implementations keep working unchanged.
120 ///
121 /// # Safety
122 ///
123 /// `ptr` must identify one live allocation from this coherent mechanism
124 /// with exactly this `bytes` and `align`, and must not be released twice.
125 unsafe fn deallocate(&self, ptr: NonNull<u8>, bytes: usize, align: usize);
126
127 /// Give back a whole allocation and report bytes whose global mapping
128 /// reference transitioned to unmapped.
129 ///
130 /// Eager allocators have no mapped attribution and inherit zero. This
131 /// remains part of canonical whole-allocation release; it is intentionally
132 /// not a method on either optional capability.
133 ///
134 /// Like [`deallocate`](Self::deallocate), this is a migration adapter that
135 /// cannot express partial failure. Zero is a valid answer here and never
136 /// means the release failed.
137 ///
138 /// # Safety
139 ///
140 /// The same requirements as [`deallocate`](Self::deallocate).
141 unsafe fn deallocate_with_unmapped(&self, ptr: NonNull<u8>, bytes: usize, align: usize) -> u64 {
142 // SAFETY: forwarded under this method's identical contract.
143 unsafe { self.deallocate(ptr, bytes, align) };
144 0
145 }
146
147 /// Give back a whole allocation and report a **structured** outcome.
148 ///
149 /// This is the Phase-4 canonical release entry point. It is additive: the
150 /// default implementation is an eager adapter over
151 /// [`deallocate_with_unmapped`](Self::deallocate_with_unmapped), so every
152 /// existing allocator keeps working unchanged and reports
153 /// [`AllocationReleaseOutcome::Complete`].
154 ///
155 /// # Honesty requirements
156 ///
157 /// * [`AllocationReleaseOutcome::Complete`] means the whole allocation is
158 /// gone (freed or pooled). Zero unmapped bytes is a valid complete
159 /// result and must never be used to signal failure.
160 /// * [`AllocationReleaseOutcome::Failed`] may be returned **only** when
161 /// nothing was mutated. It is the one shape that implies "unchanged".
162 /// * Any partial mutation — some granules unmapped, some handles released,
163 /// an error partway through a multi-step teardown — must be
164 /// [`AllocationReleaseOutcome::Quarantined`] carrying the bytes actually
165 /// unmapped and the residual ownership that remains.
166 ///
167 /// # Safety
168 ///
169 /// The same requirements as [`deallocate`](Self::deallocate).
170 unsafe fn release(
171 &self,
172 ptr: NonNull<u8>,
173 bytes: usize,
174 align: usize,
175 ) -> AllocationReleaseOutcome {
176 // SAFETY: forwarded under this method's identical contract.
177 let unmapped_bytes = unsafe { self.deallocate_with_unmapped(ptr, bytes, align) };
178 AllocationReleaseOutcome::complete(ReleaseAccounting {
179 allocation_bytes: bytes as u64,
180 unmapped_bytes,
181 })
182 }
183
184 fn device(&self) -> DeviceKey;
185
186 /// Whether this allocator maps physical memory lazily **and** charges a
187 /// governor as each physical commitment is made.
188 ///
189 /// This is an accounting promise, not capability discovery. An allocator
190 /// may expose [`VirtualBacking`] while returning `false` here when its
191 /// commit operations are not integrated with a governor. Consumers that
192 /// skip an eager full-footprint reservation rely on both halves of this
193 /// contract, so `false` is the safe default.
194 fn commits_on_demand(&self) -> bool {
195 false
196 }
197
198 /// Discover lazy reserve/commit/decommit support from this selected
199 /// allocator reference.
200 fn as_virtual_backing(&self) -> Option<&dyn VirtualBacking> {
201 None
202 }
203
204 /// Discover shared physical mapping support independently from virtual
205 /// backing support.
206 fn as_shared_mapping(&self) -> Option<&dyn SharedMapping> {
207 None
208 }
209}
210
211/// Host memory from the global allocator.
212///
213/// This is intentionally eager-only: it implements no optional capability.
214#[derive(Debug, Default, Clone, Copy)]
215pub struct HostAllocator;
216
217impl DeviceAllocator for HostAllocator {
218 fn allocate(&self, bytes: usize, align: usize) -> Result<NonNull<u8>, MemoryError> {
219 let layout = std::alloc::Layout::from_size_align(bytes.max(1), align).map_err(|_| {
220 MemoryError::InvalidRequest {
221 tier: Tier::Host.name(),
222 requested: bytes as u64,
223 reason: "the requested size and alignment are not a valid layout; the alignment \
224 must be a power of two and the rounded size must not overflow",
225 }
226 })?;
227 // SAFETY: `layout` has a non-zero size and valid power-of-two alignment.
228 let ptr = unsafe { std::alloc::alloc(layout) };
229 NonNull::new(ptr).ok_or_else(|| MemoryError::AllocationFailed {
230 tier: Tier::Host.name(),
231 requested: bytes as u64,
232 reason: String::from(
233 "the system allocator refused bytes the governor had granted; the process is \
234 out of address space or the host is out of memory",
235 ),
236 })
237 }
238
239 unsafe fn deallocate(&self, ptr: NonNull<u8>, bytes: usize, align: usize) {
240 let Ok(layout) = std::alloc::Layout::from_size_align(bytes.max(1), align) else {
241 return;
242 };
243 // SAFETY: delegated to this method's contract.
244 unsafe { std::alloc::dealloc(ptr.as_ptr(), layout) };
245 }
246
247 fn device(&self) -> DeviceKey {
248 DeviceKey::HOST
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[derive(Debug)]
257 struct EagerOnly;
258
259 impl DeviceAllocator for EagerOnly {
260 fn allocate(&self, bytes: usize, align: usize) -> Result<NonNull<u8>, MemoryError> {
261 HostAllocator.allocate(bytes, align)
262 }
263
264 unsafe fn deallocate(&self, ptr: NonNull<u8>, bytes: usize, align: usize) {
265 // SAFETY: forwarded unchanged from this method's contract.
266 unsafe { HostAllocator.deallocate(ptr, bytes, align) };
267 }
268
269 fn device(&self) -> DeviceKey {
270 DeviceKey::HOST
271 }
272 }
273
274 #[test]
275 fn eager_allocator_requires_only_the_ordinary_contract() {
276 let allocator: &dyn DeviceAllocator = &EagerOnly;
277 assert!(!allocator.commits_on_demand());
278 assert!(allocator.as_virtual_backing().is_none());
279 assert!(allocator.as_shared_mapping().is_none());
280 let ptr = allocator.allocate(64, 16).expect("ordinary allocation");
281 // SAFETY: exact live allocation returned above.
282 unsafe { allocator.deallocate(ptr, 64, 16) };
283 }
284
285 #[test]
286 fn host_allocations_are_aligned_as_requested() {
287 for (bytes, align) in [(1usize, 64usize), (100, 64), (4096, 256), (7, 8)] {
288 let ptr = HostAllocator.allocate(bytes, align).expect("granted");
289 assert_eq!(ptr.as_ptr() as usize % align, 0);
290 // SAFETY: exact live allocation returned above.
291 unsafe { HostAllocator.deallocate(ptr, bytes, align) };
292 }
293 }
294
295 #[test]
296 fn zero_byte_allocation_is_non_null() {
297 let ptr = HostAllocator.allocate(0, 64).expect("zero bytes is valid");
298 // SAFETY: exact live allocation returned above.
299 unsafe { HostAllocator.deallocate(ptr, 0, 64) };
300 }
301
302 #[test]
303 fn invalid_alignment_is_refused_with_a_reason() {
304 let error = HostAllocator
305 .allocate(64, 3)
306 .expect_err("alignment must be a power of two");
307 assert!(error.to_string().contains("power of two"), "{error}");
308 }
309
310 #[test]
311 fn live_host_allocations_are_distinct_and_writable() {
312 let first = HostAllocator.allocate(256, 64).expect("first");
313 let second = HostAllocator.allocate(256, 64).expect("second");
314 unsafe {
315 std::ptr::write_bytes(first.as_ptr(), 0x11, 256);
316 std::ptr::write_bytes(second.as_ptr(), 0x22, 256);
317 for offset in 0..256 {
318 assert_eq!(*first.as_ptr().add(offset), 0x11);
319 assert_eq!(*second.as_ptr().add(offset), 0x22);
320 }
321 HostAllocator.deallocate(first, 256, 64);
322 HostAllocator.deallocate(second, 256, 64);
323 }
324 }
325
326 #[test]
327 fn device_keys_distinguish_host_and_accelerators() {
328 assert_eq!(HostAllocator.device(), DeviceKey::HOST);
329 assert_ne!(DeviceKey::device(0), DeviceKey::device(1));
330 assert_eq!(DeviceKey::device(1).tier, Tier::Device);
331 }
332}