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 is_available(device: Device) -> bool {
87 #[cfg(feature = "cuda")]
88 if device == Device::Cuda {
89 return rlx_cuda::is_available();
90 }
91 #[cfg(feature = "rocm")]
92 if device == Device::Rocm {
93 return rlx_rocm::is_available();
94 }
95 #[cfg(feature = "gpu")]
96 if device == Device::Gpu {
97 return rlx_wgpu::is_available();
98 }
99 #[cfg(feature = "vulkan")]
100 if device == Device::Vulkan {
101 return rlx_vulkan::is_available();
102 }
103 #[cfg(feature = "oneapi")]
104 if device == Device::OneApi {
105 return rlx_oneapi::is_available();
106 }
107 #[cfg(feature = "tpu")]
108 if device == Device::Tpu {
109 return rlx_tpu::is_available();
110 }
111 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
116 if device == Device::Metal {
117 return rlx_metal::is_available();
118 }
119 #[cfg(all(feature = "mlx", rlx_mlx_host))]
120 if device == Device::Mlx {
121 return rlx_mlx::is_available();
122 }
123
124 let feature_gated = match device {
125 Device::Cpu => cfg!(feature = "cpu"),
126 Device::Metal => cfg!(all(
127 feature = "metal",
128 target_vendor = "apple",
129 not(target_os = "watchos")
130 )),
131 Device::Mlx => cfg!(feature = "mlx"),
132 Device::Ane => cfg!(any(feature = "coreml", feature = "ane")),
133 Device::Cuda => cfg!(feature = "cuda"),
134 Device::Rocm => cfg!(feature = "rocm"),
135 Device::OneApi => cfg!(feature = "oneapi"),
136 Device::Tpu => cfg!(feature = "tpu"),
137 Device::Hexagon => cfg!(feature = "qnn"),
138 Device::Gpu => cfg!(feature = "gpu"),
139 Device::Vulkan => cfg!(feature = "vulkan"),
140 Device::OpenGl => cfg!(feature = "opengl"),
141 Device::DirectX => cfg!(feature = "directx"),
142 Device::WebGpu => cfg!(feature = "webgpu"),
143 };
144 if feature_gated {
145 return true;
146 }
147 crate::registry::registered_devices().contains(&device)
148}
149
150#[cfg(all(feature = "apple", target_vendor = "apple"))]
154pub fn available_apple_devices() -> Vec<Device> {
155 [Device::Metal, Device::Mlx, Device::Gpu, Device::Ane]
156 .into_iter()
157 .filter(|d| is_available(*d))
158 .collect()
159}
160
161pub fn available_devices() -> Vec<Device> {
164 Device::all()
165 .iter()
166 .copied()
167 .filter(|d| is_available(*d))
168 .collect()
169}
170
171pub fn devices_for(graph: &Graph) -> Vec<Device> {
174 crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
175}
176
177pub fn fastest_device() -> Device {
184 fastest_among(&available_devices())
185}
186
187pub fn fastest_among(candidates: &[Device]) -> Device {
189 for &d in DEVICE_PRIORITY {
190 if candidates.contains(&d) {
191 return d;
192 }
193 }
194 candidates.first().copied().unwrap_or(Device::Cpu)
195}
196
197pub fn full_name(device: Device) -> &'static str {
202 if let Device::Cpu = device {
203 if cfg!(feature = "blas-accelerate") {
204 return "CPU (Accelerate)";
205 }
206 if cfg!(feature = "blas-mkl") {
207 return "CPU (MKL)";
208 }
209 if cfg!(feature = "blas-openblas") {
210 return "CPU (OpenBLAS)";
211 }
212 }
213 device.name()
214}
215
216pub fn supports(device: Device, op: &Op) -> bool {
240 if !is_available(device) {
241 return false;
242 }
243 match device {
244 Device::Cpu => true, Device::Mlx => mlx_supports(op),
246 Device::Metal => metal_supports(op),
247 Device::Ane => coreml_supports(op),
248 Device::Gpu | Device::Cuda | Device::Rocm => gpu_family_supports(op),
249 #[cfg(feature = "vulkan")]
250 Device::Vulkan => vulkan_supports(op),
251 #[cfg(feature = "oneapi")]
252 Device::OneApi => oneapi_supports(op),
253 Device::Hexagon => qnn_supports(op),
254 _ => false,
258 }
259}
260
261fn qnn_supports(op: &Op) -> bool {
265 use rlx_ir::op::Activation;
266 match op {
267 Op::Input { .. }
268 | Op::Param { .. }
269 | Op::Constant { .. }
270 | Op::MatMul
271 | Op::Binary(_)
272 | Op::Softmax { .. }
273 | Op::Reshape { .. }
274 | Op::Transpose { .. }
275 | Op::LayerNorm { .. }
276 | Op::RmsNorm { .. }
277 | Op::Concat { .. }
278 | Op::Narrow { .. }
279 | Op::Rope { .. }
280 | Op::Attention { .. }
281 | Op::Reduce { .. }
282 | Op::Conv { .. }
283 | Op::Gather { .. }
284 | Op::Quantize { .. }
285 | Op::Dequantize { .. } => true,
286 Op::Activation(a) => matches!(
287 a,
288 Activation::Relu
289 | Activation::Gelu
290 | Activation::Sigmoid
291 | Activation::Tanh
292 | Activation::Neg
293 ),
294 _ => false,
295 }
296}
297
298#[cfg(feature = "vulkan")]
303fn vulkan_supports(op: &Op) -> bool {
304 use rlx_ir::OpKind::*;
305 let k = op.kind();
306 rlx_vulkan::backend::SUPPORTED_OPS.contains(&k)
307 || matches!(
308 k,
309 DotGeneral
311 | Fma
312 | GroupNorm
313 | BatchNormInference
314 | ResizeNearest2x
315 | ElementwiseRegion
316 | FusedMatMulBiasAct
317 | FusedResidualLN
318 | FusedResidualRmsNorm
319 | FusedSwiGLU
320 | FusedAttentionBlock
321 | FusedTransformerLayer
322 )
323}
324
325#[cfg(feature = "oneapi")]
329fn oneapi_supports(op: &Op) -> bool {
330 use rlx_ir::OpKind::*;
331 let k = op.kind();
332 rlx_oneapi::backend::SUPPORTED_OPS.contains(&k)
333 || matches!(
334 k,
335 DotGeneral
336 | Fma
337 | GroupNorm
338 | BatchNormInference
339 | ResizeNearest2x
340 | ElementwiseRegion
341 | FusedMatMulBiasAct
342 | FusedResidualLN
343 | FusedResidualRmsNorm
344 | FusedSwiGLU
345 | FusedAttentionBlock
346 | FusedTransformerLayer
347 )
348}
349
350pub fn supports_graph(device: Device, graph: &Graph) -> bool {
356 supports_graph_with_options(device, graph, &CompileOptions::default())
357}
358
359pub fn supports_graph_with_options(
361 device: Device,
362 graph: &Graph,
363 options: &CompileOptions,
364) -> bool {
365 if !is_available(device) {
366 return false;
367 }
368 if let Some(backend) = crate::registry::backend_for(device) {
369 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
370 graph.clone(),
371 device.name(),
372 backend.supported_ops(),
373 options.kernel_dispatch,
374 );
375 return report.compile_ready;
376 }
377 graph.nodes().iter().all(|n| supports(device, &n.op))
378}
379
380pub fn legalize_graph_for_device(graph: Graph, device: Device) -> Result<Graph, String> {
389 let (graph, _report) = legalize_graph_for_device_with_report(graph, device)?;
390 Ok(graph)
391}
392
393pub fn legalize_graph_for_device_with_report(
395 graph: Graph,
396 device: Device,
397) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
398 legalize_graph_for_device_with_options(graph, device, &CompileOptions::default())
399}
400
401pub fn legalize_graph_for_device_with_options(
404 graph: Graph,
405 device: Device,
406 options: &CompileOptions,
407) -> Result<(Graph, rlx_opt::KernelDispatchReport), String> {
408 let backend = crate::registry::backend_for(device).ok_or_else(|| {
409 format!(
410 "no backend registered for {device:?} — enable the matching \
411 `rlx-runtime` Cargo feature (e.g. `metal`, `gpu`, `cuda`)"
412 )
413 })?;
414 let ops = backend.supported_ops();
415 let (graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
416 graph,
417 device.name(),
418 ops,
419 options.kernel_dispatch,
420 );
421 if !report.compile_ready {
422 return Err(format!(
423 "{}\n{}",
424 rlx_opt::format_legalize_error(device.name(), &report.still_unsupported),
425 rlx_opt::format_dispatch_report(&report)
426 ));
427 }
428 Ok((graph, report))
429}
430
431pub fn dispatch_report_for_device(
433 graph: &Graph,
434 device: Device,
435) -> Result<rlx_opt::KernelDispatchReport, String> {
436 dispatch_report_for_device_with_options(graph, device, &CompileOptions::default())
437}
438
439pub fn dispatch_report_for_device_with_options(
441 graph: &Graph,
442 device: Device,
443 options: &CompileOptions,
444) -> Result<rlx_opt::KernelDispatchReport, String> {
445 let backend = crate::registry::backend_for(device)
446 .ok_or_else(|| format!("no backend registered for {device:?}"))?;
447 Ok(rlx_opt::analyze_dispatch(
448 graph,
449 device.name(),
450 backend.supported_ops(),
451 options.kernel_dispatch,
452 ))
453}
454
455pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
459 first_unsupported_op_with_options(device, graph, &CompileOptions::default())
460}
461
462pub fn first_unsupported_op_with_options<'a>(
464 device: Device,
465 graph: &'a Graph,
466 options: &CompileOptions,
467) -> Option<(usize, &'a Op)> {
468 if !is_available(device) {
469 return graph.nodes().first().map(|n| (0, &n.op));
470 }
471 if let Some(backend) = crate::registry::backend_for(device) {
472 let (_, report) = rlx_opt::prepare_graph_for_backend_with_report(
473 graph.clone(),
474 device.name(),
475 backend.supported_ops(),
476 options.kernel_dispatch,
477 );
478 if let Some((id, kind)) = report.still_unsupported.first() {
479 let idx = graph.nodes().iter().position(|n| n.id == *id).unwrap_or(0);
480 let op = graph
481 .nodes()
482 .iter()
483 .find(|n| n.id == *id)
484 .map(|n| &n.op)
485 .unwrap_or(&graph.nodes()[0].op);
486 let _ = kind;
487 return Some((idx, op));
488 }
489 return None;
490 }
491 graph
492 .nodes()
493 .iter()
494 .enumerate()
495 .find_map(|(i, n)| (!supports(device, &n.op)).then_some((i, &n.op)))
496}
497
498#[allow(unused_variables)]
499fn mlx_supports(op: &Op) -> bool {
500 true
505}
506
507#[allow(unused_variables)]
508fn metal_supports(op: &Op) -> bool {
509 let _ = op;
515 true
516}
517
518fn coreml_supports(op: &Op) -> bool {
529 let kind = op.kind();
530 if crate::backend::COREML_SUPPORTED_OPS.contains(&kind) {
531 return true;
532 }
533 #[cfg(feature = "training")]
534 if crate::backend::COREML_BACKWARD_OPS.contains(&kind)
535 || crate::backend::COREML_NATIVE_BACKWARD_OPS.contains(&kind)
536 {
537 return true;
538 }
539 false
540}
541
542#[allow(unused_variables)]
543fn gpu_family_supports(op: &Op) -> bool {
544 let _ = op;
548 true
549}
550
551pub fn drain_device(device: Device) {
554 #[cfg(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal"))]
555 {
556 if device == Device::Metal {
557 rlx_metal::device::drain_command_queue();
558 }
559 }
560 #[cfg(not(all(target_vendor = "apple", not(target_os = "watchos"), feature = "metal")))]
561 let _ = device;
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use rlx_ir::op::{Activation, BinaryOp};
568 use rlx_ir::{DType, Graph, Shape};
569
570 fn scalar_shape() -> Shape {
571 Shape::new(&[1], DType::F32)
572 }
573
574 #[test]
575 fn cpu_supports_everything_built_in() {
576 assert!(supports(Device::Cpu, &Op::Activation(Activation::Sin)));
577 assert!(supports(Device::Cpu, &Op::Activation(Activation::Cos)));
578 assert!(supports(Device::Cpu, &Op::Activation(Activation::Exp)));
579 assert!(supports(Device::Cpu, &Op::Binary(BinaryOp::Add)));
580 }
581
582 #[test]
583 fn unbuilt_device_supports_nothing() {
584 assert!(!supports(Device::OpenGl, &Op::Activation(Activation::Relu)));
586 }
587
588 #[test]
589 #[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
590 fn metal_supports_full_activation_set() {
591 for act in [
595 Activation::Sin,
596 Activation::Cos,
597 Activation::Tan,
598 Activation::Atan,
599 Activation::Exp,
600 ] {
601 assert!(
602 supports(Device::Metal, &Op::Activation(act)),
603 "Metal should support Activation::{act:?}"
604 );
605 }
606 }
607
608 #[test]
609 fn graph_walk_reports_first_blocker() {
610 let mut g = Graph::new("walk");
611 let s = scalar_shape();
612 let x = g.input("x", s.clone());
613 let _e = g.activation(Activation::Exp, x, s.clone());
614 let _sin = g.activation(Activation::Sin, x, s);
615 assert!(supports_graph(Device::Cpu, &g));
617 assert!(first_unsupported_op(Device::Cpu, &g).is_none());
618 }
619
620 #[test]
621 fn fastest_device_returns_cpu_when_only_cpu_is_available() {
622 let pick = fastest_device();
623 assert!(is_available(pick));
624 assert_eq!(pick, fastest_among(&available_devices()));
625 }
626
627 #[test]
628 fn fastest_among_respects_priority_order() {
629 let pick = fastest_among(&[Device::Cpu, Device::Metal, Device::Mlx]);
630 assert_eq!(pick, Device::Mlx);
631 }
632
633 #[test]
634 fn devices_for_is_subset_of_available() {
635 let mut g = Graph::new("id");
636 let x = g.input("x", scalar_shape());
637 g.set_outputs(vec![x]);
638 for d in devices_for(&g) {
639 assert!(is_available(d));
640 assert!(supports_graph(d, &g));
641 }
642 }
643}