Skip to main content

xlog_cuda/device_runtime/
runtime.rs

1//! [`XlogDeviceRuntime`] hosts one provider-owned CUDA device, stream
2//! pool, and decorated memory-resource stack.
3//!
4//! The canonical provider builder constructs the complete ownership graph and
5//! shares its exact handles with the memory manager. Allocation state remains
6//! provider-owned. The common admission registry compares pending storage
7//! ranges across runtimes without owning their allocator state.
8
9use 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/// Execution counters for the device-controlled conditional-graph route.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub struct ConditionalGraphStats {
27    /// Successfully enqueued parent-graph launches.
28    pub launches: u64,
29    /// Terminal event synchronizations performed by the host.
30    pub terminal_synchronizations: u64,
31    /// Host-side fixpoint iterations (required to remain zero).
32    pub host_iterations: u64,
33    /// Allocations performed after a graph launch (required to remain zero).
34    pub host_allocations: u64,
35    /// Device status-writer kernels included in launches.
36    pub device_status_writer_launches: u64,
37    /// Terminal statuses written directly by the host (required to remain zero).
38    pub host_status_injections: u64,
39}
40
41/// Lifetime counters for CUDA events owned by resident graph launches.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub struct EventLifecycleStats {
44    /// Events that currently own a real CUDA event handle.
45    pub live_events: u64,
46    /// Events successfully created and recorded.
47    pub created_events: u64,
48    /// Event handles destroyed after completion.
49    pub destroyed_events: u64,
50    /// In-flight drops that had to wait for completion.
51    pub drop_waits: u64,
52}
53
54/// Lifetime counters for resident CUDA graph and executable handles.
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct ResidentGraphHandleLifecycleStats {
57    /// Parent graph handles currently retained by a prepared or in-flight run.
58    pub live_graphs: u64,
59    /// Instantiated graph executable handles currently retained.
60    pub live_graph_execs: u64,
61    /// Parent graph handles successfully created.
62    pub created_graphs: u64,
63    /// Parent graph handles destroyed.
64    pub destroyed_graphs: u64,
65    /// Graph executable handles successfully instantiated.
66    pub created_graph_execs: u64,
67    /// Graph executable handles destroyed.
68    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
91/// RAII proof that one live graph and executable pair is retained.
92///
93/// Construct this only after both CUDA handles have been created successfully,
94/// and retain it beside the owning graph object so its counters follow the
95/// actual handle lifetime.
96pub(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
115/// Completion event whose accounting is tied to a real cudarc event handle.
116pub struct ResidentCompletionEvent {
117    event: Option<CudaEvent>,
118    stream: Arc<CudaStream>,
119    telemetry: Arc<ResidentRuntimeTelemetry>,
120    synchronized: bool,
121}
122
123impl ResidentCompletionEvent {
124    /// Wait for the single terminal event. Repeated calls are no-ops.
125    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        // An unsuccessful or unwinding wait is not permission to release the
177        // event, exact stream/context, or their accounting owner.
178        if !synchronized {
179            telemetry.drop_waits.fetch_add(1, Ordering::Relaxed);
180            if !wait(&owners) {
181                diagnose();
182                return;
183            }
184        }
185        // SAFETY: a successful terminal wait proves the event's completion.
186        // This is the only release; unknown completion retains both owners.
187        unsafe { ManuallyDrop::drop(&mut owners) };
188        telemetry.live_events.fetch_sub(1, Ordering::AcqRel);
189        telemetry.destroyed_events.fetch_add(1, Ordering::Relaxed);
190        // SAFETY: accounting has completed and this owner was not released above.
191        unsafe { ManuallyDrop::drop(&mut telemetry) };
192    });
193}
194
195/// Provider-owned CUDA device runtime.
196///
197/// Owns the device handle, stream pool, and resource stack. Allocation and
198/// deallocation calls forward through the canonical provider-built stack:
199/// optional logging over one global byte budget and the asynchronous CUDA
200/// allocator. Tests may inject a different stack through the crate-private
201/// constructor.
202pub struct XlogDeviceRuntime {
203    device_ordinal: u32,
204    device: Arc<CudaDevice>,
205    stream_pool: Arc<StreamPool>,
206
207    resource: Arc<dyn DeviceMemoryResource + Send + Sync>,
208    /// Complete-request bytes promised but not yet materialized through the
209    /// resource stack. This mutex serializes complete budget snapshots and
210    /// materialization. Reclamation only lowers the independent physical ledger
211    /// and never takes this lock, including reentry from failed initialization.
212    reservation_bytes: Mutex<usize>,
213    resident_telemetry: Arc<ResidentRuntimeTelemetry>,
214}
215
216/// One complete byte claim against a runtime resource stack's global budget.
217pub(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        // Restore only an unacquired promise, then unlock before resuming a
259        // panic. An initializer panic must not poison future cold reclamation.
260        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    /// Compose a runtime from already validated provider-builder parts.
307    ///
308    /// Kept crate-private for allocator fault injection and the canonical
309    /// builder. Production callers cannot assemble mismatched resource stacks.
310    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    /// Atomically promise `bytes` against the complete resource-stack budget.
328    /// The stack must expose a finite reservable budget; otherwise complete
329    /// multi-allocation admission cannot be guaranteed and is refused.
330    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    /// CUDA ordinal this runtime serves.
366    pub fn device_ordinal(&self) -> u32 {
367        self.device_ordinal
368    }
369
370    /// Borrow the device handle.
371    pub fn device(&self) -> &Arc<CudaDevice> {
372        &self.device
373    }
374
375    /// Borrow the stream pool.
376    pub fn stream_pool(&self) -> &Arc<StreamPool> {
377        &self.stream_pool
378    }
379
380    /// Snapshot conditional-graph execution counters.
381    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    /// Reset per-execution conditional-graph counters.
396    ///
397    /// Handle and event lifetime counters are intentionally cumulative and are
398    /// not reset because callers compare snapshots around an execution.
399    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    /// Snapshot resident completion-event lifetime counters.
414    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    /// Snapshot resident graph-handle lifetime counters.
425    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    /// Tie lifecycle accounting to a successfully created graph/exec pair.
438    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    /// Record that one prepared parent graph was successfully enqueued.
450    #[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    /// Record a real completion event immediately after a graph launch.
463    #[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    /// Allocate via the underlying resource. Stream-ordered: the
491    /// returned [`DeviceBlock`] is bound to `stream`.
492    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    /// Deallocate via the underlying resource.
521    pub fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
522        // This is logical detachment, not proof of physical free. The backend
523        // validates the exact live identity and transfers its actual Arc into
524        // pending ownership. Only the last shared owner may reserve a free.
525        // No promises change here. Reclamation can run from failed allocation
526        // cleanup while materialization holds the promise lock on this thread.
527        self.resource.deallocate(block)
528    }
529
530    /// Sum of bytes currently outstanding on this device, as reported
531    /// by the underlying resource.
532    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    /// Drain pending async frees on the underlying resource. No-op
541    /// for synchronous backends. Callers that need an accurate
542    /// `bytes_outstanding` reading after a burst of asynchronous
543    /// deallocations should call this first.
544    pub fn reap_pending(&self) -> ResourceResult<()> {
545        // Last-owner retirement can invoke accounting callbacks. Drain outside
546        // this runtime's budget lock and outside every allocator-map lock.
547        crate::cuda_graph::reap_capture_retirements();
548        // The backend owns its queue synchronization; physical settlement is
549        // atomic in its ledger and needs no promise serialization.
550        self.resource.reap_pending()
551    }
552
553    /// Record that work has been (or is being) submitted on
554    /// `use_stream` that touches `block`. Forwards to the
555    /// underlying resource stack
556    /// (`LoggingResource` → `GlobalDeviceBudget` → `AsyncCudaResource`),
557    /// where the stream-ordered backend attaches a CUDA event so
558    /// `block.alloc_stream` waits on it before the queued
559    /// `cuMemFreeAsync` runs. This is the production-reachable
560    /// hook used by provider-recorded launches for `read`, `write`, and
561    /// `read_write` buffer arguments. Callers that submit raw CUDA work on a
562    /// stream other than `block.alloc_stream` must call this directly.
563    /// See [`DeviceMemoryResource::record_block_use`] for the
564    /// underlying contract.
565    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    /// Whether the active resource stack tracks cross-stream
574    /// uses (i.e., supports `record_block_use`). The launch
575    /// recorder's preflight checks this BEFORE queuing CUDA
576    /// work, so a misconfigured runtime fails loudly at the
577    /// boundary rather than after the launch is in flight.
578    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            &registry,
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            &registry,
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            &registry,
695            7,
696            memory,
697            reclamation.release_proof(),
698            &mut admission,
699            || {
700                reclamation.complete().unwrap();
701                // Another admission may already prune the completed free group.
702                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            &registry,
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}