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