1use std::sync::{Arc, Mutex};
9
10#[cfg(test)]
11use super::resource::AllocTag;
12use super::resource::{
13 Access, AllocationAccounting, AllocationRequest, BlockId, DeviceBlock, DeviceMemoryResource,
14 ResourceBudgetSnapshot, ResourceError, ResourceResult, StreamId,
15};
16
17struct BudgetState {
20 admitted_total: u128,
21 baseline_reclaimed: u128,
22}
23
24impl BudgetState {
25 fn reserved(&self, total_reclaimed: u128) -> ResourceResult<usize> {
26 let reclaimed = total_reclaimed
27 .checked_sub(self.baseline_reclaimed)
28 .ok_or_else(|| {
29 ResourceError::Driver("resource reclamation total precedes budget baseline".into())
30 })?;
31 let reserved = self.admitted_total.checked_sub(reclaimed).ok_or_else(|| {
32 ResourceError::Driver("resource releases exceed budget admissions".into())
33 })?;
34 usize::try_from(reserved)
35 .map_err(|_| ResourceError::Driver("global budget accounting overflow".into()))
36 }
37}
38
39struct BudgetReservation<'a> {
40 state: &'a Mutex<BudgetState>,
41 bytes: usize,
42 reclamation: Arc<crate::memory::AllocationReclamation>,
43}
44
45impl Drop for BudgetReservation<'_> {
46 fn drop(&mut self) {
47 if !self.reclamation.was_acquired() {
48 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
49 state.admitted_total = state
50 .admitted_total
51 .checked_sub(self.bytes as u128)
52 .expect("budget admission includes the refused allocation");
53 }
54 }
55}
56
57pub struct GlobalDeviceBudget {
59 inner: Box<dyn DeviceMemoryResource + Send + Sync>,
60 limit: usize,
61 state: Mutex<BudgetState>,
62 accounting: Arc<AllocationAccounting>,
63}
64
65impl GlobalDeviceBudget {
66 pub fn new(inner: Box<dyn DeviceMemoryResource + Send + Sync>, limit: usize) -> Self {
73 let accounting = inner.allocation_accounting();
74 let (initial, baseline_reclaimed) = accounting.snapshot();
75 Self {
76 inner,
77 limit,
78 state: Mutex::new(BudgetState {
79 admitted_total: initial as u128,
80 baseline_reclaimed,
81 }),
82 accounting,
83 }
84 }
85
86 pub fn limit(&self) -> usize {
88 self.limit
89 }
90
91 pub fn reserved_bytes(&self) -> usize {
95 let state = self.state.lock().expect("GlobalDeviceBudget poisoned");
96 state
97 .reserved(self.accounting.snapshot().1)
98 .expect("canonical budget release accounting is consistent")
99 }
100
101 pub fn remaining(&self) -> usize {
104 self.limit.saturating_sub(self.reserved_bytes())
105 }
106
107 fn allocate_with_pressure(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
108 let bytes = request.bytes;
109 let reservation_pressure_bytes = request.reservation_pressure_bytes;
110 {
113 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
114 let current = state
115 .reserved(self.accounting.snapshot().1)?
116 .checked_add(reservation_pressure_bytes)
117 .ok_or_else(|| {
118 ResourceError::Driver(
119 "global budget reservation accounting overflow".to_string(),
120 )
121 })?;
122 let remaining = self.limit.saturating_sub(current);
123 if bytes <= remaining {
124 state.admitted_total =
125 state
126 .admitted_total
127 .checked_add(bytes as u128)
128 .ok_or_else(|| {
129 ResourceError::Driver("global budget accounting overflow".to_string())
130 })?;
131 drop(state);
132 return self.materialize_reserved(request);
133 }
134 if bytes > self.limit.saturating_sub(reservation_pressure_bytes) {
135 return Err(ResourceError::OutOfBudget {
136 requested: bytes,
137 current,
138 remaining,
139 limit: self.limit,
140 });
141 }
142 }
143
144 let _ = self.reap_pending();
147
148 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
149 let current = state
150 .reserved(self.accounting.snapshot().1)?
151 .checked_add(reservation_pressure_bytes)
152 .ok_or_else(|| {
153 ResourceError::Driver("global budget reservation accounting overflow".to_string())
154 })?;
155 let remaining = self.limit.saturating_sub(current);
156 if bytes > remaining {
157 return Err(ResourceError::OutOfBudget {
158 requested: bytes,
159 current,
160 remaining,
161 limit: self.limit,
162 });
163 }
164 state.admitted_total =
165 state
166 .admitted_total
167 .checked_add(bytes as u128)
168 .ok_or_else(|| {
169 ResourceError::Driver("global budget accounting overflow".to_string())
170 })?;
171 drop(state);
172
173 self.materialize_reserved(request)
174 }
175
176 fn materialize_reserved(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
177 let _reservation = BudgetReservation {
178 state: &self.state,
179 bytes: request.bytes,
180 reclamation: request.reclamation(),
181 };
182 self.inner.materialize(request)
183 }
184}
185
186impl DeviceMemoryResource for GlobalDeviceBudget {
187 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
188 self.allocate_with_pressure(request)
189 }
190
191 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
192 Arc::clone(&self.accounting)
193 }
194
195 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
196 self.inner.deallocate(block)
197 }
198
199 fn device_ordinal(&self) -> u32 {
200 self.inner.device_ordinal()
201 }
202
203 fn bytes_outstanding(&self) -> usize {
204 self.accounting.snapshot().0
205 }
206
207 fn budget_snapshot(&self) -> Option<ResourceBudgetSnapshot> {
208 Some(ResourceBudgetSnapshot {
209 limit: self.limit,
210 reserved: self.reserved_bytes(),
211 })
212 }
213
214 fn reap_pending(&self) -> ResourceResult<()> {
215 self.inner.reap_pending()
216 }
217
218 fn record_block_use(&self, block: &DeviceBlock, use_stream: StreamId) -> ResourceResult<()> {
219 self.inner.record_block_use(block, use_stream)
224 }
225
226 fn supports_block_use_tracking(&self) -> bool {
227 self.inner.supports_block_use_tracking()
228 }
229
230 fn access_dependencies(
231 &self,
232 block: BlockId,
233 bytes: usize,
234 ) -> ResourceResult<Option<std::sync::Arc<super::resource::DeviceAccessDependencies>>> {
235 self.inner.access_dependencies(block, bytes)
236 }
237
238 fn prepare_block_use(
239 &self,
240 block: BlockId,
241 use_stream: StreamId,
242 access: Access,
243 ) -> ResourceResult<()> {
244 self.inner.prepare_block_use(block, use_stream, access)
247 }
248
249 fn finish_block_use(
250 &self,
251 block: BlockId,
252 use_stream: StreamId,
253 access: Access,
254 ) -> ResourceResult<()> {
255 self.inner.finish_block_use(block, use_stream, access)
257 }
258}
259
260#[cfg(test)]
261pub(crate) mod tests {
262 use super::super::async_resource::AsyncCudaResource;
263 use super::super::direct::DirectCudaResource;
264 use super::super::resource::{BlockState, Generation};
265 use super::super::stream_pool::StreamPool;
266 use super::*;
267 use std::sync::Arc;
268
269 use crate::CudaDevice;
270
271 fn try_device() -> Option<Arc<CudaDevice>> {
272 match CudaDevice::new(0) {
273 Ok(device) => Some(Arc::new(device)),
274 Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
275 panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {error}")
276 }
277 Err(error) => {
278 eprintln!("Skipping test: CUDA runtime unavailable: {error}");
279 None
280 }
281 }
282 }
283
284 struct AlwaysFailAllocResource {
289 ord: u32,
290 accounting: Arc<AllocationAccounting>,
291 }
292
293 impl AlwaysFailAllocResource {
294 fn new(ord: u32) -> Self {
295 Self {
296 ord,
297 accounting: Arc::default(),
298 }
299 }
300 }
301
302 impl DeviceMemoryResource for AlwaysFailAllocResource {
303 fn materialize(&self, _request: AllocationRequest) -> ResourceResult<DeviceBlock> {
304 Err(ResourceError::Driver("inner always fails".into()))
305 }
306 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
307 Arc::clone(&self.accounting)
308 }
309 fn deallocate(&self, _block: DeviceBlock) -> ResourceResult<()> {
310 Ok(())
311 }
312 fn device_ordinal(&self) -> u32 {
313 self.ord
314 }
315 fn bytes_outstanding(&self) -> usize {
316 self.accounting.snapshot().0
317 }
318 }
319
320 pub(crate) struct HostAllocation {
321 storage: Option<Box<[u8]>>,
322 reclamation: Arc<crate::memory::AllocationReclamation>,
323 }
324
325 impl Drop for HostAllocation {
326 fn drop(&mut self) {
327 drop(self.storage.take());
328 self.reclamation
329 .complete()
330 .expect("host allocation physically freed");
331 }
332 }
333
334 #[derive(Default)]
335 pub(crate) struct DeferredHostResource {
336 pub(crate) owners: Arc<Mutex<std::collections::HashMap<u64, HostAllocation>>>,
337 pub(crate) accounting: Arc<AllocationAccounting>,
338 pub(crate) release_on_detach: bool,
339 }
340
341 impl DeviceMemoryResource for DeferredHostResource {
342 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
343 let AllocationRequest {
344 bytes,
345 stream,
346 tag,
347 reclamation,
348 ..
349 } = request;
350 reclamation.attach_resource(Arc::clone(&self.accounting), bytes)?;
351 let storage = vec![0_u8; bytes].into_boxed_slice();
352 let ptr = storage.as_ptr() as u64;
353 let owner = HostAllocation {
354 storage: Some(storage),
355 reclamation,
356 };
357 owner.reclamation.acquired()?;
358 self.owners.lock().unwrap().insert(ptr, owner);
359 Ok(DeviceBlock {
360 ptr,
361 device_ordinal: 0,
362 alloc_stream: stream,
363 bytes,
364 align: 1,
365 tag,
366 generation: Generation::next(),
367 state: BlockState::Live,
368 })
369 }
370
371 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
372 assert!(self.owners.lock().unwrap().contains_key(&block.ptr));
373 if self.release_on_detach {
374 let owner = self.owners.lock().unwrap().remove(&block.ptr);
375 drop(owner);
376 }
377 Ok(())
378 }
379
380 fn device_ordinal(&self) -> u32 {
381 0
382 }
383
384 fn bytes_outstanding(&self) -> usize {
385 self.accounting.snapshot().0
386 }
387 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
388 Arc::clone(&self.accounting)
389 }
390 }
391
392 #[test]
393 fn host_release_outside_resource_calls_restores_budget_capacity() {
394 let resource = DeferredHostResource::default();
395 let owners = Arc::clone(&resource.owners);
396 let budget = GlobalDeviceBudget::new(Box::new(resource), 64);
397 let block = budget
398 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
399 .unwrap();
400 let ptr = block.ptr;
401 budget.deallocate(block).unwrap();
402 assert_eq!(budget.reserved_bytes(), 64);
403
404 let owner = owners.lock().unwrap().remove(&ptr).unwrap();
405 drop(owner);
406
407 assert_eq!(budget.bytes_outstanding(), 0);
408 assert_eq!(budget.reserved_bytes(), 0);
409 assert_eq!(budget.remaining(), 64);
410 let replacement = budget
411 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
412 .expect("physical release restores admission capacity");
413 budget.deallocate(replacement).unwrap();
414 }
415
416 #[test]
417 fn host_budget_construction_preserves_live_bytes_and_prior_reclamation() {
418 let resource = DeferredHostResource::default();
419 let owners = Arc::clone(&resource.owners);
420 let old = resource
421 .allocate(16, StreamId::DEFAULT, AllocTag::UNTAGGED)
422 .unwrap();
423 drop(owners.lock().unwrap().remove(&old.ptr));
424 let live = resource
425 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
426 .unwrap();
427 let inner = GlobalDeviceBudget::new(Box::new(resource), 96);
428 let outer = GlobalDeviceBudget::new(Box::new(inner), 96);
429 assert_eq!(outer.reserved_bytes(), 64);
430 assert!(outer
431 .allocate_with_reservation_pressure(1, 32, StreamId::DEFAULT, AllocTag::UNTAGGED)
432 .is_err());
433 assert_eq!(outer.reserved_bytes(), 64);
434 drop(owners.lock().unwrap().remove(&live.ptr));
435 assert_eq!(outer.reserved_bytes(), 0);
436 let next = outer
437 .allocate(96, StreamId::DEFAULT, AllocTag::UNTAGGED)
438 .unwrap();
439 assert_eq!(outer.reserved_bytes(), 96);
440 let owner = owners.lock().unwrap().remove(&next.ptr).unwrap();
441 std::thread::spawn(move || drop(owner)).join().unwrap();
442 assert_eq!(outer.remaining(), 96);
443 }
444
445 #[test]
446 fn host_budget_new_admission_does_not_mask_concurrent_physical_release() {
447 struct PausingResource {
448 inner: DeferredHostResource,
449 entered: Arc<std::sync::Barrier>,
450 proceed: Arc<std::sync::Barrier>,
451 }
452 impl DeviceMemoryResource for PausingResource {
453 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
454 self.entered.wait();
455 self.proceed.wait();
456 self.inner.materialize(request)
457 }
458 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
459 self.inner.allocation_accounting()
460 }
461 fn bytes_outstanding(&self) -> usize {
462 self.inner.bytes_outstanding()
463 }
464 fn device_ordinal(&self) -> u32 {
465 0
466 }
467 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
468 self.inner.deallocate(block)
469 }
470 }
471 let inner = DeferredHostResource::default();
472 let old = inner
473 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
474 .unwrap();
475 let owners = Arc::clone(&inner.owners);
476 let entered = Arc::new(std::sync::Barrier::new(2));
477 let proceed = Arc::new(std::sync::Barrier::new(2));
478 let budget = Arc::new(GlobalDeviceBudget::new(
479 Box::new(PausingResource {
480 inner,
481 entered: Arc::clone(&entered),
482 proceed: Arc::clone(&proceed),
483 }),
484 128,
485 ));
486 let worker = Arc::clone(&budget);
487 let admission = std::thread::spawn(move || {
488 worker
489 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
490 .unwrap()
491 });
492 entered.wait();
493 assert_eq!(budget.reserved_bytes(), 128);
494 drop(owners.lock().unwrap().remove(&old.ptr));
495 assert_eq!(budget.reserved_bytes(), 64);
496 proceed.wait();
497 let new = admission.join().unwrap();
498 assert_eq!(budget.bytes_outstanding(), 64);
499 assert_eq!(budget.reserved_bytes(), 64);
500 drop(owners.lock().unwrap().remove(&new.ptr));
501 assert_eq!(budget.remaining(), 128);
502 }
503
504 #[test]
505 fn allocate_within_limit_succeeds_and_updates_reserved() {
506 let Some(device) = try_device() else {
507 return;
508 };
509 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
510 let budget = GlobalDeviceBudget::new(inner, 64 * 1024);
511
512 let block = budget
513 .allocate(2048, StreamId::DEFAULT, AllocTag("budget-success"))
514 .expect("alloc within limit");
515 assert_eq!(budget.reserved_bytes(), 2048);
516 assert_eq!(budget.remaining(), 64 * 1024 - 2048);
517 assert_eq!(budget.bytes_outstanding(), 2048);
518
519 budget.deallocate(block).expect("dealloc");
520 assert_eq!(budget.reserved_bytes(), 0);
521 assert_eq!(budget.bytes_outstanding(), 0);
522 }
523
524 #[test]
525 fn failed_initialization_refunds_only_after_physical_release() {
526 struct AllocatedThenFailed {
527 inner: DeferredHostResource,
528 unwind: bool,
529 release_in_frame: bool,
530 }
531 impl DeviceMemoryResource for AllocatedThenFailed {
532 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
533 let reclamation = request.reclamation();
534 let block = self.inner.materialize(request)?;
535 if self.release_in_frame {
536 let owner = self.inner.owners.lock().unwrap().remove(&block.ptr);
537 drop(owner);
538 }
539 if self.unwind {
540 panic!("initialization interrupted after acquisition");
541 }
542 Err(ResourceError::Driver("ready event failed".into())
543 .retaining(block.bytes, reclamation))
544 }
545 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
546 self.inner.allocation_accounting()
547 }
548 fn deallocate(&self, _: DeviceBlock) -> ResourceResult<()> {
549 unreachable!()
550 }
551 fn device_ordinal(&self) -> u32 {
552 0
553 }
554 fn bytes_outstanding(&self) -> usize {
555 self.inner.bytes_outstanding()
556 }
557 }
558 for unwind in [false, true] {
559 for release_in_frame in [false, true] {
560 let inner = DeferredHostResource::default();
561 let owners = Arc::clone(&inner.owners);
562 let budget = GlobalDeviceBudget::new(
563 Box::new(AllocatedThenFailed {
564 inner,
565 unwind,
566 release_in_frame,
567 }),
568 64,
569 );
570 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
571 assert!(budget
572 .allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED)
573 .is_err());
574 }));
575 assert_eq!(outcome.is_err(), unwind);
576 assert_eq!(
577 budget.reserved_bytes(),
578 if release_in_frame { 0 } else { 64 }
579 );
580 let pending = std::mem::take(&mut *owners.lock().unwrap());
581 std::thread::spawn(move || drop(pending)).join().unwrap();
582 assert_eq!(budget.reserved_bytes(), 0);
583 budget.reap_pending().unwrap();
584 assert_eq!(budget.remaining(), 64);
585 }
586 }
587 }
588
589 #[test]
590 fn allocation_unwind_before_acquisition_restores_budget_charge() {
591 struct UnwindingResource(Arc<AllocationAccounting>);
592 impl DeviceMemoryResource for UnwindingResource {
593 fn materialize(&self, _: AllocationRequest) -> ResourceResult<DeviceBlock> {
594 panic!("allocator refused before acquisition")
595 }
596 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
597 Arc::clone(&self.0)
598 }
599 fn deallocate(&self, _: DeviceBlock) -> ResourceResult<()> {
600 unreachable!()
601 }
602 fn device_ordinal(&self) -> u32 {
603 0
604 }
605 fn bytes_outstanding(&self) -> usize {
606 0
607 }
608 }
609 let budget = GlobalDeviceBudget::new(Box::new(UnwindingResource(Arc::default())), 64);
610 assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
611 let _ = budget.allocate(64, StreamId::DEFAULT, AllocTag::UNTAGGED);
612 }))
613 .is_err());
614 assert_eq!(budget.reserved_bytes(), 0);
615 assert_eq!(budget.remaining(), 64);
616 }
617
618 #[test]
619 fn allocate_at_exact_limit_succeeds_then_next_byte_rejected() {
620 let Some(device) = try_device() else {
621 return;
622 };
623 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
624 let budget = GlobalDeviceBudget::new(inner, 4096);
625
626 let block = budget
627 .allocate(4096, StreamId::DEFAULT, AllocTag::UNTAGGED)
628 .expect("alloc at exact limit");
629 assert_eq!(budget.reserved_bytes(), 4096);
630 assert_eq!(budget.remaining(), 0);
631
632 let err = budget.allocate(1, StreamId::DEFAULT, AllocTag::UNTAGGED);
633 assert!(
634 matches!(
635 err,
636 Err(ResourceError::OutOfBudget {
637 requested: 1,
638 current: 4096,
639 remaining: 0,
640 limit: 4096,
641 })
642 ),
643 "expected OutOfBudget {{1,0}}, got {:?}",
644 err
645 );
646 assert_eq!(budget.reserved_bytes(), 4096);
648
649 budget.deallocate(block).expect("dealloc");
650 assert_eq!(budget.reserved_bytes(), 0);
651 }
652
653 #[test]
654 fn over_limit_alloc_returns_out_of_budget_with_correct_remaining() {
655 let Some(device) = try_device() else {
656 return;
657 };
658 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
659 let budget = GlobalDeviceBudget::new(inner, 1024);
660
661 let block = budget
663 .allocate(768, StreamId::DEFAULT, AllocTag::UNTAGGED)
664 .expect("first alloc");
665 assert_eq!(budget.remaining(), 256);
666
667 let err = budget.allocate(512, StreamId::DEFAULT, AllocTag::UNTAGGED);
668 assert!(
669 matches!(
670 err,
671 Err(ResourceError::OutOfBudget {
672 requested: 512,
673 current: 768,
674 remaining: 256,
675 limit: 1024,
676 })
677 ),
678 "expected OutOfBudget {{512,256}}, got {:?}",
679 err
680 );
681
682 budget.deallocate(block).expect("dealloc");
683 }
684
685 #[test]
686 fn memory_pressure_runtime_budget_reports_exact_limit() {
687 let Some(device) = try_device() else {
688 return;
689 };
690 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
691 let budget = GlobalDeviceBudget::new(inner, 1024);
692 let block = budget
693 .allocate(768, StreamId::DEFAULT, AllocTag::UNTAGGED)
694 .expect("baseline allocation");
695
696 let error = budget
697 .allocate(512, StreamId::DEFAULT, AllocTag::UNTAGGED)
698 .expect_err("cumulative allocation must exceed the runtime budget");
699
700 assert_eq!(
701 format!("{error:?}"),
702 "OutOfBudget { requested: 512, current: 768, remaining: 256, limit: 1024 }"
703 );
704 assert_eq!(budget.reserved_bytes(), 768);
705 budget.deallocate(block).expect("dealloc");
706 }
707
708 #[test]
709 fn failed_inner_allocation_rolls_back_reservation() {
710 let inner = Box::new(AlwaysFailAllocResource::new(0));
712 let budget = GlobalDeviceBudget::new(inner, 1024 * 1024);
713 assert_eq!(budget.reserved_bytes(), 0);
714
715 let err = budget.allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED);
716 assert!(matches!(err, Err(ResourceError::Driver(_))));
717 assert_eq!(budget.reserved_bytes(), 0);
721 assert_eq!(budget.remaining(), 1024 * 1024);
722 }
723
724 #[test]
725 fn reservation_pressure_is_included_before_inner_allocation() {
726 let inner = Box::new(AlwaysFailAllocResource::new(0));
727 let budget = GlobalDeviceBudget::new(inner, 1024);
728
729 let error = budget
730 .allocate_with_reservation_pressure(600, 500, StreamId::DEFAULT, AllocTag::UNTAGGED)
731 .expect_err("runtime promises must reduce ordinary allocation headroom");
732 assert!(matches!(
733 error,
734 ResourceError::OutOfBudget {
735 requested: 600,
736 current: 500,
737 remaining: 524,
738 limit: 1024,
739 }
740 ));
741 assert_eq!(budget.reserved_bytes(), 0);
742 }
743
744 #[test]
745 fn deallocate_releases_budget_immediately_for_synchronous_inner() {
746 let Some(device) = try_device() else {
750 return;
751 };
752 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
753 let budget = GlobalDeviceBudget::new(inner, 16 * 1024);
754
755 let block = budget
756 .allocate(8 * 1024, StreamId::DEFAULT, AllocTag::UNTAGGED)
757 .expect("alloc");
758 assert_eq!(budget.reserved_bytes(), 8 * 1024);
759 budget.deallocate(block).expect("dealloc");
760 assert_eq!(
761 budget.reserved_bytes(),
762 0,
763 "synchronous inner releases budget at deallocate"
764 );
765 budget.reap_pending().expect("reap noop");
767 assert_eq!(budget.reserved_bytes(), 0);
768 }
769
770 #[test]
771 fn deallocate_holds_budget_for_async_inner_until_reap_pending() {
772 let Some(device) = try_device() else {
773 return;
774 };
775 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
776 let inner = Box::new(AsyncCudaResource::new(
777 Arc::clone(&device),
778 0,
779 Arc::clone(&pool),
780 ));
781 let budget = GlobalDeviceBudget::new(inner, 32 * 1024);
782
783 let block = budget
784 .allocate(4096, StreamId::DEFAULT, AllocTag("budget-async"))
785 .expect("alloc");
786 assert_eq!(budget.reserved_bytes(), 4096);
787
788 budget.deallocate(block).expect("dealloc");
792 assert_eq!(
793 budget.reserved_bytes(),
794 4096,
795 "async inner: budget must stay reserved until reap_pending drains pending free"
796 );
797 assert_eq!(budget.bytes_outstanding(), 4096);
798
799 budget.reap_pending().expect("reap");
800 assert_eq!(
801 budget.reserved_bytes(),
802 0,
803 "async inner: reap_pending releases the pending bytes"
804 );
805 assert_eq!(budget.bytes_outstanding(), 0);
806 }
807
808 #[test]
809 fn deallocate_unknown_block_does_not_release_budget() {
810 let Some(device) = try_device() else {
811 return;
812 };
813 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
814 let budget = GlobalDeviceBudget::new(inner, 16 * 1024);
815
816 let block = budget
817 .allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED)
818 .expect("alloc");
819 assert_eq!(budget.reserved_bytes(), 2048);
820
821 let bogus = DeviceBlock {
824 ptr: 0xfeed_face,
825 device_ordinal: 0,
826 alloc_stream: StreamId::DEFAULT,
827 bytes: 1024,
828 align: 1,
829 tag: AllocTag::UNTAGGED,
830 generation: Generation::next(),
831 state: BlockState::Live,
832 };
833 let res = budget.deallocate(bogus);
834 assert!(matches!(res, Err(ResourceError::UseAfterFree { .. })));
835 assert_eq!(
836 budget.reserved_bytes(),
837 2048,
838 "bogus dealloc must not release budget"
839 );
840
841 budget.deallocate(block).expect("real dealloc");
842 assert_eq!(budget.reserved_bytes(), 0);
843 }
844
845 #[test]
846 fn forwards_device_ordinal() {
847 let inner = Box::new(AlwaysFailAllocResource::new(7));
848 let budget = GlobalDeviceBudget::new(inner, 1024);
849 assert_eq!(budget.device_ordinal(), 7);
850 }
851}