ml-models / Training systems / Checkpointing core

Checkpointing

A long training run will be interrupted, and resuming it should give the same run you would have had without the interruption. That takes more than the weights. This page saves and resumes a small Adam run mid-epoch, shows what goes wrong when each piece of state is left out, sizes a checkpoint, picks how often to save, loads files safely, reshards a distributed checkpoint, and covers activation checkpointing, which trades compute for memory. Every number is computed and every PyTorch link is pinned.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. State dicts
  5. Random state and data position
  6. Saving and loading files
  7. How big, and how often
  8. Distributed checkpoints
  9. Activation checkpointing
  10. Worked example
  11. A run that is interrupted
  12. Resume five ways
  13. How big is a resumable checkpoint?
  14. How often to save
  15. Loading 4 shards on 3 ranks
  16. Activation checkpointing a 16-layer stack
  17. What weights_only sees in a pickle
  18. Implementation
  19. Tradeoffs
  20. Connections
  21. Read first
  22. Read next
  23. References

Problem

A large training run lasts days or weeks on many machines, and something will stop it: a hardware fault, a preempted spot instance, a crashed process, a bug fix you need to deploy. Without a checkpoint all of that compute is lost. With a checkpoint that holds only the weights, training restarts, but as a different run: the optimizer forgets its momentum, the learning-rate schedule starts again from warmup, and the data order changes. Losses spike, results stop being reproducible, and a bug introduced by the resume is hard to tell apart from a real training problem.

Checkpoints also leave the machine that made them. People download weights, and PyTorch's default file format is a Python pickle, which can run code when it is loaded. And at scale a checkpoint is many gigabytes split across hundreds of ranks, so writing it takes real time, and the job that reads it back may not have the same number of GPUs.

The same word also names an unrelated memory technique. Activation checkpointing keeps only some of the activations from the forward pass and recomputes the rest during backward, so a deeper model or a bigger batch fits in memory. It is covered at the end of Mechanics.

Intuition

A checkpoint is a save file for a game with randomness in it. To continue exactly where you stopped, the save must hold the map and your inventory (the weights), but also how fast you are moving and in which direction (the optimizer's moments), where you are on the level's timer (the schedule), the state of the dice (the random number generators) and which cards you have already drawn from the deck (the position in the data). Leave any one out and you can still keep playing, but not the same game.

What a resumable checkpoint holds Six pieces of training state, each with the call that captures it, flow into one checkpoint. The model's parameters and buffers come from model.state_dict(). Adam's moments and step count come from optimizer.state_dict(). The schedule's step comes from scheduler.state_dict(). The mixed-precision loss scale comes from scaler.state_dict(). The CPU, CUDA and Python random generator states come from torch.get_rng_state, torch.cuda.get_rng_state_all and random.getstate. The epoch and batch within the epoch are saved by your own code, since DataLoader has no state_dict. The weights alone are enough for inference; resuming training needs all six. model parameters, buffers model.state_dict() optimizer Adam m, v, step count optimizer.state_dict() scheduler steps taken so far scheduler.state_dict() grad scaler loss scale, growth tracker scaler.state_dict() RNG CPU, each GPU, Python get_rng_state(), ... data position epoch, batch in epoch your own dict checkpoint one torch.save file, or one DCP directory inference needs the first box only exact resume needs all six plus the step counter and the config that built the model

Everything on the left changes every step. The worked example below leaves out the optimizer, scheduler, RNG and data position in turn and measures what changes; the toy run has no grad scaler.

Mechanics

State dicts

PyTorch saves state as plain dictionaries, never as live objects. Module.state_dict() maps each parameter's and buffer's dotted name (layers.0.attn.q_proj.weight) to its tensor; load_state_dict(sd, strict=True) copies tensors back by name and, with strict=False, returns the missing and unexpected keys instead of raising. Because the file holds names and tensors, not code, the model class must be rebuilt by your code before loading.

Optimizer.state_dict() has two entries: state, the per-parameter tensors (for Adam: exp_avg, exp_avg_sq and step), and param_groups, the hyperparameters of each group with parameters replaced by integer ids. Its docstring is explicit that "the parameter itself is NOT saved"; load_state_dict matches ids to the new optimizer's parameters by order, so the parameters must be passed in the same order as when the checkpoint was made. A learning-rate scheduler's state_dict is its __dict__ minus the optimizer; restoring it (load_state_dict) restores how many steps have been taken. See optimizers and learning-rate schedules for what those states mean.

Random state and data position

Shuffling, dropout, data augmentation and random cropping all draw from generators. torch.get_rng_state() returns the CPU generator's state as a byte tensor and set_rng_state restores it; torch.cuda.get_rng_state_all() and set_rng_state_all do the same for every GPU. Python's random and NumPy have their own generators, saved separately.

The position in the data is the piece most often lost. torch.utils.data.DataLoader at this pin has no state_dict, so the epoch and the number of batches already consumed must be saved by your loop. The order itself can be made a pure function of saved numbers: DistributedSampler.__iter__ shuffles with a fresh generator seeded with self.seed + self.epoch, so after set_epoch(epoch) the order is reproduced exactly, and resuming mid-epoch means skipping the batches already seen. The plain RandomSampler, given no generator, draws a fresh seed from the global generator on every iter(), so its order depends on the RNG state at the start of the epoch. The worked example uses the seed + epoch scheme for the order and a separate stateful generator for input noise, so both pieces have to be restored.

Saving and loading files

torch.save(obj, f) pickles the object with protocol DEFAULT_PROTOCOL = 2 and writes tensor storages as separate entries in a zip archive (_use_new_zipfile_serialization=True by default). Write to a temporary file and rename it over the old one, so a crash mid-write never leaves a half-written file under the name your resume logic will read; keep the last few checkpoints rather than one, in case the newest is bad.

torch.load takes map_location (load GPU tensors onto the CPU or another device), mmap (map the file instead of reading it into memory) and weights_only. Unpickling can call any importable function, because a pickle can say "call this global with these arguments" (Python's __reduce__). Since 2.6 weights_only defaults to True; the error message says setting it to False "can result in arbitrary code execution". With weights_only=True a restricted unpickler resolves each GLOBAL opcode against an allowlist (tensor rebuild functions, dtypes, torch.Size, nn.Parameter, collections.OrderedDict, collections.Counter, and others), refuses the modules sys, os, posix and nt outright, and raises "Unsupported global" for anything else. Your own trusted classes can be added with torch.serialization.add_safe_globals. Formats such as safetensors avoid pickle entirely by storing only a header and raw tensor bytes.

How big, and how often

With mixed precision and Adam, each parameter carries a bf16 copy for compute (2 bytes), an fp32 master copy (4) and two fp32 moments (4 + 4): 14 bytes to resume, against 2 bytes for an inference export. The ZeRO paper counts the same 12 bytes of fp32 optimizer state plus 2 bytes each of half-precision weights and gradients, 16 in all, in memory; gradients are recomputed every step and are not saved.

Saving often wastes time writing; saving rarely wastes time redoing lost work. If writing a checkpoint stops training for C seconds and failures arrive on average every M seconds, saving every T seconds loses about C/T of the time to writing and, on a failure, half an interval of work on average. Young (1974) minimized that first-order waste:

waste(T) ≈ C / T + T / (2M)   ⇒   T* = √(2 C M)

At T* the two terms are equal. Making writes cheaper shortens the best interval and lowers the waste, which is why large jobs save asynchronously: dcp.async_save first copies the state to staging storage (CPU memory by default) and then runs the save in a separate thread, so training is blocked only for the copy. The formula assumes failures are independent and C is much smaller than M; a job on N machines has roughly N times the failure rate of one.

Distributed checkpoints

When FSDP or tensor parallelism shards parameters and optimizer state across ranks, gathering everything onto rank 0 to call torch.save is slow and may not fit in its memory. torch.distributed.checkpoint (DCP) instead has every rank write its own shards in parallel: dcp.save "handles ShardedTensor, and DTensor by having each rank only save their local shards", and a .metadata file records which piece of which tensor lives where. On dcp.load "each rank will try to read the least amount of data necessary"; the default load planner "handles resharding by issuing multiple read requests against storage", so a checkpoint saved on 64 GPUs can be loaded on 48. get_state_dict(model, optimizers) returns model and optimizer state dicts keyed by canonical names that "can be resharded with a different number of trainers and/or different parallelisms", and set_state_dict puts them back. DCP loads in place: tensors in the state dict you pass must already be allocated with their destination layout.

Activation checkpointing

Backpropagation needs the activations of every layer, so by default the forward pass keeps all of them, and activation memory grows with depth × batch × sequence length. torch.utils.checkpoint.checkpoint(fn, *args) runs fn without saving its intermediate tensors, keeps only its inputs, and runs fn again during backward to rebuild them; its docstring describes this as trading compute for memory. With preserve_rng_state=True (the default) it stashes and restores the RNG state so dropout masks in the recomputation match the original forward. Pass use_reentrant=False; the docstring recommends it, and it works with torch.autograd.grad and keyword arguments.

checkpoint_sequential(functions, segments, input) splits a sequence of n layers into k segments of ⌊n/k⌋ layers; all segments except the last are checkpointed and the last absorbs the remainder. At the peak, memory holds the k − 1 saved segment inputs plus one segment's activations, about k + n/k layers' worth, which is smallest near k = √n: memory grows with √n instead of n, for about one extra forward pass. That is the sublinear-memory result of Chen et al. (2016). Transformer libraries usually checkpoint each block, saving one tensor per block and recomputing attention and MLP intermediates.

ComponentShape/ParamsMeaningSource
Module.state_dict → {dotted name: tensor} Parameters and persistent buffers by name; no code, no class. module.py 2203-2291
Module.load_state_dict sd, strict=True, assign=False Copies tensors in by name; returns missing and unexpected keys. module.py 2539-2652
Optimizer.state_dict {"state": {id: {...}}, "param_groups": [...]} Per-parameter moments and step; hyperparameters per group, parameters as ids. optimizer.py 718-809
Optimizer.load_state_dict state dict from the same parameter order Maps saved ids to the current parameters and casts state to their device. optimizer.py 918-1064
LRScheduler.state_dict __dict__ minus the optimizer How far along the schedule is (last_epoch) and its settings. lr_scheduler.py 182-200
torch.get_rng_state / cuda.get_rng_state_all ByteTensor / list of ByteTensor CPU and per-GPU generator states, restored with the matching setters. random.py 27-48, cuda/random.py 45-88
DistributedSampler seed, epoch, rank, num_replicas Order from seed + epoch, so set_epoch reproduces it. distributed.py 107-157
torch.save / torch.load obj, f; map_location, weights_only, mmap Zip archive of a protocol-2 pickle plus raw storages; restricted unpickling by default. serialization.py 945-1021, 1316-1650
add_safe_globals list of classes or functions Extends the weights_only allowlist with globals you trust. serialization.py 283-318
dcp.save / dcp.async_save / dcp.load state_dict, checkpoint_id Each rank writes or reads only its shards; load reshards to the current layout. state_dict_saver.py 89-397, state_dict_loader.py 60-202
get_state_dict / set_state_dict model, optimizers Canonical-name state dicts for FSDP, DDP and tensor-parallel models. state_dict.py 1271-1361, 1481-1544
checkpoint / checkpoint_sequential fn, *args, use_reentrant=False; segments Activation checkpointing: keep inputs, recompute the rest in backward. checkpoint.py 423-617, 701-792
A collaborator's checkpoint fails to load with "Unsupported global: GLOBAL mylib.config.TrainConfig". Is setting weights_only=False the fix?

Only if you trust the file completely, because weights_only=False lets the pickle call any importable function. The narrower fix is torch.serialization.add_safe_globals([TrainConfig]) after checking that the class is yours and harmless to construct, or better, asking for a checkpoint that stores the config as a plain dict. The error is the allowlist doing its job: the file asks to build an object the loader does not know is safe.

Worked example

A run that is interrupted

Fit y ≈ w·x + b to 8 points with Adam (β = 0.9, 0.999), batches of 2 for 3 epochs (12 steps). The learning rate warms up linearly over 3 steps to 0.5, then follows a cosine to zero. Each epoch's order is Random(seed + epoch), as in DistributedSampler, and each input gets Gaussian jitter (σ = 0.1) from one stateful generator, standing in for dropout or augmentation. The run is checkpointed after step 6, in the middle of epoch 2.

StepEpochBatch (point ids)Learning ratewbLoss on all points
1 1 6, 7 0.1667 0.1667 0.1667 6.2282
2 1 2, 4 0.3333 0.4007 0.4338 4.4022
3 1 0, 3 0.5000 0.7257 0.6987 2.5902
4 1 1, 5 0.5000 1.0337 0.9426 1.3554
5 2 5, 0 0.4849 1.3348 1.1309 0.5953
6 (save) 2 6, 4 0.4415 1.5859 1.2977 0.2520
7 2 1, 7 0.3750 1.7863 1.4264 0.1697
8 2 2, 3 0.2934 1.9259 1.5018 0.2002
9 3 3, 6 0.2066 2.0121 1.5464 0.2548
10 3 0, 1 0.1250 2.0619 1.5659 0.2928
11 3 5, 2 0.0585 2.0823 1.5717 0.3084
12 3 4, 7 0.0151 2.0864 1.5720 0.3108

The checkpoint after step 6 holds w = 1.5859, b = 1.2977; Adam's step count t = 6 and moments m = (−1.8582, −0.6741), v = (0.2910, 0.0868); the schedule's step 6; the jitter generator's state; and the data position: epoch 2, 2 of 4 batches done.

Resume five ways

Restoring everything replays steps 7 to 12 bit for bit: every w, b, learning rate and batch equals the uninterrupted run's exactly, not approximately. Each other row restores everything except one piece, which it resets to its value at the start of training (the position resets to the start of the current epoch, the common bug when only the epoch is saved).

ResumeStep 7 batchStep 7 learning rateIdentical?Final wFinal bFinal lossLargest gap in w or b
Full checkpoint 1, 7 0.3750 yes 2.0864 1.5720 0.3108 0.0000
Forgot optimizer state 1, 7 0.3750 no, from step 7 2.2428 1.2677 0.1700 0.3043
Forgot scheduler state 1, 7 0.1667 no, from step 7 2.5090 1.6810 0.9231 0.4226
Forgot RNG state 1, 7 0.3750 no, from step 7 2.0908 1.5580 0.2984 0.0140
Forgot position in epoch 5, 0 0.3750 no, from step 7 2.0885 1.5396 0.2785 0.0325
  • Optimizer. With t reset to 1, Adam's bias correction makes the first step's size about the full learning rate in the gradient's sign direction, whatever the gradient's size; the saved moments would have damped it. The gap peaks at 0.304.
  • Scheduler. The warmup starts again, so step 7 uses 0.1667 instead of 0.3750, and the next steps climb back to 0.5 where the schedule should be decaying. This is the largest gap of the four (0.423).
  • RNG. The batches and learning rates are right, but the jitter replays the noise from step 1. The gap is small here (0.014), but the run is no longer reproducible, and with dropout in a real network every mask changes.
  • Position. Epoch 2 starts over, so its first 2 batches are seen twice and the last 2 batches of the final epoch are never reached before the step budget ends.

3 of the four broken resumes end with a lower loss than the uninterrupted run (0.3108). A broken resume is not necessarily worse on one run; it is a different, unrepeatable run, which is what makes these bugs hard to find from the loss curve.

The weight w during the uninterrupted run and four broken resumes The weight w over 12 steps. The uninterrupted run, which the full resume matches exactly, rises from 0 to 2.086. From the checkpoint at step 6, the run that forgot the scheduler overshoots to 2.509, the run that forgot the optimizer ends at 2.243, and the runs that forgot the RNG or the data position stay close to the uninterrupted curve, ending at 2.091 and 2.088. 0 1 2 0 2 4 6 8 10 12 step w checkpoint Uninterrupted = full resume Forgot optimizer state Forgot scheduler state Forgot RNG state Forgot position in epoch

Curves leave the uninterrupted run at the checkpoint. The scheduler bug is the most visible because it re-applies a large learning rate late in training; the RNG and position bugs are nearly invisible in w, and that is why they survive.

How big is a resumable checkpoint?

For a 7-billion-parameter model trained in bf16 with Adam:

PieceBytes per parameterSizeWhy
bf16 weights214 GBthe copy the forward pass uses; also the whole inference export
fp32 master weights428 GBwhat the optimizer updates under mixed precision
Adam first moment m428 GBone fp32 value per parameter
Adam second moment v428 GBone fp32 value per parameter
Resumable checkpoint1498 GB7× the 14 GB inference export

The optimizer state alone is six times the bf16 weights, which is why sharding it (ZeRO, FSDP) and writing shards in parallel matter more than compressing the weights.

How often to save

Suppose the whole job fails on average every 6 hours (M = 21600 s), and a blocking save stops training for C = 60 s. Young's interval is T* = √(2 · 60 · 21600) = 1610 s ≈ 26.8 min, and both waste terms equal 3.73% there, 7.45% in all. If an asynchronous save blocks for only C = 5 s, T* drops to 7.7 min and the waste to 2.15%.

Blocking time CT = 5.0 minT = 15.0 minT*T = 60.0 minT = 120.0 min
60 s 20.69%8.75%26.8 min: 7.45%10.00%17.50%
5 s 2.36%2.64%7.7 min: 2.15%8.47%16.74%

The minimum is flat: saving at half or twice T* costs little extra. Being off by ten times costs a lot in either direction.

Loading 4 shards on 3 ranks

A 12-row parameter was saved row-sharded across 4 ranks and is loaded on 3. Each new rank owns a contiguous block of rows and reads, from each saved shard that overlaps it, only the overlapping rows. This is the kind of read plan DCP's metadata lets the load planner make.

New rankOwns rowsReads
0 0–3 rows 0–2 from old rank 0 (offset 0 in its shard); rows 3–3 from old rank 1 (offset 0 in its shard)
1 4–7 rows 4–5 from old rank 1 (offset 1 in its shard); rows 6–7 from old rank 2 (offset 0 in its shard)
2 8–11 rows 8–8 from old rank 2 (offset 2 in its shard); rows 9–11 from old rank 3 (offset 0 in its shard)
Resharding 12 rows from 4 saved shards onto 3 ranks Top: the saved checkpoint, 4 shards of 3 rows each, one colour per old rank. Bottom: the 3 new ranks of 4 rows each, every new rank's block coloured by the old shard each row is read from. Every new rank reads from two old shards, and every row is read exactly once. saved loaded old rank 0 old rank 1 old rank 2 old rank 3 new rank 0 new rank 1 new rank 2 0 1 2 3 4 5 6 7 8 9 10 11 12 row index; each colour band is one read request

No rank reads the whole tensor. With real FSDP or tensor-parallel layouts the shards are multi-dimensional, but the plan is the same: intersect what each rank needs with what each saved chunk holds.

Activation checkpointing a 16-layer stack

Count memory in units of one layer's saved activations and treat a segment's input as one unit. checkpoint_sequential with k segments keeps k − 1 segment inputs and, at the peak, one segment's activations; every checkpointed layer is run forward a second time.

Segments kLayers per segmentSaved inputsPeak memory (units)Layers recomputed
1160160 of 16
28198 of 16
443712 of 16
827914 of 16
161151615 of 16

The peak is lowest at k = 4 = √16: 7 units instead of 16. More segments only add saved inputs; with one layer per segment everything is saved again as inputs and nothing is gained. If backward costs about twice the forward, recomputing 12 of 16 layers adds 12/16 of a forward to 3 forward-equivalents of work, about 25% more compute.

What weights_only sees in a pickle

Each object is pickled with protocol 2, as torch.save does, and its opcodes are listed with pickletools without unpickling anything. The verdict uses an allowlist of two entries copied from PyTorch's (collections.OrderedDict and collections.Counter); the real list adds the tensor and dtype globals.

Object pickledBytesGLOBAL opcodesRestricted load
plain state dict 42 none loads
OrderedDict state dict 70 collections.OrderedDict loads
object with __reduce__ 93 builtins.print refused: builtins.print is not allowed

The third object's __reduce__ tells the unpickler to call print with a string. A plain pickle.load would call it; a real attack names os.system instead. The restricted loader refuses before calling anything, because the decision is made when the GLOBAL opcode is read.

Implementation

import os, random
import torch
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.state_dict import get_state_dict, set_state_dict
from torch.utils.checkpoint import checkpoint

def save(path, step, model, opt, sched, scaler, epoch, batches_done):
    state = {
        "model": model.state_dict(),
        "optimizer": opt.state_dict(),
        "scheduler": sched.state_dict(),
        "scaler": scaler.state_dict(),
        "rng": {"torch": torch.get_rng_state(),
                "cuda": torch.cuda.get_rng_state_all(),
                "python": random.getstate()},
        "data": {"epoch": epoch, "batches_done": batches_done},
        "step": step,
    }
    tmp = path + ".tmp"
    torch.save(state, tmp)
    os.replace(tmp, path)                       # atomic: never a half-written file at path

def resume(path, model, opt, sched, scaler, sampler):
    state = torch.load(path, map_location="cpu", weights_only=True)
    model.load_state_dict(state["model"])        # model already built and on its device
    opt.load_state_dict(state["optimizer"])      # same parameter order as when saved
    sched.load_state_dict(state["scheduler"])
    scaler.load_state_dict(state["scaler"])
    torch.set_rng_state(state["rng"]["torch"])
    torch.cuda.set_rng_state_all(state["rng"]["cuda"])
    random.setstate(state["rng"]["python"])
    sampler.set_epoch(state["data"]["epoch"])    # DistributedSampler: order = seed + epoch
    return state["step"], state["data"]["epoch"], state["data"]["batches_done"]

# Sharded models (FSDP, tensor parallel): every rank calls these.
def save_sharded(ckpt_dir, model, opt):
    model_sd, optim_sd = get_state_dict(model, opt)
    dcp.save({"model": model_sd, "optim": optim_sd}, checkpoint_id=ckpt_dir)

def load_sharded(ckpt_dir, model, opt):        # world size may differ from the save
    model_sd, optim_sd = get_state_dict(model, opt)
    state = {"model": model_sd, "optim": optim_sd}
    dcp.load(state, checkpoint_id=ckpt_dir)      # fills the tensors in place
    set_state_dict(model, opt, model_state_dict=state["model"], optim_state_dict=state["optim"])

# Activation checkpointing: keep each block's input, recompute its insides in backward.
class Stack(torch.nn.Module):
    def __init__(self, blocks):
        super().__init__()
        self.blocks = torch.nn.ModuleList(blocks)

    def forward(self, x):
        for block in self.blocks:
            x = checkpoint(block, x, use_reentrant=False)
        return x

In the loop, skip batches_done batches of the resumed epoch before training, and call save every T* seconds of wall time rather than every fixed number of steps if step times vary. The skip costs reading those batches again; a loader that saves its own position avoids that.

Tradeoffs

ChoiceGainsCosts
Full training state vs weights onlyExact, reproducible resume7× the bytes with bf16 + Adam; longer writes
Save more oftenLess work lost per failureMore time blocked writing; the optimum is T* = √(2CM)
Asynchronous saveBlocking time drops to the copy into CPU memoryExtra host memory for the staged copy; a failure during the background write loses that checkpoint
One torch.save file vs DCPSimple, portable, easy to inspectMust gather to one rank; DCP writes in parallel and reshards on load but needs a matching state-dict structure
weights_only=TrueLoading cannot run arbitrary codeCustom classes need add_safe_globals; store configs as plain dicts instead
Activation checkpointingMemory from ∝ n toward ∝ √n; bigger batches or longer sequences fitAbout one extra forward pass (≈ a third more compute); more segments than √n waste memory again
Skip-ahead resume vs a stateful loaderNo extra machineryReads and discards the batches already used; slow if an epoch is huge
Your saves now block for 5 s instead of 60 s thanks to async_save. Should you save less often, the same, or more often?

More often. T* = √(2CM) shrinks with √C: here from 26.8 min to 7.7 min. Cheaper writes make each save cost less than the work it protects, and the total waste falls from 7.5% to 2.2%. Keeping the old interval with the cheaper save would leave the work-lost term where it was.

A resumed run's loss is fine, but rerunning the same resume gives slightly different results from the original job. Which piece of state is the first suspect?

The random state. Batches and the learning rate would show up as a visible bump; a reset generator changes dropout masks, augmentation and possibly data-worker seeds while the loss looks normal, as in the RNG row above. Save the CPU, GPU and Python generator states (and seed workers from saved values), and check that the resumed run reproduces a known batch and loss at the first step after the checkpoint.

Connections

Read first

Read next

No page builds on this one yet.

The state being saved is defined on other pages: the loop that produces it on training loop, Adam's moments on optimizers, warmup and cosine on learning-rate schedules, the grad scaler and master weights on mixed precision, and DistributedSampler on data loading and batching. Sharded checkpoints follow the layouts of distributed training. Activation checkpointing trades against the memory that backpropagation keeps in the computation graph, and competes with gradient accumulation as the way to fit a bigger effective batch.

References