1use rlx_driver::Device;
13use rlx_ir::{Graph, Op};
14
15use crate::CompileOptions;
16
17pub(crate) const DEVICE_PRIORITY: &[Device] = &[
22 Device::Tpu,
23 Device::Cuda,
24 Device::Rocm,
25 Device::OneApi,
26 Device::Mlx,
27 Device::Metal,
28 Device::Ane,
29 Device::Hexagon,
30 Device::Xdna,
31 Device::Gpu,
32 Device::Vulkan,
33 Device::DirectX,
34 Device::OpenGl,
35 Device::WebGpu,
36 Device::Cpu,
37];
38
39pub const BROWSER_DEVICE_PRIORITY: &[Device] = &[Device::WebGpu, Device::OpenGl, Device::Cpu];
41
42pub fn supports_run_slots(device: Device) -> bool {
53 matches!(
54 device,
55 Device::Cpu | Device::Metal | Device::Mlx | Device::Cuda | Device::Rocm
56 )
57}
58
59pub fn supports_ragged_rope(device: Device) -> bool {
76 matches!(device, Device::Cpu | Device::Metal)
77}
78
79pub fn trim_accelerator_arena_pool(device: Device) {
82 #[cfg(feature = "cuda")]
83 if device == Device::Cuda {
84 rlx_cuda::trim_device_memory_pool();
85 }
86 #[cfg(not(feature = "cuda"))]
87 let _ = device;
88}
89
90pub fn feature_compiled(device: Device) -> bool {
97 match device {
98 Device::Cpu => true,
99 Device::Metal => cfg!(feature = "metal"),
100 Device::Mlx => cfg!(feature = "mlx"),
101 Device::Ane => cfg!(feature = "ane"),
102 Device::Cuda => cfg!(feature = "cuda"),
103 Device::Rocm => cfg!(feature = "rocm"),
104 Device::Xdna => cfg!(feature = "xdna"),
105 Device::OneApi => cfg!(feature = "oneapi"),
106 Device::Tpu => cfg!(feature = "tpu"),
107 Device::Hexagon => cfg!(feature = "qnn"),
108 Device::Gpu => cfg!(feature = "gpu"),
109 Device::Vulkan => cfg!(feature = "vulkan"),
110 Device::OpenGl => cfg!(feature = "opengl"),
111 Device::DirectX => cfg!(feature = "directx"),
112 Device::WebGpu => cfg!(feature = "webgpu"),
113 }
114}
115
116pub fn is_available(device: Device) -> bool {
117 #[cfg(feature = "cuda")]
118 if device == Device::Cuda {
119 return rlx_cuda::is_available();
120 }
121 #[cfg(feature = "rocm")]
122 if device == Device::Rocm {
123 return rlx_rocm::is_available();
124 }
125 #[cfg(feature = "xdna")]
130 if device == Device::Xdna {
131 return rlx_xdna::is_available();
132 }
133 #[cfg(feature = "gpu")]
134 if device == Device::Gpu {
135 #[cfg(target_arch = "wasm32")]
136 {
137 return rlx_wgpu::device::wgpu_device().is_some();
138 }
139 #[cfg(not(target_arch = "wasm32"))]
140 {
141 return rlx_wgpu::is_available();
142 }
143 }
144 #[cfg(feature = "webgpu")]
145 if device == Device::WebGpu {
146 #[cfg(target_arch = "wasm32")]
147 {
148 return rlx_wgpu::device::wgpu_device().is_some();
149 }
150 #[cfg(not(target_arch = "wasm32"))]
151 {
152 return rlx_wgpu::is_available();
153 }
154 }
155 #[cfg(feature = "opengl")]
156 if device == Device::OpenGl {
157 return true;
158 }
159 #[cfg(feature = "vulkan")]
160 if device == Device::Vulkan {
161 return rlx_vulkan::is_available();
162 }
163 #[cfg(feature = "oneapi")]
164 if device == Device::OneApi {
165 return rlx_oneapi::is_available();
166 }
167 #[cfg(feature = "tpu")]
168 if device == Device::Tpu {
169 return rlx_tpu::is_available();
170 }
171 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
176 if device == Device::Metal {
177 return rlx_metal::is_available();
178 }
179 #[cfg(all(feature = "mlx", rlx_mlx_host))]
180 if device == Device::Mlx {
181 return rlx_mlx::is_available();
182 }
183
184 let feature_gated = match device {
185 Device::Cpu => cfg!(feature = "cpu"),
186 Device::Metal => cfg!(all(
187 feature = "metal",
188 target_vendor = "apple",
189 not(target_os = "watchos")
190 )),
191 Device::Mlx => cfg!(feature = "mlx"),
192 Device::Ane => cfg!(all(
197 any(feature = "coreml", feature = "ane"),
198 target_vendor = "apple"
199 )),
200 Device::Cuda => cfg!(feature = "cuda"),
201 Device::Rocm => cfg!(feature = "rocm"),
202 Device::Xdna => false,
205 Device::OneApi => cfg!(feature = "oneapi"),
206 Device::Tpu => cfg!(feature = "tpu"),
207 Device::Hexagon => cfg!(feature = "qnn"),
208 Device::Gpu => cfg!(feature = "gpu"),
209 Device::Vulkan => cfg!(feature = "vulkan"),
210 Device::OpenGl => cfg!(feature = "opengl"),
211 Device::DirectX => cfg!(feature = "directx"),
212 Device::WebGpu => cfg!(feature = "webgpu"),
213 };
214 if feature_gated {
215 return true;
216 }
217 crate::registry::registered_devices().contains(&device)
218}
219
220#[cfg(all(feature = "apple", target_vendor = "apple"))]
224pub fn available_apple_devices() -> Vec<Device> {
225 [Device::Metal, Device::Mlx, Device::Gpu, Device::Ane]
226 .into_iter()
227 .filter(|d| is_available(*d))
228 .collect()
229}
230
231pub fn available_devices() -> Vec<Device> {
234 Device::all()
235 .iter()
236 .copied()
237 .filter(|d| is_available(*d))
238 .collect()
239}
240
241pub fn detected_unavailable_devices() -> Vec<(Device, String)> {
251 #[allow(unused_mut)]
252 let mut out: Vec<(Device, String)> = Vec::new();
253 #[cfg(feature = "xdna")]
254 if rlx_xdna::hardware_present() && !is_available(Device::Xdna) {
255 out.push((Device::Xdna, rlx_xdna::diagnostic()));
256 }
257 out
258}
259
260pub fn available_browser_devices() -> Vec<Device> {
262 BROWSER_DEVICE_PRIORITY
263 .iter()
264 .copied()
265 .filter(|d| is_available(*d))
266 .collect()
267}
268
269pub fn preferred_browser_device() -> Option<Device> {
271 available_browser_devices().into_iter().next()
272}
273
274pub fn devices_for(graph: &Graph) -> Vec<Device> {
277 crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
278}
279
280pub fn fastest_device() -> Device {
287 fastest_among(&available_devices())
288}
289
290pub fn fastest_among(candidates: &[Device]) -> Device {
292 for &d in DEVICE_PRIORITY {
293 if candidates.contains(&d) {
294 return d;
295 }
296 }
297 candidates.first().copied().unwrap_or(Device::Cpu)
298}
299
300pub fn full_name(device: Device) -> &'static str {
305 if let Device::Cpu = device {
306 if cfg!(feature = "blas-accelerate") {
307 return "CPU (Accelerate)";
308 }
309 if cfg!(feature = "blas-mkl") {
310 return "CPU (MKL)";
311 }
312 if cfg!(feature = "blas-openblas") {
313 return "CPU (OpenBLAS)";
314 }
315 }
316 device.name()
317}
318
319pub fn supports(device: Device, op: &Op) -> bool {
343 if !is_available(device) {
344 return false;
345 }
346 match device {
347 Device::Cpu => true, Device::Mlx => mlx_supports(op),
349 Device::Metal => metal_supports(op),
350 Device::Ane => coreml_supports(op),
351 Device::Gpu | Device::Cuda | Device::Rocm => gpu_family_supports(op),
352 #[cfg(feature = "vulkan")]
353 Device::Vulkan => vulkan_supports(op),
354 #[cfg(feature = "oneapi")]
355 Device::OneApi => oneapi_supports(op),
356 Device::Hexagon => qnn_supports(op),
357 _ => false,
361 }
362}
363
364fn qnn_supports(op: &Op) -> bool {
368 use rlx_ir::op::Activation;
369 match op {
370 Op::Input { .. }
371 | Op::Param { .. }
372 | Op::Constant { .. }
373 | Op::MatMul
374 | Op::Binary(_)
375 | Op::Softmax { .. }
376 | Op::Reshape { .. }
377 | Op::Transpose { .. }
378 | Op::LayerNorm { .. }
379 | Op::RmsNorm { .. }
380 | Op::Concat { .. }
381 | Op::Narrow { .. }
382 | Op::Rope { .. }
383 | Op::Attention { .. }
384 | Op::FusedAttentionBlock { .. }
385 | Op::Expand { .. }
386 | Op::Reduce { .. }
387 | Op::Conv { .. }
388 | Op::Gather { .. }
389 | Op::Quantize { .. }
390 | Op::Dequantize { .. }
391 | Op::DequantMatMul { .. } => true,
392 Op::Activation(a) => matches!(
393 a,
394 Activation::Relu
395 | Activation::Gelu
396 | Activation::Sigmoid
397 | Activation::Tanh
398 | Activation::Neg
399 | Activation::Silu
400 ),
401 _ => false,
402 }
403}
404
405#[cfg(feature = "vulkan")]
410fn vulkan_supports(op: &Op) -> bool {
411 use rlx_ir::OpKind::*;
412 let k = op.kind();
413 rlx_vulkan::backend::SUPPORTED_OPS.contains(&k)
414 || matches!(
415 k,
416 DotGeneral
418 | Fma
419 | GroupNorm
420 | BatchNormInference
421 | ResizeNearest2x
422 | ElementwiseRegion
423 | FusedMatMulBiasAct
424 | FusedResidualLN
425 | FusedResidualRmsNorm
426 | FusedSwiGLU
427 | FusedAttentionBlock
428 | FusedTransformerLayer
429 )
430}
431
432#[cfg(feature = "oneapi")]
436fn oneapi_supports(op: &Op) -> bool {
437 use rlx_ir::OpKind::*;
438 let k = op.kind();
439 rlx_oneapi::backend::SUPPORTED_OPS.contains(&k)
440 || matches!(
441 k,
442 DotGeneral
443 | Fma
444 | GroupNorm
445 | BatchNormInference
446 | ResizeNearest2x
447 | ElementwiseRegion
448 | FusedMatMulBiasAct
449 | FusedResidualLN
450 | FusedResidualRmsNorm
451 | FusedSwiGLU
452 | FusedAttentionBlock
453 | FusedTransformerLayer
454 )
455}
456
457pub fn supports_graph(device: Device, graph: &Graph) -> bool {
463 supports_graph_with_options(device, graph, &CompileOptions::default())
464}
465
466pub fn supports_graph_with_options(
468 device: Device,
469 graph: &Graph,
470 options: &CompileOptions,
471) -> bool {
472 if !is_available(device) {
473 return false;
474 }
475 if let Some(backend) = crate::registry::backend_for(device) {
476 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
477 graph.clone(),
478 device.name(),
479 backend.supported_ops(),
480 options.kernel_dispatch,
481 );
482 return report.compile_ready;
483 }
484 graph.nodes().iter().all(|n| supports(device, &n.op))
485}
486
487pub fn legalize_graph_for_device(graph: Graph, device: Device) -> Result<Graph, String> {
496 let (graph, _report) = legalize_graph_for_device_with_report(graph, device)?;
497 Ok(graph)
498}
499
500pub fn legalize_graph_for_device_with_report(
502 graph: Graph,
503 device: Device,
504) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
505 legalize_graph_for_device_with_options(graph, device, &CompileOptions::default())
506}
507
508pub fn legalize_graph_for_device_with_options(
511 graph: Graph,
512 device: Device,
513 options: &CompileOptions,
514) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
515 let backend = crate::registry::backend_for(device).ok_or_else(|| {
516 format!(
517 "no backend registered for {device:?} — enable the matching \
518 `rlx-runtime` Cargo feature (e.g. `metal`, `gpu`, `cuda`)"
519 )
520 })?;
521 let ops = backend.supported_ops();
522 let (graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
523 graph,
524 device.name(),
525 ops,
526 options.kernel_dispatch,
527 );
528 if !report.compile_ready {
529 return Err(format!(
530 "{}\n{}",
531 rlx_opt::format_legalize_error(device.name(), &report.still_unsupported),
532 rlx_opt::format_dispatch_report(&report)
533 ));
534 }
535 Ok((graph, report))
536}
537
538pub fn dispatch_report_for_device(
540 graph: &Graph,
541 device: Device,
542) -> Result<rlx_opt::KernelDispatchReport, String> {
543 dispatch_report_for_device_with_options(graph, device, &CompileOptions::default())
544}
545
546pub fn dispatch_report_for_device_with_options(
548 graph: &Graph,
549 device: Device,
550 options: &CompileOptions,
551) -> Result<rlx_opt::KernelDispatchReport, String> {
552 let backend = crate::registry::backend_for(device)
553 .ok_or_else(|| format!("no backend registered for {device:?}"))?;
554 Ok(rlx_opt::analyze_dispatch(
555 graph,
556 device.name(),
557 backend.supported_ops(),
558 options.kernel_dispatch,
559 ))
560}
561
562pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
566 first_unsupported_op_with_options(device, graph, &CompileOptions::default())
567}
568
569pub fn first_unsupported_op_with_options<'a>(
571 device: Device,
572 graph: &'a Graph,
573 options: &CompileOptions,
574) -> Option<(usize, &'a Op)> {
575 if !is_available(device) {
576 return graph.nodes().first().map(|n| (0, &n.op));
577 }
578 if let Some(backend) = crate::registry::backend_for(device) {
579 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
580 graph.clone(),
581 device.name(),
582 backend.supported_ops(),
583 options.kernel_dispatch,
584 );
585 if let Some((id, kind)) = report.still_unsupported.first() {
586 let idx = graph.nodes().iter().position(|n| n.id == *id).unwrap_or(0);
587 let op = graph
588 .nodes()
589 .iter()
590 .find(|n| n.id == *id)
591 .map(|n| &n.op)
592 .unwrap_or(&graph.nodes()[0].op);
593 let _ = kind;
594 return Some((idx, op));
595 }
596 return None;
597 }
598 graph
599 .nodes()
600 .iter()
601 .enumerate()
602 .find_map(|(i, n)| (!supports(device, &n.op)).then_some((i, &n.op)))
603}
604
605#[allow(unused_variables)]
606fn mlx_supports(op: &Op) -> bool {
607 true
612}
613
614#[allow(unused_variables)]
615fn metal_supports(op: &Op) -> bool {
616 let _ = op;
622 true
623}
624
625fn coreml_supports(op: &Op) -> bool {
636 #[cfg(all(
640 feature = "coreml",
641 target_vendor = "apple",
642 not(target_os = "watchos")
643 ))]
644 {
645 let kind = op.kind();
646 if rlx_coreml::SUPPORTED_OPS.contains(&kind) {
647 return true;
648 }
649 #[cfg(feature = "training")]
650 if rlx_coreml::BACKWARD_OPS.contains(&kind)
651 || rlx_coreml::NATIVE_BACKWARD_OPS.contains(&kind)
652 {
653 return true;
654 }
655 false
656 }
657 #[cfg(not(all(
658 feature = "coreml",
659 target_vendor = "apple",
660 not(target_os = "watchos")
661 )))]
662 {
663 let _ = op;
664 false
665 }
666}
667
668#[allow(unused_variables)]
669fn gpu_family_supports(op: &Op) -> bool {
670 let _ = op;
674 true
675}
676
677pub fn drain_device(device: Device) {
680 #[cfg(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal"))]
681 {
682 if device == Device::Metal {
683 rlx_metal::device::drain_command_queue();
684 }
685 }
686 #[cfg(not(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal")))]
687 let _ = device;
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use rlx_ir::op::{Activation, BinaryOp};
694 use rlx_ir::{DType, Graph, Shape};
695
696 fn scalar_shape() -> Shape {
697 Shape::new(&[1], DType::F32)
698 }
699
700 #[test]
701 fn cpu_supports_everything_built_in() {
702 assert!(supports(Device::Cpu, &Op::Activation(Activation::Sin)));
703 assert!(supports(Device::Cpu, &Op::Activation(Activation::Cos)));
704 assert!(supports(Device::Cpu, &Op::Activation(Activation::Exp)));
705 assert!(supports(Device::Cpu, &Op::Binary(BinaryOp::Add)));
706 }
707
708 #[test]
709 fn unbuilt_device_supports_nothing() {
710 assert!(!supports(Device::OpenGl, &Op::Activation(Activation::Relu)));
712 }
713
714 #[test]
715 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
716 fn metal_supports_full_activation_set() {
717 for act in [
721 Activation::Sin,
722 Activation::Cos,
723 Activation::Tan,
724 Activation::Atan,
725 Activation::Recip,
726 Activation::Exp,
727 ] {
728 assert!(
729 supports(Device::Metal, &Op::Activation(act)),
730 "Metal should support Activation::{act:?}"
731 );
732 }
733 }
734
735 #[test]
736 fn graph_walk_reports_first_blocker() {
737 let mut g = Graph::new("walk");
738 let s = scalar_shape();
739 let x = g.input("x", s.clone());
740 let _e = g.activation(Activation::Exp, x, s.clone());
741 let _sin = g.activation(Activation::Sin, x, s);
742 assert!(supports_graph(Device::Cpu, &g));
744 assert!(first_unsupported_op(Device::Cpu, &g).is_none());
745 }
746
747 #[test]
748 fn fastest_device_returns_cpu_when_only_cpu_is_available() {
749 let pick = fastest_device();
750 assert!(is_available(pick));
751 assert_eq!(pick, fastest_among(&available_devices()));
752 }
753
754 #[test]
755 fn fastest_among_respects_priority_order() {
756 let pick = fastest_among(&[Device::Cpu, Device::Metal, Device::Mlx]);
757 assert_eq!(pick, Device::Mlx);
758 }
759
760 #[test]
761 fn devices_for_is_subset_of_available() {
762 let mut g = Graph::new("id");
763 let x = g.input("x", scalar_shape());
764 g.set_outputs(vec![x]);
765 for d in devices_for(&g) {
766 assert!(is_available(d));
767 assert!(supports_graph(d, &g));
768 }
769 }
770}