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}