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