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`
63    /// - `discrete[:N]`, `integrated[:N]`, and `virtual[:N]`, where `N`
64    ///   defaults to 0
65    /// - `cpu`
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        let (kind, idx) = s.split_once(':').unwrap_or((s, "0"));
68        let index: usize = idx
69            .parse()
70            .map_err(|_| format!("invalid device index '{idx}' in '{s}'"))?;
71        match kind {
72            "default" => Ok(Device::Default),
73            "discrete" => Ok(Device::Discrete { index }),
74            "integrated" => Ok(Device::Integrated { index }),
75            "virtual" => Ok(Device::Virtual { index }),
76            "cpu" => Ok(Device::Cpu),
77            other => Err(format!(
78                "unknown device kind '{other}', expected default, discrete[:N], integrated[:N], virtual[:N], or cpu"
79            )),
80        }
81    }
82}
83
84impl fmt::Display for Device {
85    /// Writes the selector spelling [`FromStr`] accepts, so a device
86    /// prints as `discrete:1` rather than as its enum variant.
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Device::Default => f.write_str("default"),
90            Device::Discrete { index } => write!(f, "discrete:{index}"),
91            Device::Integrated { index } => write!(f, "integrated:{index}"),
92            Device::Virtual { index } => write!(f, "virtual:{index}"),
93            Device::Cpu => f.write_str("cpu"),
94        }
95    }
96}
97
98#[cfg(feature = "cuda")]
99impl Device {
100    pub fn to_cuda(&self) -> Result<cubecl::cuda::CudaDevice, anyhow::Error> {
101        match self {
102            Device::Default => Ok(cubecl::cuda::CudaDevice { index: 0 }),
103            Device::Discrete { index } => Ok(cubecl::cuda::CudaDevice { index: *index }),
104            other => Err(anyhow::anyhow!(
105                "device {other:?} is not supported on the CUDA runtime, use `default` or `discrete[:N]`"
106            )),
107        }
108    }
109}
110
111#[cfg(feature = "rocm")]
112impl Device {
113    pub fn to_amd(&self) -> Result<cubecl::hip::AmdDevice, anyhow::Error> {
114        match self {
115            Device::Default => Ok(cubecl::hip::AmdDevice { index: 0 }),
116            Device::Discrete { index } => Ok(cubecl::hip::AmdDevice { index: *index }),
117            other => Err(anyhow::anyhow!(
118                "device {other:?} is not supported on the ROCm runtime, use `default` or `discrete[:N]`"
119            )),
120        }
121    }
122}
123
124#[cfg(any(feature = "vulkan", feature = "metal"))]
125impl Device {
126    pub fn to_wgpu(&self) -> Result<cubecl::wgpu::WgpuDevice, anyhow::Error> {
127        use cubecl::wgpu::WgpuDevice;
128        Ok(match self {
129            Device::Default => WgpuDevice::DefaultDevice,
130            Device::Discrete { index } => WgpuDevice::DiscreteGpu(*index),
131            Device::Integrated { index } => WgpuDevice::IntegratedGpu(*index),
132            Device::Virtual { index } => WgpuDevice::VirtualGpu(*index),
133            Device::Cpu => WgpuDevice::Cpu,
134        })
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn parse_default() {
144        assert_eq!("default".parse::<Device>().unwrap(), Device::Default);
145    }
146
147    #[test]
148    fn parse_discrete_with_and_without_index() {
149        assert_eq!(
150            "discrete".parse::<Device>().unwrap(),
151            Device::Discrete { index: 0 },
152        );
153        assert_eq!(
154            "discrete:3".parse::<Device>().unwrap(),
155            Device::Discrete { index: 3 },
156        );
157    }
158
159    #[test]
160    fn parse_integrated_virtual_cpu() {
161        assert_eq!(
162            "integrated:1".parse::<Device>().unwrap(),
163            Device::Integrated { index: 1 },
164        );
165        assert_eq!(
166            "virtual:2".parse::<Device>().unwrap(),
167            Device::Virtual { index: 2 },
168        );
169        assert_eq!("cpu".parse::<Device>().unwrap(), Device::Cpu);
170    }
171
172    #[test]
173    fn parse_rejects_unknown_kind() {
174        assert!("unicorn".parse::<Device>().is_err());
175    }
176
177    #[test]
178    fn parse_rejects_non_numeric_index() {
179        assert!("discrete:abc".parse::<Device>().is_err());
180    }
181
182    #[test]
183    fn display_writes_selector_spellings() {
184        assert_eq!(Device::Default.to_string(), "default");
185        assert_eq!(Device::Discrete { index: 1 }.to_string(), "discrete:1");
186        assert_eq!(Device::Integrated { index: 0 }.to_string(), "integrated:0");
187        assert_eq!(Device::Virtual { index: 2 }.to_string(), "virtual:2");
188        assert_eq!(Device::Cpu.to_string(), "cpu");
189    }
190
191    #[test]
192    fn display_round_trips_through_from_str() {
193        let devices = [
194            Device::Default,
195            Device::Discrete { index: 3 },
196            Device::Integrated { index: 1 },
197            Device::Virtual { index: 0 },
198            Device::Cpu,
199        ];
200        for device in devices {
201            let printed = device.to_string();
202            assert_eq!(printed.parse::<Device>().unwrap(), device, "{printed}");
203        }
204    }
205
206    #[test]
207    fn default_is_default_variant() {
208        assert_eq!(Device::default(), Device::Default);
209    }
210}