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