Skip to main content

rlx_runtime/
device_ext.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Engine-layer extensions for [`rlx_driver::Device`] (plan #58).
17//!
18//! `is_available` and `available_devices` consult the runtime's
19//! backend registry + Cargo features, both of which are
20//! engine-layer concerns. Keeping them here preserves the
21//! one-way dep direction (driver doesn't know about engine).
22
23use rlx_driver::Device;
24use rlx_ir::{Graph, Op};
25
26use crate::CompileOptions;
27
28/// Preferred probe order for ML workloads (highest throughput first).
29///
30/// Used by [`fastest_device`] and by [`crate::cost::fastest_device_for`] when
31/// calibrated cost models are unavailable for every candidate backend.
32pub(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
49/// Check whether `device` has a compiled-in backend or has been
50/// registered by an external crate.
51///
52/// GPU-family builtins (CUDA / ROCm / wgpu / TPU) additionally probe
53/// for a live driver or adapter at runtime so CI hosts that compile
54/// with `--features cuda` but have no NVIDIA stack don't report
55/// false positives. Other devices are Cargo-feature-gated; externally
56/// registered backends are discovered via the registry.
57/// Whether [`crate::CompiledGraph::run_slots`] + [`crate::CompiledGraph::arena_ptr`]
58/// are implemented (host readback layout; not a GPU-mapped arena on CUDA).
59pub 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
66/// Whether `device`'s RoPE kernel indexes its cos/sin table **per token**
67/// (one row per batch·seq element) instead of per sequence position.
68///
69/// Required for *ragged* batched decode, where each sequence in the batch sits
70/// at a different absolute position and so needs its own RoPE row.
71///
72/// Validated against the CPU reference on:
73///   - **CPU** (`rlx-cpu` executor + thunk), and
74///   - **Metal** (`cos_per_token` kernel path; `metal_rope_parity` ragged test).
75///
76/// The remaining GPU kernels still index by seq position (CUDA `rope.cu`, wgpu
77/// `rope.wgsl`), which collapses for decode (seq = 1) and would apply row 0's
78/// position to the whole batch — so callers (e.g. the server's fused batcher)
79/// fall back to per-length **uniform** grouping there. Add a device here only
80/// once its rope + rope_backward kernels are fixed *and* validated against the
81/// CPU reference.
82pub fn supports_ragged_rope(device: Device) -> bool {
83    matches!(device, Device::Cpu | Device::Metal)
84}
85
86/// Drop backend-held device arena pools between large compiles (CUDA).
87/// No-op on backends without a pool (CPU, Metal unified memory, ROCm, …).
88pub 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    // Metal / MLX probe the live device, not just the Cargo cfg: a build can
123    // be Metal-capable yet run where no Metal device exists (e.g. a headless
124    // iOS-simulator process), and runtime selection must not pick a dead
125    // backend.
126    #[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/// Apple backends enabled in this build (`metal`, `mlx`, `gpu`, `ane`) on
162/// any Apple platform. `is_available` filters per-device, so e.g. watchOS
163/// (no Metal) yields just the CoreML/ANE entry.
164#[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
172/// Every variant currently available — Cargo-feature-gated or
173/// runtime-registered.
174pub fn available_devices() -> Vec<Device> {
175    Device::all()
176        .iter()
177        .copied()
178        .filter(|d| is_available(*d))
179        .collect()
180}
181
182/// Intersection of [`available_devices`] and [`supports_graph`]. Use with
183/// [`crate::GraphDevices`] or [`crate::DevicePolicy`] to restrict the set.
184pub fn devices_for(graph: &Graph) -> Vec<Device> {
185    crate::device_policy::devices_for_with_policy(graph, &crate::DevicePolicy::default())
186}
187
188/// Highest-priority backend that is compiled in and live on this host.
189///
190/// Probes [`DEVICE_PRIORITY`] in order (TPU → CUDA → ROCm → MLX → Metal → …
191/// → CPU). Use this when you want a sensible default `Session` target without
192/// building a graph first. For workload-specific selection, prefer
193/// [`crate::cost::fastest_device_for`].
194pub fn fastest_device() -> Device {
195    fastest_among(&available_devices())
196}
197
198/// Pick the highest-priority entry from `candidates` (see [`DEVICE_PRIORITY`]).
199pub 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
208/// Pretty name with engine-known BLAS variant for the CPU device.
209/// Gives `"CPU (Accelerate)"` etc. when the relevant feature is
210/// on; falls back to the bare driver-side `Device::name()` when
211/// no BLAS feature is selected.
212pub 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
227// ── Per-device op-support introspection ──────────────────────────
228//
229// Callers that want to dispatch graphs to a particular device need
230// to know up front whether the device's backend has every op the
231// graph uses wired up. Before this API, the only signal was a
232// runtime panic ("not yet implemented"), which forced downstream
233// crates (e.g. `eda-magnetics::graph::pick_device_for`) to bake
234// hand-maintained "what's missing on X" tables into their own
235// source — those drift the moment a backend lands the missing op.
236//
237// [`supports`] consults the backend-side knowledge (CPU is the
238// reference and assumed complete; MLX / Metal each name the ops
239// they don't yet lower) so consumers can ask once and stop
240// re-implementing the table.
241
242/// Is `op` lowerable by the backend for `device` *in this build*?
243///
244/// - CPU is the reference; always returns `true`.
245/// - GPU backends return `false` only for the specific ops/variants
246///   their lowering currently rejects. As backends close gaps, the
247///   matches here shrink and consumers automatically pick them up.
248/// - For devices not feature-gated in, returns `false` (you can't
249///   dispatch to a backend that isn't compiled in regardless).
250pub fn supports(device: Device, op: &Op) -> bool {
251    if !is_available(device) {
252        return false;
253    }
254    match device {
255        Device::Cpu => true, // reference backend; ground truth
256        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        // Other backends not yet characterised here. Conservative:
266        // assume `false` so callers won't dispatch blind; tighten as
267        // each backend grows a `<x>_supports` arm below.
268        _ => false,
269    }
270}
271
272/// Per-op support for the QNN (Hexagon NPU) backend — the ops the FFI runtime
273/// (`rlx_qnn::runtime`) lowers to QNN: MatMul, element-wise binary, a few
274/// activations, plus the structural input/param/constant tensors.
275fn 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/// Per-op heuristic for the native Vulkan backend: native primitive set plus
310/// the op families `legalize_or_rewrite_for_backend` decomposes into it. The
311/// authoritative check is `supports_graph` (runs the real legalize probe);
312/// this is the cheap single-op approximation.
313#[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            // Decomposed to the primitive set by the rewrite pass.
321            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/// Per-op heuristic for the native oneAPI (Level Zero) backend — the same
337/// primitive claim set as rlx-vulkan (the rewrite pass decomposes the rest).
338/// The authoritative check remains `supports_graph` (real legalize probe).
339#[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
361/// Is every op in `graph` lowerable by `device`?
362///
363/// When a backend is registered, uses the same rewrite + legalization probe as
364/// [`legalize_graph_for_device`] (see [`KernelDispatchReport::compile_ready`]).
365/// Otherwise falls back to per-op [`supports`] heuristics.
366pub fn supports_graph(device: Device, graph: &Graph) -> bool {
367    supports_graph_with_options(device, graph, &CompileOptions::default())
368}
369
370/// Like [`supports_graph`] with explicit [`CompileOptions::kernel_dispatch`].
371pub 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
391/// Legalize `graph` for `device` using that backend's claimed [`OpKind`] set.
392///
393/// Applies the same rewrite + legalization path as [`Backend::compile`] (e.g.
394/// CUDA/ROCm rewrites before the legality check). Returns an error when the
395/// backend feature is not enabled or the graph contains unsupported ops.
396///
397/// Does not require a live GPU/TPU driver — only that the backend crate is
398/// compiled in.
399pub 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
404/// Like [`legalize_graph_for_device`] but returns a [`KernelDispatchReport`] for tooling.
405pub 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
412/// Like [`legalize_graph_for_device_with_report`] using [`CompileOptions::kernel_dispatch`]
413/// (and the same rewrite path as [`Backend::compile`]).
414pub 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
442/// Dispatch report for `graph` on `device` without mutating the graph (static common-ir probe).
443pub 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
450/// Like [`dispatch_report_for_device`] with explicit [`CompileOptions::kernel_dispatch`].
451pub 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
466/// First op in `graph` that `device` cannot lower after rewrite, or `None`.
467///
468/// Prefer the backend claim-set probe when registered; otherwise [`supports`].
469pub fn first_unsupported_op(device: Device, graph: &Graph) -> Option<(usize, &Op)> {
470    first_unsupported_op_with_options(device, graph, &CompileOptions::default())
471}
472
473/// Like [`first_unsupported_op`] with explicit [`CompileOptions::kernel_dispatch`].
474pub 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    // After Sin/Cos wiring (forward + backward), MLX's `Activation`
512    // dispatch is complete for every variant in `rlx_ir::Activation`.
513    // Add narrow guards here only when a future Op or Activation
514    // variant lands without an MLX lowering.
515    true
516}
517
518#[allow(unused_variables)]
519fn metal_supports(op: &Op) -> bool {
520    // No characterized gaps for the activations rlx-eda exercises.
521    // The Sin/Cos/Tan/Atan MSL kernels landed in `rlx-metal/src/kernels.rs`
522    // (`{sin,cos,tan,atan}_inplace`) alongside the dispatch slots in
523    // `backend.rs:1764`. Narrow this back down if a future Op or
524    // Activation variant lands without a Metal kernel.
525    let _ = op;
526    true
527}
528
529/// CoreML / ANE lowers a fixed, declared op set (see `rlx_coreml::mil`).
530/// Unlike the GPU backends — whose lowering covers the whole IR surface —
531/// CoreML is an inference compiler with a finite op claim, so we check
532/// membership directly against the backend's published list.
533///
534/// Under the `training` feature the claim also covers the backward ops that the
535/// legalize/rewrite pass decomposes into supported primitives (or, once landed,
536/// lower through native MIL backward kernels) — so device selection picks
537/// `Device::Ane` for autodiff-produced backward graphs. See
538/// [`crate::backend::COREML_BACKWARD_OPS`].
539fn 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    // CUDA / ROCm / wgpu share the same IR surface area as CPU for the
556    // ops V-JEPA2 and other vision models exercise. Narrow when a backend
557    // reports a concrete lowering gap.
558    let _ = op;
559    true
560}
561
562/// Block until `device`'s queue is idle. Metal drains the global queue;
563/// other backends are no-ops.
564pub 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        // OpenGl isn't a workspace feature; should report false.
596        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        // After the {sin,cos,tan,atan}_inplace MSL kernels landed in
603        // rlx-metal/src/kernels.rs, Metal has every Activation variant
604        // rlx-eda exercises.
605        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        // CPU always supports.
627        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}