Skip to main content

burn_backend/backend/
base.rs

1use burn_std::DType;
2pub use burn_std::{ExecutionError, backtrace::BackTrace};
3
4use crate::distributed::DistributedOps;
5pub use crate::element::Element;
6use crate::ops::*;
7use crate::tensor::{BoolTensor, FloatTensor, IntTensor, QuantizedTensor};
8use crate::{TensorData, TensorMetadata};
9use alloc::string::String;
10use alloc::vec::Vec;
11use enumset::{EnumSet, EnumSetType};
12
13use crate::distributed::{DistributedParamId, DistributedParams};
14
15use super::DeviceOps;
16use super::{InstallMemoryPoolsError, MemoryPoolLayout, MemoryPoolUsage, SlicedPoolReport};
17
18/// The mapping of types used by Backend and traits.
19pub trait BackendTypes: Clone + Send + Sync + core::fmt::Debug + 'static {
20    /// Device type.
21    type Device: DeviceOps;
22
23    /// Tensor primitive to be used for all float operations.
24    type FloatTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
25
26    /// Tensor primitive to be used for all int operations.
27    type IntTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
28
29    /// Tensor primitive to be used for all bool operations.
30    type BoolTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
31
32    /// Tensor primitive to be used for all quantized operations.
33    type QuantizedTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
34
35    /// Captured graph primitive returned by [`Backend::graph_stop_capture`] and
36    /// consumed by [`Backend::graph_replay`]: a backend-owned recording of a
37    /// launch sequence that replays as a single dispatch.
38    ///
39    /// Backends without graph-capture support use [`GraphUnsupported`], an
40    /// uninhabited type — their capture methods only ever error, so no value of
41    /// it can exist.
42    type GraphPrimitive: Clone + Send + Sync + core::fmt::Debug + 'static;
43}
44
45/// Captured graph primitive type used by the backend (see
46/// [`BackendTypes::GraphPrimitive`]).
47pub type BackendGraph<B> = <B as BackendTypes>::GraphPrimitive;
48
49/// Placeholder [graph primitive](BackendTypes::GraphPrimitive) for backends
50/// without graph-capture support.
51///
52/// Uninhabited: `graph_stop_capture` on such backends always errors, so a value
53/// of this type can never be constructed (and `graph_replay` can never be called).
54#[derive(Debug, Clone, Copy)]
55pub enum GraphUnsupported {}
56
57/// The error returned by the default (unsupported) graph-capture methods.
58fn graph_unsupported() -> ExecutionError {
59    ExecutionError::Generic {
60        reason: alloc::string::String::from("graph capture is not supported by this backend"),
61        backtrace: BackTrace::capture(),
62    }
63}
64
65/// This trait defines all types and functions needed for a backend to be used with burn.
66///
67/// ## Design
68///
69/// This trait aims to be as unopinionated as possible and allows implementations to define
70/// their own types and patterns. Therefore, there are few pre-defined abstractions baked
71/// into this trait.
72///
73/// Backends must define their own tensor types for each data type: `float`, `int`, and `bool`.
74/// Since we minimize assumptions, we chose to separate these types, as they are used in
75/// different contexts. However, some backends may have a generic tensor type that is used
76/// for all data types.
77///
78/// ### Eager Mode
79///
80/// Because burn supports dynamic graphs, the backend trait is designed around kernel
81/// implementations that can be called without any mutable context or graph. This may not be
82/// ideal for backends that want to configure their computational graphs and execute them
83/// multiple times.
84///
85/// To implement this kind of backend, channels could be used to communicate with a backend
86/// server thread to build the computation graphs and re-execute the ones that are repeated,
87/// with some form of cache. Once that pattern has matured, a graph mode backend trait could
88/// be extracted from it, allowing other backends of the same kind to be quickly integrated
89/// with burn. This pattern could also be used to create an operation fusion trait, which
90/// allows backends to define what kind of graph structures can be fused into one operation.
91///
92/// ### Multi-Threaded
93///
94/// Backend tensor types are all `Clone` + `Send`, which allows them to be safely
95/// sent between threads. It is recommended to wrap tensors with [Arc](alloc::sync::Arc),
96/// which avoids copying the tensor's buffer. Note that it is still possible to mutate and
97/// reuse tensors' buffer without locking; see the next section on the Mutable API.
98///
99/// ### Mutable API
100///
101/// There is no mutable or inplace operation API to implement, but that does not mean that
102/// backends cannot support them. Using [try_unwrap](alloc::sync::Arc::try_unwrap) and
103/// [get_mut](alloc::sync::Arc::get_mut) allows backends to have access to an owned or mutable
104/// reference to their tensor buffer data structure if the tensor is not shared. In that case,
105/// backends can dispatch to their owned inplace operations for better performance.
106///
107/// ## Documentation
108///
109/// Most of the documentation for each function can be found on the user API
110#[cfg_attr(doc, doc = crate::doc_tensor!())]
111#[cfg_attr(not(doc), doc = "`Tensor`")]
112/// struct in the `burn-tensor` crate.
113/// For modules, public functions are often created, which can be used by `burn-core` modules.
114pub trait Backend:
115    BackendTypes
116    + FloatTensorOps<Self>
117    + BoolTensorOps<Self>
118    + IntTensorOps<Self>
119    + ModuleOps<Self>
120    + ActivationOps<Self>
121    + QTensorOps<Self>
122    + TransactionOps<Self>
123    + DistributedOps<Self>
124    + Clone
125    + Default
126    + Sized
127    + Send
128    + Sync
129    + core::fmt::Debug
130    + 'static
131{
132    /// If autodiff is enabled.
133    fn ad_enabled(_device: &Self::Device) -> bool {
134        false
135    }
136
137    /// Sets the current allocation mode to persistent.
138    #[allow(unused_variables)]
139    fn memory_persistent_allocations<
140        Output: Send,
141        Input: Send,
142        Func: Fn(Input) -> Output + Send,
143    >(
144        device: &Self::Device,
145        input: Input,
146        func: Func,
147    ) -> Output {
148        func(input)
149    }
150
151    /// Manually triggers a memory cleanup on the given device.
152    #[allow(unused_variables)]
153    fn memory_cleanup(device: &Self::Device) {}
154
155    /// Install a layout for the device's dynamic memory pools.
156    ///
157    /// A per-workload setting: the calling stream's pools are rebuilt in place
158    /// when nothing is live in them — so install at a quiescent point, after
159    /// the previous workload's tensors have dropped and a
160    /// [`memory_cleanup`](Self::memory_cleanup) — and streams created
161    /// afterwards use the new layout.
162    ///
163    /// Sizing a layout from a measurement means installing twice: once
164    /// growable, to run the workload and read
165    /// [`memory_pool_report`](Self::memory_pool_report), and once capped at
166    /// what that reported.
167    ///
168    /// # Errors
169    ///
170    /// [`InstallMemoryPoolsError::PoolsInUse`] when something is still live in
171    /// the pools being rebuilt, worth retrying once it drains;
172    /// [`StreamUnavailable`](InstallMemoryPoolsError::StreamUnavailable) when
173    /// the calling stream has already failed;
174    /// [`InvalidLayout`](InstallMemoryPoolsError::InvalidLayout) when the
175    /// layout cannot be honoured; and
176    /// [`Unsupported`](InstallMemoryPoolsError::Unsupported) — the default — on
177    /// a backend with no configurable pools. Neither of the last two is worth a
178    /// retry. The layout in force is unchanged in every case, so a caller that
179    /// cannot proceed without it has to say so rather than assume the
180    /// reservation it asked for.
181    #[allow(unused_variables)]
182    fn memory_install_pools(
183        device: &Self::Device,
184        layout: MemoryPoolLayout,
185    ) -> Result<(), InstallMemoryPoolsError> {
186        Err(InstallMemoryPoolsError::Unsupported)
187    }
188
189    /// The dynamic pools' measured state, in the order allocations are routed
190    /// through them. `None` on a backend that does not report one, or whose
191    /// stream has failed.
192    ///
193    /// Entries pair one-to-one with the pools of a
194    /// [`Sliced`](MemoryPoolLayout::Sliced) or
195    /// [`Direct`](MemoryPoolLayout::Direct) layout this caller installed, which
196    /// is what a measured layout is rebuilt from. A layout nobody installed —
197    /// the runtime's default, or a preset — also routes through pools of other
198    /// kinds, which are left out, so its entries carry no rebuildable position.
199    ///
200    /// Reporting and installing are separate capabilities: a runtime may
201    /// describe the pools it has while refusing to be given different ones, so
202    /// a report is not proof that a layout was installed. Only the result of
203    /// [`memory_install_pools`](Self::memory_install_pools) says that.
204    #[allow(unused_variables)]
205    fn memory_pool_report(device: &Self::Device) -> Option<Vec<SlicedPoolReport>> {
206        None
207    }
208
209    /// The device allocator's current state. `None` on a backend that does not
210    /// report one, or whose stream has failed.
211    #[allow(unused_variables)]
212    fn memory_pool_usage(device: &Self::Device) -> Option<MemoryPoolUsage> {
213        None
214    }
215
216    /// Name of the backend.
217    fn name(device: &Self::Device) -> String;
218
219    /// Seeds the backend on the specified device.
220    ///
221    /// There is no guarantee that only the specified device will be seeded, but it is guaranteed
222    /// that at least the specified device will be seeded.
223    ///
224    /// In all cases, this should ensure deterministic execution for a single-threaded program.
225    fn seed(device: &Self::Device, seed: u64);
226
227    /// Sync the backend, ensure that all computation are finished.
228    fn sync(_device: &Self::Device) -> Result<(), ExecutionError> {
229        Ok(())
230    }
231
232    /// Prepare `device` for an upcoming graph capture: route allocations into a
233    /// stable pool so every buffer allocated before graph_stop_capture can
234    /// be pinned. Call before the warmup run. No-op by default.
235    ///
236    /// See [`burn_graph`](crate) — the closure-based `capture` helper drives
237    /// this whole sequence.
238    fn graph_prepare(_device: &Self::Device) -> Result<(), ExecutionError> {
239        Ok(())
240    }
241
242    /// Begin recording launches on `device` into a graph (see
243    /// [`graph_stop_capture`](Backend::graph_stop_capture)). Errors on backends
244    /// without hardware graph support, so callers fall back to re-running.
245    fn graph_start_capture(_device: &Self::Device) -> Result<(), ExecutionError> {
246        Err(graph_unsupported())
247    }
248
249    /// Stop recording and return the captured [graph](BackendTypes::GraphPrimitive),
250    /// ready to [`graph_replay`](Backend::graph_replay).
251    fn graph_stop_capture(_device: &Self::Device) -> Result<BackendGraph<Self>, ExecutionError> {
252        Err(graph_unsupported())
253    }
254
255    /// Replay a captured [graph](BackendTypes::GraphPrimitive) — one dispatch
256    /// re-running the recorded launches against their original buffers.
257    ///
258    /// # Safety
259    ///
260    /// The replay dispatches raw device work against the exact buffers recorded
261    /// at capture time, with nothing tracking whether those buffers are still
262    /// valid. The caller must guarantee, for every tensor the captured closure
263    /// read or wrote:
264    ///
265    /// - its buffer is still alive — no tensor referenced by the graph has been
266    ///   freed (and its memory possibly reallocated) since capture;
267    /// - it is not concurrently read or written by work on another stream or
268    ///   thread while the replay executes;
269    /// - input refreshes and output reads are issued on the stream the graph
270    ///   was captured on, so they order correctly against the replay.
271    unsafe fn graph_replay(
272        _device: &Self::Device,
273        _graph: &BackendGraph<Self>,
274    ) -> Result<(), ExecutionError> {
275        Err(graph_unsupported())
276    }
277
278    /// Flush any pending operation of the backend.
279    fn flush(_device: &Self::Device);
280
281    /// Marks the given data as being used as a staging buffer for transfer between CPU and
282    /// accelerators like GPUs.
283    ///
284    /// The given data might be transferred to pinned memory or another format to improve data transfer
285    /// speed.
286    fn staging<'a, Iter>(_data: Iter, _device: &Self::Device)
287    where
288        Iter: Iterator<Item = &'a mut TensorData>,
289    {
290    }
291
292    /// Whether the type is fully supported by the specified device for general operations.
293    ///
294    /// A type is considered supported if it can be used for the full suite of tensor
295    /// operations, including storage, conversion, and basic arithmetic.
296    ///
297    /// Returning `false` does not necessarily mean the device cannot handle the type at all.
298    /// For instance, a device might support a type only for specialized hardware
299    /// acceleration (e.g., matrix multiplication) but lack general arithmetic support. Such
300    /// types should return `false` here as they are not globally supported.
301    fn supports_dtype(device: &Self::Device, dtype: DType) -> bool {
302        Self::dtype_usage(device, dtype).is_superset(DTypeUsage::general())
303    }
304
305    /// Returns the [DTypeUsageSet] for the given [DType] on the specified device.
306    fn dtype_usage(device: &Self::Device, dtype: DType) -> DTypeUsageSet;
307
308    /// Returns the number of devices available on this backend.
309    /// `device` is a reference device used to determine the underlying backend that should be queried.
310    /// A CUDA device will return all devices available to CUDA, a Vulkan device will return all
311    /// devices available to Vulkan, etc.
312    fn device_count(type_id: u16) -> usize;
313}
314
315/// Trait that allows a backend to support autodiff.
316pub trait AutodiffBackend: Backend {
317    /// The inner backend type.
318    type InnerBackend: Backend<Device = Self::Device>;
319
320    /// Gradients type.
321    type Gradients: Send;
322
323    /// Backward pass.
324    ///
325    /// # Arguments
326    ///
327    /// * `tensor` - The tensor is the last node of computational graph where the gradients are computed.
328    ///
329    /// # Returns
330    ///
331    /// The gradients.
332    fn backward(tensor: FloatTensor<Self>) -> Self::Gradients;
333
334    /// Returns the gradients of a tensor.
335    ///
336    /// # Arguments
337    ///
338    /// * `tensor` - The tensor to extract the gradients from.
339    ///
340    /// # Returns
341    ///
342    /// An optional tensor containing the gradient.
343    fn grad(
344        tensor: &FloatTensor<Self>,
345        grads: &Self::Gradients,
346    ) -> Option<FloatTensor<Self::InnerBackend>>;
347
348    /// Pops the gradients of a tensor and returns them.
349    ///
350    /// # Arguments
351    ///
352    /// * `tensor` - The tensor to pop the gradients from.
353    /// * `grads` - The gradients.
354    ///
355    /// # Returns
356    ///
357    /// An optional tensor containing the given gradients.
358    fn grad_remove(
359        tensor: &FloatTensor<Self>,
360        grads: &mut Self::Gradients,
361    ) -> Option<FloatTensor<Self::InnerBackend>>;
362
363    /// Replace the gradients of a tensor with the one provided.
364    ///
365    /// If no gradient existed for the provided tensor, register it.
366    ///
367    /// # Arguments
368    ///
369    /// * `tensor` - The tensor to pop the gradients from.
370    /// * `grads` - The gradients.
371    /// * `grad` - The updated grad tensor.
372    fn grad_replace(
373        tensor: &FloatTensor<Self>,
374        grads: &mut Self::Gradients,
375        grad: FloatTensor<Self::InnerBackend>,
376    );
377
378    /// Returns the tensor with inner backend type.
379    ///
380    /// # Arguments
381    ///
382    /// * `tensor` - The tensor to get the inner backend tensor for.
383    ///
384    /// # Returns
385    ///
386    /// The inner backend tensor.
387    fn inner(tensor: FloatTensor<Self>) -> FloatTensor<Self::InnerBackend>;
388
389    /// Returns the tensor with inner backend type.
390    ///
391    /// # Arguments
392    ///
393    /// * `tensor` - The tensor to get the inner backend tensor for.
394    ///
395    /// # Returns
396    ///
397    /// The inner backend tensor.
398    fn int_inner(tensor: IntTensor<Self>) -> IntTensor<Self::InnerBackend>;
399
400    /// Returns the tensor with inner backend type.
401    ///
402    /// # Arguments
403    ///
404    /// * `tensor` - The tensor to get the inner backend tensor for.
405    ///
406    /// # Returns
407    ///
408    /// The inner backend tensor.
409    fn bool_inner(tensor: BoolTensor<Self>) -> BoolTensor<Self::InnerBackend>;
410
411    /// Returns the tensor with inner backend type.
412    ///
413    /// # Arguments
414    ///
415    /// * `tensor` - The tensor to get the inner backend tensor for.
416    ///
417    /// # Returns
418    ///
419    /// The inner backend tensor.
420    fn q_inner(tensor: QuantizedTensor<Self>) -> QuantizedTensor<Self::InnerBackend>;
421
422    /// Converts the inner backend tensor to the autodiff backend tensor.
423    ///
424    /// # Arguments
425    ///
426    /// * `tensor` - The inner backend tensor to convert.
427    ///
428    ///
429    /// # Returns
430    ///
431    /// The autodiff backend tensor.
432    fn from_inner(tensor: FloatTensor<Self::InnerBackend>) -> FloatTensor<Self>;
433
434    /// Converts the inner backend tensor to the autodiff backend tensor.
435    ///
436    /// # Arguments
437    ///
438    /// * `tensor` - The inner backend tensor to convert.
439    ///
440    ///
441    /// # Returns
442    ///
443    /// The autodiff backend tensor.
444    fn int_from_inner(tensor: IntTensor<Self::InnerBackend>) -> IntTensor<Self>;
445
446    /// Converts the inner backend tensor to the autodiff backend tensor.
447    ///
448    /// # Arguments
449    ///
450    /// * `tensor` - The inner backend tensor to convert.
451    ///
452    ///
453    /// # Returns
454    ///
455    /// The autodiff backend tensor.
456    fn bool_from_inner(tensor: BoolTensor<Self::InnerBackend>) -> BoolTensor<Self>;
457
458    /// Converts the inner backend tensor to the autodiff backend tensor.
459    ///
460    /// # Arguments
461    ///
462    /// * `tensor` - The inner backend tensor to convert.
463    ///
464    ///
465    /// # Returns
466    ///
467    /// The autodiff backend tensor.
468    fn q_from_inner(tensor: QuantizedTensor<Self::InnerBackend>) -> QuantizedTensor<Self>;
469
470    /// Mark the tensor as distributed across multiple devices.
471    /// The gradients will be aggregated during the backward pass.
472    ///
473    /// This function does nothing when distributed training is not available.
474    fn set_distributed_params(
475        tensor: FloatTensor<Self>,
476        _param_id: DistributedParamId,
477    ) -> FloatTensor<Self> {
478        tensor
479    }
480
481    /// Returns the distributed parameters if the tensor was marked as distributed.
482    fn distributed_params(_tensor: &FloatTensor<Self>) -> Option<DistributedParams> {
483        None
484    }
485
486    /// Returns true if the tensor was marked as distributed.
487    fn is_distributed(_tensor: &FloatTensor<Self>) -> bool {
488        false
489    }
490}
491
492/// Describes how a data type can be used on a given device.
493///
494/// A data type may be supported for different classes of operations. Not all
495/// data types that appear in hardware or kernel implementations are suitable
496/// for general-purpose tensor operations.
497#[derive(Debug, EnumSetType)]
498pub enum DTypeUsage {
499    /// The type can be stored in device memory and converted to and from
500    /// other supported data types.
501    Storage,
502    /// The type supports general-purpose arithmetic and common tensor
503    /// operations (e.g. elementwise ops, reductions, etc.).
504    Arithmetic,
505    /// The type is supported by hardware-accelerated execution paths.
506    ///
507    /// This typically indicates support for accelerator-backed compute units (e.g., tensor
508    /// cores executing MMA instructions) for high-performance operations such as matrix
509    /// multiplication and operations that lower to it.
510    ///
511    /// # Notes
512    /// - A type can be both [`Arithmetic`](DTypeUsage::Arithmetic) and
513    ///   [`Accelerated`](DTypeUsage::Accelerated) if it supports general-purpose operations
514    ///   *and* accelerated paths.
515    /// - If a type is marked as `Accelerated` but not `Arithmetic`, it is not
516    ///   suitable for general-purpose tensor operations and may only be used
517    ///   in specific accelerated operations.
518    ///
519    /// `Accelerated` is a **flag**, not a detailed descriptor. It does not enumerate which
520    /// operations are accelerated or which accelerator features are available.
521    Accelerated,
522}
523
524/// A set of [DTypeUsage] representing the total capabilities of a data type on a device.
525pub type DTypeUsageSet = EnumSet<DTypeUsage>;
526
527impl DTypeUsage {
528    /// Returns the usage set required for general-purpose tensor support.
529    pub fn general() -> DTypeUsageSet {
530        DTypeUsage::Storage | DTypeUsage::Arithmetic
531    }
532}