Skip to content

Commit 05967a8

Browse files
macandro96Anish MahishiclaudeAnish Mahishiterrykong
authored
fix(grpo): ReplayBuffer checkpointing and fix reservation leaks (#2651)
Signed-off-by: Anish Mahishi <amahishi@cw-dfw-cs-001-vscode-02.cm.cluster> Signed-off-by: Anish Mahishi <amahishi@cw-dfw-cs-001-vscode-01.cm.cluster> Signed-off-by: Anish Mahishi <20884035+macandro96@users.noreply.github.com> Co-authored-by: Anish Mahishi <amahishi@cw-dfw-cs-001-vscode-02.cm.cluster> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Anish Mahishi <amahishi@cw-dfw-cs-001-vscode-01.cm.cluster> Co-authored-by: Terry Kong <terryk@nvidia.com>
1 parent 04fc137 commit 05967a8

9 files changed

Lines changed: 1538 additions & 90 deletions

File tree

docs/guides/async-grpo.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,31 @@ sequenceDiagram
151151
end
152152
```
153153

154+
## Checkpointing
155+
156+
Async GRPO checkpoints the replay buffer alongside the rest of training state so that in-progress trajectory generation is not lost across restarts.
157+
158+
### What is saved
159+
160+
On each checkpoint, a `replay_buffer.pt` file is written next to the other checkpoint artifacts. It contains all trajectories currently in the buffer together with their weight and target versions, and the `last_target_weight_already_generated` watermark.
161+
162+
### Restore behaviour
163+
164+
On resume, the buffer is restored before the trajectory collector starts, then cleaned up as follows:
165+
166+
1. **Past targets dropped** — trajectories whose target step is earlier than the resume step are removed.
167+
2. **Stale trajectories evicted** — if `max_trajectory_age_steps` is set, trajectories too old for their target step are removed.
168+
3. **Incomplete targets kept** — target steps that still lack a full batch are kept in the buffer. The collector will *gap-fill* only the missing trajectories for those targets before moving on.
169+
4. **Buffer truncated** — if the restored count exceeds `max_size`, the buffer is truncated, prioritising entries closest to the resume step.
170+
171+
### Gap-filling after restore
172+
173+
After a restore, `last_target_weight_already_generated` is reset to `current_training_step - 1` so the collector re-evaluates every target from the resume step onward. For each target it queries `get_trajectories_needed` and spawns only the workers required to complete the batch — previously buffered trajectories are reused and the collector does not regenerate them.
174+
175+
### Disabling replay-buffer restore
176+
177+
If no `replay_buffer.pt` file is found in the latest checkpoint directory, training starts with an empty buffer and waits for the collector to fill it before the first training step.
178+
154179
## Usage Tips
155180

156181
1. **Buffer Sizing**: The replay buffer size is automatically calculated as:

nemo_rl/algorithms/async_utils/interfaces.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import Any, Optional, Protocol
1616

1717

18-
class ReplayBufferProtocol(Protocol):
18+
class ReplayBufferProtocol(Protocol): # pragma: no cover
1919
"""Interface for the replay buffer used in async RL training."""
2020

2121
def add(
@@ -58,3 +58,35 @@ def size(self) -> int:
5858
def clear(self) -> None:
5959
"""Clear the buffer."""
6060
...
61+
62+
def state_dict(self) -> dict[str, Any]:
63+
"""Return serializable state for checkpointing."""
64+
...
65+
66+
def load_state_dict(
67+
self,
68+
state: dict[str, Any],
69+
num_prompts_per_step: int | None = None,
70+
current_training_step: int | None = None,
71+
max_age_steps: int | None = None,
72+
) -> None:
73+
"""Restore state produced by ``state_dict``."""
74+
...
75+
76+
def get_trajectories_needed(
77+
self,
78+
target_step: int,
79+
num_prompts_per_step: int,
80+
max_age_steps: int | None = None,
81+
) -> int:
82+
"""Return additional trajectories needed for ``target_step``."""
83+
...
84+
85+
def has_complete_batch(
86+
self,
87+
target_step: int,
88+
num_prompts_per_step: int,
89+
max_age_steps: int | None = None,
90+
) -> bool:
91+
"""Return whether ``target_step`` has enough trajectories to train."""
92+
...

0 commit comments

Comments
 (0)