Skip to main content

trustformers_core/
device.rs

1//! Device abstraction for hardware acceleration
2//!
3//! This module provides a simple Device enum for specifying where computations
4//! should be executed (CPU, CUDA GPU, Metal GPU, etc.).
5
6use serde::{Deserialize, Serialize};
7
8/// Device specification for tensor operations
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
10pub enum Device {
11    /// CPU execution
12    #[default]
13    CPU,
14    /// NVIDIA CUDA GPU (with device ID)
15    CUDA(usize),
16    /// Apple Metal GPU (with device ID)
17    Metal(usize),
18    /// AMD ROCm GPU (with device ID)
19    ROCm(usize),
20    /// WebGPU
21    WebGPU,
22}
23
24impl Device {
25    /// Returns true if this device is a GPU
26    pub fn is_gpu(&self) -> bool {
27        !matches!(self, Device::CPU)
28    }
29
30    /// Returns true if this device is Metal GPU
31    pub fn is_metal(&self) -> bool {
32        matches!(self, Device::Metal(_))
33    }
34
35    /// Returns true if this device is CUDA GPU
36    pub fn is_cuda(&self) -> bool {
37        matches!(self, Device::CUDA(_))
38    }
39
40    /// Returns true if this device is CPU
41    pub fn is_cpu(&self) -> bool {
42        matches!(self, Device::CPU)
43    }
44
45    /// Returns the device ID for GPU devices
46    pub fn device_id(&self) -> Option<usize> {
47        match self {
48            Device::CUDA(id) | Device::Metal(id) | Device::ROCm(id) => Some(*id),
49            _ => None,
50        }
51    }
52
53    /// Create a Metal device, or CPU if Metal is not available
54    #[cfg(all(target_os = "macos", feature = "metal"))]
55    pub fn metal_if_available(device_id: usize) -> Device {
56        // Try to initialize Metal backend to check availability
57        // This is more reliable than scirs2's platform detection
58        #[cfg(all(target_os = "macos", feature = "metal"))]
59        {
60            use metal::Device as MetalDevice;
61            if MetalDevice::system_default().is_some() {
62                Device::Metal(device_id)
63            } else {
64                Device::CPU
65            }
66        }
67
68        #[cfg(not(all(target_os = "macos", feature = "metal")))]
69        Device::CPU
70    }
71
72    #[cfg(not(all(target_os = "macos", feature = "metal")))]
73    pub fn metal_if_available(_device_id: usize) -> Device {
74        Device::CPU
75    }
76
77    /// Create a CUDA device, or CPU if CUDA is not available.
78    ///
79    /// Availability is determined by a genuine runtime probe
80    /// (`crate::gpu_ops::cuda::oxicuda_cuda_available()`), which `dlopen`s the CUDA
81    /// driver and queries the device count. This is deliberately *not* based on
82    /// `scirs2_core::simd_ops::PlatformCapabilities`: as of scirs2-core 0.6.0 its
83    /// `cuda_available` flag is hardcoded to `false` (CUDA support was retired
84    /// upstream), so relying on it here would make this constructor always return
85    /// `Device::CPU` even on machines with a working CUDA GPU.
86    #[cfg(feature = "cuda")]
87    pub fn cuda_if_available(device_id: usize) -> Device {
88        if crate::gpu_ops::cuda::oxicuda_cuda_available() {
89            Device::CUDA(device_id)
90        } else {
91            Device::CPU
92        }
93    }
94
95    #[cfg(not(feature = "cuda"))]
96    pub fn cuda_if_available(_device_id: usize) -> Device {
97        Device::CPU
98    }
99
100    /// Get the best available device, preferring CUDA, then Metal, then CPU.
101    ///
102    /// Like [`Device::cuda_if_available`] and [`Device::metal_if_available`], this
103    /// probes trustformers' own GPU backends directly rather than going through
104    /// `scirs2_core::simd_ops::PlatformCapabilities`, whose `cuda_available` is
105    /// hardcoded `false` upstream and whose `metal_available` requires a scirs2
106    /// `metal` feature that trustformers never enables. Using it here would silently
107    /// always fall back to `Device::CPU`, even on CUDA/Metal-capable machines.
108    pub fn best_available() -> Device {
109        #[cfg(feature = "cuda")]
110        if crate::gpu_ops::cuda::oxicuda_cuda_available() {
111            return Device::CUDA(0);
112        }
113
114        #[cfg(all(target_os = "macos", feature = "metal"))]
115        {
116            use metal::Device as MetalDevice;
117            if MetalDevice::system_default().is_some() {
118                return Device::Metal(0);
119            }
120        }
121
122        Device::CPU
123    }
124}
125
126impl std::fmt::Display for Device {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            Device::CPU => write!(f, "CPU"),
130            Device::CUDA(id) => write!(f, "CUDA:{}", id),
131            Device::Metal(id) => write!(f, "Metal:{}", id),
132            Device::ROCm(id) => write!(f, "ROCm:{}", id),
133            Device::WebGPU => write!(f, "WebGPU"),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_device_checks() {
144        assert!(Device::CPU.is_cpu());
145        assert!(!Device::CPU.is_gpu());
146        assert!(!Device::CPU.is_metal());
147
148        assert!(Device::Metal(0).is_metal());
149        assert!(Device::Metal(0).is_gpu());
150        assert!(!Device::Metal(0).is_cpu());
151
152        assert!(Device::CUDA(0).is_cuda());
153        assert!(Device::CUDA(0).is_gpu());
154    }
155
156    #[test]
157    fn test_device_id() {
158        assert_eq!(Device::CPU.device_id(), None);
159        assert_eq!(Device::Metal(0).device_id(), Some(0));
160        assert_eq!(Device::CUDA(1).device_id(), Some(1));
161    }
162
163    #[test]
164    fn test_device_display() {
165        assert_eq!(Device::CPU.to_string(), "CPU");
166        assert_eq!(Device::Metal(0).to_string(), "Metal:0");
167        assert_eq!(Device::CUDA(1).to_string(), "CUDA:1");
168    }
169
170    /// `cuda_if_available` must never panic, regardless of whether the host
171    /// actually has a CUDA-capable GPU. On a machine without CUDA it must fall
172    /// back to `Device::CPU`; this test intentionally does not assert which
173    /// branch is taken since that depends on the machine running the test.
174    #[test]
175    fn test_cuda_if_available_does_not_panic() {
176        let device = Device::cuda_if_available(0);
177        assert!(matches!(device, Device::CPU | Device::CUDA(0)));
178    }
179
180    /// `metal_if_available` must never panic, regardless of whether the host
181    /// actually has a Metal-capable GPU.
182    #[test]
183    fn test_metal_if_available_does_not_panic() {
184        let device = Device::metal_if_available(0);
185        assert!(matches!(device, Device::CPU | Device::Metal(0)));
186    }
187
188    /// `best_available` must never panic and must always return a valid
189    /// `Device`, without asserting a specific GPU is present (CI/dev machines
190    /// running this test are not guaranteed to have CUDA or Metal hardware).
191    #[test]
192    fn test_best_available_does_not_panic() {
193        let device = Device::best_available();
194        assert!(matches!(
195            device,
196            Device::CPU | Device::CUDA(0) | Device::Metal(0)
197        ));
198    }
199}