Skip to main content

burn_tensor/
device.rs

1pub use burn_std::{
2    DeviceError, DeviceSettings, ExecutionError, backtrace::BackTrace, device::DeviceId,
3};
4
5#[cfg(feature = "cubecl")]
6pub use burn_backend::cubecl::{
7    AdapterLuid, DeviceIdentity, MemoryAccess, PciAddress, PciVendor, PhysicalDevice,
8    ThroughputError, ThroughputKey, ThroughputMode, ThroughputValue,
9};
10use burn_backend::{Backend, DeviceOps};
11pub use burn_backend::{
12    MemoryPoolUsage, ProfileDuration, ProfileOptions, ProfileTicks, SlicedPoolReport, TimingMethod,
13};
14#[allow(unused)]
15use burn_dispatch::DispatchDeviceId;
16#[cfg(feature = "autodiff")]
17use burn_dispatch::GradientCheckpointingStrategy;
18#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
19use burn_dispatch::devices::RuntimeId;
20#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
21use burn_dispatch::devices::WgpuBackend;
22use burn_dispatch::{Dispatch, DispatchDevice};
23use burn_std::{BoolDType, FloatDType, IntDType, TensorData};
24
25#[cfg(feature = "capture")]
26pub use burn_dispatch::backends::capture::{
27    CaptureError, CaptureScope, CaptureTensor, CapturedGraph, CompletedCaptureScope,
28    IntoCaptureOutputs, TensorId,
29};
30
31#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
32use alloc::string::String;
33use alloc::vec;
34use alloc::vec::Vec;
35
36/// A high-level device handle for tensor operations.
37///
38/// [`Device`] provides a unified interface to interact with the underlying compute backend.
39///
40/// Autodiff support is configured on the device rather than through a separate type parameter.
41/// Tensors inherit that context when created and can later change it independently.
42#[cfg_attr(
43    feature = "autodiff",
44    doc = "Wrap a device with [`.autodiff()`](Device::autodiff) to enable automatic differentiation with the device."
45)]
46#[cfg_attr(
47    not(feature = "autodiff"),
48    doc = "Enable the `autodiff` feature to add automatic differentiation support to devices."
49)]
50///
51/// # Backend selection
52///
53/// Default Cargo features do not enable an execution backend. Select one explicitly,
54/// for example with the `wgpu` or `flex` feature; there is no implicit CPU fallback.
55/// Backend-free builds can expose tensor/model APIs, but cannot create an execution device.
56///
57/// [`Device::default()`] selects the first enabled backend in this order:
58/// CUDA, Metal, ROCm, Vulkan, WebGPU, wgpu, CPU, Flex, Remote.
59/// Use an explicit factory method when the choice must be independent of Cargo feature unification.
60/// Without an execution backend, `Device::default()` panics with configuration guidance.
61///
62/// Enable the desired backend via Cargo feature flags, then call the
63/// corresponding factory method:
64///
65/// ```rust,ignore
66/// // Default CUDA device (requires the `cuda` feature).
67/// let device = Device::cuda(DeviceIndex::Default);
68///
69/// // CUDA device at hardware index 1.
70/// let device = Device::cuda(1);
71///
72/// // WGPU with explicit selector (requires `wgpu`/`vulkan`/`metal`/`webgpu`).
73/// let device = Device::wgpu(DeviceKind::DiscreteGpu(0));
74///
75/// // Default device for whichever backend is enabled.
76/// let device = Default::default();
77/// ```
78///
79/// Available factory methods (each gated by its matching Cargo feature):
80/// `Device::cpu`, `Device::cuda` / `Device::rocm`
81/// (take an integer index or a [`DeviceIndex`]), `Device::wgpu` /
82/// `Device::vulkan` / `Device::metal` / `Device::webgpu` (take a
83/// [`DeviceKind`]), `Device::flex`, `Device::capture`.
84///
85/// # Autodiff
86///
87/// Requires `autodiff` feature.
88///
89/// Gradient computation is opt-in for a device:
90///
91/// ```rust,ignore
92/// let device = Device::default().autodiff();
93///
94/// // Tensors created on this device can participate in an autodiff graph.
95/// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device).require_grad();
96/// ```
97pub struct Device {
98    blob: device_opaque::Opaque,
99}
100
101// Aligned, type-erased storage for `DispatchDevice`. See `crate::macros` for
102// why this indirection exists (it keeps the dispatch type tree out of
103// downstream MIR).
104burn_std::obfuscate!(
105    type: DispatchDevice,
106    module: device_opaque,
107    derives: [Send, Sync]
108);
109
110impl Clone for Device {
111    fn clone(&self) -> Self {
112        Self::new(self.as_dispatch().clone())
113    }
114}
115
116impl Default for Device {
117    fn default() -> Self {
118        Self::new(DispatchDevice::default())
119    }
120}
121
122impl core::fmt::Debug for Device {
123    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
124        write!(f, "Device<{:?}>", self.as_dispatch())
125    }
126}
127
128// Manually implement both `eq` and `ne` to add documentation on equality.
129#[allow(clippy::partialeq_ne_impl)]
130impl PartialEq for Device {
131    /// Compares devices based on hardware identity.
132    ///
133    /// Returns `true` if both devices represent the same compute resource.
134    /// Note that this comparison ignores autodiff and checkpointing settings. Inspect
135    /// [`Device::is_autodiff`] and `Device::gradient_checkpointing_strategy()` when execution
136    /// context also matters.
137    fn eq(&self, other: &Self) -> bool {
138        self.as_dispatch() == other.as_dispatch()
139    }
140
141    /// Compares devices based on hardware identity.
142    ///
143    /// Returns `false` if both devices represent the same compute resource,
144    /// even if one has autodiff enabled and the other does not.
145    fn ne(&self, other: &Self) -> bool {
146        !self.eq(other)
147    }
148}
149
150impl Eq for Device {}
151
152impl Device {
153    /// Wrap a backend-specific device in a unified [`Device`].
154    ///
155    /// Used by:
156    /// - the backend-specific factory methods below (`Device::cuda`, etc.)
157    ///   — these are the recommended entry points for downstream code;
158    /// - burn-tensor's bridge ops, which already hold a [`DispatchDevice`]
159    ///   and just need to wrap it;
160    /// - direct callers (tests, type-erased helpers) that have a concrete
161    ///   backend device type at hand.
162    ///
163    /// Anything convertible into [`DispatchDevice`] is accepted, including
164    /// `DispatchDevice` itself.
165    pub fn new(device: impl Into<DispatchDevice>) -> Self {
166        Self {
167            blob: device_opaque::Opaque::new(device.into()),
168        }
169    }
170
171    /// Borrow the underlying [`DispatchDevice`].
172    ///
173    /// The inverse of [`Device::new`]. Useful to backend-extension authors who need to dispatch on
174    /// the concrete backend variant (e.g. matching `DispatchDevice::Remote(_)`).
175    pub fn as_dispatch(&self) -> &DispatchDevice {
176        self.blob.as_ref()
177    }
178
179    /// Crate-internal owning extraction of the underlying dispatch device.
180    pub(crate) fn into_dispatch(self) -> DispatchDevice {
181        self.blob.into_inner()
182    }
183
184    pub(crate) fn as_dispatch_vec(devices: &[Self]) -> Vec<DispatchDevice> {
185        devices
186            .iter()
187            .map(|device| device.as_dispatch().clone())
188            .collect()
189    }
190}
191
192impl<D: Into<DispatchDevice>> From<D> for Device {
193    fn from(device: D) -> Self {
194        Self::new(device)
195    }
196}
197
198/// Selector for the hardware index of a backend whose devices are simply
199/// indexed (e.g. CUDA, ROCm).
200///
201/// Backend factory methods that take an index (`Device::cuda`, `Device::rocm`)
202/// accept `impl Into<DeviceIndex>`, so the common shorthand is to pass a plain integer literal:
203///
204/// ```rust,ignore
205/// Device::cuda(0);                    // hardware index 0
206/// Device::cuda(DeviceIndex::Default); // backend-chosen default
207/// ```
208#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
209pub enum DeviceIndex {
210    /// Target a specific hardware device by its index.
211    Specified(usize),
212    /// Let the backend pick its default device (typically index `0`).
213    #[default]
214    Default,
215}
216
217impl DeviceIndex {
218    /// Construct a [`DeviceIndex::Specified`] from anything convertible into
219    /// a `usize`.
220    pub fn new(index: impl Into<usize>) -> Self {
221        Self::Specified(index.into())
222    }
223
224    /// Resolve to a concrete hardware index, defaulting to `0` for
225    /// [`DeviceIndex::Default`]. Backend factory methods are each gated by a
226    /// Cargo feature, so this looks dead when none of them are enabled.
227    #[allow(dead_code)]
228    pub(crate) fn resolve(self) -> usize {
229        match self {
230            DeviceIndex::Specified(i) => i,
231            DeviceIndex::Default => 0,
232        }
233    }
234}
235
236impl From<usize> for DeviceIndex {
237    fn from(i: usize) -> Self {
238        Self::Specified(i)
239    }
240}
241
242impl From<u32> for DeviceIndex {
243    fn from(i: u32) -> Self {
244        Self::Specified(i as usize)
245    }
246}
247
248impl From<u64> for DeviceIndex {
249    fn from(i: u64) -> Self {
250        Self::Specified(i as usize)
251    }
252}
253
254impl From<i32> for DeviceIndex {
255    fn from(i: i32) -> Self {
256        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
257    }
258}
259
260impl From<i64> for DeviceIndex {
261    fn from(i: i64) -> Self {
262        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
263    }
264}
265
266/// Selector for the more flexible backends whose device handle is a tagged
267/// enum (e.g. WGPU, which can target a discrete/integrated/virtual GPU, a CPU
268/// adapter, an externally-created wgpu setup, or just "best available").
269///
270/// The variants mirror `WgpuDeviceKind` from cubecl so the mapping is direct, but
271/// it is kept as a burn-owned enum so callers don't have to depend on cubecl.
272#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
273pub enum DeviceKind {
274    /// Discrete GPU with the given index. The index is the index of the discrete GPU in the list
275    /// of all discrete GPUs found on the system.
276    DiscreteGpu(usize),
277
278    /// Integrated GPU with the given index. The index is the index of the integrated GPU in the
279    /// list of all integrated GPUs found on the system.
280    IntegratedGpu(usize),
281
282    /// Virtual GPU with the given index. The index is the index of the virtual GPU in the list of
283    /// all virtual GPUs found on the system.
284    VirtualGpu(usize),
285
286    /// CPU.
287    Cpu,
288
289    /// The best available device found with the current graphics API.
290    ///
291    /// This will prioritize GPUs wgpu recognizes as "high power". Additionally, you can override this using
292    /// the `CUBECL_WGPU_DEFAULT_DEVICE` environment variable. This variable is spelled as if i was a `WgpuDevice`,
293    /// so for example `CUBECL_WGPU_DEFAULT_DEVICE=IntegratedGpu(1)` or `CUBECL_WGPU_DEFAULT_DEVICE=Cpu`
294    #[default]
295    DefaultDevice,
296
297    /// Use an externally created, existing, wgpu setup. This is helpful when using `CubeCL` in conjunction
298    /// with some existing wgpu setup (eg. egui or bevy), as resources can be transferred in & out of `CubeCL`.
299    ///
300    /// # Notes
301    ///
302    /// This identifies an already registered runtime. To register an application's
303    /// setup, use `Device::wgpu_options().setup(setup).init()` and retain the
304    /// returned device. Clone that device to share it; do not invent an existing ID.
305    Existing(u32),
306}
307
308impl Device {
309    /// Create a reusable graph-capture device.
310    ///
311    /// Operations on tensors moved to this device are recorded rather than executed. Use
312    /// [`Device::capture_scope`] to delimit each capture and declare its graph boundaries.
313    #[cfg(feature = "capture")]
314    pub fn capture() -> Self {
315        Self::new(DispatchDevice::capture())
316    }
317
318    /// Capture the operations performed by `capture` on this device.
319    ///
320    /// The closure receives a [`CaptureScope`] and must return the token produced by
321    /// [`CaptureScope::complete`], containing the ordered runtime input and output tensor IDs.
322    /// Requiring this return value prevents a capture from being finalized without an explicit
323    /// boundary declaration. Completing the scope immediately rejects further tensor operations;
324    /// the device can then be reused for later, independent scopes after the closure returns.
325    ///
326    /// Returns [`CaptureError::InvalidDevice`] if this is not a capture device, and
327    /// [`CaptureError::AlreadyActive`] if another scope is active on the same device.
328    #[cfg(feature = "capture")]
329    pub fn capture_scope(
330        &self,
331        capture: impl FnOnce(CaptureScope) -> CompletedCaptureScope,
332    ) -> Result<CapturedGraph, CaptureError> {
333        match self.as_dispatch() {
334            DispatchDevice::Capture(device) => device.capture_scope(capture),
335            #[allow(unreachable_patterns)] // Capture can be the only enabled backend.
336            _ => Err(CaptureError::InvalidDevice),
337        }
338    }
339
340    /// Default CPU device backed by CubeCL's CPU backend.
341    #[cfg(feature = "cpu")]
342    pub fn cpu() -> Self {
343        Self::new(burn_dispatch::devices::CpuDevice)
344    }
345
346    /// CUDA device at the given hardware index.
347    ///
348    /// Accepts a plain integer (e.g. `Device::cuda(0)`) or a
349    /// [`DeviceIndex`] — use [`DeviceIndex::Default`] to let the backend
350    /// pick.
351    #[cfg(feature = "cuda")]
352    pub fn cuda(index: impl Into<DeviceIndex>) -> Self {
353        Self::new(burn_dispatch::devices::CudaDevice::new(
354            index.into().resolve(),
355        ))
356    }
357
358    /// ROCm/HIP device at the given hardware index.
359    ///
360    /// Same selector semantics as [`Device::cuda`].
361    #[cfg(feature = "rocm")]
362    pub fn rocm(index: impl Into<DeviceIndex>) -> Self {
363        Self::new(burn_dispatch::devices::RocmDevice::new(
364            index.into().resolve(),
365        ))
366    }
367
368    /// Flex backend device.
369    #[cfg(feature = "flex")]
370    pub fn flex() -> Self {
371        Self::new(burn_dispatch::devices::FlexDevice)
372    }
373
374    /// WGPU device, selected via [`DeviceKind`].
375    ///
376    /// [`AutoCompiler`](burn_dispatch::devices::AutoCompiler) dispatches to the most
377    /// appropriate shader language (WGSL, SPIR-V, or MSL) based on the enabled features.
378    ///
379    /// For [`DeviceKind::DefaultDevice`], the adapter is picked by `wgpu`'s
380    /// selection heuristics (high-power GPU preferred, or overridden by
381    /// `CUBECL_WGPU_DEFAULT_DEVICE`).
382    ///
383    /// The graphics API — and so the shader compiler — is the runtime's to settle, from the
384    /// enabled features and what the machine offers. `Device::vulkan`, `Device::metal` and
385    /// `Device::webgpu` pin one instead: the same adapter on two APIs is two devices.
386    ///
387    /// This constructor is lazy. Use [`Device::wgpu_options`] to initialize with runtime
388    /// options or share an existing wgpu setup. In a browser, initialize asynchronously
389    /// with [`init_async`](crate::wgpu::WgpuOptions::init_async) before creating tensors.
390    #[cfg(feature = "wgpu")]
391    pub fn wgpu(device_kind: DeviceKind) -> Self {
392        Self::new(wgpu_device(
393            device_kind,
394            burn_dispatch::devices::WgpuBackend::Auto,
395        ))
396    }
397
398    /// Vulkan-backed WGPU device, selected via [`DeviceKind`] — and so `SPIR-V`, where the
399    /// build and the adapter allow it.
400    ///
401    /// Pinned to Vulkan: the device comes up there or not at all, rather than quietly on
402    /// whichever API [`Device::wgpu`] would settle on.
403    #[cfg(feature = "vulkan")]
404    pub fn vulkan(device_kind: DeviceKind) -> Self {
405        Self::new(wgpu_device(
406            device_kind,
407            burn_dispatch::devices::WgpuBackend::Vulkan,
408        ))
409    }
410
411    /// Metal-backed WGPU device, selected via [`DeviceKind`], requiring native MSL support.
412    ///
413    /// Pinned to Metal, as `Device::vulkan` is to Vulkan. Runtime initialization panics if
414    /// native MSL is unavailable on the selected device. Use [`Device::wgpu`] for automatic
415    /// compiler selection with WGSL fallback.
416    #[cfg(feature = "metal")]
417    pub fn metal(device_kind: DeviceKind) -> Self {
418        Self::new(wgpu_device(
419            device_kind,
420            burn_dispatch::devices::WgpuBackend::Metal,
421        ))
422    }
423
424    /// WebGPU-backed device, selected via [`DeviceKind`] — the browser's own. Pinned to
425    /// WebGPU, as `Device::vulkan` is to Vulkan.
426    #[cfg(feature = "webgpu")]
427    pub fn webgpu(device_kind: DeviceKind) -> Self {
428        Self::new(wgpu_device(
429            device_kind,
430            burn_dispatch::devices::WgpuBackend::WebGpu,
431        ))
432    }
433
434    /// Enables autodiff on this device.
435    ///
436    /// Tensors created on the returned device inherit its autodiff context. A tensor can later
437    /// change that context independently with [`Tensor::autodiff`](crate::Tensor::autodiff) or
438    /// [`Tensor::without_autodiff`](crate::Tensor::without_autodiff).
439    ///
440    /// Calling this method on a device that already has autodiff enabled returns it unchanged,
441    /// preserving its gradient-checkpointing strategy. This operation is idempotent.
442    /// Calling it repeatedly doesn't enable higher-order differentiation; only first-order
443    /// autodiff is supported.
444    ///
445    /// # Example
446    ///
447    /// ```rust,ignore
448    /// let device = Device::default().autodiff();
449    /// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device).require_grad();
450    /// let gradients = x.backward();
451    /// ```
452    ///
453    #[cfg(feature = "autodiff")]
454    #[must_use]
455    pub fn autodiff(self) -> Self {
456        match self.into_dispatch() {
457            device @ DispatchDevice::Autodiff(_) => Self::new(device),
458            other => Self::new(DispatchDevice::autodiff(other)),
459        }
460    }
461
462    /// Returns this device's gradient-checkpointing strategy when autodiff is enabled.
463    #[cfg(feature = "autodiff")]
464    pub fn gradient_checkpointing_strategy(&self) -> Option<GradientCheckpointingStrategy> {
465        match self.as_dispatch() {
466            DispatchDevice::Autodiff(device) => Some(device.gradient_checkpointing_strategy()),
467            _ => None,
468        }
469    }
470
471    /// Enables gradient checkpointing on the autodiff device.
472    ///
473    /// Gradient checkpointing recomputes activations during backpropagation for operations
474    /// marked as memory-bound, while compute-bound operations still cache their
475    /// output. This reduces peak memory usage at the cost of additional computation
476    /// for memory-bound ops.
477    ///
478    /// # Example
479    ///
480    /// ```rust,ignore
481    /// let device = Device::default().autodiff().gradient_checkpointing();
482    /// ```
483    ///
484    /// # Panics
485    ///
486    /// Panics if autodiff is not enabled on this device.
487    #[cfg(feature = "autodiff")]
488    #[must_use]
489    pub fn gradient_checkpointing(self) -> Self {
490        self.with_gradient_checkpointing_strategy(GradientCheckpointingStrategy::Balanced)
491    }
492
493    /// Sets the gradient-checkpointing strategy on this autodiff device.
494    #[cfg(feature = "autodiff")]
495    #[must_use]
496    pub(crate) fn with_gradient_checkpointing_strategy(
497        self,
498        strategy: GradientCheckpointingStrategy,
499    ) -> Self {
500        match self.into_dispatch() {
501            DispatchDevice::Autodiff(device) => Self::new(
502                DispatchDevice::autodiff_with_gradient_checkpointing(device.inner(), strategy),
503            ),
504            _ => panic!(
505                "Gradient checkpointing requires autodiff; use Device::autodiff().gradient_checkpointing()"
506            ),
507        }
508    }
509
510    /// Returns this device without its autodiff association.
511    ///
512    /// If autodiff is not enabled, the device is returned unchanged. This operation is idempotent.
513    ///
514    /// # Example
515    ///
516    /// ```rust,ignore
517    /// let device = Device::default().autodiff();
518    /// let inference_device = device.without_autodiff();
519    ///
520    /// assert!(!inference_device.is_autodiff());
521    /// ```
522    #[must_use]
523    pub fn without_autodiff(self) -> Self {
524        if self.is_autodiff() {
525            Self::new(self.into_dispatch().inner())
526        } else {
527            self
528        }
529    }
530
531    /// Applies `source`'s autodiff association and gradient-checkpointing strategy to this device,
532    /// discarding this device's own.
533    #[must_use]
534    #[doc(hidden)]
535    pub fn with_autodiff_context_from(self, source: &Self) -> Self {
536        #[cfg(feature = "autodiff")]
537        {
538            match source.gradient_checkpointing_strategy() {
539                Some(strategy) => self
540                    .autodiff()
541                    .with_gradient_checkpointing_strategy(strategy),
542                None => self.without_autodiff(),
543            }
544        }
545
546        #[cfg(not(feature = "autodiff"))]
547        {
548            let _ = source;
549            self
550        }
551    }
552
553    /// Returns this device without its autodiff association.
554    ///
555    /// This is equivalent to [`without_autodiff`](Self::without_autodiff). `inner` reflects the
556    /// historical backend-decorator model, while `without_autodiff` describes the device's runtime
557    /// property directly.
558    #[must_use]
559    pub fn inner(self) -> Self {
560        self.without_autodiff()
561    }
562
563    /// Synchronize the device, waiting for all pending operations to complete.
564    ///
565    /// # Errors
566    ///
567    /// Returns an [`ExecutionError`] if an operation failed to execute.
568    pub fn sync(&self) -> Result<(), ExecutionError> {
569        Dispatch::sync(self.as_dispatch())
570    }
571
572    /// Flush the device's pending operations, handing them off for execution without waiting
573    /// for them to complete.
574    ///
575    /// Backends that buffer work hold registered operations in a local queue until enough
576    /// accumulate: the fusion backend batches ops to build optimizations, and the remote backend
577    /// batches them before sending them over the network. `flush` forces that queue out now — the
578    /// fusion backend processes its pending optimizations and the remote backend sends its batch to
579    /// the server.
580    ///
581    /// Unlike [`sync`](Self::sync), this does not block on results — it only ensures buffered
582    /// operations are dispatched instead of sitting idle. Eager backends, which execute each
583    /// operation as it is registered, have nothing buffered and treat this as a no-op.
584    ///
585    /// # Errors
586    ///
587    /// Returns an [`ExecutionError`] when the buffered operations cannot be dispatched, e.g. on a
588    /// device that is poisoned.
589    pub fn flush(&self) -> Result<(), ExecutionError> {
590        Dispatch::flush(self.as_dispatch())
591    }
592
593    /// Measure how long this device spends on the work `func` puts on it, in
594    /// device time.
595    ///
596    /// The measurement is a [`ProfileDuration`]: a future the device answers
597    /// once it has run both ends of the window, so nothing here waits on the
598    /// device, and windows nest — an inner `profile` costs the outer one
599    /// nothing. Collect them and [`resolve`](ProfileDuration::resolve) once
600    /// the run is over.
601    ///
602    /// The window spans the stream from the call to `func`'s return. Work the
603    /// stream still owed from before falls in; work a backend queues past the
604    /// end falls out — the fusion backend holds a closure's last operations
605    /// back to batch them, so a window over lazy work alone can read as
606    /// empty. Ending the closure with a read, or
607    /// [`profile_with`](Self::profile_with) and
608    /// [`ProfileOptions::flush`], closes the window over all of it. A window
609    /// that nothing ran in reads as no time.
610    ///
611    /// A backend with no device clock (such as a remote device
612    /// whose server has none) measures wall-clock time between two syncs
613    /// instead: that one waits, and an inner window's syncs are charged to
614    /// the outer.
615    ///
616    /// ```rust,ignore
617    /// let (output, duration) = device.profile(|| model.forward(input))?;
618    /// // Later, once the run is over:
619    /// let ticks = duration.resolve().await.expect("the window carried work");
620    /// println!("forward: {:?}", ticks.duration());
621    /// ```
622    ///
623    /// # Errors
624    ///
625    /// Returns an [`ExecutionError`] when the device refuses to open or close
626    /// the window — a remote device does from a browser thread, which cannot
627    /// wait on the server.
628    pub fn profile<O: Send + 'static>(
629        &self,
630        func: impl FnOnce() -> O + Send,
631    ) -> Result<(O, ProfileDuration), ExecutionError> {
632        self.profile_with(ProfileOptions::default(), func)
633    }
634
635    /// [`profile`](Self::profile) with [`ProfileOptions`].
636    pub fn profile_with<O: Send + 'static>(
637        &self,
638        options: ProfileOptions,
639        func: impl FnOnce() -> O + Send,
640    ) -> Result<(O, ProfileDuration), ExecutionError> {
641        Dispatch::profile(self.as_dispatch(), options, func)
642    }
643
644    /// Seeds the random number generator for this device.
645    ///
646    /// Seeding before tensor operations that involve randomness (e.g. [`Tensor::random`](crate::Tensor::random))
647    /// makes those operations reproducible in a single-threaded program.
648    ///
649    /// # Note
650    ///
651    /// Depending on the backend, the seed may be applied globally rather than scoped
652    /// to this specific device. It is guaranteed that at least this device will be seeded.
653    ///
654    /// # Example
655    ///
656    /// ```rust,ignore
657    /// let device = Default::default();
658    /// device.seed(42);
659    /// let t = Tensor::<1>::random([8], Distribution::Default, &device);
660    /// ```
661    pub fn seed(&self, seed: u64) {
662        Dispatch::seed(self.as_dispatch(), seed)
663    }
664
665    /// Returns whether this device is associated with autodiff.
666    ///
667    /// This is device context inherited by newly created tensors, not a statement that any tensor
668    /// participates in a graph or retains gradients.
669    ///
670    /// # Example
671    ///
672    /// ```rust,ignore
673    /// let device = Default::default();
674    /// assert!(!device.is_autodiff());
675    ///
676    /// let ad_device = device.autodiff();
677    /// assert!(ad_device.is_autodiff());
678    /// ```
679    pub fn is_autodiff(&self) -> bool {
680        Dispatch::ad_enabled(self.as_dispatch())
681    }
682
683    /// Returns `true` if this device supports `dtype` for general computation:
684    /// storage, conversion, *and* arithmetic.
685    ///
686    /// A type can be less than generally supported — bf16 on a Vulkan device,
687    /// for example, is often storable and convertible but has no arithmetic
688    /// (SPIR-V's `SPV_KHR_bfloat16` permits only conversions, dot products,
689    /// and cooperative-matrix use). Computing in such a type produces
690    /// backend-dependent garbage, so check before selecting a reduced
691    /// precision:
692    ///
693    /// ```rust,ignore
694    /// let dtype = if device.supports_dtype(FloatDType::BF16) {
695    ///     FloatDType::BF16
696    /// } else {
697    ///     FloatDType::F32
698    /// };
699    /// ```
700    pub fn supports_dtype(&self, dtype: impl Into<burn_std::DType>) -> bool {
701        Dispatch::supports_dtype(self.as_dispatch(), dtype.into())
702    }
703
704    /// Sets the current allocation mode to persistent.
705    pub fn memory_persistent_allocations<
706        Output: Send,
707        Input: Send,
708        Func: Fn(Input) -> Output + Send,
709    >(
710        &self,
711        input: Input,
712        func: Func,
713    ) -> Output {
714        Dispatch::memory_persistent_allocations(self.as_dispatch(), input, func)
715    }
716
717    /// Triggers a memory cleanup on this device.
718    ///
719    /// The amount of memory reclaimed depends on the allocator implementation.
720    /// Calling this method does not guarantee that any memory will be freed.
721    pub fn memory_cleanup(&self) {
722        Dispatch::memory_cleanup(self.as_dispatch());
723    }
724
725    /// This device's dynamic pools, in the order they were installed. `None` on
726    /// a backend that does not report them.
727    pub fn memory_pool_report(&self) -> Option<Vec<SlicedPoolReport>> {
728        Dispatch::memory_pool_report(self.as_dispatch())
729    }
730
731    /// What this device's allocator currently holds. `None` on a backend that
732    /// does not report it.
733    pub fn memory_pool_usage(&self) -> Option<MemoryPoolUsage> {
734        Dispatch::memory_pool_usage(self.as_dispatch())
735    }
736
737    /// How many bytes this device can hold at once, as its runtime states it: a budget to size a
738    /// workload by, not what is free. `None` on a backend that states no figure.
739    pub fn memory_capacity(&self) -> Option<u64> {
740        Dispatch::memory_capacity(self.as_dispatch())
741    }
742
743    /// Prepares the given data for transfer between the CPU and accelerator devices such as GPUs.
744    ///
745    /// Depending on the backend, the data may be transferred to pinned memory
746    /// or another transfer-optimized format to improve transfer performance.
747    pub fn staging<'a, Iter>(&self, data: Iter)
748    where
749        Iter: Iterator<Item = &'a mut TensorData>,
750    {
751        Dispatch::staging(data, self.as_dispatch());
752    }
753
754    /// Returns the [`DeviceSettings`] for this device.
755    ///
756    /// Settings include the default float, integer, and boolean data types used when creating
757    /// tensors on this device.
758    ///
759    /// Before initialization, returns a snapshot of the backend defaults without locking them.
760    /// Another thread may configure the device afterward, so subsequent tensor operations may use
761    /// different settings. [Configure the device](Device::configure) before relying on its defaults.
762    pub fn settings(&self) -> DeviceSettings {
763        burn_backend::get_device_settings::<Dispatch>(self.as_dispatch())
764    }
765
766    /// Returns settings for tensor operations, initializing them to defaults if unset.
767    pub(crate) fn get_or_init_settings(&self) -> DeviceSettings {
768        burn_backend::get_or_init_device_settings::<Dispatch>(self.as_dispatch())
769    }
770
771    /// Configures the [settings](DeviceSettings) for this device.
772    ///
773    /// This configures the dtype used when no explicit type is specified at tensor
774    /// creation time.
775    ///
776    /// Settings can only be initialized once per device. Configure defaults before creating
777    /// tensors or initializing model parameters: tensor creation locks them to the backend's
778    /// defaults, even with an explicit dtype, and any later call returns
779    /// [`DeviceError::AlreadyInitialized`]. Querying [`settings`](Device::settings) does not lock them.
780    ///
781    /// Individual tensors can still use an explicit supported dtype at creation or be converted
782    /// with [`Tensor::cast`](crate::Tensor::cast); neither changes the defaults.
783    ///
784    /// # Errors
785    ///
786    /// Returns [`DeviceError::UnsupportedDType`] if a requested dtype is unsupported.
787    /// Returns [`DeviceError::AlreadyInitialized`] if settings have already been initialized
788    /// for this device, either by a prior call or by a tensor operation.
789    ///
790    /// # Example
791    ///
792    /// ```rust,ignore
793    /// use burn_tensor::{Device, FloatDType, Int, IntDType, Tensor};
794    ///
795    /// let mut device = Device::cuda(0);
796    /// device.configure((FloatDType::F16, IntDType::I32))?;
797    ///
798    /// // Float tensors will now use F16
799    /// let floats = Tensor::<2>::zeros([2, 3], &device);
800    /// // Int tensors will now use I32
801    /// let ints = Tensor::<2, Int>::zeros([2, 3], &device);
802    /// ```
803    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
804        let mut config = config.into();
805
806        let defaults = self.as_dispatch().defaults();
807
808        let float_dtype = config.float_dtype.take().unwrap_or(defaults.float_dtype);
809        let int_dtype = config.int_dtype.take().unwrap_or(defaults.int_dtype);
810        let bool_dtype = config.bool_dtype.take().unwrap_or(defaults.bool_dtype);
811
812        burn_backend::set_default_dtypes::<Dispatch>(
813            self.as_dispatch(),
814            float_dtype,
815            int_dtype,
816            bool_dtype,
817        )
818    }
819
820    /// Retrieves all available [`Device`]s that match the given [`DeviceType`] filter.
821    ///
822    /// Backends enumerate the hardware found on this machine, and `DeviceType::Remote` every
823    /// device a remote server hosts:
824    ///
825    /// ```rust,ignore
826    /// // Every CUDA device on this machine.
827    /// let local = Device::enumerate(DeviceType::Cuda);
828    ///
829    /// // Filters combine with `|`.
830    /// let both = Device::enumerate(DeviceType::Cuda | DeviceType::Remote(host));
831    /// ```
832    ///
833    /// # Panics
834    ///
835    /// Where `RemoteHost::devices` returns an error for a `DeviceType::Remote`, and on wasm for any
836    /// `DeviceType::Remote`, which a browser can only list with `RemoteHost::devices_async`.
837    pub fn enumerate(filter: impl Into<DeviceFilter>) -> Devices {
838        #[allow(unused)]
839        let mut devices = Vec::new();
840
841        #[allow(clippy::never_loop)] // at least one backend is expected to be enabled.
842        for device_type in filter.into() {
843            #[allow(unused)]
844            let type_id = match device_type {
845                // The cubecl runtimes are one dispatch backend, so `DispatchDeviceId::Cube`
846                // would list every runtime's devices at once. These name the runtime the
847                // caller asked for instead. `Metal`, `Vulkan` and `WebGpu` pin wgpu to the
848                // requested graphics API; `Wgpu` leaves the API to auto-selection.
849                #[cfg(feature = "cpu")]
850                DeviceType::Cpu => {
851                    push_cube(&mut devices, RuntimeId::Cpu);
852                    continue;
853                }
854                #[cfg(feature = "cuda")]
855                DeviceType::Cuda => {
856                    push_cube(&mut devices, RuntimeId::Cuda);
857                    continue;
858                }
859                #[cfg(feature = "rocm")]
860                DeviceType::Rocm => {
861                    push_cube(&mut devices, RuntimeId::Hip);
862                    continue;
863                }
864                #[cfg(feature = "wgpu")]
865                DeviceType::Wgpu => {
866                    push_cube(&mut devices, RuntimeId::Wgpu);
867                    continue;
868                }
869                #[cfg(feature = "metal")]
870                DeviceType::Metal => {
871                    push_wgpu(&mut devices, WgpuBackend::Metal);
872                    continue;
873                }
874                #[cfg(feature = "vulkan")]
875                DeviceType::Vulkan => {
876                    push_wgpu(&mut devices, WgpuBackend::Vulkan);
877                    continue;
878                }
879                #[cfg(feature = "webgpu")]
880                DeviceType::WebGpu => {
881                    push_wgpu(&mut devices, WgpuBackend::WebGpu);
882                    continue;
883                }
884                #[cfg(feature = "flex")]
885                DeviceType::Flex => DispatchDeviceId::Flex,
886                #[cfg(feature = "remote")]
887                DeviceType::Remote(host) => {
888                    devices.extend(host.enumerate());
889                    continue;
890                }
891            };
892
893            #[allow(unreachable_code)] // need to have one backend enabled, so it is reachable
894            for device in Dispatch::enumerate(type_id) {
895                devices.push(Device::new(device))
896            }
897        }
898
899        Devices(devices)
900    }
901
902    /// Who this device is: the part's name, what its kernels are compiled for and, for a card
903    /// on a bus, the card itself. `None` for a backend that does not report one. Opens the
904    /// device.
905    #[cfg(feature = "cubecl")]
906    pub fn identity(&self) -> Option<DeviceIdentity> {
907        self.as_dispatch().identity()
908    }
909
910    /// Every card this build can reach, once each, with the devices that reach it.
911    ///
912    /// A card visible to two runtimes (an NVIDIA GPU under CUDA and under Vulkan) is one entry
913    /// holding both devices, so a caller placing work on cards never puts two stages on one
914    /// card by another name. Opens every device of every runtime, so call it once and keep
915    /// the answer.
916    #[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
917    pub fn enumerate_physical() -> Vec<PhysicalGpu> {
918        let mut gpus: Vec<PhysicalGpu> = Vec::new();
919        for device in Dispatch::enumerate(DispatchDeviceId::Cube) {
920            let device = Device::new(device);
921            let Some(DeviceIdentity {
922                name,
923                physical: Some(physical),
924                ..
925            }) = device.identity()
926            else {
927                continue;
928            };
929            match gpus
930                .iter_mut()
931                .find(|gpu| gpu.physical.is_same_card(&physical))
932            {
933                Some(gpu) => {
934                    gpu.physical.fill_from(&physical);
935                    gpu.devices.0.push(device);
936                }
937                None => gpus.push(PhysicalGpu {
938                    physical,
939                    name,
940                    devices: Devices(vec![device]),
941                }),
942            }
943        }
944        gpus
945    }
946
947    /// Measure peak compute and memory throughput for this device.
948    ///
949    /// Runs cubecl-std's throughput benchmarks for each [`ThroughputKey`],
950    /// returning one [`ThroughputStat`] per key (in the same order). A key the
951    /// device has no peak for keeps its place, carrying the [`ThroughputError`]
952    /// saying why. Only cubecl-backed devices (cuda, wgpu, ...) report
953    /// measurements; other backends return an empty vector.
954    #[cfg(feature = "cubecl")]
955    pub fn performance_stats(&self, keys: &[ThroughputKey]) -> Vec<ThroughputStat> {
956        self.as_dispatch()
957            .performance_stats(keys)
958            .into_iter()
959            .zip(keys.iter().copied())
960            .map(|(value, key)| ThroughputStat { key, value })
961            .collect()
962    }
963}
964
965/// One card and every device that reaches it, from [`Device::enumerate_physical`].
966#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
967#[derive(Debug, Clone)]
968pub struct PhysicalGpu {
969    /// The card's PCI address, Windows LUID and vendor, from whichever of its runtimes reported
970    /// each.
971    pub physical: PhysicalDevice,
972    /// The part as the first runtime that reached it names it.
973    pub name: String,
974    /// Every device that runs on this card, one per runtime that reaches it.
975    pub devices: Devices,
976}
977
978/// A single peak-throughput measurement produced by [`Device::performance_stats`].
979#[cfg(feature = "cubecl")]
980#[derive(Debug, Clone, Copy, PartialEq)]
981pub struct ThroughputStat {
982    /// The measurement key (mode + dtype) that was benchmarked.
983    pub key: ThroughputKey,
984    /// The measured throughput for that key, or why the device has none.
985    pub value: Result<ThroughputValue, ThroughputError>,
986}
987
988/// Short, column-friendly name for a throughput mode.
989#[cfg(feature = "cubecl")]
990fn mode_label(mode: &ThroughputMode) -> &'static str {
991    match mode {
992        ThroughputMode::ComputeDirect { .. } => "compute-direct",
993        ThroughputMode::ComputeCmma { .. } => "compute-cmma",
994        ThroughputMode::Memory(spec) => match spec.access {
995            MemoryAccess::Copy => "memory",
996            MemoryAccess::Read => "memory-read",
997            MemoryAccess::Write => "memory-write",
998        },
999        ThroughputMode::Launch => "launch",
1000    }
1001}
1002
1003#[cfg(feature = "cubecl")]
1004impl core::fmt::Display for ThroughputStat {
1005    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1006        // Width/alignment flags are ignored on `ThroughputMode`/`ElemType` directly
1007        // (their fmt impls don't call `f.pad`), so render them to `String`s first —
1008        // `str`'s `Display` honors padding. The mode is labelled by hand rather than
1009        // derived through `Debug`: its variants carry payloads that would blow out the column.
1010        let mode = mode_label(&self.key.mode);
1011
1012        // `ThroughputKey::dtype()` reports f32 for the modes that don't compute with an
1013        // element type, so blank the column there rather than print a misleading type.
1014        let dtype = match self.key.mode {
1015            ThroughputMode::ComputeDirect { dtype } | ThroughputMode::ComputeCmma { dtype, .. } => {
1016                alloc::format!("{dtype}")
1017            }
1018            ThroughputMode::Memory(_) | ThroughputMode::Launch => alloc::string::String::new(),
1019        };
1020
1021        let value = match &self.value {
1022            Ok(value) => value.format(&self.key),
1023            Err(error) => alloc::format!("{error}"),
1024        };
1025
1026        write!(f, "{mode:<14} {dtype:<5} {value}")
1027    }
1028}
1029
1030/// Append every device of the cubecl `runtime` to `devices`, skipping any already listed.
1031///
1032/// A filter can name the same runtime twice (e.g. two `DeviceType::Cuda` entries),
1033/// and a caller enumerating hardware should see each device once.
1034#[cfg(any(feature = "cpu", feature = "cuda", feature = "rocm", feature = "wgpu"))]
1035fn push_cube(devices: &mut Vec<Device>, runtime: RuntimeId) {
1036    for device in Dispatch::enumerate_cube(runtime) {
1037        let device = Device::new(device);
1038        if !devices.contains(&device) {
1039            devices.push(device);
1040        }
1041    }
1042}
1043
1044/// Append every wgpu device on `backend`, skipping any already listed.
1045#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
1046fn push_wgpu(devices: &mut Vec<Device>, backend: WgpuBackend) {
1047    push_wgpu_with(
1048        devices,
1049        backend,
1050        burn_dispatch::devices::CubeDevice::enumerate,
1051    );
1052}
1053
1054#[cfg(any(feature = "metal", feature = "vulkan", feature = "webgpu"))]
1055fn push_wgpu_with(
1056    devices: &mut Vec<Device>,
1057    backend: WgpuBackend,
1058    mut enumerate: impl FnMut(DeviceId) -> Vec<DeviceId>,
1059) {
1060    use burn_dispatch::devices::{CubeDevice, WgpuDevice, WgpuDeviceKind};
1061
1062    // Enumeration preserves the seed's kind and graphics API. Ask for each concrete
1063    // kind on the requested API: Auto may expose different kinds or fewer adapters.
1064    // DefaultDevice is an alias, and Existing devices cannot be discovered.
1065    for kind in [
1066        WgpuDeviceKind::DiscreteGpu(0),
1067        WgpuDeviceKind::IntegratedGpu(0),
1068        WgpuDeviceKind::VirtualGpu(0),
1069        WgpuDeviceKind::Cpu,
1070        WgpuDeviceKind::Other(0),
1071    ] {
1072        let seed = CubeDevice::Wgpu(WgpuDevice::new(kind).on(backend));
1073        for id in enumerate(seed.to_id()) {
1074            let device = Device::new(CubeDevice::from_id(id));
1075            if !devices.contains(&device) {
1076                devices.push(device);
1077            }
1078        }
1079    }
1080}
1081
1082/// Map our backend-agnostic [`DeviceKind`] onto cubecl's `WgpuDevice`, on `backend`.
1083///
1084/// Shared by [`Device::wgpu`], which leaves the graphics API to the runtime, and
1085/// [`Device::vulkan`], [`Device::metal`] and [`Device::webgpu`], which each pin theirs.
1086#[cfg(feature = "wgpu")]
1087pub(crate) fn wgpu_device(
1088    device_kind: DeviceKind,
1089    backend: burn_dispatch::devices::WgpuBackend,
1090) -> burn_dispatch::devices::WgpuDevice {
1091    use burn_dispatch::devices::{WgpuDevice, WgpuDeviceKind};
1092
1093    let kind = match device_kind {
1094        DeviceKind::DiscreteGpu(i) => WgpuDeviceKind::DiscreteGpu(i),
1095        DeviceKind::IntegratedGpu(i) => WgpuDeviceKind::IntegratedGpu(i),
1096        DeviceKind::VirtualGpu(i) => WgpuDeviceKind::VirtualGpu(i),
1097        DeviceKind::Cpu => WgpuDeviceKind::Cpu,
1098        DeviceKind::DefaultDevice => WgpuDeviceKind::DefaultDevice,
1099        DeviceKind::Existing(id) => WgpuDeviceKind::Existing(id),
1100    };
1101
1102    WgpuDevice::new(kind).on(backend)
1103}
1104
1105/// Represents the devices that can be used.
1106///
1107/// `DeviceType` is used to filter the available device types for [`Device::enumerate`]. Each
1108/// variant selects a backend's hardware on this machine, except `Remote`, which selects a remote
1109/// server's devices.
1110///
1111/// Variants combine into a [`DeviceFilter`] with the `|` operator, so a single
1112/// [`Device::enumerate`] call can span several backends and remote servers.
1113#[allow(missing_docs)]
1114#[derive(Debug, Clone, PartialEq, Eq)]
1115pub enum DeviceType {
1116    #[cfg(feature = "cpu")]
1117    Cpu,
1118    #[cfg(feature = "cuda")]
1119    Cuda,
1120    #[cfg(feature = "rocm")]
1121    Rocm,
1122    #[cfg(feature = "wgpu")]
1123    Wgpu,
1124    #[cfg(feature = "metal")]
1125    Metal,
1126    #[cfg(feature = "vulkan")]
1127    Vulkan,
1128    #[cfg(feature = "webgpu")]
1129    WebGpu,
1130    #[cfg(feature = "flex")]
1131    Flex,
1132    /// Every device the remote server `host` hosts, connected on first use like any listed device.
1133    /// [`RemoteHost::devices`](crate::remote::RemoteHost::devices) does the same and returns an
1134    /// error where `enumerate` panics.
1135    #[cfg(feature = "remote")]
1136    Remote(crate::remote::RemoteHost),
1137}
1138
1139/// A set of [`DeviceType`]s passed to [`Device::enumerate`].
1140///
1141/// Built from a single [`DeviceType`], a `Vec<DeviceType>`, or by combining variants with the
1142/// `|` operator (`DeviceType::Cuda | DeviceType::Cpu`).
1143#[derive(Debug, Clone, Default)]
1144pub struct DeviceFilter(Vec<DeviceType>);
1145
1146impl DeviceFilter {
1147    /// Create an empty filter.
1148    pub fn new() -> Self {
1149        Self::default()
1150    }
1151
1152    /// Add a [`DeviceType`] to the filter.
1153    pub fn with(mut self, device_type: DeviceType) -> Self {
1154        self.0.push(device_type);
1155        self
1156    }
1157}
1158
1159impl From<DeviceType> for DeviceFilter {
1160    fn from(value: DeviceType) -> Self {
1161        DeviceFilter(vec![value])
1162    }
1163}
1164
1165impl From<Vec<DeviceType>> for DeviceFilter {
1166    fn from(value: Vec<DeviceType>) -> Self {
1167        DeviceFilter(value)
1168    }
1169}
1170
1171impl IntoIterator for DeviceFilter {
1172    type Item = DeviceType;
1173    type IntoIter = alloc::vec::IntoIter<DeviceType>;
1174    fn into_iter(self) -> Self::IntoIter {
1175        self.0.into_iter()
1176    }
1177}
1178
1179impl core::ops::BitOr for DeviceType {
1180    type Output = DeviceFilter;
1181    fn bitor(self, rhs: Self) -> DeviceFilter {
1182        DeviceFilter(vec![self, rhs])
1183    }
1184}
1185
1186impl core::ops::BitOr<DeviceType> for DeviceFilter {
1187    type Output = DeviceFilter;
1188    fn bitor(mut self, rhs: DeviceType) -> DeviceFilter {
1189        self.0.push(rhs);
1190        self
1191    }
1192}
1193
1194/// Configuration options used to initialize a device.
1195///
1196/// Unlike [`DeviceSettings`], this type represents partial user-provided
1197/// configuration and does not require all settings to be specified.
1198///
1199/// Any unspecified options will be resolved to device-specific defaults
1200/// when the device is initialized.
1201///
1202/// Use [`Device::configure`] to apply this configuration to a device.
1203#[derive(new, Debug, Clone, Default)]
1204pub struct DeviceConfig {
1205    /// Default floating-point data type.
1206    pub float_dtype: Option<FloatDType>,
1207
1208    /// Default integer data type.
1209    pub int_dtype: Option<IntDType>,
1210
1211    /// Default boolean data type.
1212    pub bool_dtype: Option<BoolDType>,
1213    // TODO: maybe quantization, but for now we keep this as device defaults
1214}
1215
1216impl DeviceConfig {
1217    /// Sets the default floating-point data type for tensors created on the device.
1218    pub fn float_dtype(mut self, dtype: impl Into<FloatDType>) -> Self {
1219        self.float_dtype = Some(dtype.into());
1220        self
1221    }
1222
1223    /// Sets the default integer data type for tensors created on the device.
1224    pub fn int_dtype(mut self, dtype: impl Into<IntDType>) -> Self {
1225        self.int_dtype = Some(dtype.into());
1226        self
1227    }
1228
1229    /// Sets the default boolean data type storage precision for tensors created on the device.
1230    pub fn bool_dtype(mut self, dtype: impl Into<BoolDType>) -> Self {
1231        self.bool_dtype = Some(dtype.into());
1232        self
1233    }
1234}
1235
1236impl From<FloatDType> for DeviceConfig {
1237    fn from(value: FloatDType) -> Self {
1238        DeviceConfig::new(Some(value), None, None)
1239    }
1240}
1241
1242impl From<IntDType> for DeviceConfig {
1243    fn from(value: IntDType) -> Self {
1244        DeviceConfig::new(None, Some(value), None)
1245    }
1246}
1247
1248impl From<BoolDType> for DeviceConfig {
1249    fn from(value: BoolDType) -> Self {
1250        DeviceConfig::new(None, None, Some(value))
1251    }
1252}
1253
1254impl From<(FloatDType, IntDType)> for DeviceConfig {
1255    fn from(value: (FloatDType, IntDType)) -> Self {
1256        DeviceConfig::new(Some(value.0), Some(value.1), None)
1257    }
1258}
1259
1260/// A collection of [`Device`]s returned by [`Device::enumerate`].
1261///
1262/// This type provides bulk operations and transformations over multiple
1263/// devices, such as enabling autodiff or configuring the device settings.
1264///
1265/// # Example
1266///
1267/// ```rust,ignore
1268/// let mut devices = Device::enumerate(DeviceType::Cuda)
1269///     .autodiff();
1270///
1271/// devices.configure(
1272///     DeviceConfig::default().float_dtype(FloatDType::F16),
1273/// )?;
1274/// ```
1275///
1276/// `Devices` dereferences to a slice of [`Device`], so it can be iterated,
1277/// indexed, and passed anywhere a `&[Device]` is expected.
1278#[derive(Debug, Clone)]
1279pub struct Devices(Vec<Device>);
1280
1281impl Devices {
1282    /// Enables autodiff across all contained devices.
1283    ///
1284    /// Devices that already have autodiff enabled are left unchanged, preserving their
1285    /// gradient-checkpointing strategy.
1286    ///
1287    /// See [`Device::autodiff`].
1288    #[cfg(feature = "autodiff")]
1289    #[must_use]
1290    pub fn autodiff(mut self) -> Self {
1291        for device in &mut self.0 {
1292            *device = core::mem::take(device).autodiff();
1293        }
1294
1295        self
1296    }
1297
1298    /// Removes the autodiff association from every contained device.
1299    ///
1300    /// See [`Device::without_autodiff`].
1301    #[must_use]
1302    pub fn without_autodiff(mut self) -> Self {
1303        for device in &mut self.0 {
1304            *device = core::mem::take(device).without_autodiff();
1305        }
1306
1307        self
1308    }
1309
1310    /// Configures the [settings](DeviceSettings) for all devices.
1311    ///
1312    /// This configures the dtype used when no explicit type is specified at tensor
1313    /// creation time.
1314    ///
1315    /// Settings can only be initialized once per device. Configure defaults before creating
1316    /// tensors or initializing model parameters; tensor creation locks them, even with an explicit
1317    /// dtype. Querying [`Device::settings`] does not lock them.
1318    ///
1319    /// Stops at the first error; devices configured before it keep their settings.
1320    ///
1321    /// See [`Device::configure`].
1322    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
1323        let config = config.into();
1324        for device in &mut self.0 {
1325            device.configure(config.clone())?;
1326        }
1327        Ok(())
1328    }
1329
1330    /// Returns the `Vec` of [`Device`]s.
1331    pub fn into_vec(self) -> Vec<Device> {
1332        self.0
1333    }
1334}
1335
1336// Loop over `&Devices` or `Devices` seamlessly
1337impl FromIterator<Device> for Devices {
1338    fn from_iter<I: IntoIterator<Item = Device>>(devices: I) -> Self {
1339        Self(devices.into_iter().collect())
1340    }
1341}
1342
1343impl IntoIterator for Devices {
1344    type Item = Device;
1345    type IntoIter = alloc::vec::IntoIter<Device>;
1346    fn into_iter(self) -> Self::IntoIter {
1347        self.0.into_iter()
1348    }
1349}
1350
1351impl core::ops::Deref for Devices {
1352    type Target = [Device];
1353    fn deref(&self) -> &Self::Target {
1354        &self.0
1355    }
1356}
1357
1358#[cfg(all(test, feature = "capture"))]
1359mod capture_tests {
1360    use super::*;
1361
1362    #[cfg(all(feature = "flex", feature = "autodiff"))]
1363    #[test]
1364    #[should_panic(expected = "Cannot move a tensor to a capture device with autodiff enabled")]
1365    fn capture_transfer_rejects_autodiff_context() {
1366        let tensor = crate::Tensor::<1, crate::Int>::from_ints([1, 2], &Device::flex().autodiff());
1367        let capture = Device::capture();
1368        let _ = capture.capture_scope(|scope| {
1369            let _ = tensor.to_device(&capture);
1370            scope.complete([], [])
1371        });
1372    }
1373
1374    #[cfg(all(feature = "flex", feature = "autodiff"))]
1375    #[test]
1376    #[should_panic(expected = "Cannot move a tensor to a capture device with autodiff enabled")]
1377    fn capture_float_transfer_rejects_autodiff_context() {
1378        let tensor =
1379            crate::Tensor::<1>::from_floats([1.0, 2.0], &Device::flex().autodiff()).require_grad();
1380        let capture = Device::capture();
1381        let _ = capture.capture_scope(|scope| {
1382            let _ = tensor.to_device(&capture);
1383            scope.complete([], [])
1384        });
1385    }
1386
1387    #[test]
1388    fn user_facing_capture_device_supports_repeated_scopes() {
1389        let device = Device::capture();
1390
1391        let first = device
1392            .capture_scope(|scope| scope.complete([], []))
1393            .unwrap();
1394        let second = device
1395            .capture_scope(|scope| scope.complete([], []))
1396            .unwrap();
1397
1398        assert!(first.graph.operations.is_empty());
1399        assert!(second.graph.operations.is_empty());
1400    }
1401
1402    #[test]
1403    fn capture_scope_rejects_a_non_capture_device() {
1404        let device = Device::default();
1405
1406        let result = device.capture_scope(|scope| scope.complete([], []));
1407
1408        assert!(matches!(result, Err(CaptureError::InvalidDevice)));
1409    }
1410
1411    #[test]
1412    fn capture_device_reports_recordable_dtype_support() {
1413        let device = Device::capture();
1414
1415        let captured = device.capture_scope(|scope| {
1416            assert!(device.supports_dtype(FloatDType::F32));
1417            assert!(device.supports_dtype(FloatDType::F64));
1418            assert!(device.supports_dtype(FloatDType::BF16));
1419            assert!(device.supports_dtype(IntDType::I32));
1420            assert!(device.supports_dtype(BoolDType::Native));
1421            scope.complete([], [])
1422        });
1423
1424        assert!(captured.is_ok());
1425    }
1426}
1427
1428#[cfg(all(test, feature = "flex", feature = "autodiff"))]
1429mod autodiff_move_tests {
1430    use crate::{Bool, Device, GradientCheckpointingStrategy, Int, Tensor, TensorData};
1431
1432    // A tensor without autodiff (e.g. a gradient) can be moved onto an autodiff device; it lands
1433    // on the underlying hardware and stays outside autodiff.
1434    #[test]
1435    fn move_non_autodiff_float_tensor_to_autodiff_device() {
1436        let device = Device::default();
1437        let ad_device = device.clone().autodiff();
1438
1439        let t = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &device);
1440        let moved = t.to_device(&ad_device);
1441
1442        assert!(!moved.is_autodiff());
1443        assert!(!moved.is_tracked());
1444        assert_eq!(
1445            moved.try_into_vec_as::<f32>().unwrap(),
1446            vec![1.0, 2.0, 3.0, 4.0]
1447        );
1448    }
1449
1450    #[test]
1451    fn move_autodiff_tensor_preserves_association_strategy_and_graph() {
1452        let device = Device::default();
1453        let ad_device = device.clone().autodiff().gradient_checkpointing();
1454
1455        let tensor = Tensor::<1>::from_floats([1.0, 2.0], &ad_device).require_grad();
1456        let moved = tensor.clone().to_device(&device);
1457
1458        assert!(moved.is_autodiff());
1459        assert!(moved.is_tracked());
1460        // The transfer is a differentiable operation, so its result is a derived tensor whose own
1461        // gradient isn't retained by default.
1462        assert!(!moved.is_require_grad());
1463        assert_eq!(
1464            moved.gradient_checkpointing_strategy(),
1465            Some(GradientCheckpointingStrategy::Balanced)
1466        );
1467
1468        let grads = moved.sum().backward();
1469        tensor
1470            .grad(&grads)
1471            .expect("the transfer should preserve the graph")
1472            .into_data()
1473            .assert_eq(&TensorData::from([1.0f32, 1.0]), true);
1474    }
1475
1476    #[test]
1477    fn move_non_float_tensors_preserves_context() {
1478        let device = Device::default();
1479        let ad_device = device.clone().autodiff().gradient_checkpointing();
1480
1481        let plain_int = Tensor::<1, Int>::from_ints([1, 2], &device).to_device(&ad_device);
1482        assert!(!plain_int.is_autodiff());
1483
1484        let autodiff_bool =
1485            Tensor::<1, Bool>::from_bool([true, false], &ad_device).to_device(&device);
1486        assert!(autodiff_bool.is_autodiff());
1487        assert_eq!(
1488            autodiff_bool.gradient_checkpointing_strategy(),
1489            Some(GradientCheckpointingStrategy::Balanced)
1490        );
1491    }
1492
1493    #[test]
1494    #[should_panic(
1495        expected = "Gradient checkpointing requires autodiff; use Device::autodiff().gradient_checkpointing()"
1496    )]
1497    fn gradient_checkpointing_requires_autodiff() {
1498        let _ = Device::default().gradient_checkpointing();
1499    }
1500}
1501
1502#[cfg(all(test, feature = "cuda", feature = "wgpu"))]
1503mod tests {
1504    use super::*;
1505
1506    /// `cargo test -p burn-tensor --features cuda,wgpu a_card_reached_by_two_runtimes -- --ignored`
1507    #[test]
1508    #[ignore = "needs an NVIDIA card reachable through CUDA and Vulkan"]
1509    fn a_card_reached_by_two_runtimes_is_listed_once() {
1510        let gpus = Device::enumerate_physical();
1511        let nvidia: Vec<_> = gpus
1512            .iter()
1513            .filter(|gpu| gpu.physical.vendor == Some(PciVendor::Nvidia))
1514            .collect();
1515        assert!(!nvidia.is_empty(), "no NVIDIA card in {gpus:?}");
1516        for gpu in &nvidia {
1517            assert!(
1518                gpu.physical.pci_address.is_some(),
1519                "{} has no PCI address",
1520                gpu.name
1521            );
1522            assert!(
1523                gpu.devices.len() >= 2,
1524                "{} is reached by {:?} only",
1525                gpu.name,
1526                gpu.devices
1527            );
1528        }
1529        for (i, gpu) in gpus.iter().enumerate() {
1530            for other in &gpus[i + 1..] {
1531                assert!(
1532                    !gpu.physical.is_same_card(&other.physical),
1533                    "one card listed twice: {gpu:?} and {other:?}"
1534                );
1535            }
1536        }
1537    }
1538}
1539
1540#[cfg(all(test, any(feature = "metal", feature = "vulkan", feature = "webgpu")))]
1541mod enumerate_wgpu_tests {
1542    use super::*;
1543    use burn_dispatch::devices::{CubeDevice, WgpuDevice, WgpuDeviceKind};
1544
1545    fn cube_device(kind: WgpuDeviceKind, backend: WgpuBackend) -> CubeDevice {
1546        CubeDevice::Wgpu(WgpuDevice::new(kind).on(backend))
1547    }
1548
1549    #[test]
1550    fn enumerate_requested_api_independently_of_auto() {
1551        for backend in [WgpuBackend::Metal, WgpuBackend::Vulkan, WgpuBackend::WebGpu] {
1552            // An existing Auto device must not limit discovery on the requested API
1553            // or be deduplicated with a device pinned to that API.
1554            let auto = cube_device(WgpuDeviceKind::DiscreteGpu(0), WgpuBackend::Auto);
1555            let expected = [
1556                WgpuDeviceKind::DiscreteGpu(0),
1557                WgpuDeviceKind::DiscreteGpu(1),
1558                WgpuDeviceKind::IntegratedGpu(0),
1559                WgpuDeviceKind::VirtualGpu(0),
1560                WgpuDeviceKind::Cpu,
1561                WgpuDeviceKind::Other(0),
1562            ]
1563            .map(|kind| cube_device(kind, backend));
1564            let enumerate = |seed: DeviceId| {
1565                let CubeDevice::Wgpu(seed_device) = CubeDevice::from_id(seed) else {
1566                    panic!("expected a wgpu seed");
1567                };
1568                assert_eq!(seed_device.backend, backend);
1569                expected
1570                    .iter()
1571                    .filter(|device| device.to_id().type_id == seed.type_id)
1572                    .map(CubeDevice::to_id)
1573                    .collect()
1574            };
1575
1576            let mut devices = vec![Device::new(auto.clone())];
1577            push_wgpu_with(&mut devices, backend, enumerate);
1578            let expected_devices: Vec<_> = core::iter::once(&auto)
1579                .chain(expected.iter())
1580                .cloned()
1581                .map(Device::new)
1582                .collect();
1583            assert_eq!(devices, expected_devices);
1584
1585            push_wgpu_with(&mut devices, backend, enumerate);
1586            assert_eq!(
1587                devices, expected_devices,
1588                "repeated filters must not duplicate devices"
1589            );
1590        }
1591    }
1592
1593    #[test]
1594    fn enumerate_unavailable_api_is_empty() {
1595        let mut devices = Vec::new();
1596        push_wgpu_with(&mut devices, WgpuBackend::Vulkan, |_| Vec::new());
1597        assert!(devices.is_empty());
1598    }
1599
1600    #[cfg(not(target_family = "wasm"))]
1601    #[test]
1602    fn enumerate_pins_the_graphics_api() {
1603        let cases = [
1604            #[cfg(feature = "metal")]
1605            (DeviceType::Metal, WgpuBackend::Metal),
1606            #[cfg(feature = "vulkan")]
1607            (DeviceType::Vulkan, WgpuBackend::Vulkan),
1608            #[cfg(feature = "webgpu")]
1609            (DeviceType::WebGpu, WgpuBackend::WebGpu),
1610        ];
1611        for (device_type, backend) in cases {
1612            for device in Device::enumerate(device_type).iter() {
1613                match device.as_dispatch() {
1614                    DispatchDevice::Cube(CubeDevice::Wgpu(wgpu)) => {
1615                        assert_eq!(wgpu.backend, backend);
1616                    }
1617                    other => panic!("expected a wgpu device, got {other:?}"),
1618                }
1619            }
1620        }
1621    }
1622}