Skip to main content

tenferro_gpu/cuda/
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
429/// Structured failures from CUDA device discovery and initialization.
430///
431/// The error retains only provider-neutral device identities and metadata. It
432/// does not expose the concrete CUDA runtime error type behind a source.
433///
434/// # Examples
435///
436/// ```
437/// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
438///
439/// let error = CudaDeviceError::Unavailable {
440///     requested: CudaDeviceId::from_ordinal(1),
441///     discovered: Vec::new().into_boxed_slice(),
442/// };
443/// assert_eq!(error.requested(), Some(CudaDeviceId::from_ordinal(1)));
444/// ```
445#[derive(Debug, thiserror::Error)]
446#[non_exhaustive]
447pub enum CudaDeviceError {
448    /// Device discovery failed while performing the named operation.
449    #[error("CUDA device discovery failed during {operation}: {source}")]
450    Discovery {
451        /// The provider-neutral discovery operation being performed.
452        operation: &'static str,
453        /// The underlying discovery failure.
454        #[source]
455        source: tenferro_tensor::BoxError,
456    },
457    /// The requested device was not present in the discovered device list.
458    #[error(
459        "requested CUDA device {requested:?} is unavailable; discovered devices: {discovered:?}"
460    )]
461    Unavailable {
462        /// The device selected by the caller.
463        requested: CudaDeviceId,
464        /// Devices found during discovery, in discovery order.
465        discovered: Box<[CudaDeviceInfo]>,
466    },
467    /// Device initialization failed while performing the named operation.
468    #[error("CUDA device {device:?} initialization failed during {operation}: {source}")]
469    Initialization {
470        /// The device whose runtime could not be initialized.
471        device: CudaDeviceId,
472        /// The provider-neutral initialization operation being performed.
473        operation: &'static str,
474        /// The underlying initialization failure.
475        #[source]
476        source: tenferro_tensor::BoxError,
477    },
478}
479
480impl CudaDeviceError {
481    /// Return the operation associated with discovery or initialization.
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
487    ///
488    /// let error = CudaDeviceError::Unavailable {
489    ///     requested: CudaDeviceId::from_ordinal(1),
490    ///     discovered: Vec::new().into_boxed_slice(),
491    /// };
492    /// assert_eq!(error.operation(), None);
493    /// ```
494    pub fn operation(&self) -> Option<&'static str> {
495        match self {
496            Self::Discovery { operation, .. } | Self::Initialization { operation, .. } => {
497                Some(operation)
498            }
499            Self::Unavailable { .. } => None,
500        }
501    }
502
503    /// Return the requested device for an unavailable-device error.
504    ///
505    /// # Examples
506    ///
507    /// ```
508    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
509    ///
510    /// let requested = CudaDeviceId::from_ordinal(1);
511    /// let error = CudaDeviceError::Unavailable {
512    ///     requested,
513    ///     discovered: Vec::new().into_boxed_slice(),
514    /// };
515    /// assert_eq!(error.requested(), Some(requested));
516    /// ```
517    pub fn requested(&self) -> Option<CudaDeviceId> {
518        match self {
519            Self::Unavailable { requested, .. } => Some(*requested),
520            Self::Discovery { .. } | Self::Initialization { .. } => None,
521        }
522    }
523
524    /// Borrow the devices discovered for an unavailable-device error.
525    ///
526    /// # Examples
527    ///
528    /// ```
529    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
530    ///
531    /// let error = CudaDeviceError::Unavailable {
532    ///     requested: CudaDeviceId::from_ordinal(1),
533    ///     discovered: Vec::new().into_boxed_slice(),
534    /// };
535    /// assert!(error.discovered().is_some_and(<[_]>::is_empty));
536    /// ```
537    pub fn discovered(&self) -> Option<&[CudaDeviceInfo]> {
538        match self {
539            Self::Unavailable { discovered, .. } => Some(discovered),
540            Self::Discovery { .. } | Self::Initialization { .. } => None,
541        }
542    }
543
544    /// Return the device whose initialization failed.
545    ///
546    /// # Examples
547    ///
548    /// ```
549    /// use tenferro_gpu::{cuda::CudaDeviceError, cuda::CudaDeviceId};
550    ///
551    /// let device = CudaDeviceId::from_ordinal(1);
552    /// let error = CudaDeviceError::Initialization {
553    ///     device,
554    ///     operation: "create_client",
555    ///     source: Box::new(std::io::Error::other("context failed")),
556    /// };
557    /// assert_eq!(error.device(), Some(device));
558    /// ```
559    pub fn device(&self) -> Option<CudaDeviceId> {
560        match self {
561            Self::Initialization { device, .. } => Some(*device),
562            Self::Discovery { .. } | Self::Unavailable { .. } => None,
563        }
564    }
565}