Forget, then remember: checkpointing on a GPU
See how GPU checkpointing saves memory for Monte Carlo Greeks by keeping selected checkpoints and recomputing values during the adjoint sweep.
The 252-step Asian tape from Chapter 3 costs a CPU only 1.7× more to get all five Greeks instead of just the price. On a GPU, the same tape costs 8×. Same tape, same arithmetic, same reverse sweep. Where did the other 6× go?
The whole story
Recomputing a segment only works because NablaTensor's GPU kernels draw
randoms from Philox, a counter-based generator: draw k of path p is
a pure function of (p, k), not the next value pulled from some mutable
stream state. Re-running a segment's forward arithmetic a second time asks
for the same draws by index and gets bit-identical numbers back, with
nothing saved anywhere for the random stream itself. A stateful generator
would need that state checkpointed too, at every cut — and the memory
saving would be a fraction of what it is.
Why a CPU doesn't notice and a GPU can't ignore it
The reverse sweep needs some of the forward sweep's intermediate values
back — an exp node needs its own output; a multiplication needs both its
inputs. Add that up over 252 time steps and it comes to roughly 760
forward values a plain kernel has to keep alive from the moment they're
computed until the reverse sweep, hundreds of nodes later, reads them.
cpu-jit keeps the whole tape in a 12 KB double[], one path at a time
per thread — a fraction of an L1 cache. A GPU lane gets, at best, a few
hundred registers, shared across however many paths the scheduler is
running at once. Ask it to hold 760 values and the compiler spills the
overflow to scratch memory; every lane doing that at once turns the
reverse sweep into a memory workload wearing an arithmetic costume. That's
the 8×.
Shared checkpoint analysis, backend-specific kernels
AadCheckpointPlan in nablatensor-core provides shared checkpoint
analysis across CUDA, ROCm, OpenCL, Vulkan, and WebGPU: it finds each node's
last use and tracks which values cross segment boundaries. Vulkan and
WebGPU use its balanced cut selection, which favors cuts with fewer
crossing values. CUDA, ROCm, and OpenCL use the same last-use and carry
analysis, while retaining their fixed segment-length policy. Each backend
still emits its own kernel code and uses its own runtime/storage layout.
The plan is shared; the kernels are not. GLSL, WGSL, and CUDA-family backends each emit and replay their own code, but they can rely on the same core analysis of last uses and values carried across cuts.
What it's worth
This part is Vulkan's own numbers, from the engine's blog article on exactly this technique — the same 252-step Asian tape, 300,000 paths, value plus five Greeks, measured as the median of three seven-point spot ladders:
| kernel | ms / 7-point ladder | Mpath/s | vs plain |
|---|---|---|---|
| plain (fully unrolled) | 138 | 15 | — |
| checkpointed, 16 segments | 47 | 46 | 2.9× |
| checkpointed, 4 markets/dispatch | 20 | — | 6.8× |
Every row returns the same price and Greeks to all seventeen significant digits — not "close," bit-exact, because every recomputed value is the same instructions on the same inputs the first pass already produced. Below the spill point (50–100 time steps) the same trick costs 0.85×, which is why Vulkan only turns it on automatically above 768 nodes.
Try it yourself
For the CUDA/ROCm/OpenCL fixed-segment policy, use
segLen = max(48, round(√n)). Would checkpointing apply to any tape this
Learn section has built so far? 1.3's European call: 26 nodes. 3.1's
Asian call at 252 steps: 1,536 nodes. The policy applies only above its
minNodes threshold; Vulkan and WebGPU instead choose balanced cuts using
the shared analysis.
▶️ Run it
This interactive example builds the same production Asian-call tape and asks
NablaTensor's AadCheckpointPlan for its fixed-length CUDA-family segment
layout. TeaVM shows the planner's checkpoint layout, but does not execute a
GPU kernel. Vulkan and WebGPU use the shared last-use and carry analysis too,
with balanced cuts instead.
To measure the checkpointed GPU kernel itself, use a machine with a Vulkan-capable device:
mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
-Dexec.mainClass=com.nablatensor.bench.CheckpointBench \
-Dengine=vulkan -Dsteps=252 -Dscenarios=300000 -Dmode=checkpointed
Nothing to switch on beyond the engine name — a tape of 768 nodes or more
gets the checkpointed sweep automatically on vulkan.
⚠️ What this doesn't do
This page focuses on vulkan's register-only checkpointing in depth. The
cuda/opencl/rocm path has a different scratch-buffer layout and its
own segment-length policy; the deeper link below covers the historical
layout bug and tuning work in more detail. It also doesn't cover sharing
random draws across a whole revaluation ladder in one GPU dispatch, a
related but different trick from the same article.
What's next
→ Deeper: Forget, then remember: checkpointing the adjoint sweep on a GPU has the full derivation, the register-spill cliff chart, the compiler- merging bug that needed an XOR to defeat, and the unmerged CUDA/OpenCL/ROCm fix this page only named. → Next: Four ways to talk to a GPU — CUDA, Vulkan, OpenCL, and ROCm, compared instead of just named.