Thomas Piacentini
← Atelier

Fiche

Training Memory, Accumulation and Checkpoints

Texte original · anglais

Fix NaN steps, memory limits, accumulation scale and broken resumes. Clip, AMP, recompute, atomic checkpoints.

Training symptoms

SymptomCauseFix
Loss goes vertical after one batchGradient norm spike, no clipclip_grad_norm_ at max_norm = 1.0
Every weight is NaN after a stepFP16 overflow gives Inf, then NaNtorch.amp.GradScaler skips the step
Scaling factor never changesNo scaler.update() on the skip pathCall scaler.update() every step
Clip acts on scaled gradientsClip runs before unscaleCall scaler.unscale_(optimizer) first
Hundreds of skips per epochModel is past the FP16 rangeAlert on a skip rate above 5 percent
Optimizer step 16 times too bigMicro-batch loss not dividedDivide the loss by accum_steps
Resumed loss curve differsNo RNG state in the checkpointSave capture_rng_state() output
Activations do not fit in memoryBackprop keeps every activationtorch.utils.checkpoint.checkpoint(fn, input)
Sources

Checkpoint load failures

SymptomCauseFix
world_size mismatchResume on a different world sizeResume on the same world size
sha256 mismatch on rankShard truncated mid-flushWrite to .tmp, fsync, then rename
shard file missingFewer rank files than manifest shardsEnumerate shards, verify each exists
unknown schemaPayload schema does not start with ckptBranch migrations on the schema string
Loss goes to NaN 100 steps after resumePartial shard behind a present manifestAtomic write plus sha256 on load
Disk fills mid-runNo rotationrotate_checkpoints keeps the last K
Sources

Safe AMP step

optimizer.zero_grad(set_to_none=True)grads to None
with torch.amp.autocast(device_type="cuda"):FP16 forward
loss = loss_fn(model(inputs), targets)
if not torch.isfinite(loss).all().item():check before backward
return skip("non_finite_loss")no scaler.update here
scaler.scale(loss).backward()scaled backward
scaler.unscale_(optimizer)unscale before the clip
if has_non_finite_grad(model.parameters()):
scaler.update()halves the factor
return skip("non_finite_grad")
pre, post = clip_global_l2_norm(params, max_norm)
scaler.step(optimizer)
scaler.update()doubles or halves
Sources

Accumulate, sync on the last

accum_steps = len(micro_batches)
zero_grads(model)
for i, (x, y) in enumerate(micro_batches):
is_last = i == accum_steps - 1
if no_sync_until_last and not is_last:
with no_sync_context(model):DDP skips the all-reduce
loss = loss_scaled_for_accum(model(x), y, accum_steps, loss_fn)
loss.backward()grads add into param.grad
else:
loss = loss_scaled_for_accum(model(x), y, accum_steps, loss_fn)
loss.backward()one sync per effective step
optimizer.step()once per effective batch
Sources

Tasks in code

TaskCode
Clip to a global L2 normclip_grad_norm_(parameters, max_norm)
Log the scaling factorscaler.get_scale()
Scale a micro-batch lossloss_scaled_for_accum(logits, target, accum_steps, loss_fn)
Recompute a segment in backwardcheckpoint(fn, input)
Save the full state atomicallysave_checkpoint(model, optimizer, scheduler, state, out_path)
Load an untrusted filetorch.load(path, map_location="cpu", weights_only=True)
Save and load per-rank shardssave_sharded(per_rank_state, dest_dir, step)
Resume and verify every shardload_sharded(src_dir, expected_world_size)
Sources

Sharded atomic save

sha256sha256resumerank 0 stateparam and optimiser shardgpurank 1 stateone file per rankgpurank0.bin.tmpwrite, flush, fsyncstorerank1.bin.tmpwrite, flush, fsyncstoremanifest.json.tmpworld_size, step, shardsstoreos.replaceshards first, manifestlastprocessload_shardedsha256 check per shardprocess
Nodes and connections
NodeRoleKind
rank 0 stateparam and optimiser shardgpu
rank 1 stateone file per rankgpu
rank0.bin.tmpwrite, flush, fsyncstore
rank1.bin.tmpwrite, flush, fsyncstore
manifest.json.tmpworld_size, step, shardsstore
os.replaceshards first, manifest lastprocess
load_shardedsha256 check per shardprocess
FromToConnection
rank 0 staterank0.bin.tmp
rank 1 staterank1.bin.tmp
rank0.bin.tmpmanifest.json.tmpsha256
rank1.bin.tmpmanifest.json.tmpsha256
manifest.json.tmpos.replace
os.replaceload_shardedresume
Sources

Save and resume mid epoch

  1. save_checkpoint packs model, optimizer, scheduler, counters and RNG.
  2. atomic_save writes a temp file, then calls os.replace.
  3. load_checkpoint calls torch.load with weights_only=True.
  4. restore_rng_state restores Python, NumPy and torch RNG.
  5. Training loop continues from batch_in_epoch. Loss diff under 1e-4.
Sources

Run the lessons

CommandWhat it does
python3 code/main.pyLesson 45: 20 steps, rows tagged STEP or SKIP
--num-steps 100Lesson 46: longer sweep for the curve
--accum-grid 1,2,4,8,16Lesson 46: accumulation steps to sweep
--total-steps 24 --interrupt-at 10Lesson 47: resume demo window
python3 code/main.pyLesson 80: 4 shards plus manifest, byte-equal reload
python3 code/main.pyLesson 81: 20 steps, 4 ranks, RESUME VERIFIED
Sources

Defaults in the code

SettingDefaultNote
DEFAULT_MAX_NORM1.0Clip threshold of AmpTrainState
amp_dtype on cputorch.bfloat16torch.float16 on cuda
window1000Steps in rolling_skip_rate
k4Layers per checkpoint segment
attention_fraction0.15Recompute share, selective mode
keep_last5Checkpoints that rotate_checkpoints keeps
CHECKPOINT_SCHEMAckpt.v1Schema string in the payload
CHECKPOINT_STEP10Sharded save step of lesson 81
Sources

Memory and cost numbers

Activations per layer
about 12 * B * L * d floats.
d=8192, L=8192, B=1
about 1.6 GB per layer in BF16. 64 layers is about 103 GB.
Full checkpointing
about 33% more FLOPs. A step is 4 units, not 3.
Selective checkpointing
recompute attention only. About 5% overhead.
sqrt(L) rule
one checkpoint every sqrt(L) layers. 8 checkpoints for L=64.
Effective batch
micro_batch * accum_steps, times the data parallel world size.
ZeRO-1 optimiser memory
12P/N bytes per rank.
Sources

Q&A

Why does the CPU GradScaler do nothing?
CPU autocast uses BF16. It does not need loss scaling.
When does the factor change?
It doubles after clean stretches. It halves on every skip.
Does accumulation save time?
No. Double accum_steps and the wall time per step doubles.
Why weights_only=True?
A .pt file is a pickle. Unpickling runs code. Use torch 2.6 or newer.
Why one file per rank?
Write bandwidth scales. 4 hours on one rank is 4 minutes on 64.
Sources

Tips

  • Match the autocast device to the optimizer device.
  • Raise ValueError, not assert. python -O strips asserts.
  • On a loss jump above 2x, roll back to the previous checkpoint.
Sources
Original source
---
id: training-memory-checkpoints
title: Training Memory, Accumulation and Checkpoints
code: TM
subtitle: Fix NaN steps, memory limits, accumulation scale and broken resumes. Clip, AMP, recompute, atomic checkpoints.
hue: 20
sources: phases/10-llms-from-scratch/34-gradient-checkpointing, phases/19-capstone-projects/45-gradient-clipping-amp, phases/19-capstone-projects/46-gradient-accumulation, phases/19-capstone-projects/47-checkpoint-save-resume, phases/19-capstone-projects/80-checkpoint-sharded-resume, phases/19-capstone-projects/81-end-to-end-distributed-train
source_hash: 0ea0fe65eca26d48
---

## Training symptoms
type: troubleshooting
from: phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md, phases/19-capstone-projects/47-checkpoint-save-resume/docs/en.md, phases/10-llms-from-scratch/34-gradient-checkpointing/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| Loss goes vertical after one batch | Gradient norm spike, no clip | `clip_grad_norm_` at `max_norm = 1.0` |
| Every weight is NaN after a step | FP16 overflow gives Inf, then NaN | `torch.amp.GradScaler` skips the step |
| Scaling factor never changes | No `scaler.update()` on the skip path | Call `scaler.update()` every step |
| Clip acts on scaled gradients | Clip runs before unscale | Call `scaler.unscale_(optimizer)` first |
| Hundreds of skips per epoch | Model is past the FP16 range | Alert on a skip rate above 5 percent |
| Optimizer step 16 times too big | Micro-batch loss not divided | Divide the loss by `accum_steps` |
| Resumed loss curve differs | No RNG state in the checkpoint | Save `capture_rng_state()` output |
| Activations do not fit in memory | Backprop keeps every activation | `torch.utils.checkpoint.checkpoint(fn, input)` |

## Checkpoint load failures
type: troubleshooting
from: phases/19-capstone-projects/80-checkpoint-sharded-resume/docs/en.md, phases/19-capstone-projects/80-checkpoint-sharded-resume/code/main.py, phases/19-capstone-projects/47-checkpoint-save-resume/code/main.py, phases/19-capstone-projects/47-checkpoint-save-resume/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| `world_size mismatch` | Resume on a different world size | Resume on the same world size |
| `sha256 mismatch on rank` | Shard truncated mid-flush | Write to .tmp, fsync, then rename |
| `shard file missing` | Fewer rank files than manifest shards | Enumerate shards, verify each exists |
| `unknown schema` | Payload schema does not start with ckpt | Branch migrations on the schema string |
| Loss goes to NaN 100 steps after resume | Partial shard behind a present manifest | Atomic write plus sha256 on load |
| Disk fills mid-run | No rotation | `rotate_checkpoints` keeps the last K |

## Safe AMP step
type: annotated-code
lang: python
from: phases/19-capstone-projects/45-gradient-clipping-amp/code/main.py, phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md

```python
optimizer.zero_grad(set_to_none=True)  # grads to None
with torch.amp.autocast(device_type="cuda"):  # FP16 forward
    loss = loss_fn(model(inputs), targets)
if not torch.isfinite(loss).all().item():  # check before backward
    return skip("non_finite_loss")  # no scaler.update here
scaler.scale(loss).backward()  # scaled backward
scaler.unscale_(optimizer)  # unscale before the clip
if has_non_finite_grad(model.parameters()):
    scaler.update()  # halves the factor
    return skip("non_finite_grad")
pre, post = clip_global_l2_norm(params, max_norm)
scaler.step(optimizer)
scaler.update()  # doubles or halves
```

## Accumulate, sync on the last
type: annotated-code
lang: python
from: phases/19-capstone-projects/46-gradient-accumulation/code/main.py, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md

```python
accum_steps = len(micro_batches)
zero_grads(model)
for i, (x, y) in enumerate(micro_batches):
    is_last = i == accum_steps - 1
    if no_sync_until_last and not is_last:
        with no_sync_context(model):  # DDP skips the all-reduce
            loss = loss_scaled_for_accum(model(x), y, accum_steps, loss_fn)
            loss.backward()  # grads add into param.grad
    else:
        loss = loss_scaled_for_accum(model(x), y, accum_steps, loss_fn)
        loss.backward()  # one sync per effective step
optimizer.step()  # once per effective batch
```

## Tasks in code
type: task-table
span: 2
from: phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/45-gradient-clipping-amp/code/main.py, phases/19-capstone-projects/46-gradient-accumulation/code/main.py, phases/10-llms-from-scratch/34-gradient-checkpointing/docs/en.md, phases/19-capstone-projects/47-checkpoint-save-resume/code/main.py, phases/19-capstone-projects/80-checkpoint-sharded-resume/code/main.py

| Task | Code |
|---|---|
| Clip to a global L2 norm | `clip_grad_norm_(parameters, max_norm)` |
| Log the scaling factor | `scaler.get_scale()` |
| Scale a micro-batch loss | `loss_scaled_for_accum(logits, target, accum_steps, loss_fn)` |
| Recompute a segment in backward | `checkpoint(fn, input)` |
| Save the full state atomically | `save_checkpoint(model, optimizer, scheduler, state, out_path)` |
| Load an untrusted file | `torch.load(path, map_location="cpu", weights_only=True)` |
| Save and load per-rank shards | `save_sharded(per_rank_state, dest_dir, step)` |
| Resume and verify every shard | `load_sharded(src_dir, expected_world_size)` |

## Sharded atomic save
type: architecture
from: phases/19-capstone-projects/80-checkpoint-sharded-resume/docs/en.md, phases/19-capstone-projects/80-checkpoint-sharded-resume/code/main.py

```arch
node | s0 | 1,1 | rank 0 state | param and optimiser shard | gpu
node | s1 | 1,2 | rank 1 state | one file per rank | gpu
node | t0 | 2,1 | rank0.bin.tmp | write, flush, fsync | store
node | t1 | 2,2 | rank1.bin.tmp | write, flush, fsync | store
node | man | 3,1 | manifest.json.tmp | world_size, step, shards | store
node | ren | 3,2 | os.replace | shards first, manifest last | process
node | load | 4,1 | load_sharded | sha256 check per shard | process
edge | s0 | t0 |
edge | s1 | t1 |
edge | t0 | man | sha256
edge | t1 | man | sha256
edge | man | ren |
edge | ren | load | resume
```

## Save and resume mid epoch
type: flow
from: phases/19-capstone-projects/47-checkpoint-save-resume/docs/en.md, phases/19-capstone-projects/47-checkpoint-save-resume/code/main.py

1. **save_checkpoint**: packs model, optimizer, scheduler, counters and RNG.
2. **atomic_save**: writes a temp file, then calls `os.replace`.
3. **load_checkpoint**: calls `torch.load` with `weights_only=True`.
4. **restore_rng_state**: restores Python, NumPy and torch RNG.
5. **Training loop**: continues from `batch_in_epoch`. Loss diff under 1e-4.

## Run the lessons
type: command-table
from: phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/code/main.py, phases/19-capstone-projects/47-checkpoint-save-resume/code/main.py, phases/19-capstone-projects/80-checkpoint-sharded-resume/docs/en.md, phases/19-capstone-projects/81-end-to-end-distributed-train/docs/en.md

| Command | What it does |
|---|---|
| `python3 code/main.py` | Lesson 45: 20 steps, rows tagged STEP or SKIP |
| `--num-steps 100` | Lesson 46: longer sweep for the curve |
| `--accum-grid 1,2,4,8,16` | Lesson 46: accumulation steps to sweep |
| `--total-steps 24 --interrupt-at 10` | Lesson 47: resume demo window |
| `python3 code/main.py` | Lesson 80: 4 shards plus manifest, byte-equal reload |
| `python3 code/main.py` | Lesson 81: 20 steps, 4 ranks, RESUME VERIFIED |

## Defaults in the code
type: defaults
from: phases/19-capstone-projects/45-gradient-clipping-amp/code/main.py, phases/10-llms-from-scratch/34-gradient-checkpointing/code/main.py, phases/19-capstone-projects/80-checkpoint-sharded-resume/code/main.py, phases/19-capstone-projects/47-checkpoint-save-resume/code/main.py, phases/19-capstone-projects/81-end-to-end-distributed-train/code/main.py

| Setting | Default | Note |
|---|---|---|
| `DEFAULT_MAX_NORM` | 1.0 | Clip threshold of `AmpTrainState` |
| `amp_dtype` on cpu | `torch.bfloat16` | `torch.float16` on cuda |
| `window` | 1000 | Steps in `rolling_skip_rate` |
| `k` | 4 | Layers per checkpoint segment |
| `attention_fraction` | 0.15 | Recompute share, selective mode |
| `keep_last` | 5 | Checkpoints that `rotate_checkpoints` keeps |
| `CHECKPOINT_SCHEMA` | ckpt.v1 | Schema string in the payload |
| `CHECKPOINT_STEP` | 10 | Sharded save step of lesson 81 |

## Memory and cost numbers
type: concepts
from: phases/10-llms-from-scratch/34-gradient-checkpointing/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md, phases/19-capstone-projects/81-end-to-end-distributed-train/docs/en.md

- **Activations per layer**: about `12 * B * L * d` floats.
- **d=8192, L=8192, B=1**: about 1.6 GB per layer in BF16. 64 layers is about 103 GB.
- **Full checkpointing**: about 33% more FLOPs. A step is 4 units, not 3.
- **Selective checkpointing**: recompute attention only. About 5% overhead.
- **sqrt(L) rule**: one checkpoint every sqrt(L) layers. 8 checkpoints for L=64.
- **Effective batch**: `micro_batch * accum_steps`, times the data parallel world size.
- **ZeRO-1 optimiser memory**: 12P/N bytes per rank.

## Q&A
type: qa
from: phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/46-gradient-accumulation/docs/en.md, phases/19-capstone-projects/47-checkpoint-save-resume/docs/en.md, phases/19-capstone-projects/80-checkpoint-sharded-resume/docs/en.md, phases/10-llms-from-scratch/34-gradient-checkpointing/docs/en.md

- **Why does the CPU GradScaler do nothing?** CPU autocast uses BF16. It does not need loss scaling.
- **When does the factor change?** It doubles after clean stretches. It halves on every skip.
- **Does accumulation save time?** No. Double `accum_steps` and the wall time per step doubles.
- **Why `weights_only=True`?** A `.pt` file is a pickle. Unpickling runs code. Use torch 2.6 or newer.
- **Why one file per rank?** Write bandwidth scales. 4 hours on one rank is 4 minutes on 64.

## Tips
type: tips
from: phases/19-capstone-projects/45-gradient-clipping-amp/docs/en.md, phases/19-capstone-projects/47-checkpoint-save-resume/docs/en.md, phases/19-capstone-projects/81-end-to-end-distributed-train/docs/en.md

- Match the autocast device to the optimizer device.
- Raise `ValueError`, not assert. `python -O` strips asserts.
- On a loss jump above 2x, roll back to the previous checkpoint.
Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓