1use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::io::Write;
6use std::sync::{Arc, Mutex, OnceLock};
7
8use cubecl::client::ComputeClient;
9use cubecl::stream_id::StreamId;
10use cubecl::Runtime;
11use cubecl_cuda::{CudaDevice, CudaRuntime as CubeclCudaRuntime};
12use cubecl_runtime::config::{CubeClRuntimeConfig, RuntimeConfig};
13use cudarc::cublas::sys as cublas_sys;
14use cudarc::driver::result::DriverError;
15use cudarc::driver::sys::{CUcontext, CUdevice, CUresult};
16use cudarc::runtime::{result as cuda_result, sys as cuda_sys, sys::cudaStream_t};
17use tenferro_tensor::AllocationDomainId;
18
19use super::device::{
20 cuda_devices, unavailable_device_error, CudaDeviceError, CudaDeviceId, CudaDeviceInfo,
21};
22use super::identity::GpuExtensionCapability;
23
24pub fn gpu_available() -> bool {
28 let library_present = std::panic::catch_unwind(|| {
29 unsafe { cudarc::driver::sys::is_culib_present() }
32 })
33 .unwrap_or(false);
34 if !library_present {
35 return false;
36 }
37 let Ok(devices) = cuda_devices() else {
38 return false;
39 };
40 let Some(device_id) = devices.first().map(|device| device.id()) else {
41 return false;
42 };
43 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
44 let Ok(runtime) = CudaRuntime::new(device_id) else {
45 return false;
46 };
47 runtime.synchronize().is_ok()
48 }))
49 .unwrap_or(false)
50}
51
52pub(crate) struct RawContextRestore {
61 saved_device: Result<i32, cudarc::runtime::result::RuntimeError>,
62 saved_context: Result<Option<CUcontext>, cudarc::driver::result::DriverError>,
63 op: &'static str,
64}
65
66impl RawContextRestore {
67 pub(crate) fn enter(op: &'static str, device: i32, context: CUcontext) -> crate::Result<Self> {
69 let saved_device = cudarc::runtime::result::device::get();
70 let saved_context = cudarc::driver::result::ctx::get_current();
71 cudarc::runtime::result::device::set(device)
72 .map_err(|err| crate::Error::backend_source(op, err))?;
73 if let Err(err) = unsafe { cudarc::driver::result::ctx::set_current(context) } {
74 if let Ok(previous_device) = saved_device {
79 let _ = cudarc::runtime::result::device::set(previous_device);
80 }
81 match saved_context {
82 Ok(Some(previous)) => {
83 let _ = unsafe { cudarc::driver::result::ctx::set_current(previous) };
84 }
85 Ok(None) => {
86 let _ =
87 unsafe { cudarc::driver::result::ctx::set_current(std::ptr::null_mut()) };
88 }
89 Err(_) => {}
90 }
91 return Err(crate::Error::backend_source(op, err));
92 }
93 Ok(Self {
94 saved_device,
95 saved_context,
96 op,
97 })
98 }
99
100 fn restore(&self) {
101 let mut stderr = std::io::stderr();
102 if let Ok(device) = self.saved_device {
103 if let Err(err) = cudarc::runtime::result::device::set(device) {
104 let _ = writeln!(
105 stderr,
106 "tenferro-gpu: failed to restore CUDA device during {}: {err:?}",
107 self.op
108 );
109 }
110 }
111 match self.saved_context {
112 Ok(Some(context)) => {
113 if let Err(err) = unsafe { cudarc::driver::result::ctx::set_current(context) } {
114 let _ = writeln!(
115 stderr,
116 "tenferro-gpu: failed to restore CUDA context during {}: {err:?}",
117 self.op
118 );
119 }
120 }
121 Ok(None) => {
124 if let Err(err) =
125 unsafe { cudarc::driver::result::ctx::set_current(std::ptr::null_mut()) }
126 {
127 let _ = writeln!(
128 stderr,
129 "tenferro-gpu: failed to clear CUDA context during {}: {err:?}",
130 self.op
131 );
132 }
133 }
134 Err(_) => {}
136 }
137 }
138}
139
140impl Drop for RawContextRestore {
141 fn drop(&mut self) {
142 self.restore();
143 }
144}
145
146#[derive(Clone, Debug)]
153pub struct CudaRuntimeIdentity {
154 marker: Arc<u8>,
155}
156
157impl CudaRuntimeIdentity {
158 fn fresh() -> Self {
159 Self {
160 marker: Arc::new(0),
161 }
162 }
163}
164
165impl PartialEq for CudaRuntimeIdentity {
166 fn eq(&self, other: &Self) -> bool {
167 Arc::ptr_eq(&self.marker, &other.marker)
168 }
169}
170
171impl Eq for CudaRuntimeIdentity {}
172
173impl Hash for CudaRuntimeIdentity {
174 fn hash<H: Hasher>(&self, state: &mut H) {
175 state.write_usize(Arc::as_ptr(&self.marker) as usize);
178 }
179}
180
181#[derive(Clone)]
194pub struct CudaRuntime {
195 inner: Arc<CudaRuntimeState>,
196}
197
198struct CudaRuntimeState {
199 client: ComputeClient<CubeclCudaRuntime>,
200 device_id: CudaDeviceId,
201 device_ordinal: usize,
202 device_info: CudaDeviceInfo,
203 primary_context: CudaPrimaryContext,
204 identity: CudaRuntimeIdentity,
205 allocation_domain: AllocationDomainId,
206 raw_streams: Box<[OnceLock<u64>]>,
216 cublas_handles: Box<[Mutex<Option<CublasStreamHandle>>]>,
221 pinned_scalar: Mutex<PinnedScalarSlot>,
225}
226
227struct CublasStreamHandle(cublas_sys::cublasHandle_t);
229
230struct PinnedScalarSlot {
232 ptr: *mut std::ffi::c_void,
233}
234
235pub(crate) const PINNED_SCALAR_BYTES: usize = 16;
238
239unsafe impl Send for CudaRuntimeState {}
245unsafe impl Sync for CudaRuntimeState {}
250
251impl fmt::Debug for CudaRuntime {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 f.debug_struct("CudaRuntime")
254 .field("device_id", &self.inner.device_id)
255 .finish_non_exhaustive()
256 }
257}
258
259struct CudaPrimaryContext {
260 cuda_device: CUdevice,
261 cuda_context: CUcontext,
262}
263
264impl CudaPrimaryContext {
265 fn retain(cuda_device: CUdevice) -> crate::Result<Self> {
266 let cuda_context = unsafe { cudarc::driver::result::primary_ctx::retain(cuda_device) }
267 .map_err(|err| crate::Error::backend_source("cubecl_runtime_init", err))?;
268 Ok(Self {
269 cuda_device,
270 cuda_context,
271 })
272 }
273
274 fn context(&self) -> CUcontext {
275 self.cuda_context
276 }
277}
278
279impl Drop for CudaPrimaryContext {
280 fn drop(&mut self) {
281 if let Err(err) = unsafe { cudarc::driver::result::primary_ctx::release(self.cuda_device) }
282 {
283 report_cuda_primary_context_release_error(&err);
284 }
285 }
286}
287
288#[cold]
289fn report_cuda_primary_context_release_error(err: &impl fmt::Debug) {
290 eprintln!("tenferro-gpu: failed to release CUDA primary context during Drop: {err:?}");
291}
292
293#[cold]
294fn report_cuda_runtime_drop_error(err: &crate::Error) {
295 eprintln!("tenferro-gpu: failed to synchronize CUDA runtime during Drop: {err}");
296}
297
298impl CudaRuntime {
299 pub fn new(device_id: CudaDeviceId) -> Result<Self, CudaDeviceError> {
317 let device_ordinal = usize::try_from(device_id.ordinal()).map_err(|source| {
318 cuda_initialization_error(device_id, "convert_device_ordinal", source)
319 })?;
320 let cuda_ordinal = i32::try_from(device_id.ordinal()).map_err(|source| {
321 cuda_initialization_error(device_id, "convert_cuda_ordinal", source)
322 })?;
323 cudarc::driver::result::init()
324 .map_err(|source| cuda_initialization_error(device_id, "initialize_driver", source))?;
325 let cuda_device = match cudarc::driver::result::device::get(cuda_ordinal) {
326 Ok(cuda_device) => cuda_device,
327 Err(source) if is_invalid_device_lookup(source) => {
328 return Err(unavailable_device_error(device_id, cuda_devices()?));
329 }
330 Err(source) => {
331 return Err(cuda_initialization_error(device_id, "get_device", source));
332 }
333 };
334 let primary_context = CudaPrimaryContext::retain(cuda_device).map_err(|source| {
335 cuda_initialization_error(device_id, "retain_primary_context", source)
336 })?;
337 unsafe { cudarc::driver::result::ctx::set_current(primary_context.context()) }.map_err(
338 |source| cuda_initialization_error(device_id, "set_current_context", source),
339 )?;
340 cudarc::runtime::result::device::set(cuda_ordinal)
341 .map_err(|source| cuda_initialization_error(device_id, "set_device", source))?;
342 let device = CudaDevice::new(device_ordinal);
343 let client = CubeclCudaRuntime::client(&device);
344 let discovered = cuda_devices()?;
345 let device_info = discovered
346 .iter()
347 .find(|info| info.id() == device_id)
348 .cloned()
349 .ok_or_else(|| unavailable_device_error(device_id, discovered))?;
350 Ok(Self {
351 inner: Arc::new(CudaRuntimeState {
352 client,
353 device_id,
354 device_ordinal,
355 device_info,
356 primary_context,
357 identity: CudaRuntimeIdentity::fresh(),
358 allocation_domain: AllocationDomainId::fresh(),
359 raw_streams: (0..cubecl_stream_slots())
360 .map(|_| OnceLock::new())
361 .collect(),
362 cublas_handles: (0..cubecl_stream_slots())
363 .map(|_| Mutex::new(None))
364 .collect(),
365 pinned_scalar: Mutex::new(PinnedScalarSlot {
366 ptr: std::ptr::null_mut(),
367 }),
368 }),
369 })
370 }
371
372 pub(crate) fn client(&self) -> &ComputeClient<CubeclCudaRuntime> {
373 &self.inner.client
374 }
375
376 pub fn device_id(&self) -> CudaDeviceId {
386 self.inner.device_id
387 }
388
389 pub fn device_info(&self) -> &CudaDeviceInfo {
400 &self.inner.device_info
401 }
402
403 pub fn allocation_domain(&self) -> AllocationDomainId {
414 self.inner.allocation_domain
415 }
416
417 pub fn supports_extension(&self, capability: GpuExtensionCapability) -> bool {
435 capabilities_for_device(capability)
436 }
437
438 pub(crate) fn device_ordinal(&self) -> usize {
439 self.inner.device_ordinal
440 }
441
442 pub(crate) fn primary_context(&self) -> CUcontext {
443 self.inner.primary_context.context()
444 }
445
446 pub fn with_current_context<R>(
474 &self,
475 op: &'static str,
476 f: impl FnOnce() -> R,
477 ) -> crate::Result<R> {
478 let device_ordinal = i32::try_from(self.device_ordinal())
479 .map_err(|source| crate::Error::backend_source(op, source))?;
480 let _guard = RawContextRestore::enter(op, device_ordinal, self.primary_context())?;
481 Ok(f())
482 }
483
484 pub(crate) fn flush_cubecl(&self, op: &'static str) -> crate::Result<()> {
489 self.inner.flush_cubecl(op)
490 }
491
492 pub fn runtime_identity(&self) -> CudaRuntimeIdentity {
503 self.inner.identity.clone()
504 }
505
506 pub(crate) fn allocation_domain_id(&self) -> AllocationDomainId {
507 self.inner.allocation_domain
508 }
509
510 pub(crate) fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
511 self.inner.set_current_cuda_context(op)
512 }
513
514 pub(crate) fn raw_cuda_stream(&self) -> crate::Result<u64> {
515 self.inner.raw_cuda_stream()
516 }
517
518 pub(crate) fn synchronize_raw_stream(
519 &self,
520 stream: u64,
521 op: &'static str,
522 ) -> crate::Result<()> {
523 self.inner.synchronize_raw_stream(stream, op)
524 }
525
526 pub(crate) fn with_cublas_handle<R>(
531 &self,
532 op: &'static str,
533 pointer_mode: cublas_sys::cublasPointerMode_t,
534 cross_stream_handles: Vec<cubecl_runtime::server::Handle>,
535 execute: impl FnOnce(cublas_sys::cublasHandle_t) -> crate::Result<R>,
536 ) -> crate::Result<R> {
537 self.inner
538 .with_cublas_handle(op, pointer_mode, cross_stream_handles, execute)
539 }
540
541 pub(crate) fn finish_vendor_enqueue<R>(
542 &self,
543 op: &'static str,
544 cross_stream_handles: Vec<cubecl_runtime::server::Handle>,
545 result: crate::Result<R>,
546 ) -> crate::Result<R> {
547 self.inner
548 .finish_vendor_enqueue(op, cross_stream_handles, result)
549 }
550
551 pub(crate) fn stream_slot(&self) -> usize {
552 self.inner.stream_slot()
553 }
554
555 pub(crate) fn stream_slot_count(&self) -> usize {
556 self.inner.raw_streams.len()
557 }
558
559 pub(crate) fn is_current_stream_slot(&self, handle: &cubecl_runtime::server::Handle) -> bool {
560 self.inner.stream_slot_for(handle.stream) == self.inner.stream_slot()
561 }
562
563 pub(crate) fn download_scalar_bytes(
570 &self,
571 device_addr: u64,
572 out: &mut [u8],
573 op: &'static str,
574 retained: cubecl_runtime::server::Handle,
575 ) -> crate::Result<()> {
576 self.inner
577 .download_scalar_bytes(device_addr, out, op, retained)
578 }
579
580 pub fn synchronize(&self) -> crate::Result<()> {
597 self.inner.synchronize()
598 }
599}
600
601impl CudaRuntimeState {
602 fn stream_slot(&self) -> usize {
603 self.stream_slot_for(StreamId::current())
604 }
605
606 fn stream_slot_for(&self, stream_id: StreamId) -> usize {
607 stream_id.value as usize % self.raw_streams.len()
608 }
609
610 fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
611 if let Ok(Some(current)) = cudarc::driver::result::ctx::get_current() {
617 if current == self.primary_context.context() {
618 return Ok(());
619 }
620 }
621 let device_ordinal = i32::try_from(self.device_id.ordinal())
624 .map_err(|source| crate::Error::backend_source(op, source))?;
625 cudarc::runtime::result::device::set(device_ordinal)
626 .map_err(|err| crate::Error::backend_source(op, err))?;
627 unsafe { cudarc::driver::result::ctx::set_current(self.primary_context.context()) }
628 .map_err(|err| crate::Error::backend_source(op, err))
629 }
630
631 fn raw_cuda_stream(&self) -> crate::Result<u64> {
632 let stream_id = StreamId::current();
633 let slot = self.stream_slot();
634 if let Some(&stream) = self.raw_streams[slot].get() {
635 return Ok(stream);
636 }
637 let stream = self
638 .client
639 .with_server(move |server| {
640 server
641 .raw_stream(stream_id)
642 .map(|stream| stream as u64)
643 .map_err(|err| crate::Error::backend_source("raw_cuda_stream", err))
644 })
645 .ok_or_else(|| {
646 crate::Error::runtime_state("raw_cuda_stream", "CubeCL server is unavailable")
647 })??;
648 Ok(*self.raw_streams[slot].get_or_init(|| stream))
649 }
650
651 fn flush_cubecl(&self, op: &'static str) -> crate::Result<()> {
652 self.client
653 .flush()
654 .map_err(|err| crate::Error::backend_source(op, err))
655 }
656
657 fn synchronize(&self) -> crate::Result<()> {
658 const OP: &str = "cubecl_runtime_synchronize";
659 self.flush_cubecl(OP)?;
661 let stream = self.raw_cuda_stream()?;
662 self.synchronize_raw_stream(stream, OP)
663 }
664
665 fn synchronize_raw_stream(&self, stream: u64, op: &'static str) -> crate::Result<()> {
666 self.set_current_cuda_context(op)?;
667 unsafe { cuda_result::stream::synchronize(stream as usize as cudaStream_t) }
668 .map_err(|err| crate::Error::backend_source(op, err))
669 }
670
671 fn retire_initialized_streams(&self) -> bool {
672 const OP: &str = "cuda_runtime_drop";
673 if let Err(error) = self.set_current_cuda_context(OP) {
674 report_cuda_runtime_drop_error(&error);
675 return false;
676 }
677 let mut retired = true;
678 for stream in &self.raw_streams {
679 let Some(&stream) = stream.get() else {
680 continue;
681 };
682 if let Err(source) =
685 unsafe { cuda_result::stream::synchronize(stream as usize as cudaStream_t) }
686 {
687 retired = false;
688 report_cuda_runtime_drop_error(&crate::Error::backend_source(OP, source));
689 }
690 }
691 retired
692 }
693
694 fn with_cublas_handle<R>(
695 &self,
696 op: &'static str,
697 pointer_mode: cublas_sys::cublasPointerMode_t,
698 cross_stream_handles: Vec<cubecl_runtime::server::Handle>,
699 execute: impl FnOnce(cublas_sys::cublasHandle_t) -> crate::Result<R>,
700 ) -> crate::Result<R> {
701 let poisoned = || crate::Error::runtime_state(op, "cuBLAS handle cache lock poisoned");
702 let slot = self.stream_slot();
703 let mut cached = self.cublas_handles[slot].lock().map_err(|_| poisoned())?;
704 let handle = match *cached {
705 Some(ref handle) => handle.0,
706 None => {
707 if !cublas_library_present() {
708 return Err(crate::Error::io_source(op, CublasLibraryMissing));
709 }
710 let stream = self.raw_cuda_stream()? as usize as cublas_sys::cudaStream_t;
711 let mut raw = std::ptr::null_mut();
712 check_cublas(op, "cublasCreate", unsafe {
715 cublas_sys::cublasCreate_v2(&mut raw)
716 })?;
717 if let Err(err) = check_cublas(op, "cublasSetStream", unsafe {
720 cublas_sys::cublasSetStream_v2(raw, stream)
721 }) {
722 let _ = unsafe { cublas_sys::cublasDestroy_v2(raw) };
724 return Err(err);
725 }
726 *cached = Some(CublasStreamHandle(raw));
727 raw
728 }
729 };
730 check_cublas(op, "cublasSetPointerMode", unsafe {
733 cublas_sys::cublasSetPointerMode_v2(handle, pointer_mode)
734 })?;
735 let result = execute(handle);
736 self.finish_vendor_enqueue(op, cross_stream_handles, result)
737 }
738
739 fn finish_vendor_enqueue<R>(
740 &self,
741 op: &'static str,
742 cross_stream_handles: Vec<cubecl_runtime::server::Handle>,
743 result: crate::Result<R>,
744 ) -> crate::Result<R> {
745 if cross_stream_handles.is_empty() {
746 return result;
747 }
748 let retirement = self.synchronize();
749 match (result, retirement) {
750 (Ok(value), Ok(())) => Ok(value),
751 (Err(error), Ok(())) => Err(error),
752 (Ok(_), Err(retirement)) => {
753 std::mem::forget(cross_stream_handles);
756 Err(crate::Error::backend_source(op, retirement))
757 }
758 (Err(error), Err(_retirement)) => {
759 std::mem::forget(cross_stream_handles);
760 Err(error)
761 }
762 }
763 }
764
765 fn download_scalar_bytes(
766 &self,
767 device_addr: u64,
768 out: &mut [u8],
769 op: &'static str,
770 retained: cubecl_runtime::server::Handle,
771 ) -> crate::Result<()> {
772 if out.len() > PINNED_SCALAR_BYTES {
773 return Err(crate::Error::Internal(format!(
774 "pinned scalar staging supports at most {PINNED_SCALAR_BYTES} bytes, got {}",
775 out.len()
776 )));
777 }
778 self.flush_cubecl(op)?;
781 self.set_current_cuda_context(op)?;
782 let stream = self.raw_cuda_stream()? as usize as cudaStream_t;
783 let mut slot = self
786 .pinned_scalar
787 .lock()
788 .map_err(|_| crate::Error::runtime_state(op, "pinned scalar staging lock poisoned"))?;
789 if slot.ptr.is_null() {
790 let mut ptr = std::ptr::null_mut();
791 unsafe {
794 cuda_sys::cudaHostAlloc(
795 &mut ptr,
796 PINNED_SCALAR_BYTES,
797 cuda_sys::cudaHostAllocDefault,
798 )
799 }
800 .result()
801 .map_err(|err| crate::Error::backend_source(op, err))?;
802 slot.ptr = ptr;
803 }
804 let src = super::interop::cuda_device_ptr_from_addr(device_addr, op)?;
805 let staging = unsafe { std::slice::from_raw_parts_mut(slot.ptr.cast::<u8>(), out.len()) };
811 let completed = unsafe { cuda_result::memcpy_dtoh_async(staging, src, stream) }
823 .and_then(|()| unsafe { cuda_result::stream::synchronize(stream) });
824 if let Err(err) = completed {
825 std::mem::forget(retained);
826 slot.ptr = std::ptr::null_mut();
827 return Err(crate::Error::backend_source(op, err));
828 }
829 out.copy_from_slice(staging);
830 Ok(())
831 }
832
833 fn release_cuda_library_resources(&mut self) {
838 for cached in &self.cublas_handles {
839 if let Ok(mut handle) = cached.lock() {
840 let Some(handle) = handle.take() else {
841 continue;
842 };
843 if let Err(err) = unsafe { cublas_sys::cublasDestroy_v2(handle.0) }.result() {
845 report_cuda_resource_release_error("cuBLAS handle", &err);
846 }
847 }
848 }
849 if let Ok(mut slot) = self.pinned_scalar.lock() {
850 if !slot.ptr.is_null() {
851 if let Err(err) = unsafe { cuda_sys::cudaFreeHost(slot.ptr) }.result() {
853 report_cuda_resource_release_error("pinned scalar staging", &err);
854 }
855 slot.ptr = std::ptr::null_mut();
856 }
857 }
858 }
859}
860
861fn cubecl_stream_slots() -> usize {
862 usize::from(CubeClRuntimeConfig::get().streaming.max_streams.max(1))
863}
864
865#[derive(Debug, thiserror::Error)]
867#[error(
868 "cuBLAS shared library not found; ensure `LD_LIBRARY_PATH` includes the CUDA toolkit library directory"
869)]
870struct CublasLibraryMissing;
871
872fn cublas_library_present() -> bool {
874 use std::sync::OnceLock;
875 static PRESENT: OnceLock<bool> = OnceLock::new();
876 *PRESENT.get_or_init(|| unsafe { cublas_sys::is_culib_present() })
879}
880
881pub(super) fn check_cublas(
883 op: &'static str,
884 call: &'static str,
885 status: cublas_sys::cublasStatus_t,
886) -> crate::Result<()> {
887 if matches!(status, cublas_sys::cublasStatus_t::CUBLAS_STATUS_SUCCESS) {
888 Ok(())
889 } else {
890 Err(super::error::provider_status(
891 op,
892 "cuBLAS",
893 call,
894 status as i32,
895 ))
896 }
897}
898
899#[cold]
900fn report_cuda_resource_release_error(what: &'static str, err: &impl fmt::Debug) {
901 eprintln!("tenferro-gpu: failed to release {what} during Drop: {err:?}");
902}
903
904fn is_invalid_device_lookup(source: DriverError) -> bool {
905 source.0 == CUresult::CUDA_ERROR_INVALID_DEVICE
906}
907
908pub(crate) fn capabilities_for_device(capability: GpuExtensionCapability) -> bool {
915 !matches!(capability, GpuExtensionCapability::PeerCopy)
916}
917
918fn cuda_initialization_error<E>(
919 device: CudaDeviceId,
920 operation: &'static str,
921 source: E,
922) -> CudaDeviceError
923where
924 E: std::error::Error + Send + Sync + 'static,
925{
926 CudaDeviceError::Initialization {
927 device,
928 operation,
929 source: Box::new(source),
930 }
931}
932
933impl Drop for CudaRuntimeState {
934 fn drop(&mut self) {
935 if self.retire_initialized_streams() {
939 self.release_cuda_library_resources();
942 }
943 }
947}
948
949#[cfg(test)]
950mod tests;