1use std::mem::ManuallyDrop;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::sync::Mutex;
13
14use cudarc::driver::{CudaEvent, CudaStream};
15use xlog_core::{Result, XlogError};
16
17use super::resource::{
18 AllocTag, AllocationRequest, BlockId, DeviceBlock, DeviceMemoryResource, ResourceError,
19 ResourceResult, StreamId,
20};
21use super::stream_pool::StreamPool;
22use crate::CudaDevice;
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub struct ConditionalGraphStats {
27 pub launches: u64,
29 pub terminal_synchronizations: u64,
31 pub host_iterations: u64,
33 pub host_allocations: u64,
35 pub device_status_writer_launches: u64,
37 pub host_status_injections: u64,
39}
40
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub struct EventLifecycleStats {
44 pub live_events: u64,
46 pub created_events: u64,
48 pub destroyed_events: u64,
50 pub drop_waits: u64,
52}
53
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct ResidentGraphHandleLifecycleStats {
57 pub live_graphs: u64,
59 pub live_graph_execs: u64,
61 pub created_graphs: u64,
63 pub destroyed_graphs: u64,
65 pub created_graph_execs: u64,
67 pub destroyed_graph_execs: u64,
69}
70
71#[derive(Default)]
72struct ResidentRuntimeTelemetry {
73 launches: AtomicU64,
74 terminal_synchronizations: AtomicU64,
75 host_iterations: AtomicU64,
76 host_allocations: AtomicU64,
77 device_status_writer_launches: AtomicU64,
78 host_status_injections: AtomicU64,
79 live_events: AtomicU64,
80 created_events: AtomicU64,
81 destroyed_events: AtomicU64,
82 drop_waits: AtomicU64,
83 live_graphs: AtomicU64,
84 live_graph_execs: AtomicU64,
85 created_graphs: AtomicU64,
86 destroyed_graphs: AtomicU64,
87 created_graph_execs: AtomicU64,
88 destroyed_graph_execs: AtomicU64,
89}
90
91pub(crate) struct ResidentGraphHandleLease {
97 telemetry: Arc<ResidentRuntimeTelemetry>,
98}
99
100impl Drop for ResidentGraphHandleLease {
101 fn drop(&mut self) {
102 self.telemetry
103 .live_graph_execs
104 .fetch_sub(1, Ordering::AcqRel);
105 self.telemetry
106 .destroyed_graph_execs
107 .fetch_add(1, Ordering::Relaxed);
108 self.telemetry.live_graphs.fetch_sub(1, Ordering::AcqRel);
109 self.telemetry
110 .destroyed_graphs
111 .fetch_add(1, Ordering::Relaxed);
112 }
113}
114
115pub struct ResidentCompletionEvent {
117 event: Option<CudaEvent>,
118 stream: Arc<CudaStream>,
119 telemetry: Arc<ResidentRuntimeTelemetry>,
120 synchronized: bool,
121}
122
123impl ResidentCompletionEvent {
124 pub fn synchronize(&mut self) -> Result<()> {
126 if self.synchronized {
127 return Ok(());
128 }
129 let _capture_exclusion = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)
130 .map_err(|error| {
131 XlogError::Kernel(format!(
132 "resident completion wait requires an uncaptured stream: {error}"
133 ))
134 })?;
135 self.event
136 .as_ref()
137 .expect("resident completion event missing before drop")
138 .synchronize()
139 .map_err(|error| {
140 XlogError::Kernel(format!(
141 "resident conditional graph terminal event synchronization failed: {error}"
142 ))
143 })?;
144 self.synchronized = true;
145 self.telemetry
146 .terminal_synchronizations
147 .fetch_add(1, Ordering::Relaxed);
148 Ok(())
149 }
150}
151
152impl Drop for ResidentCompletionEvent {
153 fn drop(&mut self) {
154 if let Some(event) = self.event.take() {
155 retire_resident_completion(
156 (event, Arc::clone(&self.stream)),
157 Arc::clone(&self.telemetry),
158 self.synchronized,
159 |(event, _)| event.synchronize().is_ok(),
160 || eprintln!("CUDA completion remains unknown; retaining event and stream owners"),
161 );
162 }
163 }
164}
165
166fn retire_resident_completion<T: Send + 'static>(
167 owners: T,
168 telemetry: Arc<ResidentRuntimeTelemetry>,
169 synchronized: bool,
170 wait: impl FnOnce(&T) -> bool + Send + 'static,
171 diagnose: impl FnOnce() + Send + 'static,
172) {
173 let mut owners = ManuallyDrop::new(owners);
174 let mut telemetry = ManuallyDrop::new(telemetry);
175 crate::cuda_graph::retire_after_stream_captures(move || {
176 if !synchronized {
179 telemetry.drop_waits.fetch_add(1, Ordering::Relaxed);
180 if !wait(&owners) {
181 diagnose();
182 return;
183 }
184 }
185 unsafe { ManuallyDrop::drop(&mut owners) };
188 telemetry.live_events.fetch_sub(1, Ordering::AcqRel);
189 telemetry.destroyed_events.fetch_add(1, Ordering::Relaxed);
190 unsafe { ManuallyDrop::drop(&mut telemetry) };
192 });
193}
194
195pub struct XlogDeviceRuntime {
203 device_ordinal: u32,
204 device: Arc<CudaDevice>,
205 stream_pool: Arc<StreamPool>,
206
207 resource: Arc<dyn DeviceMemoryResource + Send + Sync>,
208 reservation_bytes: Mutex<usize>,
213 resident_telemetry: Arc<ResidentRuntimeTelemetry>,
214}
215
216pub(crate) struct RuntimeMemoryReservation {
218 runtime: Arc<XlogDeviceRuntime>,
219 total_bytes: usize,
220 remaining_bytes: usize,
221}
222
223impl RuntimeMemoryReservation {
224 pub(crate) fn materialize(
225 &mut self,
226 mut request: AllocationRequest,
227 ) -> ResourceResult<DeviceBlock> {
228 let bytes = request.bytes;
229 if bytes > self.remaining_bytes {
230 return Err(ResourceError::OutOfBudget {
231 requested: bytes,
232 current: self.total_bytes - self.remaining_bytes,
233 remaining: self.remaining_bytes,
234 limit: self.total_bytes,
235 });
236 }
237
238 let mut reserved = self
239 .runtime
240 .reservation_bytes
241 .lock()
242 .expect("device-runtime reservation accounting poisoned");
243 *reserved = reserved.checked_sub(bytes).ok_or_else(|| {
244 ResourceError::Driver("device-runtime reservation accounting underflow".to_string())
245 })?;
246 self.remaining_bytes -= bytes;
247
248 request.reservation_pressure_bytes = *reserved;
249 let attempt = RuntimeReservationAttempt {
250 reserved: &mut reserved,
251 remaining: &mut self.remaining_bytes,
252 bytes,
253 reclamation: request.reclamation(),
254 };
255 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
256 self.runtime.resource.materialize(request)
257 }));
258 drop(attempt);
261 drop(reserved);
262 match outcome {
263 Ok(result) => result,
264 Err(panic) => std::panic::resume_unwind(panic),
265 }
266 }
267}
268
269struct RuntimeReservationAttempt<'a> {
270 reserved: &'a mut usize,
271 remaining: &'a mut usize,
272 bytes: usize,
273 reclamation: Arc<crate::memory::AllocationReclamation>,
274}
275
276impl Drop for RuntimeReservationAttempt<'_> {
277 fn drop(&mut self) {
278 if !self.reclamation.was_acquired() {
279 *self.reserved = self
280 .reserved
281 .checked_add(self.bytes)
282 .expect("runtime promise rollback remains representable");
283 *self.remaining = self
284 .remaining
285 .checked_add(self.bytes)
286 .expect("runtime token rollback remains representable");
287 }
288 }
289}
290
291impl Drop for RuntimeMemoryReservation {
292 fn drop(&mut self) {
293 let mut reserved = self
294 .runtime
295 .reservation_bytes
296 .lock()
297 .unwrap_or_else(|error| error.into_inner());
298 *reserved = reserved
299 .checked_sub(self.remaining_bytes)
300 .expect("device-runtime reservation accounting underflow");
301 self.remaining_bytes = 0;
302 }
303}
304
305impl XlogDeviceRuntime {
306 pub(crate) fn with_resource(
311 device: Arc<CudaDevice>,
312 device_ordinal: u32,
313 stream_pool: Arc<StreamPool>,
314 resource: Box<dyn DeviceMemoryResource + Send + Sync>,
315 ) -> Self {
316 Self {
317 device_ordinal,
318 device,
319 stream_pool,
320
321 resource: Arc::from(resource),
322 reservation_bytes: Mutex::new(0),
323 resident_telemetry: Arc::new(ResidentRuntimeTelemetry::default()),
324 }
325 }
326
327 pub(crate) fn reserve_memory(
331 self: &Arc<Self>,
332 bytes: usize,
333 ) -> ResourceResult<RuntimeMemoryReservation> {
334 let mut reserved = self
335 .reservation_bytes
336 .lock()
337 .expect("device-runtime reservation accounting poisoned");
338 let snapshot = self.resource.budget_snapshot().ok_or_else(|| {
339 ResourceError::Driver(
340 "device-runtime resource stack has no reservable global budget".to_string(),
341 )
342 })?;
343 let current = snapshot.reserved.checked_add(*reserved).ok_or_else(|| {
344 ResourceError::Driver("device-runtime reservation accounting overflow".to_string())
345 })?;
346 let remaining = snapshot.limit.saturating_sub(current);
347 if bytes > remaining {
348 return Err(ResourceError::OutOfBudget {
349 requested: bytes,
350 current,
351 remaining,
352 limit: snapshot.limit,
353 });
354 }
355 *reserved = reserved.checked_add(bytes).ok_or_else(|| {
356 ResourceError::Driver("device-runtime reservation accounting overflow".to_string())
357 })?;
358 Ok(RuntimeMemoryReservation {
359 runtime: Arc::clone(self),
360 total_bytes: bytes,
361 remaining_bytes: bytes,
362 })
363 }
364
365 pub fn device_ordinal(&self) -> u32 {
367 self.device_ordinal
368 }
369
370 pub fn device(&self) -> &Arc<CudaDevice> {
372 &self.device
373 }
374
375 pub fn stream_pool(&self) -> &Arc<StreamPool> {
377 &self.stream_pool
378 }
379
380 pub fn conditional_graph_stats(&self) -> ConditionalGraphStats {
382 let telemetry = &self.resident_telemetry;
383 ConditionalGraphStats {
384 launches: telemetry.launches.load(Ordering::Relaxed),
385 terminal_synchronizations: telemetry.terminal_synchronizations.load(Ordering::Relaxed),
386 host_iterations: telemetry.host_iterations.load(Ordering::Relaxed),
387 host_allocations: telemetry.host_allocations.load(Ordering::Relaxed),
388 device_status_writer_launches: telemetry
389 .device_status_writer_launches
390 .load(Ordering::Relaxed),
391 host_status_injections: telemetry.host_status_injections.load(Ordering::Relaxed),
392 }
393 }
394
395 pub fn reset_conditional_graph_stats(&self) {
400 let telemetry = &self.resident_telemetry;
401 telemetry.launches.store(0, Ordering::Relaxed);
402 telemetry
403 .terminal_synchronizations
404 .store(0, Ordering::Relaxed);
405 telemetry.host_iterations.store(0, Ordering::Relaxed);
406 telemetry.host_allocations.store(0, Ordering::Relaxed);
407 telemetry
408 .device_status_writer_launches
409 .store(0, Ordering::Relaxed);
410 telemetry.host_status_injections.store(0, Ordering::Relaxed);
411 }
412
413 pub fn event_lifecycle_stats(&self) -> EventLifecycleStats {
415 let telemetry = &self.resident_telemetry;
416 EventLifecycleStats {
417 live_events: telemetry.live_events.load(Ordering::Acquire),
418 created_events: telemetry.created_events.load(Ordering::Relaxed),
419 destroyed_events: telemetry.destroyed_events.load(Ordering::Relaxed),
420 drop_waits: telemetry.drop_waits.load(Ordering::Relaxed),
421 }
422 }
423
424 pub fn resident_graph_handle_lifecycle_stats(&self) -> ResidentGraphHandleLifecycleStats {
426 let telemetry = &self.resident_telemetry;
427 ResidentGraphHandleLifecycleStats {
428 live_graphs: telemetry.live_graphs.load(Ordering::Acquire),
429 live_graph_execs: telemetry.live_graph_execs.load(Ordering::Acquire),
430 created_graphs: telemetry.created_graphs.load(Ordering::Relaxed),
431 destroyed_graphs: telemetry.destroyed_graphs.load(Ordering::Relaxed),
432 created_graph_execs: telemetry.created_graph_execs.load(Ordering::Relaxed),
433 destroyed_graph_execs: telemetry.destroyed_graph_execs.load(Ordering::Relaxed),
434 }
435 }
436
437 pub(crate) fn resident_graph_handle_lease(&self) -> ResidentGraphHandleLease {
439 let telemetry = Arc::clone(&self.resident_telemetry);
440 telemetry.live_graphs.fetch_add(1, Ordering::AcqRel);
441 telemetry.created_graphs.fetch_add(1, Ordering::Relaxed);
442 telemetry.live_graph_execs.fetch_add(1, Ordering::AcqRel);
443 telemetry
444 .created_graph_execs
445 .fetch_add(1, Ordering::Relaxed);
446 ResidentGraphHandleLease { telemetry }
447 }
448
449 #[doc(hidden)]
451 pub fn record_conditional_graph_launch(&self, has_device_status_writer: bool) {
452 self.resident_telemetry
453 .launches
454 .fetch_add(1, Ordering::Relaxed);
455 if has_device_status_writer {
456 self.resident_telemetry
457 .device_status_writer_launches
458 .fetch_add(1, Ordering::Relaxed);
459 }
460 }
461
462 #[doc(hidden)]
464 pub fn record_resident_completion_event(
465 &self,
466 stream: &Arc<CudaStream>,
467 ) -> Result<ResidentCompletionEvent> {
468 let _capture_exclusion =
469 crate::cuda_graph::reserve_uncaptured_stream(stream).map_err(|error| {
470 XlogError::Kernel(format!(
471 "resident completion record requires an uncaptured stream: {error}"
472 ))
473 })?;
474 let event = stream.record_event(None).map_err(|error| {
475 XlogError::Kernel(format!(
476 "resident conditional graph completion event record failed: {error}"
477 ))
478 })?;
479 let telemetry = Arc::clone(&self.resident_telemetry);
480 telemetry.live_events.fetch_add(1, Ordering::AcqRel);
481 telemetry.created_events.fetch_add(1, Ordering::Relaxed);
482 Ok(ResidentCompletionEvent {
483 event: Some(event),
484 stream: Arc::clone(stream),
485 telemetry,
486 synchronized: false,
487 })
488 }
489
490 pub fn allocate(
493 &self,
494 bytes: usize,
495 stream: StreamId,
496 tag: AllocTag,
497 ) -> ResourceResult<DeviceBlock> {
498 self.materialize(AllocationRequest::new(bytes, stream, tag))
499 }
500
501 pub(crate) fn materialize(
502 &self,
503 mut request: AllocationRequest,
504 ) -> ResourceResult<DeviceBlock> {
505 let reservation_pressure_bytes = self
506 .reservation_bytes
507 .lock()
508 .expect("device-runtime reservation accounting poisoned");
509 request.reservation_pressure_bytes = *reservation_pressure_bytes;
510 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
511 self.resource.materialize(request)
512 }));
513 drop(reservation_pressure_bytes);
514 match outcome {
515 Ok(result) => result,
516 Err(panic) => std::panic::resume_unwind(panic),
517 }
518 }
519
520 pub fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
522 self.resource.deallocate(block)
528 }
529
530 pub fn bytes_outstanding(&self) -> usize {
533 self.resource.bytes_outstanding()
534 }
535
536 pub(crate) fn retirement_resource(&self) -> Arc<dyn DeviceMemoryResource + Send + Sync> {
537 Arc::clone(&self.resource)
538 }
539
540 pub fn reap_pending(&self) -> ResourceResult<()> {
545 crate::cuda_graph::reap_capture_retirements();
548 self.resource.reap_pending()
551 }
552
553 pub fn record_block_use(
566 &self,
567 block: &DeviceBlock,
568 use_stream: StreamId,
569 ) -> ResourceResult<()> {
570 self.resource.record_block_use(block, use_stream)
571 }
572
573 pub fn supports_block_use_tracking(&self) -> bool {
579 self.resource.supports_block_use_tracking()
580 }
581
582 pub(crate) fn allocation_dependencies(
583 &self,
584 block: BlockId,
585 bytes: usize,
586 ) -> ResourceResult<Option<Arc<super::resource::DeviceAccessDependencies>>> {
587 self.resource.access_dependencies(block, bytes)
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn allocation_reservation_unwind_restores_only_unacquired_promises() {
597 for acquired in [false, true] {
598 for already_released in [false, true] {
599 if already_released && !acquired {
600 continue;
601 }
602 let ticket = Arc::new(crate::memory::AllocationReclamation::default());
603 let mut reserved = 64;
604 let mut remaining = 64;
605 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
606 let _attempt = RuntimeReservationAttempt {
607 reserved: &mut reserved,
608 remaining: &mut remaining,
609 bytes: 64,
610 reclamation: Arc::clone(&ticket),
611 };
612 if acquired {
613 ticket.acquired().unwrap();
614 }
615 if already_released {
616 ticket.complete().unwrap();
617 }
618 panic!("materialization interrupted");
619 }));
620 assert!(result.is_err());
621 assert_eq!(reserved, if acquired { 64 } else { 128 });
622 assert_eq!(remaining, if acquired { 64 } else { 128 });
623 }
624 }
625 }
626 use crate::device_runtime::resource::{
627 with_reclamation_admission, BlockUseRegistry, MemoryUse,
628 };
629 use crate::device_runtime::{Access, BlockState, Generation};
630 use std::cell::Cell;
631
632 #[test]
633 fn accepted_pending_reclamation_does_not_release_range() {
634 let registry = Mutex::new(BlockUseRegistry::default());
635 let block = DeviceBlock {
636 ptr: 0x1000,
637 bytes: 64,
638 align: 8,
639 device_ordinal: 0,
640 alloc_stream: StreamId::DEFAULT,
641 tag: AllocTag::UNTAGGED,
642 generation: Generation(1),
643 state: BlockState::Live,
644 };
645 let mut admission = None;
646 let reclamation = crate::memory::AllocationReclamation::default();
647 with_reclamation_admission(
648 ®istry,
649 7,
650 MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap(),
651 reclamation.release_proof(),
652 &mut admission,
653 || Ok(()),
654 )
655 .unwrap();
656 assert!(admission.is_some());
657 assert!(registry
658 .lock()
659 .unwrap()
660 .reserve_memory_uses(
661 7,
662 &[MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap()]
663 )
664 .is_err());
665 }
666
667 #[test]
668 fn physical_admission_conflict_does_not_strand_an_exact_retry() {
669 let registry = Mutex::new(BlockUseRegistry::default());
670 let reclamation = crate::memory::AllocationReclamation::default();
671 let memory = MemoryUse::new(0x1000, 64, Access::ReadWrite).unwrap();
672 let use_group = registry
673 .lock()
674 .unwrap()
675 .reserve_memory_uses(7, &[memory])
676 .unwrap();
677 let mut admission = None;
678 assert!(with_reclamation_admission(
679 ®istry,
680 7,
681 memory,
682 reclamation.release_proof(),
683 &mut admission,
684 || panic!("conflicting admission must precede any physical release"),
685 )
686 .is_err());
687 assert!(admission.is_none());
688 registry
689 .lock()
690 .unwrap()
691 .release_memory_uses(use_group)
692 .unwrap();
693 with_reclamation_admission(
694 ®istry,
695 7,
696 memory,
697 reclamation.release_proof(),
698 &mut admission,
699 || {
700 reclamation.complete().unwrap();
701 let mut registry = registry.try_lock().unwrap();
703 let next = registry
704 .reserve_memory_uses(
705 7,
706 &[
707 super::super::resource::MemoryUse::new(0x1000, 64, Access::Write)
708 .unwrap(),
709 ],
710 )
711 .unwrap();
712 registry.release_memory_uses(next).unwrap();
713 Ok(())
714 },
715 )
716 .unwrap();
717 }
718
719 #[test]
720 fn physical_reclamation_keeps_range_reserved_without_holding_registry_mutex() {
721 let registry = Mutex::new(BlockUseRegistry::default());
722 let block = DeviceBlock {
723 ptr: 0x1000,
724 bytes: 64,
725 align: 8,
726 device_ordinal: 0,
727 alloc_stream: StreamId::DEFAULT,
728 tag: AllocTag::UNTAGGED,
729 generation: Generation(1),
730 state: BlockState::Live,
731 };
732 let memory = MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap();
733 let reclamation = crate::memory::AllocationReclamation::default();
734 let mut admission = None;
735 with_reclamation_admission(
736 ®istry,
737 7,
738 MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap(),
739 reclamation.release_proof(),
740 &mut admission,
741 || {
742 let mut guard = registry
743 .try_lock()
744 .expect("driver wait must not hold registry mutex");
745 assert!(guard.reserve_memory_uses(7, &[memory]).is_err());
746 reclamation.complete()
747 },
748 )
749 .unwrap();
750 let mut registry = registry.lock().unwrap();
751 let reused = registry.reserve_memory_uses(7, &[memory]).unwrap();
752 registry.release_memory_uses(reused).unwrap();
753 }
754
755 #[test]
756 fn physical_reclamation_is_not_forwarded_while_a_block_use_is_pending() {
757 let mut registry = BlockUseRegistry::default();
758 let context = 7;
759 let block_id = BlockId {
760 ptr: 0x1000,
761 generation: Generation(1),
762 alloc_stream: StreamId::DEFAULT,
763 device_ordinal: 0,
764 };
765 registry
766 .reserve_memory_uses(
767 context,
768 &[super::super::resource::MemoryUse::new(block_id.ptr, 64, Access::Read).unwrap()],
769 )
770 .expect("memory-use reservation");
771
772 let forwarded = Cell::new(false);
773 let block = DeviceBlock {
774 ptr: block_id.ptr,
775 device_ordinal: block_id.device_ordinal,
776 alloc_stream: block_id.alloc_stream,
777 bytes: 64,
778 align: 8,
779 tag: AllocTag::UNTAGGED,
780 generation: block_id.generation,
781 state: BlockState::Live,
782 };
783 assert!(with_reclamation_admission(
784 &Mutex::new(registry),
785 context,
786 MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap(),
787 crate::memory::AllocationReclamation::default().release_proof(),
788 &mut None,
789 || {
790 forwarded.set(true);
791 Ok(())
792 }
793 )
794 .is_err());
795 assert!(
796 !forwarded.get(),
797 "pending-use rejection must happen before resource deallocation"
798 );
799 }
800
801 fn try_runtime() -> Option<XlogDeviceRuntime> {
802 use super::super::async_resource::AsyncCudaResource;
803
804 match CudaDevice::new(0) {
805 Ok(device) => {
806 let device = Arc::new(device);
807 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
808 let resource = Box::new(AsyncCudaResource::new(
809 Arc::clone(&device),
810 0,
811 Arc::clone(&pool),
812 ));
813 Some(XlogDeviceRuntime::with_resource(device, 0, pool, resource))
814 }
815 Err(error) => {
816 if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
817 panic!("XLOG_REQUIRE_CUDA=1 but CUDA is unavailable: {error}");
818 }
819 eprintln!("Skipping device-runtime test: CUDA unavailable: {error}");
820 None
821 }
822 }
823 }
824
825 #[test]
826 fn allocate_then_deallocate_via_runtime() {
827 let Some(rt) = try_runtime() else {
828 return;
829 };
830 let before = rt.bytes_outstanding();
831 let block = rt
832 .allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED)
833 .expect("alloc");
834 assert_eq!(block.bytes, 2048);
835 assert_eq!(rt.bytes_outstanding(), before + 2048);
836 rt.deallocate(block).expect("dealloc");
837 rt.reap_pending().expect("reap pending");
838 assert_eq!(rt.bytes_outstanding(), before);
839 }
840
841 #[test]
842 fn with_resource_composes_owned_runtime() {
843 use super::super::async_resource::AsyncCudaResource;
844
845 let Some(rt) = try_runtime() else {
846 return;
847 };
848 let device = Arc::clone(rt.device());
849 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
850 let resource = Box::new(AsyncCudaResource::new(
851 Arc::clone(&device),
852 0,
853 Arc::clone(&pool),
854 ));
855
856 let owned = XlogDeviceRuntime::with_resource(device, 0, pool, resource);
857 assert_eq!(owned.device_ordinal(), 0);
858
859 let block = owned
860 .allocate(1024, StreamId::DEFAULT, AllocTag::UNTAGGED)
861 .expect("alloc through composed runtime");
862 assert_eq!(block.bytes, 1024);
863 assert_eq!(owned.bytes_outstanding(), 1024);
864 owned.deallocate(block).expect("dealloc");
865 owned.reap_pending().expect("reap");
866 assert_eq!(owned.bytes_outstanding(), 0);
867 }
868
869 #[test]
870 fn resident_completion_retirement_retains_unknown_event_and_stream() {
871 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
872 use std::sync::atomic::AtomicUsize;
873
874 struct Owner(Arc<AtomicUsize>);
875 impl Drop for Owner {
876 fn drop(&mut self) {
877 self.0.fetch_add(1, Ordering::SeqCst);
878 }
879 }
880
881 for completion in [false, true] {
882 let drops = Arc::new(AtomicUsize::new(0));
883 let event = Arc::new(Owner(Arc::clone(&drops)));
884 let stream = Arc::new(Owner(Arc::clone(&drops)));
885 let event_observer = Arc::downgrade(&event);
886 let stream_observer = Arc::downgrade(&stream);
887 let telemetry = Arc::new(super::ResidentRuntimeTelemetry::default());
888 telemetry.live_events.store(1, Ordering::Relaxed);
889 super::retire_resident_completion(
890 (event, stream),
891 Arc::clone(&telemetry),
892 false,
893 move |_| completion,
894 || {},
895 );
896 assert_eq!(drops.load(Ordering::SeqCst), if completion { 2 } else { 0 });
897 assert_eq!(event_observer.upgrade().is_some(), !completion);
898 assert_eq!(stream_observer.upgrade().is_some(), !completion);
899 assert_eq!(
900 telemetry.live_events.load(Ordering::Relaxed),
901 u64::from(!completion)
902 );
903 assert_eq!(
904 telemetry.destroyed_events.load(Ordering::Relaxed),
905 u64::from(completion)
906 );
907 assert_eq!(telemetry.drop_waits.load(Ordering::Relaxed), 1);
908 }
909 }
910
911 #[test]
912 fn resident_completion_retirement_preserves_owners_when_diagnostics_unwind() {
913 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
914 let owner = Arc::new(());
915 let observer = Arc::downgrade(&owner);
916 let telemetry = Arc::new(super::ResidentRuntimeTelemetry::default());
917 telemetry.live_events.store(1, Ordering::Relaxed);
918 let observed_telemetry = Arc::clone(&telemetry);
919 let outcome = std::panic::catch_unwind(move || {
920 super::retire_resident_completion(
921 owner,
922 telemetry,
923 false,
924 |_| false,
925 || panic!("controlled completion diagnostic failure"),
926 );
927 });
928 assert!(outcome.is_err());
929 assert!(observer.upgrade().is_some());
930 assert_eq!(observed_telemetry.live_events.load(Ordering::Relaxed), 1);
931 assert_eq!(
932 observed_telemetry.destroyed_events.load(Ordering::Relaxed),
933 0
934 );
935 }
936
937 #[test]
938 fn resident_completion_retirement_does_not_repeat_a_proven_wait() {
939 let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
940 let owner = Arc::new(());
941 let observer = Arc::downgrade(&owner);
942 let telemetry = Arc::new(super::ResidentRuntimeTelemetry::default());
943 telemetry.live_events.store(1, Ordering::Relaxed);
944 super::retire_resident_completion(
945 owner,
946 Arc::clone(&telemetry),
947 true,
948 |_| panic!("already completed event must not wait again"),
949 || panic!("already completed event must not report unknown completion"),
950 );
951 assert!(observer.upgrade().is_none());
952 assert_eq!(telemetry.drop_waits.load(Ordering::Relaxed), 0);
953 assert_eq!(telemetry.live_events.load(Ordering::Relaxed), 0);
954 assert_eq!(telemetry.destroyed_events.load(Ordering::Relaxed), 1);
955 }
956
957 #[test]
958 fn resident_completion_event_accounts_a_real_recorded_event() {
959 let Some(runtime) = try_runtime() else {
960 return;
961 };
962 let stream = runtime
963 .stream_pool()
964 .resolve(StreamId::DEFAULT)
965 .expect("default stream");
966 let before = runtime.event_lifecycle_stats();
967 let mut completion = runtime
968 .record_resident_completion_event(&stream)
969 .expect("record completion event");
970 let live = runtime.event_lifecycle_stats();
971 assert_eq!(live.live_events, before.live_events + 1);
972 assert_eq!(live.created_events, before.created_events + 1);
973 completion
974 .synchronize()
975 .expect("synchronize completion event");
976 drop(completion);
977 let after = runtime.event_lifecycle_stats();
978 assert_eq!(after.live_events, before.live_events);
979 assert_eq!(after.destroyed_events, before.destroyed_events + 1);
980 assert_eq!(after.drop_waits, before.drop_waits);
981 }
982
983 #[test]
984 fn resident_graph_handle_lease_balances_one_owner_slot() {
985 let Some(runtime) = try_runtime() else {
986 return;
987 };
988 let before = runtime.resident_graph_handle_lifecycle_stats();
989 let lease = runtime.resident_graph_handle_lease();
990 let live = runtime.resident_graph_handle_lifecycle_stats();
991 assert_eq!(live.live_graphs, before.live_graphs + 1);
992 assert_eq!(live.live_graph_execs, before.live_graph_execs + 1);
993 drop(lease);
994 let after = runtime.resident_graph_handle_lifecycle_stats();
995 assert_eq!(after.live_graphs, before.live_graphs);
996 assert_eq!(after.live_graph_execs, before.live_graph_execs);
997 assert_eq!(
998 after.created_graphs - before.created_graphs,
999 after.destroyed_graphs - before.destroyed_graphs
1000 );
1001 assert_eq!(
1002 after.created_graph_execs - before.created_graph_execs,
1003 after.destroyed_graph_execs - before.destroyed_graph_execs
1004 );
1005 }
1006}