Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/training.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,27 @@ uv run rl @ rl.toml --ckpt.interval 25 --ckpt.keep-last 3 # rolling window of 3
uv run rl @ rl.toml --ckpt.interval 25 --ckpt.keep-interval 100 # …plus permanent every 100
```

### Asynchronous Checkpoints

By default a trainer step waits until its checkpoint is written to storage. With `--ckpt.async-save` (`--trainer.ckpt.async-save` also works for `rl`), a save only copies each rank's model and optimizer state into pinned host memory. A background thread then writes the copy to storage while training continues:

```bash
uv run rl @ rl.toml --ckpt.interval 2 --ckpt.async-save
uv run sft @ sft.toml --ckpt.interval 2 --ckpt.async-save
```

The checkpoint holds the same state that a synchronous save at that step would write. It works with every `model.offload` mode:

- With `model.offload = "none"`, the copy from GPU memory runs alongside the next step's forward and backward passes. The trainer waits for it to finish before the optimizer step changes the state. Until then, any temporary GPU tensors the checkpoint is built from, such as gathered entries of fused parameters, stay allocated.
- With `optimizer` or `full` offload, the save copies the CPU optimizer state and returns once the copy is done, because offloaded state changes outside the optimizer step. The copy reads the state where it lives, without moving it through the GPU as a synchronous save does.

Some things to know:

- **Host memory.** Each rank keeps pinned host buffers about the size of its checkpoint shard (model plus optimizer state), and reuses them for every save. On a node, this adds up to the node's share of the full checkpoint. With optimizer offload, this comes on top of the offloaded state itself.
- **One save at a time.** A save first waits for the previous write to finish. If the checkpoint interval is shorter than a write takes, the trainer waits for the write.
- **Completeness.** DCP writes `trainer/.metadata` after every rank has written its shards. A resume skips any step whose trainer checkpoint has no `.metadata`, for example one that was still being written when the job died. `keep_last` and `keep_interval` only count checkpoints that finished writing, so cleanup never deletes the newest finished checkpoint to make room for one that is still in flight.
- **Failures.** If a background write fails, the trainer raises its error at the next optimizer step, the next save or the end of training, and stops. The final checkpoint, and any checkpoint requested through the control API, is written before the trainer exits or reports it as saved.

### Resuming a Run

Re-run the same launch command and pass `--resume` (latest checkpoint) or `--resume.step <N>`. Resuming reuses the run directory, so the run needs a name you can point back at — launch with `--run.name` (or pass the first run's auto-generated name). Make sure `--max-steps` is at least the target final step, not the remaining delta:
Expand Down
3 changes: 3 additions & 0 deletions packages/prime-rl-configs/src/prime_rl/configs/rl.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ class SharedCheckpointConfig(BaseConfig):
keep_interval: int | None = Field(None, ge=1)
"""Keep checkpoints at every N steps permanently (e.g. ``keep_interval=100`` keeps step 100, 200, ...). If None, no interval-based keeping."""

async_save: bool | None = None
"""Write trainer checkpoints in the background (see ``trainer.ckpt.async_save``). If None, uses the trainer's default."""


class SharedModelConfig(BaseConfig):
name: str = "Qwen/Qwen3-0.6B"
Expand Down
7 changes: 7 additions & 0 deletions packages/prime-rl-configs/src/prime_rl/configs/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,13 @@ class CheckpointConfig(BaseConfig):
skip_optimizer: bool = False
"""Skip loading the optimizer state from checkpoint."""

async_save: bool = False
"""Write checkpoints in the background. A save copies this rank's model and optimizer state into pinned host
buffers and returns; a background thread writes the copy while training continues. The buffers are kept for
the next save, so this costs pinned host memory about the size of the rank's checkpoint shard. Without
optimizer offload the copy also overlaps the next step, until its optimizer step. A save first waits for the
previous write to finish."""


class IPOLossConfig(BaseConfig):
type: Literal["ipo"] = "ipo"
Expand Down
1 change: 1 addition & 0 deletions packages/prime-rl-configs/src/prime_rl/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def propagate(shared_path: str, *targets: str) -> None:
propagate("ckpt.interval", "trainer.ckpt.interval", "orchestrator.ckpt.interval")
propagate("ckpt.keep_last", "trainer.ckpt.keep_last")
propagate("ckpt.keep_interval", "trainer.ckpt.keep_interval")
propagate("ckpt.async_save", "trainer.ckpt.async_save")

# [monitors.wandb] leaves. (Bare empty ``[monitors.wandb]`` block enablement is at the end.)
# ``monitors.wandb.name`` flows verbatim to both sub-configs — shared W&B mode is
Expand Down
Loading
Loading