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::{ThroughputKey, ThroughputMode, ThroughputValue};
7use burn_backend::{Backend, DeviceOps};
8#[allow(unused)]
9use burn_dispatch::DispatchDeviceId;
10use burn_dispatch::{Dispatch, DispatchDevice};
11use burn_std::{BoolDType, FloatDType, IntDType, TensorData};
12
13#[cfg(feature = "remote-websocket")]
14use alloc::string::String;
15use alloc::vec;
16use alloc::vec::Vec;
17
18/// A high-level device handle for tensor operations.
19///
20/// [`Device`] provides a unified interface to interact with the underlying compute backend.
21///
22/// Autodiff support is a property of the device rather than a separate type parameter.
23#[cfg_attr(
24    feature = "autodiff",
25    doc = "Wrap a device with [`.autodiff()`](Device::autodiff) to enable automatic differentiation with the device."
26)]
27#[cfg_attr(
28    not(feature = "autodiff"),
29    doc = "Enable the `autodiff` feature to add automatic differentiation support to devices."
30)]
31///
32/// # Backend selection
33///
34/// Enable the desired backend via Cargo feature flags, then call the
35/// corresponding factory method:
36///
37/// ```rust,ignore
38/// // Default CUDA device (requires the `cuda` feature).
39/// let device = Device::cuda(DeviceIndex::Default);
40///
41/// // CUDA device at hardware index 1.
42/// let device = Device::cuda(1);
43///
44/// // WGPU with explicit selector (requires `wgpu`/`vulkan`/`metal`/`webgpu`).
45/// let device = Device::wgpu(DeviceKind::DiscreteGpu(0));
46///
47/// // Default device for whichever backend is enabled.
48/// let device = Default::default();
49/// ```
50///
51/// Available factory methods (each gated by its matching Cargo feature):
52/// `Device::cpu`, `Device::cuda` / `Device::rocm` / `Device::libtorch_cuda`
53/// (take an integer index or a [`DeviceIndex`]), `Device::wgpu` /
54/// `Device::vulkan` / `Device::metal` / `Device::webgpu` (take a
55/// [`DeviceKind`]), `Device::flex`, `Device::ndarray`, `Device::libtorch`,
56/// `Device::libtorch_mps`, `Device::libtorch_vulkan`.
57///
58/// # Autodiff
59///
60/// Requires `autodiff` feature.
61///
62/// Gradient computation is opt-in for a device:
63///
64/// ```rust,ignore
65/// let device = Device::default().autodiff();
66///
67/// // Tensors created on this device will track gradients
68/// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device);
69/// ```
70pub struct Device {
71    blob: device_opaque::Opaque,
72}
73
74// Aligned, type-erased storage for `DispatchDevice`. See `crate::macros` for
75// why this indirection exists (it keeps the dispatch type tree out of
76// downstream MIR).
77burn_std::obfuscate!(
78    type: DispatchDevice,
79    module: device_opaque,
80    derives: [Send, Sync]
81);
82
83impl Clone for Device {
84    fn clone(&self) -> Self {
85        Self::new(self.as_dispatch().clone())
86    }
87}
88
89impl Default for Device {
90    fn default() -> Self {
91        Self::new(DispatchDevice::default())
92    }
93}
94
95impl core::fmt::Debug for Device {
96    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
97        write!(f, "Device<{:?}>", self.as_dispatch())
98    }
99}
100
101// Manually implement both `eq` and `ne` to add documentation on equality.
102#[allow(clippy::partialeq_ne_impl)]
103impl PartialEq for Device {
104    /// Compares devices based on hardware identity.
105    ///
106    /// Returns `true` if both devices represent the same compute resource.
107    /// Note that this comparison ignores autodiff and checkpointing settings.
108    /// To check if two devices have identical capabilities, check [`Device::is_autodiff`].
109    fn eq(&self, other: &Self) -> bool {
110        self.as_dispatch() == other.as_dispatch()
111    }
112
113    /// Compares devices based on hardware identity.
114    ///
115    /// Returns `false` if both devices represent the same compute resource,
116    /// even if one has autodiff enabled and the other does not.
117    fn ne(&self, other: &Self) -> bool {
118        !self.eq(other)
119    }
120}
121
122impl Eq for Device {}
123
124impl Device {
125    /// Wrap a backend-specific device in a unified [`Device`].
126    ///
127    /// Used by:
128    /// - the backend-specific factory methods below (`Device::cuda`, etc.)
129    ///   — these are the recommended entry points for downstream code;
130    /// - burn-tensor's bridge ops, which already hold a [`DispatchDevice`]
131    ///   and just need to wrap it;
132    /// - direct callers (tests, type-erased helpers) that have a concrete
133    ///   backend device type at hand.
134    ///
135    /// Anything convertible into [`DispatchDevice`] is accepted, including
136    /// `DispatchDevice` itself.
137    pub fn new(device: impl Into<DispatchDevice>) -> Self {
138        Self {
139            blob: device_opaque::Opaque::new(device.into()),
140        }
141    }
142
143    /// Borrow the underlying [`DispatchDevice`].
144    ///
145    /// The inverse of [`Device::new`]. Useful to backend-extension authors who need to dispatch on
146    /// the concrete backend variant (e.g. matching `DispatchDevice::Remote(_)`).
147    pub fn as_dispatch(&self) -> &DispatchDevice {
148        self.blob.as_ref()
149    }
150
151    /// Crate-internal owning extraction of the underlying dispatch device.
152    pub(crate) fn into_dispatch(self) -> DispatchDevice {
153        self.blob.into_inner()
154    }
155}
156
157impl<D: Into<DispatchDevice>> From<D> for Device {
158    fn from(device: D) -> Self {
159        Self::new(device)
160    }
161}
162
163/// Selector for the hardware index of a backend whose devices are simply
164/// indexed (e.g. CUDA, ROCm).
165///
166/// Backend factory methods that take an index (`Device::cuda`, `Device::rocm`,
167/// `Device::libtorch_cuda`) accept `impl Into<DeviceIndex>`, so the common
168/// shorthand is to pass a plain integer literal:
169///
170/// ```rust,ignore
171/// Device::cuda(0);                    // hardware index 0
172/// Device::cuda(DeviceIndex::Default); // backend-chosen default
173/// ```
174#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)]
175pub enum DeviceIndex {
176    /// Target a specific hardware device by its index.
177    Specified(usize),
178    /// Let the backend pick its default device (typically index `0`).
179    #[default]
180    Default,
181}
182
183impl DeviceIndex {
184    /// Construct a [`DeviceIndex::Specified`] from anything convertible into
185    /// a `usize`.
186    pub fn new(index: impl Into<usize>) -> Self {
187        Self::Specified(index.into())
188    }
189
190    /// Resolve to a concrete hardware index, defaulting to `0` for
191    /// [`DeviceIndex::Default`]. Backend factory methods are each gated by a
192    /// Cargo feature, so this looks dead when none of them are enabled.
193    #[allow(dead_code)]
194    fn resolve(self) -> usize {
195        match self {
196            DeviceIndex::Specified(i) => i,
197            DeviceIndex::Default => 0,
198        }
199    }
200}
201
202impl From<usize> for DeviceIndex {
203    fn from(i: usize) -> Self {
204        Self::Specified(i)
205    }
206}
207
208impl From<u32> for DeviceIndex {
209    fn from(i: u32) -> Self {
210        Self::Specified(i as usize)
211    }
212}
213
214impl From<u64> for DeviceIndex {
215    fn from(i: u64) -> Self {
216        Self::Specified(i as usize)
217    }
218}
219
220impl From<i32> for DeviceIndex {
221    fn from(i: i32) -> Self {
222        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
223    }
224}
225
226impl From<i64> for DeviceIndex {
227    fn from(i: i64) -> Self {
228        Self::Specified(usize::try_from(i).expect("device index must be non-negative"))
229    }
230}
231
232/// Selector for the more flexible backends whose device handle is a tagged
233/// enum (e.g. WGPU, which can target a discrete/integrated/virtual GPU, a CPU
234/// adapter, an externally-created wgpu setup, or just "best available").
235///
236/// The variants mirror `WgpuDevice` from cubecl so the mapping is direct, but
237/// it is kept as a burn-owned enum so callers don't have to depend on cubecl.
238#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
239pub enum DeviceKind {
240    /// Discrete GPU with the given index. The index is the index of the discrete GPU in the list
241    /// of all discrete GPUs found on the system.
242    DiscreteGpu(usize),
243
244    /// Integrated GPU with the given index. The index is the index of the integrated GPU in the
245    /// list of all integrated GPUs found on the system.
246    IntegratedGpu(usize),
247
248    /// Virtual GPU with the given index. The index is the index of the virtual GPU in the list of
249    /// all virtual GPUs found on the system.
250    VirtualGpu(usize),
251
252    /// CPU.
253    Cpu,
254
255    /// The best available device found with the current graphics API.
256    ///
257    /// This will prioritize GPUs wgpu recognizes as "high power". Additionally, you can override this using
258    /// the `CUBECL_WGPU_DEFAULT_DEVICE` environment variable. This variable is spelled as if i was a `WgpuDevice`,
259    /// so for example `CUBECL_WGPU_DEFAULT_DEVICE=IntegratedGpu(1)` or `CUBECL_WGPU_DEFAULT_DEVICE=Cpu`
260    #[default]
261    DefaultDevice,
262
263    /// Use an externally created, existing, wgpu setup. This is helpful when using `CubeCL` in conjunction
264    /// with some existing wgpu setup (eg. egui or bevy), as resources can be transferred in & out of `CubeCL`.
265    ///
266    /// # Notes
267    ///
268    /// This can be initialized with `init_device` from the wgpu runtime.
269    Existing(u32),
270}
271
272impl Device {
273    /// Default CPU device backed by CubeCL's CPU backend.
274    #[cfg(feature = "cpu")]
275    pub fn cpu() -> Self {
276        Self::new(burn_dispatch::devices::CpuDevice::default())
277    }
278
279    /// CUDA device at the given hardware index.
280    ///
281    /// Accepts a plain integer (e.g. `Device::cuda(0)`) or a
282    /// [`DeviceIndex`] — use [`DeviceIndex::Default`] to let the backend
283    /// pick.
284    #[cfg(feature = "cuda")]
285    pub fn cuda(index: impl Into<DeviceIndex>) -> Self {
286        Self::new(burn_dispatch::devices::CudaDevice::new(
287            index.into().resolve(),
288        ))
289    }
290
291    /// ROCm/HIP device at the given hardware index.
292    ///
293    /// Same selector semantics as [`Device::cuda`].
294    #[cfg(feature = "rocm")]
295    pub fn rocm(index: impl Into<DeviceIndex>) -> Self {
296        Self::new(burn_dispatch::devices::RocmDevice::new(
297            index.into().resolve(),
298        ))
299    }
300
301    /// Flex backend device.
302    #[cfg(feature = "flex")]
303    pub fn flex() -> Self {
304        Self::new(burn_dispatch::devices::FlexDevice)
305    }
306
307    /// Default NdArray (CPU) device.
308    #[cfg(feature = "ndarray")]
309    pub fn ndarray() -> Self {
310        Self::new(burn_dispatch::devices::NdArrayDevice::default())
311    }
312
313    /// LibTorch CPU device.
314    #[cfg(feature = "tch")]
315    pub fn libtorch() -> Self {
316        Self::new(burn_dispatch::devices::LibTorchDevice::Cpu)
317    }
318
319    /// LibTorch CUDA device at the given hardware index.
320    #[cfg(feature = "tch")]
321    pub fn libtorch_cuda(index: impl Into<DeviceIndex>) -> Self {
322        Self::new(burn_dispatch::devices::LibTorchDevice::Cuda(
323            index.into().resolve(),
324        ))
325    }
326
327    /// LibTorch Metal Performance Shaders (MPS) device.
328    #[cfg(feature = "tch")]
329    pub fn libtorch_mps() -> Self {
330        Self::new(burn_dispatch::devices::LibTorchDevice::Mps)
331    }
332
333    /// LibTorch Vulkan device.
334    #[cfg(feature = "tch")]
335    pub fn libtorch_vulkan() -> Self {
336        Self::new(burn_dispatch::devices::LibTorchDevice::Vulkan)
337    }
338
339    /// Legacy WebSocket remote device. New integrations should prefer [`Device::remote_iroh`].
340    ///
341    /// Connects to a burn-remote WebSocket server at the given address. `index` selects which of
342    /// the server's devices to use; two devices with the same address but different indices target
343    /// distinct devices on the same host.
344    #[cfg(feature = "remote-websocket")]
345    pub fn remote_websocket(address: &str, index: impl Into<DeviceIndex>) -> Self {
346        let index = index.into().resolve();
347        let device = burn_dispatch::devices::RemoteDevice::websocket(address, index);
348        device.connect(); // initializes the connection (required to get the device default settings)
349        Self::new(device)
350    }
351
352    /// Iroh peer-to-peer remote device.
353    ///
354    /// `endpoint` is the application-owned Iroh endpoint to dial from; `peer` is the compute
355    /// server's identity (from [`RemoteSecret::id`](burn_dispatch::backends::remote::RemoteSecret::id)),
356    /// optionally carrying direct/relay dialing hints.
357    /// On wasm, use [`remote_iroh_async`](Self::remote_iroh_async) instead since sessions cannot
358    /// be opened synchronously.
359    #[cfg(all(feature = "remote", not(target_family = "wasm")))]
360    pub fn remote_iroh(
361        endpoint: &burn_dispatch::backends::remote::Endpoint,
362        peer: impl Into<burn_dispatch::backends::remote::EndpointAddr>,
363        index: impl Into<DeviceIndex>,
364    ) -> Self {
365        let index = index.into().resolve();
366        let device =
367            burn_dispatch::backends::remote::RemoteDevice::iroh(endpoint, peer.into(), index);
368        device.connect();
369        Self::new(device)
370    }
371
372    /// Browser counterpart of [`remote_iroh`](Self::remote_iroh). Wasm cannot block to connect,
373    /// so the session is established asynchronously before the device is returned.
374    #[cfg(all(feature = "remote", any(target_family = "wasm", doc)))]
375    pub async fn remote_iroh_async(
376        endpoint: &burn_dispatch::backends::remote::Endpoint,
377        peer: impl Into<burn_dispatch::backends::remote::EndpointAddr>,
378        index: impl Into<DeviceIndex>,
379    ) -> Self {
380        let index = index.into().resolve();
381        let device =
382            burn_dispatch::backends::remote::RemoteDevice::iroh(endpoint, peer.into(), index);
383        device.connect_async().await;
384        Self::new(device)
385    }
386
387    /// Like `remote_iroh`, but carries an authorization credential the server's PeerAuthorizer
388    /// will check. Use against servers that require a credential; open servers take `remote_iroh`.
389    #[cfg(all(feature = "remote", not(target_family = "wasm")))]
390    pub fn remote_iroh_authorized(
391        endpoint: &burn_dispatch::backends::remote::Endpoint,
392        peer: impl Into<burn_dispatch::backends::remote::EndpointAddr>,
393        index: impl Into<DeviceIndex>,
394        credential: Vec<u8>,
395    ) -> Self {
396        let index = index.into().resolve();
397        let device = burn_dispatch::backends::remote::RemoteDevice::iroh_authorized(
398            endpoint,
399            peer.into(),
400            index,
401            credential,
402        );
403        device.connect();
404        Self::new(device)
405    }
406
407    /// Browser counterpart of `remote_iroh_authorized`. Establishes the session asynchronously.
408    #[cfg(all(feature = "remote", any(target_family = "wasm", doc)))]
409    pub async fn remote_iroh_authorized_async(
410        endpoint: &burn_dispatch::backends::remote::Endpoint,
411        peer: impl Into<burn_dispatch::backends::remote::EndpointAddr>,
412        index: impl Into<DeviceIndex>,
413        credential: Vec<u8>,
414    ) -> Self {
415        let index = index.into().resolve();
416        let device = burn_dispatch::backends::remote::RemoteDevice::iroh_authorized(
417            endpoint,
418            peer.into(),
419            index,
420            credential,
421        );
422        device.connect_async().await;
423        Self::new(device)
424    }
425
426    /// WGPU device, selected via [`DeviceKind`].
427    ///
428    /// This variant uses the runtime [`AutoCompiler`](burn_dispatch::backends::wgpu::AutoCompiler)
429    /// to dispatch to the most appropriate shader language (WGSL, SPIR-V, or MSL) based on the
430    /// enabled features.
431    ///
432    /// For [`DeviceKind::DefaultDevice`], the adapter is picked by `wgpu`'s
433    /// selection heuristics (high-power GPU preferred, or overridden by
434    /// `CUBECL_WGPU_DEFAULT_DEVICE`).
435    ///
436    /// `Device::vulkan`, `Device::metal`, and `Device::webgpu` also use the Wgpu runtime,
437    /// but bypass runtime dispatch by pinning specific compilers at compile time.
438    #[cfg(feature = "wgpu")]
439    pub fn wgpu(device_kind: DeviceKind) -> Self {
440        Self::new(DispatchDevice::Wgpu(wgpu_device(device_kind)))
441    }
442
443    #[cfg(all(feature = "wgpu", target_family = "wasm"))]
444    /// Asynchronously creates a WGPU device, initializing the client.
445    pub async fn wgpu_async(device_kind: DeviceKind) -> Self {
446        Self::new(DispatchDevice::Wgpu(wgpu_init_async(device_kind).await))
447    }
448
449    /// Vulkan-backed WGPU device, selected via [`DeviceKind`].
450    ///
451    /// Pins the wgpu shader compiler to SPIR-V at compile time, avoiding
452    /// the runtime [`AutoCompiler`](burn_dispatch::backends::wgpu::AutoCompiler) dispatch.
453    #[cfg(feature = "vulkan")]
454    pub fn vulkan(device_kind: DeviceKind) -> Self {
455        Self::new(DispatchDevice::Vulkan(wgpu_device(device_kind)))
456    }
457
458    /// Metal-backed WGPU device, selected via [`DeviceKind`].
459    ///
460    /// Pins the wgpu shader compiler to MSL at compile time.
461    #[cfg(feature = "metal")]
462    pub fn metal(device_kind: DeviceKind) -> Self {
463        Self::new(DispatchDevice::Metal(wgpu_device(device_kind)))
464    }
465
466    /// WebGPU-backed device, selected via [`DeviceKind`].
467    ///
468    /// Pins the wgpu shader compiler to WGSL at compile time.
469    #[cfg(feature = "webgpu")]
470    pub fn webgpu(device_kind: DeviceKind) -> Self {
471        Self::new(DispatchDevice::WebGpu(wgpu_device(device_kind)))
472    }
473
474    /// Enables autodiff on this device.
475    ///
476    /// Autodiff is a property of the device: tensors created on the returned device
477    /// will participate in the autodiff graph.
478    ///
479    /// Only first-order autodiff is supported. Calling this method on a device that
480    /// already has autodiff enabled will panic.
481    ///
482    /// # Example
483    ///
484    /// ```rust,ignore
485    /// let device = Device::default().autodiff();
486    /// let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &device);
487    /// // x.backward() is now available
488    /// ```
489    ///
490    /// # Panics
491    ///
492    /// Panics if autodiff is already enabled on this device.
493    #[cfg(feature = "autodiff")]
494    pub fn autodiff(self) -> Self {
495        match self.into_dispatch() {
496            DispatchDevice::Autodiff(_) => unimplemented!("Only first-order autodiff is supported"),
497            other => Self::new(DispatchDevice::autodiff(other)),
498        }
499    }
500
501    /// Enables gradient checkpointing on the autodiff device.
502    ///
503    /// Gradient checkpointing recomputes activations during backpropagation for operations
504    /// marked as memory-bound, while compute-bound operations still cache their
505    /// output. This reduces peak memory usage at the cost of additional computation
506    /// for memory-bound ops.
507    ///
508    /// # Example
509    ///
510    /// ```rust,ignore
511    /// let device = Device::default().autodiff().gradient_checkpointing();
512    /// ```
513    ///
514    /// # Panics
515    ///
516    /// Panics if autodiff is not enabled on this device.
517    #[cfg(feature = "autodiff")]
518    pub fn gradient_checkpointing(self) -> Self {
519        match self.into_dispatch() {
520            DispatchDevice::Autodiff(device) => {
521                use burn_dispatch::CheckpointingStrategy;
522
523                Self::new(DispatchDevice::autodiff_checkpointed(
524                    device.inner(),
525                    CheckpointingStrategy::Balanced,
526                ))
527            }
528            _ => panic!("Autodiff is not enabled on this device"),
529        }
530    }
531
532    /// Returns the underlying device, removing the autodiff capability if present.
533    ///
534    /// If autodiff is not enabled, this method returns the device as-is.
535    ///
536    /// # Example
537    ///
538    /// ```rust,ignore
539    /// let device = Device::default().autodiff();
540    /// let inner_device = device.inner();
541    ///
542    /// assert!(!inner_device.is_autodiff());
543    /// ```
544    pub fn inner(self) -> Self {
545        if self.is_autodiff() {
546            Self::new(self.into_dispatch().inner())
547        } else {
548            self
549        }
550    }
551
552    /// Synchronize the device, waiting for all pending operations to complete.
553    ///
554    /// # Errors
555    ///
556    /// Returns an [`ExecutionError`] if an operation failed to execute.
557    pub fn sync(&self) -> Result<(), ExecutionError> {
558        Dispatch::sync(self.as_dispatch())
559    }
560
561    /// Flush the device's pending operations, handing them off for execution without waiting
562    /// for them to complete.
563    ///
564    /// Backends that buffer work hold registered operations in a local queue until enough
565    /// accumulate: the fusion backend batches ops to build optimizations, and the remote backend
566    /// batches them before sending them over the network. `flush` forces that queue out now — the
567    /// fusion backend processes its pending optimizations and the remote backend sends its batch to
568    /// the server.
569    ///
570    /// Unlike [`sync`](Self::sync), this does not block on results — it only ensures buffered
571    /// operations are dispatched instead of sitting idle. Eager backends, which execute each
572    /// operation as it is registered, have nothing buffered and treat this as a no-op.
573    pub fn flush(&self) {
574        Dispatch::flush(self.as_dispatch())
575    }
576
577    /// Seeds the random number generator for this device.
578    ///
579    /// Seeding before tensor operations that involve randomness (e.g. [`Tensor::random`](crate::Tensor::random))
580    /// makes those operations reproducible in a single-threaded program.
581    ///
582    /// # Note
583    ///
584    /// Depending on the backend, the seed may be applied globally rather than scoped
585    /// to this specific device. It is guaranteed that at least this device will be seeded.
586    ///
587    /// # Example
588    ///
589    /// ```rust,ignore
590    /// let device = Default::default();
591    /// device.seed(42);
592    /// let t = Tensor::<1>::random([8], Distribution::Default, &device);
593    /// ```
594    pub fn seed(&self, seed: u64) {
595        Dispatch::seed(self.as_dispatch(), seed)
596    }
597
598    /// Returns `true` if autodiff (gradient tracking) is enabled on this device.
599    ///
600    /// # Example
601    ///
602    /// ```rust,ignore
603    /// let device = Default::default();
604    /// assert!(!device.is_autodiff());
605    ///
606    /// let ad_device = device.autodiff();
607    /// assert!(ad_device.is_autodiff());
608    /// ```
609    pub fn is_autodiff(&self) -> bool {
610        Dispatch::ad_enabled(self.as_dispatch())
611    }
612
613    /// Returns `true` if this device supports `dtype` for general computation:
614    /// storage, conversion, *and* arithmetic.
615    ///
616    /// A type can be less than generally supported — bf16 on a Vulkan device,
617    /// for example, is often storable and convertible but has no arithmetic
618    /// (SPIR-V's `SPV_KHR_bfloat16` permits only conversions, dot products,
619    /// and cooperative-matrix use). Computing in such a type produces
620    /// backend-dependent garbage, so check before selecting a reduced
621    /// precision:
622    ///
623    /// ```rust,ignore
624    /// let dtype = if device.supports_dtype(FloatDType::BF16) {
625    ///     FloatDType::BF16
626    /// } else {
627    ///     FloatDType::F32
628    /// };
629    /// ```
630    pub fn supports_dtype(&self, dtype: impl Into<burn_std::DType>) -> bool {
631        Dispatch::supports_dtype(self.as_dispatch(), dtype.into())
632    }
633
634    /// Sets the current allocation mode to persistent.
635    pub fn memory_persistent_allocations<
636        Output: Send,
637        Input: Send,
638        Func: Fn(Input) -> Output + Send,
639    >(
640        &self,
641        input: Input,
642        func: Func,
643    ) -> Output {
644        Dispatch::memory_persistent_allocations(self.as_dispatch(), input, func)
645    }
646
647    /// Triggers a memory cleanup on this device.
648    ///
649    /// The amount of memory reclaimed depends on the allocator implementation.
650    /// Calling this method does not guarantee that any memory will be freed.
651    pub fn memory_cleanup(&self) {
652        Dispatch::memory_cleanup(self.as_dispatch());
653    }
654
655    /// Prepares the given data for transfer between the CPU and accelerator devices such as GPUs.
656    ///
657    /// Depending on the backend, the data may be transferred to pinned memory
658    /// or another transfer-optimized format to improve transfer performance.
659    pub fn staging<'a, Iter>(&self, data: Iter)
660    where
661        Iter: Iterator<Item = &'a mut TensorData>,
662    {
663        Dispatch::staging(data, self.as_dispatch());
664    }
665
666    /// Returns the [`DeviceSettings`] for this device.
667    ///
668    /// Settings include the default float and integer data types used when creating
669    /// tensors on this device.
670    ///
671    /// See [`configure`](Device::configure) to configure them.
672    pub fn settings(&self) -> DeviceSettings {
673        burn_backend::get_device_settings::<Dispatch>(self.as_dispatch())
674    }
675
676    /// Configures the [settings](DeviceSettings) for this device.
677    ///
678    /// This configures the dtype used when no explicit type is specified at tensor
679    /// creation time.
680    ///
681    /// Settings can only be initialized once per device, and must happen before any
682    /// tensor is created on the device. The first tensor operation will lock the device
683    /// to its defaults, causing subsequent initializations attempt to return
684    /// [`DeviceError::AlreadyInitialized`].
685    ///
686    /// # Errors
687    ///
688    /// Returns [`DeviceError::AlreadyInitialized`] if settings have already been set
689    /// for this device (either by a prior call or because a tensor operation has
690    /// already occurred).
691    ///
692    /// # Example
693    ///
694    /// ```rust,ignore
695    /// let device = Default::default();
696    ///
697    /// device.configure((FloatDType::F16, IntDType::I32))?
698    ///
699    /// // Float tensors will now use F16
700    /// let floats = Tensor::<2>::zeros([2, 3], &device);
701    /// // Int tensors will now use I32
702    /// let ints = Tensor::<2, Int>::zeros([2, 3], &device);
703    /// ```
704    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
705        let mut config = config.into();
706
707        let defaults = self.as_dispatch().defaults();
708
709        let float_dtype = config.float_dtype.take().unwrap_or(defaults.float_dtype);
710        let int_dtype = config.int_dtype.take().unwrap_or(defaults.int_dtype);
711        let bool_dtype = config.bool_dtype.take().unwrap_or(defaults.bool_dtype);
712
713        burn_backend::set_default_dtypes::<Dispatch>(
714            self.as_dispatch(),
715            float_dtype,
716            int_dtype,
717            bool_dtype,
718        )
719    }
720
721    /// Retrieves all available [`Device`]s that match the given [`DeviceType`] filter.
722    ///
723    /// Local backends (CPU, CUDA, WGPU, …) enumerate the hardware found on the host. The
724    /// [`Remote`](DeviceType::Remote) variant instead lists every device hosted by the
725    /// `burn-remote` server at the given address — it connects to the server to learn how
726    /// many devices it exposes:
727    ///
728    /// ```rust,ignore
729    /// // Every CUDA device on this machine.
730    /// let local = Device::enumerate(DeviceType::Cuda);
731    ///
732    /// // Every device hosted by a remote server.
733    /// let remote = Device::enumerate(DeviceType::remote_websocket("ws://host:3000"));
734    ///
735    /// // Filters combine with `|`.
736    /// let both = Device::enumerate(DeviceType::Cuda | DeviceType::remote_websocket("ws://host:3000"));
737    /// ```
738    pub fn enumerate(filter: impl Into<DeviceFilter>) -> Devices {
739        #[allow(unused)]
740        let mut devices = Vec::new();
741
742        #[allow(clippy::never_loop)] // at least one backend is expected to be enabled.
743        for device_type in filter.into() {
744            #[allow(unused)]
745            let type_id = match device_type {
746                #[cfg(feature = "cpu")]
747                DeviceType::Cpu => DispatchDeviceId::Cpu,
748                #[cfg(feature = "cuda")]
749                DeviceType::Cuda => DispatchDeviceId::Cuda,
750                #[cfg(feature = "rocm")]
751                DeviceType::Rocm => DispatchDeviceId::Rocm,
752                #[cfg(feature = "wgpu")]
753                DeviceType::Wgpu => DispatchDeviceId::Wgpu,
754                #[cfg(feature = "metal")]
755                DeviceType::Metal => DispatchDeviceId::Metal,
756                #[cfg(feature = "vulkan")]
757                DeviceType::Vulkan => DispatchDeviceId::Vulkan,
758                #[cfg(feature = "webgpu")]
759                DeviceType::WebGpu => DispatchDeviceId::WebGpu,
760                #[cfg(feature = "flex")]
761                DeviceType::Flex => DispatchDeviceId::Flex,
762                #[cfg(feature = "ndarray")]
763                DeviceType::NdArray => DispatchDeviceId::NdArray,
764                #[cfg(feature = "tch")]
765                DeviceType::LibTorch => DispatchDeviceId::LibTorch,
766                // Remote devices are keyed by address, not a backend type id, so they take a
767                // dedicated enumeration path (connecting to the server for its device count).
768                #[cfg(feature = "remote-websocket")]
769                DeviceType::Remote(address) => {
770                    for device in Dispatch::enumerate_remote_websocket(&address) {
771                        devices.push(Device::new(device));
772                    }
773                    continue;
774                }
775            };
776
777            #[allow(unreachable_code)] // need to have one backend enabled, so it is reachable
778            for device in Dispatch::enumerate(type_id) {
779                devices.push(Device::new(device))
780            }
781        }
782
783        Devices(devices)
784    }
785
786    /// Measure peak compute and memory throughput for this device.
787    ///
788    /// Runs cubecl-std's throughput benchmarks for each [`ThroughputKey`],
789    /// returning one [`ThroughputStat`] per key (in the same order). Only
790    /// cubecl-backed devices (cuda, wgpu, ...) report measurements; other
791    /// backends return an empty vector.
792    #[cfg(feature = "cubecl")]
793    pub fn performance_stats(&self, keys: &[ThroughputKey]) -> Vec<ThroughputStat> {
794        self.as_dispatch()
795            .performance_stats(keys)
796            .into_iter()
797            .zip(keys.iter().copied())
798            .map(|(value, key)| ThroughputStat { key, value })
799            .collect()
800    }
801}
802
803/// A single peak-throughput measurement produced by [`Device::performance_stats`].
804#[cfg(feature = "cubecl")]
805#[derive(Debug, Clone, Copy, PartialEq)]
806pub struct ThroughputStat {
807    /// The measurement key (mode + dtype) that was benchmarked.
808    pub key: ThroughputKey,
809    /// The measured throughput for that key.
810    pub value: ThroughputValue,
811}
812
813/// Short, column-friendly name for a throughput mode.
814#[cfg(feature = "cubecl")]
815fn mode_label(mode: &ThroughputMode) -> &'static str {
816    match mode {
817        ThroughputMode::ComputeDirect { .. } => "compute-direct",
818        ThroughputMode::ComputeCmma { .. } => "compute-cmma",
819        ThroughputMode::Memory => "memory",
820        ThroughputMode::Launch => "launch",
821    }
822}
823
824#[cfg(feature = "cubecl")]
825impl core::fmt::Display for ThroughputStat {
826    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
827        // Width/alignment flags are ignored on `ThroughputMode`/`ElemType` directly
828        // (their fmt impls don't call `f.pad`), so render them to `String`s first —
829        // `str`'s `Display` honors padding. The mode is labelled by hand rather than
830        // derived through `Debug`: its variants carry payloads that would blow out the column.
831        let mode = mode_label(&self.key.mode);
832
833        // `ThroughputKey::dtype()` reports f32 for the modes that don't compute with an
834        // element type, so blank the column there rather than print a misleading type.
835        let dtype = match self.key.mode {
836            ThroughputMode::ComputeDirect { dtype } | ThroughputMode::ComputeCmma { dtype, .. } => {
837                alloc::format!("{dtype}")
838            }
839            ThroughputMode::Memory | ThroughputMode::Launch => alloc::string::String::new(),
840        };
841
842        let value = self.value.format(&self.key);
843
844        write!(f, "{mode:<14} {dtype:<5} {value}")
845    }
846}
847
848/// Map our backend-agnostic [`DeviceKind`] onto cubecl's `WgpuDevice` enum.
849///
850/// Shared by [`Device::wgpu`], [`Device::vulkan`], [`Device::metal`], and
851/// [`Device::webgpu`], which differ only in which Cargo feature gates them.
852#[cfg(feature = "wgpu")]
853fn wgpu_device(device_kind: DeviceKind) -> burn_dispatch::devices::WgpuDevice {
854    use burn_dispatch::devices::WgpuDevice;
855    match device_kind {
856        DeviceKind::DiscreteGpu(i) => WgpuDevice::DiscreteGpu(i),
857        DeviceKind::IntegratedGpu(i) => WgpuDevice::IntegratedGpu(i),
858        DeviceKind::VirtualGpu(i) => WgpuDevice::VirtualGpu(i),
859        DeviceKind::Cpu => WgpuDevice::Cpu,
860        DeviceKind::DefaultDevice => WgpuDevice::DefaultDevice,
861        DeviceKind::Existing(id) => WgpuDevice::Existing(id),
862    }
863}
864
865#[cfg(all(feature = "wgpu", target_family = "wasm"))]
866// 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
867// so we might have to introduce some wrapper types.
868async fn wgpu_init_async(device_kind: DeviceKind) -> burn_dispatch::devices::WgpuDevice {
869    use burn_dispatch::backends::wgpu::{graphics::AutoGraphicsApi, init_setup_async};
870
871    let device = wgpu_device(device_kind);
872    init_setup_async::<AutoGraphicsApi>(&device, Default::default()).await;
873    device
874}
875
876/// Represents the devices that can be used.
877///
878/// `DeviceType` is used to filter the available device types for [`Device::enumerate`]. Most
879/// variants are fieldless and select a backend's local hardware; [`Remote`](Self::Remote)
880/// carries the network address of a `burn-remote` server whose devices should be listed.
881///
882/// Variants combine into a [`DeviceFilter`] with the `|` operator, so a single
883/// [`Device::enumerate`] call can span several backends and remote hosts.
884#[allow(missing_docs)]
885#[derive(Debug, Clone, PartialEq, Eq)]
886pub enum DeviceType {
887    #[cfg(feature = "cpu")]
888    Cpu,
889    #[cfg(feature = "cuda")]
890    Cuda,
891    #[cfg(feature = "rocm")]
892    Rocm,
893    #[cfg(feature = "wgpu")]
894    Wgpu,
895    #[cfg(feature = "metal")]
896    Metal,
897    #[cfg(feature = "vulkan")]
898    Vulkan,
899    #[cfg(feature = "webgpu")]
900    WebGpu,
901    #[cfg(feature = "flex")]
902    Flex,
903    #[cfg(feature = "ndarray")]
904    NdArray,
905    #[cfg(feature = "tch")]
906    LibTorch,
907    /// Devices hosted by the `burn-remote` server at the given address
908    /// (e.g. `"ws://host:3000"`). Unlike the other variants this is resolved at runtime by
909    /// connecting to the server, which reports how many devices it exposes.
910    #[cfg(feature = "remote-websocket")]
911    Remote(String),
912}
913
914#[cfg(feature = "remote-websocket")]
915impl DeviceType {
916    /// Filter selecting every device hosted by the `burn-remote` server at `address`
917    /// (e.g. `"ws://host:3000"`).
918    ///
919    /// Convenience for [`DeviceType::Remote`] that accepts anything string-like.
920    pub fn remote_websocket(address: impl Into<String>) -> Self {
921        DeviceType::Remote(address.into())
922    }
923}
924
925/// A set of [`DeviceType`]s passed to [`Device::enumerate`].
926///
927/// Built from a single [`DeviceType`], a `Vec<DeviceType>`, or by combining variants with the
928/// `|` operator (`DeviceType::Cuda | DeviceType::Cpu`). Because [`DeviceType::Remote`] carries
929/// an address, this is a plain list rather than a bitset.
930#[derive(Debug, Clone, Default)]
931pub struct DeviceFilter(Vec<DeviceType>);
932
933impl DeviceFilter {
934    /// Create an empty filter.
935    pub fn new() -> Self {
936        Self::default()
937    }
938
939    /// Add a [`DeviceType`] to the filter.
940    pub fn with(mut self, device_type: DeviceType) -> Self {
941        self.0.push(device_type);
942        self
943    }
944}
945
946impl From<DeviceType> for DeviceFilter {
947    fn from(value: DeviceType) -> Self {
948        DeviceFilter(vec![value])
949    }
950}
951
952impl From<Vec<DeviceType>> for DeviceFilter {
953    fn from(value: Vec<DeviceType>) -> Self {
954        DeviceFilter(value)
955    }
956}
957
958impl IntoIterator for DeviceFilter {
959    type Item = DeviceType;
960    type IntoIter = alloc::vec::IntoIter<DeviceType>;
961    fn into_iter(self) -> Self::IntoIter {
962        self.0.into_iter()
963    }
964}
965
966impl core::ops::BitOr for DeviceType {
967    type Output = DeviceFilter;
968    fn bitor(self, rhs: Self) -> DeviceFilter {
969        DeviceFilter(vec![self, rhs])
970    }
971}
972
973impl core::ops::BitOr<DeviceType> for DeviceFilter {
974    type Output = DeviceFilter;
975    fn bitor(mut self, rhs: DeviceType) -> DeviceFilter {
976        self.0.push(rhs);
977        self
978    }
979}
980
981/// Configuration options used to initialize a device.
982///
983/// Unlike [`DeviceSettings`], this type represents partial user-provided
984/// configuration and does not require all settings to be specified.
985///
986/// Any unspecified options will be resolved to device-specific defaults
987/// when the device is initialized.
988///
989/// Use [`Device::configure`] to apply this configuration to a device.
990#[derive(new, Debug, Clone, Default)]
991pub struct DeviceConfig {
992    /// Default floating-point data type.
993    pub float_dtype: Option<FloatDType>,
994
995    /// Default integer data type.
996    pub int_dtype: Option<IntDType>,
997
998    /// Default boolean data type.
999    pub bool_dtype: Option<BoolDType>,
1000    // TODO: maybe quantization, but for now we keep this as device defaults
1001}
1002
1003impl DeviceConfig {
1004    /// Sets the default floating-point data type for tensors created on the device.
1005    pub fn float_dtype(mut self, dtype: impl Into<FloatDType>) -> Self {
1006        self.float_dtype = Some(dtype.into());
1007        self
1008    }
1009
1010    /// Sets the default integer data type for tensors created on the device.
1011    pub fn int_dtype(mut self, dtype: impl Into<IntDType>) -> Self {
1012        self.int_dtype = Some(dtype.into());
1013        self
1014    }
1015
1016    /// Sets the default boolean data type storage precision for tensors created on the device.
1017    pub fn bool_dtype(mut self, dtype: impl Into<BoolDType>) -> Self {
1018        self.bool_dtype = Some(dtype.into());
1019        self
1020    }
1021}
1022
1023impl From<FloatDType> for DeviceConfig {
1024    fn from(value: FloatDType) -> Self {
1025        DeviceConfig::new(Some(value), None, None)
1026    }
1027}
1028
1029impl From<IntDType> for DeviceConfig {
1030    fn from(value: IntDType) -> Self {
1031        DeviceConfig::new(None, Some(value), None)
1032    }
1033}
1034
1035impl From<BoolDType> for DeviceConfig {
1036    fn from(value: BoolDType) -> Self {
1037        DeviceConfig::new(None, None, Some(value))
1038    }
1039}
1040
1041impl From<(FloatDType, IntDType)> for DeviceConfig {
1042    fn from(value: (FloatDType, IntDType)) -> Self {
1043        DeviceConfig::new(Some(value.0), Some(value.1), None)
1044    }
1045}
1046
1047/// A collection of [`Device`]s returned by [`Device::enumerate`].
1048///
1049/// This type provides bulk operations and transformations over multiple
1050/// devices, such as enabling autodiff or configuring the device settings.
1051///
1052/// # Example
1053///
1054/// ```rust,ignore
1055/// let mut devices = Device::enumerate(DeviceType::Cuda)
1056///     .autodiff();
1057///
1058/// devices.configure(
1059///     DeviceConfig::default().float_dtype(FloatDType::F16),
1060/// )?;
1061/// ```
1062///
1063/// `Devices` dereferences to a slice of [`Device`], so it can be iterated,
1064/// indexed, and passed anywhere a `&[Device]` is expected.
1065pub struct Devices(Vec<Device>);
1066
1067impl Devices {
1068    /// Enables autodiff across all contained devices.
1069    ///
1070    /// Only first-order autodiff is supported. Calling this method on a device that
1071    /// already has autodiff enabled will panic.
1072    ///
1073    /// See [`Device::autodiff`].
1074    #[cfg(feature = "autodiff")]
1075    pub fn autodiff(mut self) -> Self {
1076        for device in &mut self.0 {
1077            *device = core::mem::take(device).autodiff();
1078        }
1079
1080        self
1081    }
1082
1083    /// Configures the [settings](DeviceSettings) for all devices.
1084    ///
1085    /// This configures the dtype used when no explicit type is specified at tensor
1086    /// creation time.
1087    ///
1088    /// Settings can only be initialized once per device, and must happen before any
1089    /// tensor is created on the device. The first tensor operation will lock the device
1090    /// to its defaults, causing subsequent initializations attempt to return
1091    /// [`DeviceError::AlreadyInitialized`].
1092    ///
1093    /// See [`Device::configure`].
1094    pub fn configure(&mut self, config: impl Into<DeviceConfig>) -> Result<(), DeviceError> {
1095        let config = config.into();
1096        for device in &mut self.0 {
1097            device.configure(config.clone())?;
1098        }
1099        Ok(())
1100    }
1101
1102    /// Returns the `Vec` of [`Device`]s.
1103    pub fn into_vec(self) -> Vec<Device> {
1104        self.0
1105    }
1106}
1107
1108// Loop over `&Devices` or `Devices` seamlessly
1109impl IntoIterator for Devices {
1110    type Item = Device;
1111    type IntoIter = alloc::vec::IntoIter<Device>;
1112    fn into_iter(self) -> Self::IntoIter {
1113        self.0.into_iter()
1114    }
1115}
1116
1117impl core::ops::Deref for Devices {
1118    type Target = [Device];
1119    fn deref(&self) -> &Self::Target {
1120        &self.0
1121    }
1122}
1123
1124#[cfg(all(test, feature = "flex", feature = "autodiff"))]
1125mod autodiff_move_tests {
1126    use crate::{Device, Tensor};
1127
1128    // A non-tracked float tensor (e.g. a gradient) can be moved onto an autodiff device; it
1129    // lands on the underlying hardware and stays non-tracked. Regression test for a panic in
1130    // `float_to_device` ("Cannot move between autodiff and non-autodiff instances").
1131    #[test]
1132    fn move_non_autodiff_float_tensor_to_autodiff_device() {
1133        let device = Device::default();
1134        let ad_device = device.clone().autodiff();
1135
1136        let t = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &device);
1137        let moved = t.to_device(&ad_device);
1138
1139        assert_eq!(
1140            moved.to_data().to_vec::<f32>().unwrap(),
1141            vec![1.0, 2.0, 3.0, 4.0]
1142        );
1143    }
1144}