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