Skip to main content

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}