1use std::sync::Mutex;
71
72use super::resource::{
73 Access, AllocTag, BlockId, DeviceBlock, DeviceMemoryResource, ResourceBudgetSnapshot,
74 ResourceError, ResourceResult, StreamId,
75};
76
77struct BudgetState {
80 reserved: usize,
81}
82
83pub struct GlobalDeviceBudget {
85 inner: Box<dyn DeviceMemoryResource + Send + Sync>,
86 limit: usize,
87 state: Mutex<BudgetState>,
88}
89
90impl GlobalDeviceBudget {
91 pub fn new(inner: Box<dyn DeviceMemoryResource + Send + Sync>, limit: usize) -> Self {
97 let initial = inner.bytes_outstanding();
98 Self {
99 inner,
100 limit,
101 state: Mutex::new(BudgetState { reserved: initial }),
102 }
103 }
104
105 pub fn limit(&self) -> usize {
107 self.limit
108 }
109
110 pub fn reserved_bytes(&self) -> usize {
114 self.state
115 .lock()
116 .expect("GlobalDeviceBudget poisoned")
117 .reserved
118 }
119
120 pub fn remaining(&self) -> usize {
123 let state = self.state.lock().expect("GlobalDeviceBudget poisoned");
124 self.limit.saturating_sub(state.reserved)
125 }
126}
127
128impl DeviceMemoryResource for GlobalDeviceBudget {
129 fn allocate(
130 &self,
131 bytes: usize,
132 stream: StreamId,
133 tag: AllocTag,
134 ) -> ResourceResult<DeviceBlock> {
135 {
139 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
140 let remaining = self.limit.saturating_sub(state.reserved);
141 if bytes <= remaining {
142 state.reserved = state.reserved.saturating_add(bytes);
143 drop(state);
144 return match self.inner.allocate(bytes, stream, tag) {
145 Ok(block) => Ok(block),
146 Err(e) => {
147 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
148 state.reserved = state.reserved.saturating_sub(bytes);
149 Err(e)
150 }
151 };
152 }
153 if bytes > self.limit {
158 return Err(ResourceError::OutOfBudget {
159 requested: bytes,
160 current: state.reserved,
161 remaining,
162 limit: self.limit,
163 });
164 }
165 }
166
167 let _ = self.reap_pending();
184
185 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
186 let remaining = self.limit.saturating_sub(state.reserved);
187 if bytes > remaining {
188 return Err(ResourceError::OutOfBudget {
189 requested: bytes,
190 current: state.reserved,
191 remaining,
192 limit: self.limit,
193 });
194 }
195 state.reserved = state.reserved.saturating_add(bytes);
196 drop(state);
197
198 match self.inner.allocate(bytes, stream, tag) {
199 Ok(block) => Ok(block),
200 Err(e) => {
201 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
202 state.reserved = state.reserved.saturating_sub(bytes);
203 Err(e)
204 }
205 }
206 }
207
208 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
209 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
210
211 let before = self.inner.bytes_outstanding();
212 let result = self.inner.deallocate(block);
213 let after = self.inner.bytes_outstanding();
214 let freed = before.saturating_sub(after);
215 if freed > 0 {
216 state.reserved = state.reserved.saturating_sub(freed);
217 }
218 result
219 }
220
221 fn device_ordinal(&self) -> u32 {
222 self.inner.device_ordinal()
223 }
224
225 fn bytes_outstanding(&self) -> usize {
226 self.inner.bytes_outstanding()
230 }
231
232 fn budget_snapshot(&self) -> Option<ResourceBudgetSnapshot> {
233 let state = self.state.lock().expect("GlobalDeviceBudget poisoned");
234 Some(ResourceBudgetSnapshot {
235 limit: self.limit,
236 reserved: state.reserved,
237 })
238 }
239
240 fn reap_pending(&self) -> ResourceResult<()> {
241 let mut state = self.state.lock().expect("GlobalDeviceBudget poisoned");
242
243 let before = self.inner.bytes_outstanding();
244 let result = self.inner.reap_pending();
245 let after = self.inner.bytes_outstanding();
246 let freed = before.saturating_sub(after);
247 if freed > 0 {
248 state.reserved = state.reserved.saturating_sub(freed);
249 }
250 result
251 }
252
253 fn record_block_use(&self, block: &DeviceBlock, use_stream: StreamId) -> ResourceResult<()> {
254 self.inner.record_block_use(block, use_stream)
259 }
260
261 fn supports_block_use_tracking(&self) -> bool {
262 self.inner.supports_block_use_tracking()
263 }
264
265 fn prepare_block_use(
266 &self,
267 block: BlockId,
268 use_stream: StreamId,
269 access: Access,
270 ) -> ResourceResult<()> {
271 self.inner.prepare_block_use(block, use_stream, access)
274 }
275
276 fn finish_block_use(
277 &self,
278 block: BlockId,
279 use_stream: StreamId,
280 access: Access,
281 ) -> ResourceResult<()> {
282 self.inner.finish_block_use(block, use_stream, access)
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::super::async_resource::AsyncCudaResource;
290 use super::super::direct::DirectCudaResource;
291 use super::super::resource::{BlockState, Generation};
292 use super::super::stream_pool::StreamPool;
293 use super::*;
294 use std::sync::Arc;
295
296 use crate::CudaDevice;
297
298 fn try_device() -> Option<Arc<CudaDevice>> {
299 match CudaDevice::new(0) {
300 Ok(device) => Some(Arc::new(device)),
301 Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
302 panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {error}")
303 }
304 Err(error) => {
305 eprintln!("Skipping test: CUDA runtime unavailable: {error}");
306 None
307 }
308 }
309 }
310
311 struct AlwaysFailAllocResource {
317 ord: u32,
318 outstanding: std::sync::atomic::AtomicUsize,
319 }
320
321 impl AlwaysFailAllocResource {
322 fn new(ord: u32) -> Self {
323 Self {
324 ord,
325 outstanding: std::sync::atomic::AtomicUsize::new(0),
326 }
327 }
328 }
329
330 impl DeviceMemoryResource for AlwaysFailAllocResource {
331 fn allocate(
332 &self,
333 _bytes: usize,
334 _stream: StreamId,
335 _tag: AllocTag,
336 ) -> ResourceResult<DeviceBlock> {
337 Err(ResourceError::Driver("inner always fails".into()))
338 }
339 fn deallocate(&self, _block: DeviceBlock) -> ResourceResult<()> {
340 Ok(())
341 }
342 fn device_ordinal(&self) -> u32 {
343 self.ord
344 }
345 fn bytes_outstanding(&self) -> usize {
346 self.outstanding.load(std::sync::atomic::Ordering::Relaxed)
347 }
348 }
349
350 #[test]
351 fn allocate_within_limit_succeeds_and_updates_reserved() {
352 let Some(device) = try_device() else {
353 return;
354 };
355 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
356 let budget = GlobalDeviceBudget::new(inner, 64 * 1024);
357
358 let block = budget
359 .allocate(2048, StreamId::DEFAULT, AllocTag("budget-success"))
360 .expect("alloc within limit");
361 assert_eq!(budget.reserved_bytes(), 2048);
362 assert_eq!(budget.remaining(), 64 * 1024 - 2048);
363 assert_eq!(budget.bytes_outstanding(), 2048);
364
365 budget.deallocate(block).expect("dealloc");
366 assert_eq!(budget.reserved_bytes(), 0);
367 assert_eq!(budget.bytes_outstanding(), 0);
368 }
369
370 #[test]
371 fn allocate_at_exact_limit_succeeds_then_next_byte_rejected() {
372 let Some(device) = try_device() else {
373 return;
374 };
375 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
376 let budget = GlobalDeviceBudget::new(inner, 4096);
377
378 let block = budget
379 .allocate(4096, StreamId::DEFAULT, AllocTag::UNTAGGED)
380 .expect("alloc at exact limit");
381 assert_eq!(budget.reserved_bytes(), 4096);
382 assert_eq!(budget.remaining(), 0);
383
384 let err = budget.allocate(1, StreamId::DEFAULT, AllocTag::UNTAGGED);
385 assert!(
386 matches!(
387 err,
388 Err(ResourceError::OutOfBudget {
389 requested: 1,
390 current: 4096,
391 remaining: 0,
392 limit: 4096,
393 })
394 ),
395 "expected OutOfBudget {{1,0}}, got {:?}",
396 err
397 );
398 assert_eq!(budget.reserved_bytes(), 4096);
400
401 budget.deallocate(block).expect("dealloc");
402 assert_eq!(budget.reserved_bytes(), 0);
403 }
404
405 #[test]
406 fn over_limit_alloc_returns_out_of_budget_with_correct_remaining() {
407 let Some(device) = try_device() else {
408 return;
409 };
410 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
411 let budget = GlobalDeviceBudget::new(inner, 1024);
412
413 let block = budget
415 .allocate(768, StreamId::DEFAULT, AllocTag::UNTAGGED)
416 .expect("first alloc");
417 assert_eq!(budget.remaining(), 256);
418
419 let err = budget.allocate(512, StreamId::DEFAULT, AllocTag::UNTAGGED);
420 assert!(
421 matches!(
422 err,
423 Err(ResourceError::OutOfBudget {
424 requested: 512,
425 current: 768,
426 remaining: 256,
427 limit: 1024,
428 })
429 ),
430 "expected OutOfBudget {{512,256}}, got {:?}",
431 err
432 );
433
434 budget.deallocate(block).expect("dealloc");
435 }
436
437 #[test]
438 fn memory_pressure_runtime_budget_reports_exact_limit() {
439 let Some(device) = try_device() else {
440 return;
441 };
442 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
443 let budget = GlobalDeviceBudget::new(inner, 1024);
444 let block = budget
445 .allocate(768, StreamId::DEFAULT, AllocTag::UNTAGGED)
446 .expect("baseline allocation");
447
448 let error = budget
449 .allocate(512, StreamId::DEFAULT, AllocTag::UNTAGGED)
450 .expect_err("cumulative allocation must exceed the runtime budget");
451
452 assert_eq!(
453 format!("{error:?}"),
454 "OutOfBudget { requested: 512, current: 768, remaining: 256, limit: 1024 }"
455 );
456 assert_eq!(budget.reserved_bytes(), 768);
457 budget.deallocate(block).expect("dealloc");
458 }
459
460 #[test]
461 fn failed_inner_allocation_rolls_back_reservation() {
462 let inner = Box::new(AlwaysFailAllocResource::new(0));
464 let budget = GlobalDeviceBudget::new(inner, 1024 * 1024);
465 assert_eq!(budget.reserved_bytes(), 0);
466
467 let err = budget.allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED);
468 assert!(matches!(err, Err(ResourceError::Driver(_))));
469 assert_eq!(budget.reserved_bytes(), 0);
473 assert_eq!(budget.remaining(), 1024 * 1024);
474 }
475
476 #[test]
477 fn deallocate_releases_budget_immediately_for_synchronous_inner() {
478 let Some(device) = try_device() else {
482 return;
483 };
484 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
485 let budget = GlobalDeviceBudget::new(inner, 16 * 1024);
486
487 let block = budget
488 .allocate(8 * 1024, StreamId::DEFAULT, AllocTag::UNTAGGED)
489 .expect("alloc");
490 assert_eq!(budget.reserved_bytes(), 8 * 1024);
491 budget.deallocate(block).expect("dealloc");
492 assert_eq!(
493 budget.reserved_bytes(),
494 0,
495 "synchronous inner releases budget at deallocate"
496 );
497 budget.reap_pending().expect("reap noop");
499 assert_eq!(budget.reserved_bytes(), 0);
500 }
501
502 #[test]
503 fn deallocate_holds_budget_for_async_inner_until_reap_pending() {
504 let Some(device) = try_device() else {
505 return;
506 };
507 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
508 let inner = Box::new(AsyncCudaResource::new(
509 Arc::clone(&device),
510 0,
511 Arc::clone(&pool),
512 ));
513 let budget = GlobalDeviceBudget::new(inner, 32 * 1024);
514
515 let block = budget
516 .allocate(4096, StreamId::DEFAULT, AllocTag("budget-async"))
517 .expect("alloc");
518 assert_eq!(budget.reserved_bytes(), 4096);
519
520 budget.deallocate(block).expect("dealloc");
524 assert_eq!(
525 budget.reserved_bytes(),
526 4096,
527 "async inner: budget must stay reserved until reap_pending drains pending free"
528 );
529 assert_eq!(budget.bytes_outstanding(), 4096);
530
531 budget.reap_pending().expect("reap");
532 assert_eq!(
533 budget.reserved_bytes(),
534 0,
535 "async inner: reap_pending releases the pending bytes"
536 );
537 assert_eq!(budget.bytes_outstanding(), 0);
538 }
539
540 #[test]
541 fn deallocate_unknown_block_does_not_release_budget() {
542 let Some(device) = try_device() else {
543 return;
544 };
545 let inner = Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
546 let budget = GlobalDeviceBudget::new(inner, 16 * 1024);
547
548 let block = budget
549 .allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED)
550 .expect("alloc");
551 assert_eq!(budget.reserved_bytes(), 2048);
552
553 let bogus = DeviceBlock {
556 ptr: 0xfeed_face,
557 device_ordinal: 0,
558 alloc_stream: StreamId::DEFAULT,
559 bytes: 1024,
560 align: 1,
561 tag: AllocTag::UNTAGGED,
562 generation: Generation::next(),
563 state: BlockState::Live,
564 };
565 let res = budget.deallocate(bogus);
566 assert!(matches!(res, Err(ResourceError::UseAfterFree { .. })));
567 assert_eq!(
568 budget.reserved_bytes(),
569 2048,
570 "bogus dealloc must not release budget"
571 );
572
573 budget.deallocate(block).expect("real dealloc");
574 assert_eq!(budget.reserved_bytes(), 0);
575 }
576
577 #[test]
578 fn forwards_device_ordinal() {
579 let inner = Box::new(AlwaysFailAllocResource::new(7));
580 let budget = GlobalDeviceBudget::new(inner, 1024);
581 assert_eq!(budget.device_ordinal(), 7);
582 }
583}