onnx_runtime_virtual_memory/backing.rs
1//! What a virtual address range is made of.
2//!
3//! [`VirtualBuffer`](crate::VirtualBuffer) owns the interesting part: growth,
4//! leasing, granule rounding, and the promise that the base address never
5//! moves. None of that is platform-specific. What is platform-specific is three
6//! operations — reserve address space, commit a block into part of it, release
7//! that block — plus the granularity everything must be a multiple of.
8//!
9//! Splitting those out is what stops the device implementation from being a
10//! second copy of the growth and leasing logic.
11//!
12//! # Host and device are the same shape
13//!
14//! | | host | CUDA |
15//! |---|---|---|
16//! | reserve | `VirtualAlloc2` placeholder / `mmap(PROT_NONE)` | `cuMemAddressReserve` |
17//! | commit | `MapViewOfFile3` / `mmap(MAP_FIXED)` | `cuMemCreate` + `cuMemMap` + `cuMemSetAccess` |
18//! | release | `UnmapViewOfFile2` / `mmap(PROT_NONE)` | `cuMemUnmap`; pool or `cuMemRelease` |
19//! | granularity | 64 KiB / page size | `cuMemGetAllocationGranularity` |
20//!
21//! Measured on the hardware this was developed against: **64 KiB** on Windows,
22//! **2 MiB** for CUDA on an RTX 4060 — where 2 MiB is roughly a thousand tokens
23//! of one KV tensor at Llama-3-8B geometry.
24//!
25//! # Why there is an associated `Reservation`
26//!
27//! A backing cannot be stateless. Windows requires a placeholder to be *split*
28//! before a block is mapped into part of it, and whether a split is needed
29//! depends on the block's already-mapped neighbours — so committing needs to
30//! know what else is mapped in the same reservation. CUDA needs the same shape
31//! because mappings still belong to a reservation even when their physical
32//! handles outlive them in a shared pool.
33//!
34//! Putting that state in an associated type rather than in the backing keeps
35//! one backing able to serve many reservations, and keeps the state next to the
36//! thing it describes.
37//!
38//! The cost is that `VirtualBacking` is not `dyn`-safe. That is deliberate and
39//! it costs nothing: what callers hold is a
40//! [`VirtualBuffer`](crate::VirtualBuffer), and *that* can be boxed behind an
41//! object-safe trait if it ever needs to be. Nobody needs to hold a backing.
42//!
43//! # Why addresses are `usize`
44//!
45//! A device address is not a host pointer and must never be dereferenced on the
46//! CPU. Typing both as `*mut u8` would invite exactly that.
47
48use crate::VirtualMemoryError;
49use onnx_runtime_memory_governor::MemoryAuthorityId;
50
51/// Who charges physical memory committed by a [`VirtualBacking`].
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum PhysicalMemoryAccounting {
54 /// [`VirtualBuffer`](crate::VirtualBuffer) leases mapped physical bytes.
55 Buffer,
56 /// The backing's authority owns physical bytes independently of mappings.
57 ///
58 /// This is the contract for a physical-handle pool: pooled-unmapped bytes
59 /// remain charged to `authority`, while mapped holder/zone bytes are only
60 /// attribution and must not be charged as additional physical ownership.
61 Backing {
62 /// The one ledger that owns every physical byte held by the backing.
63 authority: MemoryAuthorityId,
64 },
65}
66
67/// The platform operations a [`VirtualBuffer`](crate::VirtualBuffer) is built
68/// from.
69///
70/// # Safety
71///
72/// This trait is `unsafe` to implement because [`VirtualBuffer`] hands out the
73/// base address and lets callers write to the committed prefix. An
74/// implementation that reported a range it had not reserved, or a granularity
75/// it did not honour, would turn those writes into memory corruption rather
76/// than an error. Specifically:
77///
78/// * [`VirtualBacking::granularity`] is constant for the backing's life and a
79/// power of two.
80/// * [`VirtualBacking::reserve`] takes address space only. It must not commit
81/// memory: the whole design rests on reserving generously being free.
82/// * [`VirtualBacking::base`] returns the address the reservation actually
83/// starts at, and that address does not change for the reservation's life.
84/// * After [`VirtualBacking::commit`] returns `Ok`, every byte of
85/// `base + offset .. base + offset + len` is writable through that address.
86/// * Dropping a `Reservation` releases both its address space and any blocks
87/// still committed in it.
88///
89/// [`VirtualBuffer`]: crate::VirtualBuffer
90pub unsafe trait VirtualBacking: Send + Sync + std::fmt::Debug {
91 /// One reserved address range, and whatever the platform needs to remember
92 /// about what is committed in it.
93 type Reservation: Send + Sync + std::fmt::Debug;
94
95 /// Allocation granularity: every offset and length is a multiple of this.
96 fn granularity(&self) -> usize;
97
98 /// Who owns accounting for committed physical memory.
99 ///
100 /// Backings that retain physical allocations after unmapping must return
101 /// [`PhysicalMemoryAccounting::Backing`]. A buffer validates the authority
102 /// before reserving address space and does not take a second physical lease.
103 fn physical_memory_accounting(&self) -> PhysicalMemoryAccounting {
104 PhysicalMemoryAccounting::Buffer
105 }
106
107 /// Reserve `len` bytes of address space, committing nothing.
108 fn reserve(&self, len: usize) -> Result<Self::Reservation, VirtualMemoryError>;
109
110 /// The address the reservation starts at.
111 fn base(reservation: &Self::Reservation) -> usize;
112
113 /// Back `offset..offset + len` of `reservation` with fresh memory.
114 ///
115 /// `offset` and `len` are multiples of [`VirtualBacking::granularity`] and
116 /// the range lies inside the reservation; the implementation is entitled to
117 /// rely on both. Overlapping an already-committed block is a caller error
118 /// the implementation should report rather than assume away.
119 fn commit(
120 &self,
121 reservation: &mut Self::Reservation,
122 offset: usize,
123 len: usize,
124 ) -> Result<(), VirtualMemoryError>;
125
126 /// Give back the block committed at `offset`, leaving the address space
127 /// reserved so it can be committed again later.
128 fn release(
129 &self,
130 reservation: &mut Self::Reservation,
131 offset: usize,
132 len: usize,
133 ) -> Result<(), VirtualMemoryError>;
134}
135
136/// The process's own address space.
137///
138/// The default backing. Uses placeholder reservations on Windows and `mmap` on
139/// unix, both of which let a block be committed into part of a larger
140/// reservation and taken back out without disturbing its neighbours.
141#[derive(Debug, Default, Clone, Copy)]
142pub struct HostBacking;
143
144// SAFETY: every address comes from `VirtualRange::reserve`; the granularity is
145// the platform's own and constant for the process; `VirtualRange` tracks its
146// own mapped blocks and releases everything on drop.
147unsafe impl VirtualBacking for HostBacking {
148 type Reservation = crate::VirtualRange;
149
150 fn granularity(&self) -> usize {
151 crate::granularity()
152 }
153
154 fn reserve(&self, len: usize) -> Result<Self::Reservation, VirtualMemoryError> {
155 crate::VirtualRange::reserve(len)
156 }
157
158 fn base(reservation: &Self::Reservation) -> usize {
159 reservation.as_ptr() as usize
160 }
161
162 fn commit(
163 &self,
164 reservation: &mut Self::Reservation,
165 offset: usize,
166 len: usize,
167 ) -> Result<(), VirtualMemoryError> {
168 reservation.map(offset, len)
169 }
170
171 fn release(
172 &self,
173 reservation: &mut Self::Reservation,
174 offset: usize,
175 _len: usize,
176 ) -> Result<(), VirtualMemoryError> {
177 reservation.unmap(offset)
178 }
179}