Skip to main content

xlog_cuda/
cuda_graph.rs

1//! CUDA Graph RAII helpers for production graph capture/replay.
2//!
3//! This module intentionally stays close to the CUDA driver API. The bounded
4//! CSM CUDA Graph path needs explicit graph lifetime ownership and node
5//! inventory before it can safely update graph-exec parameters for runtime
6//! pointers and capacity classes.
7//!
8//! # Cold lifecycle and progress
9//!
10//! Releasing the last module or graph owner can synchronously wait for recorded
11//! execution events. During capture, destruction is deferred: finishing the last
12//! capture can therefore wait for another owner's earlier executions. Capture
13//! setup and module loading/replacement can also drain pending destruction.
14//! These are cold lifecycle boundaries, outside the measured resident replay;
15//! include their cost in end-to-end measurements.
16//! Failed memory-operation cleanup can require a barrier on the entire retained
17//! context, including after its original host thread exits without a usable
18//! completion event. This recovery runs only in the cold retirement queue with
19//! capture excluded; ordinary enqueue and commit do not add that barrier.
20//!
21//! An execution event covers all preceding work on its stream, including
22//! transitive waits, not just kernels using the retiring resource. Before any
23//! boundary that can drain destruction, that work must be able to finish without
24//! this call returning, a new module load or capture, launching the graph still
25//! being captured, or releasing a caller-held lock or Python GIL. Persistent
26//! kernels need an independently progressing stop path established beforehand.
27//! Do not rely on CUDA callbacks to advance this teardown, or destroy owners
28//! from a CUDA callback. The registry mutex is released during waits/destruction;
29//! new captures remain excluded, while already-loaded work can still be submitted.
30//! Context-barrier recovery imposes that same progress requirement on all earlier
31//! work in the retained context, not just work on one retiring stream.
32//!
33//! Recorded events or a successful barrier covering the actual submissions prove
34//! completion. EndCapture and unrecorded events do not. Unknown completion retains
35//! the concrete resources
36//! for diagnosis; it is neither successful release nor a normal lifetime policy.
37//! Raw graph/function users must retain their exact resource owners through all
38//! uses and future replays, and establish completion before releasing them.
39
40use std::collections::{BTreeMap, HashSet, VecDeque};
41use std::{
42    fmt, mem, ptr,
43    sync::{Arc, Condvar, Mutex, OnceLock},
44};
45
46use cudarc::driver::{result::DriverError, sys, CudaContext, CudaStream};
47use libloading::Library;
48use xlog_core::{Result, XlogError};
49
50use crate::device::{CapturedCudaLaunchBinding, ExecutionCompletion, LoadedModule};
51use crate::device_runtime::XlogDeviceRuntime;
52
53pub const CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION: u32 = 1;
54const CONDITIONAL_GRAPH_MINIMUM_DRIVER: i32 = 12_030;
55
56type DriverGetVersionFn = unsafe extern "C" fn(*mut i32) -> sys::CUresult;
57type ConditionalHandleCreateFn = unsafe extern "C" fn(
58    *mut sys::CUgraphConditionalHandle,
59    sys::CUgraph,
60    sys::CUcontext,
61    u32,
62    u32,
63) -> sys::CUresult;
64type GraphAddNodeFn = unsafe extern "C" fn(
65    *mut sys::CUgraphNode,
66    sys::CUgraph,
67    *const sys::CUgraphNode,
68    usize,
69    *mut sys::CUgraphNodeParams,
70) -> sys::CUresult;
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum CudaConditionalGraphUnavailable {
74    DriverLibraryUnavailable,
75    MissingDriverSymbol {
76        symbol: &'static str,
77    },
78    DriverVersionQueryFailed {
79        code: sys::CUresult,
80    },
81    DriverVersionTooOld {
82        found: i32,
83        required: i32,
84    },
85    DriverCallFailed {
86        operation: &'static str,
87        code: sys::CUresult,
88    },
89    NullDriverHandle {
90        operation: &'static str,
91    },
92    ContextMismatch,
93    StreamCaptureBusy,
94    BodyPopulationFailed {
95        detail: String,
96    },
97}
98
99impl CudaConditionalGraphUnavailable {
100    pub fn is_unsupported(&self) -> bool {
101        matches!(
102            self,
103            Self::DriverLibraryUnavailable
104                | Self::MissingDriverSymbol { .. }
105                | Self::DriverVersionTooOld { .. }
106                | Self::DriverCallFailed {
107                    code: sys::CUresult::CUDA_ERROR_NOT_SUPPORTED,
108                    ..
109                }
110        )
111    }
112
113    pub fn decline_detail(&self) -> String {
114        match self {
115            Self::DriverLibraryUnavailable => {
116                "CUDA driver library is unavailable for conditional graphs".to_string()
117            }
118            Self::MissingDriverSymbol { symbol } => {
119                format!("CUDA driver is missing required conditional-graph symbol {symbol}")
120            }
121            Self::DriverVersionQueryFailed { code } => {
122                format!("CUDA driver version query failed: {code:?}")
123            }
124            Self::DriverVersionTooOld { found, required } => {
125                format!("CUDA conditional graphs require driver API {required}, found {found}")
126            }
127            Self::DriverCallFailed { operation, code } => {
128                format!("CUDA conditional-graph operation {operation} failed: {code:?}")
129            }
130            Self::NullDriverHandle { operation } => {
131                format!("CUDA conditional-graph operation {operation} returned a null handle")
132            }
133            Self::ContextMismatch => {
134                "CUDA conditional graph and stream belong to different contexts".to_string()
135            }
136            Self::StreamCaptureBusy => {
137                "CUDA stream already has an active graph capture".to_string()
138            }
139            Self::BodyPopulationFailed { detail } => {
140                format!("CUDA conditional graph body population failed: {detail}")
141            }
142        }
143    }
144
145    pub fn body_population(error: impl fmt::Display) -> Self {
146        Self::BodyPopulationFailed {
147            detail: error.to_string(),
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153struct StreamCaptureKey {
154    context: usize,
155    stream: u64,
156}
157
158/// CUDA's process-unique ID also distinguishes per-thread default streams;
159/// their raw sentinel handle is identical on every host thread.
160pub(crate) fn stream_execution_id(stream: &CudaStream) -> std::result::Result<u64, DriverError> {
161    stream.context().bind_to_thread()?;
162    let mut id = 0;
163    unsafe { sys::cuStreamGetId(stream.cu_stream(), &mut id).result()? };
164    Ok(id)
165}
166
167#[derive(Default)]
168struct StreamCaptureRegistry {
169    active: HashSet<StreamCaptureKey>,
170    modules: BTreeMap<(usize, u64), CaptureSubmissionEntry>,
171    deferred: VecDeque<DeferredRetirement>,
172    retiring: bool,
173    capture_exclusions: usize,
174}
175
176enum DeferredRetirement {
177    Once(Option<Box<dyn FnOnce() + Send>>),
178    Retry(Box<dyn FnMut() -> bool + Send>),
179}
180
181impl DeferredRetirement {
182    fn attempt(&mut self) -> bool {
183        match self {
184            Self::Once(retire) => {
185                if let Some(retire) = retire.take() {
186                    retire();
187                }
188                true
189            }
190            Self::Retry(retire) => retire(),
191        }
192    }
193}
194
195#[derive(Default)]
196pub(crate) struct CapturedOwners {
197    pub(crate) modules: BTreeMap<usize, Arc<LoadedModule>>,
198    // Capture-time executable bindings. Conditional presence does not assert
199    // that a node ran; completed-step state separately records reached branches.
200    pub(crate) launches: Vec<CapturedCudaLaunchBinding>,
201    pub(crate) memory: Vec<Arc<crate::memory::MemoryAccessManifest>>,
202    // Actual producer and stream owners, not an attestation of pointer validity
203    // or execution. They must exist before capture can record their uses.
204    external_resources: Vec<Arc<dyn Send + Sync>>,
205    submission: Arc<Mutex<()>>,
206    failed: bool,
207}
208
209impl CapturedOwners {
210    fn with_external_resources(external_resources: Vec<Arc<dyn Send + Sync>>) -> Self {
211        Self {
212            external_resources,
213            ..Self::default()
214        }
215    }
216}
217
218impl std::fmt::Debug for CapturedOwners {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("CapturedOwners")
221            .field("modules", &self.modules.len())
222            .field("launches", &self.launches.len())
223            .field("memory_manifests", &self.memory.len())
224            .field("external_resources", &self.external_resources.len())
225            .finish()
226    }
227}
228
229pub(crate) type CaptureOwners = Arc<Mutex<CapturedOwners>>;
230
231fn retain_child_capture_owners(
232    parent: &CaptureOwners,
233    child: &CaptureOwners,
234    memory: &Arc<crate::memory::MemoryAccessManifest>,
235) {
236    // Release the child lock before locking the parent, including when nested
237    // capture already shares one owner set. Freeze the current memory binding,
238    // not the child's future mutable graph parameters.
239    let (modules, launches, external_resources) = {
240        let child = child.lock().unwrap_or_else(|error| error.into_inner());
241        (
242            child.modules.clone(),
243            child.launches.clone(),
244            child.external_resources.clone(),
245        )
246    };
247    let mut parent = parent.lock().unwrap_or_else(|error| error.into_inner());
248    parent.modules.extend(modules);
249    parent.launches.extend(launches);
250    parent.external_resources.extend(external_resources);
251    parent.memory.push(memory.clone());
252}
253
254struct CaptureSubmissionEntry {
255    owners: CaptureOwners,
256    execution_id: u64,
257    closing: bool,
258    executing: Arc<CaptureExecutingSubmissions>,
259}
260
261#[derive(Default)]
262struct CaptureExecutingSubmissions {
263    count: Mutex<usize>,
264    idle: Condvar,
265}
266
267impl CaptureExecutingSubmissions {
268    // Admission holds the registry mutex while calling this method, so closing
269    // cannot pass a newly admitted submission before its count is visible.
270    fn pin(self: &Arc<Self>) -> CaptureExecutingSubmission {
271        *self.count.lock().unwrap_or_else(|error| error.into_inner()) += 1;
272        CaptureExecutingSubmission {
273            executing: self.clone(),
274        }
275    }
276
277    fn wait_until_idle(&self) {
278        let mut count = self.count.lock().unwrap_or_else(|error| error.into_inner());
279        while *count != 0 {
280            count = self
281                .idle
282                .wait(count)
283                .unwrap_or_else(|error| error.into_inner());
284        }
285    }
286}
287
288struct CaptureExecutingSubmission {
289    executing: Arc<CaptureExecutingSubmissions>,
290}
291
292impl Drop for CaptureExecutingSubmission {
293    fn drop(&mut self) {
294        let mut count = self
295            .executing
296            .count
297            .lock()
298            .unwrap_or_else(|error| error.into_inner());
299        *count -= 1;
300        if *count == 0 {
301            self.executing.idle.notify_all();
302        }
303    }
304}
305
306/// Select the stream's ownership phase before resource admission. A captured
307/// descriptor retains its exact target but does not keep capture executing.
308pub(crate) struct StreamSubmissionPhase {
309    state: StreamSubmissionState,
310}
311
312/// Keeps one actual submission admitted until its enqueue callback returns.
313/// The borrowed phase retains ordinary capture exclusion or captured owners.
314#[must_use]
315pub(crate) struct StreamSubmissionPin<'a> {
316    _executing: Option<CaptureExecutingSubmission>,
317    owners: Option<&'a CaptureOwners>,
318    execution_id: u64,
319}
320
321fn capture_outcome<T, E>(
322    owners: Option<&CaptureOwners>,
323    submit: impl FnOnce() -> std::result::Result<T, E>,
324) -> std::result::Result<T, E> {
325    struct Outcome<'a> {
326        owners: Option<&'a CaptureOwners>,
327        success: bool,
328    }
329    impl Drop for Outcome<'_> {
330        fn drop(&mut self) {
331            if !self.success {
332                if let Some(owners) = self.owners {
333                    owners
334                        .lock()
335                        .unwrap_or_else(|error| error.into_inner())
336                        .failed = true;
337                }
338            }
339        }
340    }
341    let mut outcome = Outcome {
342        owners,
343        success: false,
344    };
345    let result = submit();
346    outcome.success = result.is_ok();
347    result
348}
349
350impl StreamSubmissionPin<'_> {
351    pub(crate) fn validate_execution(
352        &self,
353        stream: &CudaStream,
354    ) -> std::result::Result<u64, DriverError> {
355        let execution_id = if self.owners.is_some() {
356            self.execution_id
357        } else {
358            stream_execution_id(stream)?
359        };
360        if execution_id != self.execution_id {
361            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_CONTEXT));
362        }
363        Ok(execution_id)
364    }
365
366    pub(crate) fn with_serialized_submission<T, E>(
367        &self,
368        submit: impl FnOnce() -> std::result::Result<T, E>,
369    ) -> std::result::Result<T, E> {
370        let gate = self.owners.map(|owners| {
371            owners
372                .lock()
373                .unwrap_or_else(|error| error.into_inner())
374                .submission
375                .clone()
376        });
377        let _serial = gate
378            .as_ref()
379            .map(|gate| gate.lock().unwrap_or_else(|error| error.into_inner()));
380        capture_outcome(self.owners, submit)
381    }
382
383    pub(crate) fn capture_memory(
384        &self,
385        manifest: &Arc<crate::memory::MemoryAccessManifest>,
386    ) -> bool {
387        if let Some(owners) = self.owners {
388            owners
389                .lock()
390                .unwrap_or_else(|error| error.into_inner())
391                .memory
392                .push(manifest.clone());
393            true
394        } else {
395            false
396        }
397    }
398
399    pub(crate) fn with_submission<T>(
400        &self,
401        submit: impl FnOnce(Option<&CaptureOwners>) -> std::result::Result<T, DriverError>,
402    ) -> std::result::Result<T, DriverError> {
403        // The parent already authenticated and pinned the exact capture. Its
404        // close waits for this borrow; nested work must not acquire admission
405        // again after that close has forbidden independent submissions.
406        capture_outcome(self.owners, || submit(self.owners))
407    }
408}
409
410enum StreamSubmissionState {
411    Ordinary {
412        _reservation: CaptureExclusionReservation,
413        execution_id: u64,
414    },
415    Captured {
416        context: usize,
417        id: u64,
418        owners: CaptureOwners,
419        execution_id: u64,
420    },
421}
422
423impl StreamSubmissionPhase {
424    fn from_capture_info(
425        mut registry: std::sync::MutexGuard<'static, StreamCaptureRegistry>,
426        context: usize,
427        status: sys::CUstreamCaptureStatus,
428        id: u64,
429        ordinary_execution_id: Option<u64>,
430    ) -> std::result::Result<Self, DriverError> {
431        let state = match status {
432            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE => {
433                // Existing captures on other streams may continue. The query
434                // proved this stream ordinary; exclude any new managed begin.
435                StreamSubmissionState::Ordinary {
436                    _reservation: CaptureExclusionReservation::acquire(&mut registry),
437                    execution_id: ordinary_execution_id
438                        .ok_or(DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE))?,
439                }
440            }
441            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE => {
442                let entry = registry.modules.get(&(context, id)).ok_or(DriverError(
443                    sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
444                ))?;
445                if entry.closing {
446                    return Err(DriverError(
447                        sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_INVALIDATED,
448                    ));
449                }
450                StreamSubmissionState::Captured {
451                    context,
452                    id,
453                    owners: entry.owners.clone(),
454                    execution_id: entry.execution_id,
455                }
456            }
457            _ => {
458                return Err(DriverError(
459                    sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_INVALIDATED,
460                ));
461            }
462        };
463        Ok(Self { state })
464    }
465
466    /// Pin only the actual enqueue callback, after checking that the exact
467    /// capture and owner target remain open. No registry mutex crosses user or
468    /// driver submission code, and unwind releases the executing pin as well.
469    pub(crate) fn pin(&self) -> std::result::Result<StreamSubmissionPin<'_>, DriverError> {
470        match &self.state {
471            StreamSubmissionState::Ordinary { execution_id, .. } => Ok(StreamSubmissionPin {
472                _executing: None,
473                owners: None,
474                execution_id: *execution_id,
475            }),
476            StreamSubmissionState::Captured {
477                context,
478                id,
479                owners,
480                execution_id,
481            } => {
482                let executing = {
483                    let registry = stream_capture_registry();
484                    let entry = registry
485                        .modules
486                        .get(&(*context, *id))
487                        .filter(|entry| !entry.closing && Arc::ptr_eq(&entry.owners, owners))
488                        .ok_or(DriverError(
489                            sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_INVALIDATED,
490                        ))?;
491                    entry.executing.pin()
492                };
493                Ok(StreamSubmissionPin {
494                    _executing: Some(executing),
495                    owners: Some(owners),
496                    execution_id: *execution_id,
497                })
498            }
499        }
500    }
501
502    pub(crate) fn execution_id(&self) -> u64 {
503        match &self.state {
504            StreamSubmissionState::Ordinary { execution_id, .. }
505            | StreamSubmissionState::Captured { execution_id, .. } => *execution_id,
506        }
507    }
508
509    pub(crate) fn with_submission<T>(
510        &self,
511        submit: impl FnOnce(Option<&CaptureOwners>) -> std::result::Result<T, DriverError>,
512    ) -> std::result::Result<T, DriverError> {
513        let pin = self.pin()?;
514        pin.with_serialized_submission(|| pin.with_submission(submit))
515    }
516}
517
518pub(crate) fn acquire_stream_submission_phase(
519    stream: &CudaStream,
520) -> std::result::Result<StreamSubmissionPhase, DriverError> {
521    stream.context().bind_to_thread()?;
522    let registry = stream_capture_registry();
523    let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE;
524    let mut id = 0;
525    // SAFETY: the caller owns the stream and its bound context. Managed begin
526    // is serialized with this query and ordinary-exclusion acquisition; captured
527    // submission revalidates the identity before it obtains an executing pin.
528    unsafe {
529        sys::cuStreamGetCaptureInfo_v2(
530            stream.cu_stream(),
531            &mut status,
532            &mut id,
533            ptr::null_mut(),
534            ptr::null_mut(),
535            ptr::null_mut(),
536        )
537        .result()?;
538    }
539    let ordinary_execution_id =
540        if status == sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE {
541            Some(stream_execution_id(stream)?)
542        } else {
543            None
544        };
545    StreamSubmissionPhase::from_capture_info(
546        registry,
547        stream.context().cu_ctx() as usize,
548        status,
549        id,
550        ordinary_execution_id,
551    )
552}
553
554/// Called by the common kernel and graph launch boundaries before handing an
555/// executable to CUDA. The graph owns exact modules, independently of names.
556pub(crate) fn submit_with_capture(
557    stream: &CudaStream,
558    submit: impl FnOnce(Option<&CaptureOwners>) -> std::result::Result<(), DriverError>,
559) -> std::result::Result<(), DriverError> {
560    acquire_stream_submission_phase(stream)?.with_submission(submit)
561}
562
563static ACTIVE_STREAM_CAPTURES: OnceLock<Mutex<StreamCaptureRegistry>> = OnceLock::new();
564
565fn stream_capture_registry() -> std::sync::MutexGuard<'static, StreamCaptureRegistry> {
566    ACTIVE_STREAM_CAPTURES
567        .get_or_init(|| Mutex::new(StreamCaptureRegistry::default()))
568        .lock()
569        .unwrap_or_else(|poisoned| poisoned.into_inner())
570}
571
572/// One exclusive driver-mutation reservation in the capture lifecycle owner.
573/// Never hold the registry mutex while executing driver calls or destructors.
574struct DriverMutationReservation {
575    active: bool,
576    current: Option<DeferredRetirement>,
577    retry: VecDeque<DeferredRetirement>,
578}
579
580impl Drop for DriverMutationReservation {
581    fn drop(&mut self) {
582        if self.active {
583            // Restore unfinished tasks to the same authoritative queue, including
584            // the executing retry closure if its driver boundary unwound. No
585            // destructor or driver operation runs under this mutex.
586            let mut registry = stream_capture_registry();
587            registry.deferred.append(&mut self.retry);
588            if matches!(self.current, Some(DeferredRetirement::Retry(_))) {
589                registry
590                    .deferred
591                    .push_back(self.current.take().expect("executing retirement present"));
592            }
593            registry.retiring = false;
594        }
595    }
596}
597
598pub(crate) fn with_module_loading<T>(
599    load: impl FnOnce() -> std::result::Result<T, DriverError>,
600) -> std::result::Result<T, DriverError> {
601    drain_capture_retirements(stream_capture_registry());
602    let reservation = reserve_capture_exclusion()?;
603    let loaded = load();
604    drop(reservation);
605    drain_capture_retirements(stream_capture_registry());
606    loaded
607}
608
609pub(crate) fn reserve_capture_exclusion(
610) -> std::result::Result<CaptureExclusionReservation, DriverError> {
611    let mut registry = stream_capture_registry();
612    if !registry.active.is_empty() {
613        return Err(DriverError(
614            sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
615        ));
616    }
617    Ok(CaptureExclusionReservation::acquire(&mut registry))
618}
619
620/// Excludes managed capture while an ordinary resident operation submits or waits.
621/// No mutex is held by this reservation and its release does not drain the cold
622/// retirement queue. It does not prevent a cold caller from retiring resources.
623#[doc(hidden)]
624pub struct CaptureExclusionReservation {
625    _private: (),
626}
627
628impl CaptureExclusionReservation {
629    fn acquire(registry: &mut StreamCaptureRegistry) -> Self {
630        registry.capture_exclusions += 1;
631        Self { _private: () }
632    }
633}
634
635impl Drop for CaptureExclusionReservation {
636    fn drop(&mut self) {
637        stream_capture_registry().capture_exclusions -= 1;
638    }
639}
640
641/// Admit an ordinary operation on a stream, never a captured enqueue or wait.
642/// The reservation closes the check-to-submit race against managed BeginCapture.
643/// Foreign/raw capture must be externally serialized with XLOG stream operations.
644/// This call does not drain deferred destruction or add a blocking hot-loop wait.
645#[doc(hidden)]
646pub fn reserve_uncaptured_stream(
647    stream: &CudaStream,
648) -> std::result::Result<CaptureExclusionReservation, DriverError> {
649    let reservation = reserve_capture_exclusion()?;
650    stream.context().bind_to_thread()?;
651    let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE;
652    // SAFETY: the stream and context remain owned by the caller; the registry
653    // reservation excludes managed capture until the ordinary operation returns.
654    unsafe {
655        sys::cuStreamIsCapturing(stream.cu_stream(), &mut status).result()?;
656    }
657    match status {
658        sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE => Ok(reservation),
659        sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE => Err(DriverError(
660            sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
661        )),
662        _ => Err(DriverError(
663            sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_INVALIDATED,
664        )),
665    }
666}
667
668/// Run destruction only outside all captures managed by this module. The
669/// reservation also excludes new captures until destruction has returned.
670/// Closures must not begin capture; nested retirement is queued. An unwind
671/// releases the reservation without discarding other queued resources.
672/// The call that drains the queue may wait, including a later capture release
673/// or module load. Its caller must satisfy this module's cold-lifecycle contract.
674#[doc(hidden)]
675pub fn retire_after_stream_captures(retire: impl FnOnce() + Send + 'static) {
676    enqueue_retirement(DeferredRetirement::Once(Some(Box::new(retire))));
677}
678
679/// Retain an actual resource until its release can be proved. A false result
680/// leaves this same callable owner queued for the next cold lifecycle drain;
681/// it is never retried in a tight loop. Unwind preserves it for a later drain.
682pub(crate) fn retry_retirement_after_stream_captures(
683    retire: impl FnMut() -> bool + Send + 'static,
684) {
685    enqueue_retirement(DeferredRetirement::Retry(Box::new(retire)));
686}
687
688fn enqueue_retirement(retire: DeferredRetirement) {
689    let mut registry = stream_capture_registry();
690    registry.deferred.push_back(retire);
691    drain_capture_retirements(registry);
692}
693
694/// Explicit cold reaper for resources whose last allocator owner is gone.
695/// Like capture release/module loading, it may wait and must run without caller
696/// locks needed by pending work. Active captures leave the queue untouched.
697pub(crate) fn reap_capture_retirements() {
698    drain_capture_retirements(stream_capture_registry());
699}
700
701// Tests that assert a particular global capture/retirement state must not
702// interleave independent scenarios. Worker threads within a scenario remain
703// concurrent and exercise the production queue without this test-only lock.
704#[cfg(test)]
705pub(crate) fn capture_lifecycle_test_guard() -> std::sync::MutexGuard<'static, ()> {
706    static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
707    TEST_LOCK.lock().unwrap_or_else(|error| error.into_inner())
708}
709
710fn drain_capture_retirements(mut registry: std::sync::MutexGuard<'static, StreamCaptureRegistry>) {
711    if !registry.active.is_empty() || registry.retiring || registry.deferred.is_empty() {
712        return;
713    }
714    registry.retiring = true;
715    let mut reservation = DriverMutationReservation {
716        active: true,
717        current: None,
718        retry: VecDeque::new(),
719    };
720    loop {
721        let Some(retire) = registry.deferred.pop_front() else {
722            registry.deferred.append(&mut reservation.retry);
723            registry.retiring = false;
724            reservation.active = false;
725            return;
726        };
727        // No driver calls or resource destructors under the mutex. The retiring
728        // reservation excludes begin-capture across this unlock/relock window.
729        drop(registry);
730        reservation.current = Some(retire);
731        let completed = reservation
732            .current
733            .as_mut()
734            .expect("executing retirement present")
735            .attempt();
736        let retire = reservation
737            .current
738            .take()
739            .expect("executing retirement present");
740        if completed {
741            drop(retire);
742        } else {
743            reservation.retry.push_back(retire);
744        }
745        registry = stream_capture_registry();
746    }
747}
748
749fn release_stream_capture_key(key: StreamCaptureKey) {
750    let mut registry = stream_capture_registry();
751    registry.active.remove(&key);
752    drain_capture_retirements(registry);
753}
754
755#[derive(Debug)]
756struct StreamCaptureLease {
757    key: StreamCaptureKey,
758    driver_active: bool,
759    modules: CaptureOwners,
760    capture_id: Option<u64>,
761}
762
763impl StreamCaptureLease {
764    fn register_capture(&mut self, id: u64, registry: &mut StreamCaptureRegistry) {
765        self.capture_id = Some(id);
766        registry.modules.insert(
767            (self.key.context, id),
768            CaptureSubmissionEntry {
769                owners: self.modules.clone(),
770                execution_id: self.key.stream,
771                closing: false,
772                executing: Arc::default(),
773            },
774        );
775    }
776
777    fn close_submission_admission(&self) -> Option<Arc<CaptureExecutingSubmissions>> {
778        let id = self.capture_id?;
779        let mut registry = stream_capture_registry();
780        let entry = registry.modules.get_mut(&(self.key.context, id))?;
781        if !Arc::ptr_eq(&entry.owners, &self.modules) {
782            return None;
783        }
784        entry.closing = true;
785        Some(entry.executing.clone())
786    }
787
788    fn complete_driver_capture(&mut self) {
789        self.driver_active = false;
790        if let Some(id) = self.capture_id.take() {
791            let removed = stream_capture_registry()
792                .modules
793                .remove(&(self.key.context, id));
794            // Release the registry entry outside its mutex. The lease and any
795            // returned graph still own the captured resource target.
796            drop(removed);
797        }
798    }
799}
800
801fn try_acquire_stream_capture_key(
802    key: StreamCaptureKey,
803) -> std::result::Result<StreamCaptureLease, CudaConditionalGraphUnavailable> {
804    drain_capture_retirements(stream_capture_registry());
805    let mut registry = stream_capture_registry();
806    if registry.retiring || registry.capture_exclusions != 0 || !registry.active.insert(key) {
807        return Err(CudaConditionalGraphUnavailable::StreamCaptureBusy);
808    }
809    Ok(StreamCaptureLease {
810        key,
811        driver_active: false,
812        modules: Arc::default(),
813        capture_id: None,
814    })
815}
816
817fn try_acquire_stream_capture(
818    stream: &CudaStream,
819) -> std::result::Result<StreamCaptureLease, CudaConditionalGraphUnavailable> {
820    let context =
821        stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
822    try_acquire_stream_capture_key(StreamCaptureKey {
823        context: context as usize,
824        stream: stream_execution_id(stream)
825            .map_err(CudaConditionalGraphUnavailable::body_population)?,
826    })
827}
828
829impl Drop for StreamCaptureLease {
830    fn drop(&mut self) {
831        if !self.driver_active {
832            release_stream_capture_key(self.key);
833        }
834    }
835}
836
837/// Keeps the host reservation until the driver has actually left capture.
838/// A callback unwind takes the same end-capture path as an ordinary return.
839struct DriverStreamCapture<'a> {
840    lease: StreamCaptureLease,
841    stream: &'a CudaStream,
842    owns_returned_graph: bool,
843}
844
845impl<'a> DriverStreamCapture<'a> {
846    fn prepare(
847        stream: &'a CudaStream,
848        owns_returned_graph: bool,
849        modules: CaptureOwners,
850    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
851        let mut lease = try_acquire_stream_capture(stream)?;
852        lease.modules = modules.clone();
853        Ok(Self {
854            lease,
855            stream,
856            owns_returned_graph,
857        })
858    }
859
860    fn begin(
861        &mut self,
862        operation: &'static str,
863        begin: impl FnOnce() -> sys::CUresult,
864    ) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
865        self.stream
866            .context()
867            .bind_to_thread()
868            .map_err(CudaConditionalGraphUnavailable::body_population)?;
869        let mut registry = stream_capture_registry();
870        // An ordinary phase may have been acquired after this capture reserved
871        // its stream but before reaching the driver begin boundary.
872        if registry.capture_exclusions != 0 {
873            return Err(CudaConditionalGraphUnavailable::StreamCaptureBusy);
874        }
875        conditional_driver_call(operation, begin())?;
876        self.lease.driver_active = true;
877        let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE;
878        let mut id = 0;
879        conditional_driver_call("cuStreamGetCaptureInfo_v2", unsafe {
880            sys::cuStreamGetCaptureInfo_v2(
881                self.stream.cu_stream(),
882                &mut status,
883                &mut id,
884                ptr::null_mut(),
885                ptr::null_mut(),
886                ptr::null_mut(),
887            )
888        })?;
889        if status != sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE {
890            return Err(CudaConditionalGraphUnavailable::body_population(
891                "begin-capture did not enter active capture",
892            ));
893        }
894        self.lease.register_capture(id, &mut registry);
895        Ok(())
896    }
897
898    fn finish(&mut self) -> std::result::Result<sys::CUgraph, CudaConditionalGraphUnavailable> {
899        if let Some(executing) = self.lease.close_submission_admission() {
900            // Saved descriptors are passive. Only enqueues that already passed
901            // admission may delay EndCapture, without blocking registry access.
902            executing.wait_until_idle();
903        }
904        self.stream
905            .context()
906            .bind_to_thread()
907            .map_err(CudaConditionalGraphUnavailable::body_population)?;
908        let mut graph = ptr::null_mut();
909        // SAFETY: BeginCapture succeeded on this stream and thread, the stream
910        // is borrowed for this guard's lifetime, and its context is now bound.
911        let code = unsafe { sys::cuStreamEndCapture(self.stream.cu_stream(), &mut graph) };
912        let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE;
913        let driver_active = code != sys::CUresult::CUDA_SUCCESS
914            && !(unsafe { sys::cuStreamIsCapturing(self.stream.cu_stream(), &mut status) }
915                == sys::CUresult::CUDA_SUCCESS
916                && status == sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE);
917        if !driver_active {
918            self.lease.complete_driver_capture();
919        }
920        conditional_driver_call("cuStreamEndCapture", code)?;
921        if self
922            .lease
923            .modules
924            .lock()
925            .unwrap_or_else(|error| error.into_inner())
926            .failed
927        {
928            if self.owns_returned_graph && !graph.is_null() {
929                drop(UninstantiatedCudaGraph {
930                    raw: graph,
931                    context: self.stream.context().clone(),
932                    modules: self.lease.modules.clone(),
933                });
934            }
935            return Err(CudaConditionalGraphUnavailable::body_population(
936                "a captured submission failed or unwound; the graph cannot be instantiated",
937            ));
938        }
939        Ok(graph)
940    }
941}
942
943impl Drop for DriverStreamCapture<'_> {
944    fn drop(&mut self) {
945        if self.lease.driver_active {
946            match self.finish() {
947                Ok(graph) if self.owns_returned_graph && !graph.is_null() => {
948                    drop(UninstantiatedCudaGraph {
949                        raw: graph,
950                        context: self.stream.context().clone(),
951                        modules: self.lease.modules.clone(),
952                    });
953                }
954                _ => {}
955            }
956        }
957        if self.lease.driver_active {
958            // Unknown completion is not permission to release the reservation
959            // or context. Destruction queued behind this capture stays retained.
960            let context = self.stream.context().clone();
961            let modules = self.lease.modules.clone();
962            retire_after_stream_captures(move || drop((context, modules)));
963        }
964    }
965}
966
967impl fmt::Display for CudaConditionalGraphUnavailable {
968    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
969        formatter.write_str(&self.decline_detail())
970    }
971}
972
973impl std::error::Error for CudaConditionalGraphUnavailable {}
974
975impl From<XlogError> for CudaConditionalGraphUnavailable {
976    fn from(error: XlogError) -> Self {
977        Self::body_population(error)
978    }
979}
980
981struct ConditionalGraphDriverApi {
982    _library: Arc<Library>,
983    driver_get_version: DriverGetVersionFn,
984    conditional_handle_create: ConditionalHandleCreateFn,
985    graph_add_node: GraphAddNodeFn,
986}
987
988impl ConditionalGraphDriverApi {
989    fn load() -> std::result::Result<Arc<Self>, CudaConditionalGraphUnavailable> {
990        #[cfg(target_os = "windows")]
991        const CUDA_DRIVER_LIBRARY: &str = "nvcuda.dll";
992        #[cfg(not(target_os = "windows"))]
993        const CUDA_DRIVER_LIBRARY: &str = "libcuda.so.1";
994
995        let library = Arc::new(
996            unsafe { Library::new(CUDA_DRIVER_LIBRARY) }
997                .map_err(|_| CudaConditionalGraphUnavailable::DriverLibraryUnavailable)?,
998        );
999        let driver_get_version = unsafe {
1000            load_required_symbol(&library, b"cuDriverGetVersion\0", "cuDriverGetVersion")?
1001        };
1002        let conditional_handle_create = unsafe {
1003            load_required_symbol(
1004                &library,
1005                b"cuGraphConditionalHandleCreate\0",
1006                "cuGraphConditionalHandleCreate",
1007            )?
1008        };
1009        let graph_add_node =
1010            unsafe { load_required_symbol(&library, b"cuGraphAddNode\0", "cuGraphAddNode")? };
1011
1012        let api = Arc::new(Self {
1013            _library: library,
1014            driver_get_version,
1015            conditional_handle_create,
1016            graph_add_node,
1017        });
1018        api.require_supported_driver()?;
1019        Ok(api)
1020    }
1021
1022    fn require_supported_driver(&self) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
1023        let mut version = 0;
1024        let code = unsafe { (self.driver_get_version)(&mut version) };
1025        if code != sys::CUresult::CUDA_SUCCESS {
1026            return Err(CudaConditionalGraphUnavailable::DriverVersionQueryFailed { code });
1027        }
1028        require_conditional_graph_driver(version)
1029    }
1030}
1031
1032unsafe fn load_required_symbol<F: Copy>(
1033    library: &Library,
1034    name: &'static [u8],
1035    display_name: &'static str,
1036) -> std::result::Result<F, CudaConditionalGraphUnavailable> {
1037    library.get::<F>(name).map(|symbol| *symbol).map_err(|_| {
1038        CudaConditionalGraphUnavailable::MissingDriverSymbol {
1039            symbol: display_name,
1040        }
1041    })
1042}
1043
1044fn require_conditional_graph_driver(
1045    version: i32,
1046) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
1047    if version < CONDITIONAL_GRAPH_MINIMUM_DRIVER {
1048        Err(CudaConditionalGraphUnavailable::DriverVersionTooOld {
1049            found: version,
1050            required: CONDITIONAL_GRAPH_MINIMUM_DRIVER,
1051        })
1052    } else {
1053        Ok(())
1054    }
1055}
1056
1057fn conditional_node_params(
1058    handle: sys::CUgraphConditionalHandle,
1059    ctx: sys::CUcontext,
1060    kind: sys::CUgraphConditionalNodeType,
1061) -> sys::CUgraphNodeParams {
1062    let mut params: sys::CUgraphNodeParams = unsafe { mem::zeroed() };
1063    params.type_ = sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL;
1064    params.__bindgen_anon_1.conditional = sys::CUDA_CONDITIONAL_NODE_PARAMS {
1065        handle,
1066        type_: kind,
1067        size: 1,
1068        phGraph_out: ptr::null_mut(),
1069        ctx,
1070    };
1071    params
1072}
1073
1074fn conditional_driver_call(
1075    operation: &'static str,
1076    code: sys::CUresult,
1077) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
1078    if code == sys::CUresult::CUDA_SUCCESS {
1079        Ok(())
1080    } else {
1081        Err(CudaConditionalGraphUnavailable::DriverCallFailed { operation, code })
1082    }
1083}
1084
1085struct UninstantiatedCudaGraph {
1086    raw: sys::CUgraph,
1087    context: Arc<CudaContext>,
1088    modules: CaptureOwners,
1089}
1090
1091impl UninstantiatedCudaGraph {
1092    fn create(
1093        context: Arc<CudaContext>,
1094    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
1095        context
1096            .bind_to_thread()
1097            .map_err(CudaConditionalGraphUnavailable::body_population)?;
1098        let mut raw = ptr::null_mut();
1099        unsafe {
1100            conditional_driver_call("cuGraphCreate", sys::cuGraphCreate(&mut raw, 0))?;
1101        }
1102        if raw.is_null() {
1103            Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1104                operation: "cuGraphCreate",
1105            })
1106        } else {
1107            Ok(Self {
1108                raw,
1109                context,
1110                modules: Arc::default(),
1111            })
1112        }
1113    }
1114
1115    fn raw(&self) -> sys::CUgraph {
1116        self.raw
1117    }
1118
1119    fn into_raw(mut self) -> sys::CUgraph {
1120        let raw = self.raw;
1121        self.raw = ptr::null_mut();
1122        raw
1123    }
1124}
1125
1126impl Drop for UninstantiatedCudaGraph {
1127    fn drop(&mut self) {
1128        if !self.raw.is_null() {
1129            let graph = self.raw as usize;
1130            let context = self.context.clone();
1131            let modules = self.modules.clone();
1132            retire_resources_after_completion(
1133                (graph, context, modules),
1134                |(_, context, _)| context.bind_to_thread(),
1135                |(graph, _, _)| unsafe { sys::cuGraphDestroy(*graph as sys::CUgraph).result() },
1136                |_| Ok(()),
1137                |(_, context, _), error| context.record_err::<()>(Err(error)),
1138            );
1139        }
1140    }
1141}
1142
1143/// CUDA-owned body graph produced while adding one conditional-WHILE node.
1144///
1145/// The body graph and conditional handle are owned by the parent graph. They
1146/// must not be destroyed independently. The numeric handle is intended to be
1147/// passed by value to a device kernel that calls `cudaGraphSetConditional`.
1148#[derive(Debug)]
1149pub struct ConditionalCudaGraphBody {
1150    graph: sys::CUgraph,
1151    handle: sys::CUgraphConditionalHandle,
1152    context: sys::CUcontext,
1153    modules: CaptureOwners,
1154}
1155
1156impl ConditionalCudaGraphBody {
1157    pub fn graph(&self) -> sys::CUgraph {
1158        self.graph
1159    }
1160
1161    pub fn handle(&self) -> sys::CUgraphConditionalHandle {
1162        self.handle
1163    }
1164
1165    pub fn context(&self) -> sys::CUcontext {
1166        self.context
1167    }
1168
1169    /// Return this body's actual node kinds in dependency-chain order.
1170    ///
1171    /// The body must be a single linear dependency chain. CUDA's node-list
1172    /// enumeration order is not used as an execution-order signal.
1173    pub fn linear_chain_node_kinds(
1174        &self,
1175    ) -> std::result::Result<Vec<CudaGraphNodeKind>, CudaConditionalGraphUnavailable> {
1176        let mut check = |operation, code| conditional_driver_call(operation, code);
1177        let mut shape_error = |error| CudaConditionalGraphUnavailable::BodyPopulationFailed {
1178            detail: format!("conditional graph body is not a linear dependency chain: {error}"),
1179        };
1180        graph_linear_chain_node_kinds_with(self.graph, &mut check, &mut shape_error)
1181    }
1182
1183    /// Capture graph-compatible work directly into this conditional body.
1184    ///
1185    /// `stream` must be a non-default stream. The callback must not allocate or
1186    /// free CUDA memory, synchronize, or record/wait on events. Host-only
1187    /// suballocation from retained device storage is allowed if it cannot grow
1188    /// or remap that storage; captured addresses must remain valid and must not
1189    /// be recycled while graph consumers or saved backward values need them.
1190    /// CUDA conditional bodies allow only kernel, empty, child-graph, device
1191    /// memcpy/memset, and nested conditional nodes.
1192    /// Capture setup and cleanup may wait for deferred destruction under the
1193    /// [module-level lifecycle contract](crate::cuda_graph).
1194    pub fn capture_on_stream<F, E>(
1195        &self,
1196        stream: &CudaStream,
1197        record: F,
1198    ) -> std::result::Result<(), CudaConditionalGraphUnavailable>
1199    where
1200        F: FnOnce() -> std::result::Result<(), E>,
1201        E: fmt::Display,
1202    {
1203        let owners = self.modules.clone();
1204        capture_outcome(Some(&owners), || {
1205            let stream_ctx =
1206                stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
1207            if stream_ctx != self.context {
1208                return Err(CudaConditionalGraphUnavailable::ContextMismatch);
1209            }
1210            let mut capture = DriverStreamCapture::prepare(stream, false, self.modules.clone())?;
1211
1212            unsafe {
1213                capture.begin("cuStreamBeginCaptureToGraph", || {
1214                    sys::cuStreamBeginCaptureToGraph(
1215                        stream.cu_stream(),
1216                        self.graph,
1217                        ptr::null(),
1218                        ptr::null(),
1219                        0,
1220                        sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
1221                    )
1222                })?;
1223            }
1224
1225            let record_result = capture_outcome(Some(&capture.lease.modules), record);
1226            let end_result = capture.finish();
1227            if let Err(error) = record_result {
1228                return Err(CudaConditionalGraphUnavailable::body_population(error));
1229            }
1230            let captured = end_result?;
1231            if captured.is_null() {
1232                return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1233                    operation: "cuStreamEndCapture",
1234                });
1235            }
1236            if captured != self.graph {
1237                return Err(CudaConditionalGraphUnavailable::BodyPopulationFailed {
1238                    detail: "capture returned a graph other than the conditional body".to_string(),
1239                });
1240            }
1241            Ok(())
1242        })
1243    }
1244}
1245
1246/// Builds one dependency-ordered parent graph containing ordinary captured
1247/// segments, one-shot conditional-IF bodies and conditional-WHILE loops.
1248///
1249/// Stratified Datalog uses this topology so work before a
1250/// recursive strongly connected component executes once, only that component
1251/// is placed in a device-controlled WHILE, and later strata depend on its
1252/// completion. An IF body instead admits or skips a whole bounded step after
1253/// its device preflight. The finished value launches through one `cuGraphLaunch`.
1254pub struct ConditionalCudaGraphSequenceBuilder {
1255    graph: UninstantiatedCudaGraph,
1256    raw_context: sys::CUcontext,
1257    api: Arc<ConditionalGraphDriverApi>,
1258    frontier: Vec<sys::CUgraphNode>,
1259}
1260
1261impl ConditionalCudaGraphSequenceBuilder {
1262    /// Retain original resources and the native stream before recording any
1263    /// segment. They follow the same graph ownership and uncertain-retirement
1264    /// path as captured modules and memory manifests.
1265    pub(crate) fn new_retaining(
1266        stream: &Arc<CudaStream>,
1267        mut external_resources: Vec<Arc<dyn Send + Sync>>,
1268    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
1269        let builder = Self::new(stream)?;
1270        external_resources.push(stream.clone());
1271        builder
1272            .graph
1273            .modules
1274            .lock()
1275            .unwrap_or_else(|error| error.into_inner())
1276            .external_resources
1277            .extend(external_resources);
1278        Ok(builder)
1279    }
1280
1281    /// Create an empty parent graph bound to `stream`'s CUDA context.
1282    pub fn new(stream: &CudaStream) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
1283        let api = ConditionalGraphDriverApi::load()?;
1284        let context = stream.context().clone();
1285        let raw_context =
1286            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
1287        if raw_context != context.cu_ctx() {
1288            return Err(CudaConditionalGraphUnavailable::ContextMismatch);
1289        }
1290        Ok(Self {
1291            graph: UninstantiatedCudaGraph::create(Arc::clone(&context))?,
1292            raw_context,
1293            api,
1294            frontier: Vec::new(),
1295        })
1296    }
1297
1298    /// Capture one ordinary graph segment after the current dependency
1299    /// frontier. The callback may enqueue only capture-compatible operations.
1300    /// Capture setup and cleanup may wait for deferred destruction under the
1301    /// [module-level lifecycle contract](crate::cuda_graph).
1302    pub fn capture_segment_on_stream<F, E>(
1303        &mut self,
1304        stream: &CudaStream,
1305        record: F,
1306    ) -> std::result::Result<(), CudaConditionalGraphUnavailable>
1307    where
1308        F: FnOnce() -> std::result::Result<(), E>,
1309        E: fmt::Display,
1310    {
1311        let owners = self.graph.modules.clone();
1312        capture_outcome(Some(&owners), || {
1313            self.ensure_stream_context(stream)?;
1314            let mut capture =
1315                DriverStreamCapture::prepare(stream, false, self.graph.modules.clone())?;
1316            unsafe {
1317                capture.begin("cuStreamBeginCaptureToGraph", || {
1318                    sys::cuStreamBeginCaptureToGraph(
1319                        stream.cu_stream(),
1320                        self.graph.raw(),
1321                        if self.frontier.is_empty() {
1322                            ptr::null()
1323                        } else {
1324                            self.frontier.as_ptr()
1325                        },
1326                        ptr::null(),
1327                        self.frontier.len(),
1328                        sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
1329                    )
1330                })?;
1331            }
1332
1333            let record_result = capture_outcome(Some(&capture.lease.modules), record);
1334            let end_result = capture.finish();
1335            if let Err(error) = record_result {
1336                return Err(CudaConditionalGraphUnavailable::body_population(error));
1337            }
1338            let captured = end_result?;
1339            if captured != self.graph.raw() {
1340                return Err(CudaConditionalGraphUnavailable::BodyPopulationFailed {
1341                    detail: "segment capture returned a graph other than its parent".to_string(),
1342                });
1343            }
1344            self.frontier = graph_leaf_nodes(self.graph.raw())?;
1345            Ok(())
1346        })
1347    }
1348
1349    /// Append one conditional-WHILE node after the current frontier.
1350    pub fn add_conditional_while<F>(
1351        &mut self,
1352        initial_value: u32,
1353        assign_default_on_launch: bool,
1354        populate_body: F,
1355    ) -> std::result::Result<sys::CUgraphConditionalHandle, CudaConditionalGraphUnavailable>
1356    where
1357        F: FnOnce(
1358            &ConditionalCudaGraphBody,
1359        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
1360    {
1361        let owners = self.graph.modules.clone();
1362        capture_outcome(Some(&owners), || {
1363            let handle = self.create_conditional_handle(initial_value, assign_default_on_launch)?;
1364            self.append_conditional_body(
1365                handle,
1366                sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE,
1367                populate_body,
1368            )?;
1369            Ok(handle)
1370        })
1371    }
1372
1373    /// Capture a device admission test, then execute its body at most once.
1374    ///
1375    /// The preflight receives the actual graph handle and must set it on every
1376    /// replay. Its nodes precede the IF node on the same dependency frontier;
1377    /// a rejected step never enters the body. The handle defaults to false on
1378    /// each launch. Neither callback runs when the finished graph is replayed.
1379    pub fn add_conditional_if<P, F, E>(
1380        &mut self,
1381        stream: &CudaStream,
1382        preflight: P,
1383        populate_body: F,
1384    ) -> std::result::Result<sys::CUgraphConditionalHandle, CudaConditionalGraphUnavailable>
1385    where
1386        P: FnOnce(sys::CUgraphConditionalHandle) -> std::result::Result<(), E>,
1387        E: fmt::Display,
1388        F: FnOnce(
1389            &ConditionalCudaGraphBody,
1390        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
1391    {
1392        let owners = self.graph.modules.clone();
1393        capture_outcome(Some(&owners), || {
1394            self.ensure_stream_context(stream)?;
1395            let handle = self.create_conditional_handle(0, true)?;
1396            self.capture_segment_on_stream(stream, || preflight(handle))?;
1397            self.append_conditional_body(
1398                handle,
1399                sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_IF,
1400                populate_body,
1401            )?;
1402            Ok(handle)
1403        })
1404    }
1405
1406    fn create_conditional_handle(
1407        &self,
1408        initial_value: u32,
1409        assign_default_on_launch: bool,
1410    ) -> std::result::Result<sys::CUgraphConditionalHandle, CudaConditionalGraphUnavailable> {
1411        let mut handle = 0;
1412        let flags = if assign_default_on_launch {
1413            sys::CU_GRAPH_COND_ASSIGN_DEFAULT
1414        } else {
1415            0
1416        };
1417        unsafe {
1418            conditional_driver_call(
1419                "cuGraphConditionalHandleCreate",
1420                (self.api.conditional_handle_create)(
1421                    &mut handle,
1422                    self.graph.raw(),
1423                    self.raw_context,
1424                    initial_value,
1425                    flags,
1426                ),
1427            )?;
1428        }
1429        Ok(handle)
1430    }
1431
1432    fn append_conditional_body<F>(
1433        &mut self,
1434        handle: sys::CUgraphConditionalHandle,
1435        kind: sys::CUgraphConditionalNodeType,
1436        populate_body: F,
1437    ) -> std::result::Result<(), CudaConditionalGraphUnavailable>
1438    where
1439        F: FnOnce(
1440            &ConditionalCudaGraphBody,
1441        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
1442    {
1443        let mut params = conditional_node_params(handle, self.raw_context, kind);
1444        let mut conditional_node = ptr::null_mut();
1445        unsafe {
1446            conditional_driver_call(
1447                "cuGraphAddNode",
1448                (self.api.graph_add_node)(
1449                    &mut conditional_node,
1450                    self.graph.raw(),
1451                    if self.frontier.is_empty() {
1452                        ptr::null()
1453                    } else {
1454                        self.frontier.as_ptr()
1455                    },
1456                    self.frontier.len(),
1457                    &mut params,
1458                ),
1459            )?;
1460        }
1461        if conditional_node.is_null() {
1462            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1463                operation: "cuGraphAddNode",
1464            });
1465        }
1466        let conditional = unsafe { params.__bindgen_anon_1.conditional };
1467        if conditional.phGraph_out.is_null() {
1468            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1469                operation: "cuGraphAddNode body array",
1470            });
1471        }
1472        let body_graph = unsafe { *conditional.phGraph_out };
1473        if body_graph.is_null() {
1474            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1475                operation: "cuGraphAddNode conditional body",
1476            });
1477        }
1478        populate_body(&ConditionalCudaGraphBody {
1479            graph: body_graph,
1480            handle,
1481            context: self.raw_context,
1482            modules: self.graph.modules.clone(),
1483        })?;
1484        self.frontier.clear();
1485        self.frontier.push(conditional_node);
1486        Ok(())
1487    }
1488
1489    /// Instantiate the complete parent graph exactly once.
1490    pub fn instantiate(
1491        self,
1492    ) -> std::result::Result<CapturedCudaGraph, CudaConditionalGraphUnavailable> {
1493        let ConditionalCudaGraphSequenceBuilder { graph, api, .. } = self;
1494        let mut captured = CapturedCudaGraph::instantiate_graph(graph)?;
1495        captured._conditional_api = Some(api);
1496        Ok(captured)
1497    }
1498
1499    fn ensure_stream_context(
1500        &self,
1501        stream: &CudaStream,
1502    ) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
1503        let context =
1504            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
1505        if context == self.raw_context {
1506            Ok(())
1507        } else {
1508            Err(CudaConditionalGraphUnavailable::ContextMismatch)
1509        }
1510    }
1511}
1512
1513fn raw_graph_nodes_with<E>(
1514    graph: sys::CUgraph,
1515    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
1516) -> std::result::Result<Vec<sys::CUgraphNode>, E> {
1517    let mut node_count = 0usize;
1518    unsafe {
1519        check(
1520            "cuGraphGetNodes(count)",
1521            sys::cuGraphGetNodes(graph, ptr::null_mut(), &mut node_count),
1522        )?;
1523    }
1524    let mut nodes = vec![ptr::null_mut(); node_count];
1525    if node_count != 0 {
1526        unsafe {
1527            check(
1528                "cuGraphGetNodes(nodes)",
1529                sys::cuGraphGetNodes(graph, nodes.as_mut_ptr(), &mut node_count),
1530            )?;
1531        }
1532        nodes.truncate(node_count);
1533    }
1534    Ok(nodes)
1535}
1536
1537#[derive(Debug, Clone, PartialEq, Eq)]
1538enum LinearGraphChainError {
1539    ForeignDependency { node: usize, dependency: usize },
1540    DuplicateDependency { node: usize, dependency: usize },
1541    Cycle,
1542    RootCount { found: usize },
1543    IncomingDegree { node: usize, dependencies: usize },
1544    Branch { node: usize, dependents: usize },
1545    LeafCount { found: usize },
1546    Disconnected { visited: usize, total: usize },
1547}
1548
1549impl fmt::Display for LinearGraphChainError {
1550    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1551        match self {
1552            Self::ForeignDependency { node, dependency } => write!(
1553                formatter,
1554                "node {node} depends on foreign enumeration index {dependency}"
1555            ),
1556            Self::DuplicateDependency { node, dependency } => write!(
1557                formatter,
1558                "node {node} repeats dependency enumeration index {dependency}"
1559            ),
1560            Self::Cycle => formatter.write_str("dependency graph contains a cycle"),
1561            Self::RootCount { found } => {
1562                write!(
1563                    formatter,
1564                    "dependency graph has {found} roots instead of one"
1565                )
1566            }
1567            Self::IncomingDegree { node, dependencies } => write!(
1568                formatter,
1569                "non-root node {node} has {dependencies} immediate dependencies instead of one"
1570            ),
1571            Self::Branch { node, dependents } => write!(
1572                formatter,
1573                "node {node} has {dependents} immediate dependents instead of at most one"
1574            ),
1575            Self::LeafCount { found } => {
1576                write!(
1577                    formatter,
1578                    "dependency graph has {found} leaves instead of one"
1579                )
1580            }
1581            Self::Disconnected { visited, total } => write!(
1582                formatter,
1583                "dependency chain visits {visited} of {total} enumerated nodes"
1584            ),
1585        }
1586    }
1587}
1588
1589fn linear_chain_order(
1590    immediate_dependencies: &[Vec<usize>],
1591) -> std::result::Result<Vec<usize>, LinearGraphChainError> {
1592    let node_count = immediate_dependencies.len();
1593    let mut outgoing = vec![Vec::new(); node_count];
1594    for (node, dependencies) in immediate_dependencies.iter().enumerate() {
1595        let mut unique = HashSet::with_capacity(dependencies.len());
1596        for &dependency in dependencies {
1597            if dependency >= node_count {
1598                return Err(LinearGraphChainError::ForeignDependency { node, dependency });
1599            }
1600            if !unique.insert(dependency) {
1601                return Err(LinearGraphChainError::DuplicateDependency { node, dependency });
1602            }
1603            outgoing[dependency].push(node);
1604        }
1605    }
1606
1607    let mut remaining_indegree = immediate_dependencies
1608        .iter()
1609        .map(Vec::len)
1610        .collect::<Vec<_>>();
1611    let mut ready = remaining_indegree
1612        .iter()
1613        .enumerate()
1614        .filter_map(|(node, &degree)| (degree == 0).then_some(node))
1615        .collect::<Vec<_>>();
1616    let mut acyclic_nodes = 0usize;
1617    while let Some(node) = ready.pop() {
1618        acyclic_nodes += 1;
1619        for &dependent in &outgoing[node] {
1620            remaining_indegree[dependent] -= 1;
1621            if remaining_indegree[dependent] == 0 {
1622                ready.push(dependent);
1623            }
1624        }
1625    }
1626    if acyclic_nodes != node_count {
1627        return Err(LinearGraphChainError::Cycle);
1628    }
1629
1630    let roots = immediate_dependencies
1631        .iter()
1632        .enumerate()
1633        .filter_map(|(node, dependencies)| dependencies.is_empty().then_some(node))
1634        .collect::<Vec<_>>();
1635    if roots.len() != 1 {
1636        return Err(LinearGraphChainError::RootCount { found: roots.len() });
1637    }
1638    let root = roots[0];
1639    for (node, dependencies) in immediate_dependencies.iter().enumerate() {
1640        if node != root && dependencies.len() != 1 {
1641            return Err(LinearGraphChainError::IncomingDegree {
1642                node,
1643                dependencies: dependencies.len(),
1644            });
1645        }
1646    }
1647    for (node, dependents) in outgoing.iter().enumerate() {
1648        if dependents.len() > 1 {
1649            return Err(LinearGraphChainError::Branch {
1650                node,
1651                dependents: dependents.len(),
1652            });
1653        }
1654    }
1655    let leaf_count = outgoing
1656        .iter()
1657        .filter(|dependents| dependents.is_empty())
1658        .count();
1659    if leaf_count != 1 {
1660        return Err(LinearGraphChainError::LeafCount { found: leaf_count });
1661    }
1662
1663    let mut order = Vec::with_capacity(node_count);
1664    let mut visited = vec![false; node_count];
1665    let mut current = Some(root);
1666    while let Some(node) = current {
1667        if visited[node] {
1668            return Err(LinearGraphChainError::Cycle);
1669        }
1670        visited[node] = true;
1671        order.push(node);
1672        current = outgoing[node].first().copied();
1673    }
1674    if order.len() != node_count {
1675        return Err(LinearGraphChainError::Disconnected {
1676            visited: order.len(),
1677            total: node_count,
1678        });
1679    }
1680    Ok(order)
1681}
1682
1683fn raw_node_dependencies_with<E>(
1684    node: sys::CUgraphNode,
1685    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
1686) -> std::result::Result<Vec<sys::CUgraphNode>, E> {
1687    let mut dependency_count = 0usize;
1688    unsafe {
1689        check(
1690            "cuGraphNodeGetDependencies(count)",
1691            sys::cuGraphNodeGetDependencies(node, ptr::null_mut(), &mut dependency_count),
1692        )?;
1693    }
1694    let mut dependencies = vec![ptr::null_mut(); dependency_count];
1695    if dependency_count != 0 {
1696        unsafe {
1697            check(
1698                "cuGraphNodeGetDependencies(nodes)",
1699                sys::cuGraphNodeGetDependencies(
1700                    node,
1701                    dependencies.as_mut_ptr(),
1702                    &mut dependency_count,
1703                ),
1704            )?;
1705        }
1706        dependencies.truncate(dependency_count);
1707    }
1708    Ok(dependencies)
1709}
1710
1711fn graph_nodes_with<E>(
1712    graph: sys::CUgraph,
1713    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
1714) -> std::result::Result<Vec<CudaGraphNode>, E> {
1715    let raw_nodes = raw_graph_nodes_with(graph, check)?;
1716    let mut nodes = Vec::with_capacity(raw_nodes.len());
1717    for (index, raw) in raw_nodes.into_iter().enumerate() {
1718        let mut ty = sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EMPTY;
1719        unsafe {
1720            check("cuGraphNodeGetType", sys::cuGraphNodeGetType(raw, &mut ty))?;
1721        }
1722        nodes.push(CudaGraphNode {
1723            index,
1724            raw,
1725            kind: CudaGraphNodeKind::from_sys(ty),
1726        });
1727    }
1728    Ok(nodes)
1729}
1730
1731fn graph_linear_chain_node_kinds_with<E>(
1732    graph: sys::CUgraph,
1733    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
1734    shape_error: &mut impl FnMut(LinearGraphChainError) -> E,
1735) -> std::result::Result<Vec<CudaGraphNodeKind>, E> {
1736    let nodes = graph_nodes_with(graph, check)?;
1737    let mut immediate_dependencies = Vec::with_capacity(nodes.len());
1738    for (node_index, node) in nodes.iter().enumerate() {
1739        let raw_dependencies = raw_node_dependencies_with(node.raw, check)?;
1740        let mut dependency_indices = Vec::with_capacity(raw_dependencies.len());
1741        for dependency in raw_dependencies {
1742            let Some(dependency_index) = nodes
1743                .iter()
1744                .position(|candidate| candidate.raw == dependency)
1745            else {
1746                return Err(shape_error(LinearGraphChainError::ForeignDependency {
1747                    node: node_index,
1748                    dependency: nodes.len(),
1749                }));
1750            };
1751            dependency_indices.push(dependency_index);
1752        }
1753        immediate_dependencies.push(dependency_indices);
1754    }
1755    let order = linear_chain_order(&immediate_dependencies).map_err(shape_error)?;
1756    Ok(order.into_iter().map(|index| nodes[index].kind).collect())
1757}
1758
1759fn graph_leaf_nodes(
1760    graph: sys::CUgraph,
1761) -> std::result::Result<Vec<sys::CUgraphNode>, CudaConditionalGraphUnavailable> {
1762    let mut check = |_, code| conditional_driver_call("cuGraphGetNodes", code);
1763    let mut nodes = raw_graph_nodes_with(graph, &mut check)?;
1764
1765    let mut edge_count = 0usize;
1766    unsafe {
1767        conditional_driver_call(
1768            "cuGraphGetEdges",
1769            sys::cuGraphGetEdges(graph, ptr::null_mut(), ptr::null_mut(), &mut edge_count),
1770        )?;
1771    }
1772    let mut from = vec![ptr::null_mut(); edge_count];
1773    let mut to = vec![ptr::null_mut(); edge_count];
1774    if edge_count != 0 {
1775        unsafe {
1776            conditional_driver_call(
1777                "cuGraphGetEdges",
1778                sys::cuGraphGetEdges(graph, from.as_mut_ptr(), to.as_mut_ptr(), &mut edge_count),
1779            )?;
1780        }
1781        from.truncate(edge_count);
1782    }
1783    nodes.retain(|node| !from.contains(node));
1784    Ok(nodes)
1785}
1786
1787/// Instantiated CUDA Graph with owned graph + exec handles.
1788///
1789/// Destruction may wait for recorded execution events and their stream-prefix
1790/// dependencies, directly or when another capture drains deferred destruction.
1791/// Satisfy the [module-level lifecycle contract](crate::cuda_graph) before release
1792/// or capture. This waiting behavior belongs to the wrapper, not GraphExecDestroy.
1793/// Modules captured through XLOG launches remain alive until replay completes.
1794pub struct CapturedCudaGraph {
1795    graph: sys::CUgraph,
1796    exec: sys::CUgraphExec,
1797    context: Arc<CudaContext>,
1798    _conditional_api: Option<Arc<ConditionalGraphDriverApi>>,
1799    _resident_lifecycle_lease: Option<Box<dyn Send + Sync>>,
1800    modules: CaptureOwners,
1801    execution: Mutex<GraphExecution>,
1802}
1803
1804impl CapturedCudaGraph {
1805    #[cfg(feature = "semantic-policy")]
1806    pub(crate) fn captured_launch_bindings(&self) -> Vec<CapturedCudaLaunchBinding> {
1807        self.modules
1808            .lock()
1809            .unwrap_or_else(|poisoned| poisoned.into_inner())
1810            .launches
1811            .clone()
1812    }
1813}
1814
1815struct GraphExecution {
1816    completion: ExecutionCompletion,
1817    binding: GraphMemoryBinding,
1818}
1819
1820#[derive(Default)]
1821struct GraphMemoryBinding {
1822    memory: Arc<crate::memory::MemoryAccessManifest>,
1823    unusable: bool,
1824}
1825
1826/// Preserve both generations before the first potentially partial driver update.
1827/// Failure and unwind leave the executable unusable and both sets of owners live.
1828fn replace_graph_memory<E>(
1829    binding: &mut GraphMemoryBinding,
1830    candidate: Arc<crate::memory::MemoryAccessManifest>,
1831    wait: impl FnOnce() -> std::result::Result<(), E>,
1832    update: impl FnOnce() -> std::result::Result<(), E>,
1833) -> std::result::Result<Arc<crate::memory::MemoryAccessManifest>, E> {
1834    wait()?;
1835    binding.memory =
1836        crate::memory::MemoryAccessManifest::combine(&[binding.memory.clone(), candidate.clone()]);
1837    binding.unusable = true;
1838    update()?;
1839    let obsolete = mem::replace(&mut binding.memory, candidate);
1840    binding.unusable = false;
1841    Ok(obsolete)
1842}
1843
1844pub(crate) struct KernelNodeUpdate<'a> {
1845    pub(crate) node: CudaGraphNode,
1846    pub(crate) params: &'a sys::CUDA_KERNEL_NODE_PARAMS,
1847}
1848
1849// CUDA graph handles are context-owned driver handles. xlog stores them behind
1850// provider-level synchronization when caching graph executions.
1851unsafe impl Send for CapturedCudaGraph {}
1852unsafe impl Sync for CapturedCudaGraph {}
1853
1854#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1855pub enum CudaGraphNodeKind {
1856    Kernel,
1857    Memcpy,
1858    Memset,
1859    Host,
1860    Graph,
1861    Empty,
1862    WaitEvent,
1863    EventRecord,
1864    ExternalSemaphoresSignal,
1865    ExternalSemaphoresWait,
1866    MemAlloc,
1867    MemFree,
1868    BatchMemOp,
1869    Conditional,
1870}
1871
1872#[derive(Debug, Clone, Copy)]
1873pub struct CudaGraphNode {
1874    pub index: usize,
1875    pub raw: sys::CUgraphNode,
1876    pub kind: CudaGraphNodeKind,
1877}
1878
1879unsafe impl Send for CudaGraphNode {}
1880unsafe impl Sync for CudaGraphNode {}
1881
1882#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1883pub enum CsmCudaGraphJoinKind {
1884    Inner,
1885    IndexedInner,
1886}
1887
1888#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1889pub struct ScanTopology {
1890    pub input_len: u32,
1891    pub block_size: u32,
1892    pub scratch_lengths: Vec<u32>,
1893    pub kernel_node_count: usize,
1894}
1895
1896#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1897pub struct CsmCudaGraphKey {
1898    pub join_kind: CsmCudaGraphJoinKind,
1899    pub key_arity: u8,
1900    pub key_bytes: u32,
1901    pub probe_capacity_class: u32,
1902    pub output_capacity_class: u32,
1903    pub scan_topology: ScanTopology,
1904    pub node_layout_version: u32,
1905}
1906
1907impl CsmCudaGraphKey {
1908    pub fn inner(
1909        key_arity: usize,
1910        key_bytes: u32,
1911        probe_capacity: u32,
1912        output_capacity: u32,
1913    ) -> Result<Self> {
1914        let key_arity = u8::try_from(key_arity).map_err(|_| {
1915            XlogError::Kernel(format!(
1916                "CSM CUDA Graph key arity {} exceeds u8::MAX",
1917                key_arity
1918            ))
1919        })?;
1920        Ok(Self {
1921            join_kind: CsmCudaGraphJoinKind::Inner,
1922            key_arity,
1923            key_bytes,
1924            probe_capacity_class: graph_capacity_class_u32(probe_capacity),
1925            output_capacity_class: graph_capacity_class_u32(output_capacity),
1926            scan_topology: scan_topology_u32(probe_capacity),
1927            node_layout_version: CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION,
1928        })
1929    }
1930}
1931
1932pub fn graph_capacity_class_u32(n: u32) -> u32 {
1933    if n <= 1 {
1934        1
1935    } else {
1936        n.checked_next_power_of_two().unwrap_or(u32::MAX)
1937    }
1938}
1939
1940pub fn scan_topology_u32(mut n: u32) -> ScanTopology {
1941    let input_len = n;
1942    let block_size = 256u32;
1943    let mut scratch_lengths = Vec::new();
1944    let mut kernel_node_count = if n == 0 { 0 } else { 1 };
1945    while n > block_size {
1946        let num_blocks = n.div_ceil(block_size);
1947        scratch_lengths.push(num_blocks);
1948        kernel_node_count += 2;
1949        n = num_blocks;
1950    }
1951    ScanTopology {
1952        input_len,
1953        block_size,
1954        scratch_lengths,
1955        kernel_node_count,
1956    }
1957}
1958
1959impl CapturedCudaGraph {
1960    /// Rebind all changed kernel nodes under one execution lock and one admitted
1961    /// candidate manifest. A partial driver update requires rebuilding the graph.
1962    ///
1963    /// # Safety
1964    /// Each argument array must match the original kernel ABI. The candidate
1965    /// admission must cover every allocation referenced by the resulting graph,
1966    /// including unchanged nodes and hidden pointers in by-value descriptors.
1967    pub(crate) unsafe fn rebind_kernel_nodes_in(
1968        &mut self,
1969        enqueue: &crate::launch::CudaEnqueue<'_>,
1970        updates: &[KernelNodeUpdate<'_>],
1971    ) -> Result<()> {
1972        enqueue
1973            .submit(|owners| {
1974                if owners.is_some() {
1975                    Err(cudarc::driver::DriverError(
1976                        sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
1977                    ))
1978                } else {
1979                    Ok(())
1980                }
1981            })
1982            .map_err(|error| {
1983                XlogError::Kernel(format!(
1984                    "graph rebinding requires an ordinary admitted stream: {error}"
1985                ))
1986            })?;
1987        self.context
1988            .bind_to_thread()
1989            .map_err(|error| XlogError::Kernel(error.to_string()))?;
1990        let mut execution = self
1991            .execution
1992            .lock()
1993            .unwrap_or_else(|error| error.into_inner());
1994        self.require_replay(enqueue.stream(), &execution)?;
1995        let nodes = self.nodes_unlocked()?;
1996        let mut seen = std::collections::BTreeSet::new();
1997        for update in updates {
1998            if update.node.kind != CudaGraphNodeKind::Kernel
1999                || !nodes.iter().any(|node| {
2000                    node.raw == update.node.raw && node.kind == CudaGraphNodeKind::Kernel
2001                })
2002                || !seen.insert(update.node.raw as usize)
2003                || self.kernel_node_params_unlocked(update.node)?.func != update.params.func
2004            {
2005                return Err(XlogError::Kernel("graph rebinding requires unique owned kernel nodes with their original functions".into()));
2006            }
2007        }
2008        let GraphExecution {
2009            completion,
2010            binding,
2011        } = &mut *execution;
2012        let obsolete = replace_graph_memory(
2013            binding,
2014            enqueue.manifest().clone(),
2015            || {
2016                completion.wait().map_err(|error| {
2017                    XlogError::Kernel(format!("graph rebinding prior completion failed: {error}"))
2018                })
2019            },
2020            || {
2021                for update in updates {
2022                    cuda_graph_check(
2023                        "cuGraphExecKernelNodeSetParams_v2",
2024                        sys::cuGraphExecKernelNodeSetParams_v2(
2025                            self.exec,
2026                            update.node.raw,
2027                            update.params,
2028                        ),
2029                    )?;
2030                    // Nested capture clones the graph template, not its exec.
2031                    // Keep both representations on the same admitted generation.
2032                    cuda_graph_check(
2033                        "cuGraphKernelNodeSetParams_v2",
2034                        sys::cuGraphKernelNodeSetParams_v2(update.node.raw, update.params),
2035                    )?;
2036                }
2037                Ok(())
2038            },
2039        )?;
2040        drop(execution);
2041        drop(obsolete);
2042        Ok(())
2043    }
2044
2045    /// Tie resident lifecycle accounting to this real graph/exec owner.
2046    ///
2047    /// Binding is idempotent. The lease is created only after graph
2048    /// instantiation has succeeded and is dropped after this type's `Drop`
2049    /// implementation destroys the executable and parent graph handles.
2050    pub fn bind_resident_lifecycle(mut self, runtime: &XlogDeviceRuntime) -> Self {
2051        if self._resident_lifecycle_lease.is_none() {
2052            self._resident_lifecycle_lease = Some(Box::new(runtime.resident_graph_handle_lease()));
2053        }
2054        self
2055    }
2056
2057    /// Create, populate, and instantiate a parent graph containing exactly one
2058    /// root conditional-WHILE node.
2059    ///
2060    /// The body is borrowed only during construction; it cannot escape the
2061    /// callback or modify an instantiated graph. Its numeric conditional handle
2062    /// remains valid until this graph is dropped. CUDA permits only one live
2063    /// executable instantiation of a graph containing a conditional node.
2064    pub fn conditional_while_on_stream<F>(
2065        stream: &CudaStream,
2066        initial_value: u32,
2067        assign_default_on_launch: bool,
2068        populate_body: F,
2069    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable>
2070    where
2071        F: FnOnce(
2072            &ConditionalCudaGraphBody,
2073        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
2074    {
2075        let api = ConditionalGraphDriverApi::load()?;
2076        let context = stream.context().clone();
2077        let raw_context =
2078            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
2079        if raw_context != context.cu_ctx() {
2080            return Err(CudaConditionalGraphUnavailable::ContextMismatch);
2081        }
2082
2083        let graph = UninstantiatedCudaGraph::create(Arc::clone(&context))?;
2084        let mut handle = 0;
2085        let flags = if assign_default_on_launch {
2086            sys::CU_GRAPH_COND_ASSIGN_DEFAULT
2087        } else {
2088            0
2089        };
2090        unsafe {
2091            conditional_driver_call(
2092                "cuGraphConditionalHandleCreate",
2093                (api.conditional_handle_create)(
2094                    &mut handle,
2095                    graph.raw(),
2096                    raw_context,
2097                    initial_value,
2098                    flags,
2099                ),
2100            )?;
2101        }
2102
2103        let mut params = conditional_node_params(
2104            handle,
2105            raw_context,
2106            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE,
2107        );
2108        let mut conditional_node = ptr::null_mut();
2109        unsafe {
2110            conditional_driver_call(
2111                "cuGraphAddNode",
2112                (api.graph_add_node)(
2113                    &mut conditional_node,
2114                    graph.raw(),
2115                    ptr::null(),
2116                    0,
2117                    &mut params,
2118                ),
2119            )?;
2120        }
2121        if conditional_node.is_null() {
2122            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
2123                operation: "cuGraphAddNode",
2124            });
2125        }
2126
2127        let conditional = unsafe { params.__bindgen_anon_1.conditional };
2128        if conditional.phGraph_out.is_null() {
2129            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
2130                operation: "cuGraphAddNode body array",
2131            });
2132        }
2133        let body_graph = unsafe { *conditional.phGraph_out };
2134        if body_graph.is_null() {
2135            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
2136                operation: "cuGraphAddNode WHILE body",
2137            });
2138        }
2139        populate_body(&ConditionalCudaGraphBody {
2140            graph: body_graph,
2141            handle,
2142            context: raw_context,
2143            modules: graph.modules.clone(),
2144        })?;
2145
2146        let mut instantiated = Self::instantiate_graph(graph)?;
2147        instantiated._conditional_api = Some(api);
2148        Ok(instantiated)
2149    }
2150
2151    /// Instantiate and assume sole ownership of `graph`.
2152    ///
2153    /// The graph is destroyed on instantiation failure. On success, this value
2154    /// destroys the executable first and the source graph second.
2155    ///
2156    /// # Safety
2157    /// `graph` must be a valid, unowned graph in `context`, and no other owner
2158    /// may destroy or instantiate it while this value exists.
2159    /// The caller must retain modules and allocations referenced by this foreign
2160    /// graph through its last execution. Ownership of raw CUDA nodes does not
2161    /// transfer ownership of their external resources.
2162    pub unsafe fn instantiate_owned_graph(
2163        graph: sys::CUgraph,
2164        context: Arc<CudaContext>,
2165    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
2166        if graph.is_null() {
2167            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
2168                operation: "instantiate_owned_graph",
2169            });
2170        }
2171        Self::instantiate_graph(UninstantiatedCudaGraph {
2172            raw: graph,
2173            context: Arc::clone(&context),
2174            modules: Arc::default(),
2175        })
2176    }
2177
2178    fn instantiate_graph(
2179        owned_graph: UninstantiatedCudaGraph,
2180    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
2181        if owned_graph
2182            .modules
2183            .lock()
2184            .unwrap_or_else(|error| error.into_inner())
2185            .failed
2186        {
2187            return Err(CudaConditionalGraphUnavailable::body_population(
2188                "a captured submission failed or unwound; rebuild the graph before instantiation",
2189            ));
2190        }
2191        let context = owned_graph.context.clone();
2192        context
2193            .bind_to_thread()
2194            .map_err(CudaConditionalGraphUnavailable::body_population)?;
2195        let completion = ExecutionCompletion::new(&context)
2196            .map_err(CudaConditionalGraphUnavailable::body_population)?;
2197        let mut exec = ptr::null_mut();
2198        conditional_driver_call(
2199            "cuGraphInstantiateWithFlags",
2200            // SAFETY: the graph is owned, uninstantiated, and its context is bound.
2201            unsafe { sys::cuGraphInstantiateWithFlags(&mut exec, owned_graph.raw(), 0) },
2202        )?;
2203        if exec.is_null() {
2204            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
2205                operation: "cuGraphInstantiateWithFlags",
2206            });
2207        }
2208        let modules = owned_graph.modules.clone();
2209        let memory = {
2210            let mut owners = modules.lock().unwrap_or_else(|error| error.into_inner());
2211            crate::memory::MemoryAccessManifest::combine(&mem::take(&mut owners.memory))
2212        };
2213        Ok(Self {
2214            graph: owned_graph.into_raw(),
2215            exec,
2216            context,
2217            _conditional_api: None,
2218            _resident_lifecycle_lease: None,
2219            modules,
2220            execution: Mutex::new(GraphExecution {
2221                completion,
2222                binding: GraphMemoryBinding {
2223                    memory,
2224                    unusable: false,
2225                },
2226            }),
2227        })
2228    }
2229
2230    /// Capture work submitted by `record` on `stream`, instantiate it, and take
2231    /// ownership of the resulting graph handles.
2232    ///
2233    /// Capture setup and cleanup may wait for deferred destruction under the
2234    /// [module-level lifecycle contract](crate::cuda_graph), including on errors
2235    /// and callback unwind. The resulting graph has not executed at that point.
2236    pub fn capture_on_stream<F>(stream: &CudaStream, record: F) -> Result<Self>
2237    where
2238        F: FnOnce() -> Result<()>,
2239    {
2240        Self::capture_with_owners(stream, Arc::default(), record)
2241    }
2242
2243    /// Keep the actual stream and preallocated producer resources in the same
2244    /// graph ownership chain before the first capture operation. Unknown capture
2245    /// or execution completion retains them through the ordinary retirement path.
2246    /// Resources allocated by `record` must already be retained by these owners
2247    /// before being submitted; this does not validate foreign pointer aliases.
2248    pub(crate) fn capture_on_stream_retaining<F>(
2249        stream: &Arc<CudaStream>,
2250        mut external_resources: Vec<Arc<dyn Send + Sync>>,
2251        record: F,
2252    ) -> Result<Self>
2253    where
2254        F: FnOnce() -> Result<()>,
2255    {
2256        external_resources.push(stream.clone());
2257        let owners = Arc::new(Mutex::new(CapturedOwners::with_external_resources(
2258            external_resources,
2259        )));
2260        Self::capture_with_owners(stream, owners, record)
2261    }
2262
2263    fn capture_with_owners<F>(
2264        stream: &CudaStream,
2265        modules: CaptureOwners,
2266        record: F,
2267    ) -> Result<Self>
2268    where
2269        F: FnOnce() -> Result<()>,
2270    {
2271        let mut capture = DriverStreamCapture::prepare(stream, true, modules.clone())
2272            .map_err(|error| XlogError::Kernel(error.decline_detail()))?;
2273        unsafe {
2274            capture
2275                .begin("cuStreamBeginCapture_v2", || {
2276                    sys::cuStreamBeginCapture_v2(
2277                        stream.cu_stream(),
2278                        sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
2279                    )
2280                })
2281                .map_err(|error| XlogError::Kernel(error.decline_detail()))?;
2282        }
2283
2284        let record_result = capture_outcome(Some(&capture.lease.modules), record);
2285        let end_result = capture.finish();
2286
2287        if let Err(record_err) = record_result {
2288            if let Ok(graph) = end_result {
2289                if !graph.is_null() {
2290                    drop(UninstantiatedCudaGraph {
2291                        raw: graph,
2292                        context: stream.context().clone(),
2293                        modules: modules.clone(),
2294                    });
2295                }
2296            }
2297            return Err(record_err);
2298        }
2299        let graph = end_result.map_err(|error| XlogError::Kernel(error.decline_detail()))?;
2300        if graph.is_null() {
2301            return Err(XlogError::Kernel(
2302                "cuStreamEndCapture returned a null CUDA graph".to_string(),
2303            ));
2304        }
2305
2306        // SAFETY: EndCapture returned this new graph and ownership has not been
2307        // exposed. The canonical owner handles all instantiation error paths.
2308        Self::instantiate_graph(UninstantiatedCudaGraph {
2309            raw: graph,
2310            context: stream.context().clone(),
2311            modules,
2312        })
2313        .map_err(|error| XlogError::Kernel(error.decline_detail()))
2314    }
2315
2316    /// Replay after admitting the complete current memory manifest on this stream.
2317    /// Submission is asynchronous; dropping the graph may wait for completion.
2318    pub fn launch(&self, stream: &Arc<CudaStream>) -> Result<()> {
2319        let memory = {
2320            let execution = self
2321                .execution
2322                .lock()
2323                .unwrap_or_else(|error| error.into_inner());
2324            self.require_replay(stream, &execution)?;
2325            execution.binding.memory.clone()
2326        };
2327        crate::memory::with_memory_manifest(stream.clone(), memory.clone(), |enqueue| {
2328            let mut execution = self
2329                .execution
2330                .lock()
2331                .unwrap_or_else(|error| error.into_inner());
2332            self.require_replay(stream, &execution)
2333                .map_err(|error| crate::device_runtime::ResourceError::Driver(error.to_string()))?;
2334            if !Arc::ptr_eq(&memory, &execution.binding.memory) {
2335                return Err(crate::device_runtime::ResourceError::StreamMisuse(
2336                    "graph bindings changed during memory admission".into(),
2337                ));
2338            }
2339            self.submit_graph(&mut execution, enqueue)
2340                .map_err(|error| crate::device_runtime::ResourceError::Driver(error.to_string()))
2341        })
2342        .map_err(|error| {
2343            XlogError::Kernel(format!(
2344                "CUDA graph memory admission/replay failed: {error}"
2345            ))
2346        })
2347    }
2348
2349    /// Materialize graph-owned device resources before the first replay without
2350    /// executing any node. A failed or uncertain upload keeps the graph owner
2351    /// behind its completion fence instead of releasing captured allocations.
2352    pub(crate) fn upload_cold(&self, stream: &Arc<CudaStream>) -> Result<()> {
2353        let _ordinary = reserve_uncaptured_stream(stream).map_err(|error| {
2354            XlogError::Kernel(format!("CUDA graph upload admission failed: {error}"))
2355        })?;
2356        let mut execution = self
2357            .execution
2358            .lock()
2359            .unwrap_or_else(|error| error.into_inner());
2360        self.require_replay(stream, &execution)?;
2361        self.context.bind_to_thread().map_err(|error| {
2362            XlogError::Kernel(format!("CUDA graph upload context failed: {error}"))
2363        })?;
2364        execution
2365            .completion
2366            .submit(stream, None, || {
2367                // SAFETY: the executable, context, and original stream remain
2368                // owned for the completion fence's entire lifetime.
2369                unsafe { sys::cuGraphUpload(self.exec, stream.cu_stream()).result() }
2370            })
2371            .map_err(|error| XlogError::Kernel(format!("CUDA graph upload failed: {error}")))?;
2372        execution.completion.wait().map_err(|error| {
2373            XlogError::Kernel(format!("CUDA graph upload completion failed: {error}"))
2374        })
2375    }
2376
2377    /// Replay inside an existing admission without acquiring an overlapping group
2378    /// or a new capture pin.
2379    pub fn launch_in(&self, enqueue: &crate::launch::CudaEnqueue<'_>) -> Result<()> {
2380        let mut execution = self
2381            .execution
2382            .lock()
2383            .unwrap_or_else(|error| error.into_inner());
2384        self.require_replay(enqueue.stream(), &execution)?;
2385        if !execution.binding.memory.covered_by(enqueue.manifest()) {
2386            return Err(XlogError::Kernel(
2387                "CUDA graph replay lacks admission for its complete memory manifest".into(),
2388            ));
2389        }
2390        self.submit_graph(&mut execution, enqueue)
2391            .map_err(|error| XlogError::Kernel(format!("CUDA graph submission failed: {error}")))
2392    }
2393
2394    fn require_replay(&self, stream: &CudaStream, execution: &GraphExecution) -> Result<()> {
2395        if execution.binding.unusable {
2396            return Err(XlogError::Kernel(
2397                "CUDA graph has an incomplete parameter update; rebuild it before replay".into(),
2398            ));
2399        }
2400        if stream.context().cu_ctx() != self.context.cu_ctx() {
2401            return Err(XlogError::Kernel(
2402                CudaConditionalGraphUnavailable::ContextMismatch.decline_detail(),
2403            ));
2404        }
2405        Ok(())
2406    }
2407
2408    fn submit_graph(
2409        &self,
2410        execution: &mut GraphExecution,
2411        enqueue: &crate::launch::CudaEnqueue<'_>,
2412    ) -> std::result::Result<(), cudarc::driver::DriverError> {
2413        self.context.bind_to_thread()?;
2414        enqueue.submit(|owners| {
2415            let launch = || unsafe { sys::cuGraphLaunch(self.exec, enqueue.stream().cu_stream()).result() };
2416            if let Some(owners) = owners {
2417                // AddChildGraphNode clones the updated template. Freeze this
2418                // generation's owners alongside that clone in the parent.
2419                retain_child_capture_owners(owners, &self.modules, &execution.binding.memory);
2420                unsafe {
2421                    let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE;
2422                    let mut id = 0;
2423                    let mut graph = ptr::null_mut();
2424                    let mut dependencies = ptr::null();
2425                    let mut count = 0;
2426                    sys::cuStreamGetCaptureInfo_v2(enqueue.stream().cu_stream(), &mut status, &mut id, &mut graph, &mut dependencies, &mut count).result()?;
2427                    if status != sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE || graph.is_null() {
2428                        return Err(cudarc::driver::DriverError(sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_INVALIDATED));
2429                    }
2430                    let mut child = ptr::null_mut();
2431                    sys::cuGraphAddChildGraphNode(&mut child, graph, dependencies, count, self.graph).result()?;
2432                    sys::cuStreamUpdateCaptureDependencies(enqueue.stream().cu_stream(), &mut child, 1, sys::CUstreamUpdateCaptureDependencies_flags::CU_STREAM_SET_CAPTURE_DEPENDENCIES as u32).result()
2433                }
2434            } else {
2435                execution.completion.submit(enqueue.stream(), Some(enqueue.completion()), launch)
2436            }
2437        })
2438    }
2439
2440    /// Number of nodes in the captured graph. Used by bounded CSM CUDA Graph
2441    /// cache-key and node-inventory certs to prove topology stability.
2442    pub fn node_count(&self) -> Result<usize> {
2443        let _execution = self
2444            .execution
2445            .lock()
2446            .unwrap_or_else(|error| error.into_inner());
2447        self.context
2448            .bind_to_thread()
2449            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2450        let mut count = 0usize;
2451        unsafe {
2452            cuda_graph_check(
2453                "cuGraphGetNodes(count)",
2454                sys::cuGraphGetNodes(self.graph, ptr::null_mut(), &mut count),
2455            )?;
2456        }
2457        Ok(count)
2458    }
2459
2460    /// Return graph nodes in CUDA's enumeration order with their node type.
2461    ///
2462    /// CUDA does not define this list as dependency or execution order. Use
2463    /// [`Self::linear_chain_node_kinds`] when a linear topology is required.
2464    pub fn nodes(&self) -> Result<Vec<CudaGraphNode>> {
2465        let _execution = self
2466            .execution
2467            .lock()
2468            .unwrap_or_else(|error| error.into_inner());
2469        self.context
2470            .bind_to_thread()
2471            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2472        self.nodes_unlocked()
2473    }
2474
2475    fn nodes_unlocked(&self) -> Result<Vec<CudaGraphNode>> {
2476        let mut check = |operation, code| cuda_graph_check(operation, code);
2477        graph_nodes_with(self.graph, &mut check)
2478    }
2479
2480    /// Return actual node kinds in root-to-leaf dependency order.
2481    ///
2482    /// This fails unless the graph is one connected linear chain with exactly
2483    /// one root, one leaf, one immediate dependency per non-root node, and no
2484    /// branches, duplicate dependencies, foreign dependencies, or cycles.
2485    pub fn linear_chain_node_kinds(&self) -> Result<Vec<CudaGraphNodeKind>> {
2486        let _execution = self
2487            .execution
2488            .lock()
2489            .unwrap_or_else(|error| error.into_inner());
2490        self.context
2491            .bind_to_thread()
2492            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2493        let mut check = |operation, code| cuda_graph_check(operation, code);
2494        let mut shape_error = |error| {
2495            XlogError::Kernel(format!(
2496                "CUDA graph is not a linear dependency chain: {error}"
2497            ))
2498        };
2499        graph_linear_chain_node_kinds_with(self.graph, &mut check, &mut shape_error)
2500    }
2501
2502    /// Read CUDA's raw kernel-node params for inventory/update code.
2503    ///
2504    /// The returned `kernelParams` pointer is CUDA-owned capture metadata. Treat
2505    /// it as read-only unless constructing a fresh params object for
2506    /// a complete admitted parameter update.
2507    pub fn kernel_node_params(&self, node: CudaGraphNode) -> Result<sys::CUDA_KERNEL_NODE_PARAMS> {
2508        let _execution = self
2509            .execution
2510            .lock()
2511            .unwrap_or_else(|error| error.into_inner());
2512        self.context
2513            .bind_to_thread()
2514            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2515        if !self
2516            .nodes_unlocked()?
2517            .iter()
2518            .any(|owned| owned.raw == node.raw && owned.kind == node.kind)
2519        {
2520            return Err(XlogError::Kernel(
2521                "kernel metadata requires a node owned by this graph".into(),
2522            ));
2523        }
2524        self.kernel_node_params_unlocked(node)
2525    }
2526
2527    fn kernel_node_params_unlocked(
2528        &self,
2529        node: CudaGraphNode,
2530    ) -> Result<sys::CUDA_KERNEL_NODE_PARAMS> {
2531        if node.kind != CudaGraphNodeKind::Kernel {
2532            return Err(XlogError::Kernel(format!(
2533                "kernel_node_params called for non-kernel graph node {:?}",
2534                node.kind
2535            )));
2536        }
2537        let mut params: sys::CUDA_KERNEL_NODE_PARAMS = unsafe { mem::zeroed() };
2538        unsafe {
2539            cuda_graph_check(
2540                "cuGraphKernelNodeGetParams_v2",
2541                sys::cuGraphKernelNodeGetParams_v2(node.raw, &mut params),
2542            )?;
2543        }
2544        Ok(params)
2545    }
2546
2547    /// Read CUDA's raw memset-node params for inventory/update code.
2548    pub fn memset_node_params(&self, node: CudaGraphNode) -> Result<sys::CUDA_MEMSET_NODE_PARAMS> {
2549        let _execution = self
2550            .execution
2551            .lock()
2552            .unwrap_or_else(|error| error.into_inner());
2553        self.context
2554            .bind_to_thread()
2555            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2556        if !self
2557            .nodes_unlocked()?
2558            .iter()
2559            .any(|owned| owned.raw == node.raw && owned.kind == node.kind)
2560        {
2561            return Err(XlogError::Kernel(
2562                "memset metadata requires a node owned by this graph".into(),
2563            ));
2564        }
2565        if node.kind != CudaGraphNodeKind::Memset {
2566            return Err(XlogError::Kernel(format!(
2567                "memset_node_params called for non-memset graph node {:?}",
2568                node.kind
2569            )));
2570        }
2571        let mut params: sys::CUDA_MEMSET_NODE_PARAMS = unsafe { mem::zeroed() };
2572        unsafe {
2573            cuda_graph_check(
2574                "cuGraphMemsetNodeGetParams",
2575                sys::cuGraphMemsetNodeGetParams(node.raw, &mut params),
2576            )?;
2577        }
2578        Ok(params)
2579    }
2580
2581    /// Update one memset node using a complete admitted candidate manifest.
2582    ///
2583    /// # Safety
2584    /// The candidate admission must cover all unchanged graph accesses as well
2585    /// as the new destination. Parameters must obey CUDA's memset-node ABI.
2586    pub unsafe fn set_memset_node_params_in(
2587        &mut self,
2588        node: CudaGraphNode,
2589        params: &sys::CUDA_MEMSET_NODE_PARAMS,
2590        enqueue: &crate::launch::CudaEnqueue<'_>,
2591    ) -> Result<()> {
2592        enqueue
2593            .submit(|owners| {
2594                if owners.is_some() {
2595                    Err(cudarc::driver::DriverError(
2596                        sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED,
2597                    ))
2598                } else {
2599                    Ok(())
2600                }
2601            })
2602            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2603        self.context
2604            .bind_to_thread()
2605            .map_err(|error| XlogError::Kernel(error.to_string()))?;
2606        let mut execution = self
2607            .execution
2608            .lock()
2609            .unwrap_or_else(|error| error.into_inner());
2610        self.require_replay(enqueue.stream(), &execution)?;
2611        if node.kind != CudaGraphNodeKind::Memset
2612            || !self
2613                .nodes_unlocked()?
2614                .iter()
2615                .any(|owned| owned.raw == node.raw && owned.kind == node.kind)
2616        {
2617            return Err(XlogError::Kernel(
2618                "memset update requires an owned memset node".into(),
2619            ));
2620        }
2621        let bytes = params
2622            .width
2623            .checked_mul(params.elementSize as usize)
2624            .and_then(|row| {
2625                params
2626                    .height
2627                    .saturating_sub(1)
2628                    .checked_mul(params.pitch)
2629                    .and_then(|offset| offset.checked_add(row))
2630            })
2631            .ok_or_else(|| XlogError::Kernel("memset destination span overflow".into()))?;
2632        let context = enqueue.stream().context().cu_ctx() as usize;
2633        let range = crate::device_runtime::resource::MemoryUse::new(
2634            params.dst,
2635            bytes,
2636            crate::device_runtime::Access::Write,
2637        )
2638        .map_err(|error| XlogError::Kernel(error.to_string()))?;
2639        if !enqueue.manifest().covers(&[(context, range)]) {
2640            return Err(XlogError::Kernel(
2641                "memset update destination lacks an admitted write".into(),
2642            ));
2643        }
2644        let GraphExecution {
2645            completion,
2646            binding,
2647        } = &mut *execution;
2648        let obsolete = replace_graph_memory(
2649            binding,
2650            enqueue.manifest().clone(),
2651            || {
2652                completion
2653                    .wait()
2654                    .map_err(|error| XlogError::Kernel(error.to_string()))
2655            },
2656            || {
2657                cuda_graph_check(
2658                    "cuGraphExecMemsetNodeSetParams",
2659                    sys::cuGraphExecMemsetNodeSetParams(
2660                        self.exec,
2661                        node.raw,
2662                        params,
2663                        self.context.cu_ctx(),
2664                    ),
2665                )?;
2666                cuda_graph_check(
2667                    "cuGraphMemsetNodeSetParams",
2668                    sys::cuGraphMemsetNodeSetParams(node.raw, params),
2669                )
2670            },
2671        )?;
2672        drop(execution);
2673        drop(obsolete);
2674        Ok(())
2675    }
2676
2677    /// Raw graph handle for low-level node inventory/update code.
2678    pub fn graph(&self) -> sys::CUgraph {
2679        self.graph
2680    }
2681
2682    /// Raw instantiated graph handle for low-level graph-exec update code.
2683    /// Executions submitted outside [`Self::launch`] are not tracked by this
2684    /// owner. The unsafe raw-API caller must finish them before dropping it.
2685    pub fn exec(&self) -> sys::CUgraphExec {
2686        self.exec
2687    }
2688}
2689
2690impl Drop for CapturedCudaGraph {
2691    fn drop(&mut self) {
2692        let graph = self.graph as usize;
2693        let exec = self.exec as usize;
2694        let context = self.context.clone();
2695        let modules = self.modules.clone();
2696        let execution = self
2697            .execution
2698            .get_mut()
2699            .unwrap_or_else(|error| error.into_inner());
2700        let completion = mem::take(&mut execution.completion);
2701        let binding = mem::take(&mut execution.binding);
2702        let conditional_api = self._conditional_api.take();
2703        let resident_lease = self._resident_lifecycle_lease.take();
2704        retire_resources_after_completion(
2705            (
2706                graph,
2707                exec,
2708                context,
2709                modules,
2710                completion,
2711                binding,
2712                conditional_api,
2713                resident_lease,
2714            ),
2715            |(_, _, context, _, completion, _, _, _)| {
2716                context.bind_to_thread().and_then(|()| completion.wait())
2717            },
2718            |(graph, exec, _, _, _, _, _, _)| unsafe {
2719                sys::cuGraphExecDestroy(*exec as sys::CUgraphExec).result()?;
2720                sys::cuGraphDestroy(*graph as sys::CUgraph).result()
2721            },
2722            |_| Ok(()),
2723            |(_, _, context, _, _, _, _, _), error| context.record_err::<()>(Err(error)),
2724        );
2725    }
2726}
2727
2728/// Retry completion prerequisites and post-destruction release publication.
2729/// The destructive callback runs only once. Its error or unwind retains the
2730/// entire chain without resubmitting handles of unknown validity. This is
2731/// quarantine, not proof of completed destruction. On success, `finish` may
2732/// retry independently and must not access resources consumed by `destroy`.
2733pub(crate) fn retire_resources_after_completion<T: Send + 'static, E: std::fmt::Display>(
2734    resources: T,
2735    ready: impl Fn(&T) -> std::result::Result<(), E> + Send + 'static,
2736    destroy: impl Fn(&mut T) -> std::result::Result<(), E> + Send + 'static,
2737    finish: impl Fn(&mut T) -> std::result::Result<(), E> + Send + 'static,
2738    diagnose: impl Fn(&T, E) + Send + 'static,
2739) {
2740    let mut pending = mem::ManuallyDrop::new(Some(resources));
2741    let mut destroyed = false;
2742    retry_retirement_after_stream_captures(move || {
2743        let Some(resources) = pending.as_ref() else {
2744            return true;
2745        };
2746        if !destroyed {
2747            if let Err(error) = ready(resources) {
2748                // Do not reinsert this error into the context: bind_to_thread
2749                // consumes stored errors, so reinsertion prevents later retry.
2750                eprintln!("CUDA resource retirement prerequisites incomplete: {error}");
2751                return false;
2752            }
2753            let mut resources =
2754                mem::ManuallyDrop::new(pending.take().expect("retirement owns its resources"));
2755            if let Err(error) = destroy(&mut resources) {
2756                retain_failed_resources(resources, |resources| diagnose(resources, error));
2757                return true;
2758            }
2759            // Restore the capsule before retryable publication, including its
2760            // unwind path. Consumed owners must be taken out by `destroy`.
2761            *pending = Some(mem::ManuallyDrop::into_inner(resources));
2762            destroyed = true;
2763        }
2764        if let Err(error) = finish(pending.as_mut().expect("retirement owns its resources")) {
2765            eprintln!("CUDA resource release publication incomplete: {error}");
2766            return false;
2767        }
2768        drop(pending.take());
2769        true
2770    });
2771}
2772
2773fn retain_failed_resources<T: 'static>(resources: T, diagnose: impl FnOnce(&T)) {
2774    // Unknown completion cannot release dependencies, even if diagnosis unwinds.
2775    let resources = Box::leak(Box::new(resources));
2776    diagnose(resources);
2777}
2778
2779impl CudaGraphNodeKind {
2780    fn from_sys(kind: sys::CUgraphNodeType) -> Self {
2781        match kind {
2782            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_KERNEL => Self::Kernel,
2783            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMCPY => Self::Memcpy,
2784            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMSET => Self::Memset,
2785            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_HOST => Self::Host,
2786            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_GRAPH => Self::Graph,
2787            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EMPTY => Self::Empty,
2788            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_WAIT_EVENT => Self::WaitEvent,
2789            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EVENT_RECORD => Self::EventRecord,
2790            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_SIGNAL => {
2791                Self::ExternalSemaphoresSignal
2792            }
2793            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_WAIT => Self::ExternalSemaphoresWait,
2794            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_ALLOC => Self::MemAlloc,
2795            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_FREE => Self::MemFree,
2796            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_BATCH_MEM_OP => Self::BatchMemOp,
2797            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL => Self::Conditional,
2798        }
2799    }
2800}
2801
2802fn cuda_graph_check(label: &str, code: sys::CUresult) -> Result<()> {
2803    if code == sys::CUresult::CUDA_SUCCESS {
2804        Ok(())
2805    } else {
2806        Err(XlogError::Kernel(format!("{label} failed: {code:?}")))
2807    }
2808}
2809
2810fn stream_context(stream: &CudaStream) -> Result<sys::CUcontext> {
2811    stream
2812        .context()
2813        .bind_to_thread()
2814        .map_err(|error| XlogError::Kernel(error.to_string()))?;
2815    let mut ctx = ptr::null_mut();
2816    unsafe {
2817        cuda_graph_check(
2818            "cuStreamGetCtx",
2819            sys::cuStreamGetCtx(stream.cu_stream(), &mut ctx),
2820        )?;
2821    }
2822    if ctx.is_null() {
2823        Err(XlogError::Kernel(
2824            "cuStreamGetCtx returned a null CUDA context".to_string(),
2825        ))
2826    } else {
2827        Ok(ctx)
2828    }
2829}
2830
2831#[cfg(test)]
2832mod tests {
2833    use super::*;
2834    use cudarc::{
2835        driver::{DevicePtr, LaunchConfig, PushKernelArg},
2836        nvrtc::compile_ptx,
2837    };
2838    use std::sync::Barrier;
2839    use std::thread;
2840
2841    #[test]
2842    fn failed_graph_retirement_retains_dependencies_before_diagnostic_unwind() {
2843        use std::sync::atomic::{AtomicUsize, Ordering};
2844
2845        struct DropProbe(Arc<AtomicUsize>);
2846
2847        impl Drop for DropProbe {
2848            fn drop(&mut self) {
2849                self.0.fetch_add(1, Ordering::SeqCst);
2850            }
2851        }
2852
2853        for panic_diagnostic in [false, true] {
2854            let drops = Arc::new(AtomicUsize::new(0));
2855            let resources = (
2856                Arc::new(DropProbe(drops.clone())),
2857                Arc::new(DropProbe(drops.clone())),
2858                Arc::new(DropProbe(drops.clone())),
2859            );
2860            // No external strong owner can mask premature dependency release.
2861            let observers = [
2862                Arc::downgrade(&resources.0),
2863                Arc::downgrade(&resources.1),
2864                Arc::downgrade(&resources.2),
2865            ];
2866            let diagnosed = std::cell::Cell::new(false);
2867            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2868                retain_failed_resources(resources, |_| {
2869                    diagnosed.set(true);
2870                    if panic_diagnostic {
2871                        panic!("graph retirement diagnostic failed");
2872                    }
2873                });
2874            }));
2875
2876            assert!(diagnosed.get());
2877            assert_eq!(result.is_err(), panic_diagnostic);
2878            assert_eq!(drops.load(Ordering::SeqCst), 0);
2879            assert!(observers.iter().all(|owner| owner.upgrade().is_some()));
2880        }
2881    }
2882
2883    fn conditional_cuda_context_or_skip() -> Option<Arc<CudaContext>> {
2884        if let Err(error) = ConditionalGraphDriverApi::load() {
2885            if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
2886                panic!("XLOG_REQUIRE_CUDA=1 but CUDA conditional-graph setup failed: {error}");
2887            }
2888            eprintln!("Skipping test: {}", error.decline_detail());
2889            return None;
2890        }
2891
2892        match CudaContext::new(0) {
2893            Ok(context) => Some(context),
2894            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
2895                panic!("XLOG_REQUIRE_CUDA=1 but CUDA setup failed: {error}")
2896            }
2897            Err(error) => {
2898                eprintln!("Skipping test: CUDA unavailable: {error}");
2899                None
2900            }
2901        }
2902    }
2903
2904    #[test]
2905    fn conditional_graphs_expose_dependency_ordered_linear_chain_inventory() {
2906        let _: fn(
2907            &ConditionalCudaGraphBody,
2908        )
2909            -> std::result::Result<Vec<CudaGraphNodeKind>, CudaConditionalGraphUnavailable> =
2910            ConditionalCudaGraphBody::linear_chain_node_kinds;
2911        let _: fn(&CapturedCudaGraph) -> Result<Vec<CudaGraphNodeKind>> =
2912            CapturedCudaGraph::linear_chain_node_kinds;
2913    }
2914
2915    #[test]
2916    fn linear_chain_inventory_recovers_dependency_order_from_shuffled_nodes() {
2917        let enumerated_kinds = [
2918            CudaGraphNodeKind::Kernel,
2919            CudaGraphNodeKind::Kernel,
2920            CudaGraphNodeKind::Conditional,
2921            CudaGraphNodeKind::Kernel,
2922            CudaGraphNodeKind::Conditional,
2923        ];
2924        let immediate_dependencies = vec![vec![2], vec![4], vec![3], vec![], vec![0]];
2925        let dependency_order = linear_chain_order(&immediate_dependencies).unwrap();
2926        assert_eq!(dependency_order, vec![3, 2, 0, 4, 1]);
2927        assert_eq!(
2928            dependency_order
2929                .into_iter()
2930                .map(|index| enumerated_kinds[index])
2931                .collect::<Vec<_>>(),
2932            vec![
2933                CudaGraphNodeKind::Kernel,
2934                CudaGraphNodeKind::Conditional,
2935                CudaGraphNodeKind::Kernel,
2936                CudaGraphNodeKind::Conditional,
2937                CudaGraphNodeKind::Kernel,
2938            ]
2939        );
2940        assert_eq!(linear_chain_order(&[vec![]]).unwrap(), vec![0]);
2941    }
2942
2943    #[test]
2944    fn linear_chain_inventory_rejects_non_linear_dependency_shapes() {
2945        let cases = [
2946            (
2947                "empty graph",
2948                Vec::new(),
2949                LinearGraphChainError::RootCount { found: 0 },
2950            ),
2951            (
2952                "branch",
2953                vec![vec![], vec![0], vec![0]],
2954                LinearGraphChainError::Branch {
2955                    node: 0,
2956                    dependents: 2,
2957                },
2958            ),
2959            (
2960                "disconnected",
2961                vec![vec![], vec![]],
2962                LinearGraphChainError::RootCount { found: 2 },
2963            ),
2964            (
2965                "cycle",
2966                vec![vec![1], vec![0]],
2967                LinearGraphChainError::Cycle,
2968            ),
2969            (
2970                "foreign dependency",
2971                vec![vec![], vec![2]],
2972                LinearGraphChainError::ForeignDependency {
2973                    node: 1,
2974                    dependency: 2,
2975                },
2976            ),
2977            (
2978                "duplicate edge",
2979                vec![vec![], vec![0, 0]],
2980                LinearGraphChainError::DuplicateDependency {
2981                    node: 1,
2982                    dependency: 0,
2983                },
2984            ),
2985        ];
2986        for (case, dependencies, expected) in cases {
2987            assert_eq!(linear_chain_order(&dependencies), Err(expected), "{case}");
2988        }
2989    }
2990
2991    #[test]
2992    fn capture_retirement_waits_for_every_active_capture() {
2993        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
2994        use std::sync::atomic::{AtomicUsize, Ordering};
2995        let completed = Arc::new(AtomicUsize::new(0));
2996        let first = try_acquire_stream_capture_key(StreamCaptureKey {
2997            context: 0xc1,
2998            stream: 0xc2,
2999        })
3000        .unwrap();
3001        let second = try_acquire_stream_capture_key(StreamCaptureKey {
3002            context: 0xc1,
3003            stream: 0xc3,
3004        })
3005        .unwrap();
3006        let observed = completed.clone();
3007        retire_after_stream_captures(move || {
3008            observed.fetch_add(1, Ordering::SeqCst);
3009        });
3010        assert_eq!(completed.load(Ordering::SeqCst), 0);
3011        drop(first);
3012        assert_eq!(completed.load(Ordering::SeqCst), 0);
3013        drop(second);
3014        assert_eq!(completed.load(Ordering::SeqCst), 1);
3015    }
3016
3017    #[test]
3018    fn capture_retirement_retries_owned_work_once_per_cold_drain() {
3019        use std::sync::atomic::{AtomicUsize, Ordering};
3020
3021        let _capture_test = capture_lifecycle_test_guard();
3022        let attempts = Arc::new(AtomicUsize::new(0));
3023        let completed = Arc::new(AtomicUsize::new(0));
3024        let allocation = Arc::new(vec![42_u8; 64].into_boxed_slice());
3025        let observer = Arc::downgrade(&allocation);
3026        let observed_attempts = attempts.clone();
3027        let observed_completed = completed.clone();
3028        retry_retirement_after_stream_captures(move || {
3029            assert_eq!(allocation[0], 42);
3030            let attempt = observed_attempts.fetch_add(1, Ordering::SeqCst);
3031            if attempt == 0 {
3032                let nested = observed_completed.clone();
3033                retire_after_stream_captures(move || {
3034                    nested.fetch_add(1, Ordering::SeqCst);
3035                });
3036                return false;
3037            }
3038            true
3039        });
3040        assert_eq!(attempts.load(Ordering::SeqCst), 1);
3041        assert_eq!(completed.load(Ordering::SeqCst), 1);
3042        assert!(observer.upgrade().is_some());
3043
3044        let unrelated = completed.clone();
3045        thread::spawn(move || {
3046            retire_after_stream_captures(move || {
3047                unrelated.fetch_add(1, Ordering::SeqCst);
3048            });
3049        })
3050        .join()
3051        .unwrap();
3052        assert_eq!(attempts.load(Ordering::SeqCst), 2);
3053        assert_eq!(completed.load(Ordering::SeqCst), 2);
3054        assert!(observer.upgrade().is_none());
3055    }
3056
3057    #[test]
3058    fn capture_retirement_retry_unwind_restores_executing_and_pending_owners() {
3059        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3060
3061        let _capture_test = capture_lifecycle_test_guard();
3062        let earlier = Arc::new(vec![3_u8; 16].into_boxed_slice());
3063        let earlier_observer = Arc::downgrade(&earlier);
3064        let ready = Arc::new(AtomicBool::new(false));
3065        let observed_ready = ready.clone();
3066        retry_retirement_after_stream_captures(move || {
3067            assert_eq!(earlier[0], 3);
3068            observed_ready.load(Ordering::SeqCst)
3069        });
3070        let allocation = Arc::new(vec![7_u8; 32].into_boxed_slice());
3071        let observer = Arc::downgrade(&allocation);
3072        let attempts = Arc::new(AtomicUsize::new(0));
3073        let observed_attempts = attempts.clone();
3074        let pending = Arc::new(AtomicUsize::new(0));
3075        let observed_pending = pending.clone();
3076        let failed = std::panic::catch_unwind(move || {
3077            retry_retirement_after_stream_captures(move || {
3078                assert_eq!(allocation[0], 7);
3079                if observed_attempts.fetch_add(1, Ordering::SeqCst) == 0 {
3080                    let nested = observed_pending.clone();
3081                    retire_after_stream_captures(move || {
3082                        nested.fetch_add(1, Ordering::SeqCst);
3083                    });
3084                    panic!("reclamation fence wait unwound");
3085                }
3086                true
3087            });
3088        });
3089        assert!(failed.is_err());
3090        assert!(observer.upgrade().is_some());
3091        assert!(earlier_observer.upgrade().is_some());
3092        assert!(!stream_capture_registry().retiring);
3093        ready.store(true, Ordering::SeqCst);
3094        retire_after_stream_captures(|| {});
3095        assert_eq!(attempts.load(Ordering::SeqCst), 2);
3096        assert_eq!(pending.load(Ordering::SeqCst), 1);
3097        assert!(observer.upgrade().is_none());
3098        assert!(earlier_observer.upgrade().is_none());
3099    }
3100
3101    fn host_capture_phase(
3102        key: StreamCaptureKey,
3103        id: u64,
3104    ) -> (StreamCaptureLease, StreamSubmissionPhase) {
3105        let mut lease = try_acquire_stream_capture_key(key).unwrap();
3106        lease.driver_active = true;
3107        lease.register_capture(id, &mut stream_capture_registry());
3108        let phase = StreamSubmissionPhase::from_capture_info(
3109            stream_capture_registry(),
3110            key.context,
3111            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE,
3112            id,
3113            None,
3114        )
3115        .unwrap();
3116        (lease, phase)
3117    }
3118
3119    fn capture_phase_owners(phase: &StreamSubmissionPhase) -> &CaptureOwners {
3120        match &phase.state {
3121            StreamSubmissionState::Captured { owners, .. } => owners,
3122            StreamSubmissionState::Ordinary { .. } => panic!("expected a captured phase"),
3123        }
3124    }
3125
3126    #[test]
3127    fn capture_submission_borrowed_pin_survives_closing() {
3128        let _capture_test = capture_lifecycle_test_guard();
3129        let (mut lease, phase) = host_capture_phase(
3130            StreamCaptureKey {
3131                context: 0x301,
3132                stream: 0x302,
3133            },
3134            0x303,
3135        );
3136        let pin = phase.pin().unwrap();
3137        let executing = lease.close_submission_admission().unwrap();
3138        assert!(phase.pin().is_err());
3139        pin.with_submission(|owners| {
3140            assert!(Arc::ptr_eq(owners.unwrap(), capture_phase_owners(&phase)));
3141            assert!(ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok());
3142            Ok(())
3143        })
3144        .unwrap();
3145        drop(pin);
3146        executing.wait_until_idle();
3147        lease.complete_driver_capture();
3148    }
3149
3150    #[test]
3151    fn graph_retirement_unknown_destroy_never_resubmits_or_releases_owners() {
3152        use std::sync::atomic::{AtomicUsize, Ordering};
3153        let _capture_test = capture_lifecycle_test_guard();
3154        for unwind in [false, true] {
3155            let calls = Arc::new(AtomicUsize::new(0));
3156            let called = calls.clone();
3157            let (owner, bytes) = crate::memory::test_memory_manifest(|| {});
3158            let resource = Arc::new(vec![71_u8]);
3159            let original = Arc::downgrade(&resource);
3160            let mut captured = CapturedOwners::with_external_resources(vec![resource]);
3161            captured.memory.push(owner);
3162            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3163                retire_resources_after_completion(
3164                    Arc::new(Mutex::new(captured)),
3165                    |_| Ok(()),
3166                    move |_| {
3167                        called.fetch_add(1, Ordering::SeqCst);
3168                        if unwind {
3169                            panic!("destructive call interrupted");
3170                        }
3171                        Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN))
3172                    },
3173                    |_| Ok(()),
3174                    |_, _| {},
3175                )
3176            }));
3177            assert_eq!(result.is_err(), unwind);
3178            reap_capture_retirements();
3179            reap_capture_retirements();
3180            assert_eq!(calls.load(Ordering::SeqCst), 1);
3181            assert!(bytes.upgrade().is_some());
3182            assert!(original.upgrade().is_some());
3183        }
3184    }
3185
3186    #[test]
3187    fn graph_retirement_precondition_unwind_keeps_retry_callable() {
3188        use std::sync::atomic::{AtomicBool, Ordering};
3189        let _capture_test = capture_lifecycle_test_guard();
3190        let first = Arc::new(AtomicBool::new(true));
3191        let (owner, bytes) = crate::memory::test_memory_manifest(|| {});
3192        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3193            retire_resources_after_completion(
3194                owner,
3195                move |_| {
3196                    if first.swap(false, Ordering::SeqCst) {
3197                        panic!("completion wait interrupted");
3198                    }
3199                    Ok(())
3200                },
3201                |_| Ok(()),
3202                |_| Ok(()),
3203                |_, _: DriverError| panic!("no terminal failure expected"),
3204            )
3205        }));
3206        assert!(result.is_err());
3207        assert!(bytes.upgrade().is_some());
3208        reap_capture_retirements();
3209        assert!(bytes.upgrade().is_none());
3210    }
3211
3212    #[test]
3213    fn graph_retirement_retries_preconditions_without_resubmitting_destroy() {
3214        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3215        let _capture_test = capture_lifecycle_test_guard();
3216        let ready = Arc::new(AtomicBool::new(false));
3217        let destroyed = Arc::new(AtomicUsize::new(0));
3218        let ready_probe = ready.clone();
3219        let destroy_probe = destroyed.clone();
3220        let (owner, bytes) = crate::memory::test_memory_manifest(|| {});
3221        let resource = Arc::new(vec![83_u8]);
3222        let original = Arc::downgrade(&resource);
3223        let mut captured = CapturedOwners::with_external_resources(vec![resource]);
3224        captured.memory.push(owner);
3225        retire_resources_after_completion(
3226            Arc::new(Mutex::new(captured)),
3227            move |_| {
3228                if ready_probe.load(Ordering::SeqCst) {
3229                    Ok(())
3230                } else {
3231                    Err(DriverError(sys::CUresult::CUDA_ERROR_NOT_READY))
3232                }
3233            },
3234            move |_| {
3235                destroy_probe.fetch_add(1, Ordering::SeqCst);
3236                Ok(())
3237            },
3238            |_| Ok(()),
3239            |_, _| panic!("no terminal failure expected"),
3240        );
3241        assert_eq!(destroyed.load(Ordering::SeqCst), 0);
3242        assert!(bytes.upgrade().is_some());
3243        assert!(original.upgrade().is_some());
3244        ready.store(true, Ordering::SeqCst);
3245        reap_capture_retirements();
3246        assert_eq!(destroyed.load(Ordering::SeqCst), 1);
3247        assert!(bytes.upgrade().is_none());
3248        assert!(original.upgrade().is_none());
3249        reap_capture_retirements();
3250        assert_eq!(destroyed.load(Ordering::SeqCst), 1);
3251    }
3252
3253    #[test]
3254    fn graph_memory_partial_update_retains_both_owner_generations() {
3255        use crate::memory::test_memory_manifest;
3256        use std::panic::{catch_unwind, AssertUnwindSafe};
3257        for unwind in [false, true] {
3258            for failed_step in 0..6 {
3259                let (old, old_bytes) = test_memory_manifest(|| {});
3260                let (candidate, new_bytes) = test_memory_manifest(|| {});
3261                let mut binding = GraphMemoryBinding {
3262                    memory: old,
3263                    unusable: false,
3264                };
3265                let steps = std::cell::Cell::new(0);
3266                let result = catch_unwind(AssertUnwindSafe(|| {
3267                    replace_graph_memory(
3268                        &mut binding,
3269                        candidate,
3270                        || Ok::<(), ()>(()),
3271                        || {
3272                            for step in 0..6 {
3273                                assert!(old_bytes.upgrade().is_some());
3274                                assert!(new_bytes.upgrade().is_some());
3275                                steps.set(steps.get() + 1);
3276                                if step == failed_step {
3277                                    if unwind {
3278                                        panic!("parameter update interrupted");
3279                                    }
3280                                    return Err(());
3281                                }
3282                            }
3283                            Ok(())
3284                        },
3285                    )
3286                }));
3287                assert_eq!(result.is_err(), unwind);
3288                if let Ok(result) = result {
3289                    assert!(result.is_err());
3290                }
3291                assert_eq!(steps.get(), failed_step + 1);
3292                assert!(binding.unusable);
3293                assert!(old_bytes.upgrade().is_some());
3294                assert!(new_bytes.upgrade().is_some());
3295                drop(binding);
3296                assert!(old_bytes.upgrade().is_none());
3297                assert!(new_bytes.upgrade().is_none());
3298            }
3299        }
3300    }
3301
3302    #[test]
3303    fn graph_memory_wait_failure_preserves_original_binding_without_update() {
3304        let (old, old_bytes) = crate::memory::test_memory_manifest(|| {});
3305        let (candidate, new_bytes) = crate::memory::test_memory_manifest(|| {});
3306        let mut binding = GraphMemoryBinding {
3307            memory: old,
3308            unusable: false,
3309        };
3310        assert!(replace_graph_memory(
3311            &mut binding,
3312            candidate,
3313            || Err(()),
3314            || panic!("update after failed wait")
3315        )
3316        .is_err());
3317        assert!(!binding.unusable);
3318        assert!(old_bytes.upgrade().is_some());
3319        assert!(new_bytes.upgrade().is_none());
3320        drop(binding);
3321        assert!(old_bytes.upgrade().is_none());
3322    }
3323
3324    #[test]
3325    fn graph_memory_success_releases_obsolete_owners_outside_lock() {
3326        use std::sync::atomic::{AtomicBool, Ordering};
3327        let gate = Arc::new(Mutex::new(GraphMemoryBinding::default()));
3328        let weak_gate = Arc::downgrade(&gate);
3329        let released_outside = Arc::new(AtomicBool::new(false));
3330        let released = released_outside.clone();
3331        let (old, old_bytes) = crate::memory::test_memory_manifest(move || {
3332            released.store(
3333                weak_gate.upgrade().unwrap().try_lock().is_ok(),
3334                Ordering::SeqCst,
3335            );
3336        });
3337        let (candidate, new_bytes) = crate::memory::test_memory_manifest(|| {});
3338        let mut guard = gate.lock().unwrap();
3339        guard.memory = old;
3340        let obsolete =
3341            replace_graph_memory(&mut guard, candidate, || Ok::<(), ()>(()), || Ok(())).unwrap();
3342        assert!(!guard.unusable);
3343        assert!(old_bytes.upgrade().is_some());
3344        assert!(new_bytes.upgrade().is_some());
3345        drop(guard);
3346        drop(obsolete);
3347        assert!(old_bytes.upgrade().is_none());
3348        assert!(released_outside.load(Ordering::SeqCst));
3349        assert!(new_bytes.upgrade().is_some());
3350        drop(gate);
3351        assert!(new_bytes.upgrade().is_none());
3352    }
3353
3354    #[test]
3355    fn capture_submission_serializes_one_target_without_blocking_another() {
3356        use std::sync::mpsc::{channel, RecvTimeoutError};
3357        use std::time::Duration;
3358        let _capture_test = capture_lifecycle_test_guard();
3359        let (mut first, phase) = host_capture_phase(
3360            StreamCaptureKey {
3361                context: 0x191,
3362                stream: 0x192,
3363            },
3364            0x193,
3365        );
3366        let (mut other, other_phase) = host_capture_phase(
3367            StreamCaptureKey {
3368                context: 0x191,
3369                stream: 0x194,
3370            },
3371            0x195,
3372        );
3373        let (entered_tx, entered_rx) = channel();
3374        let (release_tx, release_rx) = channel();
3375        let (second_tx, second_rx) = channel();
3376        thread::scope(|scope| {
3377            let first_phase = &phase;
3378            let callback = scope.spawn(move || {
3379                let pin = first_phase.pin().unwrap();
3380                pin.with_serialized_submission(|| {
3381                    assert!(capture_phase_owners(first_phase).try_lock().is_ok());
3382                    assert!(ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok());
3383                    pin.with_submission(|_| Ok(()))?;
3384                    entered_tx.send(()).unwrap();
3385                    release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3386                    Ok::<(), DriverError>(())
3387                })
3388                .unwrap();
3389            });
3390            entered_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3391            let same_phase = &phase;
3392            let gate = capture_phase_owners(&phase)
3393                .lock()
3394                .unwrap()
3395                .submission
3396                .clone();
3397            assert!(matches!(
3398                gate.try_lock(),
3399                Err(std::sync::TryLockError::WouldBlock)
3400            ));
3401            let second = scope.spawn(move || {
3402                same_phase
3403                    .with_submission(|_| {
3404                        second_tx.send(()).unwrap();
3405                        Ok(())
3406                    })
3407                    .unwrap();
3408            });
3409            other_phase.with_submission(|_| Ok(())).unwrap();
3410            assert_eq!(
3411                second_rx.recv_timeout(Duration::from_millis(100)),
3412                Err(RecvTimeoutError::Timeout)
3413            );
3414            release_tx.send(()).unwrap();
3415            second_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3416            callback.join().unwrap();
3417            second.join().unwrap();
3418        });
3419        first
3420            .close_submission_admission()
3421            .unwrap()
3422            .wait_until_idle();
3423        first.complete_driver_capture();
3424        other
3425            .close_submission_admission()
3426            .unwrap()
3427            .wait_until_idle();
3428        other.complete_driver_capture();
3429    }
3430
3431    #[test]
3432    fn capture_retained_resources_survive_callback_failure_and_unwind() {
3433        for unwind in [false, true] {
3434            let resource = Arc::new(vec![17_u8, 29]);
3435            let original = Arc::downgrade(&resource);
3436            let owners = Arc::new(Mutex::new(CapturedOwners::with_external_resources(vec![
3437                resource,
3438            ])));
3439            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3440                capture_outcome(Some(&owners), || {
3441                    assert_eq!(&**original.upgrade().unwrap(), &[17, 29]);
3442                    if unwind {
3443                        panic!("producer capture interrupted");
3444                    }
3445                    Err::<(), ()>(())
3446                })
3447            }));
3448            assert_eq!(result.is_err(), unwind);
3449            assert!(owners.lock().unwrap().failed);
3450            assert!(original.upgrade().is_some());
3451            drop(owners);
3452            assert!(original.upgrade().is_none());
3453        }
3454    }
3455
3456    #[test]
3457    fn capture_retained_resources_follow_the_child_snapshot() {
3458        let resource = Arc::new(vec![41_u8, 53]);
3459        let original = Arc::downgrade(&resource);
3460        let child = Arc::new(Mutex::new(CapturedOwners::with_external_resources(vec![
3461            resource,
3462        ])));
3463        let parent = CaptureOwners::default();
3464        let (memory, bytes) = crate::memory::test_memory_manifest(|| {});
3465        retain_child_capture_owners(&parent, &child, &memory);
3466        drop(child);
3467        drop(memory);
3468        assert_eq!(&**original.upgrade().unwrap(), &[41, 53]);
3469        assert!(bytes.upgrade().is_some());
3470        drop(parent);
3471        assert!(original.upgrade().is_none());
3472        assert!(bytes.upgrade().is_none());
3473    }
3474
3475    #[test]
3476    fn capture_submission_outer_callback_failure_marks_the_shared_target() {
3477        for unwind in [false, true] {
3478            let owners = CaptureOwners::default();
3479            let (manifest, bytes) = crate::memory::test_memory_manifest(|| {});
3480            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3481                capture_outcome(Some(&owners), || {
3482                    // Successful earlier nodes have already transferred their owner.
3483                    owners.lock().unwrap().memory.push(manifest);
3484                    if unwind {
3485                        panic!("outer capture callback interrupted");
3486                    }
3487                    Err::<(), ()>(())
3488                })
3489            }));
3490            assert_eq!(result.is_err(), unwind);
3491            assert!(owners.lock().unwrap().failed);
3492            assert!(bytes.upgrade().is_some());
3493            drop(owners);
3494            assert!(bytes.upgrade().is_none());
3495        }
3496    }
3497
3498    #[test]
3499    fn capture_submission_swallowed_nested_failure_stays_poisoned() {
3500        let _capture_test = capture_lifecycle_test_guard();
3501        for unwind in [false, true] {
3502            let (mut lease, phase) = host_capture_phase(
3503                StreamCaptureKey {
3504                    context: 0x171,
3505                    stream: 0x172,
3506                },
3507                0x173,
3508            );
3509            let pin = phase.pin().unwrap();
3510            pin.with_serialized_submission(|| {
3511                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3512                    pin.with_submission(|_| {
3513                        if unwind {
3514                            panic!("nested submission interrupted");
3515                        }
3516                        Err::<(), _>(DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE))
3517                    })
3518                }));
3519                assert_eq!(result.is_err(), unwind);
3520                pin.with_submission(|_| Ok(()))?;
3521                Ok::<(), DriverError>(())
3522            })
3523            .unwrap();
3524            assert!(capture_phase_owners(&phase).lock().unwrap().failed);
3525            drop(pin);
3526            lease
3527                .close_submission_admission()
3528                .unwrap()
3529                .wait_until_idle();
3530            lease.complete_driver_capture();
3531        }
3532    }
3533
3534    #[test]
3535    fn capture_submission_child_snapshot_survives_rebind_and_child_drop() {
3536        let _capture_test = capture_lifecycle_test_guard();
3537        let (mut lease, phase) = host_capture_phase(
3538            StreamCaptureKey {
3539                context: 0x181,
3540                stream: 0x182,
3541            },
3542            0x183,
3543        );
3544        let (old, old_bytes) = crate::memory::test_memory_manifest(|| {});
3545        let (candidate, new_bytes) = crate::memory::test_memory_manifest(|| {});
3546        let mut binding = GraphMemoryBinding {
3547            memory: old,
3548            unusable: false,
3549        };
3550        let pin = phase.pin().unwrap();
3551        assert!(pin.capture_memory(&binding.memory));
3552        drop(pin);
3553        drop(
3554            replace_graph_memory(&mut binding, candidate, || Ok::<(), ()>(()), || Ok(())).unwrap(),
3555        );
3556        assert!(old_bytes.upgrade().is_some());
3557        let parent = crate::memory::MemoryAccessManifest::combine(
3558            &capture_phase_owners(&phase).lock().unwrap().memory,
3559        );
3560        drop(binding);
3561        assert!(new_bytes.upgrade().is_none());
3562        lease
3563            .close_submission_admission()
3564            .unwrap()
3565            .wait_until_idle();
3566        lease.complete_driver_capture();
3567        drop(lease);
3568        drop(phase);
3569        assert!(old_bytes.upgrade().is_some());
3570        drop(parent);
3571        assert!(old_bytes.upgrade().is_none());
3572    }
3573
3574    #[test]
3575    fn capture_submission_close_waits_only_for_executing_callback_and_releases_owners() {
3576        use std::sync::atomic::{AtomicUsize, Ordering};
3577        use std::sync::mpsc::{channel, RecvTimeoutError};
3578        use std::time::Duration;
3579
3580        struct DropProbe(Arc<AtomicUsize>);
3581        impl Drop for DropProbe {
3582            fn drop(&mut self) {
3583                self.0.fetch_add(1, Ordering::SeqCst);
3584            }
3585        }
3586
3587        let _capture_test = capture_lifecycle_test_guard();
3588        for unwind in [false, true] {
3589            let (mut lease, phase) = host_capture_phase(
3590                StreamCaptureKey {
3591                    context: 0x101,
3592                    stream: 0x102,
3593                },
3594                0x103,
3595            );
3596            let module_owners = Arc::downgrade(capture_phase_owners(&phase));
3597            let drops = Arc::new(AtomicUsize::new(0));
3598            let owner = DropProbe(drops.clone());
3599            retire_after_stream_captures(move || drop(owner));
3600            let (entered_tx, entered_rx) = channel();
3601            let (release_tx, release_rx) = channel();
3602            let (closed_tx, closed_rx) = channel();
3603            let (waiting_tx, waiting_rx) = channel();
3604            thread::scope(|scope| {
3605                let callback_phase = &phase;
3606                let callback = scope.spawn(move || {
3607                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3608                        let pin = callback_phase.pin()?;
3609                        let submit = |owners: Option<&CaptureOwners>| {
3610                            assert!(Arc::ptr_eq(
3611                                owners.unwrap(),
3612                                capture_phase_owners(callback_phase)
3613                            ));
3614                            assert!(ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok());
3615                            // Reentrant submission uses the same owner and never takes a
3616                            // registry mutex recursively around the callback.
3617                            pin.with_submission(|_| Ok(()))?;
3618                            entered_tx.send(()).unwrap();
3619                            release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3620                            if unwind {
3621                                panic!("capture submission unwind");
3622                            }
3623                            Ok(())
3624                        };
3625                        pin.with_serialized_submission(|| pin.with_submission(submit))
3626                    }))
3627                });
3628                entered_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3629                // Closing admission is the same production transition used before
3630                // EndCapture, independent of waiting for the admitted callback.
3631                let executing = lease.close_submission_admission().unwrap();
3632                assert!(phase.with_submission(|_| Ok(())).is_err());
3633                let finish = scope.spawn(move || {
3634                    waiting_tx.send(()).unwrap();
3635                    executing.wait_until_idle();
3636                    lease.complete_driver_capture();
3637                    drop(lease);
3638                    closed_tx.send(()).unwrap();
3639                });
3640                waiting_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3641                let registry_available = ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok();
3642                let returned_before_completion = closed_rx.recv_timeout(Duration::from_millis(100));
3643                let dropped_before_completion = drops.load(Ordering::SeqCst);
3644                release_tx.send(()).unwrap();
3645                let callback = callback.join().unwrap();
3646                finish.join().unwrap();
3647                assert_eq!(callback.is_err(), unwind);
3648                if let Ok(result) = callback {
3649                    result.unwrap();
3650                }
3651                assert!(registry_available);
3652                assert_eq!(returned_before_completion, Err(RecvTimeoutError::Timeout));
3653                assert_eq!(dropped_before_completion, 0);
3654            });
3655            assert_eq!(drops.load(Ordering::SeqCst), 1);
3656            // The passive saved descriptor neither pins a callback nor permits a
3657            // post-finish enqueue to downgrade to ordinary execution.
3658            assert!(phase.with_submission(|_| Ok(())).is_err());
3659            assert!(module_owners.upgrade().is_some());
3660            drop(phase);
3661            assert!(module_owners.upgrade().is_none());
3662        }
3663    }
3664
3665    #[test]
3666    fn capture_submission_rejects_wrong_identity_without_blocking_other_capture() {
3667        let _capture_test = capture_lifecycle_test_guard();
3668        let (mut first, first_phase) = host_capture_phase(
3669            StreamCaptureKey {
3670                context: 0x111,
3671                stream: 0x112,
3672            },
3673            0x113,
3674        );
3675        let (mut second, second_phase) = host_capture_phase(
3676            StreamCaptureKey {
3677                context: 0x111,
3678                stream: 0x114,
3679            },
3680            0x115,
3681        );
3682        let first_executing = first.close_submission_admission().unwrap();
3683        first_executing.wait_until_idle();
3684        first.complete_driver_capture();
3685        drop(first);
3686        second_phase.with_submission(|_| Ok(())).unwrap();
3687        let ordinary = StreamSubmissionPhase::from_capture_info(
3688            stream_capture_registry(),
3689            0x111,
3690            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE,
3691            0,
3692            Some(0x112),
3693        )
3694        .unwrap();
3695        ordinary
3696            .with_submission(|owners| {
3697                assert!(owners.is_none());
3698                second_phase.with_submission(|_| Ok(()))
3699            })
3700            .unwrap();
3701        drop(ordinary);
3702        for (context, id, owners) in [
3703            (0x111, 0x113, capture_phase_owners(&first_phase).clone()),
3704            (0x116, 0x115, capture_phase_owners(&second_phase).clone()),
3705            (0x111, 0x115, capture_phase_owners(&first_phase).clone()),
3706        ] {
3707            let stale = StreamSubmissionPhase {
3708                state: StreamSubmissionState::Captured {
3709                    context,
3710                    id,
3711                    owners,
3712                    execution_id: id,
3713                },
3714            };
3715            let mut called = false;
3716            assert!(stale
3717                .with_submission(|_| {
3718                    called = true;
3719                    Ok(())
3720                })
3721                .is_err());
3722            assert!(!called);
3723        }
3724        assert!(StreamSubmissionPhase::from_capture_info(
3725            stream_capture_registry(),
3726            0x111,
3727            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE,
3728            0x117,
3729            None,
3730        )
3731        .is_err());
3732        second
3733            .close_submission_admission()
3734            .unwrap()
3735            .wait_until_idle();
3736        second.complete_driver_capture();
3737        drop(second);
3738    }
3739
3740    #[test]
3741    fn capture_submission_ordinary_phase_excludes_capture_through_callback() {
3742        fn assert_send_sync<T: Send + Sync>() {}
3743        assert_send_sync::<StreamSubmissionPhase>();
3744        let _capture_test = capture_lifecycle_test_guard();
3745        let phase = StreamSubmissionPhase::from_capture_info(
3746            stream_capture_registry(),
3747            0x121,
3748            sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE,
3749            0,
3750            Some(0x122),
3751        )
3752        .unwrap();
3753        assert!(matches!(
3754            phase.state,
3755            StreamSubmissionState::Ordinary { .. }
3756        ));
3757        let key = StreamCaptureKey {
3758            context: 0x121,
3759            stream: 0x122,
3760        };
3761        let pin = phase.pin().unwrap();
3762        assert!(pin.owners.is_none());
3763        assert!(pin._executing.is_none());
3764        assert!(try_acquire_stream_capture_key(key).is_err());
3765        drop(pin);
3766        phase
3767            .with_submission(|owners| {
3768                assert!(owners.is_none());
3769                assert!(ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok());
3770                assert!(try_acquire_stream_capture_key(key).is_err());
3771                Ok(())
3772            })
3773            .unwrap();
3774        assert!(try_acquire_stream_capture_key(key).is_err());
3775        drop(phase);
3776        drop(try_acquire_stream_capture_key(key).unwrap());
3777    }
3778
3779    #[test]
3780    fn ordinary_execution_reservation_excludes_capture_without_draining_or_locking() {
3781        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3782        use std::sync::atomic::{AtomicUsize, Ordering};
3783        let reservation = reserve_capture_exclusion().unwrap();
3784        let mutex_available = ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock().is_ok();
3785        let capture = try_acquire_stream_capture_key(StreamCaptureKey {
3786            context: 0xca,
3787            stream: 0xcb,
3788        });
3789        assert!(mutex_available);
3790        assert!(capture.is_err());
3791        let releases = Arc::new(AtomicUsize::new(0));
3792        let observed = Arc::clone(&releases);
3793        retire_after_stream_captures(move || {
3794            observed.fetch_add(1, Ordering::SeqCst);
3795        });
3796        // A cold retirement must not strand completed resources just because
3797        // another ordinary execution still excludes capture.
3798        assert_eq!(releases.load(Ordering::SeqCst), 1);
3799        drop(reservation);
3800        // Returning from a measured operation does not acquire a new cold wait.
3801        assert_eq!(releases.load(Ordering::SeqCst), 1);
3802        drain_capture_retirements(stream_capture_registry());
3803        assert_eq!(releases.load(Ordering::SeqCst), 1);
3804
3805        let capture = try_acquire_stream_capture_key(StreamCaptureKey {
3806            context: 0xca,
3807            stream: 0xcb,
3808        })
3809        .unwrap();
3810        assert!(reserve_capture_exclusion().is_err());
3811        drop(capture);
3812        assert!(reserve_capture_exclusion().is_ok());
3813    }
3814
3815    #[test]
3816    fn capture_retirement_release_waits_without_holding_registry_mutex() {
3817        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3818        use std::sync::mpsc::{channel, TryRecvError};
3819        use std::sync::TryLockError;
3820        use std::time::Duration;
3821
3822        let lease = try_acquire_stream_capture_key(StreamCaptureKey {
3823            context: 0xc4,
3824            stream: 0xc5,
3825        })
3826        .unwrap();
3827        let (entered_tx, entered_rx) = channel();
3828        let (release_tx, release_rx) = channel();
3829        let (returned_tx, returned_rx) = channel();
3830        retire_after_stream_captures(move || {
3831            entered_tx.send(()).unwrap();
3832            release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3833        });
3834        let release = thread::spawn(move || {
3835            drop(lease);
3836            returned_tx.send(()).unwrap();
3837        });
3838
3839        entered_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3840        // Inspect without blocking, so a lock regression can still release the
3841        // waiting destructor and join its thread before failing the assertion.
3842        let registry_state = match ACTIVE_STREAM_CAPTURES.get().unwrap().try_lock() {
3843            Ok(registry) => Some((registry.active.is_empty(), registry.retiring)),
3844            Err(TryLockError::Poisoned(error)) => {
3845                let registry = error.into_inner();
3846                Some((registry.active.is_empty(), registry.retiring))
3847            }
3848            Err(TryLockError::WouldBlock) => None,
3849        };
3850        let returned_before_completion = returned_rx.try_recv();
3851        release_tx.send(()).unwrap();
3852        release.join().unwrap();
3853        assert_eq!(registry_state, Some((true, true)));
3854        assert_eq!(returned_before_completion, Err(TryRecvError::Empty));
3855        returned_rx.recv_timeout(Duration::from_secs(5)).unwrap();
3856        assert!(!stream_capture_registry().retiring);
3857    }
3858
3859    #[test]
3860    fn capture_retirement_unwind_preserves_pending_work_and_releases_reservation() {
3861        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3862        use std::panic::{catch_unwind, AssertUnwindSafe};
3863        use std::sync::atomic::{AtomicBool, Ordering};
3864        let key = StreamCaptureKey {
3865            context: 0xe1,
3866            stream: 0xe2,
3867        };
3868        let lease = try_acquire_stream_capture_key(key).unwrap();
3869        let completed = Arc::new(AtomicBool::new(false));
3870        retire_after_stream_captures(|| panic!("retirement unwind"));
3871        let observed = completed.clone();
3872        retire_after_stream_captures(move || observed.store(true, Ordering::SeqCst));
3873        assert!(catch_unwind(AssertUnwindSafe(|| drop(lease))).is_err());
3874        assert!(!completed.load(Ordering::SeqCst));
3875        drop(try_acquire_stream_capture_key(key).expect("unwind must release reservation"));
3876        assert!(completed.load(Ordering::SeqCst));
3877    }
3878
3879    #[test]
3880    fn module_loading_excludes_capture_and_releases_after_error_or_unwind() {
3881        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3882        use std::panic::{catch_unwind, AssertUnwindSafe};
3883        let key = StreamCaptureKey {
3884            context: 0xf1,
3885            stream: 0xf2,
3886        };
3887        let lease = try_acquire_stream_capture_key(key).unwrap();
3888        let mut called = false;
3889        assert_eq!(
3890            with_module_loading(|| {
3891                called = true;
3892                Ok(())
3893            }),
3894            Err(DriverError(
3895                sys::CUresult::CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED
3896            ))
3897        );
3898        assert!(!called, "capture rejection must precede module loading");
3899        drop(lease);
3900        let result = with_module_loading(|| {
3901            assert_eq!(
3902                try_acquire_stream_capture_key(key).unwrap_err(),
3903                CudaConditionalGraphUnavailable::StreamCaptureBusy
3904            );
3905            Err::<(), _>(DriverError(sys::CUresult::CUDA_ERROR_INVALID_PTX))
3906        });
3907        assert_eq!(
3908            result,
3909            Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_PTX))
3910        );
3911        drop(try_acquire_stream_capture_key(key).unwrap());
3912        assert!(catch_unwind(AssertUnwindSafe(|| {
3913            with_module_loading(|| -> std::result::Result<(), DriverError> {
3914                panic!("module load unwind");
3915            })
3916        }))
3917        .is_err());
3918        drop(try_acquire_stream_capture_key(key).unwrap());
3919    }
3920
3921    #[test]
3922    fn concurrent_module_loads_do_not_report_a_nonexistent_capture() {
3923        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3924        let (entered_tx, entered_rx) = std::sync::mpsc::channel();
3925        let (release_tx, release_rx) = std::sync::mpsc::channel();
3926        let first = thread::spawn(move || {
3927            with_module_loading(|| {
3928                entered_tx.send(()).unwrap();
3929                release_rx.recv().unwrap();
3930                Ok(())
3931            })
3932        });
3933        entered_rx.recv().unwrap();
3934        let second = with_module_loading(|| Ok(()));
3935        let capture = try_acquire_stream_capture_key(StreamCaptureKey {
3936            context: 0xf3,
3937            stream: 0xf4,
3938        });
3939        release_tx.send(()).unwrap();
3940        first.join().unwrap().unwrap();
3941        assert_eq!(second.map_err(|error| error.0), Ok(()));
3942        assert_eq!(
3943            capture.unwrap_err(),
3944            CudaConditionalGraphUnavailable::StreamCaptureBusy
3945        );
3946    }
3947
3948    #[test]
3949    fn capture_retirement_excludes_new_capture_and_drains_nested_retirement() {
3950        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3951        use std::sync::atomic::{AtomicUsize, Ordering};
3952        let completed = Arc::new(AtomicUsize::new(0));
3953        let observed = completed.clone();
3954        let key = StreamCaptureKey {
3955            context: 0xd1,
3956            stream: 0xd2,
3957        };
3958        retire_after_stream_captures(move || {
3959            assert_eq!(
3960                try_acquire_stream_capture_key(key).unwrap_err(),
3961                CudaConditionalGraphUnavailable::StreamCaptureBusy
3962            );
3963            retire_after_stream_captures(move || {
3964                observed.fetch_add(1, Ordering::SeqCst);
3965            });
3966        });
3967        assert_eq!(completed.load(Ordering::SeqCst), 1);
3968        drop(try_acquire_stream_capture_key(key).unwrap());
3969    }
3970
3971    #[test]
3972    fn capture_retirement_keeps_unknown_driver_capture_reserved() {
3973        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3974        use std::sync::atomic::{AtomicBool, Ordering};
3975        let key = StreamCaptureKey {
3976            context: 0xe1,
3977            stream: 0xe2,
3978        };
3979        let mut lease = try_acquire_stream_capture_key(key).unwrap();
3980        lease.driver_active = true;
3981        let completed = Arc::new(AtomicBool::new(false));
3982        let observed = completed.clone();
3983        retire_after_stream_captures(move || observed.store(true, Ordering::SeqCst));
3984        drop(lease);
3985        assert!(!completed.load(Ordering::SeqCst));
3986        assert_eq!(
3987            try_acquire_stream_capture_key(key).unwrap_err(),
3988            CudaConditionalGraphUnavailable::StreamCaptureBusy
3989        );
3990        // Only this test's host-only reservation is removed. Production removes
3991        // the reservation only after the CUDA driver proves capture has ended.
3992        release_stream_capture_key(key);
3993        assert!(completed.load(Ordering::SeqCst));
3994    }
3995
3996    #[test]
3997    fn same_stream_capture_registry_is_deterministically_busy_until_release() {
3998        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
3999        let key = StreamCaptureKey {
4000            context: 0x51,
4001            stream: 0x73,
4002        };
4003        let entered = Arc::new(Barrier::new(2));
4004        let release = Arc::new(Barrier::new(2));
4005        let first_entered = Arc::clone(&entered);
4006        let first_release = Arc::clone(&release);
4007        let first = thread::spawn(move || {
4008            let _lease = try_acquire_stream_capture_key(key).expect("first capture lease");
4009            first_entered.wait();
4010            first_release.wait();
4011        });
4012        entered.wait();
4013        assert_eq!(
4014            try_acquire_stream_capture_key(key).expect_err("same stream must be busy"),
4015            CudaConditionalGraphUnavailable::StreamCaptureBusy
4016        );
4017        release.wait();
4018        first.join().expect("first capture thread");
4019        drop(try_acquire_stream_capture_key(key).expect("capture after release"));
4020    }
4021
4022    #[test]
4023    fn different_stream_capture_registry_entries_can_coexist() {
4024        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4025        let first = try_acquire_stream_capture_key(StreamCaptureKey {
4026            context: 0x91,
4027            stream: 0x92,
4028        })
4029        .expect("first stream capture");
4030        let second = try_acquire_stream_capture_key(StreamCaptureKey {
4031            context: 0x91,
4032            stream: 0x93,
4033        })
4034        .expect("different stream capture");
4035        drop((first, second));
4036    }
4037
4038    #[test]
4039    fn real_capture_helper_returns_typed_busy_before_beginning_driver_capture() {
4040        let Some(context) = conditional_cuda_context_or_skip() else {
4041            return;
4042        };
4043        let stream = context.new_stream().expect("non-default CUDA stream");
4044        let _lease = try_acquire_stream_capture(&stream).expect("held stream capture lease");
4045        let mut builder = match ConditionalCudaGraphSequenceBuilder::new(&stream) {
4046            Ok(builder) => builder,
4047            Err(error) if error.is_unsupported() => return,
4048            Err(error) => panic!("sequence builder failed: {error}"),
4049        };
4050        let error = builder
4051            .capture_segment_on_stream(&stream, || Ok::<(), XlogError>(()))
4052            .expect_err("same stream capture must decline before driver capture");
4053        assert_eq!(error, CudaConditionalGraphUnavailable::StreamCaptureBusy);
4054    }
4055
4056    #[test]
4057    fn capture_callback_error_and_panic_leave_every_capture_path_reusable() {
4058        if std::env::var("XLOG_REQUIRE_CUDA").as_deref() != Ok("1") {
4059            return;
4060        }
4061        use std::panic::{catch_unwind, AssertUnwindSafe};
4062        let context = CudaContext::new(0).unwrap();
4063        let stream = context.new_stream().unwrap();
4064        for path in 0..3 {
4065            for unwind in [false, true] {
4066                let fail = || -> Result<()> {
4067                    if unwind {
4068                        panic!("capture callback unwind");
4069                    }
4070                    Err(XlogError::Kernel("capture callback error".into()))
4071                };
4072                let result = catch_unwind(AssertUnwindSafe(|| match path {
4073                    0 => CapturedCudaGraph::capture_on_stream(&stream, fail).map(|_| ()),
4074                    1 => CapturedCudaGraph::conditional_while_on_stream(&stream, 0, true, |body| {
4075                        body.capture_on_stream(&stream, fail)
4076                    })
4077                    .map(|_| ())
4078                    .map_err(|error| XlogError::Kernel(error.decline_detail())),
4079                    _ => ConditionalCudaGraphSequenceBuilder::new(&stream)
4080                        .and_then(|mut builder| builder.capture_segment_on_stream(&stream, fail))
4081                        .map_err(|error| XlogError::Kernel(error.decline_detail())),
4082                }));
4083                if unwind {
4084                    assert!(result.is_err(), "callback must unwind for path {path}");
4085                } else {
4086                    assert!(result
4087                        .unwrap()
4088                        .unwrap_err()
4089                        .to_string()
4090                        .contains("callback error"));
4091                }
4092                let mut status = sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_ACTIVE;
4093                unsafe { sys::cuStreamIsCapturing(stream.cu_stream(), &mut status).result() }
4094                    .unwrap();
4095                assert_eq!(
4096                    status,
4097                    sys::CUstreamCaptureStatus::CU_STREAM_CAPTURE_STATUS_NONE
4098                );
4099                let graph = CapturedCudaGraph::capture_on_stream(&stream, || Ok(())).unwrap();
4100                graph.launch(&stream).unwrap();
4101                stream.synchronize().unwrap();
4102            }
4103        }
4104    }
4105
4106    #[test]
4107    fn per_thread_default_streams_have_distinct_execution_ids() {
4108        if std::env::var("XLOG_REQUIRE_CUDA").as_deref() != Ok("1") {
4109            return;
4110        }
4111        let context = CudaContext::new(0).unwrap();
4112        let barrier = Arc::new(Barrier::new(2));
4113        let mut threads = Vec::new();
4114        for _ in 0..2 {
4115            let context = context.clone();
4116            let barrier = barrier.clone();
4117            threads.push(thread::spawn(move || {
4118                let stream = context.per_thread_stream();
4119                let id = stream_execution_id(&stream).unwrap();
4120                barrier.wait();
4121                (stream.cu_stream() as usize, id)
4122            }));
4123        }
4124        let first = threads.remove(0).join().unwrap();
4125        let second = threads.remove(0).join().unwrap();
4126        assert_eq!(
4127            first.0, second.0,
4128            "both wrappers use the per-thread sentinel"
4129        );
4130        assert_ne!(
4131            first.1, second.1,
4132            "completion fences need distinct execution IDs"
4133        );
4134    }
4135
4136    #[cfg(unix)]
4137    #[test]
4138    fn missing_driver_symbol_is_a_typed_error_instead_of_a_panic() {
4139        let library = libloading::Library::from(libloading::os::unix::Library::this());
4140        let error = unsafe {
4141            load_required_symbol::<ConditionalHandleCreateFn>(
4142                &library,
4143                b"xlog_missing_cuda_conditional_symbol_for_test\0",
4144                "xlog_missing_cuda_conditional_symbol_for_test",
4145            )
4146        }
4147        .expect_err("the deliberately absent symbol must fail closed");
4148
4149        assert_eq!(
4150            error,
4151            CudaConditionalGraphUnavailable::MissingDriverSymbol {
4152                symbol: "xlog_missing_cuda_conditional_symbol_for_test",
4153            }
4154        );
4155        assert!(error.is_unsupported());
4156        assert_eq!(
4157            error.decline_detail(),
4158            "CUDA driver is missing required conditional-graph symbol \
4159             xlog_missing_cuda_conditional_symbol_for_test"
4160        );
4161    }
4162
4163    #[test]
4164    fn driver_versions_before_cuda_twelve_three_decline_conditionals() {
4165        let error = require_conditional_graph_driver(12_020).expect_err("CUDA 12.2 is too old");
4166        assert_eq!(
4167            error,
4168            CudaConditionalGraphUnavailable::DriverVersionTooOld {
4169                found: 12_020,
4170                required: 12_030,
4171            }
4172        );
4173        assert!(error.is_unsupported());
4174        assert_eq!(
4175            error.decline_detail(),
4176            "CUDA conditional graphs require driver API 12030, found 12020"
4177        );
4178        require_conditional_graph_driver(12_030).expect("CUDA 12.3 is supported");
4179    }
4180
4181    #[test]
4182    fn while_node_params_use_the_driver_abi_and_return_one_body() {
4183        let handle = 0x1234_u64;
4184        let ctx = 0x5678_usize as sys::CUcontext;
4185        let params = conditional_node_params(
4186            handle,
4187            ctx,
4188            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE,
4189        );
4190
4191        assert_eq!(
4192            params.type_,
4193            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL
4194        );
4195        let conditional = unsafe { params.__bindgen_anon_1.conditional };
4196        assert_eq!(conditional.handle, handle);
4197        assert_eq!(
4198            conditional.type_,
4199            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE
4200        );
4201        assert_eq!(conditional.size, 1);
4202        assert!(conditional.phGraph_out.is_null());
4203        assert_eq!(conditional.ctx, ctx);
4204    }
4205
4206    #[test]
4207    fn conditional_node_params_preserve_one_shot_and_repeating_body_semantics() {
4208        let handle = 0x1234_u64;
4209        let context = 0x5678_usize as sys::CUcontext;
4210        for kind in [
4211            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_IF,
4212            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE,
4213        ] {
4214            let params = conditional_node_params(handle, context, kind);
4215            assert_eq!(
4216                params.type_,
4217                sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL
4218            );
4219            // SAFETY: the production constructor selected the conditional ABI.
4220            let conditional = unsafe { params.__bindgen_anon_1.conditional };
4221            assert_eq!(conditional.type_, kind);
4222            assert_eq!(conditional.handle, handle);
4223            assert_eq!(conditional.ctx, context);
4224            assert_eq!(conditional.size, 1);
4225            assert!(conditional.phGraph_out.is_null());
4226        }
4227    }
4228
4229    #[test]
4230    fn conditional_body_exposes_device_setter_handle_and_context() {
4231        let graph = 0x1234_usize as sys::CUgraph;
4232        let handle = 0x5678_u64;
4233        let context = 0x9abc_usize as sys::CUcontext;
4234        let body = ConditionalCudaGraphBody {
4235            graph,
4236            handle,
4237            context,
4238            modules: Arc::default(),
4239        };
4240
4241        assert_eq!(body.graph(), graph);
4242        assert_eq!(body.handle(), handle);
4243        assert_eq!(body.context(), context);
4244    }
4245
4246    #[test]
4247    fn real_conditional_while_graph_creates_instantiates_and_launches() {
4248        let Some(context) = conditional_cuda_context_or_skip() else {
4249            return;
4250        };
4251        let stream = context.new_stream().expect("non-default CUDA stream");
4252        let body_buffer = stream.alloc_zeros::<u32>(1).expect("body buffer");
4253        let (body_ptr, _body_sync) = body_buffer.device_ptr(&stream);
4254        let ptx = compile_ptx(
4255            r#"
4256            extern "C" __device__ void cudaGraphSetConditional(
4257                unsigned long long handle,
4258                unsigned int value
4259            );
4260
4261            extern "C" __global__ void run_once(
4262                unsigned long long handle,
4263                unsigned int *counter
4264            ) {
4265                if (blockIdx.x == 0 && threadIdx.x == 0) {
4266                    *counter += 1;
4267                    cudaGraphSetConditional(handle, 0);
4268                }
4269            }
4270            "#,
4271        )
4272        .expect("compile conditional setter kernel");
4273        let module = context.load_module(ptx).expect("load setter module");
4274        let run_once = module
4275            .load_function("run_once")
4276            .expect("load setter function");
4277        let graph = CapturedCudaGraph::conditional_while_on_stream(&stream, 1, true, |body| {
4278            body.capture_on_stream(&stream, || {
4279                let handle = body.handle();
4280                let mut launch = stream.launch_builder(&run_once);
4281                launch.arg(&handle).arg(&body_ptr);
4282                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4283                    .map(|_| ())
4284                    .map_err(|error| XlogError::Kernel(error.to_string()))
4285            })
4286        });
4287        let graph = match graph {
4288            Ok(graph) => graph,
4289            Err(error) if error.is_unsupported() => {
4290                if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
4291                    panic!("CUDA conditional graphs are required: {error}");
4292                }
4293                eprintln!("Skipping test: {error}");
4294                return;
4295            }
4296            Err(error) => panic!("conditional graph construction failed: {error}"),
4297        };
4298
4299        assert_eq!(graph.node_count().expect("node count"), 1);
4300        assert_eq!(
4301            graph.nodes().expect("nodes")[0].kind,
4302            CudaGraphNodeKind::Conditional
4303        );
4304        graph.launch(&stream).expect("conditional graph launch");
4305        stream.synchronize().expect("conditional graph completion");
4306        let mut observed = [0_u32; 1];
4307        stream
4308            .memcpy_dtoh(&body_buffer, &mut observed)
4309            .expect("read body effect");
4310        assert_eq!(observed, [1], "WHILE body must execute exactly once");
4311    }
4312
4313    #[test]
4314    fn real_conditional_sequence_orders_segments_around_multiple_while_capability() {
4315        let Some(context) = conditional_cuda_context_or_skip() else {
4316            return;
4317        };
4318        let stream = context.new_stream().expect("non-default CUDA stream");
4319        let buffer = stream.alloc_zeros::<u32>(1).expect("sequence buffer");
4320        let (buffer_ptr, _buffer_sync) = buffer.device_ptr(&stream);
4321        let ptx = compile_ptx(
4322            r#"
4323            extern "C" __device__ void cudaGraphSetConditional(
4324                unsigned long long handle,
4325                unsigned int value
4326            );
4327
4328            extern "C" __global__ void add_value(
4329                unsigned int *counter,
4330                unsigned int value
4331            ) {
4332                if (blockIdx.x == 0 && threadIdx.x == 0) *counter += value;
4333            }
4334
4335            extern "C" __global__ void add_once(
4336                unsigned long long handle,
4337                unsigned int *counter
4338            ) {
4339                if (blockIdx.x == 0 && threadIdx.x == 0) {
4340                    *counter += 2;
4341                    cudaGraphSetConditional(handle, 0);
4342                }
4343            }
4344            "#,
4345        )
4346        .expect("compile sequence kernels");
4347        let module = context.load_module(ptx).expect("load sequence module");
4348        let add_value = module
4349            .load_function("add_value")
4350            .expect("load add function");
4351        let add_once = module
4352            .load_function("add_once")
4353            .expect("load conditional function");
4354
4355        let sequence = ConditionalCudaGraphSequenceBuilder::new(&stream).and_then(|mut builder| {
4356            builder.capture_segment_on_stream(&stream, || {
4357                let value = 1_u32;
4358                let mut launch = stream.launch_builder(&add_value);
4359                launch.arg(&buffer_ptr).arg(&value);
4360                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4361                    .map(|_| ())
4362                    .map_err(|error| XlogError::Kernel(error.to_string()))
4363            })?;
4364            builder.add_conditional_while(1, true, |body| {
4365                body.capture_on_stream(&stream, || {
4366                    let handle = body.handle();
4367                    let mut launch = stream.launch_builder(&add_once);
4368                    launch.arg(&handle).arg(&buffer_ptr);
4369                    unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4370                        .map(|_| ())
4371                        .map_err(|error| XlogError::Kernel(error.to_string()))
4372                })
4373            })?;
4374            builder.capture_segment_on_stream(&stream, || {
4375                let value = 4_u32;
4376                let mut launch = stream.launch_builder(&add_value);
4377                launch.arg(&buffer_ptr).arg(&value);
4378                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4379                    .map(|_| ())
4380                    .map_err(|error| XlogError::Kernel(error.to_string()))
4381            })?;
4382            builder.instantiate()
4383        });
4384        let graph = match sequence {
4385            Ok(graph) => graph,
4386            Err(error) if error.is_unsupported() => {
4387                if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
4388                    panic!("CUDA conditional graphs are required: {error}");
4389                }
4390                eprintln!("Skipping test: {error}");
4391                return;
4392            }
4393            Err(error) => panic!("conditional graph sequence construction failed: {error}"),
4394        };
4395        assert_eq!(graph.node_count().expect("sequence node count"), 3);
4396        graph.launch(&stream).expect("sequence graph launch");
4397        stream.synchronize().expect("sequence graph completion");
4398        let mut observed = [0_u32; 1];
4399        stream
4400            .memcpy_dtoh(&buffer, &mut observed)
4401            .expect("read sequence effect");
4402        assert_eq!(observed, [7]);
4403    }
4404
4405    #[test]
4406    #[ignore = "requires explicitly authorized CUDA conditional graph execution"]
4407    fn conditional_if_skips_the_whole_body_and_rechecks_each_replay() {
4408        let context = conditional_cuda_context_or_skip().expect("CUDA context required");
4409        let stream = context.new_stream().expect("non-default CUDA stream");
4410        let mut admission = stream.alloc_zeros::<u32>(1).expect("admission input");
4411        let counters = stream
4412            .alloc_zeros::<u32>(2)
4413            .expect("body and suffix counters");
4414        let (admission_ptr, _admission_sync) = admission.device_ptr(&stream);
4415        let (counters_ptr, _counters_sync) = counters.device_ptr(&stream);
4416        drop(_admission_sync);
4417        let ptx = compile_ptx(
4418            r#"
4419            extern "C" __device__ void cudaGraphSetConditional(
4420                unsigned long long handle, unsigned int value);
4421            extern "C" __global__ void preflight(
4422                unsigned long long handle, const unsigned int *admission) {
4423                if (blockIdx.x == 0 && threadIdx.x == 0)
4424                    cudaGraphSetConditional(handle, *admission);
4425            }
4426            extern "C" __global__ void body(unsigned int *counters) {
4427                if (blockIdx.x == 0 && threadIdx.x == 0) ++counters[0];
4428            }
4429            extern "C" __global__ void suffix(unsigned int *counters) {
4430                if (blockIdx.x == 0 && threadIdx.x == 0) ++counters[1];
4431            }
4432        "#,
4433        )
4434        .expect("compile device admission and body kernels");
4435        let module = context.load_module(ptx).expect("load conditional module");
4436        let preflight = module.load_function("preflight").expect("preflight kernel");
4437        let body_kernel = module.load_function("body").expect("body kernel");
4438        let suffix = module.load_function("suffix").expect("suffix kernel");
4439        let resource = Arc::new(vec![17_u8, 29]);
4440        let retained_resource = Arc::downgrade(&resource);
4441        let mut builder =
4442            ConditionalCudaGraphSequenceBuilder::new_retaining(&stream, vec![resource])
4443                .expect("conditional graph support required");
4444        assert!(retained_resource.upgrade().is_some());
4445        builder
4446            .add_conditional_if(
4447                &stream,
4448                |handle| {
4449                    let mut launch = stream.launch_builder(&preflight);
4450                    launch.arg(&handle).arg(&admission_ptr);
4451                    unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4452                        .map(|_| ())
4453                        .map_err(|error| XlogError::Kernel(error.to_string()))
4454                },
4455                |body| {
4456                    body.capture_on_stream(&stream, || {
4457                        let mut launch = stream.launch_builder(&body_kernel);
4458                        launch.arg(&counters_ptr);
4459                        unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4460                            .map(|_| ())
4461                            .map_err(|error| XlogError::Kernel(error.to_string()))
4462                    })
4463                },
4464            )
4465            .expect("capture guarded body");
4466        builder
4467            .capture_segment_on_stream(&stream, || {
4468                let mut launch = stream.launch_builder(&suffix);
4469                launch.arg(&counters_ptr);
4470                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
4471                    .map(|_| ())
4472                    .map_err(|error| XlogError::Kernel(error.to_string()))
4473            })
4474            .expect("capture dependent suffix");
4475        let graph = builder.instantiate().expect("instantiate guarded sequence");
4476        assert!(retained_resource.upgrade().is_some());
4477        assert_eq!(graph.node_count().expect("parent node count"), 3);
4478        for (allowed, expected) in [(0, [0, 1]), (1, [1, 2]), (0, [1, 3]), (1, [2, 4])] {
4479            // Explicit test input change between completed launches, outside capture.
4480            stream
4481                .memcpy_htod(&[allowed], &mut admission)
4482                .expect("set admission");
4483            graph.launch(&stream).expect("launch guarded sequence");
4484            stream.synchronize().expect("guarded sequence completion");
4485            let mut observed = [0_u32; 2];
4486            stream
4487                .memcpy_dtoh(&counters, &mut observed)
4488                .expect("read execution counters");
4489            assert_eq!(observed, expected);
4490        }
4491    }
4492
4493    #[test]
4494    fn scan_topology_matches_recursive_multiblock_shape() {
4495        assert_eq!(
4496            scan_topology_u32(0),
4497            ScanTopology {
4498                input_len: 0,
4499                block_size: 256,
4500                scratch_lengths: vec![],
4501                kernel_node_count: 0,
4502            }
4503        );
4504        assert_eq!(scan_topology_u32(256).scratch_lengths, Vec::<u32>::new());
4505        assert_eq!(scan_topology_u32(256).kernel_node_count, 1);
4506        assert_eq!(scan_topology_u32(257).scratch_lengths, vec![2]);
4507        assert_eq!(scan_topology_u32(257).kernel_node_count, 3);
4508        assert_eq!(scan_topology_u32(65_537).scratch_lengths, vec![257, 2]);
4509        assert_eq!(scan_topology_u32(65_537).kernel_node_count, 5);
4510    }
4511
4512    #[test]
4513    fn csm_key_uses_capacity_classes_and_layout_version() {
4514        let key = CsmCudaGraphKey::inner(2, 16, 257, 513).expect("key");
4515        assert_eq!(key.join_kind, CsmCudaGraphJoinKind::Inner);
4516        assert_eq!(key.key_arity, 2);
4517        assert_eq!(key.key_bytes, 16);
4518        assert_eq!(key.probe_capacity_class, 512);
4519        assert_eq!(key.output_capacity_class, 1024);
4520        assert_eq!(key.scan_topology.scratch_lengths, vec![2]);
4521        assert_eq!(key.node_layout_version, CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION);
4522    }
4523}