Skip to main content

burn_dispatch/
backend.rs

1use alloc::format;
2use alloc::string::String;
3use alloc::vec::Vec;
4#[cfg(cube_backend)]
5use burn_backend::cubecl::{Device as CubeDevice, RuntimeId};
6
7// The cubecl runtimes — `cpu` among them — enumerate through `cube_devices` rather than a
8// `vec![]` literal, so only the backends that still list a fixed device need this.
9#[cfg(any(feature = "ndarray", feature = "flex"))]
10use alloc::vec;
11
12#[cfg(feature = "autodiff")]
13use burn_backend::distributed::{DistributedParamId, DistributedParams};
14use burn_backend::{
15    AutodiffBackend, Backend, BackendGraph, BackendTypes, DType, ExecutionError, MemoryPoolUsage,
16    ProfileDuration, ProfileOptions, ProfileToken, SlicedPoolReport,
17};
18
19/// A captured graph from one of the dispatched backends (see
20/// [`BackendTypes::GraphPrimitive`]).
21///
22/// Like [`DispatchTensorKind`], one variant per enabled backend: the graph is
23/// captured by, and can only replay on, the backend it was recorded on.
24#[derive(Debug, Clone)]
25pub enum DispatchGraph {
26    #[cfg(not(backend_enabled))]
27    #[doc(hidden)]
28    Unavailable(crate::NoBackend),
29    /// A graph captured on the [cubecl backend](Cube).
30    #[cfg(cube_backend)]
31    Cube(BackendGraph<Cube>),
32
33    /// A graph captured on the [Flex backend](Flex).
34    #[cfg(feature = "flex")]
35    Flex(BackendGraph<Flex>),
36
37    /// A graph captured on the [NdArray backend](NdArray).
38    #[cfg(feature = "ndarray")]
39    NdArray(BackendGraph<NdArray>),
40
41    /// A graph captured on the [LibTorch backend](LibTorch).
42    #[cfg(feature = "tch")]
43    LibTorch(BackendGraph<LibTorch>),
44
45    /// A graph captured on the [Remote backend](Remote).
46    #[cfg(feature = "remote")]
47    Remote(BackendGraph<Remote>),
48    /// A graph captured by the non-executing capture backend.
49    #[cfg(feature = "capture")]
50    Capture(BackendGraph<Capture>),
51}
52
53/// The error returned when a graph operation cannot be dispatched.
54fn graph_dispatch_err(reason: alloc::string::String) -> ExecutionError {
55    ExecutionError::WithContext { reason }
56}
57
58/// Match arm generator for [`Backend::graph_stop_capture`] on [`Dispatch`]:
59/// each backend's captured graph is wrapped in its [`DispatchGraph`] variant.
60macro_rules! graph_stop_capture_arms {
61    ($device:expr; $([$Backend:ident, $cfg:meta]),*) => {
62        match $device {
63            $(
64                #[cfg($cfg)]
65                $crate::DispatchDevice::$Backend(device) => {
66                    <$crate::backends::$Backend as Backend>::graph_stop_capture(device)
67                        .map(DispatchGraph::$Backend)
68                }
69            )*
70            #[allow(unreachable_patterns)]
71            other => Err(graph_dispatch_err(format!(
72                "Graph capture is not supported for device {other:?}"
73            ))),
74        }
75    };
76}
77
78/// Match arm generator for [`Backend::graph_replay`] on [`Dispatch`]: the graph
79/// variant must match the device's backend, since a graph only replays on the
80/// backend that captured it.
81macro_rules! graph_replay_arms {
82    ($device:expr, $graph:expr; $([$Backend:ident, $cfg:meta]),*) => {
83        match ($device, $graph) {
84            $(
85                #[cfg($cfg)]
86                ($crate::DispatchDevice::$Backend(device), DispatchGraph::$Backend(graph)) => {
87                    // Safety: forwarded verbatim from `Dispatch::graph_replay`'s
88                    // own contract.
89                    unsafe {
90                        <$crate::backends::$Backend as Backend>::graph_replay(device, graph)
91                    }
92                }
93            )*
94            #[allow(unreachable_patterns)]
95            (device, _) => Err(graph_dispatch_err(format!(
96                "The graph was not captured on the backend of device {device:?}"
97            ))),
98        }
99    };
100}
101
102#[cfg(feature = "autodiff")]
103macro_rules! is_tracked_arms {
104    ($tensor:expr; $([$Backend:ident, $cfg:meta]),*) => {
105        match &$tensor.kind {
106            DispatchTensorKind::Autodiff(inner) => match &**inner {
107                $(
108                    #[cfg($cfg)]
109                    DispatchTensorKind::$Backend(tensor) => tensor.as_autodiff().is_tracked(),
110                )*
111                DispatchTensorKind::Autodiff(_) => {
112                    unreachable!("Autodiff should not wrap an autodiff tensor")
113                }
114                #[allow(unreachable_patterns)]
115                _ => false,
116            },
117            _ => false,
118        }
119    };
120}
121
122#[cfg(all(feature = "autodiff", executing_backend))]
123use alloc::boxed::Box;
124#[cfg(feature = "autodiff")]
125use burn_autodiff::grads::Gradients;
126
127#[cfg(feature = "autodiff")]
128use crate::DispatchAutodiffContext;
129#[allow(unused)]
130use crate::DispatchDeviceId;
131#[allow(unused)]
132use crate::DispatchTensorKind;
133#[cfg(feature = "flex")]
134use crate::devices::FlexDevice;
135#[cfg(feature = "tch")]
136use crate::devices::LibTorchDevice;
137#[cfg(feature = "ndarray")]
138use crate::devices::NdArrayDevice;
139use crate::{DispatchDevice, DispatchTensor, backends::*};
140
141/// The main execution backend in Burn.
142///
143/// [`Dispatch`] acts as a global backend that can manage multiple underlying
144/// backends (e.g., `Cpu`, `Cuda`, `Wgpu`, `Metal`, etc.).
145/// It is responsible for:
146/// - Dispatching tensor operations to the appropriate backend.
147/// - Managing cross-backend tensor transfers.
148///
149/// Essentially, [`Dispatch`] is the single entry point for executing tensor operations
150/// in a backend-agnostic way. It allows Burn to provide a unified, global backend
151/// for users while still leveraging multiple specialized backends under the hood.
152///
153/// # Example
154///
155/// ```ignore
156/// use burn::Dispatch;
157/// use burn::DispatchDevice;
158///
159/// // Select the device to execute operations on
160/// let device = DispatchDevice::Cube(cubecl::Device::Cuda(Default::default()));
161///
162/// // Create a tensor using the global backend
163/// let t = Tensor::<Dispatch, 2>::zeros([128, 128], &device);
164/// ```
165#[derive(Debug, Default, Clone)]
166pub struct Dispatch;
167
168#[cfg(feature = "autodiff")]
169impl Dispatch {
170    /// Returns whether an autodiff tensor's node is marked for graph participation.
171    #[doc(hidden)]
172    pub fn is_tracked(tensor: &DispatchTensor) -> bool {
173        backend_list!(is_tracked_arms, tensor)
174    }
175}
176
177impl BackendTypes for Dispatch {
178    type Device = DispatchDevice;
179
180    type FloatTensorPrimitive = DispatchTensor;
181    type IntTensorPrimitive = DispatchTensor;
182    type BoolTensorPrimitive = DispatchTensor;
183    type QuantizedTensorPrimitive = DispatchTensor;
184
185    type GraphPrimitive = DispatchGraph;
186}
187
188impl Backend for Dispatch {
189    fn name(device: &Self::Device) -> String {
190        let inner = dispatch_device!(device, |device| B::name(device));
191        format!("dispatch<{inner}>")
192    }
193
194    fn seed(device: &Self::Device, seed: u64) {
195        dispatch_device!(device, |device| B::seed(device, seed))
196    }
197
198    fn sync(device: &Self::Device) -> Result<(), ExecutionError> {
199        dispatch_device!(device, |device| B::sync(device))
200    }
201
202    fn profile<O: Send + 'static>(
203        device: &Self::Device,
204        options: ProfileOptions,
205        func: impl FnOnce() -> O + Send,
206    ) -> Result<(O, ProfileDuration), ExecutionError> {
207        dispatch_device!(device, |device| B::profile(device, options, func))
208    }
209
210    fn profile_start(device: &Self::Device) -> Result<Option<ProfileToken>, ExecutionError> {
211        dispatch_device!(device, |device| B::profile_start(device))
212    }
213
214    fn profile_end(
215        device: &Self::Device,
216        token: ProfileToken,
217        options: ProfileOptions,
218    ) -> Result<ProfileDuration, ExecutionError> {
219        dispatch_device!(device, |device| B::profile_end(device, token, options))
220    }
221
222    fn profile_abandon(device: &Self::Device, token: ProfileToken) {
223        dispatch_device!(device, |device| B::profile_abandon(device, token))
224    }
225
226    fn graph_prepare(device: &Self::Device) -> Result<(), ExecutionError> {
227        dispatch_device!(device, |device| B::graph_prepare(device))
228    }
229
230    fn graph_start_capture(device: &Self::Device) -> Result<(), ExecutionError> {
231        dispatch_device!(device, |device| B::graph_start_capture(device))
232    }
233
234    fn graph_stop_capture(device: &Self::Device) -> Result<DispatchGraph, ExecutionError> {
235        backend_list!(graph_stop_capture_arms, device)
236    }
237
238    unsafe fn graph_replay(
239        device: &Self::Device,
240        graph: &DispatchGraph,
241    ) -> Result<(), ExecutionError> {
242        backend_list!(graph_replay_arms, device, graph)
243    }
244
245    fn dtype_usage(device: &Self::Device, dtype: DType) -> burn_backend::DTypeUsageSet {
246        dispatch_device!(device, |device| B::dtype_usage(device, dtype))
247    }
248
249    fn ad_enabled(device: &Self::Device) -> bool {
250        match device {
251            #[cfg(feature = "autodiff")]
252            DispatchDevice::Autodiff(_) => true,
253            _ => false,
254        }
255    }
256
257    fn device_count(type_id: u16) -> usize {
258        let (dispatch_id, backend_type_id) = DispatchDevice::decode_type_id(type_id);
259        match dispatch_id {
260            #[cfg(cube_backend)]
261            DispatchDeviceId::Cube => Cube::device_count(backend_type_id),
262            #[cfg(feature = "flex")]
263            DispatchDeviceId::Flex => Flex::device_count(backend_type_id),
264            #[cfg(feature = "ndarray")]
265            DispatchDeviceId::NdArray => NdArray::device_count(backend_type_id),
266            #[cfg(feature = "tch")]
267            DispatchDeviceId::LibTorch => LibTorch::device_count(backend_type_id),
268            #[cfg(feature = "remote")]
269            DispatchDeviceId::Remote => Remote::device_count(backend_type_id),
270            #[cfg(feature = "capture")]
271            DispatchDeviceId::Capture => Capture::device_count(backend_type_id),
272            _ => unreachable!("No backend feature enabled."),
273        }
274    }
275
276    fn memory_persistent_allocations<
277        Output: Send,
278        Input: Send,
279        Func: Fn(Input) -> Output + Send,
280    >(
281        device: &Self::Device,
282        input: Input,
283        func: Func,
284    ) -> Output {
285        dispatch_device!(device, |device| B::memory_persistent_allocations(
286            device, input, func
287        ))
288    }
289
290    fn memory_cleanup(device: &Self::Device) {
291        dispatch_device!(device, |device| B::memory_cleanup(device))
292    }
293
294    fn memory_pool_report(device: &Self::Device) -> Option<Vec<SlicedPoolReport>> {
295        dispatch_device!(device, |device| B::memory_pool_report(device))
296    }
297
298    fn memory_pool_usage(device: &Self::Device) -> Option<MemoryPoolUsage> {
299        dispatch_device!(device, |device| B::memory_pool_usage(device))
300    }
301
302    fn staging<'a, Iter>(data: Iter, device: &Self::Device)
303    where
304        Iter: Iterator<Item = &'a mut burn_backend::TensorData>,
305    {
306        dispatch_device!(device, |device| B::staging(data, device))
307    }
308
309    fn supports_dtype(device: &Self::Device, dtype: DType) -> bool {
310        dispatch_device!(device, |device| B::supports_dtype(device, dtype))
311    }
312
313    fn flush(device: &Self::Device) -> Result<(), ExecutionError> {
314        dispatch_device!(device, |device| B::flush(device))
315    }
316}
317
318#[cfg(feature = "autodiff")]
319fn disable_autodiff_context(context: DispatchAutodiffContext) -> DispatchAutodiffContext {
320    assert!(
321        matches!(context, DispatchAutodiffContext::Enabled(_)),
322        "tensor is already on the inner backend"
323    );
324    DispatchAutodiffContext::Disabled
325}
326
327#[cfg(feature = "autodiff")]
328fn enable_autodiff_context(context: DispatchAutodiffContext) -> DispatchAutodiffContext {
329    assert_eq!(
330        context,
331        DispatchAutodiffContext::Disabled,
332        "tensor is already associated with autodiff"
333    );
334    DispatchAutodiffContext::Enabled(crate::GradientCheckpointingStrategy::Disabled)
335}
336
337#[cfg(feature = "autodiff")]
338// Capture does not support autodiff. Without an execution backend, the dispatch
339// arms below only panic, leaving gradient arguments unused and return code unreachable.
340#[cfg_attr(not(executing_backend), allow(unused_variables, unreachable_code))]
341impl AutodiffBackend for Dispatch {
342    type InnerBackend = Dispatch;
343
344    type Gradients = Gradients;
345
346    fn backward(tensor: DispatchTensor) -> Self::Gradients {
347        let DispatchTensor { kind, .. } = tensor;
348
349        match kind {
350            DispatchTensorKind::Autodiff(tensor) => match *tensor {
351                #[cfg(not(backend_enabled))]
352                DispatchTensorKind::Unavailable(never) => never.unreachable(),
353                #[cfg(cube_backend)]
354                DispatchTensorKind::Cube(tensor) => tensor.autodiff().backward(),
355                #[cfg(feature = "flex")]
356                DispatchTensorKind::Flex(tensor) => tensor.autodiff().backward(),
357                #[cfg(feature = "ndarray")]
358                DispatchTensorKind::NdArray(tensor) => tensor.autodiff().backward(),
359                #[cfg(feature = "tch")]
360                DispatchTensorKind::LibTorch(tensor) => tensor.autodiff().backward(),
361                #[cfg(feature = "remote")]
362                DispatchTensorKind::Remote(tensor) => tensor.autodiff().backward(),
363                #[cfg(feature = "capture")]
364                DispatchTensorKind::Capture(_) => {
365                    panic!("Capture tensors do not support autodiff")
366                }
367                DispatchTensorKind::Autodiff(_) => {
368                    panic!("Autodiff should not wrap an autodiff tensor.")
369                }
370            },
371            _ => panic!("Requires autodiff tensor."),
372        }
373    }
374
375    fn grad(tensor: &DispatchTensor, grads: &Self::Gradients) -> Option<DispatchTensor> {
376        let DispatchTensor { kind, .. } = tensor;
377        let grad: Option<DispatchTensorKind> = match &kind {
378            DispatchTensorKind::Autodiff(inner_kind) => match &**inner_kind {
379                #[cfg(not(backend_enabled))]
380                DispatchTensorKind::Unavailable(never) => never.unreachable(),
381                #[cfg(cube_backend)]
382                DispatchTensorKind::Cube(tensor) => tensor
383                    .as_autodiff()
384                    .grad(grads)
385                    .map(|t| DispatchTensorKind::Cube(crate::BackendTensor::Float(t))),
386                #[cfg(feature = "flex")]
387                DispatchTensorKind::Flex(tensor) => tensor
388                    .as_autodiff()
389                    .grad(grads)
390                    .map(|t| DispatchTensorKind::Flex(crate::BackendTensor::Float(t))),
391                #[cfg(feature = "ndarray")]
392                DispatchTensorKind::NdArray(tensor) => tensor
393                    .as_autodiff()
394                    .grad(grads)
395                    .map(|t| DispatchTensorKind::NdArray(crate::BackendTensor::Float(t))),
396                #[cfg(feature = "tch")]
397                DispatchTensorKind::LibTorch(tensor) => tensor
398                    .as_autodiff()
399                    .grad(grads)
400                    .map(|t| DispatchTensorKind::LibTorch(crate::BackendTensor::Float(t))),
401                #[cfg(feature = "remote")]
402                DispatchTensorKind::Remote(tensor) => tensor
403                    .as_autodiff()
404                    .grad(grads)
405                    .map(|t| DispatchTensorKind::Remote(crate::BackendTensor::Float(t))),
406                #[cfg(feature = "capture")]
407                DispatchTensorKind::Capture(_) => {
408                    panic!("Capture tensors do not support autodiff")
409                }
410                DispatchTensorKind::Autodiff(_) => {
411                    panic!("Autodiff should not wrap an autodiff tensor.")
412                }
413            },
414            _ => panic!("Requires autodiff tensor."),
415        };
416        grad.map(|kind| DispatchTensor {
417            kind,
418            autodiff: DispatchAutodiffContext::Disabled,
419        })
420    }
421
422    fn grad_remove(tensor: &DispatchTensor, grads: &mut Self::Gradients) -> Option<DispatchTensor> {
423        let DispatchTensor { kind, .. } = tensor;
424        let grad: Option<DispatchTensorKind> = match &kind {
425            DispatchTensorKind::Autodiff(inner_kind) => match &**inner_kind {
426                #[cfg(not(backend_enabled))]
427                DispatchTensorKind::Unavailable(never) => never.unreachable(),
428                #[cfg(cube_backend)]
429                DispatchTensorKind::Cube(tensor) => tensor
430                    .as_autodiff()
431                    .grad_remove(grads)
432                    .map(|t| DispatchTensorKind::Cube(crate::BackendTensor::Float(t))),
433                #[cfg(feature = "flex")]
434                DispatchTensorKind::Flex(tensor) => tensor
435                    .as_autodiff()
436                    .grad_remove(grads)
437                    .map(|t| DispatchTensorKind::Flex(crate::BackendTensor::Float(t))),
438                #[cfg(feature = "ndarray")]
439                DispatchTensorKind::NdArray(tensor) => tensor
440                    .as_autodiff()
441                    .grad_remove(grads)
442                    .map(|t| DispatchTensorKind::NdArray(crate::BackendTensor::Float(t))),
443                #[cfg(feature = "tch")]
444                DispatchTensorKind::LibTorch(tensor) => tensor
445                    .as_autodiff()
446                    .grad_remove(grads)
447                    .map(|t| DispatchTensorKind::LibTorch(crate::BackendTensor::Float(t))),
448                #[cfg(feature = "remote")]
449                DispatchTensorKind::Remote(tensor) => tensor
450                    .as_autodiff()
451                    .grad_remove(grads)
452                    .map(|t| DispatchTensorKind::Remote(crate::BackendTensor::Float(t))),
453                #[cfg(feature = "capture")]
454                DispatchTensorKind::Capture(_) => {
455                    panic!("Capture tensors do not support autodiff")
456                }
457                DispatchTensorKind::Autodiff(_) => {
458                    panic!("Autodiff should not wrap an autodiff tensor.")
459                }
460            },
461            _ => panic!("Requires autodiff tensor."),
462        };
463        grad.map(|kind| DispatchTensor {
464            kind,
465            autodiff: DispatchAutodiffContext::Disabled,
466        })
467    }
468
469    fn grad_replace(tensor: &DispatchTensor, grads: &mut Self::Gradients, grad: DispatchTensor) {
470        let DispatchTensor { kind, .. } = tensor;
471        let DispatchTensor {
472            kind: grad,
473            autodiff,
474        } = grad;
475        assert_eq!(
476            autodiff,
477            DispatchAutodiffContext::Disabled,
478            "replacement gradients must use the inner backend"
479        );
480
481        match &kind {
482            DispatchTensorKind::Autodiff(inner_kind) => match (&**inner_kind, grad) {
483                #[cfg(cube_backend)]
484                (DispatchTensorKind::Cube(tensor), DispatchTensorKind::Cube(grad)) => {
485                    tensor.as_autodiff().grad_replace(grads, grad.float())
486                }
487                #[cfg(feature = "flex")]
488                (DispatchTensorKind::Flex(tensor), DispatchTensorKind::Flex(grad)) => {
489                    tensor.as_autodiff().grad_replace(grads, grad.float())
490                }
491                #[cfg(feature = "ndarray")]
492                (DispatchTensorKind::NdArray(tensor), DispatchTensorKind::NdArray(grad)) => {
493                    tensor.as_autodiff().grad_replace(grads, grad.float())
494                }
495                #[cfg(feature = "remote")]
496                (DispatchTensorKind::Remote(tensor), DispatchTensorKind::Remote(grad)) => {
497                    tensor.as_autodiff().grad_replace(grads, grad.float())
498                }
499                (DispatchTensorKind::Autodiff(_), _) => {
500                    panic!("Autodiff should not wrap an autodiff tensor.")
501                }
502                // TODO: distributed message?
503                (t, g) => panic!(
504                    "The provided tensors are not on the same backend. Got backends {t:?} and {g:?}."
505                ),
506            },
507            _ => panic!("Requires autodiff tensor."),
508        }
509    }
510
511    fn inner(tensor: DispatchTensor) -> DispatchTensor {
512        let DispatchTensor { kind, autodiff } = tensor;
513        assert!(
514            matches!(autodiff, DispatchAutodiffContext::Enabled(_)),
515            "Requires autodiff tensor."
516        );
517
518        let kind = match kind {
519            DispatchTensorKind::Autodiff(inner_kind) => match *inner_kind {
520                #[cfg(not(backend_enabled))]
521                DispatchTensorKind::Unavailable(ref never) => never.unreachable(),
522                #[cfg(cube_backend)]
523                DispatchTensorKind::Cube(tensor) => DispatchTensorKind::Cube(
524                    crate::BackendTensor::Float(tensor.autodiff().into_primitive()),
525                ),
526                #[cfg(feature = "flex")]
527                DispatchTensorKind::Flex(tensor) => DispatchTensorKind::Flex(
528                    crate::BackendTensor::Float(tensor.autodiff().into_primitive()),
529                ),
530                #[cfg(feature = "ndarray")]
531                DispatchTensorKind::NdArray(tensor) => DispatchTensorKind::NdArray(
532                    crate::BackendTensor::Float(tensor.autodiff().into_primitive()),
533                ),
534                #[cfg(feature = "tch")]
535                DispatchTensorKind::LibTorch(tensor) => DispatchTensorKind::LibTorch(
536                    crate::BackendTensor::Float(tensor.autodiff().into_primitive()),
537                ),
538                #[cfg(feature = "remote")]
539                DispatchTensorKind::Remote(tensor) => DispatchTensorKind::Remote(
540                    crate::BackendTensor::Float(tensor.autodiff().into_primitive()),
541                ),
542                #[cfg(feature = "capture")]
543                DispatchTensorKind::Capture(_) => {
544                    panic!("Capture tensors do not support autodiff")
545                }
546                DispatchTensorKind::Autodiff(_) => {
547                    panic!("Autodiff should not wrap an autodiff tensor.")
548                }
549            },
550            _ => panic!("Requires autodiff tensor."),
551        };
552        DispatchTensor {
553            kind,
554            autodiff: DispatchAutodiffContext::Disabled,
555        }
556    }
557
558    fn int_inner(mut tensor: DispatchTensor) -> DispatchTensor {
559        tensor.autodiff = disable_autodiff_context(tensor.autodiff);
560        tensor
561    }
562
563    fn bool_inner(mut tensor: DispatchTensor) -> DispatchTensor {
564        tensor.autodiff = disable_autodiff_context(tensor.autodiff);
565        tensor
566    }
567
568    fn q_inner(mut tensor: DispatchTensor) -> DispatchTensor {
569        tensor.autodiff = disable_autodiff_context(tensor.autodiff);
570        tensor
571    }
572
573    fn from_inner(tensor: DispatchTensor) -> DispatchTensor {
574        let DispatchTensor { kind, autodiff } = tensor;
575        assert_eq!(
576            autodiff,
577            DispatchAutodiffContext::Disabled,
578            "tensor is already associated with autodiff"
579        );
580
581        let kind = match kind {
582            #[cfg(cube_backend)]
583            DispatchTensorKind::Cube(tensor) => {
584                DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::Cube(
585                    crate::BackendTensor::Autodiff(Autodiff::<Cube>::from_inner(tensor.float())),
586                )))
587            }
588            #[cfg(feature = "flex")]
589            DispatchTensorKind::Flex(tensor) => {
590                DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::Flex(
591                    crate::BackendTensor::Autodiff(Autodiff::<Flex>::from_inner(tensor.float())),
592                )))
593            }
594            #[cfg(feature = "ndarray")]
595            DispatchTensorKind::NdArray(tensor) => {
596                DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::NdArray(
597                    crate::BackendTensor::Autodiff(Autodiff::<NdArray>::from_inner(tensor.float())),
598                )))
599            }
600            #[cfg(feature = "tch")]
601            DispatchTensorKind::LibTorch(tensor) => DispatchTensorKind::Autodiff(Box::new(
602                DispatchTensorKind::LibTorch(crate::BackendTensor::Autodiff(
603                    Autodiff::<LibTorch>::from_inner(tensor.float()),
604                )),
605            )),
606            #[cfg(feature = "remote")]
607            DispatchTensorKind::Remote(tensor) => {
608                DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::Remote(
609                    crate::BackendTensor::Autodiff(Autodiff::<Remote>::from_inner(tensor.float())),
610                )))
611            }
612            #[cfg(feature = "capture")]
613            DispatchTensorKind::Capture(_) => {
614                panic!("Capture tensors do not support autodiff")
615            }
616            DispatchTensorKind::Autodiff(_) => {
617                panic!("Autodiff should not wrap an autodiff tensor.")
618            }
619        };
620
621        DispatchTensor {
622            kind,
623            autodiff: DispatchAutodiffContext::Enabled(
624                crate::GradientCheckpointingStrategy::Disabled,
625            ),
626        }
627    }
628
629    fn int_from_inner(mut tensor: DispatchTensor) -> DispatchTensor {
630        tensor.autodiff = enable_autodiff_context(tensor.autodiff);
631        tensor
632    }
633
634    fn bool_from_inner(mut tensor: DispatchTensor) -> DispatchTensor {
635        tensor.autodiff = enable_autodiff_context(tensor.autodiff);
636        tensor
637    }
638
639    fn q_from_inner(mut tensor: DispatchTensor) -> DispatchTensor {
640        tensor.autodiff = enable_autodiff_context(tensor.autodiff);
641        tensor
642    }
643
644    // Only the collective-capable backends (Cube/Remote) carry distributed params; in builds
645    // without them the match arms cfg out, leaving the bindings unused and the tail unreachable.
646    #[allow(unused_variables, unreachable_code)]
647    fn set_distributed_params(
648        tensor: DispatchTensor,
649        param_id: DistributedParamId,
650    ) -> DispatchTensor {
651        let DispatchTensor { kind, autodiff } = tensor;
652        assert!(
653            matches!(autodiff, DispatchAutodiffContext::Enabled(_)),
654            "Requires autodiff tensor."
655        );
656
657        let kind = match kind {
658            DispatchTensorKind::Autodiff(inner_kind) => match *inner_kind {
659                #[cfg(not(backend_enabled))]
660                DispatchTensorKind::Unavailable(ref never) => never.unreachable(),
661                #[cfg(cube_backend)]
662                DispatchTensorKind::Cube(tensor) => {
663                    DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::Cube(
664                        crate::BackendTensor::Autodiff(Autodiff::<Cube>::set_distributed_params(
665                            tensor.as_autodiff().clone(),
666                            param_id,
667                        )),
668                    )))
669                }
670                #[cfg(feature = "remote")]
671                DispatchTensorKind::Remote(tensor) => {
672                    DispatchTensorKind::Autodiff(Box::new(DispatchTensorKind::Remote(
673                        crate::BackendTensor::Autodiff(Autodiff::<Remote>::set_distributed_params(
674                            tensor.as_autodiff().clone(),
675                            param_id,
676                        )),
677                    )))
678                }
679                DispatchTensorKind::Autodiff(_) => {
680                    panic!("Autodiff should not wrap an autodiff tensor.")
681                }
682                #[allow(unreachable_patterns)]
683                other => {
684                    panic!("Distributed operations are not supported for tensor kind {other:?}")
685                }
686            },
687            _ => panic!("Requires autodiff tensor."),
688        };
689
690        DispatchTensor { kind, autodiff }
691    }
692
693    #[allow(unused_variables)]
694    fn distributed_params(tensor: &DispatchTensor) -> Option<DistributedParams> {
695        let DispatchTensor { kind, autodiff: _ } = tensor;
696
697        match &kind {
698            DispatchTensorKind::Autodiff(inner_kind) => match &**inner_kind {
699                #[cfg(not(backend_enabled))]
700                DispatchTensorKind::Unavailable(never) => never.unreachable(),
701                #[cfg(cube_backend)]
702                DispatchTensorKind::Cube(tensor) => {
703                    Autodiff::<Cube>::distributed_params(tensor.as_autodiff())
704                }
705                #[cfg(feature = "remote")]
706                DispatchTensorKind::Remote(tensor) => {
707                    Autodiff::<Remote>::distributed_params(tensor.as_autodiff())
708                }
709
710                DispatchTensorKind::Autodiff(_) => {
711                    panic!("Autodiff should not wrap an autodiff tensor.")
712                }
713                // Backends without distributed support never carry distributed params.
714                #[allow(unreachable_patterns)]
715                _ => None,
716            },
717            _ => panic!("Requires autodiff tensor."),
718        }
719    }
720
721    #[allow(unused_variables)]
722    fn is_distributed(tensor: &DispatchTensor) -> bool {
723        let DispatchTensor { kind, autodiff: _ } = tensor;
724
725        match &kind {
726            DispatchTensorKind::Autodiff(inner_kind) => match &**inner_kind {
727                #[cfg(not(backend_enabled))]
728                DispatchTensorKind::Unavailable(never) => never.unreachable(),
729                #[cfg(cube_backend)]
730                DispatchTensorKind::Cube(tensor) => {
731                    Autodiff::<Cube>::is_distributed(tensor.as_autodiff())
732                }
733                #[cfg(feature = "remote")]
734                DispatchTensorKind::Remote(tensor) => {
735                    Autodiff::<Remote>::is_distributed(tensor.as_autodiff())
736                }
737
738                DispatchTensorKind::Autodiff(_) => {
739                    panic!("Autodiff should not wrap an autodiff tensor.")
740                }
741                // Backends without distributed support are never distributed.
742                #[allow(unreachable_patterns)]
743                _ => false,
744            },
745            _ => panic!("Requires autodiff tensor."),
746        }
747    }
748}
749
750#[cfg(all(test, feature = "autodiff", feature = "flex"))]
751mod autodiff_context_tests {
752    use super::*;
753    use crate::{DispatchAutodiffContext, DispatchTensorKind, GradientCheckpointingStrategy};
754    use alloc::vec;
755    use burn_backend::{
756        TensorData, TensorMetadata, TensorPrimitive,
757        ops::{BoolTensorOps, FloatTensorOps, IntTensorOps, ModuleOps, QTensorOps},
758        quantization::QuantScheme,
759        tensor::{FloatTensor, IntTensor},
760    };
761    use burn_backend_extension::backend_dispatch;
762
763    #[backend_dispatch]
764    impl Dispatch {
765        fn direct_float(tensor: FloatTensor<Self>) -> FloatTensor<Self> {
766            tensor
767        }
768
769        fn conditional_float_route(
770            int: IntTensor<Self>,
771            float: Option<FloatTensor<Self>>,
772        ) -> IntTensor<Self> {
773            // `optional_float_only_uses_autodiff_when_present` exercises both routes: an enabled
774            // int alone uses the concrete backend, while a present float selects `Autodiff<B>`.
775            assert_eq!(float.is_some(), B::ad_enabled(&int.device()));
776            int
777        }
778
779        fn optional_routing_tensor(
780            first: Option<FloatTensor<Self>>,
781            second: Option<FloatTensor<Self>>,
782        ) -> FloatTensor<Self> {
783            first.or(second).expect("test requires one tensor")
784        }
785
786        fn vector_routing_tensor(
787            first: Vec<IntTensor<Self>>,
788            second: Vec<IntTensor<Self>>,
789        ) -> IntTensor<Self> {
790            first
791                .into_iter()
792                .chain(second)
793                .next()
794                .expect("test requires one tensor")
795        }
796    }
797
798    fn device(strategy: GradientCheckpointingStrategy) -> DispatchDevice {
799        DispatchDevice::autodiff_with_gradient_checkpointing(
800            DispatchDevice::Flex(Default::default()),
801            strategy,
802        )
803    }
804
805    fn inner_device() -> DispatchDevice {
806        DispatchDevice::Flex(Default::default())
807    }
808
809    fn float(values: [f32; 2], device: &DispatchDevice) -> DispatchTensor {
810        Dispatch::float_from_data(TensorData::from(values), device)
811    }
812
813    fn float_2d(values: [[f32; 2]; 2], device: &DispatchDevice) -> DispatchTensor {
814        Dispatch::float_from_data(TensorData::from(values), device)
815    }
816
817    fn assert_enabled_float(tensor: &DispatchTensor, strategy: GradientCheckpointingStrategy) {
818        assert_eq!(tensor.autodiff, DispatchAutodiffContext::Enabled(strategy));
819        assert!(matches!(tensor.kind, DispatchTensorKind::Autodiff(_)));
820        let DispatchDevice::Autodiff(device) = tensor.device() else {
821            panic!("enabled float should report an autodiff device")
822        };
823        assert_eq!(device.checkpointing, strategy);
824    }
825
826    #[test]
827    fn enabled_float_creation_is_untracked_for_both_strategies() {
828        for strategy in [
829            GradientCheckpointingStrategy::Disabled,
830            GradientCheckpointingStrategy::Balanced,
831        ] {
832            let tensor = float([1.0, 2.0], &device(strategy));
833            assert_enabled_float(&tensor, strategy);
834            assert!(!Dispatch::float_is_require_grad(&tensor));
835        }
836    }
837
838    #[test]
839    fn enabled_int_bool_and_float_conversions_preserve_association() {
840        let strategy = GradientCheckpointingStrategy::Balanced;
841        let device = device(strategy);
842        let int = Dispatch::int_from_data(TensorData::from([1i32, 2]), &device);
843        assert_eq!(int.autodiff, DispatchAutodiffContext::Enabled(strategy));
844        assert!(!matches!(int.kind, DispatchTensorKind::Autodiff(_)));
845        assert!(matches!(int.device(), DispatchDevice::Autodiff(_)));
846
847        let converted = Dispatch::int_into_float(int, burn_backend::FloatDType::F32);
848        assert_enabled_float(&converted, strategy);
849
850        let boolean = Dispatch::bool_from_data(TensorData::from([true, false]), &device);
851        assert_eq!(boolean.autodiff, DispatchAutodiffContext::Enabled(strategy));
852        assert!(!matches!(boolean.kind, DispatchTensorKind::Autodiff(_)));
853        let converted = Dispatch::bool_into_float(boolean, burn_backend::FloatDType::F32);
854        assert_enabled_float(&converted, strategy);
855    }
856
857    #[test]
858    fn uniform_contexts_propagate_through_fixed_arity_and_vector_ops() {
859        let strategy = GradientCheckpointingStrategy::Disabled;
860        let lhs = float([1.0, 2.0], &device(strategy));
861        let rhs = float([3.0, 4.0], &device(strategy));
862
863        let added = Dispatch::float_add(lhs.clone(), rhs.clone());
864        assert_enabled_float(&added, strategy);
865
866        let concatenated = Dispatch::float_cat(vec![lhs.clone(), rhs], 0);
867        assert_enabled_float(&concatenated, strategy);
868
869        let normalized = Dispatch::layer_norm(
870            lhs,
871            float([1.0, 1.0], &device(strategy)),
872            Some(float([0.0, 0.0], &device(strategy))),
873            1e-5,
874        );
875        assert_enabled_float(&normalized, strategy);
876    }
877
878    #[test]
879    fn optional_and_vector_routing_candidates_skip_empty_inputs() {
880        let strategy = GradientCheckpointingStrategy::Balanced;
881        let float = float([1.0, 2.0], &device(strategy));
882        let output = Dispatch::optional_routing_tensor(None, Some(float));
883        assert_enabled_float(&output, strategy);
884
885        let int = Dispatch::int_from_data(TensorData::from([1i32, 2]), &device(strategy));
886        let output = Dispatch::vector_routing_tensor(vec![], vec![int]);
887        assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
888    }
889
890    #[test]
891    fn optional_float_only_uses_autodiff_when_present() {
892        let strategy = GradientCheckpointingStrategy::Balanced;
893        let int = Dispatch::int_from_data(TensorData::from([1i32, 2]), &device(strategy));
894        let output = Dispatch::conditional_float_route(int, None);
895        assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
896
897        let int = Dispatch::int_from_data(TensorData::from([1i32, 2]), &inner_device());
898        let float = float([1.0, 2.0], &device(strategy));
899        let output = Dispatch::conditional_float_route(int, Some(float));
900        assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
901    }
902
903    #[test]
904    #[should_panic(expected = "an enabled float tensor must use an autodiff primitive")]
905    fn direct_routing_tensor_validates_its_context() {
906        let mut malformed = float([1.0, 2.0], &inner_device());
907        malformed.autodiff =
908            DispatchAutodiffContext::Enabled(GradientCheckpointingStrategy::Disabled);
909        let _ = Dispatch::direct_float(malformed);
910    }
911
912    #[test]
913    #[should_panic(expected = "Gradient checkpointing strategy mismatch")]
914    fn fixed_arity_inputs_reject_mismatched_checkpointing_strategies() {
915        let lhs = float([1.0, 2.0], &device(GradientCheckpointingStrategy::Balanced));
916        let rhs = float([3.0, 4.0], &device(GradientCheckpointingStrategy::Disabled));
917        let _ = Dispatch::float_add(lhs, rhs);
918    }
919
920    #[test]
921    fn disabled_and_enabled_integer_contexts_merge_in_both_orders() {
922        for strategy in [
923            GradientCheckpointingStrategy::Disabled,
924            GradientCheckpointingStrategy::Balanced,
925        ] {
926            let lhs = Dispatch::int_from_data(TensorData::from([1i32, 2]), &inner_device());
927            let rhs = Dispatch::int_from_data(TensorData::from([3i32, 4]), &device(strategy));
928            let output = Dispatch::int_add(lhs, rhs);
929            assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
930
931            let lhs = Dispatch::int_from_data(TensorData::from([1i32, 2]), &device(strategy));
932            let rhs = Dispatch::int_from_data(TensorData::from([3i32, 4]), &inner_device());
933            let output = Dispatch::int_add(lhs, rhs);
934            assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
935        }
936    }
937
938    #[test]
939    fn disabled_and_enabled_float_contexts_merge_in_both_orders() {
940        for strategy in [
941            GradientCheckpointingStrategy::Disabled,
942            GradientCheckpointingStrategy::Balanced,
943        ] {
944            let enabled = float([1.0, 2.0], &device(strategy));
945            let disabled = float([3.0, 4.0], &inner_device());
946
947            let output = Dispatch::float_add(enabled, disabled);
948            assert_enabled_float(&output, strategy);
949
950            let enabled = float([1.0, 2.0], &device(strategy));
951            let disabled = float([3.0, 4.0], &inner_device());
952            let output = Dispatch::float_add(disabled, enabled);
953            assert_enabled_float(&output, strategy);
954        }
955    }
956
957    #[test]
958    fn disabled_and_enabled_contexts_merge_across_vector_inputs() {
959        let strategy = GradientCheckpointingStrategy::Balanced;
960        let disabled = float([1.0, 2.0], &inner_device());
961        let enabled = float([3.0, 4.0], &device(strategy));
962
963        let output = Dispatch::float_cat(vec![disabled, enabled], 0);
964        assert_enabled_float(&output, strategy);
965    }
966
967    #[test]
968    #[should_panic(expected = "Gradient checkpointing strategy mismatch")]
969    fn vector_inputs_reject_mismatched_checkpointing_strategies() {
970        let balanced = float([1.0, 2.0], &device(GradientCheckpointingStrategy::Balanced));
971        let disabled = float([3.0, 4.0], &device(GradientCheckpointingStrategy::Disabled));
972        let _ = Dispatch::float_cat(vec![balanced, disabled], 0);
973    }
974
975    #[test]
976    #[should_panic(expected = "Gradient checkpointing strategy mismatch")]
977    fn q_matmul_rejects_mismatched_checkpointing_strategies() {
978        let balanced = Dispatch::quantize_dynamic(
979            float([1.0, 2.0], &device(GradientCheckpointingStrategy::Balanced)),
980            &QuantScheme::default(),
981        );
982        let disabled = Dispatch::quantize_dynamic(
983            float([3.0, 4.0], &device(GradientCheckpointingStrategy::Disabled)),
984            &QuantScheme::default(),
985        );
986        let _ = Dispatch::q_matmul(
987            TensorPrimitive::QFloat(balanced),
988            TensorPrimitive::QFloat(disabled),
989        );
990    }
991
992    #[test]
993    fn q_matmul_merges_disabled_and_enabled_contexts() {
994        let strategy = GradientCheckpointingStrategy::Balanced;
995        let disabled = Dispatch::quantize_dynamic(
996            float_2d([[1.0, 2.0], [3.0, 4.0]], &inner_device()),
997            &QuantScheme::default(),
998        );
999        let enabled = Dispatch::quantize_dynamic(
1000            float_2d([[1.0, 0.0], [0.0, 1.0]], &device(strategy)),
1001            &QuantScheme::default(),
1002        );
1003        let output = Dispatch::q_matmul(
1004            TensorPrimitive::QFloat(disabled),
1005            TensorPrimitive::QFloat(enabled),
1006        );
1007        let output = match output {
1008            TensorPrimitive::QFloat(output) | TensorPrimitive::Float(output) => output,
1009        };
1010
1011        assert_eq!(output.autodiff, DispatchAutodiffContext::Enabled(strategy));
1012    }
1013
1014    #[test]
1015    fn inner_transitions_clear_and_restore_context() {
1016        let tensor = float([1.0, 2.0], &device(GradientCheckpointingStrategy::Balanced));
1017        let inner = <Dispatch as AutodiffBackend>::inner(tensor);
1018        assert_eq!(inner.autodiff, DispatchAutodiffContext::Disabled);
1019        assert!(!matches!(inner.kind, DispatchTensorKind::Autodiff(_)));
1020
1021        let enabled = <Dispatch as AutodiffBackend>::from_inner(inner);
1022        assert_enabled_float(&enabled, GradientCheckpointingStrategy::Disabled);
1023
1024        let int = Dispatch::int_from_data(
1025            TensorData::from([1i32, 2]),
1026            &device(GradientCheckpointingStrategy::Balanced),
1027        );
1028        let int = <Dispatch as AutodiffBackend>::int_inner(int);
1029        assert_eq!(int.autodiff, DispatchAutodiffContext::Disabled);
1030        let int = <Dispatch as AutodiffBackend>::int_from_inner(int);
1031        assert_eq!(
1032            int.autodiff,
1033            DispatchAutodiffContext::Enabled(GradientCheckpointingStrategy::Disabled)
1034        );
1035    }
1036
1037    #[test]
1038    fn gradients_are_inner_backend_tensors() {
1039        let x = Dispatch::float_set_require_grad(
1040            float([2.0, 3.0], &device(GradientCheckpointingStrategy::Balanced)),
1041            true,
1042        );
1043        let output = Dispatch::float_mul(x.clone(), x.clone());
1044        let gradients = <Dispatch as AutodiffBackend>::backward(output);
1045        let gradient = <Dispatch as AutodiffBackend>::grad(&x, &gradients).unwrap();
1046        assert_eq!(gradient.autodiff, DispatchAutodiffContext::Disabled);
1047        assert!(!matches!(gradient.kind, DispatchTensorKind::Autodiff(_)));
1048    }
1049}
1050
1051// NOTE: placeholder for autodiff module requirements
1052#[cfg(not(feature = "autodiff"))]
1053impl AutodiffBackend for Dispatch {
1054    type InnerBackend = Dispatch;
1055
1056    type Gradients = bool;
1057
1058    fn backward(_tensor: DispatchTensor) -> Self::Gradients {
1059        unimplemented!("Requires `autodiff` feature")
1060    }
1061
1062    fn grad(_tensor: &DispatchTensor, _grads: &Self::Gradients) -> Option<DispatchTensor> {
1063        unimplemented!("Requires `autodiff` feature")
1064    }
1065
1066    fn grad_remove(
1067        _tensor: &DispatchTensor,
1068        _grads: &mut Self::Gradients,
1069    ) -> Option<DispatchTensor> {
1070        unimplemented!("Requires `autodiff` feature")
1071    }
1072
1073    fn grad_replace(_tensor: &DispatchTensor, _grads: &mut Self::Gradients, _grad: DispatchTensor) {
1074        unimplemented!("Requires `autodiff` feature")
1075    }
1076
1077    fn inner(_tensor: DispatchTensor) -> DispatchTensor {
1078        unimplemented!("Requires `autodiff` feature")
1079    }
1080
1081    fn int_inner(_tensor: DispatchTensor) -> DispatchTensor {
1082        unimplemented!("Requires `autodiff` feature")
1083    }
1084
1085    fn bool_inner(_tensor: DispatchTensor) -> DispatchTensor {
1086        unimplemented!("Requires `autodiff` feature")
1087    }
1088
1089    fn q_inner(_tensor: DispatchTensor) -> DispatchTensor {
1090        unimplemented!("Requires `autodiff` feature")
1091    }
1092
1093    fn from_inner(_tensor: DispatchTensor) -> DispatchTensor {
1094        unimplemented!("Requires `autodiff` feature")
1095    }
1096
1097    fn int_from_inner(_tensor: DispatchTensor) -> DispatchTensor {
1098        unimplemented!("Requires `autodiff` feature")
1099    }
1100
1101    fn bool_from_inner(_tensor: DispatchTensor) -> DispatchTensor {
1102        unimplemented!("Requires `autodiff` feature")
1103    }
1104
1105    fn q_from_inner(_tensor: DispatchTensor) -> DispatchTensor {
1106        unimplemented!("Requires `autodiff` feature")
1107    }
1108}
1109
1110impl Dispatch {
1111    /// List all available devices of the specified [type id](DispatchDeviceId).
1112    pub fn enumerate(type_id: DispatchDeviceId) -> Vec<DispatchDevice> {
1113        // TODO: right now this assumes `type_id = 0`, but WgpuDevice and LibTorchDevice have other types.
1114        match type_id {
1115            #[cfg(cube_backend)]
1116            DispatchDeviceId::Cube => CubeDevice::enumerate_all()
1117                .into_iter()
1118                .filter(|device| cube_runtime_enabled(device.runtime()))
1119                .map(DispatchDevice::Cube)
1120                .collect(),
1121            #[cfg(feature = "flex")]
1122            DispatchDeviceId::Flex => vec![FlexDevice.into()],
1123            #[cfg(feature = "ndarray")]
1124            DispatchDeviceId::NdArray => vec![NdArrayDevice::Cpu.into()],
1125            #[cfg(feature = "tch")]
1126            DispatchDeviceId::LibTorch => (0..LibTorch::device_count(0))
1127                .map(|i| LibTorchDevice::Cuda(i).into())
1128                .collect(),
1129            #[cfg(feature = "remote")]
1130            // A remote device needs its server's address, which a type id cannot carry.
1131            DispatchDeviceId::Remote => Vec::new(),
1132            #[cfg(feature = "capture")]
1133            // Capture devices are created together with a lifecycle handle and therefore
1134            // cannot be reconstructed from a type ID alone.
1135            DispatchDeviceId::Capture => Vec::new(),
1136            _ => unreachable!("No backend feature enabled."),
1137        }
1138    }
1139
1140    /// List every device of the cubecl `runtime`.
1141    ///
1142    /// The cubecl runtimes share one backend, so [`enumerate`](Self::enumerate) with
1143    /// [`DispatchDeviceId::Cube`] lists all of them at once. A caller that asked for one
1144    /// runtime — CUDA, say — wants that runtime's devices and not the wgpu and CPU ones found
1145    /// beside them, which is what this narrows to. Empty for a runtime this build's features
1146    /// did not enable.
1147    #[cfg(cube_backend)]
1148    pub fn enumerate_cube(runtime: RuntimeId) -> Vec<DispatchDevice> {
1149        cube_devices(runtime)
1150            .into_iter()
1151            .map(DispatchDevice::Cube)
1152            .collect()
1153    }
1154}
1155
1156/// Whether `runtime` is one this crate's features asked for.
1157///
1158/// `CubeDevice::enumerate_all` answers for the runtimes *cubecl* compiled in, and cargo unifies
1159/// features across a build: a workspace that also builds `burn-cuda` gives cubecl the CUDA
1160/// runtime even where this crate was built with only `wgpu`. What this crate hands out has to
1161/// follow its own features, so enumeration is filtered through here.
1162#[cfg(cube_backend)]
1163fn cube_runtime_enabled(runtime: RuntimeId) -> bool {
1164    match runtime {
1165        RuntimeId::Cuda => cfg!(feature = "cuda"),
1166        RuntimeId::Hip => cfg!(feature = "rocm"),
1167        RuntimeId::Wgpu => cfg!(any(
1168            feature = "wgpu",
1169            feature = "vulkan",
1170            feature = "metal",
1171            feature = "webgpu"
1172        )),
1173        RuntimeId::Cpu => cfg!(feature = "cpu"),
1174        // burn's `metal` feature is wgpu compiling to MSL; cubecl's native Metal runtime has no
1175        // burn feature to enable it.
1176        RuntimeId::Metal => false,
1177    }
1178}
1179
1180/// The cubecl devices belonging to `runtime`, in enumeration order. Empty for a runtime this
1181/// build's features did not ask for, even where cubecl compiled it in.
1182#[cfg(cube_backend)]
1183pub(crate) fn cube_devices(runtime: RuntimeId) -> Vec<CubeDevice> {
1184    if !cube_runtime_enabled(runtime) {
1185        return Vec::new();
1186    }
1187
1188    CubeDevice::enumerate_all()
1189        .into_iter()
1190        .filter(|device| device.runtime() == runtime)
1191        .collect()
1192}