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