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::Egpu,
32 Device::Gpu,
33 Device::Vulkan,
34 Device::DirectX,
35 Device::OpenGl,
36 Device::WebGpu,
37 Device::Cpu,
38];
39
40pub const BROWSER_DEVICE_PRIORITY: &[Device] = &[Device::WebGpu, Device::OpenGl, Device::Cpu];
42
43pub fn supports_run_slots(device: Device) -> bool {
54 matches!(
55 device,
56 Device::Cpu | Device::Metal | Device::Mlx | Device::Cuda | Device::Rocm
57 )
58}
59
60pub fn supports_ragged_rope(device: Device) -> bool {
77 matches!(device, Device::Cpu | Device::Metal)
78}
79
80pub fn trim_accelerator_arena_pool(device: Device) {
83 #[cfg(feature = "cuda")]
84 if device == Device::Cuda {
85 rlx_cuda::trim_device_memory_pool();
86 }
87 #[cfg(not(feature = "cuda"))]
88 let _ = device;
89}
90
91pub fn feature_compiled(device: Device) -> bool {
98 match device {
99 Device::Cpu => true,
100 Device::Metal => cfg!(feature = "metal"),
101 Device::Mlx => cfg!(feature = "mlx"),
102 Device::Ane => cfg!(feature = "ane"),
103 Device::Cuda => cfg!(feature = "cuda"),
104 Device::Rocm => cfg!(feature = "rocm"),
105 Device::Xdna => cfg!(feature = "xdna"),
106 Device::Egpu => cfg!(feature = "egpu"),
107 Device::OneApi => cfg!(feature = "oneapi"),
108 Device::Tpu => cfg!(feature = "tpu"),
109 Device::Hexagon => cfg!(feature = "qnn"),
110 Device::Gpu => cfg!(feature = "gpu"),
111 Device::Vulkan => cfg!(feature = "vulkan"),
112 Device::OpenGl => cfg!(feature = "opengl"),
113 Device::DirectX => cfg!(feature = "directx"),
114 Device::WebGpu => cfg!(feature = "webgpu"),
115 }
116}
117
118pub fn is_available(device: Device) -> bool {
119 #[cfg(feature = "cuda")]
120 if device == Device::Cuda {
121 return rlx_cuda::is_available();
122 }
123 #[cfg(feature = "rocm")]
124 if device == Device::Rocm {
125 return rlx_rocm::is_available();
126 }
127 #[cfg(feature = "xdna")]
132 if device == Device::Xdna {
133 return rlx_xdna::is_available();
134 }
135 #[cfg(feature = "egpu")]
140 if device == Device::Egpu {
141 return rlx_egpu::is_available();
142 }
143 #[cfg(feature = "gpu")]
144 if device == Device::Gpu {
145 #[cfg(target_arch = "wasm32")]
146 {
147 return rlx_wgpu::device::wgpu_device().is_some();
148 }
149 #[cfg(not(target_arch = "wasm32"))]
150 {
151 return rlx_wgpu::is_available();
152 }
153 }
154 #[cfg(feature = "webgpu")]
155 if device == Device::WebGpu {
156 #[cfg(target_arch = "wasm32")]
157 {
158 return rlx_wgpu::device::wgpu_device().is_some();
159 }
160 #[cfg(not(target_arch = "wasm32"))]
161 {
162 return rlx_wgpu::is_available();
163 }
164 }
165 #[cfg(feature = "opengl")]
166 if device == Device::OpenGl {
167 return true;
168 }
169 #[cfg(feature = "vulkan")]
170 if device == Device::Vulkan {
171 return rlx_vulkan::is_available();
172 }
173 #[cfg(feature = "oneapi")]
174 if device == Device::OneApi {
175 return rlx_oneapi::is_available();
176 }
177 #[cfg(feature = "tpu")]
178 if device == Device::Tpu {
179 return rlx_tpu::is_available();
180 }
181 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
186 if device == Device::Metal {
187 return rlx_metal::is_available();
188 }
189 #[cfg(all(feature = "mlx", rlx_mlx_host))]
190 if device == Device::Mlx {
191 return rlx_mlx::is_available();
192 }
193
194 let feature_gated = match device {
195 Device::Cpu => cfg!(feature = "cpu"),
196 Device::Metal => cfg!(all(
197 feature = "metal",
198 target_vendor = "apple",
199 not(target_os = "watchos")
200 )),
201 Device::Mlx => cfg!(feature = "mlx"),
202 Device::Ane => cfg!(all(
207 any(feature = "coreml", feature = "ane"),
208 target_vendor = "apple"
209 )),
210 Device::Cuda => cfg!(feature = "cuda"),
211 Device::Rocm => cfg!(feature = "rocm"),
212 Device::Xdna => false,
215 Device::Egpu => false,
217 Device::OneApi => cfg!(feature = "oneapi"),
218 Device::Tpu => cfg!(feature = "tpu"),
219 Device::Hexagon => cfg!(feature = "qnn"),
220 Device::Gpu => cfg!(feature = "gpu"),
221 Device::Vulkan => cfg!(feature = "vulkan"),
222 Device::OpenGl => cfg!(feature = "opengl"),
223 Device::DirectX => cfg!(feature = "directx"),
224 Device::WebGpu => cfg!(feature = "webgpu"),
225 };
226 if feature_gated {
227 return true;
228 }
229 crate::registry::registered_devices().contains(&device)
230}
231
232#[cfg(all(feature = "apple", target_vendor = "apple"))]
236pub fn available_apple_devices() -> Vec<Device> {
237 [Device::Metal, Device::Mlx, Device::Gpu, Device::Ane]
238 .into_iter()
239 .filter(|d| is_available(*d))
240 .collect()
241}
242
243pub fn available_devices() -> Vec<Device> {
246 Device::all()
247 .iter()
248 .copied()
249 .filter(|d| is_available(*d))
250 .collect()
251}
252
253pub fn detected_unavailable_devices() -> Vec<(Device, String)> {
263 #[allow(unused_mut)]
264 let mut out: Vec<(Device, String)> = Vec::new();
265 #[cfg(feature = "xdna")]
266 if rlx_xdna::hardware_present() && !is_available(Device::Xdna) {
267 out.push((Device::Xdna, rlx_xdna::diagnostic()));
268 }
269 #[cfg(feature = "egpu")]
270 if rlx_egpu::hardware_present() && !is_available(Device::Egpu) {
271 out.push((Device::Egpu, rlx_egpu::diagnostic()));
272 }
273 out
274}
275
276pub fn available_browser_devices() -> Vec<Device> {
278 BROWSER_DEVICE_PRIORITY
279 .iter()
280 .copied()
281 .filter(|d| is_available(*d))
282 .collect()
283}
284
285pub fn preferred_browser_device() -> Option<Device> {
287 available_browser_devices().into_iter().next()
288}
289
290pub fn devices_for(graph: &Graph) -> Vec<Device> {
293 crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
294}
295
296pub fn fastest_device() -> Device {
303 fastest_among(&available_devices())
304}
305
306pub fn fastest_among(candidates: &[Device]) -> Device {
308 for &d in DEVICE_PRIORITY {
309 if candidates.contains(&d) {
310 return d;
311 }
312 }
313 candidates.first().copied().unwrap_or(Device::Cpu)
314}
315
316pub fn full_name(device: Device) -> &'static str {
321 if let Device::Cpu = device {
322 if cfg!(feature = "blas-accelerate") {
323 return "CPU (Accelerate)";
324 }
325 if cfg!(feature = "blas-mkl") {
326 return "CPU (MKL)";
327 }
328 if cfg!(feature = "blas-openblas") {
329 return "CPU (OpenBLAS)";
330 }
331 }
332 device.name()
333}
334
335pub fn supports(device: Device, op: &Op) -> bool {
359 if !is_available(device) {
360 return false;
361 }
362 match device {
363 Device::Cpu => true, Device::Mlx => mlx_supports(op),
365 Device::Metal => metal_supports(op),
366 Device::Ane => coreml_supports(op),
367 Device::Gpu | Device::Cuda | Device::Rocm => gpu_family_supports(device, op),
368 #[cfg(feature = "vulkan")]
369 Device::Vulkan => vulkan_supports(op),
370 #[cfg(feature = "oneapi")]
371 Device::OneApi => oneapi_supports(op),
372 Device::Hexagon => qnn_supports(op),
373 _ => false,
377 }
378}
379
380fn qnn_supports(op: &Op) -> bool {
384 use rlx_ir::op::Activation;
385 match op {
386 Op::Input { .. }
387 | Op::Param { .. }
388 | Op::Constant { .. }
389 | Op::MatMul
390 | Op::Binary(_)
391 | Op::Softmax { .. }
392 | Op::Reshape { .. }
393 | Op::Transpose { .. }
394 | Op::LayerNorm { .. }
395 | Op::RmsNorm { .. }
396 | Op::Concat { .. }
397 | Op::Narrow { .. }
398 | Op::Rope { .. }
399 | Op::Attention { .. }
400 | Op::FusedAttentionBlock { .. }
401 | Op::Expand { .. }
402 | Op::Reduce { .. }
403 | Op::Conv { .. }
404 | Op::Gather { .. }
405 | Op::Quantize { .. }
406 | Op::Dequantize { .. }
407 | Op::DequantMatMul { .. } => true,
408 Op::Activation(a) => matches!(
409 a,
410 Activation::Relu
411 | Activation::Gelu
412 | Activation::Sigmoid
413 | Activation::Tanh
414 | Activation::Neg
415 | Activation::Silu
416 ),
417 _ => false,
418 }
419}
420
421#[cfg(feature = "vulkan")]
426fn vulkan_supports(op: &Op) -> bool {
427 use rlx_ir::OpKind::*;
428 let k = op.kind();
429 rlx_vulkan::backend::SUPPORTED_OPS.contains(&k)
430 || matches!(
431 k,
432 DotGeneral
434 | Fma
435 | GroupNorm
436 | BatchNormInference
437 | ResizeNearest2x
438 | ElementwiseRegion
439 | FusedMatMulBiasAct
440 | FusedResidualLN
441 | FusedResidualRmsNorm
442 | FusedSwiGLU
443 | FusedAttentionBlock
444 | FusedTransformerLayer
445 )
446}
447
448#[cfg(feature = "oneapi")]
452fn oneapi_supports(op: &Op) -> bool {
453 use rlx_ir::OpKind::*;
454 let k = op.kind();
455 rlx_oneapi::backend::SUPPORTED_OPS.contains(&k)
456 || matches!(
457 k,
458 DotGeneral
459 | Fma
460 | GroupNorm
461 | BatchNormInference
462 | ResizeNearest2x
463 | ElementwiseRegion
464 | FusedMatMulBiasAct
465 | FusedResidualLN
466 | FusedResidualRmsNorm
467 | FusedSwiGLU
468 | FusedAttentionBlock
469 | FusedTransformerLayer
470 )
471}
472
473pub fn supports_graph(device: Device, graph: &Graph) -> bool {
479 supports_graph_with_options(device, graph, &CompileOptions::default())
480}
481
482pub fn supports_graph_with_options(
484 device: Device,
485 graph: &Graph,
486 options: &CompileOptions,
487) -> bool {
488 if !is_available(device) {
489 return false;
490 }
491 if let Some(backend) = crate::registry::backend_for(device) {
492 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
493 graph.clone(),
494 device.name(),
495 backend.supported_ops(),
496 options.kernel_dispatch,
497 );
498 return report.compile_ready;
499 }
500 graph.nodes().iter().all(|n| supports(device, &n.op))
501}
502
503pub fn legalize_graph_for_device(graph: Graph, device: Device) -> Result<Graph, String> {
512 let (graph, _report) = legalize_graph_for_device_with_report(graph, device)?;
513 Ok(graph)
514}
515
516pub fn legalize_graph_for_device_with_report(
518 graph: Graph,
519 device: Device,
520) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
521 legalize_graph_for_device_with_options(graph, device, &CompileOptions::default())
522}
523
524pub fn legalize_graph_for_device_with_options(
527 graph: Graph,
528 device: Device,
529 options: &CompileOptions,
530) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
531 let backend = crate::registry::backend_for(device).ok_or_else(|| {
532 format!(
533 "no backend registered for {device:?} — enable the matching \
534 `rlx-runtime` Cargo feature (e.g. `metal`, `gpu`, `cuda`)"
535 )
536 })?;
537 let ops = backend.supported_ops();
538 let (graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
539 graph,
540 device.name(),
541 ops,
542 options.kernel_dispatch,
543 );
544 if !report.compile_ready {
545 return Err(format!(
546 "{}\n{}",
547 rlx_opt::format_legalize_error(device.name(), &report.still_unsupported),
548 rlx_opt::format_dispatch_report(&report)
549 ));
550 }
551 Ok((graph, report))
552}
553
554pub fn dispatch_report_for_device(
556 graph: &Graph,
557 device: Device,
558) -> Result<rlx_opt::KernelDispatchReport, String> {
559 dispatch_report_for_device_with_options(graph, device, &CompileOptions::default())
560}
561
562pub fn dispatch_report_for_device_with_options(
564 graph: &Graph,
565 device: Device,
566 options: &CompileOptions,
567) -> Result<rlx_opt::KernelDispatchReport, String> {
568 let backend = crate::registry::backend_for(device)
569 .ok_or_else(|| format!("no backend registered for {device:?}"))?;
570 Ok(rlx_opt::analyze_dispatch(
571 graph,
572 device.name(),
573 backend.supported_ops(),
574 options.kernel_dispatch,
575 ))
576}
577
578pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
582 first_unsupported_op_with_options(device, graph, &CompileOptions::default())
583}
584
585pub fn first_unsupported_op_with_options<'a>(
587 device: Device,
588 graph: &'a Graph,
589 options: &CompileOptions,
590) -> Option<(usize, &'a Op)> {
591 if !is_available(device) {
592 return graph.nodes().first().map(|n| (0, &n.op));
593 }
594 if let Some(backend) = crate::registry::backend_for(device) {
595 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
596 graph.clone(),
597 device.name(),
598 backend.supported_ops(),
599 options.kernel_dispatch,
600 );
601 if let Some((id, kind)) = report.still_unsupported.first() {
602 let idx = graph.nodes().iter().position(|n| n.id == *id).unwrap_or(0);
603 let op = graph
604 .nodes()
605 .iter()
606 .find(|n| n.id == *id)
607 .map(|n| &n.op)
608 .unwrap_or(&graph.nodes()[0].op);
609 let _ = kind;
610 return Some((idx, op));
611 }
612 return None;
613 }
614 graph
615 .nodes()
616 .iter()
617 .enumerate()
618 .find_map(|(i, n)| (!supports(device, &n.op)).then_some((i, &n.op)))
619}
620
621#[allow(unused_variables)]
622fn mlx_supports(op: &Op) -> bool {
623 true
628}
629
630#[allow(unused_variables)]
631fn metal_supports(op: &Op) -> bool {
632 let _ = op;
638 true
639}
640
641fn coreml_supports(op: &Op) -> bool {
652 #[cfg(all(
656 feature = "coreml",
657 target_vendor = "apple",
658 not(target_os = "watchos")
659 ))]
660 {
661 let kind = op.kind();
662 if rlx_coreml::SUPPORTED_OPS.contains(&kind) {
663 return true;
664 }
665 #[cfg(feature = "training")]
666 if rlx_coreml::BACKWARD_OPS.contains(&kind)
667 || rlx_coreml::NATIVE_BACKWARD_OPS.contains(&kind)
668 {
669 return true;
670 }
671 false
672 }
673 #[cfg(not(all(
674 feature = "coreml",
675 target_vendor = "apple",
676 not(target_os = "watchos")
677 )))]
678 {
679 let _ = op;
680 false
681 }
682}
683
684#[allow(unused_variables)]
685fn gpu_family_supports(device: Device, op: &Op) -> bool {
686 let kind = op.kind();
698 let _ = kind;
699 match device {
700 #[cfg(feature = "cuda")]
701 Device::Cuda => rlx_cuda::SUPPORTED_OPS.contains(&kind),
702 #[cfg(feature = "rocm")]
703 Device::Rocm => rlx_rocm::SUPPORTED_OPS.contains(&kind),
704 #[cfg(feature = "gpu")]
705 Device::Gpu => rlx_wgpu::SUPPORTED_OPS.contains(&kind),
706 _ => true,
707 }
708}
709
710pub fn drain_device(device: Device) {
713 #[cfg(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal"))]
714 {
715 if device == Device::Metal {
716 rlx_metal::device::drain_command_queue();
717 }
718 }
719 #[cfg(not(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal")))]
720 let _ = device;
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use rlx_ir::op::{Activation, BinaryOp};
727 use rlx_ir::{DType, Graph, Shape};
728
729 fn scalar_shape() -> Shape {
730 Shape::new(&[1], DType::F32)
731 }
732
733 #[test]
734 fn cpu_supports_everything_built_in() {
735 assert!(supports(Device::Cpu, &Op::Activation(Activation::Sin)));
736 assert!(supports(Device::Cpu, &Op::Activation(Activation::Cos)));
737 assert!(supports(Device::Cpu, &Op::Activation(Activation::Exp)));
738 assert!(supports(Device::Cpu, &Op::Binary(BinaryOp::Add)));
739 }
740
741 #[test]
742 fn unbuilt_device_supports_nothing() {
743 assert!(!supports(Device::OpenGl, &Op::Activation(Activation::Relu)));
745 }
746
747 #[test]
748 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
749 fn metal_supports_full_activation_set() {
750 for act in [
754 Activation::Sin,
755 Activation::Cos,
756 Activation::Tan,
757 Activation::Atan,
758 Activation::Recip,
759 Activation::Exp,
760 ] {
761 assert!(
762 supports(Device::Metal, &Op::Activation(act)),
763 "Metal should support Activation::{act:?}"
764 );
765 }
766 }
767
768 #[test]
769 fn graph_walk_reports_first_blocker() {
770 let mut g = Graph::new("walk");
771 let s = scalar_shape();
772 let x = g.input("x", s.clone());
773 let _e = g.activation(Activation::Exp, x, s.clone());
774 let _sin = g.activation(Activation::Sin, x, s);
775 assert!(supports_graph(Device::Cpu, &g));
777 assert!(first_unsupported_op(Device::Cpu, &g).is_none());
778 }
779
780 #[test]
781 fn fastest_device_returns_cpu_when_only_cpu_is_available() {
782 let pick = fastest_device();
783 assert!(is_available(pick));
784 assert_eq!(pick, fastest_among(&available_devices()));
785 }
786
787 #[test]
788 fn fastest_among_respects_priority_order() {
789 let pick = fastest_among(&[Device::Cpu, Device::Metal, Device::Mlx]);
790 assert_eq!(pick, Device::Mlx);
791 }
792
793 #[test]
794 fn devices_for_is_subset_of_available() {
795 let mut g = Graph::new("id");
796 let x = g.input("x", scalar_shape());
797 g.set_outputs(vec![x]);
798 for d in devices_for(&g) {
799 assert!(is_available(d));
800 assert!(supports_graph(d, &g));
801 }
802 }
803}