1use std::mem::ManuallyDrop;
7use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
8use std::sync::Arc;
9
10use cudarc::driver::{CudaStream, DevicePtr, DevicePtrMut, DeviceRepr, DeviceSlice, SyncOnDrop};
11use xlog_core::{resolve_bool, MemoryBudget, Result, Schema, XlogError};
12
13use crate::arrow_device::ArrowDeviceImport;
14use crate::cuda_compat::{AsKernelParam, DeviceParamStorage, IntoKernelParamStorage};
15use crate::device_runtime::resource::{
16 block_use_registry, Access, AllocationAccounting, AllocationRequest, BlockUseRegistry,
17 DeviceAccessDependencies, MemoryStorageOwner, MemoryUse, MemoryUseGroup, RetainedStorageUse,
18};
19use crate::device_runtime::{
20 AllocTag, BlockId, BlockState, DeviceBlock, ResourceError, RuntimeMemoryReservation, StreamId,
21 XlogDeviceRuntime,
22};
23use crate::dlpack::DlpackManagedTensor;
24use crate::launch::RecorderTransaction;
25use crate::CudaDevice;
26
27#[cfg(test)]
28type AfterLocalReservationHook = std::sync::Mutex<Option<Arc<dyn Fn(u64) + Send + Sync + 'static>>>;
29
30pub struct GpuMemoryManager {
34 device: Arc<CudaDevice>,
36 budget: MemoryBudget,
38 accounting: Arc<GpuMemoryAccounting>,
40 runtime: Option<Arc<XlogDeviceRuntime>>,
45 #[cfg(test)]
48 after_local_reservation_hook: AfterLocalReservationHook,
49}
50
51#[derive(Default)]
52struct GpuMemoryAccounting {
53 mutation_lock: std::sync::Mutex<()>,
56 budget_reserved: AtomicU64,
61 allocated: AtomicU64,
65 peak: AtomicU64,
69 alloc_count: AtomicU64,
74 deallocation_failure_count: AtomicU64,
77 deallocation_failure_bytes: AtomicU64,
78}
79
80impl GpuMemoryAccounting {
81 fn release_reserved(&self, bytes: u64) -> Result<()> {
82 let _mutation = self.mutation_lock.lock().map_err(|_| {
83 XlogError::Kernel("GPU memory accounting poisoned during reservation release".into())
84 })?;
85 let previous = self.budget_reserved.load(Ordering::SeqCst);
86 let next = previous.checked_sub(bytes).ok_or_else(|| {
87 XlogError::Kernel(format!(
88 "GPU memory reservation release underflow: current_bytes={} requested_bytes={}",
89 previous, bytes,
90 ))
91 })?;
92 self.budget_reserved.store(next, Ordering::SeqCst);
93 Ok(())
94 }
95
96 fn release_owned_allocation(&self, bytes: u64) -> Result<()> {
97 let _mutation = self.mutation_lock.lock().map_err(|_| {
98 XlogError::Kernel("GPU memory accounting poisoned during release".into())
99 })?;
100 let admitted = self.allocated.load(Ordering::SeqCst);
101 let reserved = self.budget_reserved.load(Ordering::SeqCst);
102 let next_admitted = admitted.checked_sub(bytes).ok_or_else(|| {
103 XlogError::Kernel(format!(
104 "GPU admitted allocation release underflow: current_bytes={} requested_bytes={}",
105 admitted, bytes
106 ))
107 })?;
108 let next_reserved = reserved.checked_sub(bytes).ok_or_else(|| {
109 XlogError::Kernel(format!(
110 "GPU local reservation release underflow: current_bytes={} requested_bytes={}",
111 reserved, bytes
112 ))
113 })?;
114 self.allocated.store(next_admitted, Ordering::SeqCst);
115 self.budget_reserved.store(next_reserved, Ordering::SeqCst);
116 Ok(())
117 }
118}
119
120#[derive(Default)]
128pub struct AllocationReclamation {
129 state: std::sync::Mutex<AllocationReclamationState>,
130 acquired: std::sync::atomic::AtomicBool,
131 released: Arc<std::sync::atomic::AtomicBool>,
132}
133
134#[derive(Default)]
135struct AllocationReclamationState {
136 charge: Option<LocalAllocationCharge>,
137 local_published: bool,
138 resource: Option<(Arc<AllocationAccounting>, usize)>,
139 resource_published: bool,
140 pending: Option<(Arc<std::sync::atomic::AtomicUsize>, usize)>,
141}
142
143struct LocalAllocationCharge {
144 accounting: Arc<GpuMemoryAccounting>,
145 bytes: u64,
146}
147
148impl AllocationReclamation {
149 fn attach_local(
151 &self,
152 accounting: Arc<GpuMemoryAccounting>,
153 bytes: u64,
154 ) -> crate::device_runtime::ResourceResult<()> {
155 let charge = LocalAllocationCharge { accounting, bytes };
156 let mut state = self.state.lock().map_err(|_| {
157 ResourceError::Driver(
158 "allocation reclamation state poisoned during charge transfer".into(),
159 )
160 })?;
161 if self.was_acquired() || self.was_released() || state.charge.is_some() {
162 return Err(ResourceError::Driver(
163 "allocation charge transfer requires an unreleased, unbound owner".into(),
164 ));
165 }
166 state.charge = Some(charge);
167 Ok(())
168 }
169
170 pub fn attach_resource(
172 &self,
173 accounting: Arc<AllocationAccounting>,
174 bytes: usize,
175 ) -> crate::device_runtime::ResourceResult<()> {
176 let mut state = self.state.lock().map_err(|_| {
177 ResourceError::Driver("allocation accounting poisoned during binding".into())
178 })?;
179 if self.was_acquired() || self.was_released() || state.resource.is_some() {
180 return Err(ResourceError::Driver(
181 "resource accounting requires an unacquired, unbound allocation".into(),
182 ));
183 }
184 state.resource = Some((accounting, bytes));
185 Ok(())
186 }
187
188 pub fn acquired(&self) -> crate::device_runtime::ResourceResult<()> {
192 self.acquired.store(true, Ordering::Release);
193 let mut state = self.state.lock().map_err(|_| {
194 ResourceError::Driver("allocation accounting poisoned during acquisition".into())
195 })?;
196 if self.was_released() {
197 return Err(ResourceError::Driver(
198 "cannot acquire released storage".into(),
199 ));
200 }
201 if !state.resource_published {
202 if let Some((accounting, bytes)) = &state.resource {
203 accounting.acquire(*bytes)?;
204 state.resource_published = true;
205 }
206 }
207 if !state.local_published {
208 if let Some(charge) = &state.charge {
209 let _mutation = charge.accounting.mutation_lock.lock().map_err(|_| {
210 ResourceError::Driver("local allocation accounting poisoned".into())
211 })?;
212 let previous = charge.accounting.allocated.load(Ordering::SeqCst);
213 let admitted = previous.checked_add(charge.bytes).ok_or_else(|| {
214 ResourceError::Driver("local admitted allocation accounting overflow".into())
215 })?;
216 charge
217 .accounting
218 .allocated
219 .store(admitted, Ordering::SeqCst);
220 charge.accounting.peak.fetch_max(admitted, Ordering::SeqCst);
221 }
222 state.local_published = true;
223 }
224 Ok(())
225 }
226
227 pub fn was_acquired(&self) -> bool {
229 self.acquired.load(Ordering::Acquire)
230 }
231
232 pub(crate) fn attach_pending(
235 &self,
236 accounting: Arc<std::sync::atomic::AtomicUsize>,
237 bytes: usize,
238 ) -> crate::device_runtime::ResourceResult<()> {
239 let mut state = self.state.lock().map_err(|_| {
240 ResourceError::Driver("allocation accounting poisoned during detach".into())
241 })?;
242 if !self.was_acquired() || self.was_released() || state.pending.is_some() {
243 return Err(ResourceError::Driver(
244 "pending charge requires acquired live storage".into(),
245 ));
246 }
247 accounting
248 .try_update(Ordering::SeqCst, Ordering::SeqCst, |value| {
249 value.checked_add(bytes)
250 })
251 .map_err(|_| ResourceError::Driver("pending allocation accounting overflow".into()))?;
252 state.pending = Some((accounting, bytes));
253 Ok(())
254 }
255
256 pub unsafe fn confirm_physical_release(&self) -> crate::device_runtime::ResourceResult<()> {
264 self.complete()
265 }
266
267 pub(crate) fn complete(&self) -> crate::device_runtime::ResourceResult<()> {
270 if self.was_acquired() && !self.was_released() {
271 if let Err(error) = self.acquired() {
274 if self.was_released() {
277 return Ok(());
278 }
279 return Err(error);
280 }
281 }
282 let (charge, resource, pending) = {
283 let mut state = self.state.lock().map_err(|_| {
284 ResourceError::Driver("allocation reclamation state poisoned during release".into())
285 })?;
286 if self.was_released() {
287 return Ok(());
288 }
289 if let Some(charge) = &state.charge {
290 if !state.local_published {
291 return Err(ResourceError::Driver(
292 "local allocation acquisition was not published".into(),
293 ));
294 }
295 charge
296 .accounting
297 .release_owned_allocation(charge.bytes)
298 .map_err(|error| ResourceError::Driver(error.to_string()))?;
299 }
300 let charge = state.charge.take();
303 if let Some((accounting, bytes)) = &state.resource {
304 if !state.resource_published {
305 return Err(ResourceError::Driver(
306 "resource allocation acquisition was not published".into(),
307 ));
308 }
309 accounting.release(*bytes)?;
310 }
311 let resource = state.resource.take();
312 if let Some((accounting, bytes)) = &state.pending {
313 accounting
314 .try_update(Ordering::SeqCst, Ordering::SeqCst, |value| {
315 value.checked_sub(*bytes)
316 })
317 .map_err(|_| {
318 ResourceError::Driver("pending allocation accounting underflow".into())
319 })?;
320 }
321 self.released.store(true, Ordering::Release);
322 (charge, resource, state.pending.take())
323 };
324 drop((charge, resource, pending));
325 Ok(())
326 }
327
328 pub(crate) fn was_released(&self) -> bool {
329 self.released.load(Ordering::Acquire)
330 }
331
332 pub(crate) fn release_proof(&self) -> Arc<std::sync::atomic::AtomicBool> {
333 Arc::clone(&self.released)
334 }
335}
336
337struct LocalAllocationAttempt<'a> {
341 accounting: Arc<GpuMemoryAccounting>,
342 bytes: u64,
343 remaining: Option<&'a mut u64>,
344 reclamation: Arc<AllocationReclamation>,
345}
346
347impl<'a> LocalAllocationAttempt<'a> {
348 fn new(
349 accounting: Arc<GpuMemoryAccounting>,
350 bytes: u64,
351 remaining: Option<&'a mut u64>,
352 ) -> Self {
353 let reclamation = Arc::new(AllocationReclamation::default());
354 let attempt = Self {
355 accounting,
356 bytes,
357 remaining,
358 reclamation,
359 };
360 attempt
361 .reclamation
362 .attach_local(Arc::clone(&attempt.accounting), bytes)
363 .expect("new allocation attempt has no bound charge");
364 attempt
365 }
366}
367
368impl Drop for LocalAllocationAttempt<'_> {
369 fn drop(&mut self) {
370 if self.reclamation.was_acquired() {
371 return;
372 }
373 if let Some(remaining) = self.remaining.as_mut() {
374 **remaining = remaining
375 .checked_add(self.bytes)
376 .expect("unused allocation reservation remains representable");
377 } else if let Err(error) = self.accounting.release_reserved(self.bytes) {
378 eprintln!("local allocation reservation rollback failed: {error}");
379 }
380 }
381}
382
383pub(crate) struct ResourceBlockRetirement {
387 payload: Option<ResourceBlockRetirementPayload>,
388}
389
390struct ResourceBlockRetirementPayload {
391 block: DeviceBlock,
392 resource: Arc<dyn crate::device_runtime::DeviceMemoryResource + Send + Sync>,
393 reclamation: Arc<AllocationReclamation>,
394 detached: bool,
395}
396
397impl ResourceBlockRetirement {
398 pub(crate) fn new(
399 block: DeviceBlock,
400 resource: Arc<dyn crate::device_runtime::DeviceMemoryResource + Send + Sync>,
401 reclamation: Arc<AllocationReclamation>,
402 ) -> Self {
403 Self {
404 payload: Some(ResourceBlockRetirementPayload {
405 block,
406 resource,
407 reclamation,
408 detached: false,
409 }),
410 }
411 }
412
413 pub(crate) fn block(&self) -> &DeviceBlock {
414 &self
415 .payload
416 .as_ref()
417 .expect("unpublished block present")
418 .block
419 }
420
421 pub(crate) fn into_block(mut self) -> DeviceBlock {
422 self.payload.take().expect("published block present").block
423 }
424
425 fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
426 reclaim_allocation(&mut self.payload, |owner| {
427 if owner.reclamation.was_released() {
428 return Ok(());
429 }
430 let mut detach_error = None;
431 let mut detach_panic = None;
432 if !owner.detached {
433 let block = &owner.block;
434 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
437 owner.resource.deallocate(DeviceBlock {
438 ptr: block.ptr,
439 device_ordinal: block.device_ordinal,
440 alloc_stream: block.alloc_stream,
441 bytes: block.bytes,
442 align: block.align,
443 tag: block.tag,
444 generation: block.generation,
445 state: block.state,
446 })
447 }));
448 match result {
449 Ok(Ok(())) | Ok(Err(ResourceError::UseAfterFree { .. })) => {
450 owner.detached = true
451 }
452 Ok(Err(error)) => detach_error = Some(error),
453 Err(panic) => detach_panic = Some(panic),
454 }
455 }
456 let reaped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
460 owner.resource.reap_pending()
461 }));
462 if let Some(panic) = detach_panic {
463 drop(reaped);
466 std::panic::resume_unwind(panic);
467 }
468 let reaped = match reaped {
469 Ok(result) => result,
470 Err(panic) => std::panic::resume_unwind(panic),
471 };
472 allocation_reclamation_outcome(
473 owner.reclamation.was_released(),
474 detach_error.map_or(reaped, Err),
475 )
476 })
477 }
478}
479
480impl Drop for ResourceBlockRetirement {
481 fn drop(&mut self) {
482 if let Some(payload) = self.payload.take() {
483 let mut owner = Self {
484 payload: Some(payload),
485 };
486 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
487 crate::cuda_graph::retry_retirement_after_stream_captures(move || {
488 owner.release().is_ok()
489 });
490 }));
491 }
492 }
493}
494
495#[must_use = "dropping the reservation immediately releases its unused budget"]
501pub struct GpuMemoryReservation {
502 manager: Arc<GpuMemoryManager>,
503 runtime_reservation: Option<RuntimeMemoryReservation>,
504 total_bytes: u64,
505 remaining_bytes: u64,
506}
507
508impl std::fmt::Debug for GpuMemoryReservation {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.debug_struct("GpuMemoryReservation")
511 .field("total_bytes", &self.total_bytes)
512 .field("remaining_bytes", &self.remaining_bytes)
513 .finish()
514 }
515}
516
517impl GpuMemoryReservation {
518 pub fn memory_manager_ptr_value(&self) -> usize {
520 Arc::as_ptr(&self.manager) as usize
521 }
522
523 pub fn total_bytes(&self) -> u64 {
525 self.total_bytes
526 }
527
528 pub fn remaining_bytes(&self) -> u64 {
530 self.remaining_bytes
531 }
532
533 pub fn used_bytes(&self) -> u64 {
535 self.total_bytes - self.remaining_bytes
536 }
537
538 pub fn alloc<T: cudarc::driver::DeviceRepr>(
540 &mut self,
541 len: usize,
542 ) -> Result<TrackedCudaSlice<T>> {
543 self.manager
544 .accounting
545 .alloc_count
546 .fetch_add(1, Ordering::Relaxed);
547 let bytes = (len as u64)
548 .checked_mul(std::mem::size_of::<T>() as u64)
549 .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
550 let used = self.used_bytes();
551 if bytes > self.remaining_bytes {
552 return Err(MemoryPressure {
553 layer: "manager_reservation_alloc",
554 current_bytes: used as u128,
555 requested_bytes: bytes as u128,
556 budget_bytes: self.total_bytes,
557 prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
558 }
559 .into_error());
560 }
561
562 self.remaining_bytes -= bytes;
563 let manager = Arc::clone(&self.manager);
564 let attempt = LocalAllocationAttempt::new(
565 Arc::clone(&manager.accounting),
566 bytes,
567 Some(&mut self.remaining_bytes),
568 );
569 let runtime_reservation = self.runtime_reservation.as_mut();
570 manager
571 .alloc_after_local_reservation::<T>(
572 len,
573 bytes,
574 runtime_reservation,
575 Arc::clone(&attempt.reclamation),
576 )
577 .map_err(|error| map_resource_error(error, manager.peak_bytes()))
578 }
579
580 pub fn alloc_raw(&mut self, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
583 let bytes_u64 = u64::try_from(bytes)
584 .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
585 let used = self.used_bytes();
586 if bytes_u64 > self.remaining_bytes {
587 return Err(MemoryPressure {
588 layer: "manager_reservation_alloc_raw",
589 current_bytes: used as u128,
590 requested_bytes: bytes_u64 as u128,
591 budget_bytes: self.total_bytes,
592 prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
593 }
594 .into_error());
595 }
596
597 self.remaining_bytes -= bytes_u64;
598 let manager = Arc::clone(&self.manager);
599 let attempt = LocalAllocationAttempt::new(
600 Arc::clone(&manager.accounting),
601 bytes_u64,
602 Some(&mut self.remaining_bytes),
603 );
604 let runtime_reservation = self.runtime_reservation.as_mut();
605 manager
606 .alloc_raw_after_local_reservation(
607 bytes,
608 bytes_u64,
609 tag,
610 runtime_reservation,
611 Arc::clone(&attempt.reclamation),
612 )
613 .map_err(|error| map_resource_error(error, manager.peak_bytes()))
614 }
615}
616
617impl Drop for GpuMemoryReservation {
618 fn drop(&mut self) {
619 let unused = std::mem::take(&mut self.remaining_bytes);
620 let release = self.manager.rollback_local_reservation(unused);
621 debug_assert!(release.is_ok(), "reservation release must be balanced");
622 }
623}
624
625struct MemoryPressure {
626 layer: &'static str,
627 current_bytes: u128,
628 requested_bytes: u128,
629 budget_bytes: u64,
630 prior_peak_bytes: u64,
631}
632
633impl MemoryPressure {
634 fn required_bytes(&self) -> u128 {
635 self.current_bytes + self.requested_bytes
636 }
637
638 fn into_error(self) -> XlogError {
639 let required_bytes = self.required_bytes();
640 let required_u64_overflow = required_bytes > u64::MAX as u128;
641 XlogError::ResourceExhausted {
642 context: format!(
643 "GPU memory pressure: layer={} current_bytes={} requested_bytes={} required_bytes={} required_u64_overflow={} budget_bytes={} prior_peak_bytes={}",
644 self.layer,
645 self.current_bytes,
646 self.requested_bytes,
647 required_bytes,
648 required_u64_overflow,
649 self.budget_bytes,
650 self.prior_peak_bytes,
651 ),
652 estimated_bytes: u64::try_from(required_bytes).unwrap_or(u64::MAX),
653 budget_bytes: self.budget_bytes,
654 }
655 }
656}
657
658struct DeviceStorage {
660 backing: ManuallyDrop<Backing>,
661 stream: Arc<CudaStream>,
662 dependencies: Arc<DeviceAccessDependencies>,
663}
664
665enum Backing {
666 Native(Arc<RawDeviceAllocation>),
667 Runtime(RuntimeAllocBlock),
668 Foreign {
669 _owner: Box<dyn Send + Sync>,
670 _source: Option<Arc<TrackedCudaSlice<u8>>>,
671 bytes: u64,
672 },
673}
674
675pub(crate) struct RawDeviceAllocation {
679 payload: Option<RawAllocationPayload>,
680 reclamation_admission: Option<MemoryUseGroup>,
681}
682
683struct RawAllocationPayload {
684 ptr: u64,
685 acquired: bool,
686 bytes: usize,
687 stream: Arc<CudaStream>,
688 allocation_stream: Arc<CudaStream>,
689 dependencies: Option<Arc<DeviceAccessDependencies>>,
690 reclamation: Arc<AllocationReclamation>,
691 asynchronous: bool,
692 free_state: DriverReleaseState,
693 manager: Option<Arc<GpuMemoryManager>>,
694 lifecycle_exclusion: Option<AllocationLifecyclePermit>,
695}
696
697struct AllocationLifecycleExclusion {
700 active: std::sync::Mutex<bool>,
701 changed: std::sync::Condvar,
702}
703
704struct AllocationLifecyclePermit {
705 exclusion: Arc<AllocationLifecycleExclusion>,
706}
707
708impl AllocationLifecycleExclusion {
709 fn enter(self: &Arc<Self>) -> crate::device_runtime::ResourceResult<AllocationLifecyclePermit> {
710 let mut active = self.active.lock().map_err(|_| {
711 ResourceError::Driver("CUDA allocation lifecycle exclusion poisoned".into())
712 })?;
713 while *active {
714 active = self.changed.wait(active).map_err(|_| {
715 ResourceError::Driver("CUDA allocation lifecycle exclusion poisoned".into())
716 })?;
717 }
718 *active = true;
719 Ok(AllocationLifecyclePermit {
720 exclusion: Arc::clone(self),
721 })
722 }
723}
724
725impl Drop for AllocationLifecyclePermit {
726 fn drop(&mut self) {
727 let mut active = self
728 .exclusion
729 .active
730 .lock()
731 .unwrap_or_else(|error| error.into_inner());
732 *active = false;
733 self.exclusion.changed.notify_one();
734 }
735}
736
737fn allocation_lifecycle_exclusion(
738 context: usize,
739) -> crate::device_runtime::ResourceResult<Arc<AllocationLifecycleExclusion>> {
740 static EXCLUSIONS: std::sync::OnceLock<
741 std::sync::Mutex<
742 std::collections::HashMap<usize, std::sync::Weak<AllocationLifecycleExclusion>>,
743 >,
744 > = std::sync::OnceLock::new();
745 let mut exclusions = EXCLUSIONS
746 .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
747 .lock()
748 .map_err(|_| ResourceError::Driver("CUDA allocation lifecycle registry poisoned".into()))?;
749 if let Some(exclusion) = exclusions.get(&context).and_then(std::sync::Weak::upgrade) {
750 return Ok(exclusion);
751 }
752 let exclusion = Arc::new(AllocationLifecycleExclusion {
753 active: std::sync::Mutex::new(false),
754 changed: std::sync::Condvar::new(),
755 });
756 exclusions.insert(context, Arc::downgrade(&exclusion));
757 Ok(exclusion)
758}
759
760#[derive(Default, PartialEq, Eq)]
761enum DriverReleaseState {
762 #[default]
763 Owned,
764 OutcomeUnknown,
765 Submitted,
766}
767
768impl DriverReleaseState {
769 #[cfg(test)]
770 fn confirm_async(
771 &self,
772 recorded: &mut bool,
773 record: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
774 wait: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
775 ) -> crate::device_runtime::ResourceResult<()> {
776 if *self != Self::Submitted {
777 return Err(ResourceError::Driver(
778 "completion cannot prove an unconfirmed allocation free submission".into(),
779 ));
780 }
781 if !*recorded {
782 record()?;
783 *recorded = true;
784 }
785 wait()
786 }
787
788 fn submit(
789 &mut self,
790 free: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
791 ) -> crate::device_runtime::ResourceResult<()> {
792 match self {
793 Self::Submitted => return Ok(()),
794 Self::OutcomeUnknown => {
795 return Err(ResourceError::Driver(
796 "previous resource release outcome is unknown; refusing a second release"
797 .into(),
798 ))
799 }
800 Self::Owned => {}
801 }
802 *self = Self::OutcomeUnknown;
803 free()?;
804 *self = Self::Submitted;
805 Ok(())
806 }
807}
808
809impl RawDeviceAllocation {
810 pub(crate) fn allocate(
811 stream: Arc<CudaStream>,
812 allocation_stream: Arc<CudaStream>,
813 bytes: usize,
814 manager: Option<Arc<GpuMemoryManager>>,
815 reclamation: Arc<AllocationReclamation>,
816 ) -> crate::device_runtime::ResourceResult<Arc<Self>> {
817 let _capture_exclusion = crate::cuda_graph::reserve_uncaptured_stream(&stream)?;
818 let asynchronous = stream.context().has_async_alloc();
819 stream.context().bind_to_thread()?;
820 stream.synchronize()?;
823 let mut allocation = initialize_allocation(
826 Self {
827 reclamation_admission: None,
828 payload: Some(RawAllocationPayload {
829 ptr: 0,
830 acquired: false,
831 bytes,
832 stream,
833 allocation_stream,
834 dependencies: None,
835 reclamation: Arc::clone(&reclamation),
836 asynchronous,
837 free_state: DriverReleaseState::Owned,
838 manager,
839 lifecycle_exclusion: None,
840 }),
841 },
842 bytes,
843 Arc::clone(&reclamation),
844 |allocation| {
845 let payload = allocation
846 .payload
847 .as_mut()
848 .expect("allocation owner present");
849 payload.lifecycle_exclusion = Some(
852 allocation_lifecycle_exclusion(payload.stream.context().cu_ctx() as usize)?
853 .enter()?,
854 );
855 let ptr = if bytes == 0 {
856 0
857 } else {
858 unsafe {
861 if asynchronous {
862 cudarc::driver::result::malloc_async(
863 payload.allocation_stream.cu_stream(),
864 bytes,
865 )?
866 } else {
867 cudarc::driver::result::malloc_sync(bytes)?
868 }
869 }
870 };
871 payload.ptr = ptr;
872 payload.acquired = true;
873 payload.reclamation.acquired()?;
874 let poison = payload.manager.is_some() && bytes != 0 && poison_alloc_enabled();
875 if payload.manager.is_some() {
876 alloc_guard_insert(payload.ptr, bytes as u64);
877 if poison {
878 unsafe {
880 cudarc::driver::sys::cuMemsetD8Async(
881 payload.ptr,
882 0xDD,
883 bytes,
884 payload.allocation_stream.cu_stream(),
885 )
886 .result()?;
887 }
888 }
889 }
890 if (asynchronous && bytes != 0) || poison {
891 unsafe {
894 cudarc::driver::result::stream::synchronize(
895 payload.allocation_stream.cu_stream(),
896 )?;
897 }
898 }
899 payload.dependencies = Some(Arc::new(DeviceAccessDependencies::after_ready(
900 Arc::clone(payload.stream.context()),
901 Arc::clone(&reclamation),
902 )));
903 Ok(())
904 },
905 )?;
906 let pending_lifecycle_exclusion = allocation
908 .payload
909 .as_mut()
910 .expect("initialized allocation")
911 .lifecycle_exclusion
912 .take()
913 .expect("allocation lifecycle exclusion held");
914 let allocation = Arc::new(allocation);
915 let lifecycle_exclusion = pending_lifecycle_exclusion;
918 let dependencies = allocation.dependencies();
919 dependencies.bind_allocation(&allocation);
920 {
921 let owner: Arc<dyn MemoryStorageOwner> =
922 Arc::clone(&allocation) as Arc<dyn MemoryStorageOwner>;
923 let payload = allocation.payload.as_ref().expect("initialized allocation");
924 let range = match MemoryUse::new(payload.ptr, payload.bytes, Access::ReadWrite) {
925 Ok(range) => range,
926 Err(error) => return Err(error.retaining(bytes, reclamation)),
927 };
928 block_use_registry()
929 .lock()
930 .expect("device block-use registry poisoned")
931 .register_storage(
932 payload.stream.context().cu_ctx() as usize,
933 range,
934 Arc::downgrade(&owner),
935 dependencies.reclamation.release_proof(),
936 );
937 }
938 drop(lifecycle_exclusion);
939 Ok(allocation)
940 }
941
942 pub(crate) fn ptr(&self) -> u64 {
943 self.payload.as_ref().expect("allocation live").ptr
944 }
945 pub(crate) fn len(&self) -> usize {
946 self.payload.as_ref().expect("allocation live").bytes
947 }
948 pub(crate) fn dependencies(&self) -> Arc<DeviceAccessDependencies> {
949 Arc::clone(
950 self.payload
951 .as_ref()
952 .expect("allocation live")
953 .dependencies
954 .as_ref()
955 .expect("allocation producer was recorded"),
956 )
957 }
958
959 pub(crate) fn reclamation(&self) -> &Arc<AllocationReclamation> {
960 &self
961 .payload
962 .as_ref()
963 .expect("allocation owner remains live")
964 .reclamation
965 }
966
967 pub(crate) fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
970 let admission = &mut self.reclamation_admission;
971 reclaim_allocation(&mut self.payload, |payload| {
972 if !payload.acquired {
975 return Ok(());
976 }
977 if payload.lifecycle_exclusion.is_none() {
978 payload.lifecycle_exclusion = Some(
981 allocation_lifecycle_exclusion(payload.stream.context().cu_ctx() as usize)?
982 .enter()?,
983 );
984 }
985 crate::device_runtime::resource::with_reclamation_admission(
986 block_use_registry(),
987 payload.stream.context().cu_ctx() as usize,
988 MemoryUse::new(payload.ptr, payload.bytes, Access::ReadWrite)?,
989 payload.reclamation.release_proof(),
990 admission,
991 || {
992 let _capture_exclusion = crate::cuda_graph::reserve_capture_exclusion()?;
995 payload.stream.context().bind_to_thread()?;
996 if payload.free_state != DriverReleaseState::Submitted {
997 if payload.free_state == DriverReleaseState::OutcomeUnknown {
998 return Err(ResourceError::Driver(
999 "previous allocation free outcome is unknown".into(),
1000 ));
1001 }
1002 if let Some(dependencies) = &payload.dependencies {
1003 dependencies.synchronize()?;
1004 }
1005 payload.stream.context().bind_to_thread()?;
1006 let poison = payload.manager.is_some()
1007 && payload.bytes != 0
1008 && poison_free_enabled();
1009 if poison {
1010 unsafe {
1014 cudarc::driver::sys::cuMemsetD8Async(
1015 payload.ptr,
1016 0xDD,
1017 payload.bytes,
1018 payload.allocation_stream.cu_stream(),
1019 )
1020 .result()?;
1021 }
1022 unsafe {
1024 cudarc::driver::result::stream::synchronize(
1025 payload.allocation_stream.cu_stream(),
1026 )?;
1027 }
1028 }
1029 payload.free_state.submit(|| {
1030 if payload.bytes != 0 {
1031 if payload.manager.is_some() {
1035 alloc_guard_remove(payload.ptr);
1036 }
1037 unsafe {
1040 if payload.asynchronous {
1041 cudarc::driver::result::free_async(
1042 payload.ptr,
1043 payload.allocation_stream.cu_stream(),
1044 )?;
1045 } else {
1046 cudarc::driver::result::free_sync(payload.ptr)?;
1047 }
1048 }
1049 }
1050 Ok(())
1051 })?;
1052 }
1053 if payload.asynchronous && payload.bytes != 0 {
1054 unsafe {
1057 cudarc::driver::result::stream::synchronize(
1058 payload.allocation_stream.cu_stream(),
1059 )?;
1060 }
1061 }
1062 payload.reclamation.complete()?;
1063 Ok(())
1064 },
1065 )
1066 })
1067 }
1068}
1069
1070impl MemoryStorageOwner for RawDeviceAllocation {
1071 fn dependencies(&self) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
1072 Ok(RawDeviceAllocation::dependencies(self))
1073 }
1074}
1075
1076pub(crate) fn reclaim_allocation<T>(
1079 payload: &mut Option<T>,
1080 release: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1081) -> crate::device_runtime::ResourceResult<()> {
1082 if let Some(owner) = payload.as_mut() {
1083 release(owner)?;
1084 *payload = None;
1085 }
1086 Ok(())
1087}
1088
1089pub(crate) fn reclaim_shared_allocation<T>(
1094 pending: &mut Option<Arc<T>>,
1095 release: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1096) -> crate::device_runtime::ResourceResult<bool> {
1097 let Some(shared) = pending.take() else {
1098 return Ok(true);
1099 };
1100 let owner = match Arc::try_unwrap(shared) {
1101 Ok(owner) => owner,
1102 Err(shared) => {
1103 *pending = Some(shared);
1104 return Ok(false);
1105 }
1106 };
1107 struct RestoreOwner<'a, T> {
1108 pending: &'a mut Option<Arc<T>>,
1109 owner: Option<T>,
1110 }
1111 impl<T> Drop for RestoreOwner<'_, T> {
1112 fn drop(&mut self) {
1113 if let Some(owner) = self.owner.take() {
1114 *self.pending = Some(Arc::new(owner));
1115 }
1116 }
1117 }
1118 let mut guard = RestoreOwner {
1119 pending,
1120 owner: Some(owner),
1121 };
1122 release(guard.owner.as_mut().expect("physical owner present"))?;
1123 drop(guard.owner.take());
1124 Ok(true)
1125}
1126
1127pub(crate) struct PendingReclamationBatch<'a, T> {
1130 queue: &'a std::sync::Mutex<T>,
1131 owners: ManuallyDrop<T>,
1132 merge: fn(&mut T, &mut T),
1133}
1134
1135impl<'a, T: Default> PendingReclamationBatch<'a, T> {
1136 pub(crate) fn take(queue: &'a std::sync::Mutex<T>, merge: fn(&mut T, &mut T)) -> Self {
1137 let owners = {
1138 let mut pending = queue.lock().expect("pending allocation queue poisoned");
1139 ManuallyDrop::new(std::mem::take(&mut *pending))
1140 };
1141 Self {
1142 queue,
1143 owners,
1144 merge,
1145 }
1146 }
1147}
1148
1149impl<T> std::ops::Deref for PendingReclamationBatch<'_, T> {
1150 type Target = T;
1151 fn deref(&self) -> &T {
1152 &self.owners
1153 }
1154}
1155
1156impl<T> std::ops::DerefMut for PendingReclamationBatch<'_, T> {
1157 fn deref_mut(&mut self) -> &mut T {
1158 &mut self.owners
1159 }
1160}
1161
1162impl<T> Drop for PendingReclamationBatch<'_, T> {
1163 fn drop(&mut self) {
1164 let mut queue = self.queue.lock().unwrap_or_else(|error| error.into_inner());
1169 (self.merge)(&mut *queue, &mut self.owners);
1170 drop(queue);
1171 unsafe { ManuallyDrop::drop(&mut self.owners) };
1174 }
1175}
1176
1177pub(crate) fn reap_raw_allocations(
1181 allocations: &mut Vec<Option<Arc<RawDeviceAllocation>>>,
1182) -> crate::device_runtime::ResourceResult<()> {
1183 for allocation in allocations.iter_mut() {
1184 reclaim_shared_allocation(allocation, RawDeviceAllocation::release)?;
1185 }
1186 allocations.retain(Option::is_some);
1187 Ok(())
1188}
1189
1190impl Drop for RawDeviceAllocation {
1191 fn drop(&mut self) {
1192 if let Some(payload) = self.payload.take() {
1193 let mut owner = Self {
1194 payload: Some(payload),
1195 reclamation_admission: self.reclamation_admission.take(),
1196 };
1197 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1201 crate::cuda_graph::retry_retirement_after_stream_captures(move || {
1202 if let Err(error) = owner.release() {
1203 if let Some(manager) = owner
1204 .payload
1205 .as_ref()
1206 .filter(|p| !p.reclamation.was_released())
1207 .and_then(|p| p.manager.as_ref())
1208 {
1209 manager.record_deallocation_failure(owner.len() as u64);
1210 }
1211 eprintln!(
1212 "CUDA allocation cleanup incomplete; retaining unresolved memory or handle ownership in the cold reaper: {error}"
1213 );
1214 return false;
1215 }
1216 true
1217 });
1218 }));
1219 }
1220 }
1221}
1222
1223fn initialize_allocation<T: Send + Sync + 'static>(
1227 mut owner: T,
1228 bytes: usize,
1229 reclamation: Arc<AllocationReclamation>,
1230 initialize: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1231) -> crate::device_runtime::ResourceResult<T> {
1232 match initialize(&mut owner) {
1233 Ok(()) => Ok(owner),
1234 Err(error) if reclamation.was_acquired() => Err(error.retaining(bytes, reclamation)),
1235 Err(error) => Err(error),
1236 }
1237}
1238
1239impl std::panic::RefUnwindSafe for DeviceStorage {}
1243
1244impl DeviceStorage {
1245 fn new(backing: Backing, stream: Arc<CudaStream>, raw_ptr: u64) -> Arc<Self> {
1246 let dependencies = match &backing {
1247 Backing::Native(allocation) => allocation.dependencies(),
1248 Backing::Runtime(allocation) => Arc::clone(&allocation.dependencies),
1249 Backing::Foreign { .. } => Arc::new(DeviceAccessDependencies::after_ready(
1250 Arc::clone(stream.context()),
1251 Arc::default(),
1252 )),
1253 };
1254 let storage = Arc::new(Self {
1255 backing: ManuallyDrop::new(backing),
1256 stream,
1257 dependencies,
1258 });
1259 if matches!(&*storage.backing, Backing::Foreign { .. }) {
1264 let owner: Arc<dyn MemoryStorageOwner> = storage.clone();
1265 let range = MemoryUse::new(raw_ptr, storage.bytes() as usize, Access::ReadWrite)
1266 .expect("CUDA allocation must describe a representable device range");
1267 block_use_registry()
1268 .lock()
1269 .expect("device block-use registry poisoned")
1270 .register_storage(
1271 storage.stream.context().cu_ctx() as usize,
1272 range,
1273 Arc::downgrade(&owner),
1274 storage.dependencies.reclamation.release_proof(),
1275 );
1276 }
1277 storage
1278 }
1279
1280 fn manager(&self) -> Option<&Arc<GpuMemoryManager>> {
1281 match &*self.backing {
1282 Backing::Native(allocation) => {
1283 allocation.payload.as_ref().and_then(|p| p.manager.as_ref())
1284 }
1285 Backing::Runtime(allocation) => Some(&allocation.manager),
1286 Backing::Foreign { .. } => None,
1287 }
1288 }
1289
1290 fn bytes(&self) -> u64 {
1291 match &*self.backing {
1292 Backing::Native(allocation) => allocation.len() as u64,
1293 Backing::Runtime(allocation) => allocation.bytes,
1294 Backing::Foreign { bytes, .. } => *bytes,
1295 }
1296 }
1297}
1298
1299impl MemoryStorageOwner for DeviceStorage {
1300 fn dependencies(&self) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
1301 Ok(Arc::clone(&self.dependencies))
1302 }
1303}
1304
1305impl Drop for DeviceStorage {
1306 fn drop(&mut self) {
1307 let backing = unsafe { ManuallyDrop::take(&mut self.backing) };
1308 let dependencies = Arc::clone(&self.dependencies);
1309 let stream = Arc::clone(&self.stream);
1310 match backing {
1311 Backing::Foreign { .. } => crate::cuda_graph::retire_resources_after_completion(
1312 (Some(backing), dependencies, stream),
1313 |(_, dependencies, _)| dependencies.synchronize(),
1314 |owners| {
1315 drop(owners.0.take());
1319 Ok(())
1320 },
1321 |owners| owners.1.mark_allocation_released(),
1322 |_, error| eprintln!("CUDA imported storage retirement incomplete: {error}"),
1323 ),
1324 backing => crate::cuda_graph::retire_after_stream_captures(move || {
1325 drop(backing);
1329 }),
1330 }
1331 }
1332}
1333
1334fn debug_env_enabled(name: &str) -> bool {
1338 resolve_bool(None, name, false)
1339 .unwrap_or_else(|error| panic!("invalid CUDA memory debug configuration: {error}"))
1340}
1341
1342fn poison_free_enabled() -> bool {
1343 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1344 *ENABLED.get_or_init(|| debug_env_enabled("XLOG_DEBUG_POISON_FREE"))
1345}
1346
1347fn poison_alloc_enabled() -> bool {
1351 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1352 *ENABLED.get_or_init(|| debug_env_enabled("XLOG_DEBUG_POISON_ALLOC"))
1353}
1354
1355fn alloc_guard() -> Option<&'static std::sync::Mutex<std::collections::BTreeMap<u64, u64>>> {
1360 static GUARD: std::sync::OnceLock<
1361 Option<std::sync::Mutex<std::collections::BTreeMap<u64, u64>>>,
1362 > = std::sync::OnceLock::new();
1363 GUARD
1364 .get_or_init(|| {
1365 if debug_env_enabled("XLOG_DEBUG_ALLOC_GUARD") {
1366 Some(std::sync::Mutex::new(std::collections::BTreeMap::new()))
1367 } else {
1368 None
1369 }
1370 })
1371 .as_ref()
1372}
1373
1374fn alloc_guard_insert(ptr: u64, bytes: u64) {
1375 let Some(guard) = alloc_guard() else { return };
1376 if bytes == 0 {
1377 return;
1378 }
1379 let mut live = guard.lock().unwrap();
1380 if let Some((&p, &b)) = live.range(..=ptr).next_back() {
1383 if p + b > ptr {
1384 panic!(
1385 "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live [{:#x}, {:#x})",
1386 ptr,
1387 ptr + bytes,
1388 p,
1389 p + b
1390 );
1391 }
1392 }
1393 if let Some((&p, _)) = live.range(ptr + 1..).next() {
1394 if ptr + bytes > p {
1395 panic!(
1396 "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live starting at {:#x}",
1397 ptr,
1398 ptr + bytes,
1399 p
1400 );
1401 }
1402 }
1403 live.insert(ptr, bytes);
1404}
1405
1406fn alloc_guard_remove(ptr: u64) {
1407 let Some(guard) = alloc_guard() else { return };
1408 guard.lock().unwrap().remove(&ptr);
1409}
1410
1411pub struct TrackedCudaSlice<T: cudarc::driver::DeviceRepr> {
1435 storage: Arc<DeviceStorage>,
1436 ptr: u64,
1437 len: usize,
1438 element: std::marker::PhantomData<T>,
1439}
1440
1441pub struct DeviceMemoryView<T> {
1444 ptr: u64,
1445 len: usize,
1446 storage: Arc<DeviceStorage>,
1447 element: std::marker::PhantomData<T>,
1448}
1449
1450#[derive(Clone)]
1456pub struct DeviceAllocationProvenance {
1457 allocation: Arc<RawDeviceAllocation>,
1458 byte_offset: u64,
1459 view_bytes: u64,
1460}
1461
1462impl DeviceAllocationProvenance {
1463 pub fn same_allocation(&self, other: &Self) -> bool {
1465 Arc::ptr_eq(&self.allocation, &other.allocation)
1466 }
1467
1468 pub fn allocation_bytes(&self) -> u64 {
1470 self.allocation.len() as u64
1471 }
1472
1473 pub fn byte_offset(&self) -> u64 {
1474 self.byte_offset
1475 }
1476
1477 pub fn view_bytes(&self) -> u64 {
1478 self.view_bytes
1479 }
1480}
1481
1482impl<T> Clone for DeviceMemoryView<T> {
1483 fn clone(&self) -> Self {
1484 Self {
1485 ptr: self.ptr,
1486 len: self.len,
1487 storage: Arc::clone(&self.storage),
1488 element: std::marker::PhantomData,
1489 }
1490 }
1491}
1492
1493impl<T> DeviceMemoryView<T> {
1494 pub fn len(&self) -> usize {
1495 self.len
1496 }
1497 pub fn is_empty(&self) -> bool {
1498 self.len == 0
1499 }
1500 pub fn stream(&self) -> &Arc<CudaStream> {
1501 &self.storage.stream
1502 }
1503 pub fn device_ptr(&self) -> &u64 {
1504 &self.ptr
1505 }
1506
1507 pub fn allocation_provenance(&self) -> Option<DeviceAllocationProvenance> {
1511 let allocation = match &*self.storage.backing {
1512 Backing::Native(allocation) => allocation,
1513 Backing::Runtime(block) => block.allocation.as_ref()?,
1514 Backing::Foreign { .. } => return None,
1515 };
1516 let byte_offset = self.ptr.checked_sub(allocation.ptr())?;
1517 let view_bytes = u64::try_from(self.len.checked_mul(std::mem::size_of::<T>())?).ok()?;
1518 let allocation_bytes = u64::try_from(allocation.len()).ok()?;
1519 if byte_offset.checked_add(view_bytes)? > allocation_bytes {
1520 return None;
1521 }
1522 Some(DeviceAllocationProvenance {
1523 allocation: Arc::clone(allocation),
1524 byte_offset,
1525 view_bytes,
1526 })
1527 }
1528
1529 pub(crate) fn allocation_view(&self) -> Option<DeviceMemoryView<u8>> {
1533 let provenance = self.allocation_provenance()?;
1534 Some(DeviceMemoryView {
1535 ptr: provenance.allocation.ptr(),
1536 len: provenance.allocation.len(),
1537 storage: Arc::clone(&self.storage),
1538 element: std::marker::PhantomData,
1539 })
1540 }
1541
1542 pub fn try_slice(&self, range: impl std::ops::RangeBounds<usize>) -> Option<Self> {
1543 use std::ops::Bound;
1544 let start = match range.start_bound() {
1545 Bound::Included(&n) => n,
1546 Bound::Excluded(&n) => n.checked_add(1)?,
1547 Bound::Unbounded => 0,
1548 };
1549 let end = match range.end_bound() {
1550 Bound::Included(&n) => n.checked_add(1)?,
1551 Bound::Excluded(&n) => n,
1552 Bound::Unbounded => self.len,
1553 };
1554 if start > end || end > self.len {
1555 return None;
1556 }
1557 let offset = start.checked_mul(std::mem::size_of::<T>())?;
1558 Some(Self {
1559 ptr: self.ptr.checked_add(u64::try_from(offset).ok()?)?,
1560 len: end - start,
1561 storage: Arc::clone(&self.storage),
1562 element: std::marker::PhantomData,
1563 })
1564 }
1565
1566 pub fn slice(&self, range: impl std::ops::RangeBounds<usize>) -> Self {
1567 self.try_slice(range)
1568 .expect("device slice range is out of bounds")
1569 }
1570
1571 pub fn slice_mut(&mut self, range: impl std::ops::RangeBounds<usize>) -> Self {
1572 self.slice(range)
1573 }
1574
1575 pub unsafe fn cast<U>(&self) -> Option<DeviceMemoryView<U>> {
1581 let bytes = self.len.checked_mul(std::mem::size_of::<T>())?;
1582 let size = std::mem::size_of::<U>();
1583 if size == 0
1584 || !bytes.is_multiple_of(size)
1585 || !self.ptr.is_multiple_of(std::mem::align_of::<U>() as u64)
1586 {
1587 return None;
1588 }
1589 Some(DeviceMemoryView {
1590 ptr: self.ptr,
1591 len: bytes / size,
1592 storage: Arc::clone(&self.storage),
1593 element: std::marker::PhantomData,
1594 })
1595 }
1596}
1597
1598impl<T: DeviceRepr + 'static> DeviceMemoryView<T> {
1599 pub(crate) fn allocate(
1601 stream: Arc<CudaStream>,
1602 allocation_stream: Arc<CudaStream>,
1603 len: usize,
1604 ) -> crate::device_runtime::ResourceResult<Self> {
1605 let bytes = len
1606 .checked_mul(std::mem::size_of::<T>())
1607 .ok_or_else(|| ResourceError::Driver("allocation size overflow".into()))?;
1608 let allocation = RawDeviceAllocation::allocate(
1609 Arc::clone(&stream),
1610 allocation_stream,
1611 bytes,
1612 None,
1613 Arc::default(),
1614 )?;
1615 let ptr = allocation.ptr();
1616 Ok(Self {
1617 ptr,
1618 len,
1619 storage: DeviceStorage::new(Backing::Native(allocation), stream, ptr),
1620 element: std::marker::PhantomData,
1621 })
1622 }
1623}
1624
1625impl<T> DeviceSlice<T> for DeviceMemoryView<T> {
1626 fn len(&self) -> usize {
1627 self.len
1628 }
1629 fn stream(&self) -> &Arc<CudaStream> {
1630 &self.storage.stream
1631 }
1632}
1633
1634pub trait DeviceRead<T>: private_access::Sealed {
1637 fn device_view(&self) -> DeviceMemoryView<T>;
1638}
1639
1640pub trait DeviceWrite<T>: DeviceRead<T> {}
1643
1644mod private_access {
1645 pub trait Sealed {}
1646}
1647
1648impl<T: DeviceRepr> private_access::Sealed for TrackedCudaSlice<T> {}
1649impl<T: DeviceRepr> DeviceRead<T> for TrackedCudaSlice<T> {
1650 fn device_view(&self) -> DeviceMemoryView<T> {
1651 self.view()
1652 }
1653}
1654impl<T: DeviceRepr> DeviceWrite<T> for TrackedCudaSlice<T> {}
1655impl<T> private_access::Sealed for DeviceMemoryView<T> {}
1656impl<T> DeviceRead<T> for DeviceMemoryView<T> {
1657 fn device_view(&self) -> DeviceMemoryView<T> {
1658 self.clone()
1659 }
1660}
1661impl<T> DeviceWrite<T> for DeviceMemoryView<T> {}
1662
1663impl<T: DeviceRepr> AsKernelParam for &DeviceMemoryView<T> {
1664 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1665 (&self.ptr as *const u64).cast_mut().cast()
1666 }
1667}
1668
1669impl<T: DeviceRepr> AsKernelParam for &mut DeviceMemoryView<T> {
1670 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1671 (&self.ptr as *const u64).cast_mut().cast()
1672 }
1673}
1674
1675impl<'a, T: DeviceRepr> IntoKernelParamStorage for &'a DeviceMemoryView<T> {
1676 type Storage = DeviceParamStorage<'a>;
1677
1678 fn into_kernel_param_storage(self) -> Self::Storage {
1679 DeviceParamStorage::unsynced(self.ptr)
1680 }
1681}
1682
1683impl<'a, T: DeviceRepr> IntoKernelParamStorage for &'a mut DeviceMemoryView<T> {
1684 type Storage = DeviceParamStorage<'a>;
1685
1686 fn into_kernel_param_storage(self) -> Self::Storage {
1687 DeviceParamStorage::unsynced(self.ptr)
1688 }
1689}
1690
1691#[derive(Clone)]
1694pub(crate) struct DeviceMemoryAccess {
1695 storage: Arc<DeviceStorage>,
1696 range: MemoryUse,
1697}
1698
1699impl DeviceMemoryAccess {
1700 pub(crate) fn retained_owner(&self) -> RetainedStorageUse {
1701 RetainedStorageUse {
1702 owner: self.storage.clone(),
1703 access: self.range.access(),
1704 }
1705 }
1706
1707 pub(crate) fn context(&self) -> usize {
1708 self.storage.stream.context().cu_ctx() as usize
1709 }
1710}
1711
1712impl<T> DeviceMemoryView<T> {
1713 pub(crate) fn access(
1714 &self,
1715 access: Access,
1716 ) -> crate::device_runtime::ResourceResult<DeviceMemoryAccess> {
1717 let bytes = self
1718 .len
1719 .checked_mul(std::mem::size_of::<T>())
1720 .ok_or_else(|| ResourceError::StreamMisuse("device view byte size overflow".into()))?;
1721 Ok(DeviceMemoryAccess {
1722 storage: Arc::clone(&self.storage),
1723 range: MemoryUse::new(self.ptr, bytes, access)?,
1724 })
1725 }
1726}
1727
1728#[derive(Default)]
1731pub(crate) struct MemoryAccessManifest {
1732 accesses: Vec<DeviceMemoryAccess>,
1733 ranges: Vec<(usize, MemoryUse)>,
1734 retained: Vec<RetainedStorageUse>,
1735 runtimes: Vec<Arc<XlogDeviceRuntime>>,
1736 runtime_dependencies: Vec<(Arc<DeviceAccessDependencies>, Access)>,
1737 runtime_allocations: Vec<Arc<RawDeviceAllocation>>,
1738}
1739
1740impl MemoryAccessManifest {
1741 pub(crate) fn combine(manifests: &[Arc<Self>]) -> Arc<Self> {
1742 let mut combined = Self::default();
1743 for manifest in manifests {
1744 combined.accesses.extend(manifest.accesses.iter().cloned());
1745 combined.ranges.extend_from_slice(&manifest.ranges);
1746 combined
1747 .retained
1748 .extend(manifest.retained.iter().map(|retained| RetainedStorageUse {
1749 owner: Arc::clone(&retained.owner),
1750 access: retained.access,
1751 }));
1752 combined.runtimes.extend(manifest.runtimes.iter().cloned());
1753 combined
1754 .runtime_dependencies
1755 .extend(manifest.runtime_dependencies.iter().cloned());
1756 combined
1757 .runtime_allocations
1758 .extend(manifest.runtime_allocations.iter().cloned());
1759 }
1760 Arc::new(combined)
1761 }
1762
1763 pub(crate) fn covers(&self, ranges: &[(usize, MemoryUse)]) -> bool {
1764 ranges.iter().all(|(context, range)| {
1765 self.ranges
1766 .iter()
1767 .any(|(owned_context, owned)| context == owned_context && owned.covers(*range))
1768 })
1769 }
1770
1771 pub(crate) fn covered_by(&self, other: &Self) -> bool {
1772 other.covers(&self.ranges)
1773 }
1774}
1775
1776#[derive(Debug)]
1780pub(crate) struct OperationCompletion {
1781 execution_id: u64,
1782 complete: std::sync::atomic::AtomicBool,
1783}
1784
1785impl OperationCompletion {
1786 pub(crate) fn new(execution_id: u64) -> Self {
1787 Self {
1788 execution_id,
1789 complete: std::sync::atomic::AtomicBool::new(false),
1790 }
1791 }
1792
1793 pub(crate) fn validate_submission(
1794 &self,
1795 execution_id: u64,
1796 ) -> crate::device_runtime::ResourceResult<()> {
1797 if execution_id != self.execution_id || self.is_complete() {
1798 return Err(ResourceError::StreamMisuse(
1799 "memory operation requires its original uncompleted stream execution".into(),
1800 ));
1801 }
1802 Ok(())
1803 }
1804
1805 pub(crate) fn synchronize_with(
1806 &self,
1807 execution_id: u64,
1808 synchronize: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
1809 ) -> crate::device_runtime::ResourceResult<()> {
1810 if execution_id != self.execution_id {
1811 return Err(ResourceError::StreamMisuse(
1812 "memory operation cannot synchronize another stream execution".into(),
1813 ));
1814 }
1815 synchronize()?;
1816 self.complete.store(true, Ordering::Release);
1817 Ok(())
1818 }
1819
1820 pub(crate) fn is_complete(&self) -> bool {
1821 self.complete.load(Ordering::Acquire)
1822 }
1823
1824 fn synchronize_retired_with(
1828 &self,
1829 synchronize_context: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
1830 ) -> crate::device_runtime::ResourceResult<()> {
1831 if !self.is_complete() {
1832 synchronize_context()?;
1833 self.complete.store(true, Ordering::Release);
1834 }
1835 Ok(())
1836 }
1837}
1838
1839pub(crate) struct MemoryOperationOwner {
1840 stream: Arc<CudaStream>,
1841 manifest: Arc<MemoryAccessManifest>,
1842 retained: Vec<RetainedStorageUse>,
1843 captured: std::sync::atomic::AtomicBool,
1844 dependencies: std::sync::OnceLock<Vec<(Arc<DeviceAccessDependencies>, Access)>>,
1845 completion: Arc<OperationCompletion>,
1846}
1847
1848impl crate::launch::RecorderCleanup<MemoryUseGroup> for MemoryOperationOwner {
1849 fn synchronize_retired(&self) -> crate::device_runtime::ResourceResult<()> {
1850 if self.captured.load(Ordering::Acquire) {
1851 return Ok(());
1854 }
1855 let _ordinary = crate::cuda_graph::reserve_capture_exclusion()?;
1856 self.completion.synchronize_retired_with(|| {
1857 self.stream.context().bind_to_thread()?;
1863 self.stream.context().synchronize().map_err(|error| {
1864 ResourceError::Driver(format!(
1865 "retired memory operation context wait failed: {error}"
1866 ))
1867 })
1868 })
1869 }
1870
1871 fn cancel_retired(&self, group: MemoryUseGroup) -> crate::device_runtime::ResourceResult<()> {
1872 self.cancel(&[group])
1873 }
1874}
1875
1876impl MemoryOperationOwner {
1877 pub(crate) fn stream(&self) -> &Arc<CudaStream> {
1878 &self.stream
1879 }
1880
1881 pub(crate) fn manifest(&self) -> &Arc<MemoryAccessManifest> {
1882 &self.manifest
1883 }
1884
1885 pub(crate) fn completion(&self) -> &Arc<OperationCompletion> {
1886 &self.completion
1887 }
1888
1889 pub(crate) fn bind_submission(
1890 self: &Arc<Self>,
1891 pin: &crate::cuda_graph::StreamSubmissionPin<'_>,
1892 ) -> crate::device_runtime::ResourceResult<()> {
1893 self.completion
1896 .validate_submission(pin.validate_execution(&self.stream)?)?;
1897 if pin.capture_memory(&self.manifest) {
1900 self.captured.store(true, Ordering::Release);
1901 }
1902 Ok(())
1903 }
1904
1905 pub(crate) fn synchronize(&self) -> crate::device_runtime::ResourceResult<()> {
1906 if self.captured.load(Ordering::Acquire) {
1907 return Ok(());
1910 }
1911 self.completion.synchronize_with(
1912 crate::cuda_graph::stream_execution_id(&self.stream)?,
1913 || {
1914 self.stream.synchronize().map_err(|error| {
1915 ResourceError::Driver(format!(
1916 "device memory operation synchronization failed: {error}"
1917 ))
1918 })
1919 },
1920 )
1921 }
1922
1923 pub(crate) fn cancel(
1924 &self,
1925 groups: &[MemoryUseGroup],
1926 ) -> crate::device_runtime::ResourceResult<()> {
1927 let [group] = groups else {
1928 return Err(ResourceError::StreamMisuse(
1929 "device memory operation must own one exact reservation".into(),
1930 ));
1931 };
1932 block_use_registry()
1933 .lock()
1934 .expect("device block-use registry poisoned")
1935 .release_memory_uses(*group)
1936 }
1937
1938 pub(crate) fn prepare(&self) -> crate::device_runtime::ResourceResult<()> {
1939 if self.captured.load(Ordering::Acquire) {
1940 return Ok(());
1941 }
1942 let mut dependencies = Vec::new();
1943 for (dependency, access) in &self.manifest.runtime_dependencies {
1944 if let Some((_, existing_access)) = dependencies
1945 .iter_mut()
1946 .find(|(existing, _)| Arc::ptr_eq(existing, dependency))
1947 {
1948 *existing_access = crate::launch::combine_access(*existing_access, *access);
1949 } else {
1950 dependencies.push((Arc::clone(dependency), *access));
1951 }
1952 }
1953 for retained in &self.retained {
1954 let dependency = retained.owner.dependencies()?;
1955 if let Some((_, access)) = dependencies
1956 .iter_mut()
1957 .find(|(existing, _)| Arc::ptr_eq(existing, &dependency))
1958 {
1959 *access = crate::launch::combine_access(*access, retained.access);
1960 } else {
1961 dependencies.push((dependency, retained.access));
1962 }
1963 }
1964 self.dependencies.set(dependencies).map_err(|_| {
1965 ResourceError::StreamMisuse(
1966 "device memory dependencies may be prepared only once".into(),
1967 )
1968 })?;
1969 for (dependency, access) in self.dependencies.get().expect("dependencies installed") {
1970 dependency.prepare(&self.stream, *access)?;
1971 }
1972 Ok(())
1973 }
1974
1975 pub(crate) fn finish(
1976 &self,
1977 group: MemoryUseGroup,
1978 ) -> crate::device_runtime::ResourceResult<()> {
1979 if self.captured.load(Ordering::Acquire) {
1980 return self.cancel(&[group]);
1981 }
1982 let dependencies = self.dependencies.get().ok_or_else(|| {
1986 ResourceError::StreamMisuse(
1987 "device memory completion requires prepared dependencies".into(),
1988 )
1989 })?;
1990 DeviceAccessDependencies::record_operation_completion(
1991 dependencies,
1992 Arc::clone(&self.stream),
1993 )?;
1994 self.cancel(&[group])
1995 }
1996}
1997
1998pub(crate) struct MemoryEnqueue<'a> {
2002 owner: &'a Arc<MemoryOperationOwner>,
2003 submission: &'a crate::cuda_graph::StreamSubmissionPin<'a>,
2004}
2005
2006impl MemoryEnqueue<'_> {
2007 pub(crate) fn cuda_enqueue(
2008 &self,
2009 ) -> crate::device_runtime::ResourceResult<crate::launch::CudaEnqueue<'_>> {
2010 crate::launch::CudaEnqueue::from_admission(self.owner, self.submission)
2011 }
2012
2013 fn validate<T>(
2014 &self,
2015 view: &DeviceMemoryView<T>,
2016 access: Access,
2017 ) -> crate::device_runtime::ResourceResult<()> {
2018 let range = view.access(access)?.range;
2019 if !self.owner.manifest.accesses.iter().any(|admitted| {
2020 Arc::ptr_eq(&admitted.storage, &view.storage) && admitted.range.covers(range)
2021 }) {
2022 return Err(ResourceError::StreamMisuse(
2023 "native device view is not covered by the admitted access manifest".into(),
2024 ));
2025 }
2026 Ok(())
2027 }
2028
2029 pub(crate) fn read<'a, T>(
2030 &'a self,
2031 view: &'a DeviceMemoryView<T>,
2032 ) -> crate::device_runtime::ResourceResult<PreparedDeviceRead<'a, T>> {
2033 self.validate(view, Access::Read)?;
2034 self.validate_native_context(view)?;
2035 Ok(PreparedDeviceRead { view, proof: self })
2036 }
2037
2038 pub(crate) fn write<'a, T>(
2039 &'a self,
2040 view: &'a DeviceMemoryView<T>,
2041 ) -> crate::device_runtime::ResourceResult<PreparedDeviceWrite<'a, T>> {
2042 self.validate(view, Access::Write)?;
2043 self.validate_native_context(view)?;
2044 Ok(PreparedDeviceWrite { view, proof: self })
2045 }
2046
2047 fn validate_native_context<T>(
2048 &self,
2049 view: &DeviceMemoryView<T>,
2050 ) -> crate::device_runtime::ResourceResult<()> {
2051 if view.stream().context().cu_ctx() != self.owner.stream.context().cu_ctx() {
2052 return Err(ResourceError::StreamMisuse(
2053 "native device access requires the allocation context".into(),
2054 ));
2055 }
2056 Ok(())
2057 }
2058
2059 pub(crate) fn copy<T>(
2060 &self,
2061 source: &DeviceMemoryView<T>,
2062 destination: &DeviceMemoryView<T>,
2063 ) -> crate::device_runtime::ResourceResult<()> {
2064 self.validate(source, Access::Read)?;
2065 self.validate(destination, Access::Write)?;
2066 self.validate_native_context(destination)?;
2067 let source_context = source.stream().context().cu_ctx();
2068 let destination_context = destination.stream().context().cu_ctx();
2069 source.access(Access::Read)?.range.validate_copy_to(
2070 source_context as usize,
2071 destination.access(Access::Write)?.range,
2072 destination_context as usize,
2073 )?;
2074 if source_context == destination_context {
2075 self.owner
2076 .stream
2077 .memcpy_dtod(&self.read(source)?, &mut self.write(destination)?)?;
2078 } else {
2079 self.owner.stream.context().bind_to_thread()?;
2080 unsafe {
2085 cudarc::driver::result::memcpy_peer_async(
2086 destination_context,
2087 destination.ptr,
2088 source_context,
2089 source.ptr,
2090 source
2091 .len
2092 .checked_mul(std::mem::size_of::<T>())
2093 .ok_or_else(|| {
2094 ResourceError::StreamMisuse("device copy byte size overflow".into())
2095 })?,
2096 self.owner.stream.cu_stream(),
2097 )?;
2098 }
2099 }
2100 Ok(())
2101 }
2102}
2103
2104pub(crate) struct PreparedDeviceRead<'a, T> {
2105 view: &'a DeviceMemoryView<T>,
2106 proof: &'a MemoryEnqueue<'a>,
2107}
2108
2109pub(crate) struct PreparedDeviceWrite<'a, T> {
2110 view: &'a DeviceMemoryView<T>,
2111 proof: &'a MemoryEnqueue<'a>,
2112}
2113
2114impl<T> DeviceSlice<T> for PreparedDeviceRead<'_, T> {
2115 fn len(&self) -> usize {
2116 self.view.len
2117 }
2118 fn stream(&self) -> &Arc<CudaStream> {
2119 self.view.stream()
2120 }
2121}
2122
2123impl<T> DevicePtr<T> for PreparedDeviceRead<'_, T> {
2124 fn device_ptr<'a>(&'a self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2125 assert!(
2126 std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2127 "prepared view used on a different stream"
2128 );
2129 (self.view.ptr, SyncOnDrop::Sync(None))
2130 }
2131}
2132
2133impl<T> DeviceSlice<T> for PreparedDeviceWrite<'_, T> {
2134 fn len(&self) -> usize {
2135 self.view.len
2136 }
2137 fn stream(&self) -> &Arc<CudaStream> {
2138 self.view.stream()
2139 }
2140}
2141
2142impl<T> DevicePtr<T> for PreparedDeviceWrite<'_, T> {
2143 fn device_ptr<'a>(&'a self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2144 assert!(
2145 std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2146 "prepared view used on a different stream"
2147 );
2148 (self.view.ptr, SyncOnDrop::Sync(None))
2149 }
2150}
2151
2152impl<T> DevicePtrMut<T> for PreparedDeviceWrite<'_, T> {
2153 fn device_ptr_mut<'a>(&'a mut self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2154 assert!(
2155 std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2156 "prepared view used on a different stream"
2157 );
2158 (self.view.ptr, SyncOnDrop::Sync(None))
2159 }
2160}
2161
2162struct MemoryOperation {
2163 transaction: RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>,
2164 submission: crate::cuda_graph::StreamSubmissionPhase,
2166}
2167
2168impl Drop for MemoryOperation {
2169 fn drop(&mut self) {
2170 if self.transaction.is_prepared() {
2171 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2174 self.transaction.abort_with(
2175 MemoryOperationOwner::synchronize,
2176 MemoryOperationOwner::cancel,
2177 )
2178 }));
2179 }
2180 }
2181}
2182
2183pub(crate) fn admit_memory_access(
2187 stream: Arc<CudaStream>,
2188 accesses: Vec<DeviceMemoryAccess>,
2189 runtime: Option<Arc<XlogDeviceRuntime>>,
2190 runtime_uses: &[crate::device_runtime::BlockUse],
2191 execution_id: u64,
2192) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2193{
2194 let mut ranges_by_context = std::collections::BTreeMap::<usize, Vec<MemoryUse>>::new();
2195 for access in &accesses {
2196 ranges_by_context
2197 .entry(access.context())
2198 .or_default()
2199 .push(access.range);
2200 }
2201 if let Some(runtime) = &runtime {
2202 let context = runtime.device().inner().stream().context().cu_ctx() as usize;
2203 for use_ in runtime_uses {
2204 ranges_by_context
2205 .entry(context)
2206 .or_default()
2207 .push(MemoryUse::new(use_.block.ptr, use_.bytes, use_.access)?);
2208 }
2209 } else if !runtime_uses.is_empty() {
2210 return Err(ResourceError::StreamMisuse(
2211 "recorded blocks require their runtime owner".into(),
2212 ));
2213 }
2214 let ranges = ranges_by_context
2215 .iter()
2216 .flat_map(|(context, ranges)| ranges.iter().map(|range| (*context, *range)))
2217 .collect::<Vec<_>>();
2218 let mut manifest = MemoryAccessManifest {
2219 retained: accesses
2222 .iter()
2223 .map(DeviceMemoryAccess::retained_owner)
2224 .collect(),
2225 accesses,
2226 ranges,
2227 runtimes: runtime.iter().cloned().collect(),
2228 runtime_dependencies: Vec::new(),
2229 runtime_allocations: Vec::new(),
2230 };
2231 let mut registry = block_use_registry()
2232 .lock()
2233 .expect("device block-use registry poisoned");
2234 if let Some(runtime) = &runtime {
2235 for use_ in runtime_uses {
2236 let dependencies = runtime
2237 .allocation_dependencies(use_.block, use_.bytes)?
2238 .ok_or_else(|| {
2239 ResourceError::StreamMisuse(
2240 "recorded block backend does not expose allocation dependencies".into(),
2241 )
2242 })?;
2243 manifest
2244 .runtime_allocations
2245 .push(dependencies.retain_allocation()?);
2246 manifest
2247 .runtime_dependencies
2248 .push((dependencies, use_.access));
2249 }
2250 }
2251 for (context, ranges) in &ranges_by_context {
2252 registry.retain_storage_uses(*context, ranges, &mut manifest.retained)?;
2253 }
2254 let manifest = Arc::new(manifest);
2255 let group = reserve_memory_manifest_locked(&mut registry, &manifest);
2258 drop(registry);
2259 let retained = manifest
2260 .retained
2261 .iter()
2262 .map(|retained| RetainedStorageUse {
2263 owner: Arc::clone(&retained.owner),
2264 access: retained.access,
2265 })
2266 .collect();
2267 memory_operation_transaction(stream, manifest, retained, execution_id, group?)
2268}
2269
2270pub(crate) fn admit_memory_manifest(
2271 stream: Arc<CudaStream>,
2272 manifest: Arc<MemoryAccessManifest>,
2273 execution_id: u64,
2274) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2275{
2276 let mut retained = manifest
2277 .retained
2278 .iter()
2279 .map(|retained| RetainedStorageUse {
2280 owner: Arc::clone(&retained.owner),
2281 access: retained.access,
2282 })
2283 .collect::<Vec<_>>();
2284 let mut registry = block_use_registry()
2285 .lock()
2286 .expect("device block-use registry poisoned");
2287 let mut ranges_by_context = std::collections::BTreeMap::<usize, Vec<MemoryUse>>::new();
2290 for (context, range) in &manifest.ranges {
2291 ranges_by_context.entry(*context).or_default().push(*range);
2292 }
2293 for (context, ranges) in ranges_by_context {
2294 registry.retain_storage_uses(context, &ranges, &mut retained)?;
2295 }
2296 let group = reserve_memory_manifest_locked(&mut registry, &manifest);
2297 drop(registry);
2298 memory_operation_transaction(stream, manifest, retained, execution_id, group?)
2299}
2300
2301fn reserve_memory_manifest_locked(
2302 registry: &mut BlockUseRegistry,
2303 manifest: &MemoryAccessManifest,
2304) -> crate::device_runtime::ResourceResult<MemoryUseGroup> {
2305 let source_proofs = manifest
2306 .accesses
2307 .iter()
2308 .map(|access| access.storage.dependencies.reclamation.release_proof())
2309 .chain(
2310 manifest
2311 .runtime_dependencies
2312 .iter()
2313 .map(|(deps, _)| deps.reclamation.release_proof()),
2314 )
2315 .collect::<Vec<_>>();
2316 registry.reserve_owned_memory_uses(&manifest.ranges, &source_proofs)
2317}
2318
2319fn memory_operation_transaction(
2320 stream: Arc<CudaStream>,
2321 manifest: Arc<MemoryAccessManifest>,
2322 retained: Vec<RetainedStorageUse>,
2323 execution_id: u64,
2324 group: MemoryUseGroup,
2325) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2326{
2327 let owner = Arc::new(MemoryOperationOwner {
2328 stream,
2329 manifest,
2330 retained,
2331 captured: std::sync::atomic::AtomicBool::new(false),
2332 dependencies: std::sync::OnceLock::new(),
2333 completion: Arc::new(OperationCompletion::new(execution_id)),
2334 });
2335 Ok(RecorderTransaction::from_admitted(owner, Box::new([group])))
2336}
2337
2338pub(crate) fn with_memory_access<R>(
2339 stream: Arc<CudaStream>,
2340 accesses: Vec<DeviceMemoryAccess>,
2341 operation: impl FnOnce(&MemoryEnqueue<'_>) -> crate::device_runtime::ResourceResult<R>,
2342) -> crate::device_runtime::ResourceResult<R> {
2343 let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream)?;
2344 let execution_id = submission.execution_id();
2345 let guard = MemoryOperation {
2346 transaction: admit_memory_access(stream, accesses, None, &[], execution_id)?,
2347 submission,
2348 };
2349 with_memory_operation(guard, |owner, submission| {
2350 operation(&MemoryEnqueue { owner, submission })
2351 })
2352}
2353
2354pub(crate) fn with_memory_manifest<T>(
2355 stream: Arc<CudaStream>,
2356 manifest: Arc<MemoryAccessManifest>,
2357 operation: impl FnOnce(&crate::launch::CudaEnqueue<'_>) -> crate::device_runtime::ResourceResult<T>,
2358) -> crate::device_runtime::ResourceResult<T> {
2359 let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream)?;
2360 let execution_id = submission.execution_id();
2361 let guard = MemoryOperation {
2362 transaction: admit_memory_manifest(stream, manifest, execution_id)?,
2363 submission,
2364 };
2365 with_memory_operation(guard, |owner, pin| {
2366 operation(&crate::launch::CudaEnqueue::from_admission(owner, pin)?)
2367 })
2368}
2369
2370fn with_memory_operation<T>(
2371 mut guard: MemoryOperation,
2372 operation: impl FnOnce(
2373 &Arc<MemoryOperationOwner>,
2374 &crate::cuda_graph::StreamSubmissionPin<'_>,
2375 ) -> crate::device_runtime::ResourceResult<T>,
2376) -> crate::device_runtime::ResourceResult<T> {
2377 let owner = Arc::clone(
2378 guard
2379 .transaction
2380 .prepared_owner()
2381 .expect("memory operation admitted"),
2382 );
2383 let mut result = None;
2384 guard
2385 .transaction
2386 .enqueue_operation_with(
2387 || {
2388 let pin = guard.submission.pin()?;
2389 owner.bind_submission(&pin)?;
2390 Ok(pin)
2391 },
2392 MemoryOperationOwner::prepare,
2393 |pin| {
2394 result = Some(pin.with_serialized_submission(|| operation(&owner, pin))?);
2395 Ok::<_, ResourceError>(())
2396 },
2397 MemoryOperationOwner::synchronize,
2398 MemoryOperationOwner::cancel,
2399 )
2400 .map_err(|error| ResourceError::Driver(error.to_string()))?;
2401 guard.transaction.commit_with(
2402 MemoryOperationOwner::finish,
2403 MemoryOperationOwner::synchronize,
2404 MemoryOperationOwner::cancel,
2405 )?;
2406 Ok(result.expect("successful memory operation produced its result"))
2407}
2408
2409#[derive(Clone)]
2410pub(crate) struct RuntimeAllocationIdentity {
2411 pub(crate) manager_id: usize,
2412 pub(crate) allocation_ptr: u64,
2413 pub(crate) allocation_bytes: usize,
2414 pub(crate) block_id: BlockId,
2415 pub(crate) block_bytes: usize,
2416 pub(crate) block_state: BlockState,
2417 pub(crate) context: Arc<cudarc::driver::CudaContext>,
2418}
2419
2420impl<T: cudarc::driver::DeviceRepr> DeviceSlice<T> for TrackedCudaSlice<T> {
2421 fn len(&self) -> usize {
2422 self.len
2423 }
2424
2425 fn stream(&self) -> &Arc<CudaStream> {
2426 &self.storage.stream
2427 }
2428}
2429
2430impl<T: cudarc::driver::DeviceRepr> TrackedCudaSlice<T> {
2431 pub fn len(&self) -> usize {
2432 self.len
2433 }
2434
2435 pub fn is_empty(&self) -> bool {
2436 self.len == 0
2437 }
2438
2439 pub fn stream(&self) -> &Arc<CudaStream> {
2440 &self.storage.stream
2441 }
2442
2443 pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
2444 &self.ptr
2445 }
2446
2447 pub fn device_ptr_value(&self) -> cudarc::driver::sys::CUdeviceptr {
2448 self.ptr
2449 }
2450
2451 pub fn view(&self) -> DeviceMemoryView<T> {
2453 DeviceMemoryView {
2454 ptr: self.ptr,
2455 len: self.len,
2456 storage: Arc::clone(&self.storage),
2457 element: std::marker::PhantomData,
2458 }
2459 }
2460
2461 pub(crate) fn try_owned_subslice<U: cudarc::driver::DeviceRepr>(
2467 &self,
2468 byte_range: std::ops::Range<usize>,
2469 ) -> Option<TrackedCudaSlice<U>> {
2470 let bytes = self.view().try_slice(byte_range)?;
2471 let typed = unsafe { bytes.cast::<U>()? };
2475 Some(TrackedCudaSlice {
2476 storage: Arc::clone(&typed.storage),
2477 ptr: typed.ptr,
2478 len: typed.len,
2479 element: std::marker::PhantomData,
2480 })
2481 }
2482
2483 pub fn try_slice(
2484 &self,
2485 range: impl std::ops::RangeBounds<usize>,
2486 ) -> Option<DeviceMemoryView<T>> {
2487 self.view().try_slice(range)
2488 }
2489
2490 pub fn slice(&self, range: impl std::ops::RangeBounds<usize>) -> DeviceMemoryView<T> {
2491 self.try_slice(range)
2492 .expect("device slice range is out of bounds")
2493 }
2494
2495 pub fn slice_mut(&mut self, range: impl std::ops::RangeBounds<usize>) -> DeviceMemoryView<T> {
2496 self.slice(range)
2497 }
2498
2499 pub fn memory_manager_ptr_value(&self) -> usize {
2501 Arc::as_ptr(
2502 self.storage
2503 .manager()
2504 .expect("tracked allocation has a memory manager"),
2505 ) as usize
2506 }
2507
2508 pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
2509 let Some(block) = self.runtime_block() else {
2510 return Ok(None);
2511 };
2512 let allocation_bytes = self
2513 .len()
2514 .checked_mul(std::mem::size_of::<T>())
2515 .ok_or_else(|| XlogError::Kernel("runtime allocation byte size overflow".into()))?;
2516 Ok(Some(RuntimeAllocationIdentity {
2517 manager_id: self.memory_manager_ptr_value(),
2518 allocation_ptr: self.device_ptr_value(),
2519 allocation_bytes,
2520 block_id: BlockId::from_block(block),
2521 block_bytes: block.bytes,
2522 block_state: block.state,
2523 context: Arc::clone(DeviceSlice::stream(self).context()),
2524 }))
2525 }
2526
2527 pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
2530 match &*self.storage.backing {
2531 Backing::Native(_) | Backing::Foreign { .. } => None,
2532 Backing::Runtime(allocation) => Some(allocation.device_block()),
2533 }
2534 }
2535
2536 pub fn into_bytes(self) -> TrackedCudaSlice<u8> {
2545 let len = self
2546 .len
2547 .checked_mul(std::mem::size_of::<T>())
2548 .expect("tracked slice byte size must fit into usize");
2549 TrackedCudaSlice {
2550 storage: self.storage,
2551 ptr: self.ptr,
2552 len,
2553 element: std::marker::PhantomData,
2554 }
2555 }
2556}
2557
2558impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &TrackedCudaSlice<T> {
2559 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
2560 ((*self).device_ptr() as *const cudarc::driver::sys::CUdeviceptr)
2561 .cast_mut()
2562 .cast()
2563 }
2564}
2565
2566impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &mut TrackedCudaSlice<T> {
2567 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
2568 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
2569 .cast_mut()
2570 .cast()
2571 }
2572}
2573
2574impl<'a, T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &'a TrackedCudaSlice<T> {
2575 type Storage = DeviceParamStorage<'a>;
2576
2577 fn into_kernel_param_storage(self) -> Self::Storage {
2578 DeviceParamStorage::unsynced(self.ptr)
2579 }
2580}
2581
2582impl<T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &mut TrackedCudaSlice<T> {
2583 type Storage = DeviceParamStorage<'static>;
2584
2585 fn into_kernel_param_storage(self) -> Self::Storage {
2586 DeviceParamStorage::unsynced(self.ptr)
2587 }
2588}
2589
2590impl GpuMemoryManager {
2591 pub(crate) fn new(device: Arc<CudaDevice>, budget: MemoryBudget) -> Self {
2597 Self {
2598 device,
2599 budget,
2600 accounting: Arc::new(GpuMemoryAccounting::default()),
2601 runtime: None,
2602 #[cfg(test)]
2603 after_local_reservation_hook: std::sync::Mutex::new(None),
2604 }
2605 }
2606
2607 pub(crate) fn with_runtime(
2619 device: Arc<CudaDevice>,
2620 budget: MemoryBudget,
2621 runtime: Arc<XlogDeviceRuntime>,
2622 ) -> Self {
2623 Self {
2624 device,
2625 budget,
2626 accounting: Arc::new(GpuMemoryAccounting::default()),
2627 runtime: Some(runtime),
2628 #[cfg(test)]
2629 after_local_reservation_hook: std::sync::Mutex::new(None),
2630 }
2631 }
2632
2633 pub fn reserve_bytes(self: &Arc<Self>, bytes: u64) -> Result<GpuMemoryReservation> {
2642 let runtime_reservation = match &self.runtime {
2643 Some(runtime) => {
2644 let bytes_usize = usize::try_from(bytes).map_err(|_| {
2645 XlogError::Kernel(format!(
2646 "GPU reservation size {} bytes exceeds platform usize",
2647 bytes
2648 ))
2649 })?;
2650 Some(runtime.reserve_memory(bytes_usize).map_err(|error| {
2651 map_resource_error(error, self.accounting.peak.load(Ordering::SeqCst))
2652 })?)
2653 }
2654 None => None,
2655 };
2656 self.reserve_local_bytes(bytes, "manager_reserve")?;
2657 Ok(GpuMemoryReservation {
2658 manager: Arc::clone(self),
2659 runtime_reservation,
2660 total_bytes: bytes,
2661 remaining_bytes: bytes,
2662 })
2663 }
2664
2665 fn reserve_local_bytes(&self, bytes: u64, layer: &'static str) -> Result<()> {
2666 let _mutation = self
2667 .accounting
2668 .mutation_lock
2669 .lock()
2670 .expect("GPU memory accounting poisoned");
2671 loop {
2672 let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
2673 let required = current as u128 + bytes as u128;
2674 if required > self.budget.device_bytes as u128 {
2675 return Err(MemoryPressure {
2676 layer,
2677 current_bytes: current as u128,
2678 requested_bytes: bytes as u128,
2679 budget_bytes: self.budget.device_bytes,
2680 prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
2681 }
2682 .into_error());
2683 }
2684 if self
2685 .accounting
2686 .budget_reserved
2687 .compare_exchange(current, required as u64, Ordering::SeqCst, Ordering::SeqCst)
2688 .is_ok()
2689 {
2690 return Ok(());
2691 }
2692 }
2693 }
2694
2695 pub fn runtime(&self) -> Option<&Arc<XlogDeviceRuntime>> {
2704 self.runtime.as_ref()
2705 }
2706
2707 pub fn reap_pending_deallocations(&self) -> Result<()> {
2713 let Some(runtime) = self.runtime.as_ref() else {
2714 crate::cuda_graph::reap_capture_retirements();
2715 return Ok(());
2716 };
2717 runtime
2718 .reap_pending()
2719 .map_err(|error| map_resource_error(error, self.peak_bytes()))
2720 }
2721
2722 fn rollback_local_reservation(&self, bytes: u64) -> Result<()> {
2726 self.accounting.release_reserved(bytes)
2727 }
2728
2729 fn record_deallocation_failure(&self, bytes: u64) {
2730 let _ = self.accounting.deallocation_failure_count.try_update(
2731 Ordering::SeqCst,
2732 Ordering::SeqCst,
2733 |current| Some(current.saturating_add(1)),
2734 );
2735 let _ = self.accounting.deallocation_failure_bytes.try_update(
2736 Ordering::SeqCst,
2737 Ordering::SeqCst,
2738 |current| Some(current.saturating_add(bytes)),
2739 );
2740 }
2741
2742 pub fn alloc<T: cudarc::driver::DeviceRepr>(
2746 self: &Arc<Self>,
2747 len: usize,
2748 ) -> Result<TrackedCudaSlice<T>> {
2749 self.accounting.alloc_count.fetch_add(1, Ordering::Relaxed);
2751
2752 let bytes = (len as u64)
2754 .checked_mul(std::mem::size_of::<T>() as u64)
2755 .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
2756
2757 self.reserve_local_bytes(bytes, "manager_alloc")?;
2758 let attempt = LocalAllocationAttempt::new(Arc::clone(&self.accounting), bytes, None);
2759 self.alloc_after_local_reservation::<T>(len, bytes, None, Arc::clone(&attempt.reclamation))
2760 .map_err(|error| map_resource_error(error, self.peak_bytes()))
2761 }
2762
2763 fn alloc_after_local_reservation<T: cudarc::driver::DeviceRepr>(
2764 self: &Arc<Self>,
2765 len: usize,
2766 bytes: u64,
2767 runtime_reservation: Option<&mut RuntimeMemoryReservation>,
2768 reclamation: Arc<AllocationReclamation>,
2769 ) -> crate::device_runtime::ResourceResult<TrackedCudaSlice<T>> {
2770 let extent = usize::try_from(bytes)
2771 .map_err(|_| ResourceError::Driver("allocation size exceeds platform usize".into()))?;
2772 if self.runtime.is_some() && bytes != 0 {
2773 let allocation = self.alloc_raw_after_local_reservation(
2774 extent,
2775 bytes,
2776 AllocTag::UNTAGGED,
2777 runtime_reservation,
2778 reclamation,
2779 )?;
2780 let raw_ptr = allocation.device_block().ptr;
2781 return Ok(TrackedCudaSlice {
2782 storage: DeviceStorage::new(
2783 Backing::Runtime(allocation),
2784 Arc::clone(self.device.inner().stream()),
2785 raw_ptr,
2786 ),
2787 ptr: raw_ptr,
2788 len,
2789 element: std::marker::PhantomData,
2790 });
2791 }
2792
2793 #[cfg(test)]
2794 self.run_after_local_reservation_hook(bytes);
2795 let allocation = RawDeviceAllocation::allocate(
2796 Arc::clone(self.device.inner().stream()),
2797 Arc::clone(self.device.inner().allocation_stream()),
2798 extent,
2799 Some(Arc::clone(self)),
2800 reclamation,
2801 )?;
2802 let raw_ptr = allocation.ptr();
2803 Ok(TrackedCudaSlice {
2804 storage: DeviceStorage::new(
2805 Backing::Native(allocation),
2806 Arc::clone(self.device.inner().stream()),
2807 raw_ptr,
2808 ),
2809 ptr: raw_ptr,
2810 len,
2811 element: std::marker::PhantomData,
2812 })
2813 }
2814
2815 pub fn check_budget(&self, bytes: u64) -> Result<()> {
2826 let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
2829 let required = current as u128 + bytes as u128;
2830
2831 if required > self.budget.device_bytes as u128 {
2832 return Err(MemoryPressure {
2833 layer: "manager_check_budget",
2834 current_bytes: current as u128,
2835 requested_bytes: bytes as u128,
2836 budget_bytes: self.budget.device_bytes,
2837 prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
2838 }
2839 .into_error());
2840 }
2841
2842 Ok(())
2843 }
2844
2845 pub fn allocated_bytes(&self) -> u64 {
2847 self.accounting.allocated.load(Ordering::SeqCst)
2848 }
2849
2850 pub fn deallocation_failure_count(&self) -> u64 {
2853 self.accounting
2854 .deallocation_failure_count
2855 .load(Ordering::SeqCst)
2856 }
2857
2858 pub fn deallocation_failure_bytes(&self) -> u64 {
2861 self.accounting
2862 .deallocation_failure_bytes
2863 .load(Ordering::SeqCst)
2864 }
2865
2866 pub fn peak_bytes(&self) -> u64 {
2870 self.accounting.peak.load(Ordering::SeqCst)
2871 }
2872
2873 pub fn reset_peak(&self) {
2877 self.accounting.peak.store(
2878 self.accounting.allocated.load(Ordering::SeqCst),
2879 Ordering::SeqCst,
2880 );
2881 }
2882
2883 pub fn alloc_count(&self) -> u64 {
2887 self.accounting.alloc_count.load(Ordering::Relaxed)
2888 }
2889
2890 pub fn reset_alloc_count(&self) {
2892 self.accounting.alloc_count.store(0, Ordering::Relaxed);
2893 }
2894
2895 pub fn budget(&self) -> &MemoryBudget {
2897 &self.budget
2898 }
2899
2900 pub fn budget_limit_bytes(&self) -> u64 {
2902 self.budget.device_bytes
2903 }
2904
2905 pub fn device(&self) -> &Arc<CudaDevice> {
2907 &self.device
2908 }
2909
2910 pub fn record_free(&self, bytes: u64) -> Result<()> {
2917 let _mutation = self
2918 .accounting
2919 .mutation_lock
2920 .lock()
2921 .expect("GPU memory accounting poisoned");
2922 let admitted = self.accounting.allocated.load(Ordering::SeqCst);
2923 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
2924 if admitted != 0 || reserved != 0 {
2925 return Err(XlogError::Kernel(format!(
2926 "manual GPU accounting release refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
2927 admitted, reserved
2928 )));
2929 }
2930 if bytes != 0 {
2931 return Err(XlogError::Kernel(format!(
2932 "manual GPU accounting release underflow: current_bytes=0 requested_bytes={}",
2933 bytes
2934 )));
2935 }
2936 Ok(())
2937 }
2938
2939 pub fn alloc_raw(self: &Arc<Self>, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
2961 if self.runtime.is_none() {
2962 return Err(XlogError::Kernel(
2963 "GpuMemoryManager::alloc_raw called without an attached XlogDeviceRuntime; \
2964 construct via with_runtime to enable runtime routing"
2965 .to_string(),
2966 ));
2967 }
2968 let bytes_u64 = u64::try_from(bytes)
2969 .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
2970 self.reserve_local_bytes(bytes_u64, "manager_alloc_raw")?;
2971 let attempt = LocalAllocationAttempt::new(Arc::clone(&self.accounting), bytes_u64, None);
2972 self.alloc_raw_after_local_reservation(
2973 bytes,
2974 bytes_u64,
2975 tag,
2976 None,
2977 Arc::clone(&attempt.reclamation),
2978 )
2979 .map_err(|error| map_resource_error(error, self.peak_bytes()))
2980 }
2981
2982 fn alloc_raw_after_local_reservation(
2983 self: &Arc<Self>,
2984 bytes: usize,
2985 bytes_u64: u64,
2986 tag: AllocTag,
2987 runtime_reservation: Option<&mut RuntimeMemoryReservation>,
2988 reclamation: Arc<AllocationReclamation>,
2989 ) -> crate::device_runtime::ResourceResult<RuntimeAllocBlock> {
2990 let runtime = self.runtime.as_ref().ok_or_else(|| {
2991 ResourceError::Driver("raw allocation requires an attached device runtime".into())
2992 })?;
2993 #[cfg(test)]
2994 self.run_after_local_reservation_hook(bytes_u64);
2995 let request = AllocationRequest {
2996 bytes,
2997 stream: StreamId::DEFAULT,
2998 tag,
2999 reservation_pressure_bytes: 0,
3000 reclamation: Arc::clone(&reclamation),
3001 };
3002 let allocation = match runtime_reservation {
3003 Some(reservation) => reservation.materialize(request),
3004 None => runtime.materialize(request),
3005 };
3006 match allocation {
3007 Ok(block) => {
3008 let owner = ResourceBlockRetirement::new(
3010 block,
3011 runtime.retirement_resource(),
3012 Arc::clone(&reclamation),
3013 );
3014 let bind_charge: crate::device_runtime::ResourceResult<_> = (|| {
3015 let block = owner.block();
3016 let dependencies =
3017 runtime
3018 .allocation_dependencies(BlockId::from_block(block), block.bytes)?
3019 .ok_or_else(|| {
3020 ResourceError::Driver(
3021 "runtime allocation does not provide physical release ownership".into())
3022 })?;
3023 if !Arc::ptr_eq(&dependencies.reclamation, &reclamation) {
3024 return Err(ResourceError::Driver(
3025 "runtime allocation replaced its accounting owner".into(),
3026 ));
3027 }
3028 let allocation = dependencies.retain_allocation()?;
3029 Ok((dependencies, allocation))
3030 })();
3031 let (dependencies, allocation) = match bind_charge {
3032 Ok(owners) => owners,
3033 Err(error) => {
3034 return Err(error.retaining(bytes, reclamation));
3035 }
3036 };
3037 let block = owner.into_block();
3038 Ok(RuntimeAllocBlock {
3039 bytes: bytes_u64,
3040 manager: Arc::clone(self),
3041 runtime: Arc::clone(runtime),
3042 block: Some(block),
3043 dependencies,
3044 allocation: Some(allocation),
3045 })
3046 }
3047 Err(error) => Err(error),
3048 }
3049 }
3050
3051 pub fn remaining_bytes(&self) -> u64 {
3053 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
3054 self.budget.device_bytes.saturating_sub(reserved)
3055 }
3056
3057 pub fn reset_tracking(&self) -> Result<()> {
3059 let _mutation = self
3060 .accounting
3061 .mutation_lock
3062 .lock()
3063 .expect("GPU memory accounting poisoned");
3064 let admitted = self.accounting.allocated.load(Ordering::SeqCst);
3065 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
3066 if admitted != 0 || reserved != 0 {
3067 return Err(XlogError::Kernel(format!(
3068 "GPU accounting reset refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
3069 admitted, reserved
3070 )));
3071 }
3072 self.accounting.peak.store(0, Ordering::SeqCst);
3073 Ok(())
3074 }
3075
3076 #[cfg(test)]
3077 fn run_after_local_reservation_hook(&self, bytes: u64) {
3078 let hook = self
3079 .after_local_reservation_hook
3080 .lock()
3081 .expect("after-local-reservation test hook poisoned")
3082 .clone();
3083 if let Some(hook) = hook {
3084 hook(bytes);
3085 }
3086 }
3087}
3088
3089fn map_resource_error(e: ResourceError, prior_peak_bytes: u64) -> XlogError {
3090 match e {
3091 error @ ResourceError::AllocationRetained(_) => XlogError::Kernel(error.to_string()),
3092 ResourceError::OutOfBudget {
3093 requested,
3094 current,
3095 limit,
3096 ..
3097 } => MemoryPressure {
3098 layer: "device_runtime",
3099 current_bytes: current as u128,
3100 requested_bytes: requested as u128,
3101 budget_bytes: u64::try_from(limit).unwrap_or(u64::MAX),
3102 prior_peak_bytes,
3103 }
3104 .into_error(),
3105 ResourceError::Driver(msg) => XlogError::Kernel(format!("device-runtime driver: {}", msg)),
3106 ResourceError::StreamMisuse(msg) => {
3107 XlogError::Kernel(format!("device-runtime stream misuse: {}", msg))
3108 }
3109 ResourceError::UseAfterFree { generation } => XlogError::Kernel(format!(
3110 "device-runtime use-after-free on generation {:?}",
3111 generation
3112 )),
3113 ResourceError::OutOfBounds { generation } => XlogError::Kernel(format!(
3114 "device-runtime out-of-bounds on generation {:?}",
3115 generation
3116 )),
3117 }
3118}
3119
3120pub struct RuntimeAllocBlock {
3125 bytes: u64,
3126 manager: Arc<GpuMemoryManager>,
3127 runtime: Arc<XlogDeviceRuntime>,
3128 dependencies: Arc<DeviceAccessDependencies>,
3129 allocation: Option<Arc<RawDeviceAllocation>>,
3130 block: Option<DeviceBlock>,
3134}
3135
3136fn allocation_reclamation_outcome(
3137 physically_released: bool,
3138 queue_result: crate::device_runtime::ResourceResult<()>,
3139) -> crate::device_runtime::ResourceResult<()> {
3140 if physically_released {
3141 return Ok(());
3142 }
3143 queue_result?;
3144 Err(ResourceError::Driver(
3145 "allocation reclamation is still owned by another pending reaper".into(),
3146 ))
3147}
3148
3149impl RuntimeAllocBlock {
3150 fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
3151 let Some(block) = self.block.take() else {
3155 return allocation_reclamation_outcome(
3156 self.dependencies.allocation_was_released(),
3157 Ok(()),
3158 );
3159 };
3160 drop(self.allocation.take());
3163 let mut retirement = ResourceBlockRetirement::new(
3164 block,
3165 self.runtime.retirement_resource(),
3166 Arc::clone(&self.dependencies.reclamation),
3167 );
3168 let queue_result = retirement.release();
3169 match allocation_reclamation_outcome(
3170 self.dependencies.allocation_was_released(),
3171 queue_result,
3172 ) {
3173 Ok(()) => Ok(()),
3174 Err(error) => {
3175 self.manager.record_deallocation_failure(self.bytes);
3176 Err(error)
3177 }
3178 }
3179 }
3180
3181 pub fn ptr(&self) -> u64 {
3184 self.block
3185 .as_ref()
3186 .expect("RuntimeAllocBlock used after drop")
3187 .ptr
3188 }
3189
3190 pub fn bytes(&self) -> usize {
3192 self.bytes as usize
3193 }
3194
3195 pub fn device_block(&self) -> &DeviceBlock {
3198 self.block
3199 .as_ref()
3200 .expect("RuntimeAllocBlock used after drop")
3201 }
3202}
3203
3204impl std::fmt::Debug for RuntimeAllocBlock {
3205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3206 let mut dbg = f.debug_struct("RuntimeAllocBlock");
3207 dbg.field("bytes", &self.bytes);
3208 match &self.block {
3209 Some(b) => {
3210 dbg.field("ptr", &format_args!("{:#x}", b.ptr));
3211 dbg.field("device_ordinal", &b.device_ordinal);
3212 dbg.field("alloc_stream", &b.alloc_stream);
3213 dbg.field("tag", &b.tag);
3214 dbg.field("generation", &b.generation);
3215 dbg.field("state", &b.state);
3216 }
3217 None => {
3218 dbg.field("block", &"<dropped>");
3219 }
3220 }
3221 dbg.finish()
3222 }
3223}
3224
3225impl Drop for RuntimeAllocBlock {
3226 fn drop(&mut self) {
3227 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.release()));
3228 }
3229}
3230
3231pub enum CudaColumn {
3237 Owned(TrackedCudaSlice<u8>),
3238 Dlpack(DlpackColumn),
3239 ArrowDevice(ArrowDeviceColumn),
3240}
3241
3242pub struct DlpackColumn {
3243 ptr: cudarc::driver::sys::CUdeviceptr,
3244 len_bytes: usize,
3245 stream: Arc<CudaStream>,
3246 storage: Arc<DeviceStorage>,
3247 source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
3261}
3262
3263pub struct ArrowDeviceColumn {
3264 ptr: cudarc::driver::sys::CUdeviceptr,
3265 len_bytes: usize,
3266 stream: Arc<CudaStream>,
3267 storage: Arc<DeviceStorage>,
3268 source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
3272}
3273
3274impl CudaColumn {
3275 pub fn owned(slice: TrackedCudaSlice<u8>) -> Self {
3276 Self::Owned(slice)
3277 }
3278
3279 pub unsafe fn dlpack(
3286 ptr: cudarc::driver::sys::CUdeviceptr,
3287 len_bytes: usize,
3288 stream: Arc<CudaStream>,
3289 tensor: DlpackManagedTensor,
3290 ) -> Self {
3291 let storage = DeviceStorage::new(
3292 Backing::Foreign {
3293 _owner: Box::new(tensor),
3294 _source: None,
3295 bytes: len_bytes as u64,
3296 },
3297 Arc::clone(&stream),
3298 ptr,
3299 );
3300 Self::Dlpack(DlpackColumn {
3301 ptr,
3302 len_bytes,
3303 stream,
3304 storage,
3305 source_slice: None,
3306 })
3307 }
3308
3309 pub fn dlpack_xlog_owned(
3326 source_slice: Arc<TrackedCudaSlice<u8>>,
3327 stream: Arc<CudaStream>,
3328 tensor: DlpackManagedTensor,
3329 ) -> Self {
3330 let ptr = *source_slice.device_ptr();
3331 let len_bytes = source_slice.len();
3332 assert_eq!(
3333 source_slice.stream().context().cu_ctx(),
3334 stream.context().cu_ctx(),
3335 "import stream must belong to the source allocation context"
3336 );
3337 let storage = DeviceStorage::new(
3338 Backing::Foreign {
3339 _owner: Box::new(tensor),
3340 _source: Some(Arc::clone(&source_slice)),
3341 bytes: len_bytes as u64,
3342 },
3343 Arc::clone(&stream),
3344 ptr,
3345 );
3346 Self::Dlpack(DlpackColumn {
3347 ptr,
3348 len_bytes,
3349 stream,
3350 storage,
3351 source_slice: Some(source_slice),
3352 })
3353 }
3354
3355 pub unsafe fn arrow_device(
3362 ptr: cudarc::driver::sys::CUdeviceptr,
3363 len_bytes: usize,
3364 stream: Arc<CudaStream>,
3365 import: Arc<ArrowDeviceImport>,
3366 ) -> Self {
3367 let storage = DeviceStorage::new(
3368 Backing::Foreign {
3369 _owner: Box::new(import),
3370 _source: None,
3371 bytes: len_bytes as u64,
3372 },
3373 Arc::clone(&stream),
3374 ptr,
3375 );
3376 Self::ArrowDevice(ArrowDeviceColumn {
3377 ptr,
3378 len_bytes,
3379 stream,
3380 storage,
3381 source_slice: None,
3382 })
3383 }
3384
3385 pub fn arrow_device_xlog_owned(
3395 source_slice: Arc<TrackedCudaSlice<u8>>,
3396 stream: Arc<CudaStream>,
3397 import: Arc<ArrowDeviceImport>,
3398 ) -> Self {
3399 let ptr = *source_slice.device_ptr();
3400 let len_bytes = source_slice.len();
3401 assert_eq!(
3402 source_slice.stream().context().cu_ctx(),
3403 stream.context().cu_ctx(),
3404 "import stream must belong to the source allocation context"
3405 );
3406 let storage = DeviceStorage::new(
3407 Backing::Foreign {
3408 _owner: Box::new(import),
3409 _source: Some(Arc::clone(&source_slice)),
3410 bytes: len_bytes as u64,
3411 },
3412 Arc::clone(&stream),
3413 ptr,
3414 );
3415 Self::ArrowDevice(ArrowDeviceColumn {
3416 ptr,
3417 len_bytes,
3418 stream,
3419 storage,
3420 source_slice: Some(source_slice),
3421 })
3422 }
3423
3424 pub fn stream(&self) -> &Arc<CudaStream> {
3425 match self {
3426 CudaColumn::Owned(slice) => slice.stream(),
3427 CudaColumn::Dlpack(col) => &col.stream,
3428 CudaColumn::ArrowDevice(col) => &col.stream,
3429 }
3430 }
3431
3432 pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
3433 match self {
3434 CudaColumn::Owned(slice) => slice.device_ptr(),
3435 CudaColumn::Dlpack(col) => &col.ptr,
3436 CudaColumn::ArrowDevice(col) => &col.ptr,
3437 }
3438 }
3439
3440 pub fn memory_manager_ptr_value(&self) -> Option<usize> {
3445 match self {
3446 CudaColumn::Owned(slice) => Some(slice.memory_manager_ptr_value()),
3447 CudaColumn::Dlpack(col) => col
3448 .source_slice
3449 .as_ref()
3450 .map(|slice| slice.memory_manager_ptr_value()),
3451 CudaColumn::ArrowDevice(col) => col
3452 .source_slice
3453 .as_ref()
3454 .map(|slice| slice.memory_manager_ptr_value()),
3455 }
3456 }
3457
3458 pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
3459 match self {
3460 CudaColumn::Owned(slice) => slice.runtime_allocation_identity(),
3461 CudaColumn::Dlpack(col) => {
3462 let Some(source) = &col.source_slice else {
3463 return Ok(None);
3464 };
3465 let mut identity = source.runtime_allocation_identity()?;
3466 if let Some(identity) = &mut identity {
3467 identity.allocation_ptr = col.ptr;
3468 identity.allocation_bytes = col.len_bytes;
3469 }
3470 Ok(identity)
3471 }
3472 CudaColumn::ArrowDevice(col) => {
3473 let Some(source) = &col.source_slice else {
3474 return Ok(None);
3475 };
3476 let mut identity = source.runtime_allocation_identity()?;
3477 if let Some(identity) = &mut identity {
3478 identity.allocation_ptr = col.ptr;
3479 identity.allocation_bytes = col.len_bytes;
3480 }
3481 Ok(identity)
3482 }
3483 }
3484 }
3485
3486 pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
3501 match self {
3502 CudaColumn::Owned(slice) => slice.runtime_block(),
3503 CudaColumn::Dlpack(col) => col.source_slice.as_ref().and_then(|s| s.runtime_block()),
3504 CudaColumn::ArrowDevice(col) => {
3505 col.source_slice.as_ref().and_then(|s| s.runtime_block())
3506 }
3507 }
3508 }
3509
3510 pub fn is_external(&self) -> bool {
3525 match self {
3526 CudaColumn::Owned(_) => false,
3527 CudaColumn::Dlpack(col) => col.source_slice.is_none(),
3528 CudaColumn::ArrowDevice(col) => col.source_slice.is_none(),
3529 }
3530 }
3531}
3532
3533impl From<TrackedCudaSlice<u8>> for CudaColumn {
3534 fn from(value: TrackedCudaSlice<u8>) -> Self {
3535 CudaColumn::Owned(value)
3536 }
3537}
3538
3539impl DeviceSlice<u8> for CudaColumn {
3540 fn len(&self) -> usize {
3541 match self {
3542 CudaColumn::Owned(slice) => slice.len(),
3543 CudaColumn::Dlpack(col) => col.len_bytes,
3544 CudaColumn::ArrowDevice(col) => col.len_bytes,
3545 }
3546 }
3547
3548 fn stream(&self) -> &Arc<CudaStream> {
3549 self.stream()
3550 }
3551}
3552
3553impl private_access::Sealed for CudaColumn {}
3554
3555impl DeviceRead<u8> for CudaColumn {
3556 fn device_view(&self) -> DeviceMemoryView<u8> {
3557 match self {
3558 CudaColumn::Owned(slice) => slice.view(),
3559 CudaColumn::Dlpack(col) => DeviceMemoryView {
3560 ptr: col.ptr,
3561 len: col.len_bytes,
3562 storage: Arc::clone(&col.storage),
3563 element: std::marker::PhantomData,
3564 },
3565 CudaColumn::ArrowDevice(col) => DeviceMemoryView {
3566 ptr: col.ptr,
3567 len: col.len_bytes,
3568 storage: Arc::clone(&col.storage),
3569 element: std::marker::PhantomData,
3570 },
3571 }
3572 }
3573}
3574
3575impl DeviceWrite<u8> for CudaColumn {}
3576
3577impl AsKernelParam for &CudaColumn {
3578 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
3579 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
3580 .cast_mut()
3581 .cast()
3582 }
3583}
3584
3585impl AsKernelParam for &mut CudaColumn {
3586 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
3587 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
3588 .cast_mut()
3589 .cast()
3590 }
3591}
3592
3593impl<'a> IntoKernelParamStorage for &'a CudaColumn {
3594 type Storage = DeviceParamStorage<'a>;
3595
3596 fn into_kernel_param_storage(self) -> Self::Storage {
3597 match self {
3598 CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
3599 CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
3600 CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
3601 }
3602 }
3603}
3604
3605impl<'a> IntoKernelParamStorage for &'a mut CudaColumn {
3606 type Storage = DeviceParamStorage<'a>;
3607
3608 fn into_kernel_param_storage(self) -> Self::Storage {
3609 match self {
3610 CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
3611 CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
3612 CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
3613 }
3614 }
3615}
3616
3617pub struct CudaBuffer {
3622 pub(crate) columns: Vec<CudaColumn>,
3624 pub(crate) row_cap: u64,
3626 pub(crate) d_num_rows: TrackedCudaSlice<u32>,
3628 pub(crate) schema: Schema,
3630 cached_row_count: AtomicU32,
3634 canonical_full_row_set_certified: bool,
3637}
3638
3639impl CudaBuffer {
3640 pub fn from_columns(
3651 columns: Vec<CudaColumn>,
3652 row_cap: u64,
3653 d_num_rows: TrackedCudaSlice<u32>,
3654 schema: Schema,
3655 ) -> Self {
3656 assert_eq!(
3657 columns.len(),
3658 schema.arity(),
3659 "Number of columns ({}) must match schema arity ({})",
3660 columns.len(),
3661 schema.arity()
3662 );
3663 Self {
3664 columns,
3665 row_cap,
3666 d_num_rows,
3667 schema,
3668 cached_row_count: AtomicU32::new(u32::MAX),
3669 canonical_full_row_set_certified: false,
3670 }
3671 }
3672
3673 pub fn from_columns_with_host_count(
3676 columns: Vec<CudaColumn>,
3677 row_cap: u64,
3678 d_num_rows: TrackedCudaSlice<u32>,
3679 schema: Schema,
3680 host_row_count: u32,
3681 ) -> Self {
3682 assert_eq!(
3683 columns.len(),
3684 schema.arity(),
3685 "Number of columns ({}) must match schema arity ({})",
3686 columns.len(),
3687 schema.arity()
3688 );
3689 Self {
3690 columns,
3691 row_cap,
3692 d_num_rows,
3693 schema,
3694 cached_row_count: AtomicU32::new(host_row_count),
3695 canonical_full_row_set_certified: false,
3696 }
3697 }
3698
3699 pub fn cached_row_count(&self) -> Option<u32> {
3701 let v = self.cached_row_count.load(Ordering::Relaxed);
3702 if v == u32::MAX {
3703 None
3704 } else {
3705 Some(v)
3706 }
3707 }
3708
3709 pub(crate) fn set_cached_row_count_if_unset(&self, count: u32) {
3712 let _ = self.cached_row_count.compare_exchange(
3713 u32::MAX,
3714 count,
3715 Ordering::Relaxed,
3716 Ordering::Relaxed,
3717 );
3718 }
3719
3720 pub(crate) fn set_cached_row_count_after_device_copy(&self, count: u32) {
3721 self.cached_row_count.store(count, Ordering::Relaxed);
3722 }
3723
3724 pub(crate) fn invalidate_cached_row_count(&self) {
3725 self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3726 }
3727
3728 pub fn canonical_full_row_set_certified(&self) -> bool {
3730 self.canonical_full_row_set_certified
3731 }
3732
3733 pub(crate) fn certify_canonical_full_row_set(&mut self) {
3735 self.canonical_full_row_set_certified = true;
3736 }
3737
3738 pub fn columns(&self) -> &[CudaColumn] {
3740 &self.columns
3741 }
3742
3743 pub fn columns_mut(&mut self) -> &mut [CudaColumn] {
3745 self.canonical_full_row_set_certified = false;
3746 &mut self.columns
3747 }
3748
3749 pub fn set_schema(&mut self, schema: Schema) {
3751 assert_eq!(self.columns.len(), schema.arity());
3752 self.schema = schema;
3753 self.canonical_full_row_set_certified = false;
3754 }
3755
3756 pub fn set_row_capacity(&mut self, row_cap: u64) {
3758 self.row_cap = row_cap;
3759 self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3760 self.canonical_full_row_set_certified = false;
3761 }
3762
3763 pub fn num_rows_device_mut(&mut self) -> &mut TrackedCudaSlice<u32> {
3765 self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3766 self.canonical_full_row_set_certified = false;
3767 &mut self.d_num_rows
3768 }
3769
3770 pub fn num_rows(&self) -> u64 {
3772 self.row_cap
3773 }
3774
3775 pub fn num_rows_device(&self) -> &TrackedCudaSlice<u32> {
3777 &self.d_num_rows
3778 }
3779
3780 pub fn is_empty(&self) -> bool {
3782 self.row_cap == 0
3783 }
3784
3785 pub fn schema(&self) -> &Schema {
3787 &self.schema
3788 }
3789
3790 pub fn arity(&self) -> usize {
3792 self.schema.arity()
3793 }
3794
3795 pub fn estimated_bytes(&self) -> u64 {
3797 self.row_cap * self.schema.row_size_bytes() as u64
3798 }
3799
3800 pub fn column(&self, index: usize) -> Option<&CudaColumn> {
3802 self.columns.get(index)
3803 }
3804}
3805
3806pub fn validate_logical_row_count(row_cap: u64, logical_rows: usize) -> Result<usize> {
3807 let row_cap_usize = usize::try_from(row_cap)
3808 .map_err(|_| XlogError::Kernel(format!("Row capacity {} exceeds usize::MAX", row_cap)))?;
3809 if logical_rows > row_cap_usize {
3810 return Err(XlogError::Kernel(format!(
3811 "Logical row count {} exceeds row capacity {}",
3812 logical_rows, row_cap
3813 )));
3814 }
3815 debug_assert!(logical_rows <= row_cap_usize);
3816 Ok(logical_rows)
3817}
3818
3819#[cfg(test)]
3820pub(crate) fn test_memory_manifest(
3821 on_drop: impl FnOnce() + Send + Sync + 'static,
3822) -> (Arc<MemoryAccessManifest>, std::sync::Weak<[u8]>) {
3823 struct Bytes<F: FnOnce()>(Arc<[u8]>, Option<F>);
3824 impl<F: FnOnce()> Drop for Bytes<F> {
3825 fn drop(&mut self) {
3826 if let Some(on_drop) = self.1.take() {
3827 on_drop();
3828 }
3829 }
3830 }
3831 impl<F: FnOnce() + Send + Sync + 'static> MemoryStorageOwner for Bytes<F> {
3832 fn dependencies(
3833 &self,
3834 ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
3835 panic!("a passive host manifest must not touch CUDA dependencies")
3836 }
3837 }
3838 let bytes = Arc::<[u8]>::from(vec![7; 64]);
3839 let weak = Arc::downgrade(&bytes);
3840 let owner = Arc::new(Bytes(bytes, Some(on_drop)));
3841 let mut manifest = MemoryAccessManifest::default();
3842 manifest.ranges.push((
3843 17,
3844 MemoryUse::new(owner.0.as_ptr() as u64, owner.0.len(), Access::ReadWrite).unwrap(),
3845 ));
3846 manifest.retained.push(RetainedStorageUse {
3847 owner,
3848 access: Access::ReadWrite,
3849 });
3850 (Arc::new(manifest), weak)
3851}
3852
3853#[cfg(test)]
3854mod tests {
3855 impl crate::launch::RecorderCleanup<u8> for OperationCompletion {
3856 fn synchronize_retired(&self) -> ResourceResult<()> {
3857 if self.is_complete() {
3858 Ok(())
3859 } else {
3860 Err(ResourceError::Driver("completion remains unknown".into()))
3861 }
3862 }
3863
3864 fn cancel_retired(&self, _use: u8) -> ResourceResult<()> {
3865 Ok(())
3866 }
3867 }
3868
3869 #[test]
3870 fn retired_context_completion_retries_unknown_wait_without_changing_execution_identity() {
3871 let proof = OperationCompletion::new(17);
3872 assert!(proof
3873 .synchronize_retired_with(|| Err(ResourceError::Driver("context wait failed".into())))
3874 .is_err());
3875 assert!(!proof.is_complete());
3876 assert_eq!(proof.execution_id, 17);
3877 assert!(std::panic::catch_unwind(|| {
3878 let _ = proof.synchronize_retired_with(|| panic!("context wait panic"));
3879 })
3880 .is_err());
3881 assert!(!proof.is_complete());
3882 proof.synchronize_retired_with(|| Ok(())).unwrap();
3883 proof
3884 .synchronize_retired_with(|| panic!("completed wait must not repeat"))
3885 .unwrap();
3886 assert!(proof.is_complete());
3887 assert_eq!(proof.execution_id, 17);
3888 assert!(proof.validate_submission(17).is_err());
3889 assert!(proof.validate_submission(18).is_err());
3890 }
3891
3892 #[test]
3893 #[ignore = "requires authorized CUDA execution"]
3894 fn failed_abort_reclaims_real_allocation_after_origin_thread_exit() {
3895 use crate::cuda_graph::{reap_capture_retirements, CapturedCudaGraph};
3896 use crate::device_runtime::{AsyncCudaResource, GlobalDeviceBudget, StreamPool};
3897 use std::sync::mpsc;
3898
3899 let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
3900 let device = Arc::new(CudaDevice::new(0).unwrap());
3901 let default_stream = Arc::clone(device.inner().stream());
3902 let context = Arc::clone(device.inner().stream().context());
3903 assert!(
3904 context.has_async_alloc(),
3905 "this lifecycle regression requires real stream-ordered allocation and free"
3906 );
3907 let capture_stream = context.new_stream().unwrap();
3908 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
3909 let resource = Box::new(GlobalDeviceBudget::new(
3910 Box::new(AsyncCudaResource::new(
3911 Arc::clone(&device),
3912 0,
3913 Arc::clone(&pool),
3914 )),
3915 4096,
3916 ));
3917 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
3918 Arc::clone(&device),
3919 0,
3920 pool,
3921 resource,
3922 ));
3923 let manager = Arc::new(GpuMemoryManager::with_runtime(
3924 device,
3925 MemoryBudget::with_limit(4096),
3926 Arc::clone(&runtime),
3927 ));
3928 let (sent, received) = mpsc::channel();
3929 let (abort, wait_for_abort) = mpsc::channel();
3930 let worker_manager = Arc::clone(&manager);
3931 let worker = std::thread::spawn(move || {
3932 let stream = Arc::clone(worker_manager.device.inner().stream());
3935 let prefix = DeviceMemoryView::<u8>::allocate(
3936 Arc::clone(&stream),
3937 Arc::clone(worker_manager.device.inner().allocation_stream()),
3938 16,
3939 )
3940 .unwrap();
3941 with_memory_access(
3942 Arc::clone(&stream),
3943 vec![prefix.access(Access::Write).unwrap()],
3944 |_| {
3945 unsafe {
3947 cudarc::driver::sys::cuMemsetD8Async(
3948 *prefix.device_ptr(),
3949 0x3c,
3950 16,
3951 stream.cu_stream(),
3952 )
3953 .result()?;
3954 }
3955 Ok(())
3956 },
3957 )
3958 .unwrap();
3959 let allocation = worker_manager.alloc::<u8>(4096).unwrap();
3960 let raw = allocation.storage.dependencies.retain_allocation().unwrap();
3961 let stream = Arc::clone(allocation.stream());
3962 let ptr = *allocation.device_ptr();
3963 let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream).unwrap();
3964 let execution_id = submission.execution_id();
3965 let mut operation = MemoryOperation {
3966 submission,
3967 transaction: admit_memory_access(
3968 Arc::clone(&stream),
3969 vec![allocation.view().access(Access::Write).unwrap()],
3970 None,
3971 &[],
3972 execution_id,
3973 )
3974 .unwrap(),
3975 };
3976 let admitted = Arc::clone(operation.transaction.prepared_owner().unwrap());
3977 let completion = Arc::clone(admitted.completion());
3978 let owner = Arc::downgrade(&admitted);
3979 operation
3980 .transaction
3981 .enqueue_operation_with(
3982 || {
3983 let pin = operation.submission.pin()?;
3984 admitted.bind_submission(&pin)?;
3985 Ok(pin)
3986 },
3987 MemoryOperationOwner::prepare,
3988 |pin| {
3989 pin.with_serialized_submission(
3990 || -> crate::device_runtime::ResourceResult<()> {
3991 unsafe {
3994 cudarc::driver::sys::cuMemsetD8Async(
3995 ptr,
3996 0x5a,
3997 4096,
3998 stream.cu_stream(),
3999 )
4000 .result()?;
4001 }
4002 Ok(())
4003 },
4004 )
4005 },
4006 MemoryOperationOwner::synchronize,
4007 MemoryOperationOwner::cancel,
4008 )
4009 .unwrap();
4010 drop(admitted);
4011 drop(allocation);
4012 sent.send((raw, completion, owner, prefix)).unwrap();
4013 wait_for_abort.recv().unwrap();
4014 let error = operation
4018 .transaction
4019 .abort_with(
4020 |_| {
4021 Err(ResourceError::Driver(
4022 "injected original completion wait failure".into(),
4023 ))
4024 },
4025 MemoryOperationOwner::cancel,
4026 )
4027 .unwrap_err();
4028 assert!(error
4029 .to_string()
4030 .contains("original completion wait failure"));
4031 });
4032 let (raw, completion, owner, prefix) = received.recv().unwrap();
4033 assert_ne!(
4034 completion.execution_id,
4035 crate::cuda_graph::stream_execution_id(&default_stream).unwrap(),
4036 "the reaper must not identify its own PTDS as the exited producer's execution"
4037 );
4038 let reclamation = Arc::clone(raw.reclamation());
4039 let graph = CapturedCudaGraph::capture_on_stream(&capture_stream, || {
4040 abort.send(()).unwrap();
4041 worker.join().unwrap();
4042 reap_capture_retirements();
4043 assert!(!completion.is_complete());
4044 assert!(owner.upgrade().is_some());
4045 assert!(!reclamation.was_released());
4046 assert_eq!(manager.allocated_bytes(), 4096);
4047 assert_eq!(runtime.bytes_outstanding(), 4096);
4048 assert!(matches!(
4049 runtime.reserve_memory(1),
4050 Err(ResourceError::OutOfBudget {
4051 current: 4096,
4052 remaining: 0,
4053 ..
4054 })
4055 ));
4056 Ok(())
4057 })
4058 .unwrap();
4059 drop(graph);
4060 runtime.reap_pending().unwrap();
4061 assert!(completion.is_complete());
4062 assert!(owner.upgrade().is_none());
4063 let pending = Arc::clone(
4066 &reclamation
4067 .state
4068 .lock()
4069 .unwrap()
4070 .pending
4071 .as_ref()
4072 .unwrap()
4073 .0,
4074 );
4075 assert_eq!(pending.load(Ordering::Acquire), 4096);
4076 assert!(!reclamation.was_released());
4077 assert_eq!(manager.allocated_bytes(), 4096);
4078 assert_eq!(runtime.bytes_outstanding(), 4096);
4079 assert!(matches!(
4080 runtime.reserve_memory(1),
4081 Err(ResourceError::OutOfBudget {
4082 current: 4096,
4083 remaining: 0,
4084 ..
4085 })
4086 ));
4087 context.bind_to_thread().unwrap();
4088 let mut observed = [0u8; 4096];
4089 unsafe {
4092 cudarc::driver::sys::cuMemcpyDtoH_v2(
4093 observed.as_mut_ptr().cast(),
4094 raw.ptr(),
4095 observed.len(),
4096 )
4097 .result()
4098 .unwrap();
4099 }
4100 assert!(observed.iter().all(|byte| *byte == 0x5a));
4101 let mut observed_prefix = [0u8; 16];
4102 unsafe {
4105 cudarc::driver::sys::cuMemcpyDtoH_v2(
4106 observed_prefix.as_mut_ptr().cast(),
4107 *prefix.device_ptr(),
4108 observed_prefix.len(),
4109 )
4110 .result()
4111 .unwrap();
4112 }
4113 assert_eq!(observed_prefix, [0x3c; 16]);
4114 drop(prefix);
4115 drop(raw);
4116 runtime.reap_pending().unwrap();
4117 assert!(reclamation.was_released());
4118 assert_eq!(pending.load(Ordering::Acquire), 0);
4119 assert_eq!(manager.allocated_bytes(), 0);
4120 assert_eq!(manager.remaining_bytes(), 4096);
4121 assert_eq!(runtime.bytes_outstanding(), 0);
4122 drop(runtime.reserve_memory(4096).unwrap());
4123 }
4124
4125 #[test]
4126 fn operation_completion_rejects_moved_execution_before_dependency_prepare() {
4127 let proof = Arc::new(OperationCompletion::new(17));
4128 let mut transaction =
4129 RecorderTransaction::from_admitted(Arc::clone(&proof), Box::new([0u8]));
4130 let mut cancelled = false;
4131 let result = transaction.enqueue_operation_with(
4132 || proof.validate_submission(18),
4133 |_| panic!("moved execution must not prepare dependencies"),
4134 |_| -> std::result::Result<(), &str> { panic!("moved execution must not launch") },
4135 |_| panic!("refused admission must not wait on another execution"),
4136 |_, _| {
4137 cancelled = true;
4138 Ok(())
4139 },
4140 );
4141 assert!(matches!(
4142 result,
4143 Err(crate::launch::LaunchEnqueueError::Preparation(_))
4144 ));
4145 assert!(cancelled);
4146 assert!(!proof.is_complete());
4147 }
4148
4149 #[test]
4150 fn operation_completion_requires_exact_successful_synchronization() {
4151 let proof = OperationCompletion::new(17);
4152 assert!(proof.validate_submission(18).is_err());
4153 assert!(proof
4154 .synchronize_with(18, || panic!("wrong stream must not be waited"))
4155 .is_err());
4156 assert!(!proof.is_complete());
4157 assert!(proof
4158 .synchronize_with(17, || Err(ResourceError::Driver("wait failed".into())))
4159 .is_err());
4160 assert!(!proof.is_complete());
4161 let unwound = std::panic::catch_unwind(|| {
4162 let _ = proof.synchronize_with(17, || panic!("wait unwound"));
4163 });
4164 assert!(unwound.is_err());
4165 assert!(!proof.is_complete());
4166 proof.synchronize_with(17, || Ok(())).unwrap();
4167 assert!(proof.is_complete());
4168 assert!(proof.validate_submission(17).is_err());
4169 }
4170
4171 #[test]
4172 fn operation_completion_survives_failed_reservation_cancellation() {
4173 let proof = Arc::new(OperationCompletion::new(17));
4174 let mut transaction =
4175 RecorderTransaction::from_admitted(Arc::clone(&proof), Box::new([0u8]));
4176 let result = transaction.enqueue_operation_with(
4177 || Ok(()),
4178 |proof| proof.validate_submission(17),
4179 |_| Err("record failed"),
4180 |proof| proof.synchronize_with(17, || Ok(())),
4181 |_, _| Err(ResourceError::Driver("cancellation failed".into())),
4182 );
4183 assert!(matches!(
4184 result,
4185 Err(crate::launch::LaunchEnqueueError::OperationAndCleanup { .. })
4186 ));
4187 assert!(proof.is_complete());
4188 }
4189
4190 #[test]
4191 fn passive_manifest_keeps_storage_and_context_qualified_coverage() {
4192 struct Bytes(Box<[u8]>);
4193 impl MemoryStorageOwner for Bytes {
4194 fn dependencies(
4195 &self,
4196 ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
4197 panic!("a passive manifest must not touch CUDA dependencies")
4198 }
4199 }
4200 let bytes = Arc::new(Bytes(vec![7; 64].into_boxed_slice()));
4201 assert_eq!(bytes.0[0], 7);
4202 let weak = Arc::downgrade(&bytes);
4203 let mut source = MemoryAccessManifest::default();
4204 source
4205 .ranges
4206 .push((17, MemoryUse::new(0x1000, 64, Access::ReadWrite).unwrap()));
4207 source.retained.push(RetainedStorageUse {
4208 owner: bytes,
4209 access: Access::ReadWrite,
4210 });
4211 let source = Arc::new(source);
4212 let combined = MemoryAccessManifest::combine(std::slice::from_ref(&source));
4213 drop(source);
4214 assert!(weak.upgrade().is_some());
4215 assert!(combined.covers(&[(17, MemoryUse::new(0x1010, 16, Access::Read).unwrap())]));
4216 assert!(!combined.covers(&[(18, MemoryUse::new(0x1010, 16, Access::Read).unwrap())]));
4217 assert!(!combined.covers(&[(17, MemoryUse::new(0x1030, 32, Access::Read).unwrap())]));
4218 drop(combined);
4219 assert!(weak.upgrade().is_none());
4220 }
4221
4222 #[test]
4223 fn shared_reclamation_preserves_leases_until_the_actual_owner_can_retire() {
4224 use crate::device_runtime::resource::{BlockUseRegistry, MemoryUse};
4225 use std::sync::atomic::AtomicUsize;
4226
4227 struct PhysicalStorage {
4228 bytes: Box<[u8]>,
4229 drops: Arc<AtomicUsize>,
4230 }
4231 impl Drop for PhysicalStorage {
4232 fn drop(&mut self) {
4233 self.drops.fetch_add(1, Ordering::SeqCst);
4234 }
4235 }
4236 struct Payload {
4237 physical: Option<PhysicalStorage>,
4238 reclamation: Arc<AllocationReclamation>,
4239 admission: Option<crate::device_runtime::resource::MemoryUseGroup>,
4240 }
4241
4242 let registry = Arc::new(std::sync::Mutex::new(BlockUseRegistry::default()));
4243 let accounting = Arc::new(GpuMemoryAccounting::default());
4244 accounting.budget_reserved.store(64, Ordering::SeqCst);
4245 let reclamation = Arc::new(AllocationReclamation::default());
4246 reclamation
4247 .attach_local(Arc::clone(&accounting), 64)
4248 .unwrap();
4249 reclamation.acquired().unwrap();
4250 let physical_drops = Arc::new(AtomicUsize::new(0));
4251 let physical = PhysicalStorage {
4252 bytes: vec![3u8; 64].into_boxed_slice(),
4253 drops: Arc::clone(&physical_drops),
4254 };
4255 let memory = MemoryUse::new(physical.bytes.as_ptr() as u64, 64, Access::ReadWrite).unwrap();
4256 let mut pending = Some(Arc::new(Payload {
4257 physical: Some(physical),
4258 reclamation: Arc::clone(&reclamation),
4259 admission: None,
4260 }));
4261 let lease = Arc::clone(pending.as_ref().unwrap());
4262 let observer = Arc::downgrade(&lease);
4263
4264 assert!(!reclaim_shared_allocation(&mut pending, |_| {
4267 panic!("a live storage lease must prevent physical reclamation")
4268 })
4269 .unwrap());
4270 assert!(Arc::ptr_eq(pending.as_ref().unwrap(), &lease));
4271 let use_group = registry
4272 .lock()
4273 .unwrap()
4274 .reserve_owned_memory_uses(&[(7, memory)], &[reclamation.release_proof()])
4275 .unwrap();
4276 assert_eq!(lease.physical.as_ref().unwrap().bytes[9], 3);
4277 registry
4278 .lock()
4279 .unwrap()
4280 .release_memory_uses(use_group)
4281 .unwrap();
4282 drop(lease);
4283
4284 let attempt = |owner: &mut Payload| {
4287 crate::device_runtime::resource::with_reclamation_admission(
4288 ®istry,
4289 7,
4290 memory,
4291 owner.reclamation.release_proof(),
4292 &mut owner.admission,
4293 || Err(ResourceError::Driver("physical completion pending".into())),
4294 )
4295 };
4296 assert!(reclaim_shared_allocation(&mut pending, attempt).is_err());
4297 assert!(observer.upgrade().is_none());
4298 let queue = std::sync::Mutex::new(vec![pending.take()]);
4299 assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4300 let mut batch =
4301 PendingReclamationBatch::take(&queue, |queue, owners| queue.append(owners));
4302 let _ = reclaim_shared_allocation(&mut batch[0], |_| panic!("wait unwound"));
4303 }))
4304 .is_err());
4305 pending = queue
4306 .lock()
4307 .unwrap()
4308 .pop()
4309 .expect("failed owner returned to canonical queue");
4310 assert!(pending.as_ref().unwrap().physical.is_some());
4311 assert_eq!(physical_drops.load(Ordering::SeqCst), 0);
4312 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4313 assert!(registry
4314 .lock()
4315 .unwrap()
4316 .reserve_memory_uses(7, &[memory])
4317 .is_err());
4318
4319 let late_registry = Arc::clone(®istry);
4320 std::thread::spawn(move || {
4321 assert!(reclaim_shared_allocation(&mut pending, |owner| {
4322 crate::device_runtime::resource::with_reclamation_admission(
4323 &late_registry,
4324 7,
4325 memory,
4326 owner.reclamation.release_proof(),
4327 &mut owner.admission,
4328 || {
4329 assert!(late_registry.try_lock().is_ok());
4330 drop(owner.physical.take());
4331 owner.reclamation.complete()
4332 },
4333 )
4334 })
4335 .unwrap());
4336 assert!(pending.is_none());
4337 })
4338 .join()
4339 .unwrap();
4340 assert_eq!(physical_drops.load(Ordering::SeqCst), 1);
4341 assert!(reclamation.was_released());
4342 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4343 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4344 let replacement = AllocationReclamation::default();
4345 let group = registry
4346 .lock()
4347 .unwrap()
4348 .reserve_owned_memory_uses(&[(7, memory)], &[replacement.release_proof()])
4349 .unwrap();
4350 registry.lock().unwrap().release_memory_uses(group).unwrap();
4351 }
4352
4353 #[test]
4354 fn late_reclamation_retires_actual_payload_charge_and_same_range_together() {
4355 use crate::device_runtime::resource::{
4356 with_reclamation_admission, BlockUseRegistry, MemoryStorageOwner, MemoryUse,
4357 };
4358 use std::sync::atomic::AtomicUsize;
4359
4360 struct PhysicalStorage {
4361 _bytes: Box<[u8]>,
4362 drops: Arc<AtomicUsize>,
4363 }
4364 impl Drop for PhysicalStorage {
4365 fn drop(&mut self) {
4366 self.drops.fetch_add(1, Ordering::SeqCst);
4367 }
4368 }
4369 struct Payload {
4370 physical: Option<PhysicalStorage>,
4371 reclamation: Arc<AllocationReclamation>,
4372 drops: Arc<AtomicUsize>,
4373 admission: Option<MemoryUseGroup>,
4374 }
4375 impl Drop for Payload {
4376 fn drop(&mut self) {
4377 self.drops.fetch_add(1, Ordering::SeqCst);
4378 }
4379 }
4380 struct StorageView;
4381 impl MemoryStorageOwner for StorageView {
4382 fn dependencies(
4383 &self,
4384 ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
4385 panic!("host driver boundary must not initialize CUDA");
4386 }
4387 }
4388
4389 for enqueue_error in [false, true] {
4390 let registry = std::sync::Mutex::new(BlockUseRegistry::default());
4391 let accounting = Arc::new(GpuMemoryAccounting::default());
4392 accounting.budget_reserved.store(64, Ordering::SeqCst);
4393 let reclamation = Arc::new(AllocationReclamation::default());
4394 reclamation
4395 .attach_local(Arc::clone(&accounting), 64)
4396 .unwrap();
4397 reclamation.acquired().unwrap();
4398 let physical_drops = Arc::new(AtomicUsize::new(0));
4399 let payload_drops = Arc::new(AtomicUsize::new(0));
4400 let bytes = vec![0u8; 64].into_boxed_slice();
4401 let ptr = bytes.as_ptr() as u64;
4402 let mut live = Some(Payload {
4403 physical: Some(PhysicalStorage {
4404 _bytes: bytes,
4405 drops: Arc::clone(&physical_drops),
4406 }),
4407 reclamation: Arc::clone(&reclamation),
4408 drops: Arc::clone(&payload_drops),
4409 admission: None,
4410 });
4411 let view: Arc<dyn MemoryStorageOwner> = Arc::new(StorageView);
4412 let memory = MemoryUse::new(ptr, 64, Access::ReadWrite).unwrap();
4413 registry.lock().unwrap().register_storage(
4414 7,
4415 memory,
4416 Arc::downgrade(&view),
4417 reclamation.release_proof(),
4418 );
4419 let mut pending = live.take();
4420 let owner = pending.as_mut().unwrap();
4421 let result = with_reclamation_admission(
4422 ®istry,
4423 7,
4424 memory,
4425 reclamation.release_proof(),
4426 &mut owner.admission,
4427 || {
4428 assert!(registry.try_lock().is_ok());
4429 if enqueue_error {
4430 Err(ResourceError::Driver(
4431 "pending free completion unknown".into(),
4432 ))
4433 } else {
4434 Ok(())
4435 }
4436 },
4437 );
4438 assert_eq!(result.is_err(), enqueue_error);
4439 drop(view);
4440 assert!(live.is_none());
4441 assert!(
4442 reclaim_allocation(&mut pending, |_| Err(ResourceError::Driver(
4443 "driver completion still pending".into()
4444 )))
4445 .is_err()
4446 );
4447 assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4448 let _ = reclaim_allocation(&mut pending, |_| panic!("driver wait unwound"));
4449 }))
4450 .is_err());
4451 assert!(pending.as_ref().unwrap().physical.is_some());
4452 assert_eq!(physical_drops.load(Ordering::SeqCst), 0);
4453 assert_eq!(payload_drops.load(Ordering::SeqCst), 0);
4454 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4455 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4456 assert!(!reclamation.was_released());
4457 {
4458 let mut registry = registry.lock().unwrap();
4459 assert!(registry.reserve_memory_uses(7, &[memory]).is_err());
4460 assert!(registry
4461 .retain_storage_uses(7, &[memory], &mut Vec::new())
4462 .is_err());
4463 }
4464
4465 std::thread::spawn(move || {
4468 reclaim_allocation(&mut pending, |owner| {
4469 drop(owner.physical.take());
4470 owner.reclamation.complete()
4471 })
4472 .unwrap();
4473 assert!(pending.is_none());
4474 reclaim_allocation(&mut pending, |_| panic!("must not free twice")).unwrap();
4475 })
4476 .join()
4477 .unwrap();
4478 assert_eq!(physical_drops.load(Ordering::SeqCst), 1);
4479 assert_eq!(payload_drops.load(Ordering::SeqCst), 1);
4480 assert!(reclamation.was_released());
4481 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4482 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4483 assert_eq!(Arc::strong_count(&accounting), 1);
4484 let replacement: Arc<dyn MemoryStorageOwner> = Arc::new(StorageView);
4485 let replacement_reclamation = AllocationReclamation::default();
4486 let mut retained = Vec::new();
4487 {
4488 let mut registry = registry.lock().unwrap();
4489 registry.register_storage(
4490 7,
4491 memory,
4492 Arc::downgrade(&replacement),
4493 replacement_reclamation.release_proof(),
4494 );
4495 assert!(registry
4497 .reserve_owned_memory_uses(&[(7, memory)], &[reclamation.release_proof()],)
4498 .is_err());
4499 registry
4500 .retain_storage_uses(7, &[memory], &mut retained)
4501 .unwrap();
4502 let group = registry
4503 .reserve_owned_memory_uses(
4504 &[(7, memory)],
4505 &[replacement_reclamation.release_proof()],
4506 )
4507 .unwrap();
4508 registry.release_memory_uses(group).unwrap();
4509 }
4510 assert_eq!(retained.len(), 1);
4511 assert!(Arc::ptr_eq(&retained[0].owner, &replacement));
4512 }
4513 }
4514
4515 #[test]
4516 fn foreign_storage_retirement_retries_completion_and_releases_the_actual_owner() {
4517 use std::sync::atomic::{AtomicBool, AtomicUsize};
4518
4519 struct Producer(Arc<AtomicUsize>);
4520 impl Drop for Producer {
4521 fn drop(&mut self) {
4522 self.0.fetch_add(1, Ordering::SeqCst);
4523 }
4524 }
4525 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4526 let drops = Arc::new(AtomicUsize::new(0));
4527 let producer = Arc::new(Producer(Arc::clone(&drops)));
4528 let retained = Arc::downgrade(&producer);
4529 let ready = Arc::new(AtomicBool::new(false));
4530 let completion = Arc::clone(&ready);
4531 let reclamation = Arc::new(AllocationReclamation::default());
4532 let proof = Arc::clone(&reclamation);
4533 crate::cuda_graph::retire_resources_after_completion(
4534 Some(producer),
4535 move |_| {
4536 if completion.load(Ordering::SeqCst) {
4537 Ok(())
4538 } else {
4539 Err(ResourceError::Driver(
4540 "producer use completion pending".into(),
4541 ))
4542 }
4543 },
4544 |owner| {
4545 drop(owner.take());
4546 Ok(())
4547 },
4548 move |_| proof.complete(),
4549 |_, error| panic!("unexpected producer retirement failure: {error}"),
4550 );
4551 assert!(retained.upgrade().is_some());
4552 assert_eq!(drops.load(Ordering::SeqCst), 0);
4553 assert!(!reclamation.was_released());
4554 ready.store(true, Ordering::SeqCst);
4555 crate::cuda_graph::reap_capture_retirements();
4556 assert!(retained.upgrade().is_none());
4557 assert_eq!(drops.load(Ordering::SeqCst), 1);
4558 assert!(reclamation.was_released());
4559 crate::cuda_graph::reap_capture_retirements();
4560 assert_eq!(drops.load(Ordering::SeqCst), 1);
4561 }
4562
4563 #[test]
4564 fn foreign_storage_retirement_retries_release_publication_without_repeating_deleter() {
4565 use std::sync::atomic::AtomicUsize;
4566 struct Producer(Arc<AtomicUsize>);
4567 impl Drop for Producer {
4568 fn drop(&mut self) {
4569 self.0.fetch_add(1, Ordering::SeqCst);
4570 }
4571 }
4572 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4573 let drops = Arc::new(AtomicUsize::new(0));
4574 let accounting = Arc::new(GpuMemoryAccounting::default());
4575 let reclamation = Arc::new(AllocationReclamation::default());
4576 reclamation
4577 .attach_local(Arc::clone(&accounting), 64)
4578 .unwrap();
4579 reclamation.acquired().unwrap();
4580 let resources = (Some(Producer(Arc::clone(&drops))), Arc::clone(&reclamation));
4581 crate::cuda_graph::retire_resources_after_completion(
4582 resources,
4583 |_| Ok(()),
4584 |owners| {
4585 drop(owners.0.take());
4586 Ok(())
4587 },
4588 |owners| owners.1.complete(),
4589 |_, _| {},
4590 );
4591 assert_eq!(drops.load(Ordering::SeqCst), 1);
4592 assert!(!reclamation.was_released());
4593 accounting.allocated.store(64, Ordering::SeqCst);
4594 accounting.budget_reserved.store(64, Ordering::SeqCst);
4595 std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4596 .join()
4597 .unwrap();
4598 assert!(reclamation.was_released());
4599 assert_eq!(drops.load(Ordering::SeqCst), 1);
4600 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4601 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4602 }
4603
4604 #[test]
4605 fn foreign_storage_retirement_publication_unwind_keeps_retry_without_deleter() {
4606 use std::sync::atomic::{AtomicBool, AtomicUsize};
4607 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4608 let destroyed = Arc::new(AtomicUsize::new(0));
4609 let destroy_probe = Arc::clone(&destroyed);
4610 let first = Arc::new(AtomicBool::new(true));
4611 let context = Arc::new(());
4612 let retained = Arc::downgrade(&context);
4613 let reclamation = Arc::new(AllocationReclamation::default());
4614 let proof = Arc::clone(&reclamation);
4615 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4616 crate::cuda_graph::retire_resources_after_completion(
4617 context,
4618 |_| Ok(()),
4619 move |_| {
4620 destroy_probe.fetch_add(1, Ordering::SeqCst);
4621 Ok(())
4622 },
4623 move |_| {
4624 if first.swap(false, Ordering::SeqCst) {
4625 panic!("release publication interrupted");
4626 }
4627 proof.complete()
4628 },
4629 |_, error| panic!("unexpected destructive failure: {error}"),
4630 );
4631 }));
4632 assert!(result.is_err());
4633 assert_eq!(destroyed.load(Ordering::SeqCst), 1);
4634 assert!(retained.upgrade().is_some());
4635 assert!(!reclamation.was_released());
4636 std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4637 .join()
4638 .unwrap();
4639 assert_eq!(destroyed.load(Ordering::SeqCst), 1);
4640 assert!(reclamation.was_released());
4641 assert!(retained.upgrade().is_none());
4642 }
4643
4644 #[test]
4645 fn foreign_storage_retirement_unwinding_deleter_keeps_context_without_retry() {
4646 use std::sync::atomic::AtomicUsize;
4647
4648 struct Producer(Arc<AtomicUsize>);
4649 impl Drop for Producer {
4650 fn drop(&mut self) {
4651 self.0.fetch_add(1, Ordering::SeqCst);
4652 panic!("producer deleter interrupted");
4653 }
4654 }
4655 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4656 let drops = Arc::new(AtomicUsize::new(0));
4657 let context = Arc::new(());
4658 let retained_context = Arc::downgrade(&context);
4659 let reclamation = Arc::new(AllocationReclamation::default());
4660 let resources = (
4661 Some(Producer(Arc::clone(&drops))),
4662 Arc::clone(&reclamation),
4663 context,
4664 );
4665 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4666 crate::cuda_graph::retire_resources_after_completion(
4667 resources,
4668 |_| Ok(()),
4669 |owners| {
4670 drop(owners.0.take());
4671 Ok(())
4672 },
4673 |owners| owners.1.complete(),
4674 |_, error| panic!("unexpected retirement result: {error}"),
4675 );
4676 }));
4677 assert!(result.is_err());
4678 crate::cuda_graph::reap_capture_retirements();
4679 crate::cuda_graph::reap_capture_retirements();
4680 assert_eq!(drops.load(Ordering::SeqCst), 1);
4681 assert!(retained_context.upgrade().is_some());
4682 assert!(!reclamation.was_released());
4683 }
4684
4685 #[test]
4686 fn allocation_reclamation_refunds_local_charge_once_on_the_actual_reaper() {
4687 let accounting = Arc::new(GpuMemoryAccounting::default());
4688 accounting.budget_reserved.store(64, Ordering::SeqCst);
4689 let reclamation = Arc::new(AllocationReclamation::default());
4690 reclamation
4691 .attach_local(Arc::clone(&accounting), 64)
4692 .unwrap();
4693 reclamation.acquired().unwrap();
4694 let reaper = Arc::clone(&reclamation);
4695 std::thread::spawn(move || reaper.complete().unwrap())
4696 .join()
4697 .unwrap();
4698 reclamation.complete().unwrap();
4699 assert!(reclamation.was_released());
4700 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4701 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4702 assert_eq!(Arc::strong_count(&accounting), 1);
4703 }
4704
4705 #[test]
4706 fn allocation_reclamation_settles_all_byte_charges_before_handle_retirement() {
4707 let local = Arc::new(GpuMemoryAccounting::default());
4708 local.budget_reserved.store(64, Ordering::SeqCst);
4709 let ledger = Arc::new(AllocationAccounting::default());
4710 let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4711 let ticket = Arc::new(AllocationReclamation::default());
4712 ticket.attach_local(Arc::clone(&local), 64).unwrap();
4713 ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4714 let storage = vec![0_u8; 64].into_boxed_slice();
4715 ticket.acquired().unwrap();
4716 ticket.attach_pending(Arc::clone(&pending), 64).unwrap();
4717 assert!(ticket.attach_pending(Arc::clone(&pending), 64).is_err());
4718 assert_eq!(pending.load(Ordering::SeqCst), 64);
4719 let mut handle_owner = Some((storage, Arc::clone(&ticket)));
4720 assert!(reclaim_allocation(&mut handle_owner, |owner| {
4721 drop(std::mem::take(&mut owner.0));
4723 owner.1.complete()?;
4724 Err(ResourceError::Driver(
4725 "stream destruction outcome unknown".into(),
4726 ))
4727 })
4728 .is_err());
4729 assert!(handle_owner.is_some());
4730 assert!(ticket.was_acquired());
4731 assert!(ticket.was_released());
4732 assert_eq!(local.budget_reserved.load(Ordering::SeqCst), 0);
4733 assert_eq!(local.allocated.load(Ordering::SeqCst), 0);
4734 assert_eq!(ledger.snapshot(), (0, 64));
4735 assert_eq!(pending.load(Ordering::SeqCst), 0);
4736 ticket.complete().unwrap();
4737 assert_eq!(ledger.snapshot(), (0, 64));
4738 }
4739
4740 #[test]
4741 fn allocation_reclamation_concurrent_completion_settles_once() {
4742 let ledger = Arc::new(AllocationAccounting::default());
4743 let ticket = Arc::new(AllocationReclamation::default());
4744 ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4745 let storage = vec![0_u8; 64].into_boxed_slice();
4746 ticket.acquired().unwrap();
4747 drop(storage);
4748 let barrier = Arc::new(std::sync::Barrier::new(8));
4749 std::thread::scope(|scope| {
4750 for _ in 0..8 {
4751 let ticket = Arc::clone(&ticket);
4752 let barrier = Arc::clone(&barrier);
4753 scope.spawn(move || {
4754 barrier.wait();
4755 for _ in 0..100 {
4756 ticket.complete().unwrap();
4757 }
4758 });
4759 }
4760 });
4761 assert_eq!(ledger.snapshot(), (0, 64));
4762 }
4763
4764 #[test]
4765 fn allocation_reclamation_detach_panic_does_not_skip_physical_reap() {
4766 use crate::device_runtime::{DeviceMemoryResource, ResourceResult};
4767 struct InterruptedDetach(
4768 crate::device_runtime::budget::tests::DeferredHostResource,
4769 bool,
4770 );
4771 impl DeviceMemoryResource for InterruptedDetach {
4772 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
4773 self.0.materialize(request)
4774 }
4775 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
4776 self.0.allocation_accounting()
4777 }
4778 fn device_ordinal(&self) -> u32 {
4779 0
4780 }
4781 fn bytes_outstanding(&self) -> usize {
4782 self.0.bytes_outstanding()
4783 }
4784 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
4785 self.0.deallocate(block)?;
4786 panic!("detach telemetry interrupted");
4787 }
4788 fn reap_pending(&self) -> ResourceResult<()> {
4789 let owners = std::mem::take(&mut *self.0.owners.lock().unwrap());
4790 drop(owners);
4791 if self.1 {
4792 panic!("reap telemetry interrupted");
4793 }
4794 Ok(())
4795 }
4796 }
4797 for reap_panics in [false, true] {
4798 let resource: Arc<dyn DeviceMemoryResource + Send + Sync> =
4799 Arc::new(InterruptedDetach(Default::default(), reap_panics));
4800 let ledger = resource.allocation_accounting();
4801 let request = AllocationRequest::new(64, StreamId::DEFAULT, AllocTag::UNTAGGED);
4802 let ticket = request.reclamation();
4803 let block = resource.materialize(request).unwrap();
4804 let mut owner = ResourceBlockRetirement::new(block, resource, ticket);
4805 let outcome =
4806 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| owner.release()));
4807 let panic = outcome.expect_err("detach panic must be preserved");
4808 assert_eq!(
4809 panic.downcast_ref::<&str>(),
4810 Some(&"detach telemetry interrupted")
4811 );
4812 assert_eq!(ledger.snapshot(), (0, 64));
4813 owner.release().unwrap();
4814 }
4815 }
4816
4817 #[test]
4818 fn allocation_reclamation_partial_settlement_does_not_refund_twice() {
4819 let local = Arc::new(GpuMemoryAccounting::default());
4820 local.budget_reserved.store(64, Ordering::SeqCst);
4821 let ledger = Arc::new(AllocationAccounting::default());
4822 let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4823 let ticket = AllocationReclamation::default();
4824 ticket.attach_local(Arc::clone(&local), 64).unwrap();
4825 ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4826 ticket.acquired().unwrap();
4827 ticket.attach_pending(Arc::clone(&pending), 64).unwrap();
4828 pending.store(32, Ordering::SeqCst); assert!(ticket.complete().is_err());
4830 assert_eq!(local.allocated.load(Ordering::SeqCst), 0);
4831 assert_eq!(ledger.snapshot(), (0, 64));
4832 assert!(!ticket.was_released());
4833 pending.store(64, Ordering::SeqCst);
4834 ticket.complete().unwrap();
4835 assert_eq!(ledger.snapshot(), (0, 64));
4836 assert_eq!(pending.load(Ordering::SeqCst), 0);
4837 assert!(ticket.was_released());
4838 }
4839
4840 #[test]
4841 fn allocation_reclamation_local_attempt_restores_only_unacquired_claims() {
4842 for from_token in [false, true] {
4843 for acquired in [false, true] {
4844 let accounting = Arc::new(GpuMemoryAccounting::default());
4845 accounting.budget_reserved.store(128, Ordering::SeqCst);
4846 let mut remaining = 64;
4847 let mut ticket = None;
4848 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4849 let attempt = LocalAllocationAttempt::new(
4850 Arc::clone(&accounting),
4851 64,
4852 if from_token {
4853 Some(&mut remaining)
4854 } else {
4855 None
4856 },
4857 );
4858 ticket = Some(Arc::clone(&attempt.reclamation));
4859 if acquired {
4860 attempt.reclamation.acquired().unwrap();
4861 attempt.reclamation.complete().unwrap();
4864 }
4865 panic!("allocation frame interrupted");
4866 }));
4867 assert!(panic.is_err());
4868 assert_eq!(remaining, if from_token && !acquired { 128 } else { 64 });
4869 assert_eq!(
4870 accounting.budget_reserved.load(Ordering::SeqCst),
4871 if from_token && !acquired { 128 } else { 64 }
4872 );
4873 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4874 }
4875 }
4876 }
4877
4878 #[test]
4879 fn allocation_reclamation_unknown_completion_retains_canonical_charge() {
4880 let accounting = Arc::new(GpuMemoryAccounting::default());
4881 accounting.allocated.store(64, Ordering::SeqCst);
4882 accounting.budget_reserved.store(64, Ordering::SeqCst);
4883 let reclamation = AllocationReclamation::default();
4884 accounting.allocated.store(0, Ordering::SeqCst);
4885 reclamation
4886 .attach_local(Arc::clone(&accounting), 64)
4887 .unwrap();
4888 reclamation.acquired().unwrap();
4889 assert!(!reclamation.was_released());
4890 drop(reclamation);
4891 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4892 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4893 }
4894
4895 #[test]
4896 fn allocation_reclamation_failed_refund_is_atomic_and_retryable() {
4897 let accounting = Arc::new(GpuMemoryAccounting::default());
4898 accounting.budget_reserved.store(64, Ordering::SeqCst);
4899 let reclamation = AllocationReclamation::default();
4900 reclamation
4901 .attach_local(Arc::clone(&accounting), 64)
4902 .unwrap();
4903 reclamation.acquired().unwrap();
4904 accounting.allocated.store(32, Ordering::SeqCst);
4905 assert!(reclamation.complete().is_err());
4906 assert!(!reclamation.was_released());
4907 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 32);
4908 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4909 assert_eq!(Arc::strong_count(&accounting), 2);
4910 accounting.allocated.store(64, Ordering::SeqCst);
4911 reclamation.complete().unwrap();
4912 assert!(reclamation.was_released());
4913 assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4914 assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4915 }
4916
4917 #[test]
4918 fn physical_release_proof_is_independent_of_another_pending_failure() {
4919 assert!(allocation_reclamation_outcome(
4920 true,
4921 Err(ResourceError::Driver("another allocation failed".into()))
4922 )
4923 .is_ok());
4924 assert!(allocation_reclamation_outcome(false, Ok(())).is_err());
4925 assert!(allocation_reclamation_outcome(
4926 false,
4927 Err(ResourceError::Driver("own free failed".into()))
4928 )
4929 .is_err());
4930 }
4931 #[test]
4932 fn allocation_initialization_failure_reaches_the_canonical_cold_reaper() {
4933 use std::sync::atomic::{AtomicBool, AtomicUsize};
4934
4935 struct DeferredOwner {
4936 storage: Option<Box<[u8]>>,
4937 completed: Arc<AtomicBool>,
4938 releases: Arc<AtomicUsize>,
4939 }
4940 impl Drop for DeferredOwner {
4941 fn drop(&mut self) {
4942 let mut storage = self.storage.take();
4943 let completed = Arc::clone(&self.completed);
4944 let releases = Arc::clone(&self.releases);
4945 crate::cuda_graph::retry_retirement_after_stream_captures(move || {
4946 if !completed.load(Ordering::Acquire) {
4947 return false;
4948 }
4949 drop(storage.take());
4950 releases.fetch_add(1, Ordering::SeqCst);
4951 true
4952 });
4953 }
4954 }
4955
4956 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4957 for unwind in [false, true] {
4958 let completed = Arc::new(AtomicBool::new(false));
4959 let releases = Arc::new(AtomicUsize::new(0));
4960 let owner = DeferredOwner {
4961 storage: Some(vec![0; 64].into_boxed_slice()),
4962 completed: Arc::clone(&completed),
4963 releases: Arc::clone(&releases),
4964 };
4965 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4966 let result = initialize_allocation(owner, 64, Arc::default(), |_| {
4967 if unwind {
4968 panic!("allocation initialization interrupted");
4969 }
4970 Err(ResourceError::Driver(
4971 "allocation initialization failed".into(),
4972 ))
4973 });
4974 assert!(result.is_err());
4975 }));
4976 assert_eq!(result.is_err(), unwind);
4977 crate::cuda_graph::reap_capture_retirements();
4978 assert_eq!(releases.load(Ordering::SeqCst), 0);
4979 completed.store(true, Ordering::Release);
4980 std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4981 .join()
4982 .unwrap();
4983 assert_eq!(releases.load(Ordering::SeqCst), 1);
4984 crate::cuda_graph::reap_capture_retirements();
4985 assert_eq!(releases.load(Ordering::SeqCst), 1);
4986 }
4987 }
4988
4989 #[test]
4990 fn allocation_initialization_failure_transfers_actual_owner_to_drop() {
4991 struct Owner(Arc<std::sync::atomic::AtomicUsize>);
4992 impl Drop for Owner {
4993 fn drop(&mut self) {
4994 self.0.fetch_add(1, Ordering::SeqCst);
4995 }
4996 }
4997 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4998 let reclamation = Arc::new(AllocationReclamation::default());
4999 reclamation.acquired().unwrap();
5000 let error = initialize_allocation(Owner(Arc::clone(&drops)), 64, reclamation, |_| {
5001 Err(ResourceError::Driver(
5002 "allocation-ready record failed".into(),
5003 ))
5004 })
5005 .err()
5006 .expect("initialization must fail");
5007 assert_eq!(error.retained_allocation_bytes(), 64);
5008 drop(error);
5009 assert_eq!(drops.load(Ordering::SeqCst), 1);
5010 }
5011
5012 #[test]
5013 fn allocation_initialization_unwind_transfers_actual_owner_to_drop() {
5014 struct Owner(Arc<std::sync::atomic::AtomicUsize>);
5015 impl Drop for Owner {
5016 fn drop(&mut self) {
5017 self.0.fetch_add(1, Ordering::SeqCst);
5018 }
5019 }
5020 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
5021 let result = std::panic::catch_unwind(|| {
5022 let _ = initialize_allocation(Owner(Arc::clone(&drops)), 64, Arc::default(), |_| {
5023 panic!("initialization unwound")
5024 });
5025 });
5026 assert!(result.is_err());
5027 assert_eq!(drops.load(Ordering::SeqCst), 1);
5028 }
5029
5030 #[test]
5031 fn allocation_initialization_success_transfers_owner_once() {
5032 let owner = Arc::new(());
5033 let initialized =
5034 initialize_allocation(Arc::clone(&owner), 64, Arc::default(), |_| Ok(())).unwrap();
5035 assert_eq!(Arc::strong_count(&owner), 2);
5036 drop(initialized);
5037 assert_eq!(Arc::strong_count(&owner), 1);
5038 }
5039
5040 #[test]
5041 fn allocation_release_retries_only_unconfirmed_fence_steps() {
5042 use std::cell::Cell;
5043
5044 let mut state = DriverReleaseState::default();
5045 let frees = Cell::new(0);
5046 state
5047 .submit(|| {
5048 frees.set(frees.get() + 1);
5049 Ok(())
5050 })
5051 .unwrap();
5052 let records = Cell::new(0);
5053 let waits = Cell::new(0);
5054 let mut recorded = false;
5055 assert!(state
5056 .confirm_async(
5057 &mut recorded,
5058 || {
5059 records.set(records.get() + 1);
5060 Err(ResourceError::Driver("record failed".into()))
5061 },
5062 || {
5063 waits.set(waits.get() + 1);
5064 Ok(())
5065 },
5066 )
5067 .is_err());
5068 assert!(!recorded);
5069 assert_eq!(waits.get(), 0, "an unrecorded event proves no free");
5070 assert!(state
5071 .confirm_async(
5072 &mut recorded,
5073 || {
5074 records.set(records.get() + 1);
5075 Ok(())
5076 },
5077 || {
5078 waits.set(waits.get() + 1);
5079 Err(ResourceError::Driver("wait failed".into()))
5080 },
5081 )
5082 .is_err());
5083 assert!(recorded);
5084 state
5085 .confirm_async(
5086 &mut recorded,
5087 || panic!("recorded fence must not be replaced"),
5088 || {
5089 waits.set(waits.get() + 1);
5090 Ok(())
5091 },
5092 )
5093 .unwrap();
5094 assert_eq!(frees.get(), 1);
5095 assert_eq!(records.get(), 2);
5096 assert_eq!(waits.get(), 2);
5097
5098 for state in [
5099 DriverReleaseState::Owned,
5100 DriverReleaseState::OutcomeUnknown,
5101 ] {
5102 assert!(state
5103 .confirm_async(
5104 &mut false,
5105 || panic!("cannot invent proof of an unconfirmed free submission"),
5106 || panic!("cannot confirm an unsubmitted free"),
5107 )
5108 .is_err());
5109 }
5110 }
5111
5112 #[test]
5113 fn allocation_release_does_not_resubmit_unknown_free() {
5114 let attempts = std::cell::Cell::new(0);
5115 let mut state = DriverReleaseState::default();
5116 assert!(state
5117 .submit(|| {
5118 attempts.set(attempts.get() + 1);
5119 Err(ResourceError::Driver("unknown free outcome".into()))
5120 })
5121 .is_err());
5122 assert!(state
5123 .submit(|| {
5124 attempts.set(attempts.get() + 1);
5125 Ok(())
5126 })
5127 .is_err());
5128 assert_eq!(attempts.get(), 1);
5129 }
5130
5131 #[test]
5132 fn allocation_release_does_not_resubmit_successful_free() {
5133 let attempts = std::cell::Cell::new(0);
5134 let mut state = DriverReleaseState::default();
5135 for _ in 0..2 {
5136 state
5137 .submit(|| {
5138 attempts.set(attempts.get() + 1);
5139 Ok(())
5140 })
5141 .unwrap();
5142 }
5143 assert_eq!(attempts.get(), 1);
5144 }
5145 use super::*;
5146 use crate::device_runtime::{DeviceMemoryResource, DirectCudaResource, ResourceResult};
5147 use xlog_core::ScalarType;
5148
5149 #[test]
5150 fn reservation_exposes_exact_memory_manager_identity() {
5151 let _: fn(&GpuMemoryReservation) -> usize = GpuMemoryReservation::memory_manager_ptr_value;
5152 }
5153
5154 #[test]
5155 fn cuda_column_exposes_optional_memory_manager_identity() {
5156 let _: fn(&CudaColumn) -> Option<usize> = CudaColumn::memory_manager_ptr_value;
5157 }
5158
5159 #[test]
5160 fn tracked_slice_exposes_runtime_allocation_identity_snapshot() {
5161 let _: fn(&TrackedCudaSlice<u32>) -> Result<Option<RuntimeAllocationIdentity>> =
5162 TrackedCudaSlice::<u32>::runtime_allocation_identity;
5163 }
5164
5165 #[test]
5166 fn cuda_column_exposes_runtime_allocation_identity_snapshot() {
5167 let _: fn(&CudaColumn) -> Result<Option<RuntimeAllocationIdentity>> =
5168 CudaColumn::runtime_allocation_identity;
5169 }
5170
5171 struct FailAfterDeallocateResource {
5172 inner: DirectCudaResource,
5173 deallocate_calls: Arc<AtomicU64>,
5174 }
5175
5176 struct FailFirstAllocationResource {
5177 inner: DirectCudaResource,
5178 fail_next: std::sync::atomic::AtomicBool,
5179 }
5180
5181 impl DeviceMemoryResource for FailFirstAllocationResource {
5182 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
5183 if self.fail_next.swap(false, Ordering::SeqCst) {
5184 return Err(ResourceError::Driver(
5185 "injected allocation failure".to_string(),
5186 ));
5187 }
5188 self.inner.materialize(request)
5189 }
5190
5191 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
5192 self.inner.allocation_accounting()
5193 }
5194
5195 fn access_dependencies(
5196 &self,
5197 block: BlockId,
5198 bytes: usize,
5199 ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
5200 self.inner.access_dependencies(block, bytes)
5201 }
5202
5203 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
5204 self.inner.deallocate(block)
5205 }
5206
5207 fn device_ordinal(&self) -> u32 {
5208 self.inner.device_ordinal()
5209 }
5210
5211 fn bytes_outstanding(&self) -> usize {
5212 self.inner.bytes_outstanding()
5213 }
5214 }
5215
5216 impl DeviceMemoryResource for FailAfterDeallocateResource {
5217 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
5218 self.inner.materialize(request)
5219 }
5220
5221 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
5222 self.inner.allocation_accounting()
5223 }
5224
5225 fn access_dependencies(
5226 &self,
5227 block: BlockId,
5228 bytes: usize,
5229 ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
5230 self.inner.access_dependencies(block, bytes)
5231 }
5232
5233 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
5234 self.inner.deallocate(block)?;
5235 self.deallocate_calls.fetch_add(1, Ordering::SeqCst);
5236 Err(ResourceError::Driver(
5237 "injected deallocation completion failure".to_string(),
5238 ))
5239 }
5240
5241 fn device_ordinal(&self) -> u32 {
5242 self.inner.device_ordinal()
5243 }
5244
5245 fn bytes_outstanding(&self) -> usize {
5246 self.inner.bytes_outstanding()
5247 }
5248 }
5249
5250 fn try_device() -> Option<Arc<CudaDevice>> {
5251 match CudaDevice::new(0) {
5252 Ok(d) => Some(Arc::new(d)),
5253 Err(e) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
5254 panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {e}")
5255 }
5256 Err(e) => {
5257 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
5258 None
5259 }
5260 }
5261 }
5262
5263 fn assert_memory_pressure(
5264 error: XlogError,
5265 expected_context: &str,
5266 expected_required: u64,
5267 expected_budget: u64,
5268 ) {
5269 match error {
5270 XlogError::ResourceExhausted {
5271 context,
5272 estimated_bytes,
5273 budget_bytes,
5274 } => {
5275 assert_eq!(context, expected_context);
5276 assert_eq!(estimated_bytes, expected_required);
5277 assert_eq!(budget_bytes, expected_budget);
5278 }
5279 other => panic!("expected ResourceExhausted, got {other:?}"),
5280 }
5281 }
5282
5283 #[test]
5285 fn test_cuda_buffer_empty() {
5286 let Some(device) = try_device() else {
5287 return;
5288 };
5289 let budget = MemoryBudget::with_limit(1024 * 1024);
5290 let manager = Arc::new(GpuMemoryManager::new(device, budget));
5291 let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
5292 manager
5293 .device()
5294 .inner()
5295 .htod_sync_copy_into(&[0u32], &mut d_num_rows)
5296 .unwrap();
5297 let buffer = CudaBuffer::from_columns(Vec::new(), 0, d_num_rows, Schema::new(vec![]));
5298 assert!(buffer.is_empty());
5299 assert_eq!(buffer.num_rows(), 0);
5300 assert_eq!(buffer.arity(), 0);
5301 assert_eq!(buffer.estimated_bytes(), 0);
5302 }
5303
5304 #[test]
5305 fn test_cuda_buffer_schema() {
5306 let schema = Schema::new(vec![
5307 ("a".to_string(), ScalarType::U32),
5308 ("b".to_string(), ScalarType::U64),
5309 ]);
5310
5311 let Some(device) = try_device() else {
5312 return;
5313 };
5314 let budget = MemoryBudget::with_limit(1024 * 1024);
5315 let manager = Arc::new(GpuMemoryManager::new(device, budget));
5316 let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
5317 manager
5318 .device()
5319 .inner()
5320 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
5321 .unwrap();
5322
5323 let col_a = CudaColumn::owned(manager.alloc::<u8>(100 * 4).unwrap()); let col_b = CudaColumn::owned(manager.alloc::<u8>(100 * 8).unwrap()); let buffer = CudaBuffer::from_columns(vec![col_a, col_b], 100, d_num_rows, schema.clone());
5327
5328 assert_eq!(buffer.num_rows(), 100);
5329 assert_eq!(buffer.arity(), 2);
5330 assert_eq!(buffer.estimated_bytes(), 1200);
5332 assert_eq!(buffer.schema(), &schema);
5333 }
5334
5335 #[test]
5337 fn test_memory_manager_creation() {
5338 let Some(device) = try_device() else {
5339 return;
5340 };
5341 let budget = MemoryBudget::with_limit(1024 * 1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
5343
5344 assert_eq!(manager.allocated_bytes(), 0);
5345 assert_eq!(manager.budget().device_bytes, 1024 * 1024);
5346 assert_eq!(manager.remaining_bytes(), 1024 * 1024);
5347 }
5348
5349 #[test]
5350 fn test_memory_manager_alloc() {
5351 let Some(device) = try_device() else {
5352 return;
5353 };
5354 let budget = MemoryBudget::with_limit(1024 * 1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
5356
5357 let _slice = manager
5359 .alloc::<u32>(256)
5360 .expect("Allocation should succeed");
5361
5362 assert_eq!(manager.allocated_bytes(), 1024);
5363 assert_eq!(manager.remaining_bytes(), 1024 * 1024 - 1024);
5364 }
5365
5366 #[test]
5367 fn test_memory_manager_budget_exceeded() {
5368 let Some(device) = try_device() else {
5369 return;
5370 };
5371 let budget = MemoryBudget::with_limit(1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
5373
5374 let result = manager.alloc::<u32>(512);
5376
5377 assert!(result.is_err());
5378 if let Err(XlogError::ResourceExhausted {
5379 estimated_bytes,
5380 budget_bytes,
5381 ..
5382 }) = result
5383 {
5384 assert_eq!(estimated_bytes, 2048);
5385 assert_eq!(budget_bytes, 1024);
5386 } else {
5387 panic!("Expected ResourceExhausted error");
5388 }
5389 }
5390
5391 #[test]
5392 fn test_memory_manager_check_budget() {
5393 let Some(device) = try_device() else {
5394 return;
5395 };
5396 let budget = MemoryBudget::with_limit(1000);
5397 let manager = Arc::new(GpuMemoryManager::new(device, budget));
5398
5399 assert!(manager.check_budget(500).is_ok());
5401
5402 assert!(manager.check_budget(1001).is_err());
5404 }
5405
5406 #[test]
5407 fn check_budget_does_not_reserve_a_multi_allocation_request() {
5408 let Some(device) = try_device() else {
5409 return;
5410 };
5411 let manager = Arc::new(GpuMemoryManager::new(
5412 device,
5413 MemoryBudget::with_limit(8192),
5414 ));
5415 let checked = Arc::new(std::sync::Barrier::new(2));
5416 let competitor_allocated = Arc::new(std::sync::Barrier::new(2));
5417 let release_competitor = Arc::new(std::sync::Barrier::new(2));
5418
5419 let checked_in_thread = Arc::clone(&checked);
5420 let allocated_in_thread = Arc::clone(&competitor_allocated);
5421 let release_in_thread = Arc::clone(&release_competitor);
5422 let competitor_manager = Arc::clone(&manager);
5423 let competitor = std::thread::spawn(move || {
5424 checked_in_thread.wait();
5425 let allocation = competitor_manager
5426 .alloc::<u8>(4096)
5427 .expect("competing allocation must fit after the check-only query");
5428 allocated_in_thread.wait();
5429 release_in_thread.wait();
5430 drop(allocation);
5431 });
5432
5433 manager
5434 .check_budget(8192)
5435 .expect("the complete request fits before the competing allocation");
5436 checked.wait();
5437 competitor_allocated.wait();
5438 let first = manager
5439 .alloc::<u8>(4096)
5440 .expect("the first materialized allocation must fit");
5441 let second = manager.alloc::<u8>(4096);
5442 assert!(
5443 matches!(second, Err(XlogError::ResourceExhausted { .. })),
5444 "a check-only query cannot protect later allocations from a competitor"
5445 );
5446
5447 release_competitor.wait();
5448 competitor.join().expect("competitor thread panicked");
5449 drop(first);
5450 assert_eq!(manager.allocated_bytes(), 0);
5451 assert_eq!(manager.remaining_bytes(), 8192);
5452 }
5453
5454 #[test]
5455 fn reservation_rejects_the_complete_request_before_any_allocation() {
5456 let Some(device) = try_device() else {
5457 return;
5458 };
5459 let manager = Arc::new(GpuMemoryManager::new(
5460 device,
5461 MemoryBudget::with_limit(4095),
5462 ));
5463 manager.reset_alloc_count();
5464
5465 let error = manager
5466 .reserve_bytes(4096)
5467 .expect_err("a request one byte above the budget must be rejected atomically");
5468
5469 assert_memory_pressure(
5470 error,
5471 "GPU memory pressure: layer=manager_reserve current_bytes=0 requested_bytes=4096 required_bytes=4096 required_u64_overflow=false budget_bytes=4095 prior_peak_bytes=0",
5472 4096,
5473 4095,
5474 );
5475 assert_eq!(manager.alloc_count(), 0);
5476 assert_eq!(manager.allocated_bytes(), 0);
5477 assert_eq!(manager.peak_bytes(), 0);
5478 assert_eq!(manager.remaining_bytes(), 4095);
5479 }
5480
5481 #[test]
5482 fn competing_complete_reservations_cannot_both_claim_the_budget() {
5483 let Some(device) = try_device() else {
5484 return;
5485 };
5486 let manager = Arc::new(GpuMemoryManager::new(
5487 device,
5488 MemoryBudget::with_limit(4096),
5489 ));
5490 let attempted = Arc::new(std::sync::Barrier::new(2));
5491
5492 let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
5493 std::thread::spawn(move || {
5494 let reservation = manager.reserve_bytes(4096);
5495 attempted.wait();
5496 let admitted = reservation.is_ok();
5497 drop(reservation);
5498 admitted
5499 })
5500 };
5501 let left = reserve(Arc::clone(&manager), Arc::clone(&attempted));
5502 let right = reserve(Arc::clone(&manager), Arc::clone(&attempted));
5503
5504 let admitted = [
5505 left.join().expect("left reservation thread panicked"),
5506 right.join().expect("right reservation thread panicked"),
5507 ];
5508 assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
5509 assert_eq!(manager.allocated_bytes(), 0);
5510 assert_eq!(manager.remaining_bytes(), 4096);
5511 }
5512
5513 #[test]
5514 fn competing_managers_share_one_runtime_reservation_budget() {
5515 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
5516 return;
5517 };
5518 let left_manager = Arc::new(GpuMemoryManager::with_runtime(
5519 Arc::clone(&device),
5520 MemoryBudget::with_limit(4096),
5521 Arc::clone(&runtime),
5522 ));
5523 let right_manager = Arc::new(GpuMemoryManager::with_runtime(
5524 device,
5525 MemoryBudget::with_limit(4096),
5526 runtime,
5527 ));
5528 let attempted = Arc::new(std::sync::Barrier::new(2));
5529
5530 let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
5531 std::thread::spawn(move || {
5532 let reservation = manager.reserve_bytes(4096);
5533 attempted.wait();
5534 let admitted = reservation.is_ok();
5535 drop(reservation);
5536 admitted
5537 })
5538 };
5539 let left = reserve(left_manager, Arc::clone(&attempted));
5540 let right = reserve(right_manager, attempted);
5541
5542 let admitted = [
5543 left.join().expect("left reservation thread panicked"),
5544 right.join().expect("right reservation thread panicked"),
5545 ];
5546 assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
5547 }
5548
5549 #[test]
5550 fn runtime_reservation_blocks_competing_ordinary_allocation_until_materialized() {
5551 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
5552 return;
5553 };
5554 let reserving_manager = Arc::new(GpuMemoryManager::with_runtime(
5555 Arc::clone(&device),
5556 MemoryBudget::with_limit(4096),
5557 Arc::clone(&runtime),
5558 ));
5559 let competing_manager = Arc::new(GpuMemoryManager::with_runtime(
5560 device,
5561 MemoryBudget::with_limit(4096),
5562 runtime,
5563 ));
5564 let mut reservation = reserving_manager
5565 .reserve_bytes(4096)
5566 .expect("complete runtime reservation");
5567
5568 assert!(matches!(
5569 competing_manager.alloc::<u8>(1),
5570 Err(XlogError::ResourceExhausted { .. })
5571 ));
5572 assert_eq!(competing_manager.allocated_bytes(), 0);
5573 assert_eq!(competing_manager.remaining_bytes(), 4096);
5574
5575 let allocation = reservation
5576 .alloc::<u8>(4096)
5577 .expect("reserved bytes cannot be stolen by an ordinary allocator");
5578 assert_eq!(reserving_manager.allocated_bytes(), 4096);
5579 drop(reservation);
5580 drop(allocation);
5581 assert_eq!(reserving_manager.allocated_bytes(), 0);
5582 }
5583
5584 #[test]
5585 fn global_budget_rejects_complete_manifest_before_first_inner_allocation() {
5586 let Some((device, runtime, sink)) = try_runtime_with_logging_budget(4095) else {
5587 return;
5588 };
5589 let manager = Arc::new(GpuMemoryManager::with_runtime(
5590 device,
5591 MemoryBudget::with_limit(8192),
5592 runtime,
5593 ));
5594
5595 let error = manager
5596 .reserve_bytes(4096)
5597 .expect_err("the complete request is one byte above the runtime budget");
5598 assert_memory_pressure(
5599 error,
5600 "GPU memory pressure: layer=device_runtime current_bytes=0 requested_bytes=4096 required_bytes=4096 required_u64_overflow=false budget_bytes=4095 prior_peak_bytes=0",
5601 4096,
5602 4095,
5603 );
5604 assert!(sink.snapshot().is_empty());
5605 assert_eq!(manager.allocated_bytes(), 0);
5606 assert_eq!(manager.remaining_bytes(), 8192);
5607 }
5608
5609 #[test]
5610 fn runtime_backed_reservation_requires_a_reservable_global_budget() {
5611 let Some((device, runtime)) = try_unbudgeted_runtime() else {
5612 return;
5613 };
5614 let manager = Arc::new(GpuMemoryManager::with_runtime(
5615 device,
5616 MemoryBudget::with_limit(4096),
5617 runtime,
5618 ));
5619
5620 let error = manager
5621 .reserve_bytes(1024)
5622 .expect_err("an unbudgeted runtime cannot promise complete admission");
5623 assert!(
5624 matches!(error, XlogError::Kernel(ref detail) if detail.contains("reservable global budget")),
5625 "unexpected error: {error}"
5626 );
5627 assert_eq!(manager.allocated_bytes(), 0);
5628 assert_eq!(manager.remaining_bytes(), 4096);
5629 }
5630
5631 #[test]
5632 fn reservation_materializes_exactly_its_declared_bytes() {
5633 let Some(device) = try_device() else {
5634 return;
5635 };
5636 let manager = Arc::new(GpuMemoryManager::new(
5637 device,
5638 MemoryBudget::with_limit(4096),
5639 ));
5640 let mut reservation = manager
5641 .reserve_bytes(4096)
5642 .expect("the exact complete request must fit");
5643 assert_eq!(reservation.total_bytes(), 4096);
5644 assert_eq!(reservation.remaining_bytes(), 4096);
5645 assert_eq!(reservation.used_bytes(), 0);
5646 assert_eq!(manager.remaining_bytes(), 0);
5647
5648 let words = reservation
5649 .alloc::<u32>(512)
5650 .expect("first reserved allocation");
5651 let bytes = reservation
5652 .alloc::<u8>(2048)
5653 .expect("second reserved allocation");
5654 assert_eq!(reservation.remaining_bytes(), 0);
5655 assert_eq!(reservation.used_bytes(), 4096);
5656 assert_eq!(manager.allocated_bytes(), 4096);
5657 assert_eq!(manager.peak_bytes(), 4096);
5658
5659 let error = match reservation.alloc::<u8>(1) {
5660 Err(error) => error,
5661 Ok(_) => panic!("a reservation cannot materialize more than its declaration"),
5662 };
5663 assert_memory_pressure(
5664 error,
5665 "GPU memory pressure: layer=manager_reservation_alloc current_bytes=4096 requested_bytes=1 required_bytes=4097 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=4096",
5666 4097,
5667 4096,
5668 );
5669
5670 drop(reservation);
5671 assert_eq!(manager.remaining_bytes(), 0);
5672 drop(bytes);
5673 assert_eq!(manager.remaining_bytes(), 2048);
5674 drop(words);
5675 assert_eq!(manager.allocated_bytes(), 0);
5676 assert_eq!(manager.remaining_bytes(), 4096);
5677 }
5678
5679 #[test]
5680 fn failed_reserved_raw_allocation_preserves_unused_claim_until_drop() {
5681 let Some((device, runtime)) = try_runtime_with_first_allocation_failure(4096) else {
5682 return;
5683 };
5684 let manager = Arc::new(GpuMemoryManager::with_runtime(
5685 device,
5686 MemoryBudget::with_limit(4096),
5687 runtime,
5688 ));
5689 let mut reservation = manager
5690 .reserve_bytes(4096)
5691 .expect("local complete request must fit");
5692
5693 let error = reservation
5694 .alloc_raw(2048, AllocTag::UNTAGGED)
5695 .expect_err("the injected underlying allocation must fail");
5696 assert!(
5697 matches!(error, XlogError::Kernel(ref detail) if detail.contains("injected allocation failure")),
5698 "unexpected error: {error}"
5699 );
5700 assert_eq!(reservation.remaining_bytes(), 4096);
5701 assert_eq!(manager.allocated_bytes(), 0);
5702 assert_eq!(manager.remaining_bytes(), 0);
5703
5704 let allocation = reservation
5705 .alloc_raw(1024, AllocTag::UNTAGGED)
5706 .expect("a smaller raw suballocation must fit both budgets");
5707 assert_eq!(reservation.remaining_bytes(), 3072);
5708 assert_eq!(manager.allocated_bytes(), 1024);
5709
5710 drop(reservation);
5711 assert_eq!(manager.remaining_bytes(), 3072);
5712 drop(allocation);
5713 assert_eq!(manager.allocated_bytes(), 0);
5714 assert_eq!(manager.remaining_bytes(), 4096);
5715 }
5716
5717 #[test]
5718 fn partial_typed_materialization_releases_each_byte_once_in_either_drop_order() {
5719 let Some(device) = try_device() else {
5720 return;
5721 };
5722 let manager = Arc::new(GpuMemoryManager::new(
5723 device,
5724 MemoryBudget::with_limit(4096),
5725 ));
5726 let mut reservation = manager.reserve_bytes(4096).expect("complete request");
5727 let allocation = reservation
5728 .alloc::<u8>(1024)
5729 .expect("partial materialization");
5730 let error = match reservation.alloc::<u8>(4096) {
5731 Err(error) => error,
5732 Ok(_) => panic!("the remaining reservation is only 3072 bytes"),
5733 };
5734 assert_memory_pressure(
5735 error,
5736 "GPU memory pressure: layer=manager_reservation_alloc current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
5737 5120,
5738 4096,
5739 );
5740 assert_eq!(reservation.remaining_bytes(), 3072);
5741
5742 drop(allocation);
5743 assert_eq!(manager.allocated_bytes(), 0);
5744 assert_eq!(manager.remaining_bytes(), 1024);
5745 drop(reservation);
5746 assert_eq!(manager.remaining_bytes(), 4096);
5747 }
5748
5749 #[test]
5750 fn ordinary_allocations_and_complete_reservations_share_one_budget() {
5751 let Some(device) = try_device() else {
5752 return;
5753 };
5754 let manager = Arc::new(GpuMemoryManager::new(
5755 device,
5756 MemoryBudget::with_limit(4096),
5757 ));
5758 let ordinary = manager
5759 .alloc::<u8>(1024)
5760 .expect("ordinary allocation before reservation");
5761 let mut reservation = manager
5762 .reserve_bytes(3072)
5763 .expect("reservation must fit beside the ordinary owner");
5764 assert_eq!(manager.remaining_bytes(), 0);
5765 assert!(matches!(
5766 manager.alloc::<u8>(1),
5767 Err(XlogError::ResourceExhausted { .. })
5768 ));
5769
5770 let reserved = reservation
5771 .alloc::<u8>(3072)
5772 .expect("reserved allocation bypasses the already-paid local claim");
5773 assert_eq!(manager.allocated_bytes(), 4096);
5774 drop(reservation);
5775 drop(reserved);
5776 assert_eq!(manager.allocated_bytes(), 1024);
5777 assert_eq!(manager.remaining_bytes(), 3072);
5778 drop(ordinary);
5779 assert_eq!(manager.allocated_bytes(), 0);
5780 assert_eq!(manager.remaining_bytes(), 4096);
5781 }
5782
5783 #[test]
5784 fn public_accounting_mutators_refuse_live_reservations_and_allocations() {
5785 let Some(device) = try_device() else {
5786 return;
5787 };
5788 let manager = Arc::new(GpuMemoryManager::new(
5789 device,
5790 MemoryBudget::with_limit(4096),
5791 ));
5792 let reservation = manager.reserve_bytes(2048).expect("reservation");
5793
5794 let free_error = manager
5795 .record_free(1)
5796 .expect_err("public accounting cannot release a live reservation");
5797 assert!(
5798 matches!(free_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
5799 );
5800 let reset_error = manager
5801 .reset_tracking()
5802 .expect_err("public reset cannot erase a live reservation");
5803 assert!(
5804 matches!(reset_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
5805 );
5806 assert_eq!(manager.remaining_bytes(), 2048);
5807
5808 drop(reservation);
5809 let allocation = manager.alloc::<u8>(1024).expect("allocation");
5810 assert!(manager.record_free(1024).is_err());
5811 assert!(manager.reset_tracking().is_err());
5812 assert_eq!(manager.allocated_bytes(), 1024);
5813 assert_eq!(manager.remaining_bytes(), 3072);
5814
5815 drop(allocation);
5816 manager
5817 .reset_tracking()
5818 .expect("quiescent accounting can reset diagnostic peaks");
5819 assert_eq!(manager.allocated_bytes(), 0);
5820 assert_eq!(manager.remaining_bytes(), 4096);
5821 }
5822
5823 #[test]
5824 fn public_record_free_refuses_underflow_without_mutation() {
5825 let Some(device) = try_device() else {
5826 return;
5827 };
5828 let manager = Arc::new(GpuMemoryManager::new(
5829 device,
5830 MemoryBudget::with_limit(4096),
5831 ));
5832
5833 let error = manager
5834 .record_free(1)
5835 .expect_err("unowned bytes cannot be released from accounting");
5836 assert!(matches!(error, XlogError::Kernel(detail) if detail.contains("underflow")));
5837 assert_eq!(manager.allocated_bytes(), 0);
5838 assert_eq!(manager.remaining_bytes(), 4096);
5839 manager
5840 .record_free(0)
5841 .expect("zero-byte release is a no-op");
5842 }
5843
5844 #[test]
5845 fn typed_post_release_error_honors_physical_release_proof() {
5846 let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
5847 else {
5848 return;
5849 };
5850 let manager = Arc::new(GpuMemoryManager::with_runtime(
5851 device,
5852 MemoryBudget::with_limit(4096),
5853 Arc::clone(&runtime),
5854 ));
5855 let allocation = manager.alloc::<u8>(1024).expect("typed allocation");
5856
5857 drop(allocation);
5858
5859 assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
5860 assert_eq!(runtime.bytes_outstanding(), 0);
5861 assert_eq!(manager.allocated_bytes(), 0);
5862 assert_eq!(manager.remaining_bytes(), 4096);
5863 assert_eq!(manager.deallocation_failure_count(), 0);
5864 assert_eq!(manager.deallocation_failure_bytes(), 0);
5865 manager.reset_tracking().unwrap();
5866 }
5867
5868 #[test]
5869 fn raw_post_release_error_honors_physical_release_proof() {
5870 let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
5871 else {
5872 return;
5873 };
5874 let manager = Arc::new(GpuMemoryManager::with_runtime(
5875 device,
5876 MemoryBudget::with_limit(4096),
5877 Arc::clone(&runtime),
5878 ));
5879 let allocation = manager
5880 .alloc_raw(512, AllocTag::UNTAGGED)
5881 .expect("raw allocation");
5882
5883 drop(allocation);
5884
5885 assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
5886 assert_eq!(runtime.bytes_outstanding(), 0);
5887 assert_eq!(manager.allocated_bytes(), 0);
5888 assert_eq!(manager.remaining_bytes(), 4096);
5889 assert_eq!(manager.deallocation_failure_count(), 0);
5890 assert_eq!(manager.deallocation_failure_bytes(), 0);
5891 manager.reset_tracking().unwrap();
5892 }
5893
5894 #[test]
5895 fn test_memory_manager_multiple_allocs() {
5896 let Some(device) = try_device() else {
5897 return;
5898 };
5899 let budget = MemoryBudget::with_limit(4096); let manager = Arc::new(GpuMemoryManager::new(device, budget));
5901
5902 let _slice1 = manager
5904 .alloc::<u32>(256)
5905 .expect("First allocation should succeed");
5906 assert_eq!(manager.allocated_bytes(), 1024);
5907
5908 let _slice2 = manager
5910 .alloc::<u32>(256)
5911 .expect("Second allocation should succeed");
5912 assert_eq!(manager.allocated_bytes(), 2048);
5913
5914 let result = manager.alloc::<u32>(1024); assert!(result.is_err());
5917
5918 assert_eq!(manager.allocated_bytes(), 2048);
5920 }
5921
5922 #[test]
5923 fn test_memory_manager_record_free() {
5924 let Some(device) = try_device() else {
5925 return;
5926 };
5927 let budget = MemoryBudget::with_limit(4096);
5928 let manager = Arc::new(GpuMemoryManager::new(device, budget));
5929
5930 let slice = manager
5932 .alloc::<u32>(256)
5933 .expect("Allocation should succeed");
5934 assert_eq!(manager.allocated_bytes(), 1024);
5935
5936 drop(slice);
5938 assert_eq!(manager.allocated_bytes(), 0);
5939 assert_eq!(manager.remaining_bytes(), 4096);
5940 }
5941
5942 #[test]
5943 fn test_memory_manager_peak_tracking() {
5944 let Some(device) = try_device() else {
5945 return;
5946 };
5947 let budget = MemoryBudget::with_limit(8192);
5948 let manager = Arc::new(GpuMemoryManager::new(device, budget));
5949
5950 let a = manager.alloc::<u32>(256).expect("alloc a"); let b = manager.alloc::<u32>(512).expect("alloc b"); assert_eq!(manager.peak_bytes(), 3072);
5953
5954 drop(b);
5956 assert_eq!(manager.allocated_bytes(), 1024);
5957 assert_eq!(manager.peak_bytes(), 3072);
5958
5959 manager.reset_peak();
5961 assert_eq!(manager.peak_bytes(), 1024);
5962
5963 let c = manager.alloc::<u32>(128).expect("alloc c"); assert_eq!(manager.peak_bytes(), 1536);
5965
5966 drop(c);
5967 drop(a);
5968 assert_eq!(manager.allocated_bytes(), 0);
5969 assert_eq!(manager.peak_bytes(), 1536);
5970 }
5971
5972 #[test]
5973 fn memory_pressure_alloc_reports_exact_cumulative_pressure() {
5974 let Some(device) = try_device() else {
5975 return;
5976 };
5977 let manager = Arc::new(GpuMemoryManager::new(
5978 device,
5979 MemoryBudget::with_limit(4096),
5980 ));
5981 let baseline = manager.alloc::<u8>(1024).expect("baseline allocation");
5982
5983 let error = match manager.alloc::<u8>(4096) {
5984 Err(error) => error,
5985 Ok(_) => panic!("cumulative allocation must exceed the local budget"),
5986 };
5987
5988 assert_memory_pressure(
5989 error,
5990 "GPU memory pressure: layer=manager_alloc current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
5991 5120,
5992 4096,
5993 );
5994 assert_eq!(manager.allocated_bytes(), 1024);
5995 assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
5996 drop(baseline);
5997 }
5998
5999 #[test]
6000 fn memory_pressure_check_budget_reports_exact_cumulative_pressure() {
6001 let Some(device) = try_device() else {
6002 return;
6003 };
6004 let manager = Arc::new(GpuMemoryManager::new(
6005 device,
6006 MemoryBudget::with_limit(1000),
6007 ));
6008 let baseline = manager.alloc::<u8>(512).expect("baseline allocation");
6009
6010 let error = manager
6011 .check_budget(600)
6012 .expect_err("cumulative request must exceed the local budget");
6013
6014 assert_memory_pressure(
6015 error,
6016 "GPU memory pressure: layer=manager_check_budget current_bytes=512 requested_bytes=600 required_bytes=1112 required_u64_overflow=false budget_bytes=1000 prior_peak_bytes=512",
6017 1112,
6018 1000,
6019 );
6020 assert_eq!(manager.allocated_bytes(), 512);
6021 assert_eq!(
6022 manager.peak_bytes(),
6023 512,
6024 "check-only refusal must not raise peak"
6025 );
6026 drop(baseline);
6027 }
6028
6029 #[test]
6030 fn memory_pressure_check_budget_reports_u64_representability_overflow() {
6031 let Some(device) = try_device() else {
6032 return;
6033 };
6034 let manager = Arc::new(GpuMemoryManager::new(
6035 device,
6036 MemoryBudget::with_limit(u64::MAX),
6037 ));
6038 manager
6039 .accounting
6040 .budget_reserved
6041 .store(u64::MAX - 3, Ordering::SeqCst);
6042 manager
6043 .accounting
6044 .allocated
6045 .store(u64::MAX - 3, Ordering::SeqCst);
6046 manager
6047 .accounting
6048 .peak
6049 .store(u64::MAX - 3, Ordering::SeqCst);
6050
6051 let error = manager
6052 .check_budget(8)
6053 .expect_err("the exact required byte count is not representable as u64");
6054
6055 assert_memory_pressure(
6056 error,
6057 "GPU memory pressure: layer=manager_check_budget current_bytes=18446744073709551612 requested_bytes=8 required_bytes=18446744073709551620 required_u64_overflow=true budget_bytes=18446744073709551615 prior_peak_bytes=18446744073709551612",
6058 u64::MAX,
6059 u64::MAX,
6060 );
6061 assert_eq!(manager.allocated_bytes(), u64::MAX - 3);
6062 assert_eq!(manager.peak_bytes(), u64::MAX - 3);
6063
6064 manager
6066 .accounting
6067 .budget_reserved
6068 .store(0, Ordering::SeqCst);
6069 manager.accounting.allocated.store(0, Ordering::SeqCst);
6070 manager.accounting.peak.store(0, Ordering::SeqCst);
6071 }
6072
6073 #[test]
6074 fn test_cuda_buffer_from_columns() {
6075 let Some(device) = try_device() else {
6076 return;
6077 };
6078 let budget = MemoryBudget::with_limit(1024 * 1024);
6079 let manager = Arc::new(GpuMemoryManager::new(device, budget));
6080
6081 let schema = Schema::new(vec![
6082 ("col1".to_string(), ScalarType::U32),
6083 ("col2".to_string(), ScalarType::U32),
6084 ]);
6085
6086 let col1 = manager.alloc::<u8>(400).expect("Alloc col1");
6088 let col2 = manager.alloc::<u8>(400).expect("Alloc col2");
6089
6090 let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
6091 manager
6092 .device()
6093 .inner()
6094 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
6095 .expect("Upload row count");
6096 let buffer =
6097 CudaBuffer::from_columns(vec![col1.into(), col2.into()], 100, d_num_rows, schema);
6098
6099 assert_eq!(buffer.num_rows(), 100);
6100 assert_eq!(buffer.arity(), 2);
6101 assert!(!buffer.is_empty());
6102 assert!(buffer.column(0).is_some());
6103 assert!(buffer.column(1).is_some());
6104 assert!(buffer.column(2).is_none());
6105 }
6106
6107 #[test]
6108 fn test_cuda_buffer_from_columns_mismatch() {
6109 let schema = Schema::new(vec![
6110 ("col1".to_string(), ScalarType::U32),
6111 ("col2".to_string(), ScalarType::U32),
6112 ]);
6113
6114 let Some(device) = try_device() else {
6115 return;
6116 };
6117 let budget = MemoryBudget::with_limit(1024 * 1024);
6118 let manager = Arc::new(GpuMemoryManager::new(device, budget));
6119 let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
6120 manager
6121 .device()
6122 .inner()
6123 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
6124 .expect("Upload row count");
6125
6126 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6128 CudaBuffer::from_columns(vec![], 100, d_num_rows, schema);
6129 }));
6130 assert!(
6131 result.is_err(),
6132 "Expected from_columns to panic on schema mismatch"
6133 );
6134 }
6135
6136 fn try_runtime() -> Option<(
6137 Arc<CudaDevice>,
6138 Arc<crate::device_runtime::XlogDeviceRuntime>,
6139 )> {
6140 use crate::device_runtime::{
6141 AsyncCudaResource, DeviceMemoryResource, GlobalDeviceBudget, StreamPool,
6142 XlogDeviceRuntime,
6143 };
6144 let device = try_device()?;
6145 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6146 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
6147 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
6148 );
6149 let budget: Box<dyn DeviceMemoryResource + Send + Sync> =
6150 Box::new(GlobalDeviceBudget::new(async_resource, 64 * 1024 * 1024));
6151 Some((
6152 Arc::clone(&device),
6153 Arc::new(XlogDeviceRuntime::with_resource(
6154 Arc::clone(&device),
6155 0,
6156 pool,
6157 budget,
6158 )),
6159 ))
6160 }
6161
6162 fn try_unbudgeted_runtime() -> Option<(
6163 Arc<CudaDevice>,
6164 Arc<crate::device_runtime::XlogDeviceRuntime>,
6165 )> {
6166 use crate::device_runtime::{
6167 AsyncCudaResource, DeviceMemoryResource, StreamPool, XlogDeviceRuntime,
6168 };
6169 let device = try_device()?;
6170 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6171 let resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
6172 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
6173 );
6174 Some((
6175 Arc::clone(&device),
6176 Arc::new(XlogDeviceRuntime::with_resource(
6177 Arc::clone(&device),
6178 0,
6179 pool,
6180 resource,
6181 )),
6182 ))
6183 }
6184
6185 fn try_runtime_with_deallocation_failure() -> Option<(
6186 Arc<CudaDevice>,
6187 Arc<crate::device_runtime::XlogDeviceRuntime>,
6188 Arc<AtomicU64>,
6189 )> {
6190 use crate::device_runtime::{StreamPool, XlogDeviceRuntime};
6191 let device = try_device()?;
6192 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6193 let deallocate_calls = Arc::new(AtomicU64::new(0));
6194 let resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6195 Box::new(FailAfterDeallocateResource {
6196 inner: DirectCudaResource::new(Arc::clone(&device), 0),
6197 deallocate_calls: Arc::clone(&deallocate_calls),
6198 });
6199 Some((
6200 Arc::clone(&device),
6201 Arc::new(XlogDeviceRuntime::with_resource(
6202 Arc::clone(&device),
6203 0,
6204 pool,
6205 resource,
6206 )),
6207 deallocate_calls,
6208 ))
6209 }
6210
6211 fn try_runtime_with_first_allocation_failure(
6212 limit: usize,
6213 ) -> Option<(
6214 Arc<CudaDevice>,
6215 Arc<crate::device_runtime::XlogDeviceRuntime>,
6216 )> {
6217 use crate::device_runtime::{GlobalDeviceBudget, StreamPool, XlogDeviceRuntime};
6218 let device = try_device()?;
6219 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6220 let failing_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6221 Box::new(FailFirstAllocationResource {
6222 inner: DirectCudaResource::new(Arc::clone(&device), 0),
6223 fail_next: std::sync::atomic::AtomicBool::new(true),
6224 });
6225 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6226 Box::new(GlobalDeviceBudget::new(failing_resource, limit));
6227 Some((
6228 Arc::clone(&device),
6229 Arc::new(XlogDeviceRuntime::with_resource(
6230 Arc::clone(&device),
6231 0,
6232 pool,
6233 budget_resource,
6234 )),
6235 ))
6236 }
6237
6238 fn try_runtime_with_budget(
6239 limit: usize,
6240 ) -> Option<(
6241 Arc<CudaDevice>,
6242 Arc<crate::device_runtime::XlogDeviceRuntime>,
6243 )> {
6244 use crate::device_runtime::{
6245 DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, StreamPool,
6246 XlogDeviceRuntime,
6247 };
6248 let device = try_device()?;
6249 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6250 let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6251 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
6252 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6253 Box::new(GlobalDeviceBudget::new(direct_resource, limit));
6254 Some((
6255 Arc::clone(&device),
6256 Arc::new(XlogDeviceRuntime::with_resource(
6257 Arc::clone(&device),
6258 0,
6259 pool,
6260 budget_resource,
6261 )),
6262 ))
6263 }
6264
6265 fn try_runtime_with_logging_budget(
6266 limit: usize,
6267 ) -> Option<(
6268 Arc<CudaDevice>,
6269 Arc<crate::device_runtime::XlogDeviceRuntime>,
6270 Arc<crate::device_runtime::InMemorySink>,
6271 )> {
6272 use crate::device_runtime::{
6273 DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, InMemorySink,
6274 LoggingResource, LoggingSink, StreamPool, XlogDeviceRuntime,
6275 };
6276 let device = try_device()?;
6277 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6278 let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6279 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
6280 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6281 Box::new(GlobalDeviceBudget::new(direct_resource, limit));
6282 let sink = Arc::new(InMemorySink::new());
6283 let logging_sink: Arc<dyn LoggingSink> = sink.clone();
6284 let logging_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6285 Box::new(LoggingResource::new(budget_resource, logging_sink));
6286 Some((
6287 Arc::clone(&device),
6288 Arc::new(XlogDeviceRuntime::with_resource(
6289 Arc::clone(&device),
6290 0,
6291 pool,
6292 logging_resource,
6293 )),
6294 sink,
6295 ))
6296 }
6297
6298 fn pause_request_after_local_reservation(
6299 manager: &GpuMemoryManager,
6300 paused_bytes: u64,
6301 ) -> (Arc<std::sync::Barrier>, Arc<std::sync::Barrier>) {
6302 let entered = Arc::new(std::sync::Barrier::new(2));
6303 let release = Arc::new(std::sync::Barrier::new(2));
6304 let hook_entered = Arc::clone(&entered);
6305 let hook_release = Arc::clone(&release);
6306 *manager
6307 .after_local_reservation_hook
6308 .lock()
6309 .expect("after-local-reservation test hook poisoned") = Some(Arc::new(move |bytes| {
6310 if bytes == paused_bytes {
6311 hook_entered.wait();
6312 hook_release.wait();
6313 }
6314 }));
6315 (entered, release)
6316 }
6317
6318 #[test]
6319 fn memory_pressure_concurrent_alloc_raw_refusal_excludes_provisional_bytes() {
6320 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6321 return;
6322 };
6323 let manager = Arc::new(GpuMemoryManager::with_runtime(
6324 device,
6325 MemoryBudget::with_limit(8192),
6326 runtime,
6327 ));
6328 let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
6329
6330 let refused_manager = Arc::clone(&manager);
6331 let refused = std::thread::spawn(move || {
6332 refused_manager
6333 .alloc_raw(4096, AllocTag::UNTAGGED)
6334 .expect_err("runtime must refuse the paused request")
6335 });
6336 paused.wait();
6337
6338 let admitted = manager
6339 .alloc_raw(1024, AllocTag::UNTAGGED)
6340 .expect("concurrent smaller request must be admitted");
6341 release.wait();
6342 let error = refused.join().expect("paused allocation thread panicked");
6343
6344 assert_memory_pressure(
6345 error,
6346 "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6347 5120,
6348 4096,
6349 );
6350 assert_eq!(manager.allocated_bytes(), 1024);
6351 assert_eq!(
6352 manager.peak_bytes(),
6353 1024,
6354 "refused provisional bytes must never enter the admitted peak"
6355 );
6356 drop(admitted);
6357 }
6358
6359 #[test]
6360 fn memory_pressure_concurrent_typed_alloc_refusal_excludes_provisional_bytes() {
6361 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6362 return;
6363 };
6364 let manager = Arc::new(GpuMemoryManager::with_runtime(
6365 device,
6366 MemoryBudget::with_limit(8192),
6367 runtime,
6368 ));
6369 let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
6370
6371 let refused_manager = Arc::clone(&manager);
6372 let refused = std::thread::spawn(move || match refused_manager.alloc::<u8>(4096) {
6373 Err(error) => error,
6374 Ok(_) => panic!("runtime must refuse the paused typed request"),
6375 });
6376 paused.wait();
6377
6378 let admitted = manager
6379 .alloc::<u8>(1024)
6380 .expect("concurrent smaller typed request must be admitted");
6381 release.wait();
6382 let error = refused.join().expect("paused allocation thread panicked");
6383
6384 assert_memory_pressure(
6385 error,
6386 "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6387 5120,
6388 4096,
6389 );
6390 assert_eq!(manager.allocated_bytes(), 1024);
6391 assert_eq!(
6392 manager.peak_bytes(),
6393 1024,
6394 "refused provisional bytes must never enter the admitted peak"
6395 );
6396 drop(admitted);
6397 }
6398
6399 #[test]
6400 fn memory_pressure_alloc_raw_reports_exact_local_pressure() {
6401 let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
6402 return;
6403 };
6404 let manager = Arc::new(GpuMemoryManager::with_runtime(
6405 device,
6406 MemoryBudget::with_limit(4096),
6407 runtime,
6408 ));
6409 let baseline = manager
6410 .alloc_raw(1024, AllocTag::UNTAGGED)
6411 .expect("baseline allocation");
6412
6413 let error = manager
6414 .alloc_raw(4096, AllocTag::UNTAGGED)
6415 .expect_err("cumulative allocation must exceed the local budget");
6416
6417 assert_memory_pressure(
6418 error,
6419 "GPU memory pressure: layer=manager_alloc_raw current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6420 5120,
6421 4096,
6422 );
6423 assert_eq!(manager.allocated_bytes(), 1024);
6424 assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
6425 drop(baseline);
6426 }
6427
6428 #[test]
6429 fn memory_pressure_alloc_raw_drop_restores_local_headroom() {
6430 let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
6431 return;
6432 };
6433 let manager = Arc::new(GpuMemoryManager::with_runtime(
6434 device,
6435 MemoryBudget::with_limit(1024),
6436 runtime,
6437 ));
6438
6439 let allocation = manager
6440 .alloc_raw(1024, AllocTag::UNTAGGED)
6441 .expect("initial allocation must consume the local budget");
6442 assert_eq!(manager.allocated_bytes(), 1024);
6443 assert_eq!(manager.remaining_bytes(), 0);
6444
6445 drop(allocation);
6446 assert_eq!(manager.allocated_bytes(), 0);
6447 assert_eq!(
6448 manager.remaining_bytes(),
6449 1024,
6450 "dropping an admitted raw allocation must restore local headroom"
6451 );
6452
6453 let replacement = manager
6454 .alloc_raw(1024, AllocTag::UNTAGGED)
6455 .expect("restored local headroom must admit a replacement allocation");
6456 drop(replacement);
6457 }
6458
6459 #[test]
6460 fn memory_pressure_runtime_rejection_preserves_peak() {
6461 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6462 return;
6463 };
6464 let manager = Arc::new(GpuMemoryManager::with_runtime(
6465 device,
6466 MemoryBudget::with_limit(8192),
6467 runtime,
6468 ));
6469 let baseline = manager
6470 .alloc_raw(1024, AllocTag::UNTAGGED)
6471 .expect("baseline allocation");
6472
6473 let error = manager
6474 .alloc_raw(4096, AllocTag::UNTAGGED)
6475 .expect_err("runtime budget must reject the cumulative allocation");
6476
6477 assert_memory_pressure(
6478 error,
6479 "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6480 5120,
6481 4096,
6482 );
6483 assert_eq!(
6484 manager.allocated_bytes(),
6485 1024,
6486 "local reservation must roll back"
6487 );
6488 assert_eq!(
6489 manager.peak_bytes(),
6490 1024,
6491 "runtime refusal must not advance the manager peak"
6492 );
6493 drop(baseline);
6494 }
6495
6496 #[test]
6502 fn test_xlog_owned_dlpack_runtime_backed_carries_identity() {
6503 let Some((device, runtime)) = try_runtime() else {
6504 return;
6505 };
6506 let manager = Arc::new(GpuMemoryManager::with_runtime(
6507 Arc::clone(&device),
6508 MemoryBudget::with_limit(1024 * 1024),
6509 Arc::clone(&runtime),
6510 ));
6511 let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
6512 assert!(slice.runtime_block().is_some());
6513 let stream = device.inner().stream().clone();
6514 let slice = Arc::new(slice);
6515 let tensor = crate::dlpack::export_slice_managed_tensor(
6516 Arc::clone(&slice),
6517 device.ordinal() as i32,
6518 crate::dlpack::DLDataType {
6519 code: crate::dlpack::K_DLUINT,
6520 bits: 8,
6521 lanes: 1,
6522 },
6523 1,
6524 slice.len(),
6525 )
6526 .expect("real export owner");
6527 stream.synchronize().expect("producer ready");
6528 let col = CudaColumn::dlpack_xlog_owned(slice, stream, tensor);
6529 assert!(
6530 !col.is_external(),
6531 "xlog-owned DLPack column must report is_external=false"
6532 );
6533 assert!(
6534 col.runtime_block().is_some(),
6535 "xlog-owned DLPack column over a runtime-backed slice must expose runtime_block"
6536 );
6537 }
6538
6539 #[test]
6546 fn test_xlog_owned_dlpack_legacy_backed_no_runtime_block() {
6547 let Some(device) = try_device() else {
6548 return;
6549 };
6550 let manager = Arc::new(GpuMemoryManager::new(
6551 Arc::clone(&device),
6552 MemoryBudget::with_limit(1024 * 1024),
6553 ));
6554 let slice = manager.alloc::<u8>(64).expect("alloc legacy");
6555 assert!(slice.runtime_block().is_none());
6556 let stream = device.inner().stream().clone();
6557 let slice = Arc::new(slice);
6558 let tensor = crate::dlpack::export_slice_managed_tensor(
6559 Arc::clone(&slice),
6560 device.ordinal() as i32,
6561 crate::dlpack::DLDataType {
6562 code: crate::dlpack::K_DLUINT,
6563 bits: 8,
6564 lanes: 1,
6565 },
6566 1,
6567 slice.len(),
6568 )
6569 .expect("real export owner");
6570 stream.synchronize().expect("producer ready");
6571 let col = CudaColumn::dlpack_xlog_owned(slice, stream, tensor);
6572 assert!(
6573 !col.is_external(),
6574 "xlog-owned DLPack column is owned regardless of allocator backing"
6575 );
6576 assert!(
6577 col.runtime_block().is_none(),
6578 "legacy-backed slice has no runtime block, even when wrapped xlog-owned"
6579 );
6580 }
6581
6582 #[test]
6587 fn test_external_dlpack_remains_external() {
6588 let Some(device) = try_device() else {
6589 return;
6590 };
6591 let stream = device.inner().stream().clone();
6592 let manager = Arc::new(GpuMemoryManager::new(
6593 Arc::clone(&device),
6594 MemoryBudget::with_limit(1024 * 1024),
6595 ));
6596 let slice = Arc::new(manager.alloc::<u8>(1).expect("external allocation"));
6597 let ptr = slice.device_ptr_value();
6598 let tensor = crate::dlpack::export_slice_managed_tensor(
6599 slice,
6600 device.ordinal() as i32,
6601 crate::dlpack::DLDataType {
6602 code: crate::dlpack::K_DLUINT,
6603 bits: 8,
6604 lanes: 1,
6605 },
6606 1,
6607 1,
6608 )
6609 .expect("real external owner");
6610 stream.synchronize().expect("producer ready");
6611 let col = unsafe { CudaColumn::dlpack(ptr, 1, stream, tensor) };
6614 assert!(
6615 col.is_external(),
6616 "true external DLPack column must report is_external=true"
6617 );
6618 assert!(
6619 col.runtime_block().is_none(),
6620 "true external DLPack column has no xlog-side runtime block"
6621 );
6622 }
6623
6624 #[test]
6629 fn test_xlog_owned_arrow_device_runtime_backed_carries_identity() {
6630 let Some((device, runtime)) = try_runtime() else {
6631 return;
6632 };
6633 let manager = Arc::new(GpuMemoryManager::with_runtime(
6634 Arc::clone(&device),
6635 MemoryBudget::with_limit(1024 * 1024),
6636 Arc::clone(&runtime),
6637 ));
6638 let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
6639 assert!(slice.runtime_block().is_some());
6640 let stream = device.inner().stream().clone();
6641 let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
6645 arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
6646 ));
6647 let col = CudaColumn::arrow_device_xlog_owned(Arc::new(slice), stream, import);
6648 assert!(
6649 !col.is_external(),
6650 "xlog-owned Arrow device column must report is_external=false"
6651 );
6652 assert!(
6653 col.runtime_block().is_some(),
6654 "xlog-owned Arrow column over a runtime-backed slice must expose runtime_block"
6655 );
6656 }
6657
6658 #[test]
6661 fn test_external_arrow_device_remains_external() {
6662 let Some(device) = try_device() else {
6663 return;
6664 };
6665 let stream = device.inner().stream().clone();
6666 let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
6667 arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
6668 ));
6669 let col = unsafe { CudaColumn::arrow_device(0, 0, stream, import) };
6672 assert!(
6673 col.is_external(),
6674 "true external Arrow column must report is_external=true"
6675 );
6676 assert!(
6677 col.runtime_block().is_none(),
6678 "true external Arrow column has no xlog-side runtime block"
6679 );
6680 }
6681}