TAI BUI
Back to curriculum
CapstonePython

Sharded Checkpoint and Atomic Resume

A 70B-parameter training job is paused by a node failure every few hours, and the checkpoint format decides whether you lose 30 minutes or 30 hours. A sharded checkpoint has each rank write its own shard in parallel and records ownership in a manifest. Resume loads each rank's shard from its own file on the same world size, and an atomic write keeps a half-finished checkpoint from poisoning the next resume.

TIME: ~90 minPREREQUISITES: Phase 19 Track C lessons 42-49

Learning Objectives

  • Save a multi-rank checkpoint as a per-rank shard file plus a manifest that records which rank owns what.
  • Use the atomic write pattern (write to a temp path then rename) so a crash mid-write never produces a half-finished checkpoint.
  • Resume from the manifest and verify that every rank's parameter shard comes back byte-equal, with the ZeRO optimiser shards saved and loaded alongside it.
  • Defend the manifest schema against the three failure modes: world-size change, shard count mismatch, and partial write.

The Problem

A vanilla checkpoint gathers all parameters and optimiser state onto rank 0 and writes a single file. For a 70B model that is 1.1 TB of state through one rank's network port, and the other ranks sit idle until the gather finishes. Write bandwidth is the slowest single GPU's network link, not the aggregate. On a real cluster the gather-then-write step can take longer than the previous training hour, so the job ships less than one checkpoint per training day.

A sharded checkpoint flips the pattern: each rank writes its own shard to its own file in parallel, and a manifest records which rank owned which shard so resume can put each one back where it came from. Aggregate write bandwidth scales with the cluster. A 1 TB checkpoint that took 4 hours through one rank takes 4 minutes through 64 ranks. The manifest also gives you a contract for incompatible resumes. A world-size change is detectable, a partial write is detectable, and the load path can fail loudly instead of quietly using stale data.

Warm-up0/2
1. A process overwrites a 100 GB file in place and the machine loses power halfway through. What state is the file in afterwards?Warm-up
2. You download a large file and want to confirm that no byte changed in transit. What do you compare?Warm-up
Check: The Problem0/2
1. Why is gather-then-write the wrong checkpoint pattern at scale?Explain
2. Beyond holding the shard files, what job does the manifest do in a sharded save?Explain

The Concept

A sharded checkpoint is a checkpoint in which each rank writes its own slice of the training state to its own file. A manifest is a small JSON file that records which shard belongs to which rank, plus the world size and the per-shard hashes needed to validate the shards on load. An atomic write puts a file at its final name in one step, so a reader sees either the complete old file or the complete new one.

Mermaid diagram

Manifest schema

Abridged to two of the four shards:

json
{
  "world_size": 4,
  "step": 1234,
  "wall_clock_seconds": 4521,
  "shards": [
    {"rank": 0, "path": "rank0.bin", "sha256": "...", "param_shard_offset": 0, "param_shard_numel": 65536},
    {"rank": 1, "path": "rank1.bin", "sha256": "...", "param_shard_offset": 65536, "param_shard_numel": 65536}
  ],
  "schema_version": 1
}

Three fields are load-bearing. world_size makes a resume on a different size fail loudly instead of silently corrupting state. sha256 per shard catches partial or corrupted writes. param_shard_offset and param_shard_numel record where each rank's slice sits in the flat parameter tensor (the offset is a running sum of the earlier shards' sizes). The demo's load_sharded does not read them and simply indexes by rank, but a cross-world-size loader (exercise 4) needs them to concatenate the slices and re-shard.

Atomic write

The standard pattern is to write each shard to <name>.tmp, write the manifest to manifest.json.tmp, fsync each, then os.replace each shard into place and the manifest last. POSIX rename within the same filesystem is atomic, so each file is either fully new or fully old. That guarantee is per file, not per checkpoint: save_sharded does one os.replace per shard and then one for the manifest, all into the same directory. A crash before the first os.replace leaves only .tmp files and the previous checkpoint untouched. A crash partway through the replaces leaves a mix, and the load checks catch it: a missing manifest if the directory is new, or a sha256 mismatch on the first replaced shard if the directory held an older checkpoint. Writing each step into its own directory (step_0100) keeps a fresh save from ever overwriting the live one. Without atomic write, a crash can leave a partial shard next to a manifest that points at it, and the load corrupts the optimiser state on resume.

Three failure modes the schema must defend against

FailureSymptomDefence
World-size changeresume on N=8 with manifest from N=4world_size mismatch in manifest, fail loudly
Shard count mismatchmanifest lists a different number of shards than world_size, repeats or skips a rank, or names a shard file that is not on diskcheck len(shards) == world_size, every rank unique and in range, and every listed file exists
Partial writeshard file truncated mid-flushsha256 verification on load

Each defence rejects the bad load early. The alternative is silent corruption that surfaces 100 steps later when loss goes to NaN.

The loader also rejects a schema_version it does not recognise, and any shard path that is absolute, contains a separator, or resolves outside the checkpoint directory, so a tampered manifest cannot point the loader at another file. Shards are read with torch.load(..., weights_only=False), which unpickles arbitrary objects. The sha256 is stored in the manifest next to the data it covers, so it detects corruption, not a malicious writer. Only load checkpoints you wrote.

Why per-rank files, not one big file

Concurrent writes to one file via O_APPEND work on POSIX for byte-aligned writes, but in practice the offsets within one shard span MB-sized regions and locking dominates. Per-rank files have no contention and benefit from striping when the underlying filesystem is parallel (Lustre, GPFS). Production stacks such as DeepSpeed, FSDP, and NeMo use per-rank files for that reason.

Check: The Concept0/24
1. What is a sharded checkpoint?Define
2. What is a manifest in this checkpoint design?Define
3. What is an atomic write?Define
4. What is a partial write?Define
5. What do param_shard_offset and param_shard_numel record for a shard?Define
6. Why write each shard to <name>.tmp and rename instead of writing the final name directly?Explain
7. What does the per-shard sha256 defend against?Explain
8. Why does this lesson prefer per-rank files over one shared file written with O_APPEND?Explain
9. In a new directory, what does renaming the manifest last guarantee if a crash lands between the shard renames?Explain
10. Why does the manifest record world_size?Explain
11. Why does the sha256 in the manifest not protect against a malicious writer?Explain
12. Why is the atomic rename alone not enough to protect a whole checkpoint?Explain
13. You saved a checkpoint on 4 ranks and the scheduler restarts the job on 8. What should load_sharded do?Apply
14. A manifest lists 4 shards, but rank2.bin was deleted from the directory. What happens on load?Apply
15. save_sharded replaced rank0.bin and rank1.bin in a directory holding an older checkpoint, then crashed before replacing the manifest. What does the next load do?Apply
16. The demo saves 4 ranks whose param_shard each holds 1024 elements. What is rank 3's param_shard_offset in the manifest?Apply
17. A manifest with schema_version 2 is loaded by code whose SCHEMA_VERSION is 1. World size and shard count match. What happens?Apply
18. A tampered manifest sets one shard's path to ../elsewhere/rank1.bin. What does load_sharded do with it?Apply
19. A 4-rank manifest lists four shard entries, but two of them both say rank 0. The world_size field is 4. What does load_sharded do?Apply
20. A crash hits after every .tmp file is written but before the first os.replace, in a directory that holds a complete checkpoint. What is the state afterwards?Apply
21. A team writes every checkpoint into one directory, and a crash lands between two shard replaces. Which lesson practice would have kept an intact checkpoint available?Analyze
22. A loader skips sha256 verification and a shard on disk was damaged mid-write. By the lesson's account of what the defences prevent, what is the likely symptom?Analyze
23. rank1.bin is replaced by a well-formed shard file from a different run, and the manifest is left unchanged. Which check rejects the load?Analyze
24. A job saved on 4 ranks must resume on 8. Using the lesson's manifest, which approach fits?Design

Build It

code/main.py implements:

  • ShardEntry and ShardManifest dataclasses with the schema above plus to_json/from_json.
  • save_sharded(per_rank_state, dest_dir, step, wall_clock_seconds=0.0) that writes each rank's binary state to its own .tmp file, then renames the shards and the manifest into place, manifest last.
  • load_sharded(src_dir, expected_world_size) that validates the manifest, verifies each shard's sha256, and returns the manifest and the per-rank state dicts.
  • rotate_checkpoints(parent_dir, keep_last=5) that deletes the oldest step_* directories.
  • A round-trip test: build per-rank state, save, load, and torch.equal each rank's param_shard. The m_shard and v_shard optimiser tensors are saved and loaded with it but not compared.

Run it:

bash
python3 code/main.py

The demo runs four steps in order: write 4 shards plus a manifest and reload them with the parameter shards checked equal, reload with the wrong world size (8) and watch it get rejected, append bytes to rank0.bin and watch the sha256 check reject it, then write 8 more checkpoints (step_0000 to step_0007) next to step_0100 and rotate down to the newest 5. That makes nine directories, so four are deleted: step_0100, step_0000, step_0001 and step_0002.

Check: Build It0/4
1. A 4-rank manifest is intact and rank0.bin was also corrupted. load_sharded is called with expected_world_size=8. Which error is raised?Apply
2. The demo writes step_0100, then step_0000 to step_0007 in order, then calls rotate_checkpoints(keep_last=5), which orders by mtime. How many directories does it delete?Apply
3. The demo appends the bytes b"corruption" to rank0.bin and then calls load_sharded with the correct world size. What is raised?Apply
4. You are reviewing the demo before trusting it for a real resume. Its round-trip asserts torch.equal on param_shard only. What should the review ask for?Analyze

Production patterns in the wild

Three patterns harden the checkpoint enough to ship.

Async write. Production stacks issue the checkpoint write on a separate thread or process so training continues. The barrier sits at the next checkpoint: the next save must wait until the previous one is complete. DeepSpeed's async_io flag handles this. code/main.py keeps the write synchronous so the steps are visible.

Local fast disk first, then async upload. Write to local NVMe, which is fast, then async-upload to S3 or GCS. The two tiers keep the in-cluster checkpoint quick to resume from while a durable copy ships off-cluster for archive. The manifest carries the local path, and an upload manifest carries the remote path.

Rotation matters. Production runs keep the last K checkpoints (typically 3-5) and rotate the oldest out. Without rotation the disk fills mid-run and the next checkpoint fails. With rotation, old checkpoints get deleted and the budget frees up. The demo writes all eight checkpoints and then rotates, so it briefly holds nine directories against K=5. Rotating after every save would peak at K+1, and exercise 2 moves the rotation before the save.

Check: Production patterns in the wild0/5
1. What is rotation in checkpointing?Define
2. What is the barrier condition for async checkpoint writes?Explain
3. How does the two-tier storage pattern split checkpoint responsibilities?Explain
4. A run saves every 30 minutes with no rotation, and at hour 20 a save fails because the disk is full. What is the fix?Analyze
5. You must save every 10 minutes without stalling training, resume quickly in-cluster, and keep a durable off-cluster copy. Which design fits?Design

Use It

Where the pattern shows up:

  • DeepSpeed checkpointing. deepspeed.save_checkpoint(tag=step) writes per-rank files and a latest file pointing at the active tag.
  • PyTorch FSDP checkpointing. torch.distributed.checkpoint saves sharded state with a Planner that decides the per-rank layout.
  • NeMo. Wraps DeepSpeed and FSDP behind a uniform save_to_checkpoint API that adds metadata.

Ship It

Lesson 81 saves a sharded checkpoint of the end-to-end DDP+ZeRO run and reloads it on the same world size to show that the resume contract holds. This lesson's main.py stops at the reload: no optimiser step runs after the load, so the optimiser shards are restored but never exercised.

Check: Ship It0/1
1. Why does the lesson say the optimiser shards are restored but never exercised by main.py?Explain

Exercises

  1. Add async write: kick off the save in a thread and let training continue. Block the next save until the previous one completes.
  2. Extend the rotation in main.py. rotate_checkpoints(parent_dir, keep_last=5) already exists and main() calls it after writing eight checkpoints, ordering them by directory mtime. Make it run before the new save with keep_last set to K-1, so disk use peaks at K checkpoints mid-write instead of K+1, and order by the step in each manifest instead of mtime.
  3. Add a CRC-only fast verification path for the inner-loop reload (rotation rolls a checkpoint into being the new active one without full sha256).
  4. Add a cross-world-size load: shard rebalance from N=4 to N=8 by reading the manifest, concatenating, and re-sharding.
  5. Add an upload to a fake S3 (a second directory) and write the upload manifest. Defend the two-tier storage policy.

Key Terms

TermWhat people sayWhat it actually means
Sharded checkpoint"Per-rank save"Each rank writes its own shard file in parallel
Manifest"Index"JSON file recording shard paths, offsets, and sha256
Atomic write"tmp then rename"Write to .tmp then POSIX rename so a crash leaves the previous file live
Partial write"Truncated shard"A crash during write produces a corrupt shard; sha256 catches it
Rotation"Keep last K"Delete all but the newest K checkpoint directories to bound disk usage
Author's Note

I'd treat the manifest as the part to get right first. The shard files are opaque bytes, and load_sharded checks world_size, schema_version, and the shard count before it deserializes anything, then checks each shard's sha256 before it deserializes that shard. I'd rather fail on a bad manifest or a bad hash than spend a little less time on resume.

I'd also be precise about what the rename buys you. os.replace is atomic for one file at a time, and save_sharded renames the shards first and the manifest last. A directory where those renames did not all land gets caught by the load checks: a missing manifest if the directory is new, a sha256 mismatch if it held an older checkpoint. I wouldn't ship the rename pattern without the hash.

The demo's round-trip asserts equality on param_shard alone, and each rank's state also carries m_shard and v_shard. Before trusting a resume, I'd extend the check to those optimiser shards.

Further Reading

Code