use std::fmt;
use tenferro_tensor::BoxError;
use super::identity::{CudaComputeCapability, CudaDeviceUuid};
#[derive(Debug, thiserror::Error)]
enum CudaDriverDiscoveryError {
#[error("CUDA driver call {function} failed: {source}")]
DriverCall {
function: &'static str,
#[source]
source: cudarc::driver::result::DriverError,
},
#[error("CUDA returned an invalid device count {count}")]
InvalidDeviceCount { count: i32 },
#[error("CUDA device ordinal {device:?} is out of range")]
DeviceOrdinalOutOfRange { device: CudaDeviceId },
}
fn boxed_discovery_error(error: CudaDriverDiscoveryError) -> BoxError {
Box::new(error)
}
struct CudaDriverApi;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct CudaDeviceId(u32);
impl CudaDeviceId {
pub const fn from_ordinal(ordinal: u32) -> Self {
Self(ordinal)
}
pub const fn ordinal(self) -> u32 {
self.0
}
}
impl fmt::Debug for CudaDeviceId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("CudaDeviceId")
.field(&self.0)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CudaDeviceInfo {
id: CudaDeviceId,
name: String,
uuid: CudaDeviceUuid,
compute_capability: CudaComputeCapability,
total_memory_bytes: u64,
}
impl CudaDeviceInfo {
pub(crate) fn new(
id: CudaDeviceId,
name: impl Into<String>,
uuid: CudaDeviceUuid,
compute_capability: CudaComputeCapability,
total_memory_bytes: u64,
) -> Self {
Self {
id,
name: name.into(),
uuid,
compute_capability,
total_memory_bytes,
}
}
pub fn id(&self) -> CudaDeviceId {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn uuid(&self) -> CudaDeviceUuid {
self.uuid
}
pub fn compute_capability(&self) -> CudaComputeCapability {
self.compute_capability
}
pub fn total_memory_bytes(&self) -> u64 {
self.total_memory_bytes
}
}
pub(crate) trait DiscoveryDriver {
fn initialize(&self) -> Result<(), BoxError>;
fn device_count(&self) -> Result<u32, BoxError>;
fn device_name(&self, device: CudaDeviceId) -> Result<String, BoxError>;
fn device_uuid(&self, device: CudaDeviceId) -> Result<CudaDeviceUuid, BoxError>;
fn compute_capability(&self, device: CudaDeviceId) -> Result<CudaComputeCapability, BoxError>;
fn total_memory_bytes(&self, device: CudaDeviceId) -> Result<u64, BoxError>;
}
pub(crate) fn discover_with(
driver: &impl DiscoveryDriver,
) -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> {
driver
.initialize()
.map_err(|source| CudaDeviceError::Discovery {
operation: "initialize_driver",
source,
})?;
let device_count = driver
.device_count()
.map_err(|source| CudaDeviceError::Discovery {
operation: "enumerate_devices",
source,
})?;
let mut devices = Vec::new();
for ordinal in 0..device_count {
let id = CudaDeviceId::from_ordinal(ordinal);
let name = driver
.device_name(id)
.map_err(|source| CudaDeviceError::Discovery {
operation: "get_device_name",
source,
})?;
let uuid = driver
.device_uuid(id)
.map_err(|source| CudaDeviceError::Discovery {
operation: "get_device_uuid",
source,
})?;
let compute_capability =
driver
.compute_capability(id)
.map_err(|source| CudaDeviceError::Discovery {
operation: "get_compute_capability",
source,
})?;
let total_memory_bytes =
driver
.total_memory_bytes(id)
.map_err(|source| CudaDeviceError::Discovery {
operation: "get_total_memory",
source,
})?;
devices.push(CudaDeviceInfo::new(
id,
name,
uuid,
compute_capability,
total_memory_bytes,
));
}
Ok(devices)
}
impl DiscoveryDriver for CudaDriverApi {
fn initialize(&self) -> Result<(), BoxError> {
cudarc::driver::result::init().map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuInit",
source,
})
})
}
fn device_count(&self) -> Result<u32, BoxError> {
let count = cudarc::driver::result::device::get_count().map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGetCount",
source,
})
})?;
u32::try_from(count).map_err(|_| {
boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count })
})
}
fn device_name(&self, device: CudaDeviceId) -> Result<String, BoxError> {
let cuda_device = self.cuda_device(device)?;
let name = cudarc::driver::result::device::get_name(cuda_device).map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGetName",
source,
})
})?;
Ok(name)
}
fn device_uuid(&self, device: CudaDeviceId) -> Result<CudaDeviceUuid, BoxError> {
let cuda_device = self.cuda_device(device)?;
let uuid = cudarc::driver::result::device::get_uuid(cuda_device).map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGetUuid",
source,
})
})?;
let mut bytes = [0u8; 16];
#[allow(clippy::needless_range_loop)]
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = uuid.bytes[index] as u8;
}
Ok(CudaDeviceUuid::from_bytes(bytes))
}
fn compute_capability(&self, device: CudaDeviceId) -> Result<CudaComputeCapability, BoxError> {
let cuda_device = self.cuda_device(device)?;
use cudarc::driver::sys::CUdevice_attribute_enum as Attr;
let (major_name, minor_name) = unsafe {
(
cudarc::driver::result::device::get_attribute(
cuda_device,
Attr::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR,
),
cudarc::driver::result::device::get_attribute(
cuda_device,
Attr::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR,
),
)
};
let major = major_name.map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGetAttribute(CC_MAJOR)",
source,
})
})?;
let minor = minor_name.map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGetAttribute(CC_MINOR)",
source,
})
})?;
Ok(CudaComputeCapability {
major: u32::try_from(major).map_err(|_| {
boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: major })
})?,
minor: u32::try_from(minor).map_err(|_| {
boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: minor })
})?,
})
}
fn total_memory_bytes(&self, device: CudaDeviceId) -> Result<u64, BoxError> {
let cuda_device = self.cuda_device(device)?;
let bytes = unsafe { cudarc::driver::result::device::total_mem(cuda_device) }.map_err(
|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceTotalMem",
source,
})
},
)?;
u64::try_from(bytes).map_err(|_| {
boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: i32::MAX })
})
}
}
impl CudaDriverApi {
fn cuda_device(&self, device: CudaDeviceId) -> Result<cudarc::driver::sys::CUdevice, BoxError> {
let ordinal = i32::try_from(device.ordinal()).map_err(|_| {
boxed_discovery_error(CudaDriverDiscoveryError::DeviceOrdinalOutOfRange { device })
})?;
cudarc::driver::result::device::get(ordinal).map_err(|source| {
boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
function: "cuDeviceGet",
source,
})
})
}
}
pub fn cuda_devices() -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> {
discover_with(&CudaDriverApi)
}
pub(crate) fn unavailable_device_error(
requested: CudaDeviceId,
discovered: Vec<CudaDeviceInfo>,
) -> CudaDeviceError {
CudaDeviceError::Unavailable {
requested,
discovered: discovered.into_boxed_slice(),
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CudaDeviceError {
#[error("CUDA device discovery failed during {operation}: {source}")]
Discovery {
operation: &'static str,
#[source]
source: tenferro_tensor::BoxError,
},
#[error(
"requested CUDA device {requested:?} is unavailable; discovered devices: {discovered:?}"
)]
Unavailable {
requested: CudaDeviceId,
discovered: Box<[CudaDeviceInfo]>,
},
#[error("CUDA device {device:?} initialization failed during {operation}: {source}")]
Initialization {
device: CudaDeviceId,
operation: &'static str,
#[source]
source: tenferro_tensor::BoxError,
},
}
impl CudaDeviceError {
pub fn operation(&self) -> Option<&'static str> {
match self {
Self::Discovery { operation, .. } | Self::Initialization { operation, .. } => {
Some(operation)
}
Self::Unavailable { .. } => None,
}
}
pub fn requested(&self) -> Option<CudaDeviceId> {
match self {
Self::Unavailable { requested, .. } => Some(*requested),
Self::Discovery { .. } | Self::Initialization { .. } => None,
}
}
pub fn discovered(&self) -> Option<&[CudaDeviceInfo]> {
match self {
Self::Unavailable { discovered, .. } => Some(discovered),
Self::Discovery { .. } | Self::Initialization { .. } => None,
}
}
pub fn device(&self) -> Option<CudaDeviceId> {
match self {
Self::Initialization { device, .. } => Some(*device),
Self::Discovery { .. } | Self::Unavailable { .. } => None,
}
}
}