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