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