Their difference estimates the local error; the step is judged against \(\mathrm{sc}_i = \texttt{atol} + \texttt{rtol}\cdot\max(|x_{n,i}|,|x_{n+1,i}|)\).
Two things that are easy to get wrong, both checked: propagate the higher-order value, and update \(h\) after a rejection too.
Checkpoint — your own spiral
The U0.T5 icon, now self-made. \(h = 2\pi/40\).
The assert is not on the picture: Euler’s energy grows by exactly\(1 + h^2\) per step, and the notebook checks that ratio.
Checkpoint — where adaptivity pays
Not “adaptive is better”. Two problems, same achieved error, against a fixed-step Heun — the oscillator, and the oscillator with a burst: \(\omega(t) = 1 + 40\,e^{-((t-\pi)/0.1)^2}\).
Problem
rtol
Adaptive NFE
Fixed NFE, matched error
Ratio
harmonic
\(10^{-4}\)
968
1054
\(1.09\times\)
harmonic
\(10^{-6}\)
9712
9854
\(1.01\times\)
burst
\(10^{-4}\)
2834
24 062
\(8.5\times\)
burst
\(10^{-6}\)
28 142
224 118
\(8.0\times\)
Adaptivity pays where the difficulty is nonuniform. A measurement, not a theorem — and the notebook asserts both halves.
The controller finding the burst
The scope is deliberate
This module integrates forward. No adjoint, no differentiation through the solve.
Sampling is inference: no gradient is needed to draw a sample.
Nothing blocks autograd — and what it would cost you in memory is precisely the problem U2.T1 exists to solve.
03The convergence study
Collecting a prediction from U0.T5
Exercise 2 asked you to predict the three slopes before making the plot. The answer was 1, 2, 4.
Now you make the plot with your own solvers.
Measuring the order is how you find a bug in a Runge–Kutta method. A tableau typo usually leaves a method that still converges — at the wrong rate.
The same cost, spent three ways
Method
Steps at NFE \(=4000\)
Error at \(t=2\pi\)
Euler
4000
\(4.9 \times 10^{-3}\)
Heun
2000
\(1.0 \times 10^{-5}\)
RK4
1000
\(8.2 \times 10^{-11}\)
Almost eight orders of magnitude — a factor of \(6\times10^{7}\) — and nobody spent more than anybody else.
Equal budget, three methods
The caveat that matters later
The table on the previous slide assumes you want high accuracy.
At a loose tolerance the three methods sit much closer together. At one or two evaluations total, a high-order method has no advantage at all — it has not taken enough steps for its order to express itself.
Sampling a generative model at 4 NFE lives in exactly that regime.
Sweeping the tolerance
rtol from \(10^{-1}\) to \(10^{-8}\). (a) NFE against error achieved, fitted slope \(-0.50\). (b) The outcome against the request, with the diagonal.
Reading 1 — the request is honoured, here
Panel (b) is a straight line parallel to the diagonal.
Nothing guarantees that. The controller bounds a local error estimate; panel (b) plots the global error at the end.
“I set rtol to \(10^{-6}\)” is a statement about what you asked for, never about what you got.
Reading 2 — the slope is the order
Fitted slope \(-0.50\); an order-2 method predicts \(-1/2\).
Halving the error costs \(\sqrt{2}\) times the work here. With a fifth-order method it would cost \(2^{1/5}\).
Block E measures exactly that difference.
Reading 3 — rejections are rare, and should be
Across all eight runs: \(0, 0, 1, 1, 2, 3, 3, 4\) rejected steps — out of up to 48 561 accepted.
The safety factor \(0.9\) is what buys that.
A rejected step costs its evaluations and makes no progress. A controller that rejects constantly is misconfigured.
The two failure smells, on their own plot
Left end: 32 evaluations, error \(0.2\) on an orbit of radius \(1\). Fast, smooth, plausible, wrong. Nothing complained.
Right end: 97 130 evaluations, error \(1.8\times10^{-8}\), cost \(\times 3035\). You notice immediately; it costs only time.
04JAX in five moves
One sentence, then five elaborations
JAX is NumPy, plus function transformations, plus explicit randomness.
Scope, declared. Five moves and stop. No flax, no optax beyond a mention. No sharding, no TPU.
The primer optimizes for reading research JAX and mirroring this course. Not for writing production JAX.
Move 1 — pure functions and jnp
y = x.at[i].set(v) # new array; x unchanged
You give up in-place mutation. What you buy is that the function can be transformed — differentiated, vectorized, compiled, or all three.
A transformation must know that calling your function twice with the same arguments does the same thing twice.
Purity is the price of transformability.
Move 2 — grad returns a function
def loss(theta, x):return jnp.sum((theta * x -1.0) **2)dloss = jax.grad(loss) # same signatureg = dloss(theta, x) # same pytree as theta
No tape. No .backward(). No .grad attribute on anything.
Directional, as U0.L1 defined it: two evaluations whatever the parameter count. Float64 — and JAX is float32 by default, so the primer enables x64 explicitly and asserts it.
Three lines, one second, and it is the only thing that distinguishes a gradient from a plausible array of numbers.
The adaptive Heun of block B against diffrax’s Tsit5, same problem, same tolerances. At error \(1.8\times10^{-8}\): 97 130 evaluations against about 360.
The lines are diverging — an order-5 cost curve is flatter than an order-2 one.
Writing it was still necessary
You now know what Tsit5, rtol and PIDController mean — and you know it because you built the smaller version of each.
The from-scratch pass is how you learn the object. It is not how you integrate an equation you care about.
Honest timing
CPU, JAX 0.10.2 — one machine, one run.
Report compile time separately from steady-state time. Never amortise the compile into the speedup.
Both numbers, or neither
Time the first call, and jit looks four times slower.
Time only the steady state, and you have hidden a fixed cost a short script never earns back.
Together: \(1.8\times\) faster per call, \(87\) ms up front — break-even after about eight calls, a loss below that.
Two more honesties
\(1.8\times\) is modest, and it is the real number. This workload is matrix multiplications — already tuned kernels, little for a compiler to fuse. XLA earns its large factors on many small operations.
Wall clock does not reproduce. Every other number today comes back identical. These three move by milliseconds between runs, and will differ on your machine.
06Freeze, packet, PS0
Frozen and tagged
common/odesolve.py and jax-primer.ipynb, tagged u0l5-freeze.
U3.L1 imports odeint under today’s contract. A change to that signature after today is a breaking change.
The packet
ode-solver-notes (U0.T5 — written standalone-first for this)
a pointer list into UP.A for the probability assumed
a README with a three-evening pacing
It exists because of a rule from the top of the course: no joint session may require having attended U0.
PS0 — assigned now
Your VAE (U0.L3) with an encoder built from your U-Net’s down-path blocks through the frozen VAEEncoderAdapter (U0.L4) — UNet.forward(x, t, y) is neither substituted nor trained — the hygiene stack (U0.L2), and your harness (U0.L4): FID at the course-standard \(n\), plus panels.
Graded on correctness and reproducibility, not on sample quality.
The guardrails, literally
A run that is reproducible and mediocre scores above a run that is beautiful and cannot be repeated.
“Reproducible” is operational — the hygiene stack already enforces it:
config · seed · commit hash + dirty flag · the MLflow SQLite store
Compute ceiling: a single consumer GPU, a small number of epochs. More than that has misread the exercise.
Five weeks, eleven artifacts
Artifact
Built
Consumed by
training-loop-template
L1
every lab
hygiene-stack
L2
every lab; PS0
elbo-derivation
T3
U3.T2, U5.T1
vae-mnist-scratch
L3
L4, PS0
time-conditioning-patterns
T4
L4, U3 labs, U5.T1
fid-definition
T4
L4
unet-skeleton
L4
U3.L2, U3.L3, PS0
eval-harness
L4
U1.L2, U3.L2/L3, PS0
ode-solver-notes
T5
U2.L1, U3.L1, packet
ode-solvers-scratch
L5
U3.L1, packet
jax-primer
L5
JAX moments, packet
From here, the course uses its tools
The next live session is UP.T1, where the course proves the one result it uses three times:
conditional expectation as an \(L^2\) projection — the projection Lemma.
Everything after it leans on that page.
Rest during the async window. The PESC students are just arriving.