1use 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#[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 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 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
431pub 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 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 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 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 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, °ree)| (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
897pub 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
906unsafe 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn graph(&self) -> sys::CUgraph {
1369 self.graph
1370 }
1371
1372 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}