1use 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 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 *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#[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 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 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 Ok(unsafe { self.read_vec(bytes)? })
151 }
152
153 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 || 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 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 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 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 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 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 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 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 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#[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 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 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#[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 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 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
696unsafe impl Send for LoadedModule {}
699unsafe impl Sync for LoadedModule {}
700
701impl Drop for LoadedModule {
702 fn drop(&mut self) {
703 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 unsafe { result::module::unload(*module as sys::CUmodule) }
720 },
721 |_| Ok(()),
722 |(_, context, _), error| context.record_err::<()>(Err(error)),
723 );
724 }
725}
726
727#[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 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 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 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 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 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 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 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 let (destination, _guard) = unsafe { dst.stream_synced_mut_slice(&self.stream) };
1392 self.stream.synchronize()?;
1393 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 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 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 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 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
1523pub struct CudaDevice {
1528 device: Arc<CudaDeviceInner>,
1529}
1530
1531impl CudaDevice {
1532 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
1591const _: () = {
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 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 let submitted = enqueue.submit(|_| {
1691 fence.submit(
1692 Some(confirmation),
1693 || {
1694 launches += 1;
1695 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 assert_eq!(submitted, Err(first_error));
1724 assert_eq!(unconfirmed, Err(repair_error));
1725 assert_eq!(runtime.resident_graph_handle_lifecycle_stats(), live);
1726 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 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 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 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 let ended = unsafe { sys::cuStreamEndCapture(self.stream.cu_stream(), &mut graph) };
1838 if !graph.is_null() {
1839 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 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 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 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}