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}