1use 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
43pub struct VirtualBuffer<B: VirtualBacking = HostBacking> {
50 backing: B,
51 reservation: B::Reservation,
52 capacity_bytes: usize,
55 len: usize,
58 committed: usize,
61 governor: Arc<dyn MemoryGovernor + Send + Sync>,
62 tier: Tier,
63 role: MemoryRole,
64 holder: HolderId,
65 physical_memory_accounting: PhysicalMemoryAccounting,
66 lease: Option<MemoryLease>,
69}
70
71#[derive(Debug, thiserror::Error)]
73pub enum VirtualBufferError {
74 #[error(transparent)]
76 Memory(#[from] VirtualMemoryError),
77 #[error(transparent)]
79 Budget(#[from] MemoryError),
80 #[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 backing: onnx_runtime_memory_governor::MemoryAuthorityId,
88 governor: onnx_runtime_memory_governor::MemoryAuthorityId,
90 },
91 #[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 requested: usize,
100 capacity: usize,
102 },
103}
104
105impl VirtualBuffer<HostBacking> {
106 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 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 pub fn len(&self) -> usize {
164 self.len
165 }
166
167 pub fn is_empty(&self) -> bool {
169 self.len == 0
170 }
171
172 pub fn committed(&self) -> usize {
177 self.committed
178 }
179
180 pub fn capacity(&self) -> usize {
182 self.capacity_bytes
183 }
184
185 pub fn as_ptr(&self) -> *const u8 {
189 B::base(&self.reservation) as *const u8
190 }
191
192 pub fn as_mut_ptr(&mut self) -> *mut u8 {
194 B::base(&self.reservation) as *mut u8
195 }
196
197 pub unsafe fn as_slice(&self) -> &[u8] {
205 unsafe { std::slice::from_raw_parts(self.as_ptr(), self.len) }
207 }
208
209 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 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 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 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 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
308impl<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 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 #[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 #[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 #[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 #[test]
536 fn contents_survive_growth() {
537 let (mut buffer, _) = buffer(8 << 20, 16 << 20);
538 buffer.grow_to(4096).expect("granted");
539 unsafe { std::ptr::write_bytes(buffer.as_mut_ptr(), 0xC7, 4096) };
541
542 buffer.grow_to(4 << 20).expect("granted");
543 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 #[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 #[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 #[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 #[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 unsafe { std::ptr::write_bytes(buffer.as_mut_ptr(), 0x11, committed) };
625 }
626
627 #[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}