Skip to main content

onnx_runtime_virtual_memory/
buffer.rs

1//! A buffer that grows without moving.
2//!
3//! # The problem this solves
4//!
5//! A KV cache grows a token at a time. The obvious implementation reallocates
6//! and copies, and that costs three things beyond the copy itself:
7//!
8//! * **The address changes.** Anything holding the old pointer is wrong. A
9//!   captured device graph recorded the old address, so the capture is dead and
10//!   has to be retaken.
11//! * **Peak memory doubles at the seam.** Old and new are live at once, so a
12//!   grow can fail at a tier that is merely full rather than over-subscribed.
13//! * **The copy is O(everything so far)**, paid on every growth step.
14//!
15//! Reserving address space is free — it costs no memory, only address bits,
16//! and 64-bit processes have plenty. So reserve for the largest the buffer
17//! could ever be, and commit physical pages behind it as it actually grows. The
18//! base address is fixed at reservation and never moves again.
19//!
20//! # What it costs instead
21//!
22//! Growth rounds up to the platform's mapping granularity — 64 KiB on Windows,
23//! a page on unix — so a buffer that grows by a hundred bytes commits a whole
24//! granule. That is a real overhead on small buffers and irrelevant on the
25//! large ones this exists for.
26//!
27//! # Leasing
28//!
29//! Only **committed** bytes are leased. Reserving is not an allocation and
30//! charging for it would make a governor refuse a buffer that will never use
31//! the address space it reserved — which is precisely the arrangement that
32//! makes reserving generously safe.
33
34use std::sync::Arc;
35
36use onnx_runtime_memory_governor::{
37    HolderId, MemoryError, MemoryGovernor, MemoryLease, MemoryRole, Tier,
38};
39
40use crate::VirtualMemoryError;
41use crate::backing::{HostBacking, PhysicalMemoryAccounting, VirtualBacking};
42
43/// A growable region whose base address never changes.
44///
45/// Created with a *capacity* — an upper bound on address space — and a length
46/// that starts at zero. [`VirtualBuffer::grow_to`] commits pages;
47/// [`VirtualBuffer::shrink_to`] gives them back. The pointer returned by
48/// [`VirtualBuffer::as_ptr`] is the same for the buffer's whole life.
49pub struct VirtualBuffer<B: VirtualBacking = HostBacking> {
50    backing: B,
51    reservation: B::Reservation,
52    /// Address space reserved at construction. Recorded here because a
53    /// reservation is opaque to this type -- only the backing knows its shape.
54    capacity_bytes: usize,
55    /// Bytes the caller has asked for, which may be less than what is committed
56    /// because commitment rounds up to a granule.
57    len: usize,
58    /// Bytes actually backed by physical memory. Always a multiple of the
59    /// granularity and always at least `len`.
60    committed: usize,
61    governor: Arc<dyn MemoryGovernor + Send + Sync>,
62    tier: Tier,
63    role: MemoryRole,
64    holder: HolderId,
65    physical_memory_accounting: PhysicalMemoryAccounting,
66    /// Covers exactly `committed` bytes. `None` while nothing is committed,
67    /// because a zero-byte lease is not a thing to hold.
68    lease: Option<MemoryLease>,
69}
70
71/// What went wrong growing or shrinking a [`VirtualBuffer`].
72#[derive(Debug, thiserror::Error)]
73pub enum VirtualBufferError {
74    /// The address space could not be reserved or mapped.
75    #[error(transparent)]
76    Memory(#[from] VirtualMemoryError),
77    /// The governor refused to lease the pages the growth needs.
78    #[error(transparent)]
79    Budget(#[from] MemoryError),
80    /// A backing and buffer were connected to different accounting books.
81    #[error(
82        "virtual backing charges physical memory to {backing}, but the buffer governor uses \
83         {governor}; both must use the same memory authority"
84    )]
85    AuthorityMismatch {
86        /// Authority that owns the backing's physical allocations.
87        backing: onnx_runtime_memory_governor::MemoryAuthorityId,
88        /// Authority supplied to the buffer.
89        governor: onnx_runtime_memory_governor::MemoryAuthorityId,
90    },
91    /// The request exceeds the address space reserved at construction.
92    #[error(
93        "cannot grow to {requested} bytes: this buffer reserved {capacity} bytes of address \
94         space and the reservation cannot be extended in place; construct it with a larger \
95         capacity"
96    )]
97    OverCapacity {
98        /// What was asked for.
99        requested: usize,
100        /// What was reserved.
101        capacity: usize,
102    },
103}
104
105impl VirtualBuffer<HostBacking> {
106    /// Reserve capacity bytes of the process's own address space.
107    ///
108    /// The device equivalent takes a backing; see [VirtualBuffer::with_backing].
109    pub fn with_capacity(
110        capacity: usize,
111        governor: Arc<dyn MemoryGovernor + Send + Sync>,
112        tier: Tier,
113        role: MemoryRole,
114        holder: HolderId,
115    ) -> Result<Self, VirtualBufferError> {
116        Self::with_backing(HostBacking, capacity, governor, tier, role, holder)
117    }
118}
119
120impl<B: VirtualBacking> VirtualBuffer<B> {
121    /// Reserve `capacity` bytes of address space from `backing`, committing
122    /// nothing.
123    ///
124    /// `capacity` is rounded up to the backing's granularity. Reserve for the
125    /// largest the buffer could ever be: it costs address space, not memory,
126    /// and it is the only bound that cannot be raised later.
127    pub fn with_backing(
128        backing: B,
129        capacity: usize,
130        governor: Arc<dyn MemoryGovernor + Send + Sync>,
131        tier: Tier,
132        role: MemoryRole,
133        holder: HolderId,
134    ) -> Result<Self, VirtualBufferError> {
135        let physical_memory_accounting = backing.physical_memory_accounting();
136        if let PhysicalMemoryAccounting::Backing { authority } = physical_memory_accounting {
137            let governor_authority = governor.authority_id();
138            if authority != governor_authority {
139                return Err(VirtualBufferError::AuthorityMismatch {
140                    backing: authority,
141                    governor: governor_authority,
142                });
143            }
144        }
145        let capacity = round_up(backing.granularity(), capacity.max(1));
146        let reservation = backing.reserve(capacity)?;
147        Ok(Self {
148            backing,
149            reservation,
150            capacity_bytes: capacity,
151            len: 0,
152            committed: 0,
153            governor,
154            tier,
155            role,
156            holder,
157            physical_memory_accounting,
158            lease: None,
159        })
160    }
161
162    /// Bytes the caller has asked for.
163    pub fn len(&self) -> usize {
164        self.len
165    }
166
167    /// Whether the buffer holds nothing yet.
168    pub fn is_empty(&self) -> bool {
169        self.len == 0
170    }
171
172    /// Bytes backed by physical memory, and therefore leased.
173    ///
174    /// At least [`VirtualBuffer::len`] and rounded to a granule, so the two
175    /// differ by up to one granule after any growth.
176    pub fn committed(&self) -> usize {
177        self.committed
178    }
179
180    /// Address space reserved at construction. Cannot be raised.
181    pub fn capacity(&self) -> usize {
182        self.capacity_bytes
183    }
184
185    /// The base address, fixed for this buffer's whole life.
186    ///
187    /// Only the first [`VirtualBuffer::len`] bytes may be read or written.
188    pub fn as_ptr(&self) -> *const u8 {
189        B::base(&self.reservation) as *const u8
190    }
191
192    /// The base address, mutable.
193    pub fn as_mut_ptr(&mut self) -> *mut u8 {
194        B::base(&self.reservation) as *mut u8
195    }
196
197    /// The committed prefix, as bytes.
198    ///
199    /// # Safety
200    ///
201    /// Every byte of `..len` must have been initialised by the caller. Growth
202    /// commits pages but does not promise their contents beyond what the
203    /// platform guarantees for fresh mappings.
204    pub unsafe fn as_slice(&self) -> &[u8] {
205        // SAFETY: `..len` is committed, and the caller states it is initialised.
206        unsafe { std::slice::from_raw_parts(self.as_ptr(), self.len) }
207    }
208
209    /// Grow to `bytes`, committing and leasing whatever pages that needs.
210    ///
211    /// A no-op when the buffer is already at least that long. On failure the
212    /// buffer is exactly as it was: the lease is taken before the mapping, and
213    /// released again if the mapping fails.
214    pub fn grow_to(&mut self, bytes: usize) -> Result<(), VirtualBufferError> {
215        if bytes <= self.len {
216            return Ok(());
217        }
218        if bytes > self.capacity() {
219            return Err(VirtualBufferError::OverCapacity {
220                requested: bytes,
221                capacity: self.capacity(),
222            });
223        }
224
225        let needed = round_up(self.backing.granularity(), bytes);
226        if needed > self.committed {
227            let extra = needed - self.committed;
228            let backing_accounts = matches!(
229                self.physical_memory_accounting,
230                PhysicalMemoryAccounting::Backing { .. }
231            );
232            if !backing_accounts {
233                // Lease before mapping. A refusal must not commit memory, or
234                // the budget is decorative.
235                match self.lease.as_mut() {
236                    Some(lease) => lease.grow(extra as u64)?,
237                    None => {
238                        self.lease = Some(self.governor.reserve(
239                            self.tier,
240                            extra as u64,
241                            self.role,
242                            self.holder,
243                        )?);
244                    }
245                }
246            }
247            if let Err(error) = self
248                .backing
249                .commit(&mut self.reservation, self.committed, extra)
250            {
251                // Give the pages back rather than leaving the governor
252                // believing they are held.
253                if !backing_accounts {
254                    self.release(extra);
255                }
256                return Err(error.into());
257            }
258            self.committed = needed;
259        }
260        self.len = bytes;
261        Ok(())
262    }
263
264    /// Shrink to `bytes`, returning whole granules that fall entirely above it.
265    ///
266    /// A no-op when the buffer is already that short. The granule containing
267    /// `bytes` stays committed, because part of it is still in use — so a small
268    /// shrink can return nothing, which is honest rather than a failure.
269    pub fn shrink_to(&mut self, bytes: usize) -> Result<(), VirtualBufferError> {
270        if bytes >= self.len {
271            return Ok(());
272        }
273        let needed = round_up(self.backing.granularity(), bytes);
274        let mut offset = self.committed;
275        while offset > needed {
276            let granule = self.backing.granularity();
277            offset -= granule;
278            self.backing
279                .release(&mut self.reservation, offset, granule)?;
280            if matches!(
281                self.physical_memory_accounting,
282                PhysicalMemoryAccounting::Buffer
283            ) {
284                self.release(granule);
285            }
286            self.committed = offset;
287        }
288        self.len = bytes;
289        Ok(())
290    }
291
292    /// Return `bytes` of budget, dropping the lease entirely when it empties.
293    fn release(&mut self, bytes: usize) {
294        let Some(lease) = self.lease.as_mut() else {
295            return;
296        };
297        lease.shrink(bytes as u64);
298        if lease.bytes() == 0 {
299            self.lease = None;
300        }
301    }
302}
303
304fn round_up(granule: usize, bytes: usize) -> usize {
305    bytes.div_ceil(granule) * granule
306}
307
308// `MemoryGovernor` is not `Debug` — it is a trait a third party implements, and
309// requiring `Debug` of them to make this struct derivable would be the wrong
310// way round. Report what the buffer itself knows.
311impl<B: VirtualBacking> std::fmt::Debug for VirtualBuffer<B> {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct("VirtualBuffer")
314            .field("base", &self.as_ptr())
315            .field("len", &self.len)
316            .field("committed", &self.committed)
317            .field("capacity", &self.capacity())
318            .field("tier", &self.tier)
319            .field("role", &self.role)
320            .finish()
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::granularity;
328    use onnx_runtime_memory_governor::{DeviceKey, LeaseLedger, LedgerGovernor, MemoryAuthorityId};
329    use std::sync::atomic::{AtomicUsize, Ordering};
330
331    const HOLDER: HolderId = HolderId::new(4);
332
333    fn buffer(capacity: usize, budget: u64) -> (VirtualBuffer, LedgerGovernor) {
334        let governor = LedgerGovernor::new(LeaseLedger::new(0, budget, 0));
335        let buffer = VirtualBuffer::with_capacity(
336            capacity,
337            Arc::new(governor.clone()),
338            Tier::Host,
339            MemoryRole::KvCache,
340            HOLDER,
341        )
342        .expect("address space");
343        (buffer, governor)
344    }
345
346    #[derive(Debug, Clone)]
347    struct AuthorityBacking {
348        authority: MemoryAuthorityId,
349        reserves: Arc<AtomicUsize>,
350        commits: Arc<AtomicUsize>,
351    }
352
353    // SAFETY: this test backing returns an inert address, never exposes slices,
354    // and records mapping calls without touching memory.
355    unsafe impl VirtualBacking for AuthorityBacking {
356        type Reservation = usize;
357
358        fn granularity(&self) -> usize {
359            4096
360        }
361
362        fn physical_memory_accounting(&self) -> PhysicalMemoryAccounting {
363            PhysicalMemoryAccounting::Backing {
364                authority: self.authority,
365            }
366        }
367
368        fn reserve(&self, _len: usize) -> Result<Self::Reservation, VirtualMemoryError> {
369            self.reserves.fetch_add(1, Ordering::Relaxed);
370            Ok(0x1000)
371        }
372
373        fn base(reservation: &Self::Reservation) -> usize {
374            *reservation
375        }
376
377        fn commit(
378            &self,
379            _reservation: &mut Self::Reservation,
380            _offset: usize,
381            _len: usize,
382        ) -> Result<(), VirtualMemoryError> {
383            self.commits.fetch_add(1, Ordering::Relaxed);
384            Ok(())
385        }
386
387        fn release(
388            &self,
389            _reservation: &mut Self::Reservation,
390            _offset: usize,
391            _len: usize,
392        ) -> Result<(), VirtualMemoryError> {
393            Ok(())
394        }
395    }
396
397    #[test]
398    fn backing_accounting_accepts_the_same_authority_without_double_charge() {
399        let governor = LedgerGovernor::new(LeaseLedger::new_for_device(
400            DeviceKey::device(2),
401            8192,
402            0,
403            0,
404        ));
405        let commits = Arc::new(AtomicUsize::new(0));
406        let backing = AuthorityBacking {
407            authority: governor.authority_id(),
408            reserves: Arc::new(AtomicUsize::new(0)),
409            commits: Arc::clone(&commits),
410        };
411        let mut buffer = VirtualBuffer::with_backing(
412            backing,
413            8192,
414            Arc::new(governor.clone()),
415            Tier::Device,
416            MemoryRole::KvCache,
417            HOLDER,
418        )
419        .expect("matching authority");
420
421        buffer.grow_to(4096).expect("backing owns the charge");
422
423        assert_eq!(commits.load(Ordering::Relaxed), 1);
424        assert_eq!(
425            governor.used(Tier::Device),
426            0,
427            "mapped attribution must not add a second physical charge"
428        );
429    }
430
431    #[test]
432    fn backing_accounting_rejects_a_different_authority_before_reservation() {
433        let backing_governor = LedgerGovernor::new(LeaseLedger::new_for_device(
434            DeviceKey::device(2),
435            8192,
436            0,
437            0,
438        ));
439        let buffer_governor = LedgerGovernor::new(LeaseLedger::new_for_device(
440            DeviceKey::device(2),
441            8192,
442            0,
443            0,
444        ));
445        let reserves = Arc::new(AtomicUsize::new(0));
446        let commits = Arc::new(AtomicUsize::new(0));
447        let backing = AuthorityBacking {
448            authority: backing_governor.authority_id(),
449            reserves: Arc::clone(&reserves),
450            commits: Arc::clone(&commits),
451        };
452
453        let error = VirtualBuffer::with_backing(
454            backing,
455            8192,
456            Arc::new(buffer_governor.clone()),
457            Tier::Device,
458            MemoryRole::KvCache,
459            HOLDER,
460        )
461        .expect_err("different accounting authorities must be rejected");
462
463        assert!(matches!(
464            error,
465            VirtualBufferError::AuthorityMismatch { backing, governor }
466                if backing == backing_governor.authority_id()
467                    && governor == buffer_governor.authority_id()
468        ));
469        assert_eq!(reserves.load(Ordering::Relaxed), 0);
470        assert_eq!(commits.load(Ordering::Relaxed), 0);
471        assert_eq!(buffer_governor.used(Tier::Device), 0);
472    }
473
474    /// The property the whole type exists for: growth does not move the buffer.
475    ///
476    /// A reallocating buffer would pass every other test here. This is the one
477    /// that distinguishes them, and it is why a captured device graph survives
478    /// growth.
479    #[test]
480    fn the_address_does_not_change_as_the_buffer_grows() {
481        let (mut buffer, _) = buffer(64 << 20, 128 << 20);
482        let base = buffer.as_ptr();
483        for target in [1usize, 4096, 1 << 20, 8 << 20, 32 << 20] {
484            buffer.grow_to(target).expect("within capacity and budget");
485            assert_eq!(
486                buffer.as_ptr(),
487                base,
488                "growing to {target} moved the buffer, which is the one thing it must not do"
489            );
490            assert_eq!(buffer.len(), target);
491        }
492    }
493
494    /// Reserving address space costs no budget. Charging for it would make a
495    /// governor refuse a buffer that never uses what it reserved.
496    #[test]
497    fn reserving_address_space_leases_nothing() {
498        let (buffer, governor) = buffer(1 << 30, 1 << 20);
499        assert_eq!(
500            governor.available(Tier::Host),
501            1 << 20,
502            "a 1 GiB reservation must not consume a 1 MiB budget"
503        );
504        assert_eq!(buffer.committed(), 0);
505        assert!(buffer.is_empty());
506    }
507
508    /// Committed bytes are leased, and the governor sees them.
509    #[test]
510    fn growth_leases_exactly_what_it_commits() {
511        let (mut buffer, governor) = buffer(16 << 20, 32 << 20);
512        buffer.grow_to(1).expect("granted");
513        assert_eq!(
514            buffer.committed(),
515            granularity(),
516            "growth commits whole granules"
517        );
518        assert_eq!(
519            (32u64 << 20) - governor.available(Tier::Host),
520            buffer.committed() as u64,
521            "the governor must be charged the committed bytes, not the requested ones"
522        );
523
524        let before = governor.available(Tier::Host);
525        buffer.grow_to(2).expect("granted");
526        assert_eq!(
527            governor.available(Tier::Host),
528            before,
529            "growing within an already-committed granule must not lease again"
530        );
531    }
532
533    /// The written bytes survive growth. A buffer that remapped underneath the
534    /// caller would lose them.
535    #[test]
536    fn contents_survive_growth() {
537        let (mut buffer, _) = buffer(8 << 20, 16 << 20);
538        buffer.grow_to(4096).expect("granted");
539        // SAFETY: the first 4096 bytes are committed.
540        unsafe { std::ptr::write_bytes(buffer.as_mut_ptr(), 0xC7, 4096) };
541
542        buffer.grow_to(4 << 20).expect("granted");
543        // SAFETY: still committed, and just written.
544        let head = unsafe { std::slice::from_raw_parts(buffer.as_ptr(), 4096) };
545        assert!(
546            head.iter().all(|&byte| byte == 0xC7),
547            "growth lost the bytes that were already there"
548        );
549    }
550
551    /// Shrinking gives whole granules back to the governor.
552    #[test]
553    fn shrinking_returns_committed_pages() {
554        let (mut buffer, governor) = buffer(16 << 20, 32 << 20);
555        buffer.grow_to(4 << 20).expect("granted");
556        let held = buffer.committed();
557        assert!(held >= 4 << 20);
558
559        buffer.shrink_to(0).expect("shrunk");
560        assert_eq!(buffer.committed(), 0, "everything must come back");
561        assert_eq!(
562            governor.available(Tier::Host),
563            32 << 20,
564            "the governor must see the pages returned"
565        );
566        assert_eq!(buffer.len(), 0);
567    }
568
569    /// A shrink inside one granule returns nothing, because part of that
570    /// granule is still in use. Reporting that honestly beats unmapping memory
571    /// the caller still reads.
572    #[test]
573    fn a_shrink_within_a_granule_keeps_the_page() {
574        let (mut buffer, _) = buffer(16 << 20, 32 << 20);
575        buffer.grow_to(granularity()).expect("granted");
576        let committed = buffer.committed();
577
578        buffer.shrink_to(granularity() - 1).expect("shrunk");
579        assert_eq!(
580            buffer.committed(),
581            committed,
582            "the granule containing the new end must stay mapped"
583        );
584        assert_eq!(buffer.len(), granularity() - 1);
585    }
586
587    /// Growth past the reservation fails and says why, rather than silently
588    /// reallocating and moving the address.
589    #[test]
590    fn growing_past_the_reservation_is_refused_and_names_the_capacity() {
591        let (mut buffer, _) = buffer(1 << 20, 64 << 20);
592        let capacity = buffer.capacity();
593        let error = buffer
594            .grow_to(capacity + 1)
595            .expect_err("the reservation cannot be extended");
596        let message = error.to_string();
597        assert!(
598            message.contains("larger capacity"),
599            "the error must say what to do, got: {message}"
600        );
601        assert_eq!(buffer.len(), 0, "a refused growth must change nothing");
602    }
603
604    /// A refused lease commits nothing, and leaves the buffer usable.
605    #[test]
606    fn a_refused_lease_leaves_the_buffer_untouched() {
607        let (mut buffer, governor) = buffer(64 << 20, granularity() as u64);
608        buffer
609            .grow_to(1)
610            .expect("the first granule fits the budget");
611        let committed = buffer.committed();
612
613        let error = buffer.grow_to(32 << 20);
614        assert!(error.is_err(), "a 32 MiB growth cannot fit one granule");
615        assert_eq!(
616            buffer.committed(),
617            committed,
618            "a refused growth must not commit pages"
619        );
620        assert_eq!(governor.available(Tier::Host), 0);
621
622        // Still usable afterwards.
623        // SAFETY: the first granule is committed.
624        unsafe { std::ptr::write_bytes(buffer.as_mut_ptr(), 0x11, committed) };
625    }
626
627    /// Dropping the buffer returns everything, without an explicit release.
628    #[test]
629    fn dropping_the_buffer_returns_its_budget() {
630        let (mut buffer, governor) = buffer(16 << 20, 32 << 20);
631        buffer.grow_to(8 << 20).expect("granted");
632        assert!(governor.available(Tier::Host) < 32 << 20);
633
634        drop(buffer);
635        assert_eq!(
636            governor.available(Tier::Host),
637            32 << 20,
638            "the lease must be released when the buffer goes"
639        );
640    }
641}