Adjoint methods for ODEs and neural ODEs
Before this lesson: lesson 01 (vector–Jacobian products, the tape, fan-out) and lesson 02 (Euler and RK4 steps, stability, stiffness). Related lessons in other tracks: calculus 09 (the adjoint of an equation ) and deep learning 05 (residual networks). Time: about 100 minutes, plus the notebook.
Why this matters #
Fitting a model that is a differential equation needs for one scalar loss and many parameters . This happens everywhere in ML for science:
- Neural ODEs and continuous normalizing flows (Chen et al. 2018) are trained with the adjoint method or with backpropagation through the solver.
- 4D-Var data assimilation (lesson 06) finds the initial state of a weather model that best fits the observations of a time window. The gradient over millions of state variables comes from the adjoint model, run backward over the window.
- Full-waveform inversion in seismology (lesson 04) gets the gradient with respect to millions of velocity values from one forward and one adjoint wave simulation.
- Differentiable and hybrid climate models (lesson 16) are trained through multi-step rollouts of a solver.
The adjoint method costs about as much as one extra solve, whatever the number of parameters. The hard questions are practical: what to store, what to recompute, whether to run the state backward, and which gradient you get.
1. Predict first #
2. The picture #
A model that is an ODE #
A residual network (ResNet) updates a hidden state by adding a correction: . Write the correction as with a step size and one shared set of weights. Then
is exactly forward Euler for the ODE (lesson 02). The layer index plays the role of time . A neural ODE (Chen et al. 2018) takes the limit: the model is the ODE
where is a small neural network, and any ODE solver computes .
What the continuous view gives.
- Continuous time. The state exists at every , so data observed at irregular times fit in directly (latent ODEs for irregular time series, Rubanova et al. 2019).
- Choice of solver. An adaptive solver uses more steps where the dynamics are fast and fewer where they are slow, at a set tolerance.
- Memory. The backsolve adjoint (section 3) computes gradients with memory that does not grow with the number of steps.
- Invertibility. The flow map is invertible (run the ODE backward), and the log-density changes by . This is the basis of continuous normalizing flows, and of the probability-flow ODE of diffusion models (lesson 13).
What it costs.
- Every forward pass is a full ODE solve. The number of right-hand-side evaluations depends on the solver and the tolerance, and it often grows during training as becomes more complex.
- If learns fast-decaying modes, the ODE becomes stiff (lesson 02): explicit solvers need tiny steps.
- Trajectories of an ODE cannot cross (section 3), so some maps cannot be represented in the original dimension. Augmented neural ODEs (Dupont et al. 2019) add extra zero dimensions to fix this.
Training needs . The rest of the lesson answers: how do we differentiate through an ODE solve? Everything works for any parameter vector ; for a neural ODE, .
A residual block computes . Which method for is this?
Forward Euler evaluates the slope at the start of the step: . That is the residual update, with the layer index as time. Backward Euler would need , so each layer would have to solve an equation.
The adjoint as sensitivity, travelling backward #
Simplest case. Take with a number , , and a loss . A small push given to the state at time grows to at time . So the sensitivity of to the state at time is
This number is the adjoint (or adjoint state, or costate). Read the formula from right to left: it starts at the final time, , and it is known at earlier times only after we know what happens later. So it is computed backward in time. Differentiate in : . For a system the same argument gives : the propagator appears transposed. This is the vector–Jacobian product of the flow map (lesson 01).
Parameters act as a forcing. A small change of the parameters changes the slope by at every time, where . The push given during is worth in the loss. Adding all the pushes:
The discrete picture. Forward Euler is a ResNet whose layers share the weights . Backprop through it: is the cotangent of layer , and the cotangent of the shared is a sum over all layers (fan-out, lesson 01). As , the backward recursion becomes the adjoint ODE and the sum becomes the integral. The adjoint method is reverse mode in time.
Drag the time slider from down to 0, or press Play. The blue curve is the forward solution , computed first and stored. The violet curve is the adjoint ; it starts at and is filled in backward. The dashed violet curve is the running integral ; at it is , and the readout compares it with finite differences. For the logistic model, notice that is smallest in the middle, where the growth is fastest: there a small push only moves the S-curve a little earlier in time, which changes very little.
For the loss , where does the adjoint get its condition, and in which direction is it solved?
. At the final time the loss depends on directly, so . Earlier values depend on what happens later, so the equation is solved from back to . is the condition when the loss is only a running cost .
Three ways to get the gradient, and their cost #
There are three standard ways to get through a solver. The next section derives them; the figure compares their cost first.
Move the sliders for the number of parameters and the number of time steps . The upper chart is the work in units of one forward solve; the lower chart is the memory in units of one state vector (the numbers of the gradient itself are not counted). Both axes are logarithmic. Blue bars give the exact gradient of the computed loss, magenta bars an approximation. Notice that only finite differences and forward sensitivities grow with , and only the stored adjoint grows with . Checkpointing keeps about states for one extra forward pass.
3. Definitions and main results #
Setting #
We solve , for , with a state and parameters . The loss is . Along the trajectory, the Jacobians () and () are evaluated at . A time stepper uses a fixed step , steps and .
Three ways to get #
- Central finite differences. Perturb each parameter by and solve again: forward solves. The error is plus rounding noise, plus solver noise (with adaptive steps the step sequence itself changes with ).
- Forward sensitivities (forward mode). The matrix solves the forward sensitivity equation (tangent equation)
and . This is extra ODEs of size (one per column of ), solved together with . Nothing is stored over time. Why this equation: differentiate with respect to and use the chain rule: .
- Adjoint (reverse mode). One backward solve of size plus an integral, for any . It needs during the backward solve.
| method | work, in forward solves | memory over time | exact for the code? |
|---|---|---|---|
| central differences | none | no: + rounding | |
| forward sensitivities | about | none | yes, if derived from the code's steps |
| adjoint, all states stored | about 2–3, independent of | states | yes (discrete adjoint) |
| adjoint with checkpoints | about 3–4 | about states (uniform) | yes (discrete adjoint) |
| backsolve adjoint | about 3–4 | no: , can be unstable |
Rule of thumb: many parameters and one loss → adjoint; few parameters → forward sensitivities.
A model has parameters and one scalar loss on the final state. About how many solves of the ODE's size does the adjoint method need for the full gradient?
The adjoint solves one backward equation of the state's size, and the parameter gradient is an integral along it. The cost does not depend on the number of parameters. is the cost of central differences; the forward sensitivity equation needs about tangent solves.
Theorem 1 (continuous adjoint) #
Let and be continuously differentiable, let the solution exist on , and let solve the adjoint ODE backward in time:
Then
Why this is true (intuition). is "the change of the loss that a perturbation at time will cause". If nothing pushes the trajectory between and , this number must be the same at every time. The minus sign and the transpose in the adjoint ODE are exactly what keeps it constant.
Proof.
- Perturb by and by . To first order the trajectory changes by , which solves the tangent equation with .
- Differentiate the scalar in time, with the product rule: . Insert the adjoint ODE () and the tangent equation: . The two terms cancel. This is why the adjoint ODE has .
- Integrate from to : .
- By the chain rule and the final condition, .
- So for every perturbation. The coefficients of and are the two gradients.
The same result with a Lagrange multiplier
- Add zero to the loss: for any .
- Perturb: .
- Integrate by parts: it equals .
- Collect terms: .
- Choose so that the terms with the unknown and vanish: and . What remains is Theorem 1.
Extensions (each follows from the same proof).
- A running cost adds a source: , and gets the extra term .
- Losses at several times , as in fitting observed data: in the backward sweep, add to each time you pass . Between the observations, follows the adjoint ODE.
- An initial state that depends on adds to .
- The augmented backward system. Let with ; then
. Solving together from back to
, starting from , re-creates backward instead of storing
it. This is the backsolve adjoint of the neural ODE paper (Chen et al. 2018;
odeint_adjointin torchdiffeq).
Stability of the adjoint. In reversed time the adjoint reads , with the same eigenvalues as . So, for a constant , if the forward problem damps a mode, the adjoint run backward damps it too: the adjoint equation is stable backward. The re-created state is different. In reversed time it solves , so every mode that decays forward grows backward.
Theorem 2 (discrete adjoint of a one-step method) #
A one-step method computes for , and the code's loss is . Let and be the Jacobians of one step, and
Then
Proof (the same argument as Theorem 1, in discrete time).
- Tangent recursion: .
- Multiply by and use : .
- Sum over . The left side telescopes: .
- ; read off the two gradients.
This is the tape sweep of lesson 01 with one node per time step, and it is what automatic differentiation computes through the solver. It is the exact gradient of the loss that the code computes, up to rounding. It stores the states (or recomputes them).
Forward Euler. , so and :
Compare explicit Euler applied backward to the adjoint ODE: . The Jacobian sits at the other end of the step. Both are first-order approximations of the same ODE, but they give different numbers.
You write forward Euler in PyTorch and call loss.backward(). What is the gradient you get?
Automatic differentiation applies the chain rule to the operations that the code runs: this is the discrete adjoint of Theorem 2. It is exact for . It differs from the gradient of the true ODE loss by for Euler, and from the continuous adjoint solved with Euler by a different term.
Discrete adjoint of an explicit Runge–Kutta step
An explicit RK method with stages, coefficients () and weights takes one step by
Given , go through the stages in reverse order, :
Then , and is this step's term of .
Proof (reverse mode through the graph of one step).
- uses with weight and each with weight . This sends to and to .
- is also used by every later stage point (), with weight : fan-out adds to . Going through the stages in reverse order makes every final before it is needed.
- : its VJP sends to and to .
- Every uses with weight , so also collects .
The Jacobians are taken at the stage points (only ), not at . Cost per step: products with and with .
What this scheme is. Write (when ). The recursion becomes and . This is a Runge–Kutta step for the adjoint ODE, backward from to , with weights and coefficients (Hager 2000), but with the Jacobians frozen at the forward stage points . For classic RK4 the new coefficients, read in reverse stage order, are again the RK4 coefficients (, , ). For many other methods they are not. When is smooth, for a method of order , and differentiating this expansion shows that converges to the exact at the same order .
When do the continuous and the discrete adjoints differ? #
The two choices have names. Discretize-then-optimize is the discrete adjoint: differentiate the code. Optimize-then-discretize is the continuous adjoint: derive the adjoint ODE, then solve it numerically.
- They agree in one clean case: a linear autonomous system , an explicit RK method, and the gradient with respect to . One step is with a polynomial (the stability polynomial of lesson 02), so the discrete adjoint is . The same method on backward with step gives , and . The parameter gradient still differs, because depends on and the two methods evaluate it at different points.
- Finite step. In general the two differ by . The discrete adjoint is exact for . The continuous adjoint approximates , but it is not the gradient of any loss that the code computes. Near an optimum this mismatch can be as large as the gradient itself, and line searches can fail.
- Adaptive steps. The backward solve chooses its own steps, so the two time grids differ.
- Non-smooth (events,
ifbranches, clipping). The discrete adjoint differentiates exactly the code that runs. - Backward re-creation of (backsolve) can blow up; see the second figure below.
- Chaos is not a difference between the two. For a chaotic system both gradients grow exponentially backward in time (like for Lorenz-63, whose largest Lyapunov exponent is about 0.906; lesson 05). Gradients over long windows are huge and not useful for either method.
Switch between Euler and RK4, and move the step size . The problem is , , , , , with exact gradient . The orange line is the error of the discrete adjoint, the violet line the error of the continuous adjoint (computed by backsolve with the same method), and the magenta line their difference. All three have slope 1 for Euler and slope 4 for RK4: both gradients converge, but the gap between them never reaches zero.
Why backsolve fails for damped modes. Re-creating backward means solving in reversed time. A mode that decays like forward grows like backward. Every rounding error in that mode at time (size about relative to the state) is multiplied by when it reaches . With diffusion, damping, seismic attenuation or stiff chemistry, the re-created , and every Jacobian evaluated on it, is wrong. The adjoint itself stays stable.
Move the decay rate of the fast mode. The system is with having eigenvalues and (in rotated axes). RK4 with solves it forward to , then backward from the computed . The magenta curve is the error of the re-created state ; the dashed grey line is . The violet curve is the size of the adjoint , solved backward on the stored states: it stays bounded. Notice that for of about 35 and more the re-created is useless, while the forward solution was accurate.
Checkpointing: memory against recomputation #
The discrete adjoint needs the states in reverse order. For steps:
| strategy | states in memory | extra forward steps |
|---|---|---|
| store every state | 0 | |
| store only ; recompute each from it | 1 | about |
| uniform: a checkpoint every steps | about , smallest () at | fewer than (at most one more forward pass) |
| recursive (binomial, Revolve) | ||
| backsolve (continuous adjoint) | one backward re-integration of ; not exact |
Uniform checkpointing. In the forward pass, store only . In the backward pass, take the segments from last to first: recompute the missing states of the segment from its checkpoint, run the adjoint through them, and free them. The result equals full storage to rounding, because the recomputed states are the same numbers.
Why . During the backward pass of one segment we hold at most checkpoints plus the states of the segment. The function is smallest where its derivative is zero, at , and there it equals .
Choose the number of steps and the number of checkpoints , then press Backward step. Blue boxes are checkpoints stored in the forward pass; orange boxes are states recomputed for the current segment; the violet outline is the step whose adjoint is being computed. The chart shows the peak memory and the number of recomputed steps for every . Notice the minimum of the memory curve near .
Binomial checkpointing and reversible solvers
Binomial checkpointing (Griewank 1992; the Revolve algorithm of Griewank and Walther 2000)
places checkpoints recursively. With checkpoints and at most recomputations of any step, it can
reverse steps, and this number is the largest possible. So for a fixed memory the
number of reversible steps grows very fast with . Diffrax's default RecursiveCheckpointAdjoint is
a discrete adjoint with this kind of checkpointing.
Reversible solvers are time steppers whose step can be inverted exactly in floating point (for example the reversible Heun method used by Kidger et al. 2021 for neural SDEs). They give the memory of backsolve without its instability, because the backward pass recovers exactly the forward states.
Activation checkpointing in deep learning (torch.utils.checkpoint, jax.checkpoint) is the
same idea for the layers of a network.
Neural ODEs: trajectories cannot cross #
Let be Lipschitz in . Then the solution through any starting point is unique (Picard–Lindelöf). Two consequences:
- Trajectories never cross. If two solutions met at some time, the unique solution through that meeting point would have to be both of them.
- In one dimension the flow map is increasing. If , then for all : the two solutions are continuous, start in this order, and cannot pass through each other. So the map is a strictly increasing function. In general dimension the flow map is a homeomorphism (continuous, invertible, with a continuous inverse).
This is why a 1D neural ODE cannot learn , and why augmented neural ODEs add dimensions. A ResNet with finite steps can cross, because each Euler step is a jump, not a continuous path.
Answers to section 1. (1) Central differences: solves. Forward sensitivities: tangent ODEs of size , so about solves of work and numbers in . Adjoint: one forward and one backward solve (about 2–3 solves of work) for any , plus memory for the trajectory or for checkpoints. (2) No. Automatic differentiation gives the discrete adjoint, the exact gradient of the computed loss. The RK4 adjoint ODE gives another approximation of the exact gradient. For RK4 the two schemes have the same coefficients, but they evaluate and at different states. They agree to rounding only in special cases, such as for a linear autonomous system. (3) No. The flow map of a 1D ODE is strictly increasing, and is decreasing.
4. Worked example #
Take with , , , and the loss . Here , so and . Use forward Euler with , so and each step multiplies the state by .
Step 1: forward pass. , , , . The computed loss is .
Step 2: discrete adjoint (Theorem 2 with and ).
- Start: .
- Backward: , , .
- Parameter gradient: . The shared collects one term per step (fan-out); here each term is .
Step 3: check by hand. , so . Then ✓, and ✓. Exact for the code.
Step 4: forward sensitivity gives the same number. The discrete tangent recursion is with : , , . Then ✓. Forward and reverse mode of the same code give the same gradient.
Step 5: backsolve (the augmented system integrated backward with Euler, step , using the values at the right end of each step).
- : , so . The re-created , not .
- : : the same values as the discrete adjoint (a linear autonomous system), so again.
- : , so . This gives .
Step 6: exact gradient of the continuous problem. , , and ; .
| discrete adjoint (exact for the code) | ||
| backsolve with Euler | ||
| exact, continuous problem |
The table shows three things. The discrete adjoint is the gradient of what the code computes. The backsolve value is a third number, partly because the re-created is far from the true . And with neither is close to the exact gradient; both converge as (the third figure).
Same problem (, , , , Euler with ), but with only steps. What is ?
, so . Each backward step multiplies by : , . Check: , so . is , the value at the final time, not at .
5. In code #
NumPy checks the worked example, the convergence of both gradients, and the backsolve failure. The notebook has these cells and the exercises.
import numpy as np
# Worked example: u' = -p u, l = u(T)^2 / 2, forward Euler with N steps
def euler_loss(p, u0, T=1.5, N=3):
h, u = T / N, u0
for _ in range(N):
u = u + h * (-p * u)
return 0.5 * u**2
def discrete_adjoint(p, u0, T=1.5, N=3):
h = T / N
u = [u0]
for _ in range(N): # forward pass: store every state
u.append(u[-1] + h * (-p * u[-1]))
lam, dp = u[N], 0.0 # lambda_N = dl/du_N = u_N
for n in range(N - 1, -1, -1): # backward pass
dp += (-h * u[n]) * lam # Phi_p(u_n)^T lambda_{n+1}
lam = (1 - h * p) * lam # Phi_u^T lambda_{n+1}
return dp, lam # dL_h/dp, dL_h/du0
print(euler_loss(1.0, 1.0)) # 0.0078125
print(discrete_adjoint(1.0, 1.0)) # (-0.046875, 0.015625)
eps = 1e-6
fd = (euler_loss(1 + eps, 1.0) - euler_loss(1 - eps, 1.0)) / (2 * eps)
print(f"{fd:.9f}") # -0.046875000
# Both gradients converge to the exact one at order 1, but they stay different numbers
T, p = 1.5, 1.0
exact = -T * np.exp(-2 * p * T)
for N in [3, 12, 48, 192]:
h = T / N
uN = (1 - h * p) ** N
disc = -T * (1 - h * p) ** (2 * N - 1) # discrete adjoint
back = -h * uN**2 * sum((1 - (h * p) ** 2) ** j for j in range(N)) # Euler backsolve
print(N, f"{abs(disc - exact):.1e} {abs(back - exact):.1e} {abs(disc - back):.1e}")
# 3 2.8e-02 5.7e-02 2.9e-02
# 12 5.1e-03 1.9e-02 1.4e-02
# 48 1.2e-03 5.1e-03 3.9e-03
# 192 2.9e-04 1.3e-03 1.0e-03
# Backsolve: re-integrating u backward is unstable for a fast-decaying mode
def rk4(f, y, h):
k1 = f(y); k2 = f(y + h / 2 * k1); k3 = f(y + h / 2 * k2); k4 = f(y + h * k3)
return y + h / 6 * (k1 + 2 * k2 + 2 * k3 + k4)
th = 0.3
Q = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
A = Q @ np.diag([-1.0, -40.0]) @ Q.T # a slow mode and a fast-decaying mode
u = np.array([1.0, 1.0])
for _ in range(1000):
u = rk4(lambda y: A @ y, u, 1e-3) # forward to T = 1: accurate
for _ in range(1000):
u = rk4(lambda y: A @ y, u, -1e-3) # backward from T to 0
print(u) # about [ 3.2 -6.2], not [1. 1.]
The two closed forms in the middle part repeat section 4 with steps: the discrete adjoint gives , and the Euler backsolve gives , because . In the last part the forward solve is accurate. The backward solve starts from a state whose fast component is far below rounding level, and the backward flow multiplies its errors by up to . The exact digits of the wrong answer depend on rounding, so they can differ on your machine.
6. Where it shows up #
- Neural ODEs and differentiable solvers. torchdiffeq's
odeint_adjointis the backsolve adjoint. Diffrax's default,RecursiveCheckpointAdjoint, is the discrete adjoint with checkpointing, and its documentation marksBacksolveAdjointas not recommended. Kidger (2022, §5.1) recommends discretize-then-optimize first. Continuous normalizing flows (FFJORD, Grathwohl et al. 2019) and latent ODEs for irregularly sampled time series (Rubanova et al. 2019) are trained this way. - Weather and ocean. 4D-Var (lesson 06) minimizes a cost over the initial state of an assimilation window. At ECMWF the gradient of this cost comes from the adjoint of a linearized, lower-resolution version of the forecast model, run backward over the window. Ocean state estimation (ECCO) uses the adjoint of the MITgcm ocean model, generated from the model code by algorithmic differentiation: a discrete adjoint of the code.
- Seismology. Adjoint tomography and full-waveform inversion (lesson 04) get the gradient of a waveform misfit from one forward and one adjoint wave simulation per earthquake, for millions of model parameters. The adjoint run needs the forward wavefield in reverse time: store it, checkpoint it, or re-create it backward. Without attenuation the wave equation is time-reversible, so the forward field can be re-created backward; with attenuation this re-creation is unstable (Komatitsch et al. 2016). This is the backsolve problem of section 3.
- Learned and hybrid weather models. Training on multi-step rollouts, as for NeuralGCM (Kochkov et al. 2024) or the autoregressive fine-tuning of an emulator, is the discrete adjoint through the rollout. Activation memory grows with the number of steps, so each step is checkpointed. Chaos limits the useful rollout length.
7. Common mistakes #
- instead of , a wrong sign, or starting at . The adjoint has a final condition, , and runs backward with .
- Forgetting that is shared by all steps. Its cotangent is a sum (or an integral) over all steps. Also forgetting the term when the initial state depends on .
- Jacobians at the wrong points in the discrete adjoint. They belong at the stage points of the forward step. Only ; the other stage points are neither nor .
- Checking a continuous adjoint against finite differences of the code and calling the gap a bug. Check the discrete adjoint against finite differences (to about relative) and against automatic differentiation (to about ). Check the continuous adjoint by its convergence order.
- Backsolve on a dissipative or stiff problem. The re-created state is wrong, so the gradient is wrong, even though the adjoint equation is stable.
- Finite-difference checks through an adaptive solver. The step sequence changes with , which adds noise. Use fixed steps for gradient checks.
- Long chaotic windows. The exact gradient is huge and of little use for optimization; no adjoint method fixes that.
8. Check yourself #
- For , state the adjoint ODE, its condition, and the formulas for and
. Which step of the proof forces the minus sign and the transpose?
Answer
with ; and . In , the terms cancel only if , that is, . - , , , , one Euler step with ().
Compute , the discrete-adjoint , the Euler backsolve value, and the exact .
Answer
and . Discrete: , (check: , derivative ). Backsolve: . Exact: . - Write the discrete adjoint of forward Euler. Where is evaluated, and how does that differ
from explicit Euler applied backward to the adjoint ODE?
Answer
and . is at the start point of the forward step. Explicit Euler backward on uses , the start of the backward step. Same order, different numbers. - A rollout has steps. With uniform checkpointing, what number of checkpoints
minimizes memory, how many states are in memory, and what is the extra compute?
Answer
Memory , smallest at : about states instead of . Each missing state is recomputed once, so the extra work is less than one forward pass ( steps). - Why can backsolve fail for a discretized heat equation, although the adjoint equation itself is
stable backward in time?
Answer
Re-creating backward solves the backward heat equation: a Fourier mode that decays like forward grows like backward, so rounding and truncation errors in high wavenumbers explode. The adjoint with a symmetric negative definite decays when run backward, so it is stable. Only the re-creation of fails. - A model has state variables and 3 parameters, with a loss on the final state.
Adjoint or forward sensitivities?
Answer
Forward sensitivities: 3 tangent solves run together with the forward solve and store no trajectory. The adjoint needs only one backward solve, but it must store or recompute states of size . With 3 parameters, forward mode is simpler and needs less memory.
State the adjoint ODE and its condition for the loss .
with , solved backward from to .
Give and from the adjoint .
and .
Write the forward sensitivity equation for .
with ; then .
Why is a residual network a discretized ODE?
is forward Euler for , with the layer index as time.
Which gradient does automatic differentiation through a solver compute?
The discrete adjoint: the exact gradient of the computed loss , up to rounding. The continuous adjoint differs from it by .
Why can the backsolve adjoint fail on a damped or stiff problem?
It re-creates by integrating backward, and a mode that decays like forward grows like backward, so rounding errors explode.
How many checkpoints minimize memory for steps, and what does it cost?
gives about states in memory, for less than one extra forward pass. The gradient is unchanged.
Cost of the gradient for parameters: central differences versus adjoint?
Central differences need solves; the adjoint needs about 2–3 solves (one forward, one backward).
Why can a one-dimensional neural ODE not learn the map ?
ODE trajectories cannot cross, so in 1D the flow map is strictly increasing; is decreasing.
9. Interview angle #
- "How does a neural ODE compute its gradients, and what can go wrong?" — The original paper uses the backsolve adjoint with memory. The gradient is approximate, and the backward re-creation of the state can be unstable. Backpropagation through the solver with checkpointing is usually preferred.
- "Discretize-then-optimize or optimize-then-discretize?" — The first is the exact gradient of the computed loss; the second is an approximation of the true gradient. They agree only in special cases.
- "Backprop through a 1000-step simulation runs out of memory. What are your options?" — Checkpointing ( uniform or binomial), reversible solvers, forward sensitivities if there are few parameters, shorter rollouts, lower-precision stored states.
- "Derive the adjoint ODE." — The five steps of Theorem 1: tangent equation, product rule, the terms cancel, integrate, read off the gradients.
- "What is an adjoint model in data assimilation?" — The transpose of the tangent-linear model, run backward over the window. It gives the gradient of the 4D-Var cost with respect to the whole initial state for a small multiple of the cost of one forecast.
10. Practice #
The notebook for this lesson has five exercises, each with a check cell and a folded solution:
- Forward sensitivity for : solve together with and compare with the exact derivative.
- The discrete adjoint of forward Euler by hand, checked against central finite differences.
- Lorenz-63 with RK4: the discrete adjoint through the RK4 stages, the continuous (backsolve) adjoint, and the convergence order of the continuous one.
- Uniform checkpointing: the same gradient with about stored states.
- A tiny neural ODE , fitted to observations by gradient descent with the adjoint gradient.
11. Watch and read #
- S. G. Johnson, Notes on adjoint methods for 18.335: §2–3 (adjoints of linear and nonlinear equations) and §6 (initial-value problems).
- Bright, Edelman and Johnson, Matrix Calculus (for Machine Learning and Beyond), chapter 9: forward and reverse sensitivity analysis, discretize-then-differentiate.
- P. Kidger, On Neural Differential Equations, §5.1: discretize-then-optimize, optimize-then-discretize, reversible solvers.
- Chen, Rubanova, Bettencourt and Duvenaud, Neural Ordinary Differential Equations (2018).
- Dupont, Doucet and Teh, Augmented Neural ODEs (2019): why trajectories cannot cross.
- Blondel and Roulet, The Elements of Differentiable Programming: §12.6 (continuous adjoint against reverse mode on the discretization) and §8.5 (checkpointing).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.