Skip to main content

burn_tensor/tensor/api/
autodiff.rs

1use crate::{Tensor, kind::Autodiff};
2
3#[cfg(feature = "autodiff")]
4use crate::ops::{BridgeKind, BridgeTensor};
5#[cfg(feature = "autodiff")]
6use burn_backend::{AutodiffBackend, ops::FloatTensorOps};
7#[cfg(feature = "autodiff")]
8use burn_dispatch::Dispatch;
9#[cfg(feature = "autodiff")]
10use burn_dispatch::{DispatchAutodiffContext, GradientCheckpointingStrategy};
11
12#[cfg(feature = "autodiff")]
13type AutodiffGradients = <Dispatch as AutodiffBackend>::Gradients;
14
15// Aligned, type-erased storage for `AutodiffGradients`. See `crate::macros`
16// for why this indirection exists.
17#[cfg(feature = "autodiff")]
18burn_std::obfuscate!(
19    type: AutodiffGradients,
20    module: gradients_opaque,
21    derives: [Send]
22);
23
24/// Gradients container used during the backward pass.
25#[cfg(feature = "autodiff")]
26pub struct Gradients {
27    blob: gradients_opaque::Opaque,
28}
29
30#[cfg(feature = "autodiff")]
31impl Gradients {
32    /// Crate-internal constructor wrapping the dispatch-level gradients.
33    pub(crate) fn from_inner(inner: AutodiffGradients) -> Self {
34        Self {
35            blob: gradients_opaque::Opaque::new(inner),
36        }
37    }
38
39    /// Crate-internal borrow of the underlying gradients container.
40    pub(crate) fn as_inner(&self) -> &AutodiffGradients {
41        self.blob.as_ref()
42    }
43
44    /// Crate-internal mutable borrow of the underlying gradients container.
45    pub(crate) fn as_inner_mut(&mut self) -> &mut AutodiffGradients {
46        self.blob.as_mut()
47    }
48}
49
50#[cfg(feature = "autodiff")]
51impl<const D: usize> Tensor<D> {
52    /// Computes gradients by backpropagating from this tensor.
53    ///
54    /// The tensor must participate in an autodiff graph. Backward consumes the recorded steps
55    /// reachable from this tensor, even though this method borrows it. Clones share those steps;
56    /// cloning a tensor does not preserve its backward tape.
57    ///
58    /// Burn does not support retaining the graph for another backward (`retain_graph`). Combine
59    /// losses that share intermediates into one loss before calling backward, or recompute the
60    /// forward pass for each backward. Fresh forwards and existing branches can reuse parameter
61    /// leaves, including leaves used by an earlier backward, as long as they do not depend on
62    /// consumed intermediates. Gradient checkpointing does not change this contract.
63    ///
64    /// To intentionally stop gradients through earlier operations, use [`detach`](Tensor::detach).
65    /// This starts a new lineage; it does not restore the earlier graph.
66    ///
67    /// # Panics
68    ///
69    /// Panics if autodiff is disabled, the tensor doesn't participate in a recorded graph, backward
70    /// has already consumed this tensor's step, or any required intermediate step has been
71    /// consumed. Consumed ancestry is rejected before any additional steps are consumed, leaving
72    /// fresh branches available for backward. Distributed backward also panics if a distributed
73    /// parameter uses a different backend than the loss.
74    pub fn backward(&self) -> Gradients {
75        assert!(
76            self.is_tracked(),
77            "Tensor::backward requires a tracked autodiff tensor; call Tensor::autodiff().require_grad() on the source leaf before computing the output"
78        );
79        backward_impl(&self.primitive)
80    }
81
82    /// Returns this tensor's retained gradient, if present.
83    ///
84    /// The returned gradient doesn't have an autodiff association. This returns `None` when the
85    /// tensor is outside autodiff, doesn't retain gradients, or isn't present in `grads`.
86    /// Repeated calls return handles to the same gradient. If the gradient is only needed once,
87    /// prefer [`grad_remove`](Tensor::grad_remove), which can enable in-place optimizations.
88    pub fn grad(&self, grads: &Gradients) -> Option<Tensor<D>> {
89        grad_impl(&self.primitive, grads).map(Tensor::new)
90    }
91
92    /// Removes and returns this tensor's retained gradient, if present.
93    ///
94    /// The returned gradient doesn't have an autodiff association. This returns `None` when the
95    /// tensor is outside autodiff, doesn't retain gradients, or isn't present in `grads`.
96    pub fn grad_remove(&self, grads: &mut Gradients) -> Option<Tensor<D>> {
97        grad_remove_impl(&self.primitive, grads).map(Tensor::new)
98    }
99
100    /// Replaces this tensor's entry in `grads` with `grad`.
101    ///
102    /// # Panics
103    ///
104    /// Panics if this tensor isn't associated with autodiff, if `grad` has an autodiff association,
105    /// or if the tensors use incompatible backends.
106    pub fn grad_replace(&self, grads: &mut Gradients, grad: Tensor<D>) {
107        grad_replace_impl(&self.primitive, grads, grad.primitive)
108    }
109
110    /// Returns whether this tensor's node is marked for autodiff graph participation.
111    ///
112    /// A tensor can have autodiff enabled without being tracked, such as a constant that doesn't
113    /// require gradients. [`is_autodiff`](Tensor::is_autodiff) reports whether autodiff is enabled
114    /// for the tensor, while [`is_require_grad`](Tensor::is_require_grad) reports whether its
115    /// gradient is retained after backward.
116    ///
117    /// This reports graph participation and doesn't indicate whether the graph tape has already
118    /// been consumed.
119    pub fn is_tracked(&self) -> bool {
120        is_tracked_impl(&self.primitive)
121    }
122}
123
124#[cfg(feature = "autodiff")]
125fn backward_impl(p: &BridgeTensor) -> Gradients {
126    Gradients::from_inner(Dispatch::backward(p.clone().into_float()))
127}
128
129#[cfg(feature = "autodiff")]
130fn grad_impl(p: &BridgeTensor, grads: &Gradients) -> Option<BridgeTensor> {
131    // A non-float tensor — a packed base included — records no tape, so there
132    // is no gradient to look up.
133    let tensor = p.try_as_float()?;
134    if tensor.autodiff == DispatchAutodiffContext::Disabled
135        || !Dispatch::float_is_require_grad(tensor)
136    {
137        return None;
138    }
139    Dispatch::grad(tensor, grads.as_inner()).map(BridgeTensor::float)
140}
141
142#[cfg(feature = "autodiff")]
143fn grad_remove_impl(p: &BridgeTensor, grads: &mut Gradients) -> Option<BridgeTensor> {
144    let tensor = p.try_as_float()?;
145    if tensor.autodiff == DispatchAutodiffContext::Disabled
146        || !Dispatch::float_is_require_grad(tensor)
147    {
148        return None;
149    }
150    Dispatch::grad_remove(tensor, grads.as_inner_mut()).map(BridgeTensor::float)
151}
152
153#[cfg(feature = "autodiff")]
154fn grad_replace_impl(p: &BridgeTensor, grads: &mut Gradients, grad: BridgeTensor) {
155    Dispatch::grad_replace(p.as_float(), grads.as_inner_mut(), grad.into_float())
156}
157
158#[cfg(feature = "autodiff")]
159fn is_tracked_impl(p: &BridgeTensor) -> bool {
160    p.try_as_float().is_some_and(Dispatch::is_tracked)
161}
162
163impl<const D: usize, K: Autodiff> Tensor<D, K> {
164    /// Returns whether autodiff is enabled for this tensor.
165    ///
166    /// This doesn't indicate whether the tensor participates in a recorded graph or retains its
167    /// gradient. Inspect those properties with `Tensor::is_tracked()` and
168    /// [`is_require_grad`](Tensor::is_require_grad), respectively.
169    pub fn is_autodiff(&self) -> bool {
170        self.device().is_autodiff()
171    }
172
173    /// Returns this tensor without its autodiff association.
174    ///
175    /// If the tensor uses autodiff, this moves it to the inner backend and drops any graph
176    /// reference it carries. If autodiff is already disabled, the tensor is returned unchanged.
177    /// This operation is idempotent.
178    ///
179    /// This is equivalent to [`without_autodiff`](Tensor::without_autodiff). `inner` reflects the
180    /// underlying backend-decorator model, while `without_autodiff` describes the operation in
181    /// terms of the high-level tensor API.
182    #[must_use]
183    pub fn inner(self) -> Tensor<D, K> {
184        self.without_autodiff()
185    }
186
187    /// Returns this tensor without its autodiff association.
188    ///
189    /// If the tensor uses autodiff, this moves it to the inner backend and drops any graph
190    /// reference it carries. If autodiff is already disabled, the tensor is returned unchanged.
191    /// This operation is idempotent.
192    ///
193    /// Unlike [`detach`](Tensor::detach), which severs the tensor from its current graph but keeps
194    /// its autodiff association, the returned tensor pays no autodiff dispatch for subsequent
195    /// operations involving only tensors without autodiff. When combined with a tensor that still
196    /// uses autodiff, it is treated as a constant for that operation.
197    #[must_use]
198    pub fn without_autodiff(self) -> Self {
199        if self.is_autodiff() {
200            Tensor::new(K::inner(self.primitive))
201        } else {
202            self
203        }
204    }
205
206    /// Returns this tensor with autodiff enabled.
207    ///
208    /// If autodiff is disabled, this associates the tensor with the autodiff backend using the
209    /// default gradient-checkpointing strategy. If autodiff is already enabled, the tensor and its
210    /// current strategy are returned unchanged. This operation is idempotent.
211    ///
212    /// Enabling autodiff does not make a floating-point tensor require gradients. Use
213    /// [`require_grad`](Tensor::require_grad) when its gradient should be retained during the
214    /// backward pass.
215    #[must_use]
216    pub fn autodiff(self) -> Self {
217        if self.is_autodiff() {
218            self
219        } else {
220            Self::new(K::from_inner(self.primitive))
221        }
222    }
223
224    /// Returns the provided tensor with autodiff enabled.
225    ///
226    /// If the tensor does not yet use autodiff, this associates it with the autodiff backend using
227    /// the default gradient-checkpointing strategy. If autodiff is already enabled, the tensor and
228    /// its current strategy are returned unchanged. This operation is idempotent.
229    ///
230    /// This is equivalent to [`autodiff`](Tensor::autodiff). `from_inner` reflects the underlying
231    /// backend-decorator model, while `autodiff` describes the operation in terms of the high-level
232    /// tensor API.
233    ///
234    /// Enabling autodiff does not make a floating-point tensor require gradients. Use
235    /// [`require_grad`](Tensor::require_grad) when its gradient should be retained during the
236    /// backward pass.
237    #[must_use]
238    pub fn from_inner(inner: Tensor<D, K>) -> Self {
239        inner.autodiff()
240    }
241
242    /// Returns this tensor's gradient-checkpointing strategy when autodiff is enabled.
243    #[cfg(feature = "autodiff")]
244    pub fn gradient_checkpointing_strategy(&self) -> Option<GradientCheckpointingStrategy> {
245        match self.primitive.as_parts().1.autodiff {
246            DispatchAutodiffContext::Disabled => None,
247            DispatchAutodiffContext::Enabled(strategy) => Some(strategy),
248        }
249    }
250
251    /// Sets the autodiff checkpointing strategy carried by this tensor.
252    ///
253    /// The strategy is normally derived from the device the tensor was created on (see
254    /// [`Device::gradient_checkpointing`](crate::Device::gradient_checkpointing)); this
255    /// method overrides it for a single tensor. Enable autodiff first with
256    /// [`autodiff`](Tensor::autodiff) when needed; this method doesn't enable it automatically.
257    ///
258    /// # Panics
259    ///
260    /// Panics if autodiff isn't enabled. Operations combining tensors that carry different
261    /// strategies also panic; make sure all operands share the same one.
262    #[cfg(feature = "autodiff")]
263    #[must_use]
264    pub fn with_gradient_checkpointing_strategy(
265        self,
266        strategy: GradientCheckpointingStrategy,
267    ) -> Self {
268        assert!(
269            self.is_autodiff(),
270            "Tensor::with_gradient_checkpointing_strategy requires autodiff; call Tensor::autodiff first"
271        );
272        let primitive = self.primitive;
273        let (kind, mut tensor) = primitive.into_parts();
274        tensor.autodiff = DispatchAutodiffContext::Enabled(strategy);
275        Self::new(match kind {
276            BridgeKind::Bool => BridgeTensor::bool(tensor),
277            BridgeKind::Int => BridgeTensor::int(tensor),
278            BridgeKind::Float => BridgeTensor::float(tensor),
279            BridgeKind::QFloat => BridgeTensor::qfloat(tensor),
280        })
281    }
282}