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 persistent_handles: true,
184 typed_io: true,
185 active_extent: true,
186 ..C::NONE
187 },
188 Device::Metal => C {
189 clone: true,
190 gpu_handles: true,
191 kv_resident: true,
192 typed_io: true,
193 async_pipeline: true,
194 active_extent: true,
195 ..C::NONE
196 },
197 Device::Mlx => C {
198 clone: true,
199 persistent_handles: 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::Ane => C {
208 clone: true,
209 typed_io: true,
210 ..C::NONE
211 },
212 Device::Cuda => C {
213 clone: true,
214 moe: true,
215 gpu_handles: true,
216 kv_resident: true,
217 typed_io: true,
218 active_extent: true,
219 ..C::NONE
220 },
221 Device::Rocm => C {
222 clone: true,
223 moe: true,
224 gpu_handles: true,
225 kv_resident: true,
226 typed_io: true,
227 active_extent: true,
228 ..C::NONE
229 },
230 Device::Gpu | Device::WebGpu => C {
231 clone: true,
232 gpu_handles: true,
233 kv_resident: true,
234 typed_io: true,
235 active_extent: true,
236 ..C::NONE
237 },
238 Device::Vulkan => C {
239 clone: true,
240 gpu_handles: true,
241 kv_resident: true,
242 typed_io: true,
243 active_extent: true,
244 ..C::NONE
245 },
246 Device::OneApi => C {
247 clone: true,
248 typed_io: true,
249 ..C::NONE
250 },
251 Device::Tpu => C {
252 clone: true,
253 typed_io: true,
254 ..C::NONE
255 },
256 Device::OpenGl | Device::DirectX => C {
257 typed_io: true,
258 ..C::NONE
259 },
260 Device::Hexagon | Device::Xdna | Device::Egpu => C::NONE,
264 }
265}
266
267pub fn device_report(graph: &Graph, policy: &DevicePolicy) -> Vec<DeviceCandidate> {
269 let recommended = fastest_device_for_with_policy(graph, policy);
270 policy
271 .probe_set()
272 .into_iter()
273 .map(|device| {
274 let available = is_available(device);
275 let registered = backend_for(device).is_some();
276 let supports = available && supports_graph(device, graph);
277 let blocker = if !available {
278 Some("not available on this host or in this build".into())
279 } else if !supports {
280 crate::first_unsupported_op(device, graph)
281 .map(|(idx, op)| format!("unsupported op at node {idx}: {op:?}"))
282 } else if policy.deny.contains(&device) {
283 Some("denied by DevicePolicy".into())
284 } else if policy
285 .allow
286 .as_ref()
287 .is_some_and(|allow| !allow.contains(&device))
288 {
289 Some("not in DevicePolicy allow-list".into())
290 } else {
291 None
292 };
293 DeviceCandidate {
294 device,
295 label: device_label(device),
296 available,
297 registered,
298 supports_graph: supports,
299 recommended: device == recommended,
300 blocker,
301 capabilities: advisory_capabilities(device).enabled_names(),
302 }
303 })
304 .collect()
305}
306
307pub fn resolve_device(
309 graph: &Graph,
310 hint: Option<Device>,
311 policy: &DevicePolicy,
312) -> Result<Device, String> {
313 let candidates = devices_for_with_policy(graph, policy);
314 if candidates.is_empty() {
315 return Err(
316 "no backend can lower this graph under the current policy — \
317 widen DevicePolicy or enable additional Cargo features"
318 .into(),
319 );
320 }
321
322 if let Some(device) = hint {
323 return pick_from_candidates(device, &candidates, "hint");
324 }
325 if let Some(device) = device_from_env() {
326 if let Ok(device) = pick_from_candidates(device, &candidates, "RLX_DEVICE") {
327 return Ok(device);
328 }
329 }
330 Ok(fastest_device_for_with_policy(graph, policy))
331}
332
333fn pick_from_candidates(
334 device: Device,
335 candidates: &[Device],
336 source: &str,
337) -> Result<Device, String> {
338 if candidates.contains(&device) {
339 return Ok(device);
340 }
341 Err(format!(
342 "{source} requested {device} but viable backends are [{}]",
343 candidates
344 .iter()
345 .map(|d| device_label(*d))
346 .collect::<Vec<_>>()
347 .join(", ")
348 ))
349}
350
351pub fn device_chain_from_env() -> Vec<Device> {
353 device_chain_from_env_key("RLX")
354}
355
356pub fn device_chain_from_env_key(prefix: &str) -> Vec<Device> {
358 let key = format!("{prefix}_DEVICE_CHAIN");
359 rlx_ir::env::var(&key)
360 .and_then(|raw| parse_device_list(&raw).ok())
361 .unwrap_or_default()
362}
363
364pub fn resolve_device_chain(
366 graph: &Graph,
367 chain: &[Device],
368 policy: &DevicePolicy,
369) -> Result<Device, String> {
370 let viable = devices_for_with_policy(graph, policy);
371 for &device in chain {
372 if viable.contains(&device) {
373 return Ok(device);
374 }
375 }
376 Err(format!(
377 "no device in chain [{}] can run this graph — viable: [{}]",
378 chain
379 .iter()
380 .map(|d| device_label(*d))
381 .collect::<Vec<_>>()
382 .join(", "),
383 viable
384 .iter()
385 .map(|d| device_label(*d))
386 .collect::<Vec<_>>()
387 .join(", ")
388 ))
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct DeviceFallbackError {
394 pub attempts: Vec<(Device, String)>,
395}
396
397impl std::fmt::Display for DeviceFallbackError {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 write!(f, "all backends failed:")?;
400 for (d, e) in &self.attempts {
401 write!(f, "\n {}: {e}", device_label(*d))?;
402 }
403 Ok(())
404 }
405}
406
407impl std::error::Error for DeviceFallbackError {}
408
409impl From<String> for DeviceFallbackError {
410 fn from(msg: String) -> Self {
411 Self {
412 attempts: vec![(Device::Cpu, msg)],
413 }
414 }
415}
416
417pub fn run_with_fallback<T, F>(
419 graph: &Graph,
420 policy: &DevicePolicy,
421 chain: &[Device],
422 mut run: F,
423) -> Result<(Device, T), DeviceFallbackError>
424where
425 F: FnMut(Device) -> Result<T, String>,
426{
427 let viable = devices_for_with_policy(graph, policy);
428 let mut attempts = Vec::new();
429 for &device in chain {
430 if !viable.contains(&device) {
431 attempts.push((device, "not viable for this graph under policy".into()));
432 continue;
433 }
434 match run(device) {
435 Ok(value) => return Ok((device, value)),
436 Err(err) => attempts.push((device, err)),
437 }
438 }
439 if attempts.is_empty() {
440 attempts.push((Device::Cpu, "empty fallback chain".into()));
441 }
442 Err(DeviceFallbackError { attempts })
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use rlx_ir::{DType, Shape};
449
450 fn tiny_graph() -> Graph {
451 let mut g = Graph::new("tiny");
452 let x = g.input("x", Shape::new(&[2], DType::F32));
453 g.set_outputs(vec![x]);
454 g
455 }
456
457 #[test]
458 fn only_policy_restricts_devices_for() {
459 let g = tiny_graph();
460 let all = devices_for_with_policy(&g, &DevicePolicy::default());
461 let cpu_only = devices_for_with_policy(&g, &DevicePolicy::only([Device::Cpu]));
462 assert_eq!(cpu_only, vec![Device::Cpu]);
463 assert!(all.contains(&Device::Cpu));
464 }
465
466 #[test]
467 fn resolve_honors_hint_then_env() {
468 let g = tiny_graph();
469 let policy = DevicePolicy::only([Device::Cpu]);
470 assert_eq!(
471 resolve_device(&g, Some(Device::Cpu), &policy).unwrap(),
472 Device::Cpu
473 );
474
475 rlx_ir::env::set("RLX_DEVICE", "cpu");
476 assert_eq!(resolve_device(&g, None, &policy).unwrap(), Device::Cpu);
477 rlx_ir::env::unset("RLX_DEVICE");
478 }
479
480 #[test]
481 fn device_report_marks_recommended() {
482 let g = tiny_graph();
483 let policy = DevicePolicy::only([Device::Cpu]);
484 let rows = device_report(&g, &policy);
485 assert_eq!(rows.len(), 1);
486 assert!(rows[0].recommended);
487 assert!(rows[0].supports_graph);
488 assert!(
489 rows[0].capabilities.contains(&"clone"),
490 "cpu advisory caps: {:?}",
491 rows[0].capabilities
492 );
493 }
494}