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
8use std::collections::HashSet;
9use std::{
10    fmt, mem, ptr,
11    sync::{Arc, Mutex, OnceLock},
12};
13
14use cudarc::driver::{sys, CudaContext, CudaStream};
15use libloading::Library;
16use xlog_core::{Result, XlogError};
17
18use crate::device_runtime::XlogDeviceRuntime;
19
20pub const CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION: u32 = 1;
21const CONDITIONAL_GRAPH_MINIMUM_DRIVER: i32 = 12_030;
22
23type DriverGetVersionFn = unsafe extern "C" fn(*mut i32) -> sys::CUresult;
24type ConditionalHandleCreateFn = unsafe extern "C" fn(
25    *mut sys::CUgraphConditionalHandle,
26    sys::CUgraph,
27    sys::CUcontext,
28    u32,
29    u32,
30) -> sys::CUresult;
31type GraphAddNodeFn = unsafe extern "C" fn(
32    *mut sys::CUgraphNode,
33    sys::CUgraph,
34    *const sys::CUgraphNode,
35    usize,
36    *mut sys::CUgraphNodeParams,
37) -> sys::CUresult;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum CudaConditionalGraphUnavailable {
41    DriverLibraryUnavailable,
42    MissingDriverSymbol {
43        symbol: &'static str,
44    },
45    DriverVersionQueryFailed {
46        code: sys::CUresult,
47    },
48    DriverVersionTooOld {
49        found: i32,
50        required: i32,
51    },
52    DriverCallFailed {
53        operation: &'static str,
54        code: sys::CUresult,
55    },
56    NullDriverHandle {
57        operation: &'static str,
58    },
59    ContextMismatch,
60    StreamCaptureBusy,
61    BodyPopulationFailed {
62        detail: String,
63    },
64}
65
66impl CudaConditionalGraphUnavailable {
67    pub fn is_unsupported(&self) -> bool {
68        matches!(
69            self,
70            Self::DriverLibraryUnavailable
71                | Self::MissingDriverSymbol { .. }
72                | Self::DriverVersionTooOld { .. }
73                | Self::DriverCallFailed {
74                    code: sys::CUresult::CUDA_ERROR_NOT_SUPPORTED,
75                    ..
76                }
77        )
78    }
79
80    pub fn decline_detail(&self) -> String {
81        match self {
82            Self::DriverLibraryUnavailable => {
83                "CUDA driver library is unavailable for conditional graphs".to_string()
84            }
85            Self::MissingDriverSymbol { symbol } => {
86                format!("CUDA driver is missing required conditional-graph symbol {symbol}")
87            }
88            Self::DriverVersionQueryFailed { code } => {
89                format!("CUDA driver version query failed: {code:?}")
90            }
91            Self::DriverVersionTooOld { found, required } => {
92                format!("CUDA conditional graphs require driver API {required}, found {found}")
93            }
94            Self::DriverCallFailed { operation, code } => {
95                format!("CUDA conditional-graph operation {operation} failed: {code:?}")
96            }
97            Self::NullDriverHandle { operation } => {
98                format!("CUDA conditional-graph operation {operation} returned a null handle")
99            }
100            Self::ContextMismatch => {
101                "CUDA conditional graph and stream belong to different contexts".to_string()
102            }
103            Self::StreamCaptureBusy => {
104                "CUDA stream already has an active graph capture".to_string()
105            }
106            Self::BodyPopulationFailed { detail } => {
107                format!("CUDA conditional graph body population failed: {detail}")
108            }
109        }
110    }
111
112    pub fn body_population(error: impl fmt::Display) -> Self {
113        Self::BodyPopulationFailed {
114            detail: error.to_string(),
115        }
116    }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120struct StreamCaptureKey {
121    context: usize,
122    stream: usize,
123}
124
125static ACTIVE_STREAM_CAPTURES: OnceLock<Mutex<HashSet<StreamCaptureKey>>> = OnceLock::new();
126
127#[derive(Debug)]
128struct StreamCaptureLease {
129    key: StreamCaptureKey,
130}
131
132fn try_acquire_stream_capture_key(
133    key: StreamCaptureKey,
134) -> std::result::Result<StreamCaptureLease, CudaConditionalGraphUnavailable> {
135    let registry = ACTIVE_STREAM_CAPTURES.get_or_init(|| Mutex::new(HashSet::new()));
136    let mut active = registry
137        .lock()
138        .unwrap_or_else(|poisoned| poisoned.into_inner());
139    if !active.insert(key) {
140        return Err(CudaConditionalGraphUnavailable::StreamCaptureBusy);
141    }
142    Ok(StreamCaptureLease { key })
143}
144
145fn try_acquire_stream_capture(
146    stream: &CudaStream,
147) -> std::result::Result<StreamCaptureLease, CudaConditionalGraphUnavailable> {
148    let context =
149        stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
150    try_acquire_stream_capture_key(StreamCaptureKey {
151        context: context as usize,
152        stream: stream.cu_stream() as usize,
153    })
154}
155
156impl Drop for StreamCaptureLease {
157    fn drop(&mut self) {
158        let registry = ACTIVE_STREAM_CAPTURES.get_or_init(|| Mutex::new(HashSet::new()));
159        let mut active = registry
160            .lock()
161            .unwrap_or_else(|poisoned| poisoned.into_inner());
162        active.remove(&self.key);
163    }
164}
165
166impl fmt::Display for CudaConditionalGraphUnavailable {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str(&self.decline_detail())
169    }
170}
171
172impl std::error::Error for CudaConditionalGraphUnavailable {}
173
174impl From<XlogError> for CudaConditionalGraphUnavailable {
175    fn from(error: XlogError) -> Self {
176        Self::body_population(error)
177    }
178}
179
180struct ConditionalGraphDriverApi {
181    _library: Arc<Library>,
182    driver_get_version: DriverGetVersionFn,
183    conditional_handle_create: ConditionalHandleCreateFn,
184    graph_add_node: GraphAddNodeFn,
185}
186
187impl ConditionalGraphDriverApi {
188    fn load() -> std::result::Result<Arc<Self>, CudaConditionalGraphUnavailable> {
189        #[cfg(target_os = "windows")]
190        const CUDA_DRIVER_LIBRARY: &str = "nvcuda.dll";
191        #[cfg(not(target_os = "windows"))]
192        const CUDA_DRIVER_LIBRARY: &str = "libcuda.so.1";
193
194        let library = Arc::new(
195            unsafe { Library::new(CUDA_DRIVER_LIBRARY) }
196                .map_err(|_| CudaConditionalGraphUnavailable::DriverLibraryUnavailable)?,
197        );
198        let driver_get_version = unsafe {
199            load_required_symbol(&library, b"cuDriverGetVersion\0", "cuDriverGetVersion")?
200        };
201        let conditional_handle_create = unsafe {
202            load_required_symbol(
203                &library,
204                b"cuGraphConditionalHandleCreate\0",
205                "cuGraphConditionalHandleCreate",
206            )?
207        };
208        let graph_add_node =
209            unsafe { load_required_symbol(&library, b"cuGraphAddNode\0", "cuGraphAddNode")? };
210
211        let api = Arc::new(Self {
212            _library: library,
213            driver_get_version,
214            conditional_handle_create,
215            graph_add_node,
216        });
217        api.require_supported_driver()?;
218        Ok(api)
219    }
220
221    fn require_supported_driver(&self) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
222        let mut version = 0;
223        let code = unsafe { (self.driver_get_version)(&mut version) };
224        if code != sys::CUresult::CUDA_SUCCESS {
225            return Err(CudaConditionalGraphUnavailable::DriverVersionQueryFailed { code });
226        }
227        require_conditional_graph_driver(version)
228    }
229}
230
231unsafe fn load_required_symbol<F: Copy>(
232    library: &Library,
233    name: &'static [u8],
234    display_name: &'static str,
235) -> std::result::Result<F, CudaConditionalGraphUnavailable> {
236    library.get::<F>(name).map(|symbol| *symbol).map_err(|_| {
237        CudaConditionalGraphUnavailable::MissingDriverSymbol {
238            symbol: display_name,
239        }
240    })
241}
242
243fn require_conditional_graph_driver(
244    version: i32,
245) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
246    if version < CONDITIONAL_GRAPH_MINIMUM_DRIVER {
247        Err(CudaConditionalGraphUnavailable::DriverVersionTooOld {
248            found: version,
249            required: CONDITIONAL_GRAPH_MINIMUM_DRIVER,
250        })
251    } else {
252        Ok(())
253    }
254}
255
256fn conditional_while_node_params(
257    handle: sys::CUgraphConditionalHandle,
258    ctx: sys::CUcontext,
259) -> sys::CUgraphNodeParams {
260    let mut params: sys::CUgraphNodeParams = unsafe { mem::zeroed() };
261    params.type_ = sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL;
262    params.__bindgen_anon_1.conditional = sys::CUDA_CONDITIONAL_NODE_PARAMS {
263        handle,
264        type_: sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE,
265        size: 1,
266        phGraph_out: ptr::null_mut(),
267        ctx,
268    };
269    params
270}
271
272fn conditional_driver_call(
273    operation: &'static str,
274    code: sys::CUresult,
275) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
276    if code == sys::CUresult::CUDA_SUCCESS {
277        Ok(())
278    } else {
279        Err(CudaConditionalGraphUnavailable::DriverCallFailed { operation, code })
280    }
281}
282
283struct UninstantiatedCudaGraph {
284    raw: sys::CUgraph,
285    context: Arc<CudaContext>,
286}
287
288impl UninstantiatedCudaGraph {
289    fn create(
290        context: Arc<CudaContext>,
291    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
292        context
293            .bind_to_thread()
294            .map_err(CudaConditionalGraphUnavailable::body_population)?;
295        let mut raw = ptr::null_mut();
296        unsafe {
297            conditional_driver_call("cuGraphCreate", sys::cuGraphCreate(&mut raw, 0))?;
298        }
299        if raw.is_null() {
300            Err(CudaConditionalGraphUnavailable::NullDriverHandle {
301                operation: "cuGraphCreate",
302            })
303        } else {
304            Ok(Self { raw, context })
305        }
306    }
307
308    fn raw(&self) -> sys::CUgraph {
309        self.raw
310    }
311
312    fn into_raw(mut self) -> sys::CUgraph {
313        let raw = self.raw;
314        self.raw = ptr::null_mut();
315        raw
316    }
317}
318
319impl Drop for UninstantiatedCudaGraph {
320    fn drop(&mut self) {
321        if !self.raw.is_null() {
322            let _ = self.context.bind_to_thread();
323            unsafe {
324                let _ = sys::cuGraphDestroy(self.raw);
325            }
326        }
327    }
328}
329
330/// CUDA-owned body graph produced while adding one conditional-WHILE node.
331///
332/// The body graph and conditional handle are owned by the parent graph. They
333/// must not be destroyed independently. The numeric handle is intended to be
334/// passed by value to a device kernel that calls `cudaGraphSetConditional`.
335#[derive(Debug)]
336pub struct ConditionalCudaGraphBody {
337    graph: sys::CUgraph,
338    handle: sys::CUgraphConditionalHandle,
339    context: sys::CUcontext,
340}
341
342impl ConditionalCudaGraphBody {
343    pub fn graph(&self) -> sys::CUgraph {
344        self.graph
345    }
346
347    pub fn handle(&self) -> sys::CUgraphConditionalHandle {
348        self.handle
349    }
350
351    pub fn context(&self) -> sys::CUcontext {
352        self.context
353    }
354
355    /// Return this body's actual node kinds in dependency-chain order.
356    ///
357    /// The body must be a single linear dependency chain. CUDA's node-list
358    /// enumeration order is not used as an execution-order signal.
359    pub fn linear_chain_node_kinds(
360        &self,
361    ) -> std::result::Result<Vec<CudaGraphNodeKind>, CudaConditionalGraphUnavailable> {
362        let mut check = |operation, code| conditional_driver_call(operation, code);
363        let mut shape_error = |error| CudaConditionalGraphUnavailable::BodyPopulationFailed {
364            detail: format!("conditional graph body is not a linear dependency chain: {error}"),
365        };
366        graph_linear_chain_node_kinds_with(self.graph, &mut check, &mut shape_error)
367    }
368
369    /// Capture graph-compatible work directly into this conditional body.
370    ///
371    /// `stream` must be a non-default stream. The callback must not allocate,
372    /// synchronize, or record/wait on events.
373    /// CUDA conditional bodies allow only kernel, empty, child-graph, device
374    /// memcpy/memset, and nested conditional nodes.
375    pub fn capture_on_stream<F, E>(
376        &self,
377        stream: &CudaStream,
378        record: F,
379    ) -> std::result::Result<(), CudaConditionalGraphUnavailable>
380    where
381        F: FnOnce() -> std::result::Result<(), E>,
382        E: fmt::Display,
383    {
384        let stream_ctx =
385            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
386        if stream_ctx != self.context {
387            return Err(CudaConditionalGraphUnavailable::ContextMismatch);
388        }
389        let _capture_lease = try_acquire_stream_capture(stream)?;
390
391        unsafe {
392            conditional_driver_call(
393                "cuStreamBeginCaptureToGraph",
394                sys::cuStreamBeginCaptureToGraph(
395                    stream.cu_stream(),
396                    self.graph,
397                    ptr::null(),
398                    ptr::null(),
399                    0,
400                    sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
401                ),
402            )?;
403        }
404
405        let record_result = record();
406        let mut captured = ptr::null_mut();
407        let end_result = unsafe {
408            conditional_driver_call(
409                "cuStreamEndCapture",
410                sys::cuStreamEndCapture(stream.cu_stream(), &mut captured),
411            )
412        };
413        if let Err(error) = record_result {
414            return Err(CudaConditionalGraphUnavailable::body_population(error));
415        }
416        end_result?;
417        if captured.is_null() {
418            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
419                operation: "cuStreamEndCapture",
420            });
421        }
422        if captured != self.graph {
423            return Err(CudaConditionalGraphUnavailable::BodyPopulationFailed {
424                detail: "capture returned a graph other than the conditional body".to_string(),
425            });
426        }
427        Ok(())
428    }
429}
430
431/// Builds one dependency-ordered parent graph containing ordinary captured
432/// segments and any number of conditional-WHILE nodes.
433///
434/// This is the topology required by stratified Datalog: work before a
435/// recursive strongly connected component executes once, only that component
436/// is placed in a device-controlled WHILE, and later strata depend on its
437/// completion. The finished value still launches through one `cuGraphLaunch`.
438pub struct ConditionalCudaGraphSequenceBuilder {
439    graph: UninstantiatedCudaGraph,
440    context: Arc<CudaContext>,
441    raw_context: sys::CUcontext,
442    api: Arc<ConditionalGraphDriverApi>,
443    frontier: Vec<sys::CUgraphNode>,
444}
445
446impl ConditionalCudaGraphSequenceBuilder {
447    /// Create an empty parent graph bound to `stream`'s CUDA context.
448    pub fn new(stream: &CudaStream) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
449        let api = ConditionalGraphDriverApi::load()?;
450        let context = stream.context().clone();
451        let raw_context =
452            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
453        if raw_context != context.cu_ctx() {
454            return Err(CudaConditionalGraphUnavailable::ContextMismatch);
455        }
456        Ok(Self {
457            graph: UninstantiatedCudaGraph::create(Arc::clone(&context))?,
458            context,
459            raw_context,
460            api,
461            frontier: Vec::new(),
462        })
463    }
464
465    /// Capture one ordinary graph segment after the current dependency
466    /// frontier. The callback may enqueue only capture-compatible operations.
467    pub fn capture_segment_on_stream<F, E>(
468        &mut self,
469        stream: &CudaStream,
470        record: F,
471    ) -> std::result::Result<(), CudaConditionalGraphUnavailable>
472    where
473        F: FnOnce() -> std::result::Result<(), E>,
474        E: fmt::Display,
475    {
476        self.ensure_stream_context(stream)?;
477        let _capture_lease = try_acquire_stream_capture(stream)?;
478        unsafe {
479            conditional_driver_call(
480                "cuStreamBeginCaptureToGraph",
481                sys::cuStreamBeginCaptureToGraph(
482                    stream.cu_stream(),
483                    self.graph.raw(),
484                    if self.frontier.is_empty() {
485                        ptr::null()
486                    } else {
487                        self.frontier.as_ptr()
488                    },
489                    ptr::null(),
490                    self.frontier.len(),
491                    sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
492                ),
493            )?;
494        }
495
496        let record_result = record();
497        let mut captured = ptr::null_mut();
498        let end_result = unsafe {
499            conditional_driver_call(
500                "cuStreamEndCapture",
501                sys::cuStreamEndCapture(stream.cu_stream(), &mut captured),
502            )
503        };
504        if let Err(error) = record_result {
505            return Err(CudaConditionalGraphUnavailable::body_population(error));
506        }
507        end_result?;
508        if captured != self.graph.raw() {
509            return Err(CudaConditionalGraphUnavailable::BodyPopulationFailed {
510                detail: "segment capture returned a graph other than its parent".to_string(),
511            });
512        }
513        self.frontier = graph_leaf_nodes(self.graph.raw())?;
514        Ok(())
515    }
516
517    /// Append one conditional-WHILE node after the current frontier.
518    pub fn add_conditional_while<F>(
519        &mut self,
520        initial_value: u32,
521        assign_default_on_launch: bool,
522        populate_body: F,
523    ) -> std::result::Result<sys::CUgraphConditionalHandle, CudaConditionalGraphUnavailable>
524    where
525        F: FnOnce(
526            ConditionalCudaGraphBody,
527        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
528    {
529        let mut handle = 0;
530        let flags = if assign_default_on_launch {
531            sys::CU_GRAPH_COND_ASSIGN_DEFAULT
532        } else {
533            0
534        };
535        unsafe {
536            conditional_driver_call(
537                "cuGraphConditionalHandleCreate",
538                (self.api.conditional_handle_create)(
539                    &mut handle,
540                    self.graph.raw(),
541                    self.raw_context,
542                    initial_value,
543                    flags,
544                ),
545            )?;
546        }
547
548        let mut params = conditional_while_node_params(handle, self.raw_context);
549        let mut conditional_node = ptr::null_mut();
550        unsafe {
551            conditional_driver_call(
552                "cuGraphAddNode",
553                (self.api.graph_add_node)(
554                    &mut conditional_node,
555                    self.graph.raw(),
556                    if self.frontier.is_empty() {
557                        ptr::null()
558                    } else {
559                        self.frontier.as_ptr()
560                    },
561                    self.frontier.len(),
562                    &mut params,
563                ),
564            )?;
565        }
566        if conditional_node.is_null() {
567            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
568                operation: "cuGraphAddNode",
569            });
570        }
571        let conditional = unsafe { params.__bindgen_anon_1.conditional };
572        if conditional.phGraph_out.is_null() {
573            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
574                operation: "cuGraphAddNode body array",
575            });
576        }
577        let body_graph = unsafe { *conditional.phGraph_out };
578        if body_graph.is_null() {
579            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
580                operation: "cuGraphAddNode WHILE body",
581            });
582        }
583        populate_body(ConditionalCudaGraphBody {
584            graph: body_graph,
585            handle,
586            context: self.raw_context,
587        })?;
588        self.frontier.clear();
589        self.frontier.push(conditional_node);
590        Ok(handle)
591    }
592
593    /// Instantiate the complete parent graph exactly once.
594    pub fn instantiate(
595        self,
596    ) -> std::result::Result<CapturedCudaGraph, CudaConditionalGraphUnavailable> {
597        let ConditionalCudaGraphSequenceBuilder {
598            graph,
599            context,
600            api,
601            ..
602        } = self;
603        let mut captured =
604            unsafe { CapturedCudaGraph::instantiate_owned_graph(graph.into_raw(), context)? };
605        captured._conditional_api = Some(api);
606        Ok(captured)
607    }
608
609    fn ensure_stream_context(
610        &self,
611        stream: &CudaStream,
612    ) -> std::result::Result<(), CudaConditionalGraphUnavailable> {
613        let context =
614            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
615        if context == self.raw_context {
616            Ok(())
617        } else {
618            Err(CudaConditionalGraphUnavailable::ContextMismatch)
619        }
620    }
621}
622
623fn raw_graph_nodes_with<E>(
624    graph: sys::CUgraph,
625    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
626) -> std::result::Result<Vec<sys::CUgraphNode>, E> {
627    let mut node_count = 0usize;
628    unsafe {
629        check(
630            "cuGraphGetNodes(count)",
631            sys::cuGraphGetNodes(graph, ptr::null_mut(), &mut node_count),
632        )?;
633    }
634    let mut nodes = vec![ptr::null_mut(); node_count];
635    if node_count != 0 {
636        unsafe {
637            check(
638                "cuGraphGetNodes(nodes)",
639                sys::cuGraphGetNodes(graph, nodes.as_mut_ptr(), &mut node_count),
640            )?;
641        }
642        nodes.truncate(node_count);
643    }
644    Ok(nodes)
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
648enum LinearGraphChainError {
649    ForeignDependency { node: usize, dependency: usize },
650    DuplicateDependency { node: usize, dependency: usize },
651    Cycle,
652    RootCount { found: usize },
653    IncomingDegree { node: usize, dependencies: usize },
654    Branch { node: usize, dependents: usize },
655    LeafCount { found: usize },
656    Disconnected { visited: usize, total: usize },
657}
658
659impl fmt::Display for LinearGraphChainError {
660    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
661        match self {
662            Self::ForeignDependency { node, dependency } => write!(
663                formatter,
664                "node {node} depends on foreign enumeration index {dependency}"
665            ),
666            Self::DuplicateDependency { node, dependency } => write!(
667                formatter,
668                "node {node} repeats dependency enumeration index {dependency}"
669            ),
670            Self::Cycle => formatter.write_str("dependency graph contains a cycle"),
671            Self::RootCount { found } => {
672                write!(
673                    formatter,
674                    "dependency graph has {found} roots instead of one"
675                )
676            }
677            Self::IncomingDegree { node, dependencies } => write!(
678                formatter,
679                "non-root node {node} has {dependencies} immediate dependencies instead of one"
680            ),
681            Self::Branch { node, dependents } => write!(
682                formatter,
683                "node {node} has {dependents} immediate dependents instead of at most one"
684            ),
685            Self::LeafCount { found } => {
686                write!(
687                    formatter,
688                    "dependency graph has {found} leaves instead of one"
689                )
690            }
691            Self::Disconnected { visited, total } => write!(
692                formatter,
693                "dependency chain visits {visited} of {total} enumerated nodes"
694            ),
695        }
696    }
697}
698
699fn linear_chain_order(
700    immediate_dependencies: &[Vec<usize>],
701) -> std::result::Result<Vec<usize>, LinearGraphChainError> {
702    let node_count = immediate_dependencies.len();
703    let mut outgoing = vec![Vec::new(); node_count];
704    for (node, dependencies) in immediate_dependencies.iter().enumerate() {
705        let mut unique = HashSet::with_capacity(dependencies.len());
706        for &dependency in dependencies {
707            if dependency >= node_count {
708                return Err(LinearGraphChainError::ForeignDependency { node, dependency });
709            }
710            if !unique.insert(dependency) {
711                return Err(LinearGraphChainError::DuplicateDependency { node, dependency });
712            }
713            outgoing[dependency].push(node);
714        }
715    }
716
717    let mut remaining_indegree = immediate_dependencies
718        .iter()
719        .map(Vec::len)
720        .collect::<Vec<_>>();
721    let mut ready = remaining_indegree
722        .iter()
723        .enumerate()
724        .filter_map(|(node, &degree)| (degree == 0).then_some(node))
725        .collect::<Vec<_>>();
726    let mut acyclic_nodes = 0usize;
727    while let Some(node) = ready.pop() {
728        acyclic_nodes += 1;
729        for &dependent in &outgoing[node] {
730            remaining_indegree[dependent] -= 1;
731            if remaining_indegree[dependent] == 0 {
732                ready.push(dependent);
733            }
734        }
735    }
736    if acyclic_nodes != node_count {
737        return Err(LinearGraphChainError::Cycle);
738    }
739
740    let roots = immediate_dependencies
741        .iter()
742        .enumerate()
743        .filter_map(|(node, dependencies)| dependencies.is_empty().then_some(node))
744        .collect::<Vec<_>>();
745    if roots.len() != 1 {
746        return Err(LinearGraphChainError::RootCount { found: roots.len() });
747    }
748    let root = roots[0];
749    for (node, dependencies) in immediate_dependencies.iter().enumerate() {
750        if node != root && dependencies.len() != 1 {
751            return Err(LinearGraphChainError::IncomingDegree {
752                node,
753                dependencies: dependencies.len(),
754            });
755        }
756    }
757    for (node, dependents) in outgoing.iter().enumerate() {
758        if dependents.len() > 1 {
759            return Err(LinearGraphChainError::Branch {
760                node,
761                dependents: dependents.len(),
762            });
763        }
764    }
765    let leaf_count = outgoing
766        .iter()
767        .filter(|dependents| dependents.is_empty())
768        .count();
769    if leaf_count != 1 {
770        return Err(LinearGraphChainError::LeafCount { found: leaf_count });
771    }
772
773    let mut order = Vec::with_capacity(node_count);
774    let mut visited = vec![false; node_count];
775    let mut current = Some(root);
776    while let Some(node) = current {
777        if visited[node] {
778            return Err(LinearGraphChainError::Cycle);
779        }
780        visited[node] = true;
781        order.push(node);
782        current = outgoing[node].first().copied();
783    }
784    if order.len() != node_count {
785        return Err(LinearGraphChainError::Disconnected {
786            visited: order.len(),
787            total: node_count,
788        });
789    }
790    Ok(order)
791}
792
793fn raw_node_dependencies_with<E>(
794    node: sys::CUgraphNode,
795    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
796) -> std::result::Result<Vec<sys::CUgraphNode>, E> {
797    let mut dependency_count = 0usize;
798    unsafe {
799        check(
800            "cuGraphNodeGetDependencies(count)",
801            sys::cuGraphNodeGetDependencies(node, ptr::null_mut(), &mut dependency_count),
802        )?;
803    }
804    let mut dependencies = vec![ptr::null_mut(); dependency_count];
805    if dependency_count != 0 {
806        unsafe {
807            check(
808                "cuGraphNodeGetDependencies(nodes)",
809                sys::cuGraphNodeGetDependencies(
810                    node,
811                    dependencies.as_mut_ptr(),
812                    &mut dependency_count,
813                ),
814            )?;
815        }
816        dependencies.truncate(dependency_count);
817    }
818    Ok(dependencies)
819}
820
821fn graph_nodes_with<E>(
822    graph: sys::CUgraph,
823    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
824) -> std::result::Result<Vec<CudaGraphNode>, E> {
825    let raw_nodes = raw_graph_nodes_with(graph, check)?;
826    let mut nodes = Vec::with_capacity(raw_nodes.len());
827    for (index, raw) in raw_nodes.into_iter().enumerate() {
828        let mut ty = sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EMPTY;
829        unsafe {
830            check("cuGraphNodeGetType", sys::cuGraphNodeGetType(raw, &mut ty))?;
831        }
832        nodes.push(CudaGraphNode {
833            index,
834            raw,
835            kind: CudaGraphNodeKind::from_sys(ty),
836        });
837    }
838    Ok(nodes)
839}
840
841fn graph_linear_chain_node_kinds_with<E>(
842    graph: sys::CUgraph,
843    check: &mut impl FnMut(&'static str, sys::CUresult) -> std::result::Result<(), E>,
844    shape_error: &mut impl FnMut(LinearGraphChainError) -> E,
845) -> std::result::Result<Vec<CudaGraphNodeKind>, E> {
846    let nodes = graph_nodes_with(graph, check)?;
847    let mut immediate_dependencies = Vec::with_capacity(nodes.len());
848    for (node_index, node) in nodes.iter().enumerate() {
849        let raw_dependencies = raw_node_dependencies_with(node.raw, check)?;
850        let mut dependency_indices = Vec::with_capacity(raw_dependencies.len());
851        for dependency in raw_dependencies {
852            let Some(dependency_index) = nodes
853                .iter()
854                .position(|candidate| candidate.raw == dependency)
855            else {
856                return Err(shape_error(LinearGraphChainError::ForeignDependency {
857                    node: node_index,
858                    dependency: nodes.len(),
859                }));
860            };
861            dependency_indices.push(dependency_index);
862        }
863        immediate_dependencies.push(dependency_indices);
864    }
865    let order = linear_chain_order(&immediate_dependencies).map_err(shape_error)?;
866    Ok(order.into_iter().map(|index| nodes[index].kind).collect())
867}
868
869fn graph_leaf_nodes(
870    graph: sys::CUgraph,
871) -> std::result::Result<Vec<sys::CUgraphNode>, CudaConditionalGraphUnavailable> {
872    let mut check = |_, code| conditional_driver_call("cuGraphGetNodes", code);
873    let mut nodes = raw_graph_nodes_with(graph, &mut check)?;
874
875    let mut edge_count = 0usize;
876    unsafe {
877        conditional_driver_call(
878            "cuGraphGetEdges",
879            sys::cuGraphGetEdges(graph, ptr::null_mut(), ptr::null_mut(), &mut edge_count),
880        )?;
881    }
882    let mut from = vec![ptr::null_mut(); edge_count];
883    let mut to = vec![ptr::null_mut(); edge_count];
884    if edge_count != 0 {
885        unsafe {
886            conditional_driver_call(
887                "cuGraphGetEdges",
888                sys::cuGraphGetEdges(graph, from.as_mut_ptr(), to.as_mut_ptr(), &mut edge_count),
889            )?;
890        }
891        from.truncate(edge_count);
892    }
893    nodes.retain(|node| !from.contains(node));
894    Ok(nodes)
895}
896
897/// Instantiated CUDA Graph with owned graph + exec handles.
898pub struct CapturedCudaGraph {
899    graph: sys::CUgraph,
900    exec: sys::CUgraphExec,
901    context: Arc<CudaContext>,
902    _conditional_api: Option<Arc<ConditionalGraphDriverApi>>,
903    _resident_lifecycle_lease: Option<Box<dyn Send + Sync>>,
904}
905
906// CUDA graph handles are context-owned driver handles. xlog stores them behind
907// provider-level synchronization when caching graph executions.
908unsafe impl Send for CapturedCudaGraph {}
909unsafe impl Sync for CapturedCudaGraph {}
910
911#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912pub enum CudaGraphNodeKind {
913    Kernel,
914    Memcpy,
915    Memset,
916    Host,
917    Graph,
918    Empty,
919    WaitEvent,
920    EventRecord,
921    ExternalSemaphoresSignal,
922    ExternalSemaphoresWait,
923    MemAlloc,
924    MemFree,
925    BatchMemOp,
926    Conditional,
927}
928
929#[derive(Debug, Clone, Copy)]
930pub struct CudaGraphNode {
931    pub index: usize,
932    pub raw: sys::CUgraphNode,
933    pub kind: CudaGraphNodeKind,
934}
935
936unsafe impl Send for CudaGraphNode {}
937unsafe impl Sync for CudaGraphNode {}
938
939#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
940pub enum CsmCudaGraphJoinKind {
941    Inner,
942    IndexedInner,
943}
944
945#[derive(Debug, Clone, PartialEq, Eq, Hash)]
946pub struct ScanTopology {
947    pub input_len: u32,
948    pub block_size: u32,
949    pub scratch_lengths: Vec<u32>,
950    pub kernel_node_count: usize,
951}
952
953#[derive(Debug, Clone, PartialEq, Eq, Hash)]
954pub struct CsmCudaGraphKey {
955    pub join_kind: CsmCudaGraphJoinKind,
956    pub key_arity: u8,
957    pub key_bytes: u32,
958    pub probe_capacity_class: u32,
959    pub output_capacity_class: u32,
960    pub scan_topology: ScanTopology,
961    pub node_layout_version: u32,
962}
963
964impl CsmCudaGraphKey {
965    pub fn inner(
966        key_arity: usize,
967        key_bytes: u32,
968        probe_capacity: u32,
969        output_capacity: u32,
970    ) -> Result<Self> {
971        let key_arity = u8::try_from(key_arity).map_err(|_| {
972            XlogError::Kernel(format!(
973                "CSM CUDA Graph key arity {} exceeds u8::MAX",
974                key_arity
975            ))
976        })?;
977        Ok(Self {
978            join_kind: CsmCudaGraphJoinKind::Inner,
979            key_arity,
980            key_bytes,
981            probe_capacity_class: graph_capacity_class_u32(probe_capacity),
982            output_capacity_class: graph_capacity_class_u32(output_capacity),
983            scan_topology: scan_topology_u32(probe_capacity),
984            node_layout_version: CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION,
985        })
986    }
987}
988
989pub fn graph_capacity_class_u32(n: u32) -> u32 {
990    if n <= 1 {
991        1
992    } else {
993        n.checked_next_power_of_two().unwrap_or(u32::MAX)
994    }
995}
996
997pub fn scan_topology_u32(mut n: u32) -> ScanTopology {
998    let input_len = n;
999    let block_size = 256u32;
1000    let mut scratch_lengths = Vec::new();
1001    let mut kernel_node_count = if n == 0 { 0 } else { 1 };
1002    while n > block_size {
1003        let num_blocks = n.div_ceil(block_size);
1004        scratch_lengths.push(num_blocks);
1005        kernel_node_count += 2;
1006        n = num_blocks;
1007    }
1008    ScanTopology {
1009        input_len,
1010        block_size,
1011        scratch_lengths,
1012        kernel_node_count,
1013    }
1014}
1015
1016impl CapturedCudaGraph {
1017    /// Tie resident lifecycle accounting to this real graph/exec owner.
1018    ///
1019    /// Binding is idempotent. The lease is created only after graph
1020    /// instantiation has succeeded and is dropped after this type's `Drop`
1021    /// implementation destroys the executable and parent graph handles.
1022    pub fn bind_resident_lifecycle(mut self, runtime: &XlogDeviceRuntime) -> Self {
1023        if self._resident_lifecycle_lease.is_none() {
1024            self._resident_lifecycle_lease = Some(Box::new(runtime.resident_graph_handle_lease()));
1025        }
1026        self
1027    }
1028
1029    /// Create, populate, and instantiate a parent graph containing exactly one
1030    /// root conditional-WHILE node.
1031    ///
1032    /// The handle and body passed to `populate_body` remain valid until this
1033    /// `CapturedCudaGraph` is dropped. CUDA permits only one live executable
1034    /// instantiation of a graph containing a conditional node.
1035    pub fn conditional_while_on_stream<F>(
1036        stream: &CudaStream,
1037        initial_value: u32,
1038        assign_default_on_launch: bool,
1039        populate_body: F,
1040    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable>
1041    where
1042        F: FnOnce(
1043            ConditionalCudaGraphBody,
1044        ) -> std::result::Result<(), CudaConditionalGraphUnavailable>,
1045    {
1046        let api = ConditionalGraphDriverApi::load()?;
1047        let context = stream.context().clone();
1048        let raw_context =
1049            stream_context(stream).map_err(CudaConditionalGraphUnavailable::body_population)?;
1050        if raw_context != context.cu_ctx() {
1051            return Err(CudaConditionalGraphUnavailable::ContextMismatch);
1052        }
1053
1054        let graph = UninstantiatedCudaGraph::create(Arc::clone(&context))?;
1055        let mut handle = 0;
1056        let flags = if assign_default_on_launch {
1057            sys::CU_GRAPH_COND_ASSIGN_DEFAULT
1058        } else {
1059            0
1060        };
1061        unsafe {
1062            conditional_driver_call(
1063                "cuGraphConditionalHandleCreate",
1064                (api.conditional_handle_create)(
1065                    &mut handle,
1066                    graph.raw(),
1067                    raw_context,
1068                    initial_value,
1069                    flags,
1070                ),
1071            )?;
1072        }
1073
1074        let mut params = conditional_while_node_params(handle, raw_context);
1075        let mut conditional_node = ptr::null_mut();
1076        unsafe {
1077            conditional_driver_call(
1078                "cuGraphAddNode",
1079                (api.graph_add_node)(
1080                    &mut conditional_node,
1081                    graph.raw(),
1082                    ptr::null(),
1083                    0,
1084                    &mut params,
1085                ),
1086            )?;
1087        }
1088        if conditional_node.is_null() {
1089            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1090                operation: "cuGraphAddNode",
1091            });
1092        }
1093
1094        let conditional = unsafe { params.__bindgen_anon_1.conditional };
1095        if conditional.phGraph_out.is_null() {
1096            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1097                operation: "cuGraphAddNode body array",
1098            });
1099        }
1100        let body_graph = unsafe { *conditional.phGraph_out };
1101        if body_graph.is_null() {
1102            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1103                operation: "cuGraphAddNode WHILE body",
1104            });
1105        }
1106        populate_body(ConditionalCudaGraphBody {
1107            graph: body_graph,
1108            handle,
1109            context: raw_context,
1110        })?;
1111
1112        let mut instantiated = unsafe { Self::instantiate_owned_graph(graph.into_raw(), context)? };
1113        instantiated._conditional_api = Some(api);
1114        Ok(instantiated)
1115    }
1116
1117    /// Instantiate and assume sole ownership of `graph`.
1118    ///
1119    /// The graph is destroyed on instantiation failure. On success, this value
1120    /// destroys the executable first and the source graph second.
1121    ///
1122    /// # Safety
1123    /// `graph` must be a valid, unowned graph in `context`, and no other owner
1124    /// may destroy or instantiate it while this value exists.
1125    pub unsafe fn instantiate_owned_graph(
1126        graph: sys::CUgraph,
1127        context: Arc<CudaContext>,
1128    ) -> std::result::Result<Self, CudaConditionalGraphUnavailable> {
1129        if graph.is_null() {
1130            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1131                operation: "instantiate_owned_graph",
1132            });
1133        }
1134        let owned_graph = UninstantiatedCudaGraph {
1135            raw: graph,
1136            context: Arc::clone(&context),
1137        };
1138        context
1139            .bind_to_thread()
1140            .map_err(CudaConditionalGraphUnavailable::body_population)?;
1141        let mut exec = ptr::null_mut();
1142        conditional_driver_call(
1143            "cuGraphInstantiateWithFlags",
1144            sys::cuGraphInstantiateWithFlags(&mut exec, owned_graph.raw(), 0),
1145        )?;
1146        if exec.is_null() {
1147            return Err(CudaConditionalGraphUnavailable::NullDriverHandle {
1148                operation: "cuGraphInstantiateWithFlags",
1149            });
1150        }
1151        Ok(Self {
1152            graph: owned_graph.into_raw(),
1153            exec,
1154            context,
1155            _conditional_api: None,
1156            _resident_lifecycle_lease: None,
1157        })
1158    }
1159
1160    /// Capture work submitted by `record` on `stream`, instantiate it, and take
1161    /// ownership of the resulting graph handles.
1162    pub fn capture_on_stream<F>(stream: &CudaStream, record: F) -> Result<Self>
1163    where
1164        F: FnOnce() -> Result<()>,
1165    {
1166        let _capture_lease = try_acquire_stream_capture(stream)
1167            .map_err(|error| XlogError::Kernel(error.decline_detail()))?;
1168        unsafe {
1169            cuda_graph_check(
1170                "cuStreamBeginCapture_v2",
1171                sys::cuStreamBeginCapture_v2(
1172                    stream.cu_stream(),
1173                    sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_THREAD_LOCAL,
1174                ),
1175            )?;
1176        }
1177
1178        let record_result = record();
1179        let mut graph: sys::CUgraph = ptr::null_mut();
1180        let end_result = unsafe {
1181            cuda_graph_check(
1182                "cuStreamEndCapture",
1183                sys::cuStreamEndCapture(stream.cu_stream(), &mut graph),
1184            )
1185        };
1186
1187        if let Err(record_err) = record_result {
1188            if end_result.is_ok() && !graph.is_null() {
1189                unsafe {
1190                    let _ = sys::cuGraphDestroy(graph);
1191                }
1192            }
1193            return Err(record_err);
1194        }
1195        end_result?;
1196        if graph.is_null() {
1197            return Err(XlogError::Kernel(
1198                "cuStreamEndCapture returned a null CUDA graph".to_string(),
1199            ));
1200        }
1201
1202        let mut exec: sys::CUgraphExec = ptr::null_mut();
1203        unsafe {
1204            if let Err(err) = cuda_graph_check(
1205                "cuGraphInstantiateWithFlags",
1206                sys::cuGraphInstantiateWithFlags(&mut exec, graph, 0),
1207            ) {
1208                let _ = sys::cuGraphDestroy(graph);
1209                return Err(err);
1210            }
1211        }
1212        if exec.is_null() {
1213            unsafe {
1214                let _ = sys::cuGraphDestroy(graph);
1215            }
1216            return Err(XlogError::Kernel(
1217                "cuGraphInstantiateWithFlags returned a null CUDA graph exec".to_string(),
1218            ));
1219        }
1220
1221        Ok(Self {
1222            graph,
1223            exec,
1224            context: stream.context().clone(),
1225            _conditional_api: None,
1226            _resident_lifecycle_lease: None,
1227        })
1228    }
1229
1230    /// Replay the instantiated graph on `stream`.
1231    pub fn launch(&self, stream: &CudaStream) -> Result<()> {
1232        if stream.context().cu_ctx() != self.context.cu_ctx() {
1233            return Err(XlogError::Kernel(
1234                CudaConditionalGraphUnavailable::ContextMismatch.decline_detail(),
1235            ));
1236        }
1237        unsafe {
1238            cuda_graph_check(
1239                "cuGraphLaunch",
1240                sys::cuGraphLaunch(self.exec, stream.cu_stream()),
1241            )
1242        }
1243    }
1244
1245    /// Number of nodes in the captured graph. Used by bounded CSM CUDA Graph
1246    /// cache-key and node-inventory certs to prove topology stability.
1247    pub fn node_count(&self) -> Result<usize> {
1248        let mut count = 0usize;
1249        unsafe {
1250            cuda_graph_check(
1251                "cuGraphGetNodes(count)",
1252                sys::cuGraphGetNodes(self.graph, ptr::null_mut(), &mut count),
1253            )?;
1254        }
1255        Ok(count)
1256    }
1257
1258    /// Return graph nodes in CUDA's enumeration order with their node type.
1259    ///
1260    /// CUDA does not define this list as dependency or execution order. Use
1261    /// [`Self::linear_chain_node_kinds`] when a linear topology is required.
1262    pub fn nodes(&self) -> Result<Vec<CudaGraphNode>> {
1263        let mut check = |operation, code| cuda_graph_check(operation, code);
1264        graph_nodes_with(self.graph, &mut check)
1265    }
1266
1267    /// Return actual node kinds in root-to-leaf dependency order.
1268    ///
1269    /// This fails unless the graph is one connected linear chain with exactly
1270    /// one root, one leaf, one immediate dependency per non-root node, and no
1271    /// branches, duplicate dependencies, foreign dependencies, or cycles.
1272    pub fn linear_chain_node_kinds(&self) -> Result<Vec<CudaGraphNodeKind>> {
1273        let mut check = |operation, code| cuda_graph_check(operation, code);
1274        let mut shape_error = |error| {
1275            XlogError::Kernel(format!(
1276                "CUDA graph is not a linear dependency chain: {error}"
1277            ))
1278        };
1279        graph_linear_chain_node_kinds_with(self.graph, &mut check, &mut shape_error)
1280    }
1281
1282    /// Read CUDA's raw kernel-node params for inventory/update code.
1283    ///
1284    /// The returned `kernelParams` pointer is CUDA-owned capture metadata. Treat
1285    /// it as read-only unless constructing a fresh params object for
1286    /// [`Self::set_kernel_node_params`].
1287    pub fn kernel_node_params(&self, node: CudaGraphNode) -> Result<sys::CUDA_KERNEL_NODE_PARAMS> {
1288        if node.kind != CudaGraphNodeKind::Kernel {
1289            return Err(XlogError::Kernel(format!(
1290                "kernel_node_params called for non-kernel graph node {:?}",
1291                node.kind
1292            )));
1293        }
1294        let mut params: sys::CUDA_KERNEL_NODE_PARAMS = unsafe { mem::zeroed() };
1295        unsafe {
1296            cuda_graph_check(
1297                "cuGraphKernelNodeGetParams_v2",
1298                sys::cuGraphKernelNodeGetParams_v2(node.raw, &mut params),
1299            )?;
1300        }
1301        Ok(params)
1302    }
1303
1304    /// Update a kernel node in the instantiated graph.
1305    ///
1306    /// # Safety
1307    /// CUDA requires the replacement params to be topology-compatible with the
1308    /// captured node. The caller must keep every pointed-to kernel argument
1309    /// alive until CUDA has consumed the update and launched work that uses it.
1310    pub unsafe fn set_kernel_node_params(
1311        &self,
1312        node: CudaGraphNode,
1313        params: &sys::CUDA_KERNEL_NODE_PARAMS,
1314    ) -> Result<()> {
1315        if node.kind != CudaGraphNodeKind::Kernel {
1316            return Err(XlogError::Kernel(format!(
1317                "set_kernel_node_params called for non-kernel graph node {:?}",
1318                node.kind
1319            )));
1320        }
1321        cuda_graph_check(
1322            "cuGraphExecKernelNodeSetParams_v2",
1323            sys::cuGraphExecKernelNodeSetParams_v2(self.exec, node.raw, params),
1324        )
1325    }
1326
1327    /// Read CUDA's raw memset-node params for inventory/update code.
1328    pub fn memset_node_params(&self, node: CudaGraphNode) -> Result<sys::CUDA_MEMSET_NODE_PARAMS> {
1329        if node.kind != CudaGraphNodeKind::Memset {
1330            return Err(XlogError::Kernel(format!(
1331                "memset_node_params called for non-memset graph node {:?}",
1332                node.kind
1333            )));
1334        }
1335        let mut params: sys::CUDA_MEMSET_NODE_PARAMS = unsafe { mem::zeroed() };
1336        unsafe {
1337            cuda_graph_check(
1338                "cuGraphMemsetNodeGetParams",
1339                sys::cuGraphMemsetNodeGetParams(node.raw, &mut params),
1340            )?;
1341        }
1342        Ok(params)
1343    }
1344
1345    /// Update a memset node in the instantiated graph.
1346    pub fn set_memset_node_params(
1347        &self,
1348        node: CudaGraphNode,
1349        params: &sys::CUDA_MEMSET_NODE_PARAMS,
1350        stream: &CudaStream,
1351    ) -> Result<()> {
1352        if node.kind != CudaGraphNodeKind::Memset {
1353            return Err(XlogError::Kernel(format!(
1354                "set_memset_node_params called for non-memset graph node {:?}",
1355                node.kind
1356            )));
1357        }
1358        let ctx = stream_context(stream)?;
1359        unsafe {
1360            cuda_graph_check(
1361                "cuGraphExecMemsetNodeSetParams",
1362                sys::cuGraphExecMemsetNodeSetParams(self.exec, node.raw, params, ctx),
1363            )
1364        }
1365    }
1366
1367    /// Raw graph handle for low-level node inventory/update code.
1368    pub fn graph(&self) -> sys::CUgraph {
1369        self.graph
1370    }
1371
1372    /// Raw instantiated graph handle for low-level graph-exec update code.
1373    pub fn exec(&self) -> sys::CUgraphExec {
1374        self.exec
1375    }
1376}
1377
1378impl Drop for CapturedCudaGraph {
1379    fn drop(&mut self) {
1380        let _ = self.context.bind_to_thread();
1381        unsafe {
1382            if !self.exec.is_null() {
1383                let _ = sys::cuGraphExecDestroy(self.exec);
1384            }
1385            if !self.graph.is_null() {
1386                let _ = sys::cuGraphDestroy(self.graph);
1387            }
1388        }
1389    }
1390}
1391
1392impl CudaGraphNodeKind {
1393    fn from_sys(kind: sys::CUgraphNodeType) -> Self {
1394        match kind {
1395            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_KERNEL => Self::Kernel,
1396            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMCPY => Self::Memcpy,
1397            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEMSET => Self::Memset,
1398            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_HOST => Self::Host,
1399            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_GRAPH => Self::Graph,
1400            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EMPTY => Self::Empty,
1401            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_WAIT_EVENT => Self::WaitEvent,
1402            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EVENT_RECORD => Self::EventRecord,
1403            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_SIGNAL => {
1404                Self::ExternalSemaphoresSignal
1405            }
1406            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_EXT_SEMAS_WAIT => Self::ExternalSemaphoresWait,
1407            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_ALLOC => Self::MemAlloc,
1408            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_MEM_FREE => Self::MemFree,
1409            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_BATCH_MEM_OP => Self::BatchMemOp,
1410            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL => Self::Conditional,
1411        }
1412    }
1413}
1414
1415fn cuda_graph_check(label: &str, code: sys::CUresult) -> Result<()> {
1416    if code == sys::CUresult::CUDA_SUCCESS {
1417        Ok(())
1418    } else {
1419        Err(XlogError::Kernel(format!("{label} failed: {code:?}")))
1420    }
1421}
1422
1423fn stream_context(stream: &CudaStream) -> Result<sys::CUcontext> {
1424    let mut ctx = ptr::null_mut();
1425    unsafe {
1426        cuda_graph_check(
1427            "cuStreamGetCtx",
1428            sys::cuStreamGetCtx(stream.cu_stream(), &mut ctx),
1429        )?;
1430    }
1431    if ctx.is_null() {
1432        Err(XlogError::Kernel(
1433            "cuStreamGetCtx returned a null CUDA context".to_string(),
1434        ))
1435    } else {
1436        Ok(ctx)
1437    }
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442    use super::*;
1443    use cudarc::{
1444        driver::{DevicePtr, LaunchConfig, PushKernelArg},
1445        nvrtc::compile_ptx,
1446    };
1447    use std::sync::Barrier;
1448    use std::thread;
1449
1450    #[test]
1451    fn conditional_graphs_expose_dependency_ordered_linear_chain_inventory() {
1452        let _: fn(
1453            &ConditionalCudaGraphBody,
1454        )
1455            -> std::result::Result<Vec<CudaGraphNodeKind>, CudaConditionalGraphUnavailable> =
1456            ConditionalCudaGraphBody::linear_chain_node_kinds;
1457        let _: fn(&CapturedCudaGraph) -> Result<Vec<CudaGraphNodeKind>> =
1458            CapturedCudaGraph::linear_chain_node_kinds;
1459    }
1460
1461    #[test]
1462    fn linear_chain_inventory_recovers_dependency_order_from_shuffled_nodes() {
1463        let enumerated_kinds = [
1464            CudaGraphNodeKind::Kernel,
1465            CudaGraphNodeKind::Kernel,
1466            CudaGraphNodeKind::Conditional,
1467            CudaGraphNodeKind::Kernel,
1468            CudaGraphNodeKind::Conditional,
1469        ];
1470        let immediate_dependencies = vec![vec![2], vec![4], vec![3], vec![], vec![0]];
1471        let dependency_order = linear_chain_order(&immediate_dependencies).unwrap();
1472        assert_eq!(dependency_order, vec![3, 2, 0, 4, 1]);
1473        assert_eq!(
1474            dependency_order
1475                .into_iter()
1476                .map(|index| enumerated_kinds[index])
1477                .collect::<Vec<_>>(),
1478            vec![
1479                CudaGraphNodeKind::Kernel,
1480                CudaGraphNodeKind::Conditional,
1481                CudaGraphNodeKind::Kernel,
1482                CudaGraphNodeKind::Conditional,
1483                CudaGraphNodeKind::Kernel,
1484            ]
1485        );
1486        assert_eq!(linear_chain_order(&[vec![]]).unwrap(), vec![0]);
1487    }
1488
1489    #[test]
1490    fn linear_chain_inventory_rejects_non_linear_dependency_shapes() {
1491        let cases = [
1492            (
1493                "empty graph",
1494                Vec::new(),
1495                LinearGraphChainError::RootCount { found: 0 },
1496            ),
1497            (
1498                "branch",
1499                vec![vec![], vec![0], vec![0]],
1500                LinearGraphChainError::Branch {
1501                    node: 0,
1502                    dependents: 2,
1503                },
1504            ),
1505            (
1506                "disconnected",
1507                vec![vec![], vec![]],
1508                LinearGraphChainError::RootCount { found: 2 },
1509            ),
1510            (
1511                "cycle",
1512                vec![vec![1], vec![0]],
1513                LinearGraphChainError::Cycle,
1514            ),
1515            (
1516                "foreign dependency",
1517                vec![vec![], vec![2]],
1518                LinearGraphChainError::ForeignDependency {
1519                    node: 1,
1520                    dependency: 2,
1521                },
1522            ),
1523            (
1524                "duplicate edge",
1525                vec![vec![], vec![0, 0]],
1526                LinearGraphChainError::DuplicateDependency {
1527                    node: 1,
1528                    dependency: 0,
1529                },
1530            ),
1531        ];
1532        for (case, dependencies, expected) in cases {
1533            assert_eq!(linear_chain_order(&dependencies), Err(expected), "{case}");
1534        }
1535    }
1536
1537    #[test]
1538    fn same_stream_capture_registry_is_deterministically_busy_until_release() {
1539        let key = StreamCaptureKey {
1540            context: 0x51,
1541            stream: 0x73,
1542        };
1543        let entered = Arc::new(Barrier::new(2));
1544        let release = Arc::new(Barrier::new(2));
1545        let first_entered = Arc::clone(&entered);
1546        let first_release = Arc::clone(&release);
1547        let first = thread::spawn(move || {
1548            let _lease = try_acquire_stream_capture_key(key).expect("first capture lease");
1549            first_entered.wait();
1550            first_release.wait();
1551        });
1552        entered.wait();
1553        assert_eq!(
1554            try_acquire_stream_capture_key(key).expect_err("same stream must be busy"),
1555            CudaConditionalGraphUnavailable::StreamCaptureBusy
1556        );
1557        release.wait();
1558        first.join().expect("first capture thread");
1559        drop(try_acquire_stream_capture_key(key).expect("capture after release"));
1560    }
1561
1562    #[test]
1563    fn different_stream_capture_registry_entries_can_coexist() {
1564        let first = try_acquire_stream_capture_key(StreamCaptureKey {
1565            context: 0x91,
1566            stream: 0x92,
1567        })
1568        .expect("first stream capture");
1569        let second = try_acquire_stream_capture_key(StreamCaptureKey {
1570            context: 0x91,
1571            stream: 0x93,
1572        })
1573        .expect("different stream capture");
1574        drop((first, second));
1575    }
1576
1577    #[test]
1578    fn real_capture_helper_returns_typed_busy_before_beginning_driver_capture() {
1579        let context = match CudaContext::new(0) {
1580            Ok(context) => context,
1581            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
1582                panic!("XLOG_REQUIRE_CUDA=1 but CUDA setup failed: {error}")
1583            }
1584            Err(error) => {
1585                eprintln!("Skipping test: CUDA unavailable: {error}");
1586                return;
1587            }
1588        };
1589        let stream = context.new_stream().expect("non-default CUDA stream");
1590        let _lease = try_acquire_stream_capture(&stream).expect("held stream capture lease");
1591        let mut builder = match ConditionalCudaGraphSequenceBuilder::new(&stream) {
1592            Ok(builder) => builder,
1593            Err(error) if error.is_unsupported() => return,
1594            Err(error) => panic!("sequence builder failed: {error}"),
1595        };
1596        let error = builder
1597            .capture_segment_on_stream(&stream, || Ok::<(), XlogError>(()))
1598            .expect_err("same stream capture must decline before driver capture");
1599        assert_eq!(error, CudaConditionalGraphUnavailable::StreamCaptureBusy);
1600    }
1601
1602    #[cfg(unix)]
1603    #[test]
1604    fn missing_driver_symbol_is_a_typed_error_instead_of_a_panic() {
1605        let library = libloading::Library::from(libloading::os::unix::Library::this());
1606        let error = unsafe {
1607            load_required_symbol::<ConditionalHandleCreateFn>(
1608                &library,
1609                b"xlog_missing_cuda_conditional_symbol_for_test\0",
1610                "xlog_missing_cuda_conditional_symbol_for_test",
1611            )
1612        }
1613        .expect_err("the deliberately absent symbol must fail closed");
1614
1615        assert_eq!(
1616            error,
1617            CudaConditionalGraphUnavailable::MissingDriverSymbol {
1618                symbol: "xlog_missing_cuda_conditional_symbol_for_test",
1619            }
1620        );
1621        assert!(error.is_unsupported());
1622        assert_eq!(
1623            error.decline_detail(),
1624            "CUDA driver is missing required conditional-graph symbol \
1625             xlog_missing_cuda_conditional_symbol_for_test"
1626        );
1627    }
1628
1629    #[test]
1630    fn driver_versions_before_cuda_twelve_three_decline_conditionals() {
1631        let error = require_conditional_graph_driver(12_020).expect_err("CUDA 12.2 is too old");
1632        assert_eq!(
1633            error,
1634            CudaConditionalGraphUnavailable::DriverVersionTooOld {
1635                found: 12_020,
1636                required: 12_030,
1637            }
1638        );
1639        assert!(error.is_unsupported());
1640        assert_eq!(
1641            error.decline_detail(),
1642            "CUDA conditional graphs require driver API 12030, found 12020"
1643        );
1644        require_conditional_graph_driver(12_030).expect("CUDA 12.3 is supported");
1645    }
1646
1647    #[test]
1648    fn while_node_params_use_the_driver_abi_and_return_one_body() {
1649        let handle = 0x1234_u64;
1650        let ctx = 0x5678_usize as sys::CUcontext;
1651        let params = conditional_while_node_params(handle, ctx);
1652
1653        assert_eq!(
1654            params.type_,
1655            sys::CUgraphNodeType::CU_GRAPH_NODE_TYPE_CONDITIONAL
1656        );
1657        let conditional = unsafe { params.__bindgen_anon_1.conditional };
1658        assert_eq!(conditional.handle, handle);
1659        assert_eq!(
1660            conditional.type_,
1661            sys::CUgraphConditionalNodeType::CU_GRAPH_COND_TYPE_WHILE
1662        );
1663        assert_eq!(conditional.size, 1);
1664        assert!(conditional.phGraph_out.is_null());
1665        assert_eq!(conditional.ctx, ctx);
1666    }
1667
1668    #[test]
1669    fn conditional_body_exposes_device_setter_handle_and_context() {
1670        let graph = 0x1234_usize as sys::CUgraph;
1671        let handle = 0x5678_u64;
1672        let context = 0x9abc_usize as sys::CUcontext;
1673        let body = ConditionalCudaGraphBody {
1674            graph,
1675            handle,
1676            context,
1677        };
1678
1679        assert_eq!(body.graph(), graph);
1680        assert_eq!(body.handle(), handle);
1681        assert_eq!(body.context(), context);
1682    }
1683
1684    #[test]
1685    fn real_conditional_while_graph_creates_instantiates_and_launches() {
1686        let context = match CudaContext::new(0) {
1687            Ok(context) => context,
1688            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
1689                panic!("XLOG_REQUIRE_CUDA=1 but CUDA setup failed: {error}")
1690            }
1691            Err(error) => {
1692                eprintln!("Skipping test: CUDA unavailable: {error}");
1693                return;
1694            }
1695        };
1696        let stream = context.new_stream().expect("non-default CUDA stream");
1697        let body_buffer = stream.alloc_zeros::<u32>(1).expect("body buffer");
1698        let (body_ptr, _body_sync) = body_buffer.device_ptr(&stream);
1699        let ptx = compile_ptx(
1700            r#"
1701            extern "C" __device__ void cudaGraphSetConditional(
1702                unsigned long long handle,
1703                unsigned int value
1704            );
1705
1706            extern "C" __global__ void run_once(
1707                unsigned long long handle,
1708                unsigned int *counter
1709            ) {
1710                if (blockIdx.x == 0 && threadIdx.x == 0) {
1711                    *counter += 1;
1712                    cudaGraphSetConditional(handle, 0);
1713                }
1714            }
1715            "#,
1716        )
1717        .expect("compile conditional setter kernel");
1718        let module = context.load_module(ptx).expect("load setter module");
1719        let run_once = module
1720            .load_function("run_once")
1721            .expect("load setter function");
1722        let graph = CapturedCudaGraph::conditional_while_on_stream(&stream, 1, true, |body| {
1723            body.capture_on_stream(&stream, || {
1724                let handle = body.handle();
1725                let mut launch = stream.launch_builder(&run_once);
1726                launch.arg(&handle).arg(&body_ptr);
1727                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
1728                    .map(|_| ())
1729                    .map_err(|error| XlogError::Kernel(error.to_string()))
1730            })
1731        });
1732        let graph = match graph {
1733            Ok(graph) => graph,
1734            Err(error) if error.is_unsupported() => {
1735                if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
1736                    panic!("CUDA conditional graphs are required: {error}");
1737                }
1738                eprintln!("Skipping test: {error}");
1739                return;
1740            }
1741            Err(error) => panic!("conditional graph construction failed: {error}"),
1742        };
1743
1744        assert_eq!(graph.node_count().expect("node count"), 1);
1745        assert_eq!(
1746            graph.nodes().expect("nodes")[0].kind,
1747            CudaGraphNodeKind::Conditional
1748        );
1749        graph.launch(&stream).expect("conditional graph launch");
1750        stream.synchronize().expect("conditional graph completion");
1751        let mut observed = [0_u32; 1];
1752        stream
1753            .memcpy_dtoh(&body_buffer, &mut observed)
1754            .expect("read body effect");
1755        assert_eq!(observed, [1], "WHILE body must execute exactly once");
1756    }
1757
1758    #[test]
1759    fn real_conditional_sequence_orders_segments_around_multiple_while_capability() {
1760        let context = match CudaContext::new(0) {
1761            Ok(context) => context,
1762            Err(error) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
1763                panic!("XLOG_REQUIRE_CUDA=1 but CUDA setup failed: {error}")
1764            }
1765            Err(error) => {
1766                eprintln!("Skipping test: CUDA unavailable: {error}");
1767                return;
1768            }
1769        };
1770        let stream = context.new_stream().expect("non-default CUDA stream");
1771        let buffer = stream.alloc_zeros::<u32>(1).expect("sequence buffer");
1772        let (buffer_ptr, _buffer_sync) = buffer.device_ptr(&stream);
1773        let ptx = compile_ptx(
1774            r#"
1775            extern "C" __device__ void cudaGraphSetConditional(
1776                unsigned long long handle,
1777                unsigned int value
1778            );
1779
1780            extern "C" __global__ void add_value(
1781                unsigned int *counter,
1782                unsigned int value
1783            ) {
1784                if (blockIdx.x == 0 && threadIdx.x == 0) *counter += value;
1785            }
1786
1787            extern "C" __global__ void add_once(
1788                unsigned long long handle,
1789                unsigned int *counter
1790            ) {
1791                if (blockIdx.x == 0 && threadIdx.x == 0) {
1792                    *counter += 2;
1793                    cudaGraphSetConditional(handle, 0);
1794                }
1795            }
1796            "#,
1797        )
1798        .expect("compile sequence kernels");
1799        let module = context.load_module(ptx).expect("load sequence module");
1800        let add_value = module
1801            .load_function("add_value")
1802            .expect("load add function");
1803        let add_once = module
1804            .load_function("add_once")
1805            .expect("load conditional function");
1806
1807        let sequence = ConditionalCudaGraphSequenceBuilder::new(&stream).and_then(|mut builder| {
1808            builder.capture_segment_on_stream(&stream, || {
1809                let value = 1_u32;
1810                let mut launch = stream.launch_builder(&add_value);
1811                launch.arg(&buffer_ptr).arg(&value);
1812                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
1813                    .map(|_| ())
1814                    .map_err(|error| XlogError::Kernel(error.to_string()))
1815            })?;
1816            builder.add_conditional_while(1, true, |body| {
1817                body.capture_on_stream(&stream, || {
1818                    let handle = body.handle();
1819                    let mut launch = stream.launch_builder(&add_once);
1820                    launch.arg(&handle).arg(&buffer_ptr);
1821                    unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
1822                        .map(|_| ())
1823                        .map_err(|error| XlogError::Kernel(error.to_string()))
1824                })
1825            })?;
1826            builder.capture_segment_on_stream(&stream, || {
1827                let value = 4_u32;
1828                let mut launch = stream.launch_builder(&add_value);
1829                launch.arg(&buffer_ptr).arg(&value);
1830                unsafe { launch.launch(LaunchConfig::for_num_elems(1)) }
1831                    .map(|_| ())
1832                    .map_err(|error| XlogError::Kernel(error.to_string()))
1833            })?;
1834            builder.instantiate()
1835        });
1836        let graph = match sequence {
1837            Ok(graph) => graph,
1838            Err(error) if error.is_unsupported() => {
1839                if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") {
1840                    panic!("CUDA conditional graphs are required: {error}");
1841                }
1842                eprintln!("Skipping test: {error}");
1843                return;
1844            }
1845            Err(error) => panic!("conditional graph sequence construction failed: {error}"),
1846        };
1847        assert_eq!(graph.node_count().expect("sequence node count"), 3);
1848        graph.launch(&stream).expect("sequence graph launch");
1849        stream.synchronize().expect("sequence graph completion");
1850        let mut observed = [0_u32; 1];
1851        stream
1852            .memcpy_dtoh(&buffer, &mut observed)
1853            .expect("read sequence effect");
1854        assert_eq!(observed, [7]);
1855    }
1856
1857    #[test]
1858    fn scan_topology_matches_recursive_multiblock_shape() {
1859        assert_eq!(
1860            scan_topology_u32(0),
1861            ScanTopology {
1862                input_len: 0,
1863                block_size: 256,
1864                scratch_lengths: vec![],
1865                kernel_node_count: 0,
1866            }
1867        );
1868        assert_eq!(scan_topology_u32(256).scratch_lengths, Vec::<u32>::new());
1869        assert_eq!(scan_topology_u32(256).kernel_node_count, 1);
1870        assert_eq!(scan_topology_u32(257).scratch_lengths, vec![2]);
1871        assert_eq!(scan_topology_u32(257).kernel_node_count, 3);
1872        assert_eq!(scan_topology_u32(65_537).scratch_lengths, vec![257, 2]);
1873        assert_eq!(scan_topology_u32(65_537).kernel_node_count, 5);
1874    }
1875
1876    #[test]
1877    fn csm_key_uses_capacity_classes_and_layout_version() {
1878        let key = CsmCudaGraphKey::inner(2, 16, 257, 513).expect("key");
1879        assert_eq!(key.join_kind, CsmCudaGraphJoinKind::Inner);
1880        assert_eq!(key.key_arity, 2);
1881        assert_eq!(key.key_bytes, 16);
1882        assert_eq!(key.probe_capacity_class, 512);
1883        assert_eq!(key.output_capacity_class, 1024);
1884        assert_eq!(key.scan_topology.scratch_lengths, vec![2]);
1885        assert_eq!(key.node_layout_version, CSM_CUDA_GRAPH_NODE_LAYOUT_VERSION);
1886    }
1887}