3.3. Differentiation By Example
Reverse-mode automatic differentiation is usually introduced as “call backward and read the gradients.” TorchLean makes the derivative object explicit: a function, input, and output cotangent determine a vector-Jacobian product. Parameter gradients are returned in a pack whose shapes match the model's parameters.
We will begin with a two-coordinate function, then differentiate a linear model, stop a gradient
with detach, and finish with Jacobian and Hessian products.
3.3.1. Run The Complete Tour
lake exe torchlean quickstart_autograd
The run prints several derivative views. The final portion is:
jacfwdInput(square) cols = 2 col[0] = [1.000000, -0.000000] col[1] = [0.000000, -2.400000] hessianInput(mean(x^2)) cols = 2 H*e[0] = [1.000000, 0.000000] H*e[1] = [0.000000, 1.000000] vjp(square, seed=ones) = [1.000000, -2.400000] grad1(mean(x^2)) = [0.500000, -1.200000] valueAndGradScalar(mean(x^2)) value = 0.845000 grad = [0.500000, -1.200000]
We will derive each value below. The source is
AutogradBasics.lean.
3.3.2. A Scalar Function
Define:
f(x_0,x_1)=\frac{x_0^2+x_1^2}{2}.
In the autograd.func API:
import NN.API
open TorchLean
def sumsq :
autograd.func.Fn (shape![2]) Shape.scalar :=
fun x => do
let y ← nn.functional.square x
nn.functional.mean y
The type states that sumsq maps a two-vector to a scalar. The do block builds a checked tensor
program; it is not using a special untyped scalar expression language.
The mathematical gradient is:
\nabla f(x_0,x_1)=(x_0,x_1).
At x=(0.5,-1.2):
f(x)=\frac{0.25+1.44}{2}=0.845,
\qquad
\nabla f(x)=(0.5,-1.2).
The executable call is:
def example : IO Unit := do
let x : Tensor.T Float (shape![2]) :=
tensor! [0.5, -1.2]
let (value, grad) ←
autograd.func.valueAndGradScalar
(alpha := Float) sumsq x
IO.println s!"value={value}, grad={Tensor.pretty grad}"
The observed values match the hand calculation. That is a useful executable check. A theorem that the autograd transform is correct for every input requires the semantic proof layer described later; one agreeing sample is not that theorem.
At a nonsmooth point, “the derivative” also needs a convention. ReLU at zero, max ties, and similar operations have runtime VJP rules even where a classical Fréchet derivative does not exist. A correctness theorem must either exclude those points or state the selected generalized derivative; shape-correct gradient output alone does not settle that question.
3.3.3. Why A VJP Is The Primitive Reverse Operation
Let:
f:\mathbb R^n\to\mathbb R^m
with Jacobian:
J_f(x)\in\mathbb R^{m\times n}.
Reverse mode accepts an output cotangent \bar y\in\mathbb R^m and returns:
\bar x=J_f(x)^\mathsf{T}\bar y.
TorchLean writes:
let dx ←
autograd.func.vjp
(alpha := Float)
f x seedOut
The types enforce:
x : Tensor α inputShape seedOut : Tensor α outputShape dx : Tensor α inputShape
For a scalar output, choosing seed one gives the ordinary gradient. For a vector output, the seed selects a linear combination of output rows without materializing the whole Jacobian.
3.3.4. Elementwise Square
Let:
g(x_0,x_1)=(x_0^2,x_1^2).
Its Jacobian is diagonal:
J_g(x)=
\begin{bmatrix}
2x_0&0\\
0&2x_1
\end{bmatrix}.
The TorchLean definition and VJP are:
def square :
autograd.func.Fn (shape![2]) (shape![2]) :=
fun x => nn.functional.square x
def vjpExample : IO Unit := do
let x : Tensor.T Float (shape![2]) :=
tensor! [0.5, -1.2]
let seed : Tensor.T Float (shape![2]) :=
tensor! [1.0, 1.0]
let dx ←
autograd.func.vjp
(alpha := Float) square x seed
IO.println s!"dx={Tensor.pretty dx}"
The result is:
J_g(x)^\mathsf T(1,1)=(1,-2.4).
Change the seed to (1,10). The expected result becomes (1,-24). This is a clean way to see that
a VJP is not “the gradient” of a vector-valued function until an output cotangent is chosen.
3.3.5. Full Jacobians
TorchLean provides:
-
jacfwd, one output-shaped column per input coordinate; -
jacrev, one input-shaped row per output coordinate.
For square at (0.5,-1.2), the quickstart prints:
col[0] = [1.000000, -0.000000] col[1] = [0.000000, -2.400000]
These are the columns of the diagonal Jacobian. Reverse mode prints the same entries organized by rows.
The result is an array of shaped tensors rather than one anonymous flat matrix. The order follows the row-major flattened coordinate order of the relevant shape. This preserves enough structure to relate a derivative coordinate back to a tensor axis.
Forward mode is attractive when the input dimension is small; reverse mode is attractive when the output dimension is small. Building the full Jacobian requires repeating one of these products for each basis direction.
3.3.6. Parameter VJPs
Consider a linear map with three outputs and two inputs:
y=Wx+b,
where:
W\in\mathbb R^{3\times2},\qquad b\in\mathbb R^3.
For an output cotangent \bar y, the derivatives are:
\bar W=\bar y\,x^\mathsf T,
\qquad
\bar b=\bar y,
\qquad
\bar x=W^\mathsf T\bar y.
With x=(0.5,-1.2) and \bar y=(1,1,1), each row of dW is x and each bias derivative is
one. The quickstart prints:
vjpOutParams (seed=ones) dW = [[0.500000, -1.200000], [0.500000, -1.200000], [0.500000, -1.200000]] vjpOutParams (seed=ones) db = [1.000000, 1.000000, 1.000000]
The model API returns a dependent parameter pack:
let dparams ←
autograd.model.vjpParams
(alpha := Float)
model params x seedOut
dparams has exactly the shapes and order of params. There is no mutable .grad field that might
contain stale values from a previous reverse pass.
3.3.7. A Loss Turns Output Derivatives Into Parameter Gradients
For a model output and target of the same shape:
def loss :
autograd.model.OutputLoss outputShape outputShape :=
autograd.model.OutputLoss.mse
let (lossValue, dparams) ←
autograd.model.valueAndGradParamsScalar
(alpha := Float)
model loss params x target
Conceptually:
\nabla_\theta L
=
J_{F_\theta}^{\theta}(x)^\mathsf T
\nabla_{\widehat y}L(\widehat y,y).
The quickstart's concrete result is:
loss(mse) = 0.165133 gradParams (mse) gW = [[-0.156667, 0.376000], [-0.160000, 0.384000], [0.070000, -0.168000]] gradParams (mse) gb = [-0.313333, -0.320000, 0.140000]
An optimizer consumes this pack together with parameters and its own state.
3.3.8. detach Preserves Values And Cuts Derivatives
autograd.model.OutputLoss.detach leaves the forward value unchanged while replacing the backward
map by zero.
The quickstart checks both effects:
loss(mse) = 0.165133 loss(mse ∘ detach) = 0.165133 gradParams (mse ∘ detach) gW = [[0.000000, 0.000000], [0.000000, 0.000000], [0.000000, 0.000000]] gradParams (mse ∘ detach) gb = [0.000000, 0.000000, 0.000000]
This is a good diagnostic experiment: if the loss value changes, the forward implementation of detach is wrong; if a gradient remains nonzero, the backward edge was not cut.
Detach is useful for target networks, stop-gradient estimators, contrastive objectives, and statistics that should not receive gradients. It also changes the mathematical objective seen by the optimizer, so it should never be inserted merely to make an error disappear.
3.3.9. JVPs And Hessian-Vector Products
Forward mode computes:
J_f(x)v.
TorchLean exposes JVPs for functions and model parameters. Combining forward and reverse differentiation gives a Hessian-vector product:
H_f(x)v
without constructing the full Hessian.
For:
f(x)=\frac{x_0^2+x_1^2}{2},
the Hessian is the identity matrix:
H_f(x)=I_2.
The quickstart therefore prints:
H*e[0] = [1.000000, 0.000000] H*e[1] = [0.000000, 1.000000]
Change the function from mean(square x) to sum(square x). The Hessian should become 2I.
This variation checks both the reduction convention and second derivative.
3.3.10. Local Rules And The Global Reverse Pass
At runtime, each primitive operation contributes:
-
a forward value;
-
parent references;
-
a local VJP rule.
Reverse traversal starts from an output seed, applies each local rule in reverse topological order, and adds cotangents when several paths meet. For:
z=x^2+x^2,
the two paths each contribute 2x, so accumulation returns 4x. A tape that overwrote one parent
contribution instead of adding would be wrong even if the local square rule were correct.
This local-to-global split also structures the proofs. Primitive derivative theorems establish the local rules; tape soundness establishes that reverse accumulation composes them according to the graph.
3.3.11. Runtime Derivatives And Mathematical Derivatives
There are three claims to distinguish:
-
the calculus derivative of the ideal real operation;
-
the derivative program generated by TorchLean's graph/autograd transform;
-
the numeric values produced by CPU, CUDA, or an external VJP provider.
The first two can be related by Lean theorems for supported primitives and well-formed graphs. The third additionally needs a runtime refinement or an explicit backend boundary. A CUDA kernel may use floating-point rounding and a different reduction tree even when it implements the same formal VJP equation.
The autograd.func helpers execute the compiled derivative machinery directly. Trainer
eager/compiled selection and native capsule selection are separate runtime choices. Primals,
cotangents, parameters, and returned derivatives follow the scalar contract described in
Tensors And Shapes.
3.3.12. Inspect The Tape In VS Code
Open the widget deep dive and place the cursor on:
#tape_view ... #tape_grads_view ... #tape_trace_view ...
The views show nodes, parent edges, accumulated gradients, and reverse traversal. Use them to answer:
-
which operations are on the path to the scalar output?
-
where do cotangents merge?
-
which leaf corresponds to each parameter?
-
did a detached branch disappear from reverse traversal?
Widgets visualize an executable artifact. They do not add a theorem to it.
3.3.13. API Summary
For one-input tensor functions:
autograd.func.grad autograd.func.valueAndGradScalar autograd.func.vjp autograd.func.jacfwd autograd.func.jacrev autograd.func.hessian
For checked models:
autograd.model.gradParams autograd.model.gradInputs autograd.model.valueAndGrads autograd.model.vjpParams autograd.model.vjpInput autograd.model.jacrevParams autograd.model.jvpParams autograd.model.hvpParams
The next page applies the same machinery to a scientific equation where a wrong derivative sign is easy to see. The runtime chapter after it then explains which graph or tape each call constructs and why the canonical verification IR is a different artifact.
References: