Checkpointing at scale: stop losing hours to failed nodes
Async and sharded checkpointing, the Young/Daly interval formula, NVMe vs object storage tiers, and resume drills that keep failed nodes from costing you hours.
Reviosa Engineering
Reviosa Cloud
September 8, 2026 · 7 min read
Failures are a rate, not an event
On a single GPU, a hardware failure is an unlucky afternoon. On hundreds of GPUs, it is a steady background rate you plan around. Meta's Llama 3 paper reported 419 unexpected job interruptions over a 54-day period of pretraining on a cluster of roughly 16,000 H100s, about one every three hours, with GPU and HBM faults the largest category. You do not need to run anywhere near that scale for the arithmetic to matter: failure rate grows linearly with node count, while the cost of each failure grows with how much work you lose.
The goal of a checkpointing strategy is to minimize total wasted time, which has three parts: time spent writing checkpoints, work lost since the last checkpoint when a failure hits, and time to restart. This post covers each, with the math to pick an interval and the storage layout to make writes cheap.
How big is a checkpoint?
For mixed-precision training with Adam, a full training checkpoint typically holds:
| Component | Bytes per parameter |
|---|---|
| BF16 model weights | 2 |
| FP32 master weights | 4 |
| Adam first moment (FP32) | 4 |
| Adam second moment (FP32) | 4 |
| Total | ~14 |
So an 8B model checkpoint is about 112 GB and a 70B model is about 980 GB, before data-loader state, RNG state, and scheduler state, which are small. Some setups skip the separate BF16 copy and store only FP32 master weights, bringing the total to about 12 bytes per parameter. Either way, checkpoint size scales with parameters, not GPUs, so it is fixed for a model while the write bandwidth available to you scales with the number of nodes writing in parallel.
The interval math: Young/Daly
The classic result, from Young (1974) and refined by Daly (2006), gives the checkpoint interval that minimizes expected waste:
tau_opt ≈ sqrt(2 × C × M)
C = time training is blocked per checkpoint
M = mean time between failures for the whole job
Job MTBF is node MTBF divided by node count: a job on 64 nodes fails 64 times as often as one node. With an interval tau, the expected fraction of time wasted is roughly:
waste ≈ C / tau + tau / (2 × M) + R / M
R = restart cost: detect, reschedule, reload, warm up
An illustrative example, with assumed numbers rather than measurements. Suppose your 64-node job sees one interruption every 12 hours (M = 43,200 s) and restart takes 15 minutes (R = 900 s).
| Scenario | Blocking time C | Optimal interval | Checkpoint + lost work | Restart | Total waste |
|---|---|---|---|---|---|
| Synchronous save | 60 s | ~38 min | ~5.3% | ~2.1% | ~7.4% |
| Async save | 5 s | ~11 min | ~1.5% | ~2.1% | ~3.6% |
| Async + faster restart (R = 300 s) | 5 s | ~11 min | ~1.5% | ~0.7% | ~2.2% |
Two lessons fall out. First, reducing blocking time is worth more than tuning the interval, because it lets you checkpoint more often at lower cost. Second, once checkpointing is cheap, restart cost dominates, so the time from failure to resumed training deserves as much engineering attention as the save path. At cluster scale, the difference between 7.4% and 2.2% waste is several node-days per month.
Recompute tau whenever you resize the job, since M changes with node count.
Make saves asynchronous
A synchronous save blocks every rank until the last byte is on storage. An asynchronous save splits the work: block briefly to copy state from GPU memory to pinned host memory, then write to storage in a background thread while training continues. The blocking time C drops from "time to write to storage" to "time to copy to host RAM", which is usually seconds.
PyTorch Distributed Checkpoint (DCP) supports this directly:
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.state_dict import get_state_dict
pending = None
def save(step, model, optimizer, extra_state):
global pending
if pending is not None:
pending.result() # never queue two saves at once
model_sd, optim_sd = get_state_dict(model, optimizer)
state = {"model": model_sd, "optim": optim_sd, "extra": extra_state}
pending = dcp.async_save(state, checkpoint_id=f"/mnt/ckpt/step_{step:08d}")
Two cautions. Host memory must hold a staged copy of each rank's shard, so check that your per-node RAM can fit the node's share of the checkpoint alongside the data loader. And wait for the previous save to finish before starting the next; if saves start overlapping, your interval is shorter than your write time and you need faster storage or a longer interval.
Shard, don't gather
The worst checkpoint pattern at scale is gathering the full state dict to rank 0 and writing one file. It serializes the write through one process, one NIC, and one disk, and it can run rank 0 out of memory for large models.
Sharded checkpointing has every rank write its own shard in parallel, plus a small metadata file that describes how shards map to the global tensors. DCP does this by default with FSDP and tensor-parallel state. Write bandwidth then scales with node count: if each node can sustain several GB/s to its local disk, a 980 GB checkpoint spread over 8 nodes is about 122 GB per node and finishes in tens of seconds rather than many minutes.
Sharded checkpoints are also reshardable. DCP can load a checkpoint saved at one world size into a job with a different world size, which matters when a failure forces you to resume on fewer nodes, or when you scale up mid-run.
Where to put checkpoints: a two-tier layout
Speed and durability pull in different directions, so use two tiers.
Tier 1: fast, close to the GPUs. Write async checkpoints to local NVMe or to an NVMe block volume attached to the instance. Our 8×H100 nodes ship with 22 TB of local NVMe, and block volumes are triple-replicated NVMe that survive instance termination and can be reattached to a different instance. Keep the last two or three checkpoints here for fast restarts.
Tier 2: durable, decoupled from the cluster. Copy every Nth checkpoint, or every checkpoint for critical runs, to S3-compatible object storage in the background. Object storage is designed for 11 nines of durability and is independent of any node's health. It is also where checkpoints go to be shared: evaluation jobs, fine-tuning runs, and serving deployments read from there.
| Tier | Write speed | Survives node loss | Use for |
|---|---|---|---|
| Local NVMe | Fastest | No | Most recent checkpoint, scratch |
| NVMe block volume | Fast | Yes (reattach) | Last few checkpoints, fast resume |
| Object storage | Moderate, parallel | Yes | Durable history, sharing, cross-region copies |
Upload from tier 1 to tier 2 with a separate process at low priority so it does not compete with training I/O, and verify each upload with checksums before deleting the local copy. Write a small "complete" marker only after every shard and the metadata are uploaded, and have the resume logic ignore any checkpoint without one. Partial checkpoints are worse than no checkpoint, because they fail at the worst possible time.
Fast restarts
Once saves are cheap, restart cost R dominates. Ways to shrink it:
- Automatic failure detection. Use
torchrunwith--max-restartsand a rendezvous backend so a failed rank triggers a coordinated restart, and set NCCL timeouts that detect hangs in minutes, not hours. - Hot spares. For long runs, keep one spare node per few dozen in the job, already provisioned with the image and volumes mounted. With per-second billing, a spare costs exactly its runtime, and it is usually cheaper than the idle cluster waiting for a replacement.
- Fast image boot. Our median cold boot is 74 seconds from request to SSH; a replacement node joins far faster than the time it takes a person to notice the failure.
- Resume from tier 1. Reading from local NVMe or an attached block volume is much faster than pulling hundreds of gigabytes from object storage.
Run resume drills
A checkpoint you have never restored is a hope, not a backup. Schedule drills:
- Kill a rank mid-run at a random step, monthly or before any long run, and time detection to resumed training.
- Verify continuity. After resume, the loss at step N+1 should match an uninterrupted reference run closely. Large deviations usually mean the data-loader position, RNG state, or learning-rate scheduler was not saved.
- Restore at a different world size to confirm resharding works before you need it.
- Restore from tier 2 only, as if every node were gone, and measure how long it takes.
- Write down R and feed it back into the interval math.
For the network side of multi-node jobs, see our InfiniBand and NCCL tuning guide. Storage rates for volumes, object storage, and snapshots are on pricing.
FAQ
How often should I checkpoint a large training run?
Use the Young/Daly approximation: the square root of 2 × blocking time × job MTBF. With async saves that block for seconds, the optimal interval is often 10-20 minutes for multi-node jobs.
What is asynchronous checkpointing?
Training blocks only long enough to copy state to host memory, then a background thread writes it to storage while training continues. PyTorch DCP provides this through async_save.
Should checkpoints go to NVMe or object storage?
Both. Write to local NVMe or a block volume for speed and fast restarts, then copy to object storage in the background for durability and sharing.
How big is a training checkpoint?
About 12-14 bytes per parameter with Adam and mixed precision, so roughly 112 GB for an 8B model and about 980 GB for a 70B model.
Run it yourself
Same fleet, your workload
Everything we write about runs on hardware you can rent by the second — H100 SXM from $2.69/hr, live in under 90 seconds.
$10 free credit for new accounts · Per-second billing · No egress fees