1use rlx_driver::Device;
8use rlx_ir::Graph;
9
10use crate::cost::fastest_device_for_with_policy;
11use crate::device_ext::{DEVICE_PRIORITY, is_available, supports_graph};
12use crate::device_parse::{device_label, parse_device, parse_device_list};
13use crate::registry::backend_for;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum DevicePickStrategy {
18 #[default]
20 CostModel,
21 Benchmark { runs: usize },
23}
24
25#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub struct DevicePolicy {
29 allow: Option<Vec<Device>>,
30 deny: Vec<Device>,
31 prefer: Vec<Device>,
32 pick: DevicePickStrategy,
33}
34
35impl DevicePolicy {
36 pub fn all() -> Self {
38 Self::default()
39 }
40
41 pub fn only(devices: impl IntoIterator<Item = Device>) -> Self {
43 Self {
44 allow: Some(devices.into_iter().collect()),
45 ..Self::default()
46 }
47 }
48
49 pub fn with_deny(mut self, devices: impl IntoIterator<Item = Device>) -> Self {
51 self.deny.extend(devices);
52 self
53 }
54
55 pub fn with_prefer(mut self, devices: impl IntoIterator<Item = Device>) -> Self {
57 self.prefer.extend(devices);
58 self
59 }
60
61 pub fn with_benchmark_pick(mut self, runs: usize) -> Self {
63 self.pick = DevicePickStrategy::Benchmark { runs: runs.max(1) };
64 self
65 }
66
67 pub fn pick_strategy(&self) -> DevicePickStrategy {
68 self.pick
69 }
70
71 pub fn from_env() -> Self {
73 Self::from_env_key("RLX")
74 }
75
76 pub fn from_env_key(prefix: &str) -> Self {
78 let mut policy = Self::default();
79 let devices_key = format!("{prefix}_DEVICES");
80 let deny_key = format!("{prefix}_DENY_DEVICES");
81 let prefer_key = format!("{prefix}_PREFER_DEVICES");
82
83 if let Some(raw) = rlx_ir::env::var(&devices_key) {
84 if let Ok(list) = parse_device_list(&raw) {
85 policy.allow = Some(list);
86 }
87 }
88 if let Some(raw) = rlx_ir::env::var(&deny_key) {
89 if let Ok(list) = parse_device_list(&raw) {
90 policy.deny = list;
91 }
92 }
93 if let Some(raw) = rlx_ir::env::var(&prefer_key) {
94 if let Ok(list) = parse_device_list(&raw) {
95 policy.prefer = list;
96 }
97 }
98 let bench_key = format!("{prefix}_BENCHMARK_PICK");
99 if let Some(raw) = rlx_ir::env::var(&bench_key) {
100 if let Ok(runs) = raw.trim().parse::<usize>() {
101 policy.pick = DevicePickStrategy::Benchmark { runs: runs.max(1) };
102 }
103 }
104 policy
105 }
106
107 pub fn probe_set(&self) -> Vec<Device> {
109 self.allow.clone().unwrap_or_else(|| Device::all().to_vec())
110 }
111
112 pub fn apply(&self, mut candidates: Vec<Device>) -> Vec<Device> {
114 if let Some(allow) = &self.allow {
115 candidates.retain(|d| allow.contains(d));
116 }
117 candidates.retain(|d| !self.deny.contains(d));
118 candidates.sort_by_key(|d| self.rank_key(*d));
119 candidates
120 }
121
122 fn rank_key(&self, device: Device) -> (u8, u8) {
123 let prefer = self
124 .prefer
125 .iter()
126 .position(|d| *d == device)
127 .map(|i| i as u8)
128 .unwrap_or(u8::MAX);
129 let platform = DEVICE_PRIORITY
130 .iter()
131 .position(|d| *d == device)
132 .map(|i| i as u8)
133 .unwrap_or(u8::MAX);
134 (prefer, platform)
135 }
136}
137
138pub fn device_from_env() -> Option<Device> {
140 device_from_env_key("RLX")
141}
142
143pub fn device_from_env_key(prefix: &str) -> Option<Device> {
145 let key = format!("{prefix}_DEVICE");
146 rlx_ir::env::var(&key).and_then(|raw| parse_device(&raw).ok())
147}
148
149pub fn devices_for_with_policy(graph: &Graph, policy: &DevicePolicy) -> Vec<Device> {
151 policy.apply(
152 crate::available_devices()
153 .into_iter()
154 .filter(|d| supports_graph(*d, graph))
155 .collect(),
156 )
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub struct DeviceCandidate {
162 pub device: Device,
163 pub label: &'static str,
164 pub available: bool,
165 pub registered: bool,
166 pub supports_graph: bool,
167 pub recommended: bool,
168 pub blocker: Option<String>,
169 pub capabilities: Vec<&'static str>,
173}
174
175pub fn advisory_capabilities(device: Device) -> crate::ExecutableCapabilities {
178 use crate::ExecutableCapabilities as C;
179 match device {
180 Device::Cpu => C {
181 clone: true,
182 moe: true,
183 typed_io: true,
184 active_extent: true,
185 ..C::NONE
186 },
187 Device::Metal => C {
188 clone: true,
189 gpu_handles: true,
190 kv_resident: true,
191 typed_io: true,
192 async_pipeline: true,
193 active_extent: true,
194 ..C::NONE
195 },
196 Device::Mlx => C {
197 clone: true,
198 moe: true,
199 persistent_handles: true,
200 gpu_handles: true,
201 kv_resident: true,
202 typed_io: true,
203 active_extent: true,
204 ..C::NONE
205 },
206 Device::Ane => C {
207 clone: true,
208 typed_io: true,
209 ..C::NONE
210 },
211 Device::Cuda => C {
212 clone: true,
213 gpu_handles: true,
214 kv_resident: true,
215 typed_io: true,
216 active_extent: true,
217 ..C::NONE
218 },
219 Device::Rocm => C {
220 clone: true,
221 moe: true,
222 gpu_handles: true,
223 kv_resident: true,
224 ..C::NONE
225 },
226 Device::Gpu | Device::WebGpu => C {
227 clone: true,
228 gpu_handles: true,
229 typed_io: true,
230 active_extent: true,
231 ..C::NONE
232 },
233 Device::Vulkan => C {
234 clone: true,
235 gpu_handles: true,
236 kv_resident: true,
237 typed_io: true,
238 ..C::NONE
239 },
240 Device::OneApi => C {
241 clone: true,
242 typed_io: true,
243 ..C::NONE
244 },
245 Device::Tpu => C {
246 clone: true,
247 typed_io: true,
248 ..C::NONE
249 },
250 Device::OpenGl | Device::DirectX => C {
251 typed_io: true,
252 ..C::NONE
253 },
254 Device::Hexagon | Device::Xdna => C::NONE,
257 }
258}
259
260pub fn device_report(graph: &Graph, policy: &DevicePolicy) -> Vec<DeviceCandidate> {
262 let recommended = fastest_device_for_with_policy(graph, policy);
263 policy
264 .probe_set()
265 .into_iter()
266 .map(|device| {
267 let available = is_available(device);
268 let registered = backend_for(device).is_some();
269 let supports = available && supports_graph(device, graph);
270 let blocker = if !available {
271 Some("not available on this host or in this build".into())
272 } else if !supports {
273 crate::first_unsupported_op(device, graph)
274 .map(|(idx, op)| format!("unsupported op at node {idx}: {op:?}"))
275 } else if policy.deny.contains(&device) {
276 Some("denied by DevicePolicy".into())
277 } else if policy
278 .allow
279 .as_ref()
280 .is_some_and(|allow| !allow.contains(&device))
281 {
282 Some("not in DevicePolicy allow-list".into())
283 } else {
284 None
285 };
286 DeviceCandidate {
287 device,
288 label: device_label(device),
289 available,
290 registered,
291 supports_graph: supports,
292 recommended: device == recommended,
293 blocker,
294 capabilities: advisory_capabilities(device).enabled_names(),
295 }
296 })
297 .collect()
298}
299
300pub fn resolve_device(
302 graph: &Graph,
303 hint: Option<Device>,
304 policy: &DevicePolicy,
305) -> Result<Device, String> {
306 let candidates = devices_for_with_policy(graph, policy);
307 if candidates.is_empty() {
308 return Err(
309 "no backend can lower this graph under the current policy — \
310 widen DevicePolicy or enable additional Cargo features"
311 .into(),
312 );
313 }
314
315 if let Some(device) = hint {
316 return pick_from_candidates(device, &candidates, "hint");
317 }
318 if let Some(device) = device_from_env() {
319 if let Ok(device) = pick_from_candidates(device, &candidates, "RLX_DEVICE") {
320 return Ok(device);
321 }
322 }
323 Ok(fastest_device_for_with_policy(graph, policy))
324}
325
326fn pick_from_candidates(
327 device: Device,
328 candidates: &[Device],
329 source: &str,
330) -> Result<Device, String> {
331 if candidates.contains(&device) {
332 return Ok(device);
333 }
334 Err(format!(
335 "{source} requested {device} but viable backends are [{}]",
336 candidates
337 .iter()
338 .map(|d| device_label(*d))
339 .collect::<Vec<_>>()
340 .join(", ")
341 ))
342}
343
344pub fn device_chain_from_env() -> Vec<Device> {
346 device_chain_from_env_key("RLX")
347}
348
349pub fn device_chain_from_env_key(prefix: &str) -> Vec<Device> {
351 let key = format!("{prefix}_DEVICE_CHAIN");
352 rlx_ir::env::var(&key)
353 .and_then(|raw| parse_device_list(&raw).ok())
354 .unwrap_or_default()
355}
356
357pub fn resolve_device_chain(
359 graph: &Graph,
360 chain: &[Device],
361 policy: &DevicePolicy,
362) -> Result<Device, String> {
363 let viable = devices_for_with_policy(graph, policy);
364 for &device in chain {
365 if viable.contains(&device) {
366 return Ok(device);
367 }
368 }
369 Err(format!(
370 "no device in chain [{}] can run this graph — viable: [{}]",
371 chain
372 .iter()
373 .map(|d| device_label(*d))
374 .collect::<Vec<_>>()
375 .join(", "),
376 viable
377 .iter()
378 .map(|d| device_label(*d))
379 .collect::<Vec<_>>()
380 .join(", ")
381 ))
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct DeviceFallbackError {
387 pub attempts: Vec<(Device, String)>,
388}
389
390impl std::fmt::Display for DeviceFallbackError {
391 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392 write!(f, "all backends failed:")?;
393 for (d, e) in &self.attempts {
394 write!(f, "\n {}: {e}", device_label(*d))?;
395 }
396 Ok(())
397 }
398}
399
400impl std::error::Error for DeviceFallbackError {}
401
402impl From<String> for DeviceFallbackError {
403 fn from(msg: String) -> Self {
404 Self {
405 attempts: vec![(Device::Cpu, msg)],
406 }
407 }
408}
409
410pub fn run_with_fallback<T, F>(
412 graph: &Graph,
413 policy: &DevicePolicy,
414 chain: &[Device],
415 mut run: F,
416) -> Result<(Device, T), DeviceFallbackError>
417where
418 F: FnMut(Device) -> Result<T, String>,
419{
420 let viable = devices_for_with_policy(graph, policy);
421 let mut attempts = Vec::new();
422 for &device in chain {
423 if !viable.contains(&device) {
424 attempts.push((device, "not viable for this graph under policy".into()));
425 continue;
426 }
427 match run(device) {
428 Ok(value) => return Ok((device, value)),
429 Err(err) => attempts.push((device, err)),
430 }
431 }
432 if attempts.is_empty() {
433 attempts.push((Device::Cpu, "empty fallback chain".into()));
434 }
435 Err(DeviceFallbackError { attempts })
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use rlx_ir::{DType, Shape};
442
443 fn tiny_graph() -> Graph {
444 let mut g = Graph::new("tiny");
445 let x = g.input("x", Shape::new(&[2], DType::F32));
446 g.set_outputs(vec![x]);
447 g
448 }
449
450 #[test]
451 fn only_policy_restricts_devices_for() {
452 let g = tiny_graph();
453 let all = devices_for_with_policy(&g, &DevicePolicy::default());
454 let cpu_only = devices_for_with_policy(&g, &DevicePolicy::only([Device::Cpu]));
455 assert_eq!(cpu_only, vec![Device::Cpu]);
456 assert!(all.contains(&Device::Cpu));
457 }
458
459 #[test]
460 fn resolve_honors_hint_then_env() {
461 let g = tiny_graph();
462 let policy = DevicePolicy::only([Device::Cpu]);
463 assert_eq!(
464 resolve_device(&g, Some(Device::Cpu), &policy).unwrap(),
465 Device::Cpu
466 );
467
468 rlx_ir::env::set("RLX_DEVICE", "cpu");
469 assert_eq!(resolve_device(&g, None, &policy).unwrap(), Device::Cpu);
470 rlx_ir::env::unset("RLX_DEVICE");
471 }
472
473 #[test]
474 fn device_report_marks_recommended() {
475 let g = tiny_graph();
476 let policy = DevicePolicy::only([Device::Cpu]);
477 let rows = device_report(&g, &policy);
478 assert_eq!(rows.len(), 1);
479 assert!(rows[0].recommended);
480 assert!(rows[0].supports_graph);
481 assert!(
482 rows[0].capabilities.contains(&"clone"),
483 "cpu advisory caps: {:?}",
484 rows[0].capabilities
485 );
486 }
487}