Skip to main content

av_denoise_core/
device.rs

1//! Which physical device to run on.
2//!
3//! A [`Device`] picks the hardware inside a backend, while
4//! [`Accelerator`](crate::accelerate::Accelerator) picks the backend
5//! itself. The two are chosen separately because most backends expose
6//! more than one device.
7//!
8//! [`Device`] implements `FromStr`, so it can be taken straight from a
9//! command-line flag or a config file.
10//!
11//! ```
12//! use av_denoise_core::Device;
13//!
14//! // Let the backend decide.
15//! assert_eq!("default".parse::<Device>().unwrap(), Device::Default);
16//!
17//! // Or name the second discrete GPU in the machine.
18//! assert_eq!(
19//!     "discrete:1".parse::<Device>().unwrap(),
20//!     Device::Discrete { index: 1 },
21//! );
22//! ```
23
24use std::fmt;
25use std::str::FromStr;
26
27/// Where to run the compute.
28///
29/// Each variant maps onto the concrete `Device` type of whichever cubecl
30/// runtime was selected.
31///
32/// Not every variant makes sense on every backend. `Integrated` and
33/// `Virtual` are wgpu-only, and `Cpu` does nothing on the `cpu` runtime
34/// while selecting `WgpuDevice::Cpu` on wgpu.
35///
36/// Asking for a variant a runtime cannot honour returns an error from
37/// the matching `to_*` conversion.
38#[derive(Debug, Default, Clone, PartialEq, Eq)]
39pub enum Device {
40    /// Backend-chosen default device.
41    #[default]
42    Default,
43    /// Discrete GPU at ordinal `index`.
44    ///
45    /// Maps to `CudaDevice { index }`, `AmdDevice { index }`, or
46    /// `WgpuDevice::DiscreteGpu(index)`.
47    Discrete { index: usize },
48    /// Integrated GPU at ordinal `index`. wgpu-only.
49    Integrated { index: usize },
50    /// Virtual GPU at ordinal `index`. wgpu-only.
51    Virtual { index: usize },
52    /// The software device. Valid on the `cpu` runtime, and on wgpu
53    /// where it picks the lavapipe or software adapter.
54    Cpu,
55}
56
57impl FromStr for Device {
58    type Err = String;
59
60    /// Accepts the same spellings as the bench CLI.
61    ///
62    /// - `default`, which takes no index
63    /// - `discrete[:N]`, `integrated[:N]`, and `virtual[:N]`, where `N` defaults to 0
64    /// - `cpu`, which takes no index
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        let (kind, suffix) = match s.split_once(':') {
67            Some((kind, idx)) => (kind, Some(idx)),
68            None => (s, None),
69        };
70
71        if matches!(kind, "default" | "cpu") && suffix.is_some() {
72            return Err(format!(
73                "device kind '{kind}' takes no index, got '{s}'. Only discrete, integrated, and virtual take an index"
74            ));
75        }
76
77        let parse_index = |idx: &str| -> Result<usize, String> {
78            idx.parse()
79                .map_err(|_| format!("invalid device index '{idx}' in '{s}'"))
80        };
81        let idx = suffix.unwrap_or("0");
82
83        match kind {
84            "default" => Ok(Device::Default),
85            "cpu" => Ok(Device::Cpu),
86            "discrete" => Ok(Device::Discrete {
87                index: parse_index(idx)?,
88            }),
89            "integrated" => Ok(Device::Integrated {
90                index: parse_index(idx)?,
91            }),
92            "virtual" => Ok(Device::Virtual {
93                index: parse_index(idx)?,
94            }),
95            other => Err(format!(
96                "unknown device kind '{other}', expected default, discrete[:N], integrated[:N], virtual[:N], or cpu"
97            )),
98        }
99    }
100}
101
102impl fmt::Display for Device {
103    /// Writes the selector spelling [`FromStr`] accepts, so a device
104    /// prints as `discrete:1` rather than as its enum variant.
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Device::Default => f.write_str("default"),
108            Device::Discrete { index } => write!(f, "discrete:{index}"),
109            Device::Integrated { index } => write!(f, "integrated:{index}"),
110            Device::Virtual { index } => write!(f, "virtual:{index}"),
111            Device::Cpu => f.write_str("cpu"),
112        }
113    }
114}
115
116#[cfg(feature = "cuda")]
117impl Device {
118    pub fn to_cuda(&self) -> Result<cubecl::cuda::CudaDevice, anyhow::Error> {
119        match self {
120            Device::Default => Ok(cubecl::cuda::CudaDevice { index: 0 }),
121            Device::Discrete { index } => Ok(cubecl::cuda::CudaDevice { index: *index }),
122            other => Err(anyhow::anyhow!(
123                "device {other:?} is not supported on the CUDA runtime, use `default` or `discrete[:N]`"
124            )),
125        }
126    }
127}
128
129#[cfg(feature = "rocm")]
130impl Device {
131    pub fn to_amd(&self) -> Result<cubecl::hip::AmdDevice, anyhow::Error> {
132        match self {
133            Device::Default => Ok(cubecl::hip::AmdDevice { index: 0 }),
134            Device::Discrete { index } => Ok(cubecl::hip::AmdDevice { index: *index }),
135            other => Err(anyhow::anyhow!(
136                "device {other:?} is not supported on the ROCm runtime, use `default` or `discrete[:N]`"
137            )),
138        }
139    }
140}
141
142#[cfg(any(feature = "vulkan", feature = "metal"))]
143impl Device {
144    pub fn to_wgpu(&self) -> Result<cubecl::wgpu::WgpuDevice, anyhow::Error> {
145        use cubecl::wgpu::WgpuDevice;
146        Ok(match self {
147            Device::Default => WgpuDevice::DefaultDevice,
148            Device::Discrete { index } => WgpuDevice::DiscreteGpu(*index),
149            Device::Integrated { index } => WgpuDevice::IntegratedGpu(*index),
150            Device::Virtual { index } => WgpuDevice::VirtualGpu(*index),
151            Device::Cpu => WgpuDevice::Cpu,
152        })
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn parse_default() {
162        assert_eq!("default".parse::<Device>().unwrap(), Device::Default);
163    }
164
165    #[test]
166    fn parse_discrete_with_and_without_index() {
167        assert_eq!(
168            "discrete".parse::<Device>().unwrap(),
169            Device::Discrete { index: 0 },
170        );
171        assert_eq!(
172            "discrete:3".parse::<Device>().unwrap(),
173            Device::Discrete { index: 3 },
174        );
175    }
176
177    #[test]
178    fn parse_integrated_virtual_cpu() {
179        assert_eq!(
180            "integrated:1".parse::<Device>().unwrap(),
181            Device::Integrated { index: 1 },
182        );
183        assert_eq!(
184            "virtual:2".parse::<Device>().unwrap(),
185            Device::Virtual { index: 2 },
186        );
187        assert_eq!("cpu".parse::<Device>().unwrap(), Device::Cpu);
188    }
189
190    #[test]
191    fn parse_rejects_unknown_kind() {
192        assert!("unicorn".parse::<Device>().is_err());
193    }
194
195    #[test]
196    fn parse_rejects_non_numeric_index() {
197        assert!("discrete:abc".parse::<Device>().is_err());
198    }
199
200    #[test]
201    fn parse_rejects_index_on_default_and_cpu() {
202        assert!("default:0".parse::<Device>().is_err());
203        assert!("default:1".parse::<Device>().is_err());
204        assert!("cpu:2".parse::<Device>().is_err());
205    }
206
207    #[test]
208    fn rejected_index_error_names_the_kind() {
209        let err = "default:1".parse::<Device>().unwrap_err();
210        assert!(err.contains("default"), "{err}");
211        let err = "cpu:2".parse::<Device>().unwrap_err();
212        assert!(err.contains("cpu"), "{err}");
213    }
214
215    #[test]
216    fn display_writes_selector_spellings() {
217        assert_eq!(Device::Default.to_string(), "default");
218        assert_eq!(Device::Discrete { index: 1 }.to_string(), "discrete:1");
219        assert_eq!(Device::Integrated { index: 0 }.to_string(), "integrated:0");
220        assert_eq!(Device::Virtual { index: 2 }.to_string(), "virtual:2");
221        assert_eq!(Device::Cpu.to_string(), "cpu");
222    }
223
224    #[test]
225    fn display_round_trips_through_from_str() {
226        let devices = [
227            Device::Default,
228            Device::Discrete { index: 3 },
229            Device::Integrated { index: 1 },
230            Device::Virtual { index: 0 },
231            Device::Cpu,
232        ];
233        for device in devices {
234            let printed = device.to_string();
235            assert_eq!(printed.parse::<Device>().unwrap(), device, "{printed}");
236        }
237    }
238
239    #[test]
240    fn default_is_default_variant() {
241        assert_eq!(Device::default(), Device::Default);
242    }
243}