Skip to main content

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}