Skip to main content

tenferro_gpu/cubecl/
device.rs

1use std::fmt;
2
3use tenferro_tensor::BoxError;
4
5use super::identity::{CudaComputeCapability, CudaDeviceUuid};
6
7#[derive(Debug, thiserror::Error)]
8enum CudaDriverDiscoveryError {
9    #[error("CUDA driver call {function} failed: {source}")]
10    DriverCall {
11        function: &'static str,
12        #[source]
13        source: cudarc::driver::result::DriverError,
14    },
15    #[error("CUDA returned an invalid device count {count}")]
16    InvalidDeviceCount { count: i32 },
17    #[error("CUDA device ordinal {device:?} is out of range")]
18    DeviceOrdinalOutOfRange { device: CudaDeviceId },
19}
20
21fn boxed_discovery_error(error: CudaDriverDiscoveryError) -> BoxError {
22    Box::new(error)
23}
24
25struct CudaDriverApi;
26
27/// Provider-qualified identity of a CUDA device ordinal.
28///
29/// A device ID is an opaque CUDA provider value. Use [`Self::ordinal`] only
30/// when passing the selected ordinal to CUDA APIs. The ordinal is
31/// process-visible and may change when `CUDA_VISIBLE_DEVICES` changes.
32///
33/// # Examples
34///
35/// ```
36/// use tenferro_gpu::cuda::CudaDeviceId;
37///
38/// let device = CudaDeviceId::from_ordinal(2);
39/// assert_eq!(device.ordinal(), 2);
40/// ```
41#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
42pub struct CudaDeviceId(u32);
43
44impl CudaDeviceId {
45    /// Construct a device ID from its CUDA ordinal.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use tenferro_gpu::cuda::CudaDeviceId;
51    ///
52    /// const DEVICE: CudaDeviceId = CudaDeviceId::from_ordinal(0);
53    /// assert_eq!(DEVICE.ordinal(), 0);
54    /// ```
55    pub const fn from_ordinal(ordinal: u32) -> Self {
56        Self(ordinal)
57    }
58
59    /// Return the CUDA ordinal represented by this device ID.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use tenferro_gpu::cuda::CudaDeviceId;
65    ///
66    /// let device = CudaDeviceId::from_ordinal(3);
67    /// assert_eq!(device.ordinal(), 3);
68    /// ```
69    pub const fn ordinal(self) -> u32 {
70        self.0
71    }
72}
73
74impl fmt::Debug for CudaDeviceId {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        formatter
77            .debug_tuple("CudaDeviceId")
78            .field(&self.0)
79            .finish()
80    }
81}
82
83/// Immutable metadata describing one CUDA device.
84///
85/// Equality and debug output are deterministic only while the process-visible
86/// CUDA topology remains unchanged; they do not provide stable identity across
87/// topology changes.
88///
89/// # Examples
90///
91/// ```
92/// use tenferro_gpu::cuda::CudaDeviceInfo;
93///
94/// let _type_name = std::any::type_name::<CudaDeviceInfo>();
95/// ```
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct CudaDeviceInfo {
98    id: CudaDeviceId,
99    name: String,
100    uuid: CudaDeviceUuid,
101    compute_capability: CudaComputeCapability,
102    total_memory_bytes: u64,
103}
104
105impl CudaDeviceInfo {
106    /// Construct device metadata for a CUDA device.
107    pub(crate) fn new(
108        id: CudaDeviceId,
109        name: impl Into<String>,
110        uuid: CudaDeviceUuid,
111        compute_capability: CudaComputeCapability,
112        total_memory_bytes: u64,
113    ) -> Self {
114        Self {
115            id,
116            name: name.into(),
117            uuid,
118            compute_capability,
119            total_memory_bytes,
120        }
121    }
122
123    /// Return this device's provider-qualified ID.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use tenferro_gpu::cuda::CudaDeviceInfo;
129    ///
130    /// let _id = CudaDeviceInfo::id;
131    /// ```
132    pub fn id(&self) -> CudaDeviceId {
133        self.id
134    }
135
136    /// Borrow this device's display name.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use tenferro_gpu::cuda::CudaDeviceInfo;
142    ///
143    /// let _name = CudaDeviceInfo::name;
144    /// ```
145    pub fn name(&self) -> &str {
146        &self.name
147    }
148
149    /// Return the stable physical/MIG identity of this device.
150    ///
151    /// The UUID is a durable comparison identity independent of the
152    /// process-visible ordinal. It is used for diagnostics, topology, and
153    /// explicit cross-runtime/cross-device placement decisions.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use tenferro_gpu::cuda::cuda_devices;
159    ///
160    /// // `cudarc` panics when the CUDA driver library is absent, so the call
161    /// // is guarded (the same pattern `gpu_available` uses).
162    /// let devices = std::panic::catch_unwind(cuda_devices)
163    ///     .unwrap_or_else(|_| Ok(Vec::new()))
164    ///     .unwrap_or_default();
165    /// let uuids: Vec<_> = devices.iter().map(|info| info.uuid()).collect();
166    /// let _ = uuids;
167    /// ```
168    pub fn uuid(&self) -> CudaDeviceUuid {
169        self.uuid
170    }
171
172    /// Return the compute capability of this device.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use tenferro_gpu::cuda::cuda_devices;
178    ///
179    /// // `cudarc` panics when the CUDA driver library is absent, so the call
180    /// // is guarded (the same pattern `gpu_available` uses).
181    /// let devices = std::panic::catch_unwind(cuda_devices)
182    ///     .unwrap_or_else(|_| Ok(Vec::new()))
183    ///     .unwrap_or_default();
184    /// let capabilities: Vec<_> = devices
185    ///     .iter()
186    ///     .map(|info| info.compute_capability())
187    ///     .collect();
188    /// let _ = capabilities;
189    /// ```
190    pub fn compute_capability(&self) -> CudaComputeCapability {
191        self.compute_capability
192    }
193
194    /// Return the total device memory in bytes.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use tenferro_gpu::cuda::cuda_devices;
200    ///
201    /// // `cudarc` panics when the CUDA driver library is absent, so the call
202    /// // is guarded (the same pattern `gpu_available` uses).
203    /// let devices = std::panic::catch_unwind(cuda_devices)
204    ///     .unwrap_or_else(|_| Ok(Vec::new()))
205    ///     .unwrap_or_default();
206    /// let memory: Vec<u64> = devices
207    ///     .iter()
208    ///     .map(|info| info.total_memory_bytes())
209    ///     .collect();
210    /// let _ = memory;
211    /// ```
212    pub fn total_memory_bytes(&self) -> u64 {
213        self.total_memory_bytes
214    }
215}
216
217pub(crate) trait DiscoveryDriver {
218    fn initialize(&self) -> Result<(), BoxError>;
219
220    fn device_count(&self) -> Result<u32, BoxError>;
221
222    fn device_name(&self, device: CudaDeviceId) -> Result<String, BoxError>;
223
224    fn device_uuid(&self, device: CudaDeviceId) -> Result<CudaDeviceUuid, BoxError>;
225
226    fn compute_capability(&self, device: CudaDeviceId) -> Result<CudaComputeCapability, BoxError>;
227
228    fn total_memory_bytes(&self, device: CudaDeviceId) -> Result<u64, BoxError>;
229}
230
231pub(crate) fn discover_with(
232    driver: &impl DiscoveryDriver,
233) -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> {
234    driver
235        .initialize()
236        .map_err(|source| CudaDeviceError::Discovery {
237            operation: "initialize_driver",
238            source,
239        })?;
240    let device_count = driver
241        .device_count()
242        .map_err(|source| CudaDeviceError::Discovery {
243            operation: "enumerate_devices",
244            source,
245        })?;
246
247    let mut devices = Vec::new();
248    for ordinal in 0..device_count {
249        let id = CudaDeviceId::from_ordinal(ordinal);
250        let name = driver
251            .device_name(id)
252            .map_err(|source| CudaDeviceError::Discovery {
253                operation: "get_device_name",
254                source,
255            })?;
256        let uuid = driver
257            .device_uuid(id)
258            .map_err(|source| CudaDeviceError::Discovery {
259                operation: "get_device_uuid",
260                source,
261            })?;
262        let compute_capability =
263            driver
264                .compute_capability(id)
265                .map_err(|source| CudaDeviceError::Discovery {
266                    operation: "get_compute_capability",
267                    source,
268                })?;
269        let total_memory_bytes =
270            driver
271                .total_memory_bytes(id)
272                .map_err(|source| CudaDeviceError::Discovery {
273                    operation: "get_total_memory",
274                    source,
275                })?;
276        devices.push(CudaDeviceInfo::new(
277            id,
278            name,
279            uuid,
280            compute_capability,
281            total_memory_bytes,
282        ));
283    }
284    Ok(devices)
285}
286
287impl DiscoveryDriver for CudaDriverApi {
288    fn initialize(&self) -> Result<(), BoxError> {
289        cudarc::driver::result::init().map_err(|source| {
290            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
291                function: "cuInit",
292                source,
293            })
294        })
295    }
296
297    fn device_count(&self) -> Result<u32, BoxError> {
298        let count = cudarc::driver::result::device::get_count().map_err(|source| {
299            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
300                function: "cuDeviceGetCount",
301                source,
302            })
303        })?;
304        u32::try_from(count).map_err(|_| {
305            boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count })
306        })
307    }
308
309    fn device_name(&self, device: CudaDeviceId) -> Result<String, BoxError> {
310        let cuda_device = self.cuda_device(device)?;
311        let name = cudarc::driver::result::device::get_name(cuda_device).map_err(|source| {
312            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
313                function: "cuDeviceGetName",
314                source,
315            })
316        })?;
317        Ok(name)
318    }
319
320    fn device_uuid(&self, device: CudaDeviceId) -> Result<CudaDeviceUuid, BoxError> {
321        let cuda_device = self.cuda_device(device)?;
322        let uuid = cudarc::driver::result::device::get_uuid(cuda_device).map_err(|source| {
323            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
324                function: "cuDeviceGetUuid",
325                source,
326            })
327        })?;
328        let mut bytes = [0u8; 16];
329        #[allow(clippy::needless_range_loop)]
330        for (index, byte) in bytes.iter_mut().enumerate() {
331            *byte = uuid.bytes[index] as u8;
332        }
333        Ok(CudaDeviceUuid::from_bytes(bytes))
334    }
335
336    fn compute_capability(&self, device: CudaDeviceId) -> Result<CudaComputeCapability, BoxError> {
337        let cuda_device = self.cuda_device(device)?;
338        use cudarc::driver::sys::CUdevice_attribute_enum as Attr;
339        let (major_name, minor_name) = unsafe {
340            (
341                cudarc::driver::result::device::get_attribute(
342                    cuda_device,
343                    Attr::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR,
344                ),
345                cudarc::driver::result::device::get_attribute(
346                    cuda_device,
347                    Attr::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR,
348                ),
349            )
350        };
351        let major = major_name.map_err(|source| {
352            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
353                function: "cuDeviceGetAttribute(CC_MAJOR)",
354                source,
355            })
356        })?;
357        let minor = minor_name.map_err(|source| {
358            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
359                function: "cuDeviceGetAttribute(CC_MINOR)",
360                source,
361            })
362        })?;
363        Ok(CudaComputeCapability {
364            major: u32::try_from(major).map_err(|_| {
365                boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: major })
366            })?,
367            minor: u32::try_from(minor).map_err(|_| {
368                boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: minor })
369            })?,
370        })
371    }
372
373    fn total_memory_bytes(&self, device: CudaDeviceId) -> Result<u64, BoxError> {
374        let cuda_device = self.cuda_device(device)?;
375        // SAFETY: `cuda_device` was returned by `cuDeviceGet` for this session.
376        let bytes = unsafe { cudarc::driver::result::device::total_mem(cuda_device) }.map_err(
377            |source| {
378                boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
379                    function: "cuDeviceTotalMem",
380                    source,
381                })
382            },
383        )?;
384        u64::try_from(bytes).map_err(|_| {
385            boxed_discovery_error(CudaDriverDiscoveryError::InvalidDeviceCount { count: i32::MAX })
386        })
387    }
388}
389
390impl CudaDriverApi {
391    fn cuda_device(&self, device: CudaDeviceId) -> Result<cudarc::driver::sys::CUdevice, BoxError> {
392        let ordinal = i32::try_from(device.ordinal()).map_err(|_| {
393            boxed_discovery_error(CudaDriverDiscoveryError::DeviceOrdinalOutOfRange { device })
394        })?;
395        cudarc::driver::result::device::get(ordinal).map_err(|source| {
396            boxed_discovery_error(CudaDriverDiscoveryError::DriverCall {
397                function: "cuDeviceGet",
398                source,
399            })
400        })
401    }
402}
403
404/// Discover CUDA devices visible to the current process.
405///
406/// Discovery initializes the CUDA driver and queries device ordinals directly.
407/// It does not create a CUDA context, CUDA runtime, CubeCL runtime, backend, or
408/// client. Device ordinals are process-visible and may change when
409/// `CUDA_VISIBLE_DEVICES` changes. The returned values have deterministic
410/// `Eq` and `Debug` behavior only while that process-visible topology remains
411/// unchanged.
412///
413/// # Errors
414///
415/// Returns [`CudaDeviceError::Discovery`] for driver initialization, device
416/// enumeration, or device-name lookup failures.
417///
418/// # Examples
419///
420/// ```
421/// use tenferro_gpu::{cuda::cuda_devices, cuda::CudaDeviceError, cuda::CudaDeviceInfo};
422///
423/// let _discover: fn() -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> = cuda_devices;
424/// ```
425pub fn cuda_devices() -> Result<Vec<CudaDeviceInfo>, CudaDeviceError> {
426    discover_with(&CudaDriverApi)
427}
428
429pub(crate) fn unavailable_device_error(
430    requested: CudaDeviceId,
431    discovered: Vec<CudaDeviceInfo>,
432) -> CudaDeviceError {
433    CudaDeviceError::Unavailable {
434        requested,
435        discovered: discovered.into_boxed_slice(),
436    }
437}
438
439/// Structured failures from CUDA device discovery and initialization.
440///
441/// The error retains only provider-neutral device identities and metadata. It
442/// does not expose the concrete CUDA runtime error type behind a source.
443///
444/// # Examples
445///
446/// ```
447/// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
448///
449/// let error = CudaDeviceError::Unavailable {
450///     requested: CudaDeviceId::from_ordinal(1),
451///     discovered: Vec::new().into_boxed_slice(),
452/// };
453/// assert_eq!(error.requested(), Some(CudaDeviceId::from_ordinal(1)));
454/// ```
455#[derive(Debug, thiserror::Error)]
456#[non_exhaustive]
457pub enum CudaDeviceError {
458    /// Device discovery failed while performing the named operation.
459    #[error("CUDA device discovery failed during {operation}: {source}")]
460    Discovery {
461        /// The provider-neutral discovery operation being performed.
462        operation: &'static str,
463        /// The underlying discovery failure.
464        #[source]
465        source: tenferro_tensor::BoxError,
466    },
467    /// The requested device was not present in the discovered device list.
468    #[error(
469        "requested CUDA device {requested:?} is unavailable; discovered devices: {discovered:?}"
470    )]
471    Unavailable {
472        /// The device selected by the caller.
473        requested: CudaDeviceId,
474        /// Devices found during discovery, in discovery order.
475        discovered: Box<[CudaDeviceInfo]>,
476    },
477    /// Device initialization failed while performing the named operation.
478    #[error("CUDA device {device:?} initialization failed during {operation}: {source}")]
479    Initialization {
480        /// The device whose runtime could not be initialized.
481        device: CudaDeviceId,
482        /// The provider-neutral initialization operation being performed.
483        operation: &'static str,
484        /// The underlying initialization failure.
485        #[source]
486        source: tenferro_tensor::BoxError,
487    },
488}
489
490impl CudaDeviceError {
491    /// Return the operation associated with discovery or initialization.
492    ///
493    /// # Examples
494    ///
495    /// ```
496    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
497    ///
498    /// let error = CudaDeviceError::Unavailable {
499    ///     requested: CudaDeviceId::from_ordinal(1),
500    ///     discovered: Vec::new().into_boxed_slice(),
501    /// };
502    /// assert_eq!(error.operation(), None);
503    /// ```
504    pub fn operation(&self) -> Option<&'static str> {
505        match self {
506            Self::Discovery { operation, .. } | Self::Initialization { operation, .. } => {
507                Some(operation)
508            }
509            Self::Unavailable { .. } => None,
510        }
511    }
512
513    /// Return the requested device for an unavailable-device error.
514    ///
515    /// # Examples
516    ///
517    /// ```
518    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
519    ///
520    /// let requested = CudaDeviceId::from_ordinal(1);
521    /// let error = CudaDeviceError::Unavailable {
522    ///     requested,
523    ///     discovered: Vec::new().into_boxed_slice(),
524    /// };
525    /// assert_eq!(error.requested(), Some(requested));
526    /// ```
527    pub fn requested(&self) -> Option<CudaDeviceId> {
528        match self {
529            Self::Unavailable { requested, .. } => Some(*requested),
530            Self::Discovery { .. } | Self::Initialization { .. } => None,
531        }
532    }
533
534    /// Borrow the devices discovered for an unavailable-device error.
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
540    ///
541    /// let error = CudaDeviceError::Unavailable {
542    ///     requested: CudaDeviceId::from_ordinal(1),
543    ///     discovered: Vec::new().into_boxed_slice(),
544    /// };
545    /// assert!(error.discovered().is_some_and(<[_]>::is_empty));
546    /// ```
547    pub fn discovered(&self) -> Option<&[CudaDeviceInfo]> {
548        match self {
549            Self::Unavailable { discovered, .. } => Some(discovered),
550            Self::Discovery { .. } | Self::Initialization { .. } => None,
551        }
552    }
553
554    /// Return the device whose initialization failed.
555    ///
556    /// # Examples
557    ///
558    /// ```
559    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
560    ///
561    /// let device = CudaDeviceId::from_ordinal(1);
562    /// let error = CudaDeviceError::Initialization {
563    ///     device,
564    ///     operation: "create_client",
565    ///     source: Box::new(std::io::Error::other("context failed")),
566    /// };
567    /// assert_eq!(error.device(), Some(device));
568    /// ```
569    pub fn device(&self) -> Option<CudaDeviceId> {
570        match self {
571            Self::Initialization { device, .. } => Some(*device),
572            Self::Discovery { .. } | Self::Unavailable { .. } => None,
573        }
574    }
575}