1use rlx_driver::Device;
24use rlx_ir::{Graph, Op};
25
26use crate::CompileOptions;
27
28pub(crate) const DEVICE_PRIORITY: &[Device] = &[
33 Device::Tpu,
34 Device::Cuda,
35 Device::Rocm,
36 Device::OneApi,
37 Device::Mlx,
38 Device::Metal,
39 Device::Ane,
40 Device::Hexagon,
41 Device::Gpu,
42 Device::Vulkan,
43 Device::DirectX,
44 Device::OpenGl,
45 Device::WebGpu,
46 Device::Cpu,
47];
48
49pub fn supports_run_slots(device: Device) -> bool {
60 matches!(
61 device,
62 Device::Cpu | Device::Metal | Device::Mlx | Device::Cuda | Device::Rocm
63 )
64}
65
66pub fn supports_ragged_rope(device: Device) -> bool {
83 matches!(device, Device::Cpu | Device::Metal)
84}
85
86pub fn trim_accelerator_arena_pool(device: Device) {
89 #[cfg(feature = "cuda")]
90 if device == Device::Cuda {
91 rlx_cuda::trim_device_memory_pool();
92 }
93 #[cfg(not(feature = "cuda"))]
94 let _ = device;
95}
96
97pub fn is_available(device: Device) -> bool {
98 #[cfg(feature = "cuda")]
99 if device == Device::Cuda {
100 return rlx_cuda::is_available();
101 }
102 #[cfg(feature = "rocm")]
103 if device == Device::Rocm {
104 return rlx_rocm::is_available();
105 }
106 #[cfg(feature = "gpu")]
107 if device == Device::Gpu {
108 return rlx_wgpu::is_available();
109 }
110 #[cfg(feature = "vulkan")]
111 if device == Device::Vulkan {
112 return rlx_vulkan::is_available();
113 }
114 #[cfg(feature = "oneapi")]
115 if device == Device::OneApi {
116 return rlx_oneapi::is_available();
117 }
118 #[cfg(feature = "tpu")]
119 if device == Device::Tpu {
120 return rlx_tpu::is_available();
121 }
122 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
127 if device == Device::Metal {
128 return rlx_metal::is_available();
129 }
130 #[cfg(all(feature = "mlx", rlx_mlx_host))]
131 if device == Device::Mlx {
132 return rlx_mlx::is_available();
133 }
134
135 let feature_gated = match device {
136 Device::Cpu => cfg!(feature = "cpu"),
137 Device::Metal => cfg!(all(
138 feature = "metal",
139 target_vendor = "apple",
140 not(target_os = "watchos")
141 )),
142 Device::Mlx => cfg!(feature = "mlx"),
143 Device::Ane => cfg!(any(feature = "coreml", feature = "ane")),
144 Device::Cuda => cfg!(feature = "cuda"),
145 Device::Rocm => cfg!(feature = "rocm"),
146 Device::OneApi => cfg!(feature = "oneapi"),
147 Device::Tpu => cfg!(feature = "tpu"),
148 Device::Hexagon => cfg!(feature = "qnn"),
149 Device::Gpu => cfg!(feature = "gpu"),
150 Device::Vulkan => cfg!(feature = "vulkan"),
151 Device::OpenGl => cfg!(feature = "opengl"),
152 Device::DirectX => cfg!(feature = "directx"),
153 Device::WebGpu => cfg!(feature = "webgpu"),
154 };
155 if feature_gated {
156 return true;
157 }
158 crate::registry::registered_devices().contains(&device)
159}
160
161#[cfg(all(feature = "apple", target_vendor = "apple"))]
165pub fn available_apple_devices() -> Vec<Device> {
166 [Device::Metal, Device::Mlx, Device::Gpu, Device::Ane]
167 .into_iter()
168 .filter(|d| is_available(*d))
169 .collect()
170}
171
172pub fn available_devices() -> Vec<Device> {
175 Device::all()
176 .iter()
177 .copied()
178 .filter(|d| is_available(*d))
179 .collect()
180}
181
182pub fn devices_for(graph: &Graph) -> Vec<Device> {
185 crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
186}
187
188pub fn fastest_device() -> Device {
195 fastest_among(&available_devices())
196}
197
198pub fn fastest_among(candidates: &[Device]) -> Device {
200 for &d in DEVICE_PRIORITY {
201 if candidates.contains(&d) {
202 return d;
203 }
204 }
205 candidates.first().copied().unwrap_or(Device::Cpu)
206}
207
208pub fn full_name(device: Device) -> &'static str {
213 if let Device::Cpu = device {
214 if cfg!(feature = "blas-accelerate") {
215 return "CPU (Accelerate)";
216 }
217 if cfg!(feature = "blas-mkl") {
218 return "CPU (MKL)";
219 }
220 if cfg!(feature = "blas-openblas") {
221 return "CPU (OpenBLAS)";
222 }
223 }
224 device.name()
225}
226
227pub fn supports(device: Device, op: &Op) -> bool {
251 if !is_available(device) {
252 return false;
253 }
254 match device {
255 Device::Cpu => true, Device::Mlx => mlx_supports(op),
257 Device::Metal => metal_supports(op),
258 Device::Ane => coreml_supports(op),
259 Device::Gpu | Device::Cuda | Device::Rocm => gpu_family_supports(op),
260 #[cfg(feature = "vulkan")]
261 Device::Vulkan => vulkan_supports(op),
262 #[cfg(feature = "oneapi")]
263 Device::OneApi => oneapi_supports(op),
264 Device::Hexagon => qnn_supports(op),
265 _ => false,
269 }
270}
271
272fn qnn_supports(op: &Op) -> bool {
276 use rlx_ir::op::Activation;
277 match op {
278 Op::Input { .. }
279 | Op::Param { .. }
280 | Op::Constant { .. }
281 | Op::MatMul
282 | Op::Binary(_)
283 | Op::Softmax { .. }
284 | Op::Reshape { .. }
285 | Op::Transpose { .. }
286 | Op::LayerNorm { .. }
287 | Op::RmsNorm { .. }
288 | Op::Concat { .. }
289 | Op::Narrow { .. }
290 | Op::Rope { .. }
291 | Op::Attention { .. }
292 | Op::Reduce { .. }
293 | Op::Conv { .. }
294 | Op::Gather { .. }
295 | Op::Quantize { .. }
296 | Op::Dequantize { .. } => true,
297 Op::Activation(a) => matches!(
298 a,
299 Activation::Relu
300 | Activation::Gelu
301 | Activation::Sigmoid
302 | Activation::Tanh
303 | Activation::Neg
304 ),
305 _ => false,
306 }
307}
308
309#[cfg(feature = "vulkan")]
314fn vulkan_supports(op: &Op) -> bool {
315 use rlx_ir::OpKind::*;
316 let k = op.kind();
317 rlx_vulkan::backend::SUPPORTED_OPS.contains(&k)
318 || matches!(
319 k,
320 DotGeneral
322 | Fma
323 | GroupNorm
324 | BatchNormInference
325 | ResizeNearest2x
326 | ElementwiseRegion
327 | FusedMatMulBiasAct
328 | FusedResidualLN
329 | FusedResidualRmsNorm
330 | FusedSwiGLU
331 | FusedAttentionBlock
332 | FusedTransformerLayer
333 )
334}
335
336#[cfg(feature = "oneapi")]
340fn oneapi_supports(op: &Op) -> bool {
341 use rlx_ir::OpKind::*;
342 let k = op.kind();
343 rlx_oneapi::backend::SUPPORTED_OPS.contains(&k)
344 || matches!(
345 k,
346 DotGeneral
347 | Fma
348 | GroupNorm
349 | BatchNormInference
350 | ResizeNearest2x
351 | ElementwiseRegion
352 | FusedMatMulBiasAct
353 | FusedResidualLN
354 | FusedResidualRmsNorm
355 | FusedSwiGLU
356 | FusedAttentionBlock
357 | FusedTransformerLayer
358 )
359}
360
361pub fn supports_graph(device: Device, graph: &Graph) -> bool {
367 supports_graph_with_options(device, graph, &CompileOptions::default())
368}
369
370pub fn supports_graph_with_options(
372 device: Device,
373 graph: &Graph,
374 options: &CompileOptions,
375) -> bool {
376 if !is_available(device) {
377 return false;
378 }
379 if let Some(backend) = crate::registry::backend_for(device) {
380 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
381 graph.clone(),
382 device.name(),
383 backend.supported_ops(),
384 options.kernel_dispatch,
385 );
386 return report.compile_ready;
387 }
388 graph.nodes().iter().all(|n| supports(device, &n.op))
389}
390
391pub fn legalize_graph_for_device(graph: Graph, device: Device) -> Result<Graph, String> {
400 let (graph, _report) = legalize_graph_for_device_with_report(graph, device)?;
401 Ok(graph)
402}
403
404pub fn legalize_graph_for_device_with_report(
406 graph: Graph,
407 device: Device,
408) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
409 legalize_graph_for_device_with_options(graph, device, &CompileOptions::default())
410}
411
412pub fn legalize_graph_for_device_with_options(
415 graph: Graph,
416 device: Device,
417 options: &CompileOptions,
418) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
419 let backend = crate::registry::backend_for(device).ok_or_else(|| {
420 format!(
421 "no backend registered for {device:?} — enable the matching \
422 `rlx-runtime` Cargo feature (e.g. `metal`, `gpu`, `cuda`)"
423 )
424 })?;
425 let ops = backend.supported_ops();
426 let (graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
427 graph,
428 device.name(),
429 ops,
430 options.kernel_dispatch,
431 );
432 if !report.compile_ready {
433 return Err(format!(
434 "{}\n{}",
435 rlx_opt::format_legalize_error(device.name(), &report.still_unsupported),
436 rlx_opt::format_dispatch_report(&report)
437 ));
438 }
439 Ok((graph, report))
440}
441
442pub fn dispatch_report_for_device(
444 graph: &Graph,
445 device: Device,
446) -> Result<rlx_opt::KernelDispatchReport, String> {
447 dispatch_report_for_device_with_options(graph, device, &CompileOptions::default())
448}
449
450pub fn dispatch_report_for_device_with_options(
452 graph: &Graph,
453 device: Device,
454 options: &CompileOptions,
455) -> Result<rlx_opt::KernelDispatchReport, String> {
456 let backend = crate::registry::backend_for(device)
457 .ok_or_else(|| format!("no backend registered for {device:?}"))?;
458 Ok(rlx_opt::analyze_dispatch(
459 graph,
460 device.name(),
461 backend.supported_ops(),
462 options.kernel_dispatch,
463 ))
464}
465
466pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
470 first_unsupported_op_with_options(device, graph, &CompileOptions::default())
471}
472
473pub fn first_unsupported_op_with_options<'a>(
475 device: Device,
476 graph: &'a Graph,
477 options: &CompileOptions,
478) -> Option<(usize, &'a Op)> {
479 if !is_available(device) {
480 return graph.nodes().first().map(|n| (0, &n.op));
481 }
482 if let Some(backend) = crate::registry::backend_for(device) {
483 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
484 graph.clone(),
485 device.name(),
486 backend.supported_ops(),
487 options.kernel_dispatch,
488 );
489 if let Some((id, kind)) = report.still_unsupported.first() {
490 let idx = graph.nodes().iter().position(|n| n.id == *id).unwrap_or(0);
491 let op = graph
492 .nodes()
493 .iter()
494 .find(|n| n.id == *id)
495 .map(|n| &n.op)
496 .unwrap_or(&graph.nodes()[0].op);
497 let _ = kind;
498 return Some((idx, op));
499 }
500 return None;
501 }
502 graph
503 .nodes()
504 .iter()
505 .enumerate()
506 .find_map(|(i, n)| (!supports(device, &n.op)).then_some((i, &n.op)))
507}
508
509#[allow(unused_variables)]
510fn mlx_supports(op: &Op) -> bool {
511 true
516}
517
518#[allow(unused_variables)]
519fn metal_supports(op: &Op) -> bool {
520 let _ = op;
526 true
527}
528
529fn coreml_supports(op: &Op) -> bool {
540 let kind = op.kind();
541 if crate::backend::COREML_SUPPORTED_OPS.contains(&kind) {
542 return true;
543 }
544 #[cfg(feature = "training")]
545 if crate::backend::COREML_BACKWARD_OPS.contains(&kind)
546 || crate::backend::COREML_NATIVE_BACKWARD_OPS.contains(&kind)
547 {
548 return true;
549 }
550 false
551}
552
553#[allow(unused_variables)]
554fn gpu_family_supports(op: &Op) -> bool {
555 let _ = op;
559 true
560}
561
562pub fn drain_device(device: Device) {
565 #[cfg(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal"))]
566 {
567 if device == Device::Metal {
568 rlx_metal::device::drain_command_queue();
569 }
570 }
571 #[cfg(not(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal")))]
572 let _ = device;
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use rlx_ir::op::{Activation, BinaryOp};
579 use rlx_ir::{DType, Graph, Shape};
580
581 fn scalar_shape() -> Shape {
582 Shape::new(&[1], DType::F32)
583 }
584
585 #[test]
586 fn cpu_supports_everything_built_in() {
587 assert!(supports(Device::Cpu, &Op::Activation(Activation::Sin)));
588 assert!(supports(Device::Cpu, &Op::Activation(Activation::Cos)));
589 assert!(supports(Device::Cpu, &Op::Activation(Activation::Exp)));
590 assert!(supports(Device::Cpu, &Op::Binary(BinaryOp::Add)));
591 }
592
593 #[test]
594 fn unbuilt_device_supports_nothing() {
595 assert!(!supports(Device::OpenGl, &Op::Activation(Activation::Relu)));
597 }
598
599 #[test]
600 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
601 fn metal_supports_full_activation_set() {
602 for act in [
606 Activation::Sin,
607 Activation::Cos,
608 Activation::Tan,
609 Activation::Atan,
610 Activation::Exp,
611 ] {
612 assert!(
613 supports(Device::Metal, &Op::Activation(act)),
614 "Metal should support Activation::{act:?}"
615 );
616 }
617 }
618
619 #[test]
620 fn graph_walk_reports_first_blocker() {
621 let mut g = Graph::new("walk");
622 let s = scalar_shape();
623 let x = g.input("x", s.clone());
624 let _e = g.activation(Activation::Exp, x, s.clone());
625 let _sin = g.activation(Activation::Sin, x, s);
626 assert!(supports_graph(Device::Cpu, &g));
628 assert!(first_unsupported_op(Device::Cpu, &g).is_none());
629 }
630
631 #[test]
632 fn fastest_device_returns_cpu_when_only_cpu_is_available() {
633 let pick = fastest_device();
634 assert!(is_available(pick));
635 assert_eq!(pick, fastest_among(&available_devices()));
636 }
637
638 #[test]
639 fn fastest_among_respects_priority_order() {
640 let pick = fastest_among(&[Device::Cpu, Device::Metal, Device::Mlx]);
641 assert_eq!(pick, Device::Mlx);
642 }
643
644 #[test]
645 fn devices_for_is_subset_of_available() {
646 let mut g = Graph::new("id");
647 let x = g.input("x", scalar_shape());
648 g.set_outputs(vec![x]);
649 for d in devices_for(&g) {
650 assert!(is_available(d));
651 assert!(supports_graph(d, &g));
652 }
653 }
654}