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::profile::profile_unsupported;
17use super::{MemoryPoolUsage, SlicedPoolReport};
18use super::{ProfileDuration, ProfileOptions, ProfileToken, profile_system_time};
19
20/// The mapping of types used by Backend and traits.
21pub trait BackendTypes: Clone + Send + Sync + core::fmt::Debug + 'static {
22 /// Device type.
23 type Device: DeviceOps;
24
25 /// Tensor primitive to be used for all float operations.
26 type FloatTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
27
28 /// Tensor primitive to be used for all int operations.
29 type IntTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
30
31 /// Tensor primitive to be used for all bool operations.
32 type BoolTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
33
34 /// Tensor primitive to be used for all quantized operations.
35 type QuantizedTensorPrimitive: TensorMetadata<Device = Self::Device> + 'static;
36
37 /// Captured graph primitive returned by [`Backend::graph_stop_capture`] and
38 /// consumed by [`Backend::graph_replay`]: a backend-owned recording of a
39 /// launch sequence that replays as a single dispatch.
40 ///
41 /// Backends without graph-capture support use [`GraphUnsupported`], an
42 /// uninhabited type — their capture methods only ever error, so no value of
43 /// it can exist.
44 type GraphPrimitive: Clone + Send + Sync + core::fmt::Debug + 'static;
45}
46
47/// Captured graph primitive type used by the backend (see
48/// [`BackendTypes::GraphPrimitive`]).
49pub type BackendGraph<B> = <B as BackendTypes>::GraphPrimitive;
50
51/// Placeholder [graph primitive](BackendTypes::GraphPrimitive) for backends
52/// without graph-capture support.
53///
54/// Uninhabited: `graph_stop_capture` on such backends always errors, so a value
55/// of this type can never be constructed (and `graph_replay` can never be called).
56#[derive(Debug, Clone, Copy)]
57pub enum GraphUnsupported {}
58
59/// The error returned by the default (unsupported) graph-capture methods.
60fn graph_unsupported() -> ExecutionError {
61 ExecutionError::Generic {
62 reason: alloc::string::String::from("graph capture is not supported by this backend"),
63 backtrace: BackTrace::capture(),
64 }
65}
66
67/// This trait defines all types and functions needed for a backend to be used with burn.
68///
69/// ## Design
70///
71/// This trait aims to be as unopinionated as possible and allows implementations to define
72/// their own types and patterns. Therefore, there are few pre-defined abstractions baked
73/// into this trait.
74///
75/// Backends must define their own tensor types for each data type: `float`, `int`, and `bool`.
76/// Since we minimize assumptions, we chose to separate these types, as they are used in
77/// different contexts. However, some backends may have a generic tensor type that is used
78/// for all data types.
79///
80/// ### Eager Mode
81///
82/// Because burn supports dynamic graphs, the backend trait is designed around kernel
83/// implementations that can be called without any mutable context or graph. This may not be
84/// ideal for backends that want to configure their computational graphs and execute them
85/// multiple times.
86///
87/// To implement this kind of backend, channels could be used to communicate with a backend
88/// server thread to build the computation graphs and re-execute the ones that are repeated,
89/// with some form of cache. Once that pattern has matured, a graph mode backend trait could
90/// be extracted from it, allowing other backends of the same kind to be quickly integrated
91/// with burn. This pattern could also be used to create an operation fusion trait, which
92/// allows backends to define what kind of graph structures can be fused into one operation.
93///
94/// ### Multi-Threaded
95///
96/// Backend tensor types are all `Clone` + `Send`, which allows them to be safely
97/// sent between threads. It is recommended to wrap tensors with [Arc](alloc::sync::Arc),
98/// which avoids copying the tensor's buffer. Note that it is still possible to mutate and
99/// reuse tensors' buffer without locking; see the next section on the Mutable API.
100///
101/// ### Mutable API
102///
103/// There is no mutable or inplace operation API to implement, but that does not mean that
104/// backends cannot support them. Using [try_unwrap](alloc::sync::Arc::try_unwrap) and
105/// [get_mut](alloc::sync::Arc::get_mut) allows backends to have access to an owned or mutable
106/// reference to their tensor buffer data structure if the tensor is not shared. In that case,
107/// backends can dispatch to their owned inplace operations for better performance.
108///
109/// ## Documentation
110///
111/// Most of the documentation for each function can be found on the user API
112#[cfg_attr(doc, doc = crate::doc_tensor!())]
113#[cfg_attr(not(doc), doc = "`Tensor`")]
114/// struct in the `burn-tensor` crate.
115/// For modules, public functions are often created, which can be used by `burn-core` modules.
116pub trait Backend:
117 BackendTypes
118 + FloatTensorOps<Self>
119 + BoolTensorOps<Self>
120 + IntTensorOps<Self>
121 + ModuleOps<Self>
122 + ActivationOps<Self>
123 + QTensorOps<Self>
124 + TransactionOps<Self>
125 + DistributedOps<Self>
126 + CollectiveOps<Self>
127 + Clone
128 + Default
129 + Sized
130 + Send
131 + Sync
132 + core::fmt::Debug
133 + 'static
134{
135 /// If autodiff is enabled.
136 fn ad_enabled(_device: &Self::Device) -> bool {
137 false
138 }
139
140 /// Sets the current allocation mode to persistent.
141 #[allow(unused_variables)]
142 fn memory_persistent_allocations<
143 Output: Send,
144 Input: Send,
145 Func: Fn(Input) -> Output + Send,
146 >(
147 device: &Self::Device,
148 input: Input,
149 func: Func,
150 ) -> Output {
151 func(input)
152 }
153
154 /// Manually triggers a memory cleanup on the given device.
155 #[allow(unused_variables)]
156 fn memory_cleanup(device: &Self::Device) {}
157
158 /// The dynamic pools' measured state, in the order allocations are routed
159 /// through them. `None` on a backend that does not report one, or whose
160 /// stream has failed.
161 ///
162 /// One entry per pool that carves pages, the pools a growth left behind
163 /// last. Pools of other kinds — an allocation's own page, the metadata
164 /// churn — are left out.
165 #[allow(unused_variables)]
166 fn memory_pool_report(device: &Self::Device) -> Option<Vec<SlicedPoolReport>> {
167 None
168 }
169
170 /// The device allocator's current state. `None` on a backend that does not
171 /// report one, or whose stream has failed.
172 #[allow(unused_variables)]
173 fn memory_pool_usage(device: &Self::Device) -> Option<MemoryPoolUsage> {
174 None
175 }
176
177 /// How many bytes the device can hold at once, as its runtime states it. `None` on a backend
178 /// that states no figure.
179 #[allow(unused_variables)]
180 fn memory_capacity(device: &Self::Device) -> Option<u64> {
181 None
182 }
183
184 /// Name of the backend.
185 fn name(device: &Self::Device) -> String;
186
187 /// Seeds the backend on the specified device.
188 ///
189 /// There is no guarantee that only the specified device will be seeded, but it is guaranteed
190 /// that at least the specified device will be seeded.
191 ///
192 /// In all cases, this should ensure deterministic execution for a single-threaded program.
193 fn seed(device: &Self::Device, seed: u64);
194
195 /// Sync the backend, ensure that all computation are finished.
196 fn sync(_device: &Self::Device) -> Result<(), ExecutionError> {
197 Ok(())
198 }
199
200 /// Measure how long the device spends on the work `func` puts on the
201 /// calling stream, in device time.
202 ///
203 /// The window opens where the stream is when the call is made and closes
204 /// where the stream is when `func` returns: work the stream still owed
205 /// from before falls in, and work a backend queues past the end (a
206 /// batching backend's last operations, unless `options` flush) falls out.
207 /// Nothing is waited on — the [`ProfileDuration`] resolves later, when
208 /// the device has stamped both ends — so windows nest without the inner
209 /// ones being charged to the outer. Work on other streams is not kept
210 /// out, and not counted. A window that nothing ran in reads as no time.
211 ///
212 /// The default is [`profile_system_time`]: wall-clock time between two
213 /// syncs, for a backend with no device clock to read. That one does wait,
214 /// and an inner window's syncs are charged to the outer.
215 ///
216 /// # Errors
217 ///
218 /// The device refused to open or close the window, or work inside it
219 /// failed and took the measurement with it. `func` has run by then — a
220 /// window that could not be opened does not cancel the work it was asked
221 /// to measure — and its output is lost with the error, as it would be on
222 /// the read that the failure surfaces on without a window.
223 ///
224 /// **A failure that the device only reports later is not here.** The
225 /// measurement is resolved after this returns, so anything the device
226 /// learns in between — and everything a *remote* server reports, which
227 /// travels back with the measurement rather than ahead of it — arrives as
228 /// a window that resolves to no measurement, with the reason in the log.
229 /// A caller that must distinguish "nothing ran" from "the server failed"
230 /// cannot do it from the `Result` alone.
231 fn profile<O: Send + 'static>(
232 device: &Self::Device,
233 options: ProfileOptions,
234 func: impl FnOnce() -> O + Send,
235 ) -> Result<(O, ProfileDuration), ExecutionError> {
236 let _ = options;
237 profile_system_time::<Self, O>(device, func)
238 }
239
240 /// Open a [profiling window](Self::profile) at the calling stream's
241 /// current position, to be closed with
242 /// [`profile_end`](Self::profile_end) from the same stream.
243 ///
244 /// For a caller that cannot bracket the work in a closure: a backend that
245 /// forwards operations to be executed on another thread opens and closes
246 /// the window from that thread, in order with the operations.
247 ///
248 /// `None` from a backend that opens no windows and measures only with
249 /// [`profile`](Self::profile) — the default — so the caller can bracket
250 /// with [`profile_system_time`] instead.
251 fn profile_start(_device: &Self::Device) -> Result<Option<ProfileToken>, ExecutionError> {
252 Ok(None)
253 }
254
255 /// Close the window `token` at the calling stream's current position.
256 ///
257 /// When `options` flush, the work the backend still holds queued for the
258 /// stream executes first, so it falls inside the window. A backend that
259 /// forwards the close passes `options` along, so a queue further down
260 /// the chain — a remote server's fusion, say — is flushed too.
261 ///
262 /// Errors on a backend whose [`profile_start`](Self::profile_start) hands
263 /// out no token.
264 fn profile_end(
265 _device: &Self::Device,
266 _token: ProfileToken,
267 _options: ProfileOptions,
268 ) -> Result<ProfileDuration, ExecutionError> {
269 Err(profile_unsupported())
270 }
271
272 /// Drop the window `token` opened without measuring it, for a caller that
273 /// will never reach [`profile_end`](Self::profile_end).
274 ///
275 /// **An open window is not free**, and the cost is not paid once: a
276 /// backend holds a start event, keeps timestamp writes on, or retains
277 /// command buffers for as long as one is open, and on wgpu every later
278 /// pass keeps rewriting the live window's end slot. So a window whose
279 /// caller unwound between the two calls is abandoned rather than left,
280 /// which is what [`profile_with_tokens`](crate::profile_with_tokens) does
281 /// on the panic path.
282 ///
283 /// Cannot fail and answers nothing: it is called while a panic is already
284 /// unwinding, where there is nobody left to tell. The default closes the
285 /// window and discards the measurement, which every backend can already
286 /// do; one that can drop a window without recording an end does that
287 /// instead.
288 fn profile_abandon(device: &Self::Device, token: ProfileToken) {
289 let _ = Self::profile_end(device, token, ProfileOptions::default());
290 }
291
292 /// Prepare `device` for an upcoming graph capture: route allocations into a
293 /// stable pool so every buffer allocated before graph_stop_capture can
294 /// be pinned. Call before the warmup run. No-op by default.
295 ///
296 /// See [`burn_graph`](crate) — the closure-based `capture` helper drives
297 /// this whole sequence.
298 fn graph_prepare(_device: &Self::Device) -> Result<(), ExecutionError> {
299 Ok(())
300 }
301
302 /// Begin recording launches on `device` into a graph (see
303 /// [`graph_stop_capture`](Backend::graph_stop_capture)). Errors on backends
304 /// without hardware graph support, so callers fall back to re-running.
305 fn graph_start_capture(_device: &Self::Device) -> Result<(), ExecutionError> {
306 Err(graph_unsupported())
307 }
308
309 /// Stop recording and return the captured [graph](BackendTypes::GraphPrimitive),
310 /// ready to [`graph_replay`](Backend::graph_replay).
311 fn graph_stop_capture(_device: &Self::Device) -> Result<BackendGraph<Self>, ExecutionError> {
312 Err(graph_unsupported())
313 }
314
315 /// Replay a captured [graph](BackendTypes::GraphPrimitive) — one dispatch
316 /// re-running the recorded launches against their original buffers.
317 ///
318 /// # Safety
319 ///
320 /// The replay dispatches raw device work against the exact buffers recorded
321 /// at capture time, with nothing tracking whether those buffers are still
322 /// valid. The caller must guarantee, for every tensor the captured closure
323 /// read or wrote:
324 ///
325 /// - its buffer is still alive — no tensor referenced by the graph has been
326 /// freed (and its memory possibly reallocated) since capture;
327 /// - it is not concurrently read or written by work on another stream or
328 /// thread while the replay executes;
329 /// - input refreshes and output reads are issued on the stream the graph
330 /// was captured on, so they order correctly against the replay.
331 unsafe fn graph_replay(
332 _device: &Self::Device,
333 _graph: &BackendGraph<Self>,
334 ) -> Result<(), ExecutionError> {
335 Err(graph_unsupported())
336 }
337
338 /// Flush any pending operation of the backend.
339 ///
340 /// # Errors
341 ///
342 /// Returns an [`ExecutionError`] when the pending operations cannot be dispatched, e.g. on
343 /// a device that is poisoned.
344 fn flush(_device: &Self::Device) -> Result<(), ExecutionError>;
345
346 /// Marks the given data as being used as a staging buffer for transfer between CPU and
347 /// accelerators like GPUs.
348 ///
349 /// The given data might be transferred to pinned memory or another format to improve data transfer
350 /// speed.
351 fn staging<'a, Iter>(_data: Iter, _device: &Self::Device)
352 where
353 Iter: Iterator<Item = &'a mut TensorData>,
354 {
355 }
356
357 /// Whether the type is fully supported by the specified device for general operations.
358 ///
359 /// A type is considered supported if it can be used for the full suite of tensor
360 /// operations, including storage, conversion, and basic arithmetic.
361 ///
362 /// Returning `false` does not necessarily mean the device cannot handle the type at all.
363 /// For instance, a device might support a type only for specialized hardware
364 /// acceleration (e.g., matrix multiplication) but lack general arithmetic support. Such
365 /// types should return `false` here as they are not globally supported.
366 fn supports_dtype(device: &Self::Device, dtype: DType) -> bool {
367 Self::dtype_usage(device, dtype).is_superset(DTypeUsage::general())
368 }
369
370 /// Returns the [DTypeUsageSet] for the given [DType] on the specified device.
371 fn dtype_usage(device: &Self::Device, dtype: DType) -> DTypeUsageSet;
372
373 /// Returns the number of devices available on this backend.
374 /// `device` is a reference device used to determine the underlying backend that should be queried.
375 /// A CUDA device will return all devices available to CUDA, a Vulkan device will return all
376 /// devices available to Vulkan, etc.
377 fn device_count(type_id: u16) -> usize;
378}
379
380/// Trait that allows a backend to support autodiff.
381pub trait AutodiffBackend: Backend {
382 /// The inner backend type.
383 type InnerBackend: Backend<Device = Self::Device>;
384
385 /// Gradients type.
386 type Gradients: Send;
387
388 /// Backward pass.
389 ///
390 /// # Arguments
391 ///
392 /// * `tensor` - The tensor is the last node of computational graph where the gradients are computed.
393 ///
394 /// # Returns
395 ///
396 /// The gradients.
397 fn backward(tensor: FloatTensor<Self>) -> Self::Gradients;
398
399 /// Returns the gradients of a tensor.
400 ///
401 /// # Arguments
402 ///
403 /// * `tensor` - The tensor to extract the gradients from.
404 ///
405 /// # Returns
406 ///
407 /// An optional tensor containing the gradient.
408 fn grad(
409 tensor: &FloatTensor<Self>,
410 grads: &Self::Gradients,
411 ) -> Option<FloatTensor<Self::InnerBackend>>;
412
413 /// Pops the gradients of a tensor and returns them.
414 ///
415 /// # Arguments
416 ///
417 /// * `tensor` - The tensor to pop the gradients from.
418 /// * `grads` - The gradients.
419 ///
420 /// # Returns
421 ///
422 /// An optional tensor containing the given gradients.
423 fn grad_remove(
424 tensor: &FloatTensor<Self>,
425 grads: &mut Self::Gradients,
426 ) -> Option<FloatTensor<Self::InnerBackend>>;
427
428 /// Replace the gradients of a tensor with the one provided.
429 ///
430 /// If no gradient existed for the provided tensor, register it.
431 ///
432 /// # Arguments
433 ///
434 /// * `tensor` - The tensor to pop the gradients from.
435 /// * `grads` - The gradients.
436 /// * `grad` - The updated grad tensor.
437 fn grad_replace(
438 tensor: &FloatTensor<Self>,
439 grads: &mut Self::Gradients,
440 grad: FloatTensor<Self::InnerBackend>,
441 );
442
443 /// Returns the tensor with inner backend type.
444 ///
445 /// # Arguments
446 ///
447 /// * `tensor` - The tensor to get the inner backend tensor for.
448 ///
449 /// # Returns
450 ///
451 /// The inner backend tensor.
452 fn inner(tensor: FloatTensor<Self>) -> FloatTensor<Self::InnerBackend>;
453
454 /// Returns the tensor with inner backend type.
455 ///
456 /// # Arguments
457 ///
458 /// * `tensor` - The tensor to get the inner backend tensor for.
459 ///
460 /// # Returns
461 ///
462 /// The inner backend tensor.
463 fn int_inner(tensor: IntTensor<Self>) -> IntTensor<Self::InnerBackend>;
464
465 /// Returns the tensor with inner backend type.
466 ///
467 /// # Arguments
468 ///
469 /// * `tensor` - The tensor to get the inner backend tensor for.
470 ///
471 /// # Returns
472 ///
473 /// The inner backend tensor.
474 fn bool_inner(tensor: BoolTensor<Self>) -> BoolTensor<Self::InnerBackend>;
475
476 /// Returns the tensor with inner backend type.
477 ///
478 /// # Arguments
479 ///
480 /// * `tensor` - The tensor to get the inner backend tensor for.
481 ///
482 /// # Returns
483 ///
484 /// The inner backend tensor.
485 fn q_inner(tensor: QuantizedTensor<Self>) -> QuantizedTensor<Self::InnerBackend>;
486
487 /// Converts the inner backend tensor to the autodiff backend tensor.
488 ///
489 /// # Arguments
490 ///
491 /// * `tensor` - The inner backend tensor to convert.
492 ///
493 ///
494 /// # Returns
495 ///
496 /// The autodiff backend tensor.
497 fn from_inner(tensor: FloatTensor<Self::InnerBackend>) -> FloatTensor<Self>;
498
499 /// Converts the inner backend tensor to the autodiff backend tensor.
500 ///
501 /// # Arguments
502 ///
503 /// * `tensor` - The inner backend tensor to convert.
504 ///
505 ///
506 /// # Returns
507 ///
508 /// The autodiff backend tensor.
509 fn int_from_inner(tensor: IntTensor<Self::InnerBackend>) -> IntTensor<Self>;
510
511 /// Converts the inner backend tensor to the autodiff backend tensor.
512 ///
513 /// # Arguments
514 ///
515 /// * `tensor` - The inner backend tensor to convert.
516 ///
517 ///
518 /// # Returns
519 ///
520 /// The autodiff backend tensor.
521 fn bool_from_inner(tensor: BoolTensor<Self::InnerBackend>) -> BoolTensor<Self>;
522
523 /// Converts the inner backend tensor to the autodiff backend tensor.
524 ///
525 /// # Arguments
526 ///
527 /// * `tensor` - The inner backend tensor to convert.
528 ///
529 ///
530 /// # Returns
531 ///
532 /// The autodiff backend tensor.
533 fn q_from_inner(tensor: QuantizedTensor<Self::InnerBackend>) -> QuantizedTensor<Self>;
534
535 /// Mark the tensor as distributed across multiple devices.
536 /// The gradients will be aggregated during the backward pass.
537 ///
538 /// This function does nothing when distributed training is not available.
539 fn set_distributed_params(
540 tensor: FloatTensor<Self>,
541 _param_id: DistributedParamId,
542 ) -> FloatTensor<Self> {
543 tensor
544 }
545
546 /// Returns the distributed parameters if the tensor was marked as distributed.
547 fn distributed_params(_tensor: &FloatTensor<Self>) -> Option<DistributedParams> {
548 None
549 }
550
551 /// Returns true if the tensor was marked as distributed.
552 fn is_distributed(_tensor: &FloatTensor<Self>) -> bool {
553 false
554 }
555}
556
557/// Describes how a data type can be used on a given device.
558///
559/// A data type may be supported for different classes of operations. Not all
560/// data types that appear in hardware or kernel implementations are suitable
561/// for general-purpose tensor operations.
562#[derive(Debug, EnumSetType)]
563pub enum DTypeUsage {
564 /// The type can be stored in device memory and converted to and from
565 /// other supported data types.
566 Storage,
567 /// The type supports general-purpose arithmetic and common tensor
568 /// operations (e.g. elementwise ops, reductions, etc.).
569 Arithmetic,
570 /// The type is supported by hardware-accelerated execution paths.
571 ///
572 /// This typically indicates support for accelerator-backed compute units (e.g., tensor
573 /// cores executing MMA instructions) for high-performance operations such as matrix
574 /// multiplication and operations that lower to it.
575 ///
576 /// # Notes
577 /// - A type can be both [`Arithmetic`](DTypeUsage::Arithmetic) and
578 /// [`Accelerated`](DTypeUsage::Accelerated) if it supports general-purpose operations
579 /// *and* accelerated paths.
580 /// - If a type is marked as `Accelerated` but not `Arithmetic`, it is not
581 /// suitable for general-purpose tensor operations and may only be used
582 /// in specific accelerated operations.
583 ///
584 /// `Accelerated` is a **flag**, not a detailed descriptor. It does not enumerate which
585 /// operations are accelerated or which accelerator features are available.
586 Accelerated,
587}
588
589/// A set of [DTypeUsage] representing the total capabilities of a data type on a device.
590pub type DTypeUsageSet = EnumSet<DTypeUsage>;
591
592impl DTypeUsage {
593 /// Returns the usage set required for general-purpose tensor support.
594 pub fn general() -> DTypeUsageSet {
595 DTypeUsage::Storage | DTypeUsage::Arithmetic
596 }
597}