use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use cubecl::client::ComputeClient;
use cubecl::stream_id::StreamId;
use cubecl::Runtime;
use cubecl_cuda::{CudaDevice, CudaRuntime as CubeclCudaRuntime};
use cudarc::driver::result::DriverError;
use cudarc::driver::sys::{CUcontext, CUdevice, CUresult};
use cudarc::runtime::{result as cuda_result, sys::cudaStream_t};
use tenferro_tensor::AllocationDomainId;
use super::device::{cuda_devices, unavailable_device_error, CudaDeviceError, CudaDeviceId};
pub fn gpu_available() -> bool {
let library_present = std::panic::catch_unwind(|| {
unsafe { cudarc::driver::sys::is_culib_present() }
})
.unwrap_or(false);
if !library_present {
return false;
}
let Ok(devices) = cuda_devices() else {
return false;
};
let Some(device_id) = devices.first().map(|device| device.id()) else {
return false;
};
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Ok(runtime) = CudaRuntime::new(device_id) else {
return false;
};
runtime.synchronize().is_ok()
}))
.unwrap_or(false)
}
#[derive(Clone, Debug)]
pub struct CudaRuntimeIdentity {
marker: Arc<u8>,
}
impl CudaRuntimeIdentity {
fn fresh() -> Self {
Self {
marker: Arc::new(0),
}
}
}
impl PartialEq for CudaRuntimeIdentity {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.marker, &other.marker)
}
}
impl Eq for CudaRuntimeIdentity {}
impl Hash for CudaRuntimeIdentity {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_usize(Arc::as_ptr(&self.marker) as usize);
}
}
#[derive(Clone)]
pub struct CudaRuntime {
inner: Arc<CudaRuntimeState>,
}
struct CudaRuntimeState {
client: ComputeClient<CubeclCudaRuntime>,
device_id: CudaDeviceId,
device_ordinal: usize,
primary_context: CudaPrimaryContext,
identity: CudaRuntimeIdentity,
allocation_domain: AllocationDomainId,
}
unsafe impl Send for CudaRuntimeState {}
unsafe impl Sync for CudaRuntimeState {}
impl fmt::Debug for CudaRuntime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CudaRuntime")
.field("device_id", &self.inner.device_id)
.finish_non_exhaustive()
}
}
struct CudaPrimaryContext {
cuda_device: CUdevice,
cuda_context: CUcontext,
}
impl CudaPrimaryContext {
fn retain(cuda_device: CUdevice) -> crate::Result<Self> {
let cuda_context = unsafe { cudarc::driver::result::primary_ctx::retain(cuda_device) }
.map_err(|err| crate::Error::backend_source("cubecl_runtime_init", err))?;
Ok(Self {
cuda_device,
cuda_context,
})
}
fn context(&self) -> CUcontext {
self.cuda_context
}
}
impl Drop for CudaPrimaryContext {
fn drop(&mut self) {
if let Err(err) = unsafe { cudarc::driver::result::primary_ctx::release(self.cuda_device) }
{
report_cuda_primary_context_release_error(&err);
}
}
}
#[cold]
fn report_cuda_primary_context_release_error(err: &impl fmt::Debug) {
eprintln!("tenferro-gpu: failed to release CUDA primary context during Drop: {err:?}");
}
#[cold]
fn report_cuda_runtime_drop_error(err: &crate::Error) {
eprintln!("tenferro-gpu: failed to synchronize CUDA runtime during Drop: {err}");
}
impl CudaRuntime {
pub fn new(device_id: CudaDeviceId) -> Result<Self, CudaDeviceError> {
let device_ordinal = usize::try_from(device_id.ordinal()).map_err(|source| {
cuda_initialization_error(device_id, "convert_device_ordinal", source)
})?;
let cuda_ordinal = i32::try_from(device_id.ordinal()).map_err(|source| {
cuda_initialization_error(device_id, "convert_cuda_ordinal", source)
})?;
cudarc::driver::result::init()
.map_err(|source| cuda_initialization_error(device_id, "initialize_driver", source))?;
let cuda_device = match cudarc::driver::result::device::get(cuda_ordinal) {
Ok(cuda_device) => cuda_device,
Err(source) if is_invalid_device_lookup(source) => {
return Err(unavailable_device_error(device_id, cuda_devices()?));
}
Err(source) => {
return Err(cuda_initialization_error(device_id, "get_device", source));
}
};
let primary_context = CudaPrimaryContext::retain(cuda_device).map_err(|source| {
cuda_initialization_error(device_id, "retain_primary_context", source)
})?;
unsafe { cudarc::driver::result::ctx::set_current(primary_context.context()) }.map_err(
|source| cuda_initialization_error(device_id, "set_current_context", source),
)?;
cudarc::runtime::result::device::set(cuda_ordinal)
.map_err(|source| cuda_initialization_error(device_id, "set_device", source))?;
let device = CudaDevice::new(device_ordinal);
let client = CubeclCudaRuntime::client(&device);
Ok(Self {
inner: Arc::new(CudaRuntimeState {
client,
device_id,
device_ordinal,
primary_context,
identity: CudaRuntimeIdentity::fresh(),
allocation_domain: AllocationDomainId::fresh(),
}),
})
}
pub(crate) fn client(&self) -> &ComputeClient<CubeclCudaRuntime> {
&self.inner.client
}
pub fn device_id(&self) -> CudaDeviceId {
self.inner.device_id
}
pub(crate) fn device_ordinal(&self) -> usize {
self.inner.device_ordinal
}
pub fn runtime_identity(&self) -> CudaRuntimeIdentity {
self.inner.identity.clone()
}
pub(crate) fn allocation_domain_id(&self) -> AllocationDomainId {
self.inner.allocation_domain
}
#[doc(hidden)]
pub fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
self.inner.set_current_cuda_context(op)
}
pub(crate) fn raw_cuda_stream(&self) -> crate::Result<u64> {
self.inner.raw_cuda_stream()
}
pub fn synchronize(&self) -> crate::Result<()> {
self.inner.synchronize()
}
}
impl CudaRuntimeState {
fn set_current_cuda_context(&self, op: &'static str) -> crate::Result<()> {
let device_ordinal = i32::try_from(self.device_id.ordinal())
.map_err(|source| crate::Error::backend_source(op, source))?;
cudarc::runtime::result::device::set(device_ordinal)
.map_err(|err| crate::Error::backend_source(op, err))?;
unsafe { cudarc::driver::result::ctx::set_current(self.primary_context.context()) }
.map_err(|err| crate::Error::backend_source(op, err))
}
fn raw_cuda_stream(&self) -> crate::Result<u64> {
self.client
.with_server(|server| {
server
.raw_stream(StreamId::current())
.map(|stream| stream as u64)
.map_err(|err| crate::Error::backend_source("raw_cuda_stream", err))
})
.ok_or_else(|| {
crate::Error::runtime_state("raw_cuda_stream", "CubeCL server is unavailable")
})?
}
fn synchronize(&self) -> crate::Result<()> {
const OP: &str = "cubecl_runtime_synchronize";
self.set_current_cuda_context(OP)?;
let stream = self.raw_cuda_stream()? as usize as cudaStream_t;
unsafe { cuda_result::stream::synchronize(stream) }
.map_err(|err| crate::Error::backend_source(OP, err))
}
}
fn is_invalid_device_lookup(source: DriverError) -> bool {
source.0 == CUresult::CUDA_ERROR_INVALID_DEVICE
}
fn cuda_initialization_error<E>(
device: CudaDeviceId,
operation: &'static str,
source: E,
) -> CudaDeviceError
where
E: std::error::Error + Send + Sync + 'static,
{
CudaDeviceError::Initialization {
device,
operation,
source: Box::new(source),
}
}
impl Drop for CudaRuntimeState {
fn drop(&mut self) {
if let Err(err) = self.synchronize() {
report_cuda_runtime_drop_error(&err);
}
}
}
#[cfg(test)]
mod tests;