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::profile::profile_unsupported;
17use super::{InstallMemoryPoolsError, MemoryPoolLayout, 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    + Clone
127    + Default
128    + Sized
129    + Send
130    + Sync
131    + core::fmt::Debug
132    + 'static
133{
134    /// If autodiff is enabled.
135    fn ad_enabled(_device: &Self::Device) -> bool {
136        false
137    }
138
139    /// Sets the current allocation mode to persistent.
140    #[allow(unused_variables)]
141    fn memory_persistent_allocations<
142        Output: Send,
143        Input: Send,
144        Func: Fn(Input) -> Output + Send,
145    >(
146        device: &Self::Device,
147        input: Input,
148        func: Func,
149    ) -> Output {
150        func(input)
151    }
152
153    /// Manually triggers a memory cleanup on the given device.
154    #[allow(unused_variables)]
155    fn memory_cleanup(device: &Self::Device) {}
156
157    /// Install a layout for the device's dynamic memory pools.
158    ///
159    /// A per-workload setting: the calling stream's pools are rebuilt in place
160    /// when nothing is live in them — so install at a quiescent point, after
161    /// the previous workload's tensors have dropped and a
162    /// [`memory_cleanup`](Self::memory_cleanup) — and streams created
163    /// afterwards use the new layout.
164    ///
165    /// Sizing a layout from a measurement means installing twice: once
166    /// growable, to run the workload and read
167    /// [`memory_pool_report`](Self::memory_pool_report), and once capped at
168    /// what that reported.
169    ///
170    /// # Errors
171    ///
172    /// [`InstallMemoryPoolsError::PoolsInUse`] when something is still live in
173    /// the pools being rebuilt, worth retrying once it drains;
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    /// Measure how long the device spends on the work `func` puts on the
233    /// calling stream, in device time.
234    ///
235    /// The window opens where the stream is when the call is made and closes
236    /// where the stream is when `func` returns: work the stream still owed
237    /// from before falls in, and work a backend queues past the end (a
238    /// batching backend's last operations, unless `options` flush) falls out.
239    /// Nothing is waited on — the [`ProfileDuration`] resolves later, when
240    /// the device has stamped both ends — so windows nest without the inner
241    /// ones being charged to the outer. Work on other streams is not kept
242    /// out, and not counted. A window that nothing ran in reads as no time.
243    ///
244    /// The default is [`profile_system_time`]: wall-clock time between two
245    /// syncs, for a backend with no device clock to read. That one does wait,
246    /// and an inner window's syncs are charged to the outer.
247    ///
248    /// # Errors
249    ///
250    /// The device refused to open or close the window, or work inside it
251    /// failed and took the measurement with it. `func` has run by then — a
252    /// window that could not be opened does not cancel the work it was asked
253    /// to measure — and its output is lost with the error, as it would be on
254    /// the read that the failure surfaces on without a window.
255    ///
256    /// **A failure that the device only reports later is not here.** The
257    /// measurement is resolved after this returns, so anything the device
258    /// learns in between — and everything a *remote* server reports, which
259    /// travels back with the measurement rather than ahead of it — arrives as
260    /// a window that resolves to no measurement, with the reason in the log.
261    /// A caller that must distinguish "nothing ran" from "the server failed"
262    /// cannot do it from the `Result` alone.
263    fn profile<O: Send + 'static>(
264        device: &Self::Device,
265        options: ProfileOptions,
266        func: impl FnOnce() -> O + Send,
267    ) -> Result<(O, ProfileDuration), ExecutionError> {
268        let _ = options;
269        profile_system_time::<Self, O>(device, func)
270    }
271
272    /// Open a [profiling window](Self::profile) at the calling stream's
273    /// current position, to be closed with
274    /// [`profile_end`](Self::profile_end) from the same stream.
275    ///
276    /// For a caller that cannot bracket the work in a closure: a backend that
277    /// forwards operations to be executed on another thread opens and closes
278    /// the window from that thread, in order with the operations.
279    ///
280    /// `None` from a backend that opens no windows and measures only with
281    /// [`profile`](Self::profile) — the default — so the caller can bracket
282    /// with [`profile_system_time`] instead.
283    fn profile_start(_device: &Self::Device) -> Result<Option<ProfileToken>, ExecutionError> {
284        Ok(None)
285    }
286
287    /// Close the window `token` at the calling stream's current position.
288    ///
289    /// When `options` flush, the work the backend still holds queued for the
290    /// stream executes first, so it falls inside the window. A backend that
291    /// forwards the close passes `options` along, so a queue further down
292    /// the chain — a remote server's fusion, say — is flushed too.
293    ///
294    /// Errors on a backend whose [`profile_start`](Self::profile_start) hands
295    /// out no token.
296    fn profile_end(
297        _device: &Self::Device,
298        _token: ProfileToken,
299        _options: ProfileOptions,
300    ) -> Result<ProfileDuration, ExecutionError> {
301        Err(profile_unsupported())
302    }
303
304    /// Drop the window `token` opened without measuring it, for a caller that
305    /// will never reach [`profile_end`](Self::profile_end).
306    ///
307    /// **An open window is not free**, and the cost is not paid once: a
308    /// backend holds a start event, keeps timestamp writes on, or retains
309    /// command buffers for as long as one is open, and on wgpu every later
310    /// pass keeps rewriting the live window's end slot. So a window whose
311    /// caller unwound between the two calls is abandoned rather than left,
312    /// which is what [`profile_with_tokens`](crate::profile_with_tokens) does
313    /// on the panic path.
314    ///
315    /// Cannot fail and answers nothing: it is called while a panic is already
316    /// unwinding, where there is nobody left to tell. The default closes the
317    /// window and discards the measurement, which every backend can already
318    /// do; one that can drop a window without recording an end does that
319    /// instead.
320    fn profile_abandon(device: &Self::Device, token: ProfileToken) {
321        let _ = Self::profile_end(device, token, ProfileOptions::default());
322    }
323
324    /// Prepare `device` for an upcoming graph capture: route allocations into a
325    /// stable pool so every buffer allocated before graph_stop_capture can
326    /// be pinned. Call before the warmup run. No-op by default.
327    ///
328    /// See [`burn_graph`](crate) — the closure-based `capture` helper drives
329    /// this whole sequence.
330    fn graph_prepare(_device: &Self::Device) -> Result<(), ExecutionError> {
331        Ok(())
332    }
333
334    /// Begin recording launches on `device` into a graph (see
335    /// [`graph_stop_capture`](Backend::graph_stop_capture)). Errors on backends
336    /// without hardware graph support, so callers fall back to re-running.
337    fn graph_start_capture(_device: &Self::Device) -> Result<(), ExecutionError> {
338        Err(graph_unsupported())
339    }
340
341    /// Stop recording and return the captured [graph](BackendTypes::GraphPrimitive),
342    /// ready to [`graph_replay`](Backend::graph_replay).
343    fn graph_stop_capture(_device: &Self::Device) -> Result<BackendGraph<Self>, ExecutionError> {
344        Err(graph_unsupported())
345    }
346
347    /// Replay a captured [graph](BackendTypes::GraphPrimitive) — one dispatch
348    /// re-running the recorded launches against their original buffers.
349    ///
350    /// # Safety
351    ///
352    /// The replay dispatches raw device work against the exact buffers recorded
353    /// at capture time, with nothing tracking whether those buffers are still
354    /// valid. The caller must guarantee, for every tensor the captured closure
355    /// read or wrote:
356    ///
357    /// - its buffer is still alive — no tensor referenced by the graph has been
358    ///   freed (and its memory possibly reallocated) since capture;
359    /// - it is not concurrently read or written by work on another stream or
360    ///   thread while the replay executes;
361    /// - input refreshes and output reads are issued on the stream the graph
362    ///   was captured on, so they order correctly against the replay.
363    unsafe fn graph_replay(
364        _device: &Self::Device,
365        _graph: &BackendGraph<Self>,
366    ) -> Result<(), ExecutionError> {
367        Err(graph_unsupported())
368    }
369
370    /// Flush any pending operation of the backend.
371    fn flush(_device: &Self::Device);
372
373    /// Marks the given data as being used as a staging buffer for transfer between CPU and
374    /// accelerators like GPUs.
375    ///
376    /// The given data might be transferred to pinned memory or another format to improve data transfer
377    /// speed.
378    fn staging<'a, Iter>(_data: Iter, _device: &Self::Device)
379    where
380        Iter: Iterator<Item = &'a mut TensorData>,
381    {
382    }
383
384    /// Whether the type is fully supported by the specified device for general operations.
385    ///
386    /// A type is considered supported if it can be used for the full suite of tensor
387    /// operations, including storage, conversion, and basic arithmetic.
388    ///
389    /// Returning `false` does not necessarily mean the device cannot handle the type at all.
390    /// For instance, a device might support a type only for specialized hardware
391    /// acceleration (e.g., matrix multiplication) but lack general arithmetic support. Such
392    /// types should return `false` here as they are not globally supported.
393    fn supports_dtype(device: &Self::Device, dtype: DType) -> bool {
394        Self::dtype_usage(device, dtype).is_superset(DTypeUsage::general())
395    }
396
397    /// Returns the [DTypeUsageSet] for the given [DType] on the specified device.
398    fn dtype_usage(device: &Self::Device, dtype: DType) -> DTypeUsageSet;
399
400    /// Returns the number of devices available on this backend.
401    /// `device` is a reference device used to determine the underlying backend that should be queried.
402    /// A CUDA device will return all devices available to CUDA, a Vulkan device will return all
403    /// devices available to Vulkan, etc.
404    fn device_count(type_id: u16) -> usize;
405}
406
407/// Trait that allows a backend to support autodiff.
408pub trait AutodiffBackend: Backend {
409    /// The inner backend type.
410    type InnerBackend: Backend<Device = Self::Device>;
411
412    /// Gradients type.
413    type Gradients: Send;
414
415    /// Backward pass.
416    ///
417    /// # Arguments
418    ///
419    /// * `tensor` - The tensor is the last node of computational graph where the gradients are computed.
420    ///
421    /// # Returns
422    ///
423    /// The gradients.
424    fn backward(tensor: FloatTensor<Self>) -> Self::Gradients;
425
426    /// Returns the gradients of a tensor.
427    ///
428    /// # Arguments
429    ///
430    /// * `tensor` - The tensor to extract the gradients from.
431    ///
432    /// # Returns
433    ///
434    /// An optional tensor containing the gradient.
435    fn grad(
436        tensor: &FloatTensor<Self>,
437        grads: &Self::Gradients,
438    ) -> Option<FloatTensor<Self::InnerBackend>>;
439
440    /// Pops the gradients of a tensor and returns them.
441    ///
442    /// # Arguments
443    ///
444    /// * `tensor` - The tensor to pop the gradients from.
445    /// * `grads` - The gradients.
446    ///
447    /// # Returns
448    ///
449    /// An optional tensor containing the given gradients.
450    fn grad_remove(
451        tensor: &FloatTensor<Self>,
452        grads: &mut Self::Gradients,
453    ) -> Option<FloatTensor<Self::InnerBackend>>;
454
455    /// Replace the gradients of a tensor with the one provided.
456    ///
457    /// If no gradient existed for the provided tensor, register it.
458    ///
459    /// # Arguments
460    ///
461    /// * `tensor` - The tensor to pop the gradients from.
462    /// * `grads` - The gradients.
463    /// * `grad` - The updated grad tensor.
464    fn grad_replace(
465        tensor: &FloatTensor<Self>,
466        grads: &mut Self::Gradients,
467        grad: FloatTensor<Self::InnerBackend>,
468    );
469
470    /// Returns the tensor with inner backend type.
471    ///
472    /// # Arguments
473    ///
474    /// * `tensor` - The tensor to get the inner backend tensor for.
475    ///
476    /// # Returns
477    ///
478    /// The inner backend tensor.
479    fn inner(tensor: FloatTensor<Self>) -> FloatTensor<Self::InnerBackend>;
480
481    /// Returns the tensor with inner backend type.
482    ///
483    /// # Arguments
484    ///
485    /// * `tensor` - The tensor to get the inner backend tensor for.
486    ///
487    /// # Returns
488    ///
489    /// The inner backend tensor.
490    fn int_inner(tensor: IntTensor<Self>) -> IntTensor<Self::InnerBackend>;
491
492    /// Returns the tensor with inner backend type.
493    ///
494    /// # Arguments
495    ///
496    /// * `tensor` - The tensor to get the inner backend tensor for.
497    ///
498    /// # Returns
499    ///
500    /// The inner backend tensor.
501    fn bool_inner(tensor: BoolTensor<Self>) -> BoolTensor<Self::InnerBackend>;
502
503    /// Returns the tensor with inner backend type.
504    ///
505    /// # Arguments
506    ///
507    /// * `tensor` - The tensor to get the inner backend tensor for.
508    ///
509    /// # Returns
510    ///
511    /// The inner backend tensor.
512    fn q_inner(tensor: QuantizedTensor<Self>) -> QuantizedTensor<Self::InnerBackend>;
513
514    /// Converts the inner backend tensor to the autodiff backend tensor.
515    ///
516    /// # Arguments
517    ///
518    /// * `tensor` - The inner backend tensor to convert.
519    ///
520    ///
521    /// # Returns
522    ///
523    /// The autodiff backend tensor.
524    fn from_inner(tensor: FloatTensor<Self::InnerBackend>) -> FloatTensor<Self>;
525
526    /// Converts the inner backend tensor to the autodiff backend tensor.
527    ///
528    /// # Arguments
529    ///
530    /// * `tensor` - The inner backend tensor to convert.
531    ///
532    ///
533    /// # Returns
534    ///
535    /// The autodiff backend tensor.
536    fn int_from_inner(tensor: IntTensor<Self::InnerBackend>) -> IntTensor<Self>;
537
538    /// Converts the inner backend tensor to the autodiff backend tensor.
539    ///
540    /// # Arguments
541    ///
542    /// * `tensor` - The inner backend tensor to convert.
543    ///
544    ///
545    /// # Returns
546    ///
547    /// The autodiff backend tensor.
548    fn bool_from_inner(tensor: BoolTensor<Self::InnerBackend>) -> BoolTensor<Self>;
549
550    /// Converts the inner backend tensor to the autodiff backend tensor.
551    ///
552    /// # Arguments
553    ///
554    /// * `tensor` - The inner backend tensor to convert.
555    ///
556    ///
557    /// # Returns
558    ///
559    /// The autodiff backend tensor.
560    fn q_from_inner(tensor: QuantizedTensor<Self::InnerBackend>) -> QuantizedTensor<Self>;
561
562    /// Mark the tensor as distributed across multiple devices.
563    /// The gradients will be aggregated during the backward pass.
564    ///
565    /// This function does nothing when distributed training is not available.
566    fn set_distributed_params(
567        tensor: FloatTensor<Self>,
568        _param_id: DistributedParamId,
569    ) -> FloatTensor<Self> {
570        tensor
571    }
572
573    /// Returns the distributed parameters if the tensor was marked as distributed.
574    fn distributed_params(_tensor: &FloatTensor<Self>) -> Option<DistributedParams> {
575        None
576    }
577
578    /// Returns true if the tensor was marked as distributed.
579    fn is_distributed(_tensor: &FloatTensor<Self>) -> bool {
580        false
581    }
582}
583
584/// Describes how a data type can be used on a given device.
585///
586/// A data type may be supported for different classes of operations. Not all
587/// data types that appear in hardware or kernel implementations are suitable
588/// for general-purpose tensor operations.
589#[derive(Debug, EnumSetType)]
590pub enum DTypeUsage {
591    /// The type can be stored in device memory and converted to and from
592    /// other supported data types.
593    Storage,
594    /// The type supports general-purpose arithmetic and common tensor
595    /// operations (e.g. elementwise ops, reductions, etc.).
596    Arithmetic,
597    /// The type is supported by hardware-accelerated execution paths.
598    ///
599    /// This typically indicates support for accelerator-backed compute units (e.g., tensor
600    /// cores executing MMA instructions) for high-performance operations such as matrix
601    /// multiplication and operations that lower to it.
602    ///
603    /// # Notes
604    /// - A type can be both [`Arithmetic`](DTypeUsage::Arithmetic) and
605    ///   [`Accelerated`](DTypeUsage::Accelerated) if it supports general-purpose operations
606    ///   *and* accelerated paths.
607    /// - If a type is marked as `Accelerated` but not `Arithmetic`, it is not
608    ///   suitable for general-purpose tensor operations and may only be used
609    ///   in specific accelerated operations.
610    ///
611    /// `Accelerated` is a **flag**, not a detailed descriptor. It does not enumerate which
612    /// operations are accelerated or which accelerator features are available.
613    Accelerated,
614}
615
616/// A set of [DTypeUsage] representing the total capabilities of a data type on a device.
617pub type DTypeUsageSet = EnumSet<DTypeUsage>;
618
619impl DTypeUsage {
620    /// Returns the usage set required for general-purpose tensor support.
621    pub fn general() -> DTypeUsageSet {
622        DTypeUsage::Storage | DTypeUsage::Arithmetic
623    }
624}