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