1use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::io::Write;
6use std::sync::Arc;
7
8use cubecl::client::ComputeClient;
9use cubecl::stream_id::StreamId;
10use cubecl::Runtime;
11use cubecl_cuda::{CudaDevice, CudaRuntime as CubeclCudaRuntime};
12use cudarc::driver::result::DriverError;
13use cudarc::driver::sys::{CUcontext, CUdevice, CUresult};
14use cudarc::runtime::{result as cuda_result, sys::cudaStream_t};
15use tenferro_tensor::AllocationDomainId;
16
17use super::device::{
18 cuda_devices, unavailable_device_error, CudaDeviceError, CudaDeviceId, CudaDeviceInfo,
19};
20use super::identity::GpuExtensionCapability;
21
22pub fn gpu_available() -> bool {
26 let library_present = std::panic::catch_unwind(|| {
27 unsafe { cudarc::driver::sys::is_culib_present() }
30 })
31 .unwrap_or(false);
32 if !library_present {
33 return false;
34 }
35 let Ok(devices) = cuda_devices() else {
36 return false;
37 };
38 let Some(device_id) = devices.first().map(|device| device.id()) else {
39 return false;
40 };
41 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
42 let Ok(runtime) = CudaRuntime::new(device_id) else {
43 return false;
44 };
45 runtime.synchronize().is_ok()
46 }))
47 .unwrap_or(false)
48}
49
50pub(crate) struct RawContextRestore {
59 saved_device: Result<i32, cudarc::runtime::result::RuntimeError>,
60 saved_context: Result<Option<CUcontext>, cudarc::driver::result::DriverError>,
61 op: &'static str,
62}
63
64impl RawContextRestore {
65 pub(crate) fn enter(op: &'static str, device: i32, context: CUcontext) -> crate::Result<Self> {
67 let saved_device = cudarc::runtime::result::device::get();
68 let saved_context = cudarc::driver::result::ctx::get_current();
69 cudarc::runtime::result::device::set(device)
70 .map_err(|err| crate::Error::backend_source(op, err))?;
71 if let Err(err) = unsafe { cudarc::driver::result::ctx::set_current(context) } {
72 if let Ok(previous_device) = saved_device {
77 let _ = cudarc::runtime::result::device::set(previous_device);
78 }
79 match saved_context {
80 Ok(Some(previous)) => {
81 let _ = unsafe { cudarc::driver::result::ctx::set_current(previous) };
82 }
83 Ok(None) => {
84 let _ =
85 unsafe { cudarc::driver::result::ctx::set_current(std::ptr::null_mut()) };
86 }
87 Err(_) => {}
88 }
89 return Err(crate::Error::backend_source(op, err));
90 }
91 Ok(Self {
92 saved_device,
93 saved_context,
94 op,
95 })
96 }
97
98 fn restore(&self) {
99 let mut stderr = std::io::stderr();
100 if let Ok(device) = self.saved_device {
101 if let Err(err) = cudarc::runtime::result::device::set(device) {
102 let _ = writeln!(
103 stderr,
104 "tenferro-gpu: failed to restore CUDA device during {}: {err:?}",
105 self.op
106 );
107 }
108 }
109 match self.saved_context {
110 Ok(Some(context)) => {
111 if let Err(err) = unsafe { cudarc::driver::result::ctx::set_current(context) } {
112 let _ = writeln!(
113 stderr,
114 "tenferro-gpu: failed to restore CUDA context during {}: {err:?}",
115 self.op
116 );
117 }
118 }
119 Ok(None) => {
122 if let Err(err) =
123 unsafe { cudarc::driver::result::ctx::set_current(std::ptr::null_mut()) }
124 {
125 let _ = writeln!(
126 stderr,
127 "tenferro-gpu: failed to clear CUDA context during {}: {err:?}",
128 self.op
129 );
130 }
131 }
132 Err(_) => {}
134 }
135 }
136}
137
138impl Drop for RawContextRestore {
139 fn drop(&mut self) {
140 self.restore();
141 }
142}
143
144#[derive(Clone, Debug)]
151pub struct CudaRuntimeIdentity {
152 marker: Arc<u8>,
153}
154
155impl CudaRuntimeIdentity {
156 fn fresh() -> Self {
157 Self {
158 marker: Arc::new(0),
159 }
160 }
161}
162
163impl PartialEq for CudaRuntimeIdentity {
164 fn eq(&self, other: &Self) -> bool {
165 Arc::ptr_eq(&self.marker, &other.marker)
166 }
167}
168
169impl Eq for CudaRuntimeIdentity {}
170
171impl Hash for CudaRuntimeIdentity {
172 fn hash<H: Hasher>(&self, state: &mut H) {
173 state.write_usize(Arc::as_ptr(&self.marker) as usize);
176 }
177}
178
179#[derive(Clone)]
192pub struct CudaRuntime {
193 inner: Arc<CudaRuntimeState>,
194}
195
196struct CudaRuntimeState {
197 client: ComputeClient<CubeclCudaRuntime>,
198 device_id: CudaDeviceId,
199 device_ordinal: usize,
200 device_info: CudaDeviceInfo,
201 primary_context: CudaPrimaryContext,
202 identity: CudaRuntimeIdentity,
203 allocation_domain: AllocationDomainId,
204}
205
206unsafe impl Send for CudaRuntimeState {}
210unsafe impl Sync for CudaRuntimeState {}
214
215impl fmt::Debug for CudaRuntime {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 f.debug_struct("CudaRuntime")
218 .field("device_id", &self.inner.device_id)
219 .finish_non_exhaustive()
220 }
221}
222
223struct CudaPrimaryContext {
224 cuda_device: CUdevice,
225 cuda_context: CUcontext,
226}
227
228impl CudaPrimaryContext {
229 fn retain(cuda_device: CUdevice) -> crate::Result<Self> {
230 let cuda_context = unsafe { cudarc::driver::result::primary_ctx::retain(cuda_device) }
231 .map_err(|err| crate::Error::backend_source("cubecl_runtime_init", err))?;
232 Ok(Self {
233 cuda_device,
234 cuda_context,
235 })
236 }
237
238 fn context(&self) -> CUcontext {
239 self.cuda_context
240 }
241}
242
243impl Drop for CudaPrimaryContext {
244 fn drop(&mut self) {
245 if let Err(err) = unsafe { cudarc::driver::result::primary_ctx::release(self.cuda_device) }
246 {
247 report_cuda_primary_context_release_error(&err);
248 }
249 }
250}
251
252#[cold]
253fn report_cuda_primary_context_release_error(err: &impl fmt::Debug) {
254 eprintln!("tenferro-gpu: failed to release CUDA primary context during Drop: {err:?}");
255}
256
257#[cold]
258fn report_cuda_runtime_drop_error(err: &crate::Error) {
259 eprintln!("tenferro-gpu: failed to synchronize CUDA runtime during Drop: {err}");
260}
261
262impl CudaRuntime {
263 pub fn new(device_id: CudaDeviceId) -> Result<Self, CudaDeviceError> {
281 let device_ordinal = usize::try_from(device_id.ordinal()).map_err(|source| {
282 cuda_initialization_error(device_id, "convert_device_ordinal", source)
283 })?;
284 let cuda_ordinal = i32::try_from(device_id.ordinal()).map_err(|source| {
285 cuda_initialization_error(device_id, "convert_cuda_ordinal", source)
286 })?;
287 cudarc::driver::result::init()
288 .map_err(|source| cuda_initialization_error(device_id, "initialize_driver", source))?;
289 let cuda_device = match cudarc::driver::result::device::get(cuda_ordinal) {
290 Ok(cuda_device) => cuda_device,
291 Err(source) if is_invalid_device_lookup(source) => {
292 return Err(unavailable_device_error(device_id, cuda_devices()?));
293 }
294 Err(source) => {
295 return Err(cuda_initialization_error(device_id, "get_device", source));
296 }
297 };
298 let primary_context = CudaPrimaryContext::retain(cuda_device).map_err(|source| {
299 cuda_initialization_error(device_id, "retain_primary_context", source)
300 })?;
301 unsafe { cudarc::driver::result::ctx::set_current(primary_context.context()) }.map_err(
302 |source| cuda_initialization_error(device_id, "set_current_context", source),
303 )?;
304 cudarc::runtime::result::device::set(cuda_ordinal)
305 .map_err(|source| cuda_initialization_error(device_id, "set_device", source))?;
306 let device = CudaDevice::new(device_ordinal);
307 let client = CubeclCudaRuntime::client(&device);
308 let discovered = cuda_devices()?;
309 let device_info = discovered
310 .iter()
311 .find(|info| info.id() == device_id)
312 .cloned()
313 .ok_or_else(|| unavailable_device_error(device_id, discovered))?;
314 Ok(Self {
315 inner: Arc::new(CudaRuntimeState {
316 client,
317 device_id,
318 device_ordinal,
319 device_info,
320 primary_context,
321 identity: CudaRuntimeIdentity::fresh(),
322 allocation_domain: AllocationDomainId::fresh(),
323 }),
324 })
325 }
326
327 pub(crate) fn client(&self) -> &ComputeClient<CubeclCudaRuntime> {
328 &self.inner.client
329 }
330
331 pub fn device_id(&self) -> CudaDeviceId {
341 self.inner.device_id
342 }
343
344 pub fn device_info(&self) -> &CudaDeviceInfo {
355 &self.inner.device_info
356 }
357
358 pub fn allocation_domain(&self) -> AllocationDomainId {
369 self.inner.allocation_domain
370 }
371
372 pub fn supports_extension(&self, capability: GpuExtensionCapability) -> bool {
390 capabilities_for_device(capability)
391 }
392
393 pub(crate) fn device_ordinal(&self) -> usize {
394 self.inner.device_ordinal
395 }
396
397 pub(crate) fn primary_context(&self) -> CUcontext {
398 self.inner.primary_context.context()
399 }
400
401 pub fn with_current_context<R>(
429 &self,
430 op: &'static str,
431 f: impl FnOnce() -> R,
432 ) -> crate::Result<R> {
433 let device_ordinal = i32::try_from(self.device_ordinal())
434 .map_err(|source| crate::Error::backend_source(op, source))?;
435 let _guard = RawContextRestore::enter(op, device_ordinal, self.primary_context())?;
436 Ok(f())
437 }
438
439 pub(crate) fn flush_cubecl(&self, op: &'static str) -> crate::Result<()> {
444 self.client()
445 .flush()
446 .map_err(|err| crate::Error::backend_source(op, err))
447 }
448
449 pub fn runtime_identity(&self) -> CudaRuntimeIdentity {
460 self.inner.identity.clone()
461 }
462
463 pub(crate) fn allocation_domain_id(&self) -> AllocationDomainId {
464 self.inner.allocation_domain
465 }
466
467 pub(crate) fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
468 self.inner.set_current_cuda_context(op)
469 }
470
471 pub(crate) fn raw_cuda_stream(&self) -> crate::Result<u64> {
472 self.inner.raw_cuda_stream()
473 }
474
475 pub fn synchronize(&self) -> crate::Result<()> {
492 self.inner.synchronize()
493 }
494}
495
496impl CudaRuntimeState {
497 fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
498 let device_ordinal = i32::try_from(self.device_id.ordinal())
501 .map_err(|source| crate::Error::backend_source(op, source))?;
502 cudarc::runtime::result::device::set(device_ordinal)
503 .map_err(|err| crate::Error::backend_source(op, err))?;
504 unsafe { cudarc::driver::result::ctx::set_current(self.primary_context.context()) }
505 .map_err(|err| crate::Error::backend_source(op, err))
506 }
507
508 fn raw_cuda_stream(&self) -> crate::Result<u64> {
509 self.client
510 .with_server(|server| {
511 server
512 .raw_stream(StreamId::current())
513 .map(|stream| stream as u64)
514 .map_err(|err| crate::Error::backend_source("raw_cuda_stream", err))
515 })
516 .ok_or_else(|| {
517 crate::Error::runtime_state("raw_cuda_stream", "CubeCL server is unavailable")
518 })?
519 }
520
521 fn synchronize(&self) -> crate::Result<()> {
522 const OP: &str = "cubecl_runtime_synchronize";
523 self.set_current_cuda_context(OP)?;
524 let stream = self.raw_cuda_stream()? as usize as cudaStream_t;
525 unsafe { cuda_result::stream::synchronize(stream) }
526 .map_err(|err| crate::Error::backend_source(OP, err))
527 }
528}
529
530fn is_invalid_device_lookup(source: DriverError) -> bool {
531 source.0 == CUresult::CUDA_ERROR_INVALID_DEVICE
532}
533
534pub(crate) fn capabilities_for_device(capability: GpuExtensionCapability) -> bool {
541 !matches!(capability, GpuExtensionCapability::PeerCopy)
542}
543
544fn cuda_initialization_error<E>(
545 device: CudaDeviceId,
546 operation: &'static str,
547 source: E,
548) -> CudaDeviceError
549where
550 E: std::error::Error + Send + Sync + 'static,
551{
552 CudaDeviceError::Initialization {
553 device,
554 operation,
555 source: Box::new(source),
556 }
557}
558
559impl Drop for CudaRuntimeState {
560 fn drop(&mut self) {
561 if let Err(err) = self.synchronize() {
564 report_cuda_runtime_drop_error(&err);
565 }
566 }
567}
568
569#[cfg(test)]
570mod tests;