1use std::fmt;
25use std::str::FromStr;
26
27#[derive(Debug, Default, Clone, PartialEq, Eq)]
39pub enum Device {
40 #[default]
42 Default,
43 Discrete { index: usize },
48 Integrated { index: usize },
50 Virtual { index: usize },
52 Cpu,
55}
56
57impl FromStr for Device {
58 type Err = String;
59
60 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 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}