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