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 const BROWSER_DEVICE_PRIORITY: &[Device] = &[Device::WebGpu, Device::OpenGl, Device::Cpu];
51
52pub fn supports_run_slots(device: Device) -> bool {
63 matches!(
64 device,
65 Device::Cpu | Device::Metal | Device::Mlx | Device::Cuda | Device::Rocm
66 )
67}
68
69pub fn supports_ragged_rope(device: Device) -> bool {
86 matches!(device, Device::Cpu | Device::Metal)
87}
88
89pub fn trim_accelerator_arena_pool(device: Device) {
92 #[cfg(feature = "cuda")]
93 if device == Device::Cuda {
94 rlx_cuda::trim_device_memory_pool();
95 }
96 #[cfg(not(feature = "cuda"))]
97 let _ = device;
98}
99
100pub fn is_available(device: Device) -> bool {
101 #[cfg(feature = "cuda")]
102 if device == Device::Cuda {
103 return rlx_cuda::is_available();
104 }
105 #[cfg(feature = "rocm")]
106 if device == Device::Rocm {
107 return rlx_rocm::is_available();
108 }
109 #[cfg(feature = "gpu")]
110 if device == Device::Gpu {
111 #[cfg(target_arch = "wasm32")]
112 {
113 return rlx_wgpu::device::wgpu_device().is_some();
114 }
115 #[cfg(not(target_arch = "wasm32"))]
116 {
117 return rlx_wgpu::is_available();
118 }
119 }
120 #[cfg(feature = "webgpu")]
121 if device == Device::WebGpu {
122 #[cfg(target_arch = "wasm32")]
123 {
124 return rlx_wgpu::device::wgpu_device().is_some();
125 }
126 #[cfg(not(target_arch = "wasm32"))]
127 {
128 return rlx_wgpu::is_available();
129 }
130 }
131 #[cfg(feature = "opengl")]
132 if device == Device::OpenGl {
133 return true;
134 }
135 #[cfg(feature = "vulkan")]
136 if device == Device::Vulkan {
137 return rlx_vulkan::is_available();
138 }
139 #[cfg(feature = "oneapi")]
140 if device == Device::OneApi {
141 return rlx_oneapi::is_available();
142 }
143 #[cfg(feature = "tpu")]
144 if device == Device::Tpu {
145 return rlx_tpu::is_available();
146 }
147 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
152 if device == Device::Metal {
153 return rlx_metal::is_available();
154 }
155 #[cfg(all(feature = "mlx", rlx_mlx_host))]
156 if device == Device::Mlx {
157 return rlx_mlx::is_available();
158 }
159
160 let feature_gated = match device {
161 Device::Cpu => cfg!(feature = "cpu"),
162 Device::Metal => cfg!(all(
163 feature = "metal",
164 target_vendor = "apple",
165 not(target_os = "watchos")
166 )),
167 Device::Mlx => cfg!(feature = "mlx"),
168 Device::Ane => cfg!(any(feature = "coreml", feature = "ane")),
169 Device::Cuda => cfg!(feature = "cuda"),
170 Device::Rocm => cfg!(feature = "rocm"),
171 Device::OneApi => cfg!(feature = "oneapi"),
172 Device::Tpu => cfg!(feature = "tpu"),
173 Device::Hexagon => cfg!(feature = "qnn"),
174 Device::Gpu => cfg!(feature = "gpu"),
175 Device::Vulkan => cfg!(feature = "vulkan"),
176 Device::OpenGl => cfg!(feature = "opengl"),
177 Device::DirectX => cfg!(feature = "directx"),
178 Device::WebGpu => cfg!(feature = "webgpu"),
179 };
180 if feature_gated {
181 return true;
182 }
183 crate::registry::registered_devices().contains(&device)
184}
185
186#[cfg(all(feature = "apple", target_vendor = "apple"))]
190pub fn available_apple_devices() -> Vec<Device> {
191 [Device::Metal, Device::Mlx, Device::Gpu, Device::Ane]
192 .into_iter()
193 .filter(|d| is_available(*d))
194 .collect()
195}
196
197pub fn available_devices() -> Vec<Device> {
200 Device::all()
201 .iter()
202 .copied()
203 .filter(|d| is_available(*d))
204 .collect()
205}
206
207pub fn available_browser_devices() -> Vec<Device> {
209 BROWSER_DEVICE_PRIORITY
210 .iter()
211 .copied()
212 .filter(|d| is_available(*d))
213 .collect()
214}
215
216pub fn preferred_browser_device() -> Option<Device> {
218 available_browser_devices().into_iter().next()
219}
220
221pub fn devices_for(graph: &Graph) -> Vec<Device> {
224 crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
225}
226
227pub fn fastest_device() -> Device {
234 fastest_among(&available_devices())
235}
236
237pub fn fastest_among(candidates: &[Device]) -> Device {
239 for &d in DEVICE_PRIORITY {
240 if candidates.contains(&d) {
241 return d;
242 }
243 }
244 candidates.first().copied().unwrap_or(Device::Cpu)
245}
246
247pub fn full_name(device: Device) -> &'static str {
252 if let Device::Cpu = device {
253 if cfg!(feature = "blas-accelerate") {
254 return "CPU (Accelerate)";
255 }
256 if cfg!(feature = "blas-mkl") {
257 return "CPU (MKL)";
258 }
259 if cfg!(feature = "blas-openblas") {
260 return "CPU (OpenBLAS)";
261 }
262 }
263 device.name()
264}
265
266pub fn supports(device: Device, op: &Op) -> bool {
290 if !is_available(device) {
291 return false;
292 }
293 match device {
294 Device::Cpu => true, Device::Mlx => mlx_supports(op),
296 Device::Metal => metal_supports(op),
297 Device::Ane => coreml_supports(op),
298 Device::Gpu | Device::Cuda | Device::Rocm => gpu_family_supports(op),
299 #[cfg(feature = "vulkan")]
300 Device::Vulkan => vulkan_supports(op),
301 #[cfg(feature = "oneapi")]
302 Device::OneApi => oneapi_supports(op),
303 Device::Hexagon => qnn_supports(op),
304 _ => false,
308 }
309}
310
311fn qnn_supports(op: &Op) -> bool {
315 use rlx_ir::op::Activation;
316 match op {
317 Op::Input { .. }
318 | Op::Param { .. }
319 | Op::Constant { .. }
320 | Op::MatMul
321 | Op::Binary(_)
322 | Op::Softmax { .. }
323 | Op::Reshape { .. }
324 | Op::Transpose { .. }
325 | Op::LayerNorm { .. }
326 | Op::RmsNorm { .. }
327 | Op::Concat { .. }
328 | Op::Narrow { .. }
329 | Op::Rope { .. }
330 | Op::Attention { .. }
331 | Op::FusedAttentionBlock { .. }
332 | Op::Expand { .. }
333 | Op::Reduce { .. }
334 | Op::Conv { .. }
335 | Op::Gather { .. }
336 | Op::Quantize { .. }
337 | Op::Dequantize { .. }
338 | Op::DequantMatMul { .. } => true,
339 Op::Activation(a) => matches!(
340 a,
341 Activation::Relu
342 | Activation::Gelu
343 | Activation::Sigmoid
344 | Activation::Tanh
345 | Activation::Neg
346 | Activation::Silu
347 ),
348 _ => false,
349 }
350}
351
352#[cfg(feature = "vulkan")]
357fn vulkan_supports(op: &Op) -> bool {
358 use rlx_ir::OpKind::*;
359 let k = op.kind();
360 rlx_vulkan::backend::SUPPORTED_OPS.contains(&k)
361 || matches!(
362 k,
363 DotGeneral
365 | Fma
366 | GroupNorm
367 | BatchNormInference
368 | ResizeNearest2x
369 | ElementwiseRegion
370 | FusedMatMulBiasAct
371 | FusedResidualLN
372 | FusedResidualRmsNorm
373 | FusedSwiGLU
374 | FusedAttentionBlock
375 | FusedTransformerLayer
376 )
377}
378
379#[cfg(feature = "oneapi")]
383fn oneapi_supports(op: &Op) -> bool {
384 use rlx_ir::OpKind::*;
385 let k = op.kind();
386 rlx_oneapi::backend::SUPPORTED_OPS.contains(&k)
387 || matches!(
388 k,
389 DotGeneral
390 | Fma
391 | GroupNorm
392 | BatchNormInference
393 | ResizeNearest2x
394 | ElementwiseRegion
395 | FusedMatMulBiasAct
396 | FusedResidualLN
397 | FusedResidualRmsNorm
398 | FusedSwiGLU
399 | FusedAttentionBlock
400 | FusedTransformerLayer
401 )
402}
403
404pub fn supports_graph(device: Device, graph: &Graph) -> bool {
410 supports_graph_with_options(device, graph, &CompileOptions::default())
411}
412
413pub fn supports_graph_with_options(
415 device: Device,
416 graph: &Graph,
417 options: &CompileOptions,
418) -> bool {
419 if !is_available(device) {
420 return false;
421 }
422 if let Some(backend) = crate::registry::backend_for(device) {
423 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
424 graph.clone(),
425 device.name(),
426 backend.supported_ops(),
427 options.kernel_dispatch,
428 );
429 return report.compile_ready;
430 }
431 graph.nodes().iter().all(|n| supports(device, &n.op))
432}
433
434pub fn legalize_graph_for_device(graph: Graph, device: Device) -> Result<Graph, String> {
443 let (graph, _report) = legalize_graph_for_device_with_report(graph, device)?;
444 Ok(graph)
445}
446
447pub fn legalize_graph_for_device_with_report(
449 graph: Graph,
450 device: Device,
451) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
452 legalize_graph_for_device_with_options(graph, device, &CompileOptions::default())
453}
454
455pub fn legalize_graph_for_device_with_options(
458 graph: Graph,
459 device: Device,
460 options: &CompileOptions,
461) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
462 let backend = crate::registry::backend_for(device).ok_or_else(|| {
463 format!(
464 "no backend registered for {device:?} — enable the matching \
465 `rlx-runtime` Cargo feature (e.g. `metal`, `gpu`, `cuda`)"
466 )
467 })?;
468 let ops = backend.supported_ops();
469 let (graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
470 graph,
471 device.name(),
472 ops,
473 options.kernel_dispatch,
474 );
475 if !report.compile_ready {
476 return Err(format!(
477 "{}\n{}",
478 rlx_opt::format_legalize_error(device.name(), &report.still_unsupported),
479 rlx_opt::format_dispatch_report(&report)
480 ));
481 }
482 Ok((graph, report))
483}
484
485pub fn dispatch_report_for_device(
487 graph: &Graph,
488 device: Device,
489) -> Result<rlx_opt::KernelDispatchReport, String> {
490 dispatch_report_for_device_with_options(graph, device, &CompileOptions::default())
491}
492
493pub fn dispatch_report_for_device_with_options(
495 graph: &Graph,
496 device: Device,
497 options: &CompileOptions,
498) -> Result<rlx_opt::KernelDispatchReport, String> {
499 let backend = crate::registry::backend_for(device)
500 .ok_or_else(|| format!("no backend registered for {device:?}"))?;
501 Ok(rlx_opt::analyze_dispatch(
502 graph,
503 device.name(),
504 backend.supported_ops(),
505 options.kernel_dispatch,
506 ))
507}
508
509pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
513 first_unsupported_op_with_options(device, graph, &CompileOptions::default())
514}
515
516pub fn first_unsupported_op_with_options<'a>(
518 device: Device,
519 graph: &'a Graph,
520 options: &CompileOptions,
521) -> Option<(usize, &'a Op)> {
522 if !is_available(device) {
523 return graph.nodes().first().map(|n| (0, &n.op));
524 }
525 if let Some(backend) = crate::registry::backend_for(device) {
526 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
527 graph.clone(),
528 device.name(),
529 backend.supported_ops(),
530 options.kernel_dispatch,
531 );
532 if let Some((id, kind)) = report.still_unsupported.first() {
533 let idx = graph.nodes().iter().position(|n| n.id == *id).unwrap_or(0);
534 let op = graph
535 .nodes()
536 .iter()
537 .find(|n| n.id == *id)
538 .map(|n| &n.op)
539 .unwrap_or(&graph.nodes()[0].op);
540 let _ = kind;
541 return Some((idx, op));
542 }
543 return None;
544 }
545 graph
546 .nodes()
547 .iter()
548 .enumerate()
549 .find_map(|(i, n)| (!supports(device, &n.op)).then_some((i, &n.op)))
550}
551
552#[allow(unused_variables)]
553fn mlx_supports(op: &Op) -> bool {
554 true
559}
560
561#[allow(unused_variables)]
562fn metal_supports(op: &Op) -> bool {
563 let _ = op;
569 true
570}
571
572fn coreml_supports(op: &Op) -> bool {
583 #[cfg(feature = "coreml")]
584 {
585 let kind = op.kind();
586 if rlx_coreml::SUPPORTED_OPS.contains(&kind) {
587 return true;
588 }
589 #[cfg(feature = "training")]
590 if rlx_coreml::BACKWARD_OPS.contains(&kind)
591 || rlx_coreml::NATIVE_BACKWARD_OPS.contains(&kind)
592 {
593 return true;
594 }
595 false
596 }
597 #[cfg(not(feature = "coreml"))]
598 {
599 let _ = op;
600 false
601 }
602}
603
604#[allow(unused_variables)]
605fn gpu_family_supports(op: &Op) -> bool {
606 let _ = op;
610 true
611}
612
613pub fn drain_device(device: Device) {
616 #[cfg(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal"))]
617 {
618 if device == Device::Metal {
619 rlx_metal::device::drain_command_queue();
620 }
621 }
622 #[cfg(not(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal")))]
623 let _ = device;
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use rlx_ir::op::{Activation, BinaryOp};
630 use rlx_ir::{DType, Graph, Shape};
631
632 fn scalar_shape() -> Shape {
633 Shape::new(&[1], DType::F32)
634 }
635
636 #[test]
637 fn cpu_supports_everything_built_in() {
638 assert!(supports(Device::Cpu, &Op::Activation(Activation::Sin)));
639 assert!(supports(Device::Cpu, &Op::Activation(Activation::Cos)));
640 assert!(supports(Device::Cpu, &Op::Activation(Activation::Exp)));
641 assert!(supports(Device::Cpu, &Op::Binary(BinaryOp::Add)));
642 }
643
644 #[test]
645 fn unbuilt_device_supports_nothing() {
646 assert!(!supports(Device::OpenGl, &Op::Activation(Activation::Relu)));
648 }
649
650 #[test]
651 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
652 fn metal_supports_full_activation_set() {
653 for act in [
657 Activation::Sin,
658 Activation::Cos,
659 Activation::Tan,
660 Activation::Atan,
661 Activation::Exp,
662 ] {
663 assert!(
664 supports(Device::Metal, &Op::Activation(act)),
665 "Metal should support Activation::{act:?}"
666 );
667 }
668 }
669
670 #[test]
671 fn graph_walk_reports_first_blocker() {
672 let mut g = Graph::new("walk");
673 let s = scalar_shape();
674 let x = g.input("x", s.clone());
675 let _e = g.activation(Activation::Exp, x, s.clone());
676 let _sin = g.activation(Activation::Sin, x, s);
677 assert!(supports_graph(Device::Cpu, &g));
679 assert!(first_unsupported_op(Device::Cpu, &g).is_none());
680 }
681
682 #[test]
683 fn fastest_device_returns_cpu_when_only_cpu_is_available() {
684 let pick = fastest_device();
685 assert!(is_available(pick));
686 assert_eq!(pick, fastest_among(&available_devices()));
687 }
688
689 #[test]
690 fn fastest_among_respects_priority_order() {
691 let pick = fastest_among(&[Device::Cpu, Device::Metal, Device::Mlx]);
692 assert_eq!(pick, Device::Mlx);
693 }
694
695 #[test]
696 fn devices_for_is_subset_of_available() {
697 let mut g = Graph::new("id");
698 let x = g.input("x", scalar_shape());
699 g.set_outputs(vec![x]);
700 for d in devices_for(&g) {
701 assert!(is_available(d));
702 assert!(supports_graph(d, &g));
703 }
704 }
705}