Skip to main content

xlog_cuda/
device.rs

1//! CUDA device management
2//!
3//! This module keeps XLOG's historical single-stream device abstraction while
4//! targeting cudarc's newer CUDA 13-capable context/stream APIs.
5//!
6//! Loading/replacing modules and releasing their last owners can wait for
7//! execution-event completion, directly or through deferred capture cleanup.
8//! Callers must satisfy the [cold-lifecycle progress contract](crate::cuda_graph).
9
10use std::collections::BTreeMap;
11use std::ffi::{c_void, CString};
12use std::path::Path;
13use std::sync::{Arc, Mutex, RwLock};
14
15use cudarc::driver::result::{self, DriverError};
16use cudarc::driver::{
17    sys, CudaContext as CudarcContext, CudaEvent, CudaSlice, CudaStream, DevicePtr, DevicePtrMut,
18    DeviceRepr, HostSlice, LaunchConfig, ValidAsZeroBits,
19};
20use cudarc::nvrtc::Ptx;
21use sha2::{Digest, Sha256};
22use xlog_core::{Result, XlogError};
23
24use crate::device_runtime::{Access, ResourceResult};
25use crate::memory::{with_memory_access, DeviceMemoryView, DeviceRead, DeviceWrite};
26
27#[derive(Clone, Copy, Debug, Default)]
28enum HostTransferCompletion {
29    #[default]
30    Complete,
31    Unfenced,
32    EventRecorded,
33}
34
35impl HostTransferCompletion {
36    fn submit<E>(
37        &mut self,
38        transfer: impl FnOnce() -> std::result::Result<(), E>,
39        mut record: impl FnMut() -> std::result::Result<(), E>,
40    ) -> std::result::Result<(), E> {
41        *self = Self::Unfenced;
42        // Keep the original stream/frame alive while fencing even a failed or
43        // unwinding submission. Never replace its error with cleanup failure.
44        let submitted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(transfer));
45        let recorded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut record));
46        if matches!(recorded, Ok(Ok(()))) {
47            *self = Self::EventRecorded;
48        } else if matches!(
49            std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut record)),
50            Ok(Ok(()))
51        ) {
52            // A transient record error may be repaired here, but the original
53            // operation still fails. No later thread guesses a PTDS identity.
54            *self = Self::EventRecorded;
55        }
56        match submitted {
57            Err(panic) => std::panic::resume_unwind(panic),
58            Ok(Err(error)) => Err(error),
59            Ok(Ok(())) => match recorded {
60                Ok(result) => result,
61                Err(panic) => std::panic::resume_unwind(panic),
62            },
63        }
64    }
65
66    fn wait<E>(
67        &mut self,
68        event: impl FnOnce() -> std::result::Result<(), E>,
69        refence: impl FnOnce() -> std::result::Result<(), E>,
70    ) -> std::result::Result<(), E> {
71        match self {
72            Self::Complete => return Ok(()),
73            Self::Unfenced => {
74                refence()?;
75                *self = Self::EventRecorded;
76            }
77            Self::EventRecorded => {}
78        }
79        event()?;
80        *self = Self::Complete;
81        Ok(())
82    }
83}
84
85/// One page-locked host allocation, shared by synchronous staging and terminal
86/// receipt owners. An uncertain copy retains these bytes through cold retirement.
87/// No caller-owned host address is ever placed in that retirement queue.
88#[derive(Debug)]
89pub(crate) struct PinnedHostBuffer {
90    ptr: usize,
91    bytes: usize,
92    context: Arc<CudarcContext>,
93    event: Option<CudaEvent>,
94    completion: HostTransferCompletion,
95}
96
97impl PinnedHostBuffer {
98    pub(crate) fn new(stream: &CudaStream, bytes: usize) -> std::result::Result<Self, DriverError> {
99        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream)?;
100        let context = Arc::clone(stream.context());
101        let event = context.new_event(None)?;
102        context.bind_to_thread()?;
103        let mut ptr = std::ptr::null_mut();
104        // SAFETY: all fallible prerequisites precede allocation. The output is
105        // immediately owned; zero-length logical buffers still own one byte.
106        unsafe { sys::cuMemHostAlloc(&mut ptr, bytes.max(1), 0).result()? };
107        Ok(Self {
108            ptr: ptr as usize,
109            bytes,
110            context,
111            event: Some(event),
112            completion: HostTransferCompletion::Complete,
113        })
114    }
115
116    pub(crate) fn len(&self) -> usize {
117        self.bytes
118    }
119
120    pub(crate) fn copy_from_device(
121        &mut self,
122        source: &DeviceMemoryView<u8>,
123        stream: &Arc<CudaStream>,
124    ) -> ResourceResult<Vec<u8>> {
125        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream)?;
126        if source.len() != self.bytes {
127            return Err(crate::device_runtime::ResourceError::StreamMisuse(
128                "pinned host destination size does not match device source".into(),
129            ));
130        }
131        let bytes = self.bytes;
132        with_memory_access(
133            Arc::clone(stream),
134            vec![source.access(Access::Read)?],
135            |proof| {
136                let source = proof.read(source)?;
137                let (ptr, _guard) = source.device_ptr(stream);
138                // SAFETY: the read admission retains the entire device allocation;
139                // this owner retains the host destination even if abort cannot wait.
140                unsafe {
141                    self.enqueue(stream, |host| {
142                        sys::cuMemcpyDtoHAsync_v2(host.cast(), ptr, bytes, stream.cu_stream())
143                            .result()
144                    })?;
145                }
146                Ok(self.wait()?)
147            },
148        )?;
149        // SAFETY: the admitted byte source initialized the complete destination.
150        Ok(unsafe { self.read_vec(bytes)? })
151    }
152
153    /// Enqueue a whole transfer batch, recording its fence on the submitting
154    /// thread. Even a per-thread stream can then retire on a different thread.
155    ///
156    /// # Safety
157    /// The closure must access only this allocation's `len()` host bytes. Every
158    /// device range must have an admitted, retained owner for the whole batch.
159    pub(crate) unsafe fn enqueue(
160        &mut self,
161        stream: &CudaStream,
162        transfer: impl FnOnce(*mut u8) -> std::result::Result<(), DriverError>,
163    ) -> std::result::Result<(), DriverError> {
164        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream)?;
165        if stream.context().cu_ctx() != self.context.cu_ctx() {
166            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_CONTEXT));
167        }
168        self.wait()?;
169        self.context.bind_to_thread()?;
170        let event = self.event.as_ref().expect("pinned owner event present");
171        self.completion
172            .submit(|| transfer(self.ptr as *mut u8), || event.record(stream))
173    }
174
175    pub(crate) fn wait(&mut self) -> std::result::Result<(), DriverError> {
176        let _ordinary = crate::cuda_graph::reserve_capture_exclusion()?;
177        self.completion.wait(
178            || {
179                self.event
180                    .as_ref()
181                    .expect("pinned owner event present")
182                    .synchronize()
183            },
184            // Both original-frame record attempts failed. An old event or a
185            // different thread's PTDS cannot prove this prefix complete. A
186            // context fence could invalidate an uncatalogued external capture.
187            || Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN)),
188        )
189    }
190
191    fn write<T: DeviceRepr>(&mut self, source: &[T]) -> std::result::Result<(), DriverError> {
192        if std::mem::size_of_val(source) != self.bytes {
193            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE));
194        }
195        self.wait()?;
196        // SAFETY: the source is initialized, destination is an equally sized
197        // uniquely owned allocation, and previous DMA has completed.
198        unsafe {
199            std::ptr::copy_nonoverlapping(
200                source.as_ptr().cast::<u8>(),
201                self.ptr as *mut u8,
202                self.bytes,
203            );
204        }
205        Ok(())
206    }
207
208    /// # Safety
209    /// A completed transfer must have initialized all bytes as `len` valid T
210    /// values. Allocation alone does not initialize this host buffer.
211    pub(crate) unsafe fn read_vec<T: DeviceRepr>(
212        &mut self,
213        len: usize,
214    ) -> std::result::Result<Vec<T>, DriverError> {
215        if len.checked_mul(std::mem::size_of::<T>()) != Some(self.bytes) {
216            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE));
217        }
218        self.wait()?;
219        let mut values = Vec::<T>::with_capacity(len);
220        // SAFETY: caller proves initialization/type; Vec supplies alignment.
221        // Publish length only after the infallible byte copy, never before DMA.
222        unsafe {
223            std::ptr::copy_nonoverlapping(
224                self.ptr as *const u8,
225                values.as_mut_ptr().cast::<u8>(),
226                self.bytes,
227            );
228            values.set_len(len);
229        }
230        Ok(values)
231    }
232
233    /// # Safety
234    /// A completed copy must have initialized these bytes as valid `T` values.
235    unsafe fn read_into<T: DeviceRepr>(
236        &mut self,
237        dst: &mut [T],
238    ) -> std::result::Result<(), DriverError> {
239        if std::mem::size_of_val(dst) != self.bytes {
240            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE));
241        }
242        self.wait()?;
243        // SAFETY: caller proves the copied representation; the host destination
244        // is exclusively borrowed and cannot be accessed by this owner's DMA.
245        unsafe {
246            std::ptr::copy_nonoverlapping(
247                self.ptr as *const u8,
248                dst.as_mut_ptr().cast::<u8>(),
249                self.bytes,
250            );
251        }
252        Ok(())
253    }
254}
255
256impl Drop for PinnedHostBuffer {
257    fn drop(&mut self) {
258        let resources = (
259            self.ptr,
260            Arc::clone(&self.context),
261            self.event.take().expect("pinned owner event present"),
262            self.completion,
263        );
264        crate::cuda_graph::retire_resources_after_completion(
265            resources,
266            |(_, context, event, completion)| {
267                context.bind_to_thread()?;
268                let mut completion = *completion;
269                completion.wait(
270                    || event.synchronize(),
271                    || Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN)),
272                )
273            },
274            |(ptr, _, _, _)| {
275                // SAFETY: the entire copy batch is complete. The canonical
276                // retirement helper never resubmits an unknown free outcome.
277                unsafe { sys::cuMemFreeHost(*ptr as *mut c_void).result() }
278            },
279            |_| Ok(()),
280            |_, error| eprintln!("CUDA pinned host allocation retirement incomplete: {error}"),
281        );
282    }
283}
284
285#[cfg(test)]
286mod host_transfer_tests {
287    use super::*;
288
289    #[test]
290    fn execution_retirement_observes_successful_abort_after_failed_event_records() {
291        use crate::cuda_graph::{reap_capture_retirements, retire_resources_after_completion};
292        use crate::launch::{LaunchEnqueueError, RecorderTransaction};
293        use crate::memory::OperationCompletion;
294        use std::sync::atomic::{AtomicUsize, Ordering};
295
296        let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
297        let proof = Arc::new(OperationCompletion::new(17));
298        let fence = Arc::new(Mutex::new(ExecutionFence::default()));
299        let payload = Arc::new(());
300        let payload_weak = Arc::downgrade(&payload);
301        let destroys = Arc::new(AtomicUsize::new(0));
302        let mut transaction =
303            RecorderTransaction::from_admitted(Arc::clone(&proof), Box::<[u8]>::default());
304        let launches = AtomicUsize::new(0);
305        let records = AtomicUsize::new(0);
306        let error = DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN);
307        let result = transaction.enqueue_operation_with(
308            || Ok(()),
309            |proof| proof.validate_submission(17),
310            |_| {
311                let submitted = fence.lock().unwrap().submit(
312                    Some(Arc::clone(&proof)),
313                    || {
314                        launches.fetch_add(1, Ordering::SeqCst);
315                        Ok(())
316                    },
317                    || {
318                        records.fetch_add(1, Ordering::SeqCst);
319                        Err(error)
320                    },
321                );
322                let observed = Arc::clone(&fence);
323                let destroyed = Arc::clone(&destroys);
324                retire_resources_after_completion(
325                    payload,
326                    move |_| {
327                        observed
328                            .lock()
329                            .unwrap()
330                            .wait(|| panic!("unrecorded event is not proof"))
331                    },
332                    move |_| {
333                        destroyed.fetch_add(1, Ordering::SeqCst);
334                        Ok(())
335                    },
336                    |_| Ok(()),
337                    |_, _: DriverError| panic!("completion failure must remain retryable"),
338                );
339                assert!(payload_weak.upgrade().is_some());
340                assert_eq!(destroys.load(Ordering::SeqCst), 0);
341                submitted
342            },
343            |proof| proof.synchronize_with(17, || Ok(())),
344            |_, _| Ok(()),
345        );
346        assert!(
347            matches!(result, Err(LaunchEnqueueError::Operation(observed)) if observed == error)
348        );
349        assert!(proof.is_complete());
350        reap_capture_retirements();
351        assert!(payload_weak.upgrade().is_none());
352        assert_eq!(destroys.load(Ordering::SeqCst), 1);
353        reap_capture_retirements();
354        assert_eq!(destroys.load(Ordering::SeqCst), 1);
355        assert_eq!(launches.load(Ordering::SeqCst), 1);
356        assert_eq!(records.load(Ordering::SeqCst), 2);
357    }
358
359    #[test]
360    fn execution_fence_replaces_previous_operation_confirmation() {
361        use crate::memory::OperationCompletion;
362        let old = Arc::new(OperationCompletion::new(17));
363        let new = Arc::new(OperationCompletion::new(17));
364        let mut fence = ExecutionFence::default();
365        fence
366            .submit(Some(Arc::clone(&old)), || Ok(()), || Ok(()))
367            .unwrap();
368        old.synchronize_with(17, || Ok(())).unwrap();
369        let error = DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN);
370        assert_eq!(
371            fence.submit(Some(Arc::clone(&new)), || Ok(()), || Err(error)),
372            Err(error)
373        );
374        assert_eq!(
375            fence.wait(|| panic!("old event cannot confirm the new execution")),
376            Err(error)
377        );
378        new.synchronize_with(17, || Ok(())).unwrap();
379        fence
380            .wait(|| panic!("positive exact confirmation does not need the event"))
381            .unwrap();
382        assert_eq!(fence.submit(None, || Ok(()), || Err(error)), Err(error));
383        assert_eq!(
384            fence.wait(|| panic!("borrowed launch cannot reuse an earlier admission's proof")),
385            Err(error)
386        );
387    }
388
389    #[test]
390    fn execution_fence_retries_a_recorded_event_without_relaunch() {
391        let mut fence = ExecutionFence::default();
392        let error = DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN);
393        let mut records = 0;
394        assert_eq!(
395            fence.submit(
396                None,
397                || Ok(()),
398                || {
399                    records += 1;
400                    if records == 1 {
401                        Err(error)
402                    } else {
403                        Ok(())
404                    }
405                }
406            ),
407            Err(error)
408        );
409        assert_eq!(fence.wait(|| Err(error)), Err(error));
410        fence.wait(|| Ok(())).unwrap();
411        assert_eq!(records, 2);
412    }
413
414    #[test]
415    fn failed_host_submission_records_the_original_prefix_before_returning_error() {
416        let mut completion = HostTransferCompletion::default();
417        let result = completion.submit(|| Err("original transfer error"), || Ok(()));
418        assert_eq!(result, Err("original transfer error"));
419        assert!(matches!(completion, HostTransferCompletion::EventRecorded));
420    }
421
422    #[test]
423    fn failed_event_record_is_repaired_without_erasing_its_error() {
424        let mut completion = HostTransferCompletion::default();
425        let mut attempts = 0;
426        let result = completion.submit(
427            || Ok(()),
428            || {
429                attempts += 1;
430                if attempts == 1 {
431                    Err("record failed")
432                } else {
433                    Ok(())
434                }
435            },
436        );
437        assert_eq!(result, Err("record failed"));
438        assert_eq!(attempts, 2);
439        assert!(matches!(completion, HostTransferCompletion::EventRecorded));
440        assert!(completion
441            .wait(|| Ok::<(), &str>(()), || panic!("already fenced"))
442            .is_ok());
443    }
444
445    #[test]
446    fn submission_unwind_preserves_original_panic_when_both_fences_fail() {
447        let mut completion = HostTransferCompletion::default();
448        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
449            let _: std::result::Result<(), &str> = completion.submit(
450                || panic!("original submission panic"),
451                || panic!("cleanup panic"),
452            );
453        }));
454        assert_eq!(
455            *result.unwrap_err().downcast::<&str>().unwrap(),
456            "original submission panic"
457        );
458        assert!(matches!(completion, HostTransferCompletion::Unfenced));
459        assert!(completion
460            .wait(|| panic!("no valid fence"), || Err("unfenced"))
461            .is_err());
462    }
463
464    #[test]
465    fn uncertain_host_transfer_requires_completion_before_owner_release() {
466        for fail_record in [false, true] {
467            let mut completion = HostTransferCompletion::default();
468            let result = completion.submit(
469                || {
470                    if fail_record {
471                        Ok(())
472                    } else {
473                        Err("copy failed")
474                    }
475                },
476                || Err("event record failed"),
477            );
478            assert!(result.is_err());
479            assert!(completion
480                .wait(|| panic!("no recorded fence"), || Err("wait failed"))
481                .is_err());
482            assert!(matches!(completion, HostTransferCompletion::Unfenced));
483            completion.wait(|| Ok(()), || Ok::<(), &str>(())).unwrap();
484            assert!(matches!(completion, HostTransferCompletion::Complete));
485        }
486    }
487
488    #[test]
489    fn recorded_host_transfer_retries_its_exact_event_only() {
490        let mut completion = HostTransferCompletion::default();
491        completion.submit(|| Ok::<(), &str>(()), || Ok(())).unwrap();
492        assert!(completion
493            .wait(|| Err("event pending"), || panic!("recorded event exists"))
494            .is_err());
495        completion
496            .wait(|| Ok::<(), &str>(()), || panic!("recorded event exists"))
497            .unwrap();
498        completion
499            .wait(|| panic!("already complete"), || Err("already complete"))
500            .unwrap();
501    }
502
503    #[test]
504    #[ignore = "requires authorized CUDA execution"]
505    fn pinned_host_buffer_retires_real_allocation_on_another_thread_after_binding_failure() {
506        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
507        let device = CudaDevice::new(0).unwrap();
508        let context = Arc::clone(device.inner().stream().context());
509        let stream = context.new_stream().unwrap();
510        let source = device.inner().htod_sync_copy(&[0xa5_u8; 32]).unwrap();
511        let mut buffer = PinnedHostBuffer::new(&stream, 32).unwrap();
512        with_memory_access(
513            Arc::clone(&stream),
514            vec![source.access(Access::Read).unwrap()],
515            |proof| {
516                let source = proof.read(&source)?;
517                let (ptr, _guard) = source.device_ptr(&stream);
518                // SAFETY: the real source and owned pinned bank span exactly 32
519                // initialized bytes and both live through this test's proven fence.
520                unsafe {
521                    buffer.enqueue(&stream, |host| {
522                        sys::cuMemcpyDtoHAsync_v2(host.cast(), ptr, 32, stream.cu_stream()).result()
523                    })?;
524                }
525                Ok(())
526            },
527        )
528        .unwrap();
529        // Establish real completion before injecting a retirement prerequisite
530        // failure. Keep EventRecorded so production Drop still waits its event.
531        buffer.event.as_ref().unwrap().synchronize().unwrap();
532        assert_eq!(
533            unsafe { std::slice::from_raw_parts(buffer.ptr as *const u8, 32) },
534            &[0xa5; 32]
535        );
536        drop(source);
537        drop(stream);
538        drop(device);
539        crate::cuda_graph::reap_capture_retirements();
540        let retained = Arc::downgrade(&context);
541        context.record_err::<()>(Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN)));
542        drop(context);
543        drop(buffer);
544        assert!(
545            retained.upgrade().is_some(),
546            "failed binding lost the actual pinned owner"
547        );
548        std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
549            .join()
550            .unwrap();
551        assert!(
552            retained.upgrade().is_none(),
553            "completed retirement kept the event/context alive"
554        );
555        crate::cuda_graph::reap_capture_retirements();
556        assert!(retained.upgrade().is_none());
557    }
558
559    #[test]
560    fn unwinding_host_transfer_cannot_mark_its_storage_complete() {
561        let mut completion = HostTransferCompletion::default();
562        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
563            let _ = completion.submit(|| panic!("copy interrupted"), || Ok::<(), &str>(()));
564        }));
565        assert!(result.is_err());
566        assert!(matches!(completion, HostTransferCompletion::EventRecorded));
567    }
568}
569
570#[derive(Debug)]
571pub(crate) struct LoadedModule {
572    cu_module: sys::CUmodule,
573    functions: BTreeMap<Arc<str>, sys::CUfunction>,
574    module_name: Arc<str>,
575    artifact_name: Arc<str>,
576    artifact_sha256: Option<[u8; 32]>,
577    context: Arc<CudarcContext>,
578    completion: Mutex<ExecutionCompletion>,
579}
580
581#[derive(Clone, Debug, PartialEq, Eq)]
582pub(crate) struct CapturedCudaLaunchBinding {
583    pub(crate) module_name: Arc<str>,
584    pub(crate) artifact_name: Arc<str>,
585    pub(crate) artifact_sha256: Option<[u8; 32]>,
586    pub(crate) function_name: Arc<str>,
587    pub(crate) grid_dim: (u32, u32, u32),
588    pub(crate) block_dim: (u32, u32, u32),
589    pub(crate) shared_mem_bytes: u32,
590    pub(crate) parameter_count: u64,
591    pub(crate) cooperative: bool,
592}
593
594/// Completion events belong to the submitted executable, not its stream wrapper.
595/// A failed submission/record is kept uncertain even if an older event completes.
596/// Every recorded event includes the entire preceding stream and its transitive
597/// dependencies, not only work using this executable.
598#[derive(Debug, Default)]
599struct ExecutionFence {
600    state: HostTransferCompletion,
601    confirmation: Option<Arc<crate::memory::OperationCompletion>>,
602}
603
604impl ExecutionFence {
605    fn submit(
606        &mut self,
607        confirmation: Option<Arc<crate::memory::OperationCompletion>>,
608        launch: impl FnOnce() -> std::result::Result<(), DriverError>,
609        record: impl FnMut() -> std::result::Result<(), DriverError>,
610    ) -> std::result::Result<(), DriverError> {
611        // Replace the old admission's proof before any new work can be queued.
612        self.confirmation = confirmation;
613        self.state.submit(launch, record)
614    }
615
616    fn wait(
617        &self,
618        event: impl FnOnce() -> std::result::Result<(), DriverError>,
619    ) -> std::result::Result<(), DriverError> {
620        if self
621            .confirmation
622            .as_ref()
623            .is_some_and(|proof| proof.is_complete())
624        {
625            // Abort has positively waited for this exact admitted execution,
626            // even if neither private event-record attempt succeeded. This
627            // permits retirement, not reuse of a failed executable.
628            return Ok(());
629        }
630        let mut state = self.state;
631        state.wait(event, || {
632            Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN))
633        })
634    }
635}
636
637/// Per-stream execution fences and a separate, irreversible reuse poison.
638#[derive(Debug, Default)]
639pub(crate) struct ExecutionCompletion {
640    events: BTreeMap<u64, (CudaEvent, ExecutionFence)>,
641    spare: Option<CudaEvent>,
642    uncertain: bool,
643}
644
645impl ExecutionCompletion {
646    pub(crate) fn new(context: &Arc<CudarcContext>) -> std::result::Result<Self, DriverError> {
647        Ok(Self {
648            spare: Some(context.new_event(None)?),
649            ..Self::default()
650        })
651    }
652
653    pub(crate) fn submit(
654        &mut self,
655        stream: &CudaStream,
656        confirmation: Option<Arc<crate::memory::OperationCompletion>>,
657        launch: impl FnOnce() -> std::result::Result<(), DriverError>,
658    ) -> std::result::Result<(), DriverError> {
659        if self.uncertain {
660            return Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN));
661        }
662        let key = crate::cuda_graph::stream_execution_id(stream)?;
663        if let Some(proof) = &confirmation {
664            proof
665                .validate_submission(key)
666                .map_err(|_| DriverError(sys::CUresult::CUDA_ERROR_INVALID_CONTEXT))?;
667        }
668        if let std::collections::btree_map::Entry::Vacant(entry) = self.events.entry(key) {
669            // Allocate before launching. Failure here has not submitted work.
670            let event = match self.spare.take() {
671                Some(event) => event,
672                None => stream.context().new_event(None)?,
673            };
674            entry.insert((event, ExecutionFence::default()));
675        }
676        // Arm before the may-submit boundary, including an unwind before the
677        // completion event can be recorded. An older event cannot prove it done.
678        self.uncertain = true;
679        let (event, completion) = self.events.get_mut(&key).expect("execution event present");
680        let submitted = completion.submit(confirmation, launch, || event.record(stream));
681        if submitted.is_ok() {
682            self.uncertain = false;
683        }
684        submitted
685    }
686
687    pub(crate) fn wait(&self) -> std::result::Result<(), DriverError> {
688        let _ordinary = crate::cuda_graph::reserve_capture_exclusion()?;
689        for (event, completion) in self.events.values() {
690            completion.wait(|| event.synchronize())?;
691        }
692        Ok(())
693    }
694}
695
696// The driver permits module/function handles to be used from threads with the
697// owning context bound. Mutation of the module registry is separately locked.
698unsafe impl Send for LoadedModule {}
699unsafe impl Sync for LoadedModule {}
700
701impl Drop for LoadedModule {
702    fn drop(&mut self) {
703        // Wait for events recorded after this module's submissions, outside
704        // capture. They include preceding stream work and transitive waits.
705        // A context-wide wait could invalidate an unrelated stream capture.
706        let module = self.cu_module as usize;
707        let context = self.context.clone();
708        let completion = std::mem::take(
709            self.completion
710                .get_mut()
711                .unwrap_or_else(|poisoned| poisoned.into_inner()),
712        );
713        crate::cuda_graph::retire_resources_after_completion(
714            (module, context, completion),
715            |(_, context, completion)| context.bind_to_thread().and_then(|()| completion.wait()),
716            |(module, _, _)| {
717                // SAFETY: direct submissions are complete; captured submissions
718                // retained this module through their graph's retirement fence.
719                unsafe { result::module::unload(*module as sys::CUmodule) }
720            },
721            |_| Ok(()),
722            |(_, context, _), error| context.record_err::<()>(Err(error)),
723        );
724    }
725}
726
727/// Kernel handle bound to XLOG's default CUDA stream.
728/// Retains the exact loaded module independently of its registry name.
729/// Releasing the last module owner follows the potentially blocking
730/// [cold-lifecycle progress contract](crate::cuda_graph).
731#[derive(Debug, Clone)]
732pub struct CudaFunction {
733    function_name: Arc<str>,
734    context: Arc<CudarcContext>,
735    stream: Arc<CudaStream>,
736    _module: Arc<LoadedModule>,
737}
738
739impl CudaFunction {
740    // The immutable symbol table and its audited module owner retain the driver
741    // handle. Do not duplicate a raw handle in this otherwise shareable owner,
742    // or resolve the symbol through a possibly replaced module registry entry.
743    fn cu_function(&self) -> sys::CUfunction {
744        self._module.functions[self.function_name.as_ref()]
745    }
746
747    fn captured_launch_binding(
748        &self,
749        cfg: LaunchConfig,
750        parameter_count: usize,
751        cooperative: bool,
752    ) -> CapturedCudaLaunchBinding {
753        CapturedCudaLaunchBinding {
754            module_name: Arc::clone(&self._module.module_name),
755            artifact_name: Arc::clone(&self._module.artifact_name),
756            artifact_sha256: self._module.artifact_sha256,
757            function_name: Arc::clone(&self.function_name),
758            grid_dim: cfg.grid_dim,
759            block_dim: cfg.block_dim,
760            shared_mem_bytes: cfg.shared_mem_bytes,
761            parameter_count: parameter_count as u64,
762            cooperative,
763        }
764    }
765
766    fn submit(
767        &self,
768        stream: &CudaStream,
769        binding: CapturedCudaLaunchBinding,
770        launch: impl FnOnce() -> std::result::Result<(), DriverError>,
771    ) -> std::result::Result<(), DriverError> {
772        if stream.context().cu_ctx() != self.context.cu_ctx() {
773            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_CONTEXT));
774        }
775        crate::cuda_graph::submit_with_capture(stream, |owners| {
776            self.submit_admitted(stream, owners, Some(binding), None, launch)
777        })
778    }
779
780    fn submit_admitted(
781        &self,
782        stream: &CudaStream,
783        owners: Option<&crate::cuda_graph::CaptureOwners>,
784        binding: Option<CapturedCudaLaunchBinding>,
785        confirmation: Option<Arc<crate::memory::OperationCompletion>>,
786        launch: impl FnOnce() -> std::result::Result<(), DriverError>,
787    ) -> std::result::Result<(), DriverError> {
788        if let Some(owners) = owners {
789            let mut owners = owners
790                .lock()
791                .unwrap_or_else(|poisoned| poisoned.into_inner());
792            owners
793                .modules
794                .entry(Arc::as_ptr(&self._module) as usize)
795                .or_insert_with(|| self._module.clone());
796            if let Some(binding) = binding {
797                owners.launches.push(binding);
798            }
799            drop(owners);
800            launch()
801        } else {
802            self._module
803                .completion
804                .lock()
805                .unwrap_or_else(|poisoned| poisoned.into_inner())
806                .submit(stream, confirmation, launch)
807        }
808    }
809
810    pub(crate) unsafe fn launch_raw_in(
811        &self,
812        enqueue: &crate::launch::CudaEnqueue<'_>,
813        cfg: LaunchConfig,
814        params: &mut [*mut c_void],
815        cooperative: bool,
816    ) -> std::result::Result<(), DriverError> {
817        let stream = enqueue.stream();
818        if stream.context().cu_ctx() != self.context.cu_ctx() {
819            return Err(DriverError(sys::CUresult::CUDA_ERROR_INVALID_CONTEXT));
820        }
821        self.context.bind_to_thread()?;
822        let binding = self.captured_launch_binding(cfg, params.len(), cooperative);
823        enqueue.submit(|owners| {
824            self.submit_admitted(
825                stream,
826                owners,
827                Some(binding),
828                Some(enqueue.completion()),
829                || {
830                    if cooperative {
831                        result::launch_cooperative_kernel(
832                            self.cu_function(),
833                            cfg.grid_dim,
834                            cfg.block_dim,
835                            cfg.shared_mem_bytes,
836                            stream.cu_stream(),
837                            params,
838                        )
839                    } else {
840                        result::launch_kernel(
841                            self.cu_function(),
842                            cfg.grid_dim,
843                            cfg.block_dim,
844                            cfg.shared_mem_bytes,
845                            stream.cu_stream(),
846                            params,
847                        )
848                    }
849                },
850            )
851        })
852    }
853    pub(crate) unsafe fn launch_raw(
854        &self,
855        cfg: LaunchConfig,
856        params: &mut [*mut c_void],
857    ) -> std::result::Result<(), DriverError> {
858        self.launch_raw_on_stream(&self.stream, cfg, params)
859    }
860
861    pub(crate) unsafe fn launch_raw_on_stream(
862        &self,
863        stream: &CudaStream,
864        cfg: LaunchConfig,
865        params: &mut [*mut c_void],
866    ) -> std::result::Result<(), DriverError> {
867        self.context.bind_to_thread()?;
868        let parameter_count = params.len();
869        let launch = || {
870            result::launch_kernel(
871                self.cu_function(),
872                cfg.grid_dim,
873                cfg.block_dim,
874                cfg.shared_mem_bytes,
875                stream.cu_stream(),
876                params,
877            )
878        };
879        self.submit(
880            stream,
881            self.captured_launch_binding(cfg, parameter_count, false),
882            launch,
883        )
884    }
885
886    pub(crate) unsafe fn launch_raw_cooperative(
887        &self,
888        cfg: LaunchConfig,
889        params: &mut [*mut c_void],
890    ) -> std::result::Result<(), DriverError> {
891        self.launch_raw_cooperative_on_stream(&self.stream, cfg, params)
892    }
893
894    pub(crate) unsafe fn launch_raw_cooperative_on_stream(
895        &self,
896        stream: &CudaStream,
897        cfg: LaunchConfig,
898        params: &mut [*mut c_void],
899    ) -> std::result::Result<(), DriverError> {
900        self.context.bind_to_thread()?;
901        let parameter_count = params.len();
902        let launch = || {
903            result::launch_cooperative_kernel(
904                self.cu_function(),
905                cfg.grid_dim,
906                cfg.block_dim,
907                cfg.shared_mem_bytes,
908                stream.cu_stream(),
909                params,
910            )
911        };
912        self.submit(
913            stream,
914            self.captured_launch_binding(cfg, parameter_count, true),
915            launch,
916        )
917    }
918
919    pub fn occupancy_available_dynamic_smem_per_block(
920        &self,
921        num_blocks: u32,
922        block_size: u32,
923    ) -> std::result::Result<usize, DriverError> {
924        self.context.bind_to_thread()?;
925        let mut dynamic_smem_size: usize = 0;
926        unsafe {
927            sys::cuOccupancyAvailableDynamicSMemPerBlock(
928                &mut dynamic_smem_size,
929                self.cu_function(),
930                num_blocks as std::ffi::c_int,
931                block_size as std::ffi::c_int,
932            )
933            .result()?
934        };
935        Ok(dynamic_smem_size)
936    }
937
938    pub fn occupancy_max_active_blocks_per_multiprocessor(
939        &self,
940        block_size: u32,
941        dynamic_smem_size: usize,
942        flags: Option<sys::CUoccupancy_flags_enum>,
943    ) -> std::result::Result<u32, DriverError> {
944        self.context.bind_to_thread()?;
945        let mut num_blocks: std::ffi::c_int = 0;
946        let flags = flags.unwrap_or(sys::CUoccupancy_flags_enum::CU_OCCUPANCY_DEFAULT);
947        unsafe {
948            sys::cuOccupancyMaxActiveBlocksPerMultiprocessorWithFlags(
949                &mut num_blocks,
950                self.cu_function(),
951                block_size as std::ffi::c_int,
952                dynamic_smem_size,
953                flags as std::ffi::c_uint,
954            )
955            .result()?
956        };
957        Ok(num_blocks as u32)
958    }
959
960    pub fn occupancy_max_active_clusters(
961        &self,
962        config: LaunchConfig,
963    ) -> std::result::Result<u32, DriverError> {
964        self.context.bind_to_thread()?;
965        let mut num_clusters: std::ffi::c_int = 0;
966        let cfg = sys::CUlaunchConfig {
967            gridDimX: config.grid_dim.0,
968            gridDimY: config.grid_dim.1,
969            gridDimZ: config.grid_dim.2,
970            blockDimX: config.block_dim.0,
971            blockDimY: config.block_dim.1,
972            blockDimZ: config.block_dim.2,
973            sharedMemBytes: config.shared_mem_bytes,
974            hStream: self.stream.cu_stream(),
975            attrs: std::ptr::null_mut(),
976            numAttrs: 0,
977        };
978        unsafe {
979            sys::cuOccupancyMaxActiveClusters(&mut num_clusters, self.cu_function(), &cfg)
980                .result()?
981        };
982        Ok(num_clusters as u32)
983    }
984
985    pub fn occupancy_max_potential_block_size(
986        &self,
987        block_size_to_dynamic_smem_size: extern "C" fn(block_size: std::ffi::c_int) -> usize,
988        dynamic_smem_size: usize,
989        block_size_limit: u32,
990        flags: Option<sys::CUoccupancy_flags_enum>,
991    ) -> std::result::Result<(u32, u32), DriverError> {
992        self.context.bind_to_thread()?;
993        let mut min_grid_size: std::ffi::c_int = 0;
994        let mut block_size: std::ffi::c_int = 0;
995        let flags = flags.unwrap_or(sys::CUoccupancy_flags_enum::CU_OCCUPANCY_DEFAULT);
996        unsafe {
997            sys::cuOccupancyMaxPotentialBlockSizeWithFlags(
998                &mut min_grid_size,
999                &mut block_size,
1000                self.cu_function(),
1001                Some(block_size_to_dynamic_smem_size),
1002                dynamic_smem_size,
1003                block_size_limit as std::ffi::c_int,
1004                flags as std::ffi::c_uint,
1005            )
1006            .result()?
1007        };
1008        Ok((min_grid_size as u32, block_size as u32))
1009    }
1010
1011    pub fn occupancy_max_potential_cluster_size(
1012        &self,
1013        config: LaunchConfig,
1014    ) -> std::result::Result<u32, DriverError> {
1015        self.context.bind_to_thread()?;
1016        let mut cluster_size: std::ffi::c_int = 0;
1017        let cfg = sys::CUlaunchConfig {
1018            gridDimX: config.grid_dim.0,
1019            gridDimY: config.grid_dim.1,
1020            gridDimZ: config.grid_dim.2,
1021            blockDimX: config.block_dim.0,
1022            blockDimY: config.block_dim.1,
1023            blockDimZ: config.block_dim.2,
1024            sharedMemBytes: config.shared_mem_bytes,
1025            hStream: self.stream.cu_stream(),
1026            attrs: std::ptr::null_mut(),
1027            numAttrs: 0,
1028        };
1029        unsafe {
1030            sys::cuOccupancyMaxPotentialClusterSize(&mut cluster_size, self.cu_function(), &cfg)
1031                .result()?
1032        };
1033        Ok(cluster_size as u32)
1034    }
1035
1036    pub fn get_attribute(
1037        &self,
1038        attribute: sys::CUfunction_attribute_enum,
1039    ) -> std::result::Result<i32, DriverError> {
1040        self.context.bind_to_thread()?;
1041        unsafe { result::function::get_function_attribute(self.cu_function(), attribute) }
1042    }
1043
1044    pub fn num_regs(&self) -> std::result::Result<i32, DriverError> {
1045        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_NUM_REGS)
1046    }
1047
1048    pub fn shared_size_bytes(&self) -> std::result::Result<i32, DriverError> {
1049        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_SHARED_SIZE_BYTES)
1050    }
1051
1052    pub fn const_size_bytes(&self) -> std::result::Result<i32, DriverError> {
1053        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_CONST_SIZE_BYTES)
1054    }
1055
1056    pub fn local_size_bytes(&self) -> std::result::Result<i32, DriverError> {
1057        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES)
1058    }
1059
1060    pub fn max_threads_per_block(&self) -> std::result::Result<i32, DriverError> {
1061        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_MAX_THREADS_PER_BLOCK)
1062    }
1063
1064    pub fn ptx_version(&self) -> std::result::Result<i32, DriverError> {
1065        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_PTX_VERSION)
1066    }
1067
1068    pub fn binary_version(&self) -> std::result::Result<i32, DriverError> {
1069        self.get_attribute(sys::CUfunction_attribute_enum::CU_FUNC_ATTRIBUTE_BINARY_VERSION)
1070    }
1071
1072    pub fn set_attribute(
1073        &self,
1074        attribute: sys::CUfunction_attribute_enum,
1075        value: i32,
1076    ) -> std::result::Result<(), DriverError> {
1077        self.context.bind_to_thread()?;
1078        unsafe { result::function::set_function_attribute(self.cu_function(), attribute, value) }
1079    }
1080
1081    pub fn set_function_cache_config(
1082        &self,
1083        config: sys::CUfunc_cache,
1084    ) -> std::result::Result<(), DriverError> {
1085        self.context.bind_to_thread()?;
1086        unsafe { result::function::set_function_cache_config(self.cu_function(), config) }
1087    }
1088}
1089
1090#[derive(Debug)]
1091pub struct CudaDeviceInner {
1092    context: Arc<CudarcContext>,
1093    stream: Arc<CudaStream>,
1094    allocation_stream: Arc<CudaStream>,
1095    modules: RwLock<BTreeMap<String, Arc<LoadedModule>>>,
1096}
1097
1098impl CudaDeviceInner {
1099    fn insert_module(
1100        &self,
1101        module_name: &str,
1102        artifact_name: &str,
1103        artifact_sha256: Option<[u8; 32]>,
1104        cu_module: sys::CUmodule,
1105        kernels: &[&str],
1106    ) -> std::result::Result<(), DriverError> {
1107        // Establish ownership before fallible symbol lookup. A rejected module
1108        // is retired without leaking it or disturbing the previous registry entry.
1109        let mut module = LoadedModule {
1110            cu_module,
1111            functions: BTreeMap::new(),
1112            module_name: Arc::from(module_name),
1113            artifact_name: Arc::from(artifact_name),
1114            artifact_sha256,
1115            context: self.context.clone(),
1116            completion: Mutex::default(),
1117        };
1118        for &kernel in kernels {
1119            let name_c = CString::new(kernel).unwrap();
1120            let cu_function = unsafe { result::module::get_function(cu_module, name_c) }?;
1121            module.functions.insert(Arc::from(kernel), cu_function);
1122        }
1123
1124        let previous = self
1125            .modules
1126            .write()
1127            .unwrap()
1128            .insert(module_name.to_string(), Arc::new(module));
1129        // Do not synchronize while holding the registry lock. Existing function
1130        // owners (including retained session graph functions) keep this image alive.
1131        drop(previous);
1132        Ok(())
1133    }
1134
1135    pub fn stream(&self) -> &Arc<CudaStream> {
1136        &self.stream
1137    }
1138
1139    pub(crate) fn allocation_stream(&self) -> &Arc<CudaStream> {
1140        &self.allocation_stream
1141    }
1142
1143    pub fn has_func(&self, module_name: &str, func_name: &str) -> bool {
1144        let modules = self.modules.read().unwrap();
1145        modules
1146            .get(module_name)
1147            .is_some_and(|module| module.functions.contains_key(func_name))
1148    }
1149
1150    pub fn get_func(&self, module_name: &str, func_name: &str) -> Option<CudaFunction> {
1151        let modules = self.modules.read().unwrap();
1152        let module = modules.get(module_name)?;
1153        let (function_name, _) = module.functions.get_key_value(func_name)?;
1154        Some(CudaFunction {
1155            function_name: Arc::clone(function_name),
1156            context: self.context.clone(),
1157            stream: self.stream.clone(),
1158            _module: module.clone(),
1159        })
1160    }
1161
1162    pub fn load_file(
1163        &self,
1164        path: &Path,
1165        module_name: &str,
1166        kernels: &[&str],
1167    ) -> std::result::Result<(), DriverError> {
1168        let artifact_name = path
1169            .file_name()
1170            .and_then(|name| name.to_str())
1171            .unwrap_or("file-artifact")
1172            .to_string();
1173        if let Ok(bytes) = std::fs::read(path) {
1174            let is_cubin = path.extension().and_then(|value| value.to_str()) == Some("cubin");
1175            return self.load_artifact_bytes(
1176                &bytes,
1177                is_cubin,
1178                module_name,
1179                &artifact_name,
1180                kernels,
1181            );
1182        }
1183        crate::cuda_graph::with_module_loading(|| {
1184            self.context.bind_to_thread()?;
1185            let name_c = CString::new(path.to_string_lossy().as_bytes()).unwrap();
1186            let cu_module = result::module::load(name_c)?;
1187            self.insert_module(module_name, &artifact_name, None, cu_module, kernels)
1188        })
1189    }
1190
1191    pub(crate) fn load_artifact_bytes(
1192        &self,
1193        bytes: &[u8],
1194        is_cubin: bool,
1195        module_name: &str,
1196        artifact_name: &str,
1197        kernels: &[&str],
1198    ) -> std::result::Result<(), DriverError> {
1199        let artifact_sha256 = Sha256::digest(bytes).into();
1200        crate::cuda_graph::with_module_loading(|| {
1201            self.context.bind_to_thread()?;
1202            let cu_module = if is_cubin {
1203                unsafe { result::module::load_data(bytes.as_ptr().cast()) }?
1204            } else {
1205                let source = CString::new(bytes).unwrap();
1206                unsafe { result::module::load_data(source.as_ptr().cast()) }?
1207            };
1208            self.insert_module(
1209                module_name,
1210                artifact_name,
1211                Some(artifact_sha256),
1212                cu_module,
1213                kernels,
1214            )
1215        })
1216    }
1217
1218    pub fn load_ptx(
1219        &self,
1220        ptx: Ptx,
1221        module_name: &str,
1222        kernels: &[&str],
1223    ) -> std::result::Result<(), DriverError> {
1224        self.load_ptx_named(ptx, module_name, "in-memory-ptx", kernels)
1225    }
1226
1227    pub(crate) fn load_ptx_named(
1228        &self,
1229        ptx: Ptx,
1230        module_name: &str,
1231        artifact_name: &str,
1232        kernels: &[&str],
1233    ) -> std::result::Result<(), DriverError> {
1234        crate::cuda_graph::with_module_loading(|| {
1235            self.context.bind_to_thread()?;
1236            let (cu_module, artifact_sha256) = if let Some(bytes) = ptx.as_bytes() {
1237                let digest = Sha256::digest(bytes).into();
1238                (
1239                    unsafe { result::module::load_data(bytes.as_ptr() as *const _) }?,
1240                    digest,
1241                )
1242            } else {
1243                let source = ptx.to_src();
1244                let digest = Sha256::digest(source.as_bytes()).into();
1245                let src = CString::new(source).unwrap();
1246                (
1247                    unsafe { result::module::load_data(src.as_ptr() as *const _) }?,
1248                    digest,
1249                )
1250            };
1251            self.insert_module(
1252                module_name,
1253                artifact_name,
1254                Some(artifact_sha256),
1255                cu_module,
1256                kernels,
1257            )
1258        })
1259    }
1260
1261    /// Allocate an uninitialized device slice on this device stream.
1262    ///
1263    /// # Safety
1264    ///
1265    /// The caller must initialize the returned allocation before any device or
1266    /// host read observes its contents.
1267    pub unsafe fn alloc<T: DeviceRepr + 'static>(
1268        &self,
1269        len: usize,
1270    ) -> ResourceResult<DeviceMemoryView<T>> {
1271        DeviceMemoryView::allocate(
1272            Arc::clone(&self.stream),
1273            Arc::clone(&self.allocation_stream),
1274            len,
1275        )
1276    }
1277
1278    pub fn alloc_zeros<T: DeviceRepr + ValidAsZeroBits + 'static>(
1279        &self,
1280        len: usize,
1281    ) -> ResourceResult<DeviceMemoryView<T>> {
1282        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1283        let mut allocation = DeviceMemoryView::allocate(
1284            Arc::clone(&self.stream),
1285            Arc::clone(&self.allocation_stream),
1286            len,
1287        )?;
1288        self.memset_zeros(&mut allocation)?;
1289        Ok(allocation)
1290    }
1291
1292    pub fn memset_zeros<T: DeviceRepr + ValidAsZeroBits, Dst: DeviceWrite<T>>(
1293        &self,
1294        dst: &mut Dst,
1295    ) -> ResourceResult<()> {
1296        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1297        let dst = dst.device_view();
1298        with_memory_access(
1299            Arc::clone(&self.stream),
1300            vec![dst.access(Access::Write)?],
1301            |proof| {
1302                self.stream.memset_zeros(&mut proof.write(&dst)?)?;
1303                Ok(self.stream.synchronize()?)
1304            },
1305        )
1306    }
1307
1308    pub fn htod_sync_copy_into<T: DeviceRepr, Dst: DeviceWrite<T>, Src: HostSlice<T> + ?Sized>(
1309        &self,
1310        src: &Src,
1311        dst: &mut Dst,
1312    ) -> ResourceResult<()> {
1313        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1314        let dst = dst.device_view().try_slice(..src.len()).ok_or_else(|| {
1315            crate::device_runtime::ResourceError::StreamMisuse(
1316                "host-to-device copy destination is shorter than its source".into(),
1317            )
1318        })?;
1319        let bytes = src
1320            .len()
1321            .checked_mul(std::mem::size_of::<T>())
1322            .ok_or_else(|| {
1323                crate::device_runtime::ResourceError::StreamMisuse(
1324                    "host copy byte size overflow".into(),
1325                )
1326            })?;
1327        let mut staging = PinnedHostBuffer::new(&self.stream, bytes)?;
1328        {
1329            // SAFETY: HostSlice supplies its recorded producer dependencies.
1330            // Complete those waits before reading even a pinned host slice.
1331            let (source, _guard) = unsafe { src.stream_synced_slice(&self.stream) };
1332            self.stream.synchronize()?;
1333            staging.write(source)?;
1334        }
1335        with_memory_access(
1336            Arc::clone(&self.stream),
1337            vec![dst.access(Access::Write)?],
1338            |proof| {
1339                let mut destination = proof.write(&dst)?;
1340                let (ptr, _guard) = destination.device_ptr_mut(&self.stream);
1341                // SAFETY: the complete destination is admitted; only owned
1342                // staging storage participates in DMA, including on error.
1343                unsafe {
1344                    staging.enqueue(&self.stream, |host| {
1345                        sys::cuMemcpyHtoDAsync_v2(ptr, host.cast(), bytes, self.stream.cu_stream())
1346                            .result()
1347                    })?;
1348                }
1349                Ok(staging.wait()?)
1350            },
1351        )
1352    }
1353
1354    pub fn dtoh_sync_copy_into<T: DeviceRepr, Src: DeviceRead<T>, Dst: HostSlice<T> + ?Sized>(
1355        &self,
1356        src: &Src,
1357        dst: &mut Dst,
1358    ) -> ResourceResult<()> {
1359        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1360        let src = src.device_view();
1361        let len = src.len();
1362        if dst.len() < len {
1363            return Err(crate::device_runtime::ResourceError::StreamMisuse(
1364                "device-to-host copy destination is shorter than its source".into(),
1365            ));
1366        }
1367        let bytes = len.checked_mul(std::mem::size_of::<T>()).ok_or_else(|| {
1368            crate::device_runtime::ResourceError::StreamMisuse(
1369                "host copy byte size overflow".into(),
1370            )
1371        })?;
1372        let mut staging = PinnedHostBuffer::new(&self.stream, bytes)?;
1373        with_memory_access(
1374            Arc::clone(&self.stream),
1375            vec![src.access(Access::Read)?],
1376            |proof| {
1377                let source = proof.read(&src)?;
1378                let (ptr, _guard) = source.device_ptr(&self.stream);
1379                // SAFETY: admitted source and uniquely owned pinned destination
1380                // remain retained through the transaction's failure cleanup.
1381                unsafe {
1382                    staging.enqueue(&self.stream, |host| {
1383                        sys::cuMemcpyDtoHAsync_v2(host.cast(), ptr, bytes, self.stream.cu_stream())
1384                            .result()
1385                    })?;
1386                }
1387                Ok(staging.wait()?)
1388            },
1389        )?;
1390        // SAFETY: synchronize HostSlice's own dependencies before CPU writes.
1391        let (destination, _guard) = unsafe { dst.stream_synced_mut_slice(&self.stream) };
1392        self.stream.synchronize()?;
1393        // SAFETY: the admitted T source initialized every staging byte and its
1394        // completion was proven before the caller's storage is touched.
1395        unsafe { staging.read_into(&mut destination[..len])? };
1396        Ok(())
1397    }
1398
1399    pub fn htod_sync_copy<T: DeviceRepr + 'static, Src: HostSlice<T> + ?Sized>(
1400        &self,
1401        src: &Src,
1402    ) -> ResourceResult<DeviceMemoryView<T>> {
1403        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1404        let mut dst = DeviceMemoryView::allocate(
1405            Arc::clone(&self.stream),
1406            Arc::clone(&self.allocation_stream),
1407            src.len(),
1408        )?;
1409        self.htod_sync_copy_into(src, &mut dst)?;
1410        Ok(dst)
1411    }
1412
1413    pub fn dtoh_sync_copy<T: DeviceRepr, Src: DeviceRead<T>>(
1414        &self,
1415        src: &Src,
1416    ) -> ResourceResult<Vec<T>> {
1417        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1418        let src = src.device_view();
1419        let len = src.len();
1420        let bytes = len.checked_mul(std::mem::size_of::<T>()).ok_or_else(|| {
1421            crate::device_runtime::ResourceError::StreamMisuse(
1422                "host copy byte size overflow".into(),
1423            )
1424        })?;
1425        let mut staging = PinnedHostBuffer::new(&self.stream, bytes)?;
1426        with_memory_access(
1427            Arc::clone(&self.stream),
1428            vec![src.access(Access::Read)?],
1429            |proof| {
1430                let source = proof.read(&src)?;
1431                let (ptr, _guard) = source.device_ptr(&self.stream);
1432                // SAFETY: ownership is admitted and the pinned destination is
1433                // outside the callback, so unwind cannot destroy it before abort.
1434                unsafe {
1435                    staging.enqueue(&self.stream, |host| {
1436                        sys::cuMemcpyDtoHAsync_v2(host.cast(), ptr, bytes, self.stream.cu_stream())
1437                            .result()
1438                    })?;
1439                }
1440                Ok(staging.wait()?)
1441            },
1442        )?;
1443        // SAFETY: the whole admitted device source was copied and synchronized.
1444        Ok(unsafe { staging.read_vec(len)? })
1445    }
1446
1447    pub fn dtod_copy<T, Src: DeviceRead<T>, Dst: DeviceWrite<T>>(
1448        &self,
1449        src: &Src,
1450        dst: &mut Dst,
1451    ) -> ResourceResult<()> {
1452        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1453        let src = src.device_view();
1454        let dst = dst.device_view().try_slice(..src.len()).ok_or_else(|| {
1455            crate::device_runtime::ResourceError::StreamMisuse(
1456                "device copy destination is shorter than its source".into(),
1457            )
1458        })?;
1459        with_memory_access(
1460            Arc::clone(&self.stream),
1461            vec![src.access(Access::Read)?, dst.access(Access::Write)?],
1462            |proof| {
1463                proof.copy(&src, &dst)?;
1464                Ok(self.stream.synchronize()?)
1465            },
1466        )
1467    }
1468
1469    /// Enqueue a device-to-device copy without synchronizing the stream.
1470    ///
1471    /// Callers batching many copies must synchronize once after the last
1472    /// enqueue and before reading any destination.
1473    pub fn dtod_copy_async<T, Src: DeviceRead<T>, Dst: DeviceWrite<T>>(
1474        &self,
1475        src: &Src,
1476        dst: &mut Dst,
1477    ) -> ResourceResult<()> {
1478        let src = src.device_view();
1479        let dst = dst.device_view().try_slice(..src.len()).ok_or_else(|| {
1480            crate::device_runtime::ResourceError::StreamMisuse(
1481                "device copy destination is shorter than its source".into(),
1482            )
1483        })?;
1484        with_memory_access(
1485            Arc::clone(&self.stream),
1486            vec![src.access(Access::Read)?, dst.access(Access::Write)?],
1487            |proof| proof.copy(&src, &dst),
1488        )
1489    }
1490
1491    /// Wrap an existing CUDA device pointer in a typed cudarc slice.
1492    ///
1493    /// # Safety
1494    ///
1495    /// `cu_device_ptr` must point to a live allocation containing at least
1496    /// `len * size_of::<T>()` bytes, and the resulting wrapper must not outlive
1497    /// the allocation or alias another owner that will free it independently.
1498    pub unsafe fn upgrade_device_ptr<T>(
1499        &self,
1500        cu_device_ptr: sys::CUdeviceptr,
1501        len: usize,
1502    ) -> CudaSlice<T> {
1503        self.stream.upgrade_device_ptr(cu_device_ptr, len)
1504    }
1505
1506    pub fn attribute(
1507        &self,
1508        attrib: sys::CUdevice_attribute,
1509    ) -> std::result::Result<i32, DriverError> {
1510        self.context.attribute(attrib)
1511    }
1512
1513    pub fn synchronize(&self) -> std::result::Result<(), DriverError> {
1514        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(&self.stream)?;
1515        self.stream.synchronize()
1516    }
1517
1518    pub fn ordinal(&self) -> usize {
1519        self.context.ordinal()
1520    }
1521}
1522
1523/// CUDA device wrapper for GPU operations.
1524///
1525/// This keeps XLOG's historical "device with a built-in default stream" API,
1526/// but is backed by cudarc's newer `CudaContext` and `CudaStream`.
1527pub struct CudaDevice {
1528    device: Arc<CudaDeviceInner>,
1529}
1530
1531impl CudaDevice {
1532    /// Create a new CUDA device on the specified GPU ordinal.
1533    pub fn new(ordinal: usize) -> Result<Self> {
1534        let context = std::panic::catch_unwind(|| CudarcContext::new(ordinal))
1535            .map_err(|_| {
1536                XlogError::Kernel(format!(
1537                    "Failed to create CUDA device {}: cudarc panicked during driver initialization",
1538                    ordinal
1539                ))
1540            })?
1541            .map_err(|e| {
1542                XlogError::Kernel(format!("Failed to create CUDA device {}: {}", ordinal, e))
1543            })?;
1544
1545        let stream = context.default_stream();
1546        let allocation_stream = context.new_stream().map_err(|error| {
1547            XlogError::Kernel(format!(
1548                "Failed to create CUDA allocation stream on device {}: {}",
1549                ordinal, error
1550            ))
1551        })?;
1552        Ok(Self {
1553            device: Arc::new(CudaDeviceInner {
1554                context,
1555                stream,
1556                allocation_stream,
1557                modules: RwLock::new(BTreeMap::new()),
1558            }),
1559        })
1560    }
1561
1562    pub fn count() -> Result<i32> {
1563        std::panic::catch_unwind(|| {
1564            result::init()?;
1565            result::device::get_count()
1566        })
1567        .map_err(|_| {
1568            XlogError::Kernel(
1569                "Failed to count CUDA devices: cudarc panicked during driver initialization"
1570                    .to_string(),
1571            )
1572        })?
1573        .map_err(|e| XlogError::Kernel(format!("Failed to count CUDA devices: {}", e)))
1574    }
1575
1576    pub fn synchronize(&self) -> Result<()> {
1577        self.device
1578            .synchronize()
1579            .map_err(|e| XlogError::Kernel(format!("Failed to synchronize device: {}", e)))
1580    }
1581
1582    pub fn inner(&self) -> &Arc<CudaDeviceInner> {
1583        &self.device
1584    }
1585
1586    pub fn ordinal(&self) -> usize {
1587        self.device.ordinal()
1588    }
1589}
1590
1591// Compile-time assertion: CudaDevice must be Send so pyxlog can use py.allow_threads().
1592const _: () = {
1593    fn _assert_send<T: Send>() {}
1594    fn _check() {
1595        _assert_send::<CudaDevice>();
1596    }
1597};
1598
1599#[cfg(test)]
1600mod tests {
1601    use super::*;
1602
1603    #[test]
1604    fn semantic_native_owner_chain_is_send_and_sync() {
1605        fn assert_send_sync<T: Send + Sync>() {}
1606        assert_send_sync::<CudaFunction>();
1607        assert_send_sync::<crate::SemanticHypergraph>();
1608        assert_send_sync::<crate::SemanticTransitionSession>();
1609        assert_send_sync::<crate::SemanticPublishedLease>();
1610        assert_send_sync::<crate::SemanticTensorContentWitness>();
1611    }
1612    use cudarc::driver::DevicePtrMut;
1613
1614    #[test]
1615    #[ignore = "requires authorized CUDA execution"]
1616    fn exact_abort_releases_real_graph_after_failed_completion_records() {
1617        use crate::cuda_graph::{
1618            reap_capture_retirements, retire_resources_after_completion, CapturedCudaGraph,
1619        };
1620        use crate::device_runtime::{
1621            AsyncCudaResource, ResourceError, StreamPool, XlogDeviceRuntime,
1622        };
1623        use crate::memory::{with_memory_manifest, MemoryAccessManifest};
1624
1625        let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
1626        let device = Arc::new(CudaDevice::new(0).unwrap());
1627        let stream = device.inner().stream().context().new_stream().unwrap();
1628        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
1629        let resource = Box::new(AsyncCudaResource::new(
1630            Arc::clone(&device),
1631            0,
1632            Arc::clone(&pool),
1633        ));
1634        let runtime = XlogDeviceRuntime::with_resource(Arc::clone(&device), 0, pool, resource);
1635        let image = || {
1636            Ptx::from_src(
1637                ".version 7.0\n.target sm_75\n.address_size 64\n.visible .entry run() { ret; }\n",
1638            )
1639        };
1640        device
1641            .inner()
1642            .load_ptx(image(), "abort_completion_module", &["run"])
1643            .unwrap();
1644        let function = device
1645            .inner()
1646            .get_func("abort_completion_module", "run")
1647            .unwrap();
1648        let module = Arc::downgrade(&function._module);
1649        let graph = CapturedCudaGraph::capture_on_stream(&stream, || {
1650            // SAFETY: this kernel has no arguments or memory accesses.
1651            unsafe {
1652                function.launch_raw_on_stream(
1653                    &stream,
1654                    LaunchConfig {
1655                        grid_dim: (1, 1, 1),
1656                        block_dim: (1, 1, 1),
1657                        shared_mem_bytes: 0,
1658                    },
1659                    &mut [],
1660                )
1661            }
1662            .map_err(|error| XlogError::Kernel(error.to_string()))
1663        })
1664        .unwrap()
1665        .bind_resident_lifecycle(&runtime);
1666        drop(function);
1667        device
1668            .inner()
1669            .load_ptx(image(), "abort_completion_module", &["run"])
1670            .unwrap();
1671        assert!(module.upgrade().is_some());
1672        let live = runtime.resident_graph_handle_lifecycle_stats();
1673        assert_eq!((live.live_graphs, live.live_graph_execs), (1, 1));
1674
1675        let first_error = DriverError(sys::CUresult::CUDA_ERROR_INVALID_VALUE);
1676        let repair_error = DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN);
1677        let mut records = 0;
1678        let mut launches = 0;
1679        let mut proof = None;
1680        let result: ResourceResult<()> = with_memory_manifest(
1681            Arc::clone(&stream),
1682            Arc::new(MemoryAccessManifest::default()),
1683            |enqueue| {
1684                let confirmation = enqueue.completion();
1685                proof = Some(Arc::clone(&confirmation));
1686                let event = stream.context().new_event(None).unwrap();
1687                let mut fence = ExecutionFence::default();
1688                // Inject record failures at the shared fence, around a real
1689                // launch. This is not public graph event-record fault injection.
1690                let submitted = enqueue.submit(|_| {
1691                    fence.submit(
1692                        Some(confirmation),
1693                        || {
1694                            launches += 1;
1695                            // SAFETY: the graph and its captured module are owned
1696                            // until retirement; this is their original context.
1697                            unsafe { sys::cuGraphLaunch(graph.exec(), stream.cu_stream()).result() }
1698                        },
1699                        || {
1700                            records += 1;
1701                            Err(if records == 1 {
1702                                first_error
1703                            } else {
1704                                repair_error
1705                            })
1706                        },
1707                    )
1708                });
1709                let unconfirmed = fence.wait(|| event.synchronize());
1710                retire_resources_after_completion(
1711                    (fence, event, Some(graph)),
1712                    |(fence, event, _)| fence.wait(|| event.synchronize()),
1713                    |(_, _, graph)| {
1714                        drop(graph.take());
1715                        Ok(())
1716                    },
1717                    |_| Ok(()),
1718                    |_, error| eprintln!("injected completion-record failure: {error}"),
1719                );
1720                // Assertions come after transferring the actual graph into
1721                // retirement, so a failed assertion cannot drop an in-flight
1722                // raw launch through the graph's unsubmitted ordinary fence.
1723                assert_eq!(submitted, Err(first_error));
1724                assert_eq!(unconfirmed, Err(repair_error));
1725                assert_eq!(runtime.resident_graph_handle_lifecycle_stats(), live);
1726                // Returning the original error invokes MemoryOperationOwner's
1727                // real exact-stream abort, not a test synchronization callback.
1728                submitted.map_err(|error| ResourceError::Driver(error.to_string()))
1729            },
1730        );
1731        let expected = crate::launch::LaunchEnqueueError::Operation(ResourceError::Driver(
1732            first_error.to_string(),
1733        ))
1734        .to_string();
1735        assert!(matches!(result, Err(ResourceError::Driver(error)) if error == expected));
1736        assert!(proof.unwrap().is_complete());
1737        assert_eq!((launches, records), (1, 2));
1738        reap_capture_retirements();
1739        let retired = runtime.resident_graph_handle_lifecycle_stats();
1740        assert_eq!((retired.live_graphs, retired.live_graph_execs), (0, 0));
1741        assert_eq!(retired.destroyed_graphs, live.destroyed_graphs + 1);
1742        assert_eq!(
1743            retired.destroyed_graph_execs,
1744            live.destroyed_graph_execs + 1
1745        );
1746        // This weak pointer checks Rust module ownership, not driver unload.
1747        assert!(module.upgrade().is_none());
1748        reap_capture_retirements();
1749        assert_eq!(runtime.resident_graph_handle_lifecycle_stats(), retired);
1750    }
1751
1752    #[test]
1753    fn captured_function_outlives_replacement_and_external_owners() {
1754        if std::env::var("XLOG_REQUIRE_CUDA").as_deref() != Ok("1") {
1755            return;
1756        }
1757        let device = CudaDevice::new(0).unwrap();
1758        let stream = device.inner().stream().context().new_stream().unwrap();
1759        let joined = stream.context().new_stream().unwrap();
1760        let fork = stream.context().new_event(None).unwrap();
1761        let join = stream.context().new_event(None).unwrap();
1762        let mut output = device.inner().stream().alloc_zeros::<u32>(1).unwrap();
1763        let (mut address, output_record) = output.device_ptr_mut(&joined);
1764        drop(output_record);
1765        joined.synchronize().unwrap();
1766        let image = |value: u32| {
1767            Ptx::from_src(format!(
1768                ".version 7.0\n.target sm_75\n.address_size 64\n\
1769             .visible .entry write_value(.param .u64 output) {{\n\
1770             .reg .b64 address;\nld.param.u64 address, [output];\n\
1771             st.global.u32 [address], {value};\nret;\n}}\n"
1772            ))
1773        };
1774        device
1775            .inner()
1776            .load_ptx(image(7), "captured_module", &["write_value"])
1777            .unwrap();
1778        let function = device
1779            .inner()
1780            .get_func("captured_module", "write_value")
1781            .unwrap();
1782        let module = Arc::downgrade(&function._module);
1783        let graph = crate::cuda_graph::CapturedCudaGraph::capture_on_stream(&stream, || {
1784            fork.record(&stream).unwrap();
1785            joined.wait(&fork).unwrap();
1786            let mut params = [(&mut address as *mut u64).cast::<c_void>()];
1787            // SAFETY: one thread writes this live one-u32 allocation on its own
1788            // context. The allocation remains owned through graph replay below.
1789            unsafe {
1790                function.launch_raw_on_stream(
1791                    &joined,
1792                    LaunchConfig {
1793                        grid_dim: (1, 1, 1),
1794                        block_dim: (1, 1, 1),
1795                        shared_mem_bytes: 0,
1796                    },
1797                    &mut params,
1798                )
1799            }
1800            .map_err(|error| XlogError::Kernel(error.to_string()))?;
1801            drop(function);
1802            join.record(&joined).unwrap();
1803            stream.wait(&join).unwrap();
1804            Ok(())
1805        })
1806        .unwrap();
1807        device
1808            .inner()
1809            .load_ptx(image(11), "captured_module", &["write_value"])
1810            .unwrap();
1811        drop(device);
1812        assert!(module.upgrade().is_some());
1813        graph.launch(&stream).unwrap();
1814        // No external wait: graph retirement must retain its module until its
1815        // submitted execution completes, then release that exact module.
1816        drop(graph);
1817        assert!(module.upgrade().is_none());
1818        assert_eq!(stream.clone_dtoh(&output).unwrap(), [7]);
1819    }
1820
1821    #[test]
1822    #[ignore = "requires authorized CUDA execution"]
1823    fn submission_error_repairs_prefix_without_invalidating_external_capture() {
1824        struct ExternalCapture {
1825            stream: Arc<CudaStream>,
1826            active: bool,
1827        }
1828        impl ExternalCapture {
1829            fn end(&mut self) -> sys::CUresult {
1830                if !self.active {
1831                    return sys::CUresult::CUDA_SUCCESS;
1832                }
1833                self.active = false;
1834                let mut graph = std::ptr::null_mut();
1835                // SAFETY: this guard owns the external capture; EndCapture
1836                // terminates it even when the captured sequence was invalidated.
1837                let ended = unsafe { sys::cuStreamEndCapture(self.stream.cu_stream(), &mut graph) };
1838                if !graph.is_null() {
1839                    // SAFETY: EndCapture just returned this uninstantiated graph.
1840                    let destroyed = unsafe { sys::cuGraphDestroy(graph) };
1841                    if destroyed != sys::CUresult::CUDA_SUCCESS {
1842                        eprintln!("external captured graph cleanup returned {destroyed:?}");
1843                        if ended == sys::CUresult::CUDA_SUCCESS {
1844                            return destroyed;
1845                        }
1846                    }
1847                }
1848                ended
1849            }
1850        }
1851        impl Drop for ExternalCapture {
1852            fn drop(&mut self) {
1853                let ended = self.end();
1854                if ended != sys::CUresult::CUDA_SUCCESS {
1855                    eprintln!("external capture cleanup returned {ended:?}");
1856                }
1857            }
1858        }
1859        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
1860        let device = CudaDevice::new(0).unwrap();
1861        let stream = device.inner().stream().context().new_stream().unwrap();
1862        let external = stream.context().new_stream().unwrap();
1863        device.inner().load_ptx(
1864            Ptx::from_src(".version 7.0\n.target sm_75\n.address_size 64\n.visible .entry run() { ret; }\n"),
1865            "unconfirmed_module", &["run"],
1866        ).unwrap();
1867        let function = device
1868            .inner()
1869            .get_func("unconfirmed_module", "run")
1870            .unwrap();
1871        let module = Arc::downgrade(&function._module);
1872        // Deliberately bypass the XLOG capture registry, as another library
1873        // sharing this CUDA primary context can do.
1874        unsafe {
1875            sys::cuStreamBeginCapture_v2(
1876                external.cu_stream(),
1877                sys::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED,
1878            )
1879            .result()
1880            .unwrap();
1881        }
1882        let mut external_capture = ExternalCapture {
1883            stream: Arc::clone(&external),
1884            active: true,
1885        };
1886        let submitted = function
1887            ._module
1888            .completion
1889            .lock()
1890            .unwrap()
1891            .submit(&stream, None, || {
1892                // Fault injection after a real enqueue, before a completion event:
1893                // an older completed event would not prove this launch complete.
1894                unsafe {
1895                    result::launch_kernel(
1896                        function.cu_function(),
1897                        (1, 1, 1),
1898                        (1, 1, 1),
1899                        0,
1900                        stream.cu_stream(),
1901                        &mut [],
1902                    )?;
1903                }
1904                Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN))
1905            });
1906        assert_eq!(
1907            submitted,
1908            Err(DriverError(sys::CUresult::CUDA_ERROR_UNKNOWN))
1909        );
1910        assert!(function._module.completion.lock().unwrap().uncertain);
1911        drop(function);
1912        drop(device);
1913        let ended = external_capture.end();
1914        assert_eq!(
1915            ended,
1916            sys::CUresult::CUDA_SUCCESS,
1917            "retirement invalidated external capture"
1918        );
1919        stream.context().bind_to_thread().unwrap();
1920        assert!(
1921            module.upgrade().is_none(),
1922            "confirmed prefix repair did not retire the module"
1923        );
1924        stream.synchronize().unwrap();
1925    }
1926
1927    #[test]
1928    fn retiring_unrelated_module_does_not_invalidate_stream_capture() {
1929        if std::env::var("XLOG_REQUIRE_CUDA").as_deref() != Ok("1") {
1930            return;
1931        }
1932        let device = CudaDevice::new(0).unwrap();
1933        let stream = device.inner().context.new_stream().unwrap();
1934        let image = || {
1935            Ptx::from_src(
1936                ".version 7.0\n.target sm_75\n.address_size 64\n\
1937                 .visible .entry unused_function() { ret; }\n",
1938            )
1939        };
1940        device
1941            .inner()
1942            .load_ptx(image(), "retired_module", &["unused_function"])
1943            .unwrap();
1944        let original = device
1945            .inner()
1946            .get_func("retired_module", "unused_function")
1947            .unwrap();
1948        device
1949            .inner()
1950            .load_ptx(image(), "retired_module", &["unused_function"])
1951            .unwrap();
1952        let graph = crate::cuda_graph::CapturedCudaGraph::capture_on_stream(&stream, || {
1953            drop(original);
1954            Ok(())
1955        });
1956        let _graph = graph.expect("module retirement invalidated capture");
1957        stream.synchronize().unwrap();
1958    }
1959
1960    #[test]
1961    fn loaded_functions_survive_module_replacement_and_device_owner_drop() {
1962        if std::env::var("XLOG_REQUIRE_CUDA").as_deref() != Ok("1") {
1963            return;
1964        }
1965        let device = CudaDevice::new(0).unwrap();
1966        let stream = Arc::clone(device.inner().stream());
1967        let mut output = device.inner().stream().alloc_zeros::<u32>(1).unwrap();
1968        let image = |value: u32| {
1969            Ptx::from_src(format!(
1970                ".version 7.0\n.target sm_75\n.address_size 64\n\
1971                 .visible .entry write_value(.param .u64 output) {{\n\
1972                 .reg .b64 address;\nld.param.u64 address, [output];\n\
1973                 st.global.u32 [address], {value};\nret;\n}}\n"
1974            ))
1975        };
1976        let mut run = |function: &CudaFunction, expected: u32| {
1977            {
1978                let (mut address, _record) = output.device_ptr_mut(&stream);
1979                let mut params = [(&mut address as *mut u64).cast::<c_void>()];
1980                // SAFETY: this one-thread kernel writes one u32 in this live
1981                // allocation, on its owning context and stream. Wait below before
1982                // reading or reusing the allocation or releasing the function.
1983                unsafe {
1984                    function.launch_raw(
1985                        LaunchConfig {
1986                            grid_dim: (1, 1, 1),
1987                            block_dim: (1, 1, 1),
1988                            shared_mem_bytes: 0,
1989                        },
1990                        &mut params,
1991                    )
1992                }
1993                .unwrap();
1994            }
1995            let result = stream.clone_dtoh(&output).unwrap();
1996            stream.synchronize().unwrap();
1997            assert_eq!(result, [expected]);
1998        };
1999        device
2000            .inner()
2001            .load_ptx(image(7), "retained_module", &["write_value"])
2002            .unwrap();
2003        let original = device
2004            .inner()
2005            .get_func("retained_module", "write_value")
2006            .unwrap();
2007        run(&original, 7);
2008        assert!(device
2009            .inner()
2010            .load_ptx(image(11), "retained_module", &["missing_function"])
2011            .is_err());
2012        run(
2013            &device
2014                .inner()
2015                .get_func("retained_module", "write_value")
2016                .unwrap(),
2017            7,
2018        );
2019        device
2020            .inner()
2021            .load_ptx(image(11), "retained_module", &["write_value"])
2022            .unwrap();
2023        let replacement = device
2024            .inner()
2025            .get_func("retained_module", "write_value")
2026            .unwrap();
2027        run(&original, 7);
2028        run(&replacement, 11);
2029        drop(device);
2030        run(&original, 7);
2031        run(&replacement, 11);
2032    }
2033
2034    #[test]
2035    fn test_device_creation() {
2036        let device = match CudaDevice::new(0) {
2037            Ok(d) => d,
2038            Err(e) => {
2039                eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
2040                return;
2041            }
2042        };
2043        drop(device);
2044    }
2045
2046    #[test]
2047    fn test_device_synchronize() {
2048        let device = match CudaDevice::new(0) {
2049            Ok(d) => d,
2050            Err(e) => {
2051                eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
2052                return;
2053            }
2054        };
2055        let result = device.synchronize();
2056        assert!(result.is_ok(), "Failed to synchronize: {:?}", result.err());
2057    }
2058
2059    #[test]
2060    fn test_device_ordinal() {
2061        let device = match CudaDevice::new(0) {
2062            Ok(d) => d,
2063            Err(e) => {
2064                eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
2065                return;
2066            }
2067        };
2068        assert_eq!(device.ordinal(), 0);
2069    }
2070
2071    #[test]
2072    fn test_device_inner_access() {
2073        let device = match CudaDevice::new(0) {
2074            Ok(d) => d,
2075            Err(e) => {
2076                eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
2077                return;
2078            }
2079        };
2080        let inner = device.inner();
2081        assert_eq!(inner.ordinal(), 0);
2082    }
2083
2084    #[test]
2085    fn test_invalid_device_ordinal() {
2086        let result = CudaDevice::new(9999);
2087        assert!(result.is_err(), "Should fail with invalid ordinal");
2088
2089        if let Err(XlogError::Kernel(msg)) = result {
2090            assert!(msg.contains("9999"), "Error should mention device ordinal");
2091        } else {
2092            panic!("Expected XlogError::Kernel");
2093        }
2094    }
2095}