Skip to main content

burn_dispatch/
device.rs

1use burn_backend::{DeviceId, DeviceOps, DeviceSettings};
2
3#[allow(unused_imports)] // Empty in backend-free and capture-only builds.
4use crate::devices::*;
5
6#[cfg(feature = "capture")]
7use burn_capture::CaptureDevice;
8#[cfg(feature = "group")]
9use burn_group::{GroupDevice, GroupError};
10
11#[cfg(feature = "autodiff")]
12use alloc::boxed::Box;
13
14// Throughput types come from `burn-backend` (which re-exports them from cubecl),
15// so `burn-dispatch` needs no direct `cubecl` dependency.
16#[cfg(feature = "cubecl")]
17use alloc::vec::Vec;
18#[cfg(feature = "cubecl")]
19use burn_backend::cubecl::{DeviceIdentity, ThroughputError, ThroughputKey, ThroughputValue};
20// `cubecl` without a runtime feature gives the throughput *types* but no `Cube` device to
21// measure, so the measurement itself follows `cube_backend` rather than the feature.
22#[cfg(cube_backend)]
23use burn_backend::cubecl::measure_peak_throughput;
24
25/// Represents a device for the [`Dispatch`](crate::Dispatch).
26///
27/// Each variant corresponds to a backend that the [`Dispatch`](crate::Dispatch) can dispatch operations to.
28///
29/// # Example
30///
31/// ```ignore
32/// use burn::DispatchDevice;
33///
34/// // One variant covers every cubecl runtime; the device inside says which.
35/// #[cfg(feature = "cuda")]
36/// let cuda_device = DispatchDevice::Cube(cubecl::Device::Cuda(Default::default()));
37///
38/// #[cfg(feature = "flex")]
39/// let flex_device = DispatchDevice::Flex(Default::default());
40/// ```
41#[derive(Clone, Eq)]
42pub enum DispatchDevice {
43    #[cfg(not(backend_enabled))]
44    #[doc(hidden)]
45    Unavailable(crate::NoBackend),
46    /// A device of the [cubecl backend](crate::backends::Cube): CUDA, ROCm, Metal, Vulkan,
47    /// WebGPU, wgpu or the CPU runtime.
48    #[cfg(cube_backend)]
49    Cube(CubeDevice),
50
51    /// The [Flex backend](crate::backends::Flex) device (CPU-only).
52    #[cfg(feature = "flex")]
53    Flex(FlexDevice),
54
55    /// The [remote backend](crate::backends::Remote) device, identified by a network address.
56    #[cfg(feature = "remote")]
57    Remote(RemoteDevice),
58
59    /// A non-executing graph capture device.
60    #[cfg(feature = "capture")]
61    Capture(CaptureDevice),
62
63    /// A [group of devices](crate::backends::Group) that a tensor can be split over.
64    #[cfg(feature = "group")]
65    Group(GroupDevice),
66
67    /// The [autodiff enabled backend](crate::backends::Autodiff) device.
68    #[cfg(feature = "autodiff")]
69    Autodiff(AutodiffDevice),
70}
71
72#[cfg(feature = "cubecl")]
73impl DispatchDevice {
74    /// Who this device is, `None` for a backend that does not report one. An autodiff device
75    /// answers for the device it wraps. Opens the device.
76    pub fn identity(&self) -> Option<DeviceIdentity> {
77        match self {
78            #[cfg(cube_backend)]
79            DispatchDevice::Cube(device) => Some(device.client().properties().identity.clone()),
80            #[cfg(feature = "autodiff")]
81            DispatchDevice::Autodiff(device) => device.inner.identity(),
82            #[allow(unreachable_patterns)]
83            _ => None,
84        }
85    }
86
87    /// Measure peak throughput for this device against the given `keys`.
88    ///
89    /// Only cubecl-backed devices can measure throughput; other backends
90    /// (remote, ...) return an empty vector. An autodiff
91    /// device reports the peaks of the device it wraps. Each returned result
92    /// corresponds positionally to the key at the same index, and carries a
93    /// [`ThroughputError`] where the device has no peak for that key.
94    // With `cubecl` on but no runtime compiled in, every arm below ignores `keys`.
95    #[cfg_attr(not(cube_backend), allow(unused_variables))]
96    pub fn performance_stats(
97        &self,
98        keys: &[ThroughputKey],
99    ) -> Vec<Result<ThroughputValue, ThroughputError>> {
100        // No catch-all arm: a new backend must fail to compile here rather
101        // than silently report no peaks.
102        match self {
103            #[cfg(not(backend_enabled))]
104            Self::Unavailable(never) => never.unreachable(),
105            #[cfg(cube_backend)]
106            DispatchDevice::Cube(device) => {
107                let client = device.client();
108                keys.iter()
109                    .map(|key| measure_peak_throughput(&client, *key))
110                    .collect()
111            }
112            // Autodiff does not change the hardware, so measure the wrapped device.
113            #[cfg(feature = "autodiff")]
114            DispatchDevice::Autodiff(device) => device.performance_stats(keys),
115
116            // Not cubecl-backed, so there are no kernels to measure.
117            #[cfg(feature = "flex")]
118            DispatchDevice::Flex(_) => Vec::new(),
119
120            // The kernels run on the server, which this local API cannot reach.
121            #[cfg(feature = "remote")]
122            DispatchDevice::Remote(_) => Vec::new(),
123            // A group's peaks are its members', measured on them.
124            #[cfg(feature = "group")]
125            DispatchDevice::Group(_) => Vec::new(),
126            #[cfg(feature = "capture")]
127            DispatchDevice::Capture(_) => Vec::new(),
128        }
129    }
130}
131
132#[cfg(feature = "autodiff")]
133// This tuple struct mainly restricts users from creating Autodiff(Autodiff) devices.
134/// A wrapper that enables automatic differentiation for a [`DispatchDevice`].
135///
136/// Use [`DispatchDevice::autodiff`] to construct this type.
137#[derive(Debug, Clone)]
138pub struct AutodiffDevice {
139    pub(crate) inner: Box<DispatchDevice>,
140    pub(crate) checkpointing: GradientCheckpointingStrategy,
141}
142
143/// Compares on hardware identity only, ignoring the checkpointing strategy, so that this agrees
144/// with [`DispatchDevice`]'s own [`PartialEq`] — which has to ignore it, since comparing an
145/// `Autodiff` device against a raw one has no strategy to compare against. A derived impl would
146/// make `Autodiff(a) == Autodiff(b)` disagree with `DispatchDevice::Autodiff(a) ==
147/// DispatchDevice::Autodiff(b)`.
148///
149/// Use [`gradient_checkpointing_strategy`](Self::gradient_checkpointing_strategy) when the
150/// strategy is what you actually need to compare.
151#[cfg(feature = "autodiff")]
152impl PartialEq for AutodiffDevice {
153    fn eq(&self, other: &Self) -> bool {
154        self.inner == other.inner
155    }
156}
157
158#[cfg(feature = "autodiff")]
159impl Eq for AutodiffDevice {}
160
161#[cfg(feature = "autodiff")]
162impl AutodiffDevice {
163    pub(crate) fn new(
164        device: DispatchDevice,
165        checkpointing: GradientCheckpointingStrategy,
166    ) -> Self {
167        Self {
168            inner: Box::new(device),
169            checkpointing,
170        }
171    }
172
173    /// Returns the underlying device, removing the autodiff capability.
174    pub fn inner(self) -> DispatchDevice {
175        *self.inner
176    }
177
178    /// Returns the gradient checkpointing strategy.
179    pub fn gradient_checkpointing_strategy(&self) -> GradientCheckpointingStrategy {
180        self.checkpointing
181    }
182}
183
184#[cfg(feature = "autodiff")]
185// Useful for match in dispatch macros
186impl core::ops::Deref for AutodiffDevice {
187    type Target = DispatchDevice;
188
189    fn deref(&self) -> &Self::Target {
190        &self.inner
191    }
192}
193
194#[allow(missing_docs)]
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
196/// Gradient checkpointing strategy for autodiff.
197#[repr(u8)]
198pub enum GradientCheckpointingStrategy {
199    /// Recompute selected activations during backpropagation to reduce peak memory usage.
200    Balanced,
201    /// Disable gradient checkpointing while retaining autodiff tracking.
202    #[default]
203    Disabled,
204}
205
206impl core::fmt::Debug for DispatchDevice {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208        match self {
209            #[cfg(not(backend_enabled))]
210            Self::Unavailable(never) => never.unreachable(),
211            #[cfg(cube_backend)]
212            Self::Cube(device) => f.debug_tuple("Cube").field(device).finish(),
213            #[cfg(feature = "flex")]
214            Self::Flex(device) => f.debug_tuple("Flex").field(device).finish(),
215            #[cfg(feature = "remote")]
216            Self::Remote(device) => f.debug_tuple("Remote").field(device).finish(),
217            #[cfg(feature = "group")]
218            Self::Group(device) => f.debug_tuple("Group").field(device).finish(),
219            #[cfg(feature = "capture")]
220            Self::Capture(device) => f.debug_tuple("Capture").field(device).finish(),
221            #[cfg(feature = "autodiff")]
222            // Format without `AutodiffDevice` wrapper
223            Self::Autodiff(device) => f
224                .debug_struct("Autodiff")
225                .field("device", &device.inner)
226                .field("checkpointing", &device.checkpointing)
227                .finish(),
228        }
229    }
230}
231
232impl Default for DispatchDevice {
233    /// Select an enabled backend in this order: CUDA, Metal, ROCm, Vulkan, WebGPU,
234    /// wgpu, CPU, Flex, Remote. `BURN_DEVICE` overrides this in
235    /// std builds. Capture devices must be constructed explicitly.
236    ///
237    /// The `metal`, `vulkan`, and `webgpu` overrides require their matching Cargo features
238    /// and pin the graphics API. Explicit Metal selection requires native MSL support.
239    /// The `wgpu` override keeps automatic API and compiler selection with WGSL fallback.
240    ///
241    /// Panics when no execution backend is enabled, or when `BURN_DEVICE` names an unknown
242    /// backend or one whose Cargo feature is not enabled.
243    #[allow(unreachable_code)]
244    fn default() -> Self {
245        // BURN_DEVICE selects one compiled backend or reports a configuration error.
246
247        #[cfg(feature = "std")]
248        {
249            if let Ok(device_str) = std::env::var("BURN_DEVICE") {
250                match device_str.to_lowercase().as_str() {
251                    // Every cubecl runtime is the one `Cube` backend; the name here
252                    // picks the runtime and, for named wgpu APIs, pins the graphics API.
253                    "cuda" => {
254                        #[cfg(feature = "cuda")]
255                        return Self::Cube(CubeDevice::Cuda(Default::default()));
256                        panic!(
257                            "BURN_DEVICE=cuda requested, but the 'cuda' feature is not enabled."
258                        );
259                    }
260                    "rocm" => {
261                        #[cfg(feature = "rocm")]
262                        return Self::Cube(CubeDevice::Hip(Default::default()));
263                        panic!(
264                            "BURN_DEVICE=rocm requested, but the 'rocm' feature is not enabled."
265                        );
266                    }
267                    "metal" => {
268                        #[cfg(feature = "metal")]
269                        return Self::Cube(CubeDevice::Wgpu(
270                            WgpuDevice::default().on(WgpuBackend::Metal),
271                        ));
272                        panic!(
273                            "BURN_DEVICE=metal requested, but the 'metal' feature is not enabled."
274                        );
275                    }
276                    "vulkan" => {
277                        #[cfg(feature = "vulkan")]
278                        return Self::Cube(CubeDevice::Wgpu(
279                            WgpuDevice::default().on(WgpuBackend::Vulkan),
280                        ));
281                        panic!(
282                            "BURN_DEVICE=vulkan requested, but the 'vulkan' feature is not enabled."
283                        );
284                    }
285                    "webgpu" => {
286                        #[cfg(feature = "webgpu")]
287                        return Self::Cube(CubeDevice::Wgpu(
288                            WgpuDevice::default().on(WgpuBackend::WebGpu),
289                        ));
290                        panic!(
291                            "BURN_DEVICE=webgpu requested, but the 'webgpu' feature is not enabled."
292                        );
293                    }
294                    "wgpu" => {
295                        #[cfg(feature = "wgpu")]
296                        return Self::Cube(CubeDevice::Wgpu(Default::default()));
297                        panic!(
298                            "BURN_DEVICE=wgpu requested, but the 'wgpu' feature is not enabled."
299                        );
300                    }
301                    "cpu" => {
302                        #[cfg(feature = "cpu")]
303                        return Self::Cube(CubeDevice::Cpu(Default::default()));
304                        panic!("BURN_DEVICE=cpu requested, but the 'cpu' feature is not enabled.");
305                    }
306                    "remote" => {
307                        #[cfg(feature = "remote")]
308                        return Self::Remote(RemoteDevice::default());
309                        panic!(
310                            "BURN_DEVICE=remote requested, but the 'remote' feature is not enabled."
311                        );
312                    }
313                    "flex" => {
314                        #[cfg(feature = "flex")]
315                        return Self::Flex(FlexDevice);
316                        panic!(
317                            "BURN_DEVICE=flex requested, but the 'flex' feature is not enabled."
318                        );
319                    }
320                    _ => panic!("Unknown BURN_DEVICE override: '{}'.", device_str),
321                }
322            }
323        }
324
325        // Spelled out per feature rather than left to `CubeDevice::default()`: that answers for
326        // the runtimes *cubecl* compiled in, and cargo unifies features across a build, so a
327        // workspace that also builds `burn-cuda` would hand this crate a CUDA default even when
328        // it was built with only `wgpu`. The order is the one a caller who did not choose would
329        // want — a discrete accelerator, then the portable path, then the CPU.
330        #[cfg(feature = "cuda")]
331        return Self::Cube(CubeDevice::Cuda(Default::default()));
332
333        #[cfg(feature = "metal")]
334        return Self::Cube(CubeDevice::Wgpu(Default::default()));
335
336        #[cfg(feature = "rocm")]
337        return Self::Cube(CubeDevice::Hip(Default::default()));
338
339        #[cfg(feature = "vulkan")]
340        return Self::Cube(CubeDevice::Wgpu(Default::default()));
341
342        #[cfg(feature = "webgpu")]
343        return Self::Cube(CubeDevice::Wgpu(Default::default()));
344
345        #[cfg(feature = "wgpu")]
346        return Self::Cube(CubeDevice::Wgpu(Default::default()));
347
348        #[cfg(feature = "cpu")]
349        return Self::Cube(CubeDevice::Cpu(Default::default()));
350
351        #[cfg(feature = "flex")]
352        return Self::Flex(FlexDevice);
353
354        #[cfg(feature = "remote")]
355        return Self::Remote(RemoteDevice::default());
356
357        panic!(
358            "No execution backend is enabled. Enable a Burn backend feature such as `flex`, \
359             `wgpu`, or `cuda`. To record a graph without executing it, enable `capture` \
360             and use Device::capture()."
361        );
362    }
363}
364
365impl PartialEq for DispatchDevice {
366    /// Compares devices based on hardware identity.
367    ///
368    /// Returns `true` if both devices represent the same compute resource.
369    /// Note that this comparison ignores autodiff and checkpointing settings.
370    fn eq(&self, other: &Self) -> bool {
371        match (self, other) {
372            // If both are Autodiff, compare the inner devices
373            #[cfg(feature = "autodiff")]
374            (DispatchDevice::Autodiff(a), DispatchDevice::Autodiff(b)) => {
375                a.inner.as_ref() == b.inner.as_ref()
376            }
377            // If one is Autodiff, compare it to the raw device
378            #[cfg(feature = "autodiff")]
379            (DispatchDevice::Autodiff(a), b) => a.inner.as_ref() == b,
380            #[cfg(feature = "autodiff")]
381            (a, DispatchDevice::Autodiff(b)) => a == b.inner.as_ref(),
382            #[cfg(cube_backend)]
383            (Self::Cube(a), Self::Cube(b)) => a == b,
384            #[cfg(feature = "flex")]
385            (Self::Flex(a), Self::Flex(b)) => a == b,
386            #[cfg(feature = "remote")]
387            (Self::Remote(a), Self::Remote(b)) => a == b,
388            #[cfg(feature = "group")]
389            (Self::Group(a), Self::Group(b)) => a == b,
390            #[cfg(feature = "capture")]
391            (Self::Capture(a), Self::Capture(b)) => a == b,
392            #[allow(unreachable_patterns)]
393            (_, _) => false,
394        }
395    }
396}
397
398const INTERNAL_ID_MASK: u16 = 0x00FF;
399const BACKEND_SHIFT: u32 = 8;
400
401impl DispatchDevice {
402    /// Create the dispatch representation used by the high-level graph-capture device.
403    #[cfg(feature = "capture")]
404    #[doc(hidden)]
405    pub fn capture() -> Self {
406        Self::Capture(CaptureDevice::default())
407    }
408
409    /// A new group of `devices`, its members, that a tensor can be split over, apart from every
410    /// other group.
411    ///
412    /// Make a group once and reuse it, as any device: each call registers a new group for the
413    /// rest of the process.
414    ///
415    /// Each of its default dtypes is the first of the members' configured ones, in member order,
416    /// that every member supports.
417    ///
418    /// # Errors
419    ///
420    /// When `devices` is empty or mixes backends, when they are not Cube or Flex devices, when no
421    /// member's default for a dtype runs on every member, or when the members quantize
422    /// differently. An autodiff group is a group of plain devices made autodiff afterwards.
423    ///
424    /// # Panics
425    ///
426    /// When the process already has 65,536 groups.
427    #[cfg(feature = "group")]
428    pub fn group(devices: alloc::vec::Vec<DispatchDevice>) -> Result<Self, GroupError> {
429        if let Some(error) = devices.iter().find_map(Self::refused_as_member) {
430            return Err(error);
431        }
432        let group = match devices.first() {
433            #[cfg(cube_backend)]
434            Some(Self::Cube(_)) => {
435                let members = group_members!(devices, Cube)?;
436                GroupDevice::with_member_settings::<crate::backends::Cube>(&members)
437            }
438            #[cfg(feature = "flex")]
439            Some(Self::Flex(_)) => {
440                let members = group_members!(devices, Flex)?;
441                GroupDevice::with_member_settings::<crate::backends::Flex>(&members)
442            }
443            Some(other) => unreachable!("{other:?} was refused as a member"),
444            None => Err(GroupError::NoDevices),
445        }?;
446        Ok(Self::Group(group))
447    }
448
449    /// Why this device cannot be a group's member, or `None` when it can.
450    #[cfg(feature = "group")]
451    fn refused_as_member(&self) -> Option<GroupError> {
452        match self {
453            #[cfg(cube_backend)]
454            Self::Cube(_) => None,
455            #[cfg(feature = "flex")]
456            Self::Flex(_) => None,
457            #[cfg(feature = "autodiff")]
458            Self::Autodiff(_) => Some(GroupError::AutodiffMember),
459            #[cfg(feature = "remote")]
460            Self::Remote(_) => Some(GroupError::RemoteMember),
461            #[allow(unreachable_patterns)]
462            other => Some(GroupError::UnsupportedBackend {
463                device: alloc::format!("{other:?}"),
464            }),
465        }
466    }
467
468    #[cfg(feature = "autodiff")]
469    /// Creates a new [`DispatchDevice`] with
470    /// [automatic differentiation](crate::backends::Autodiff) enabled.
471    pub fn autodiff(device: impl Into<DispatchDevice>) -> DispatchDevice {
472        Self::autodiff_with_gradient_checkpointing(device, GradientCheckpointingStrategy::Disabled)
473    }
474    #[cfg(feature = "autodiff")]
475    /// Creates a new [`DispatchDevice`] with automatic differentiation and the provided gradient
476    /// checkpointing strategy enabled.
477    pub fn autodiff_with_gradient_checkpointing(
478        device: impl Into<DispatchDevice>,
479        checkpointing: GradientCheckpointingStrategy,
480    ) -> DispatchDevice {
481        let device = device.into();
482        DispatchDevice::Autodiff(AutodiffDevice::new(device, checkpointing))
483    }
484
485    /// The id of the worker this device's fusion server runs on, or `None` when its operations do
486    /// not go through one.
487    ///
488    /// This is the runtime device's own id, not the one this device reports through
489    /// [`Device::to_id`](burn_backend::Device::to_id), which encodes the dispatch variant into its
490    /// type. A [`DeviceHandle`](burn_backend::DeviceHandle) opened with it shares the fusion
491    /// server's queue, so a task submitted there runs between the operations registered before
492    /// and after it. An autodiff device answers for the device it wraps. A device group answers
493    /// `None`: each of its members has a fusion server of its own.
494    pub fn fusion_worker(&self) -> Option<DeviceId> {
495        match self {
496            #[cfg(cube_backend)]
497            DispatchDevice::Cube(device) => burn_cubecl::FUSED.then(|| device.to_id()),
498            #[cfg(feature = "autodiff")]
499            DispatchDevice::Autodiff(device) => device.inner.fusion_worker(),
500            #[allow(unreachable_patterns)]
501            _ => None,
502        }
503    }
504
505    /// Returns the inner device, without autodiff (when enabled).
506    pub fn inner(self) -> Self {
507        #[cfg(feature = "autodiff")]
508        if let DispatchDevice::Autodiff(device) = self {
509            return *device.inner;
510        }
511
512        self
513    }
514
515    /// Returns a unique number per variant to encode into type_id.
516    fn backend_id(&self) -> DispatchDeviceId {
517        match self {
518            #[cfg(not(backend_enabled))]
519            Self::Unavailable(never) => never.unreachable(),
520            #[cfg(cube_backend)]
521            Self::Cube(_) => DispatchDeviceId::Cube,
522            #[cfg(feature = "flex")]
523            Self::Flex(_) => DispatchDeviceId::Flex,
524            #[cfg(feature = "remote")]
525            Self::Remote(_) => DispatchDeviceId::Remote,
526            #[cfg(feature = "group")]
527            Self::Group(_) => DispatchDeviceId::Group,
528            #[cfg(feature = "capture")]
529            Self::Capture(_) => DispatchDeviceId::Capture,
530            #[cfg(feature = "autodiff")]
531            Self::Autodiff(device) => device.inner.backend_id(),
532        }
533    }
534
535    /// Encode variant ID and backend type ID into a unique `type_id`.
536    fn encode_type_id(&self, backend_type_id: u16) -> u16 {
537        // Use the lower 8 bits for the backend's internal type ID
538        let internal_type_id = backend_type_id & INTERNAL_ID_MASK;
539        // Use the upper 8 bits for the DispatchDevice/DispatchDeviceId
540        let backend = u16::from(self.backend_id()) << BACKEND_SHIFT;
541        backend | internal_type_id
542    }
543
544    /// Decode an encoded `type_id` into variant ID and backend type ID.
545    pub(crate) fn decode_type_id(type_id: u16) -> (DispatchDeviceId, u16) {
546        let backend_raw = type_id >> BACKEND_SHIFT;
547        let internal_type_id = type_id & INTERNAL_ID_MASK;
548
549        let backend = DispatchDeviceId::try_from(backend_raw).expect("Unknown DispatchDevice ID");
550
551        (backend, internal_type_id)
552    }
553}
554
555#[allow(missing_docs)]
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557#[repr(u16)]
558pub enum DispatchDeviceId {
559    /// Every cubecl runtime: which one is in the device's own id.
560    Cube = 0,
561    Flex = 4,
562    // 5 was LibTorch; keep the other backend IDs stable.
563    // 6 was NdArray; keep the other backend IDs stable.
564    Remote = 10,
565    Capture = 11,
566    Group = 12,
567}
568
569impl From<DispatchDeviceId> for u16 {
570    fn from(variant: DispatchDeviceId) -> Self {
571        variant as u16
572    }
573}
574
575impl TryFrom<u16> for DispatchDeviceId {
576    type Error = ();
577
578    fn try_from(value: u16) -> Result<Self, Self::Error> {
579        match value {
580            #[cfg(cube_backend)]
581            0 => Ok(Self::Cube),
582            #[cfg(feature = "flex")]
583            4 => Ok(Self::Flex),
584            #[cfg(feature = "remote")]
585            10 => Ok(Self::Remote),
586            #[cfg(feature = "group")]
587            12 => Ok(Self::Group),
588            #[cfg(feature = "capture")]
589            11 => Ok(Self::Capture),
590            _ => Err(()),
591        }
592    }
593}
594
595impl DeviceOps for DispatchDevice {
596    fn defaults(&self) -> DeviceSettings {
597        match self {
598            #[cfg(not(backend_enabled))]
599            Self::Unavailable(never) => never.unreachable(),
600            #[cfg(cube_backend)]
601            Self::Cube(device) => device.defaults(),
602            #[cfg(feature = "flex")]
603            Self::Flex(device) => device.defaults(),
604            #[cfg(feature = "remote")]
605            Self::Remote(device) => device.defaults(),
606            #[cfg(feature = "group")]
607            Self::Group(device) => device.defaults(),
608            #[cfg(feature = "capture")]
609            Self::Capture(device) => device.defaults(),
610            #[cfg(feature = "autodiff")]
611            Self::Autodiff(device) => device.inner.defaults(),
612        }
613    }
614}
615
616impl burn_backend::Device for DispatchDevice {
617    fn from_id(mut device_id: DeviceId) -> Self {
618        let (dispatch_id, backend_type_id) = Self::decode_type_id(device_id.type_id);
619        device_id.type_id = backend_type_id;
620
621        #[allow(unreachable_patterns)]
622        match dispatch_id {
623            #[cfg(cube_backend)]
624            DispatchDeviceId::Cube => Self::Cube(burn_backend::Device::from_id(device_id)),
625            #[cfg(feature = "flex")]
626            DispatchDeviceId::Flex => Self::Flex(FlexDevice::from_id(device_id)),
627            #[cfg(feature = "remote")]
628            DispatchDeviceId::Remote => Self::Remote(RemoteDevice::from_id(device_id)),
629            #[cfg(feature = "group")]
630            DispatchDeviceId::Group => Self::Group(GroupDevice::from_id(device_id)),
631            #[cfg(feature = "capture")]
632            DispatchDeviceId::Capture => Self::Capture(CaptureDevice::from_id(device_id)),
633            _ => unreachable!("No backend feature enabled."),
634        }
635    }
636
637    fn to_id(&self) -> DeviceId {
638        let mut device_id: DeviceId = match self {
639            #[cfg(not(backend_enabled))]
640            Self::Unavailable(never) => never.unreachable(),
641            #[cfg(cube_backend)]
642            Self::Cube(device) => device.to_id(),
643            #[cfg(feature = "flex")]
644            Self::Flex(device) => device.to_id(),
645            #[cfg(feature = "remote")]
646            Self::Remote(device) => device.to_id(),
647            #[cfg(feature = "group")]
648            Self::Group(device) => device.to_id(),
649            #[cfg(feature = "capture")]
650            Self::Capture(device) => device.to_id(),
651            #[cfg(feature = "autodiff")]
652            Self::Autodiff(device) => device.inner.to_id(),
653        };
654        device_id.type_id = self.encode_type_id(device_id.type_id);
655        device_id
656    }
657}
658
659/// Every cubecl device reaches the one cubecl variant.
660#[cfg(cube_backend)]
661impl From<CubeDevice> for DispatchDevice {
662    fn from(device: CubeDevice) -> Self {
663        DispatchDevice::Cube(device)
664    }
665}
666
667// A runtime's own device type converts too, since that is what its crate hands
668// out. There is one variant to reach now, so a wgpu device no longer needs a
669// priority chain of gates to decide which of four it lands in.
670#[cfg(feature = "cpu")]
671impl From<CpuDevice> for DispatchDevice {
672    fn from(device: CpuDevice) -> Self {
673        DispatchDevice::Cube(CubeDevice::Cpu(device))
674    }
675}
676
677#[cfg(feature = "cuda")]
678impl From<CudaDevice> for DispatchDevice {
679    fn from(device: CudaDevice) -> Self {
680        DispatchDevice::Cube(CubeDevice::Cuda(device))
681    }
682}
683
684#[cfg(feature = "rocm")]
685impl From<RocmDevice> for DispatchDevice {
686    fn from(device: RocmDevice) -> Self {
687        DispatchDevice::Cube(CubeDevice::Hip(device))
688    }
689}
690
691#[cfg(any(
692    feature = "wgpu",
693    feature = "metal",
694    feature = "vulkan",
695    feature = "webgpu"
696))]
697impl From<WgpuDevice> for DispatchDevice {
698    fn from(device: WgpuDevice) -> Self {
699        DispatchDevice::Cube(CubeDevice::Wgpu(device))
700    }
701}
702
703#[cfg(feature = "flex")]
704impl From<FlexDevice> for DispatchDevice {
705    fn from(device: FlexDevice) -> Self {
706        DispatchDevice::Flex(device)
707    }
708}
709
710#[cfg(feature = "group")]
711impl From<GroupDevice> for DispatchDevice {
712    fn from(device: GroupDevice) -> Self {
713        DispatchDevice::Group(device)
714    }
715}
716
717#[cfg(feature = "remote")]
718impl From<RemoteDevice> for DispatchDevice {
719    fn from(device: RemoteDevice) -> Self {
720        DispatchDevice::Remote(device)
721    }
722}
723
724#[cfg(all(test, not(backend_enabled)))]
725mod no_backend_tests {
726    #[test]
727    #[should_panic(expected = "No execution backend is enabled. Enable a Burn backend feature")]
728    fn default_requires_backend() {
729        super::DispatchDevice::default();
730    }
731}
732
733#[cfg(all(test, any(feature = "capture", feature = "remote-websocket")))]
734mod tests {
735    use super::*;
736    use burn_backend::Device;
737
738    #[test]
739    #[cfg(feature = "remote-websocket")]
740    fn remote_device_id_round_trips_through_dispatch() {
741        let device = DispatchDevice::from(RemoteDevice::default());
742        let restored = DispatchDevice::from_id(device.to_id());
743
744        assert_eq!(restored, device);
745    }
746
747    #[test]
748    #[cfg(feature = "capture")]
749    fn capture_device_id_round_trips_through_dispatch() {
750        let device = DispatchDevice::capture();
751        let restored = DispatchDevice::from_id(device.to_id());
752
753        assert_eq!(restored, device);
754    }
755}