1mod builder;
2mod env;
3pub(crate) mod find;
4mod inner;
5
6use std::fmt::Display;
7use std::str::FromStr;
8
9pub use builder::DeviceConfigBuilder;
10pub(crate) use find::{expand_status, find_device_files_in};
11use serde::{Deserialize, Serialize};
12
13pub use self::builder::NotDetermined;
14pub use self::env::EnvBuilder;
15use self::inner::DeviceConfigInner;
16use crate::{Arch, DeviceError};
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
70#[serde(into = "String", try_from = "&str")]
71pub struct DeviceConfig {
72 pub(crate) inner: DeviceConfigInner,
73}
74
75impl DeviceConfig {
76 pub fn warboy() -> DeviceConfigBuilder<Arch, NotDetermined, NotDetermined> {
78 DeviceConfigBuilder {
79 arch: Arch::WarboyB0,
80 mode: NotDetermined { _priv: () },
81 count: NotDetermined { _priv: () },
82 }
83 }
84
85 pub fn warboy_a0() -> DeviceConfigBuilder<Arch, NotDetermined, NotDetermined> {
86 DeviceConfigBuilder {
87 arch: Arch::WarboyA0,
88 mode: NotDetermined { _priv: () },
89 count: NotDetermined { _priv: () },
90 }
91 }
92
93 pub fn from_env<K: ToString>(key: K) -> EnvBuilder<NotDetermined> {
96 EnvBuilder::<NotDetermined>::from_env(key)
97 }
98}
99
100impl Default for DeviceConfig {
101 fn default() -> Self {
102 DeviceConfig::warboy().fused().count(1)
103 }
104}
105
106impl FromStr for DeviceConfig {
107 type Err = DeviceError;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 Ok(Self {
111 inner: DeviceConfigInner::from_str(s)?,
112 })
113 }
114}
115
116impl<'a> TryFrom<&'a str> for DeviceConfig {
117 type Error = DeviceError;
118
119 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
120 DeviceConfig::from_str(value)
121 }
122}
123
124impl Display for DeviceConfig {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 self.inner.fmt(f)
127 }
128}
129
130impl From<DeviceConfig> for String {
131 fn from(config: DeviceConfig) -> Self {
132 config.to_string()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::list::list_devices_with;
140
141 #[tokio::test]
142 async fn test_find_device_files() -> eyre::Result<()> {
143 let devices =
145 list_devices_with("../test_data/test-0/dev", "../test_data/test-0/sys").await?;
146 let devices_with_statuses = expand_status(devices).await?;
147
148 let config = DeviceConfig::warboy().single().count(4);
150 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
151 let mut found_device_file_names: Vec<&str> =
152 found_device_files.iter().map(|f| f.filename()).collect();
153 found_device_file_names.sort();
154 assert_eq!(
155 found_device_file_names,
156 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
157 );
158
159 let config = DeviceConfig::warboy().single().all();
161 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
162 let mut found_device_file_names: Vec<&str> =
163 found_device_files.iter().map(|f| f.filename()).collect();
164 found_device_file_names.sort();
165 assert_eq!(
166 found_device_file_names,
167 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
168 );
169
170 let config = DeviceConfig::warboy().single().count(5);
172 let found = find_device_files_in(&config, &devices_with_statuses);
173 match found {
174 Ok(_) => panic!("looking for 5 different cores should fail"),
175 Err(e) => assert!(matches!(e, DeviceError::DeviceNotFound { .. })),
176 }
177
178 let config = DeviceConfig::warboy().fused().count(2);
180 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
181 let mut found_device_file_names: Vec<&str> =
182 found_device_files.iter().map(|f| f.filename()).collect();
183 found_device_file_names.sort();
184 assert_eq!(found_device_file_names, &["npu0pe0-1", "npu1pe0-1"],);
185
186 let config = DeviceConfig::warboy().fused().all();
188 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
189 let mut found_device_file_names: Vec<&str> =
190 found_device_files.iter().map(|f| f.filename()).collect();
191 found_device_file_names.sort();
192 assert_eq!(found_device_file_names, &["npu0pe0-1", "npu1pe0-1"],);
193
194 let config = DeviceConfig::warboy().fused().count(3);
196 let found = find_device_files_in(&config, &devices_with_statuses);
197 match found {
198 Ok(_) => panic!("looking for 3 different fused cores should fail"),
199 Err(e) => assert!(matches!(e, DeviceError::DeviceNotFound { .. })),
200 }
201
202 Ok(())
203 }
204
205 #[test]
206 fn test_config_symmetric_display() -> eyre::Result<()> {
207 assert_eq!("npu:0".parse::<DeviceConfig>()?.to_string(), "npu:0");
208 assert_eq!("npu:1".parse::<DeviceConfig>()?.to_string(), "npu:1");
209 assert_eq!("npu:0:0".parse::<DeviceConfig>()?.to_string(), "npu:0:0");
210 assert_eq!("npu:0:1".parse::<DeviceConfig>()?.to_string(), "npu:0:1");
211 assert_eq!("npu:1:0".parse::<DeviceConfig>()?.to_string(), "npu:1:0");
212 assert_eq!(
213 "npu:0:0-1".parse::<DeviceConfig>()?.to_string(),
214 "npu:0:0-1"
215 );
216
217 assert_eq!(
218 "warboy(1)*2".parse::<DeviceConfig>()?.to_string(),
219 "warboy(1)*2"
220 );
221 assert_eq!(
222 "warboy(2)*4".parse::<DeviceConfig>()?.to_string(),
223 "warboy(2)*4"
224 );
225
226 Ok(())
227 }
228
229 #[test]
230 fn test_config_comma_separated() -> eyre::Result<()> {
231 let config =
232 "npu:0:0,npu:0:1,npu:0:0-1,warboy(1)*1,warboy(2)*2,npu0pe0".parse::<DeviceConfig>()?;
233
234 assert_eq!(
235 config.inner.cfgs,
236 vec![
237 "npu:0:0".parse::<crate::config::inner::Config>()?,
238 "npu:0:1".parse::<crate::config::inner::Config>()?,
239 "npu:0:0-1".parse::<crate::config::inner::Config>()?,
240 "warboy(1)*1".parse::<crate::config::inner::Config>()?,
241 "warboy(2)*2".parse::<crate::config::inner::Config>()?,
242 "npu0pe0".parse::<crate::config::inner::Config>()?,
243 ]
244 );
245 Ok(())
246 }
247
248 #[test]
249 fn test_config_from_env() -> eyre::Result<()> {
250 let key = "ENV_KEY";
251 std::env::set_var(
252 key,
253 "npu:0:0,npu:0:1,npu:0:0-1,warboy(1)*1,warboy(2)*2,npu0pe0",
254 );
255 let config = DeviceConfig::from_env(key).build()?;
256
257 assert_eq!(
258 config.inner.cfgs,
259 vec![
260 "npu:0:0".parse::<crate::config::inner::Config>()?,
261 "npu:0:1".parse::<crate::config::inner::Config>()?,
262 "npu:0:0-1".parse::<crate::config::inner::Config>()?,
263 "warboy(1)*1".parse::<crate::config::inner::Config>()?,
264 "warboy(2)*2".parse::<crate::config::inner::Config>()?,
265 "npu0pe0".parse::<crate::config::inner::Config>()?,
266 ]
267 );
268 Ok(())
269 }
270
271 #[tokio::test]
272 async fn test_find_device_files_with_comma_separated() -> eyre::Result<()> {
273 let devices =
275 list_devices_with("../test_data/test-0/dev", "../test_data/test-0/sys").await?;
276 let devices_with_statuses = expand_status(devices).await?;
277
278 let config = "npu:0:0,npu:0:1,npu:1:0,npu:1:1".parse::<DeviceConfig>()?;
280 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
281 let mut found_device_file_names: Vec<&str> =
282 found_device_files.iter().map(|f| f.filename()).collect();
283 found_device_file_names.sort();
284 assert_eq!(
285 found_device_file_names,
286 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
287 );
288
289 let config = "npu:0:0,npu0pe1,npu:1:0,npu1pe1".parse::<DeviceConfig>()?;
290 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
291 let mut found_device_file_names: Vec<&str> =
292 found_device_files.iter().map(|f| f.filename()).collect();
293 found_device_file_names.sort();
294 assert_eq!(
295 found_device_file_names,
296 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
297 );
298
299 let config = "warboy(1)*1,warboy(1)*1,warboy(1)*1,warboy(1)*1".parse::<DeviceConfig>()?;
300 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
301 let mut found_device_file_names: Vec<&str> =
302 found_device_files.iter().map(|f| f.filename()).collect();
303 found_device_file_names.sort();
304 assert_eq!(
305 found_device_file_names,
306 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
307 );
308
309 let config = "npu:0:0,npu:0:1,warboy(1)*2".parse::<DeviceConfig>()?;
310 let found_device_files = find_device_files_in(&config, &devices_with_statuses)?;
311 let mut found_device_file_names: Vec<&str> =
312 found_device_files.iter().map(|f| f.filename()).collect();
313 found_device_file_names.sort();
314 assert_eq!(
315 found_device_file_names,
316 &["npu0pe0", "npu0pe1", "npu1pe0", "npu1pe1"],
317 );
318
319 Ok(())
320 }
321
322 #[tokio::test]
323 async fn test_find_device_files_with_failing_cases() -> eyre::Result<()> {
324 let devices =
326 list_devices_with("../test_data/test-0/dev", "../test_data/test-0/sys").await?;
327 let devices_with_statuses = expand_status(devices).await?;
328
329 let config = "npu:0:0,npu:0:0".parse::<DeviceConfig>()?;
331 let found = find_device_files_in(&config, &devices_with_statuses);
332 match found {
333 Ok(_) => panic!("looking for duplicate devices should fail"),
334 Err(e) => assert!(matches!(e, DeviceError::DeviceNotFound { .. })),
335 }
336
337 let config = "npu:0:0-1,npu0pe0-1".parse::<DeviceConfig>()?;
338 let found = find_device_files_in(&config, &devices_with_statuses);
339 match found {
340 Ok(_) => panic!("looking for duplicate devices should fail"),
341 Err(e) => assert!(matches!(e, DeviceError::DeviceNotFound { .. })),
342 }
343
344 let config = "npu:2:0".parse::<DeviceConfig>()?;
345 let found = find_device_files_in(&config, &devices_with_statuses);
346 match found {
347 Ok(_) => panic!("looking for not exist device should fail"),
348 Err(e) => assert!(matches!(e, DeviceError::DeviceNotFound { .. })),
349 }
350
351 Ok(())
352 }
353}