Skip to content
Merged
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
5 changes: 5 additions & 0 deletions docs/about/algorithms/ppo.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ppo:
gae_lambda: 0.95
gae_gamma: 1.0
ppo_epochs: 4
critic_ppo_epochs: ${ppo.ppo_epochs}
policy_training_start_step: 0

value_loss_fn:
Expand All @@ -79,6 +80,10 @@ value:
model_name: "Qwen/Qwen2.5-1.5B"
```

`ppo_epochs` and `critic_ppo_epochs` are independent positive integers. The
example uses interpolation so the critic follows the actor epoch count unless
you explicitly override it.

## Additional Resources

- [PPO Paper](https://arxiv.org/abs/1707.06347)
Expand Down
22 changes: 14 additions & 8 deletions docs/guides/ppo.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ When only one node remains for policy and generation after other resources are r

### Asynchronous PPO
Comment thread
yuki-97 marked this conversation as resolved.

Set `ppo.async_ppo.enabled: true` to overlap rollout generation with training. A background collector fills a replay buffer on the non-colocated vLLM GPUs while the policy and value model train on their shared cluster. Values and policy/reference log probabilities are recomputed when a trajectory is sampled, then PPO runs GAE and its normal `ppo_epochs` updates before publishing one new policy version to vLLM.
Set `ppo.async_ppo.enabled: true` to overlap rollout generation with training. A background collector fills a replay buffer on the non-colocated vLLM GPUs while the policy and value model train on their shared cluster. Values and policy/reference log probabilities are recomputed when a trajectory is sampled, then PPO runs GAE, all `critic_ppo_epochs` critic updates, and all `ppo_epochs` policy updates before publishing one new policy version to vLLM.

Async PPO reuses the trajectory collector, replay buffer, and weight-versioning infrastructure described in the [Async GRPO guide](async-grpo.md); this section focuses on PPO-specific behavior and constraints.

Expand Down Expand Up @@ -175,21 +175,25 @@ The PPO training loop, [ppo_train](../../nemo_rl/algorithms/ppo.py), follows thi
3. **Value inference**: the value model predicts per-token state values
4. **Logprob computation**: the policy computes log probabilities for advantage estimation
5. **Advantage estimation**: GAE computes advantages using value predictions and rewards
6. **Value training**: the critic is updated first (critic-before-actor, following [veRL](https://arxiv.org/abs/2412.09613))
7. **Policy training**: the actor is updated with the clipped surrogate objective
6. **Value training**: the critic completes all of its updates first
7. **Policy training**: the actor completes all of its updates with the clipped surrogate objective

Steps 6–7 repeat `ppo_epochs` times per rollout before generating new responses.
The critic stays resident for all `critic_ppo_epochs` updates, then the policy stays resident for all `ppo_epochs` updates. This avoids moving the colocated models between CPU and GPU after every epoch.

### Multiple Training Steps per Rollout

Unlike GRPO, which performs one training update per rollout, PPO can perform multiple training steps on the same batch of rollout data:

```yaml
ppo:
ppo_epochs: 4 # Train 4 times on each rollout batch
ppo_epochs: 4 # actor passes over each rollout batch
critic_ppo_epochs: ${ppo.ppo_epochs} # critic passes; follows actor by default
```

Each step trains both the critic and the actor on the same advantage estimates computed from the initial rollout.
Each pass uses the same returns and advantage estimates computed from the initial
rollout. Both epoch counts must be at least 1 and can be configured independently;
the exemplar uses interpolation so the critic follows the actor unless explicitly
overridden.

### Critic Warmup

Expand Down Expand Up @@ -221,7 +225,7 @@ The path is a `step_<n>` directory holding a `value/` subtree — the layout a P
- `value.megatron_cfg.optimizer.lr` and `.min_lr` — they feed `max_lr`/`min_lr` and are the *first* two fields checked. They live in the optimizer block, not the scheduler block.
- `value.megatron_cfg.scheduler`.
- `value.train_global_batch_size` — it multiplies `lr_decay_steps`, `wd_incr_steps` and `lr_warmup_steps`.
- the tick budget `train_iters`. A synchronous run sets it to `min(max_num_steps, max_num_epochs × len(dataloader)) × ppo_epochs`; an async run sets it to `max_num_steps × ppo_epochs`, since async requires `max_num_epochs: -1`. `len(dataloader)` is prompt batches per epoch, so on a synchronous run the dataset size and `num_prompts_per_step` are part of the budget whenever the epoch term is the smaller one — as it is for the shipped recipes that set `max_num_epochs: 15`. Matching `max_num_steps` and `ppo_epochs` alone is not enough there.
- the tick budget `train_iters`. A synchronous run sets it to `min(max_num_steps, max_num_epochs × len(dataloader)) × critic_ppo_epochs`; an async run sets it to `max_num_steps × critic_ppo_epochs`, since async requires `max_num_epochs: -1`. `len(dataloader)` is prompt batches per epoch, so on a synchronous run the dataset size and `num_prompts_per_step` are part of the budget whenever the epoch term is the smaller one — as it is for the shipped recipes that set `max_num_epochs: 15`. Matching `max_num_steps` and `critic_ppo_epochs` alone is not enough there.

A mismatch fails during critic init. Which field is named depends on which input differs: a batch-size difference reports `warmup iterations`, a learning-rate difference reports `learning rate`.

Expand Down Expand Up @@ -270,6 +274,7 @@ ppo:
max_num_epochs: 100000
max_num_steps: 100000
ppo_epochs: 4
critic_ppo_epochs: ${ppo.ppo_epochs}
Comment thread
RayenTian marked this conversation as resolved.
policy_training_start_step: 0
warm_start_value_checkpoint: null
val_period: 20
Expand Down Expand Up @@ -326,7 +331,8 @@ value_loss_fn:
```

**PPO-specific parameters:**
- **`ppo.ppo_epochs`**: Number of training updates per rollout batch
- **`ppo.ppo_epochs`**: Number of actor training updates per rollout batch
- **`ppo.critic_ppo_epochs`**: Number of critic training updates per rollout batch. It can differ from `ppo_epochs`; the exemplar defaults it to `${ppo.ppo_epochs}`.
- **`ppo.policy_training_start_step`**: Number of critic-only warmup steps before policy training begins
- **`ppo.warm_start_value_checkpoint`**: Checkpoint step directory whose `value/` seeds the critic on a fresh run. See [Warm-Starting the Critic](#warm-starting-the-critic)
- **`ppo.seq_logprob_error_threshold`**: Nullable sequence-level multiplicative probability-error threshold. PPO always logs sequence-level train/generation mismatch metrics; when this is set, sequences above the threshold are excluded from advantage and loss computation.
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/single-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ uv run examples/run_grpo_single_controller.py --config <your-sc.yaml>
gpus_per_node: 1 # inference GPUs; remainder go to training
```

3. **One RL step = one training batch.** The batch a step trains on is the whole step (see `validate_single_controller_config` in [nemo_rl/algorithms/single_controller_utils/config.py](../../nemo_rl/algorithms/single_controller_utils/config.py)). A GRPO step is also one optimizer step; a PPO step is `ppo.ppo_epochs` of them over that same batch.
3. **One RL step = one training batch.** The batch a step trains on is the whole step (see `validate_single_controller_config` in [nemo_rl/algorithms/single_controller_utils/config.py](../../nemo_rl/algorithms/single_controller_utils/config.py)). A GRPO step is also one optimizer step. A PPO step applies `ppo.ppo_epochs` actor updates and `ppo.critic_ppo_epochs` critic updates over that same batch. Both counts must be at least 1 and can be configured independently; the exemplar defaults the critic count to `${ppo.ppo_epochs}`.

```python
num_prompts_per_step * num_generations_per_prompt == policy.train_global_batch_size
Expand Down Expand Up @@ -150,7 +150,7 @@ The shipped exemplars cover three of the five modes:
Field definitions:

- `max_buffered_rollouts` — hard cap on unconsumed rollout groups buffered in the data plane. Validated at setup against the gated sampler's required capacity; a value too small deadlocks the rollout pump, so setup raises instead of silently blocking. Sized from the widest window the run ever uses, so `warmup_lookahead_versions` rather than `max_lookahead_versions` when it is set.
- `min_groups_for_streaming_train` — minimum ready groups the trainer waits for before dispatching a batch. Set to `num_prompts_per_step` for sync/legacy semantics; lower for streaming. (PPO) Must equal `num_prompts_per_step` — the critic has no split train API, so one `train_from_meta` call is one optimizer step, and streaming a step across chunks would step the critic once per chunk.
- `min_groups_for_streaming_train` — minimum ready groups the trainer waits for before dispatching a batch. Set to `num_prompts_per_step` for sync/legacy semantics; lower for streaming. (PPO) Must equal `num_prompts_per_step` — the critic has no split train API, so each critic epoch calls the full-step `train_from_meta` once per chunk. Splitting an RL step across chunks would multiply both models' configured optimizer updates by the number of chunks.
- `sampler.warmup_lookahead_versions` (PPO) — lookahead used while `ppo.policy_training_start_step` critic warmup is in progress, shrinking back to `max_lookahead_versions` afterwards. The SC equivalent of `ppo.async_ppo.warmup_generation_lead_steps`.

## Implementation Structure
Expand Down Expand Up @@ -189,14 +189,14 @@ The SC path splits the async-GRPO loop across a rollout pump and a train pump th
#### 5. `_rollout_pump` and `_train_pump`

- `_rollout_pump`: pulls prompts from the dataloader, calls `sampler.admit`, dispatches `RolloutManager.generate_and_push`, and honours `max_inflight_prompts` as a backpressure cap.
- `_train_pump`: `sampler.evict → sampler.select → _value_stage (PPO only) → _advantage_stage → _value_train (PPO only) → TQPolicy split API (begin_train_step / train_microbatches_from_meta / finish_train_step) → dp_client.clear_samples`.
- `_train_pump`: `sampler.evict → sampler.select → _value_stage (PPO only) → _advantage_stage → _value_train_epochs (PPO only) → TQPolicy split API (begin_train_step / train_microbatches_from_meta / finish_train_step) → dp_client.clear_samples`.

### Coordination Flow

1. **Driver setup**: `setup_single_controller` builds the worker groups, virtual cluster, dp client, dataloader, `TQReplayBuffer`, `RolloutManager`, and weight synchronizer, and packs them into a `SingleControllerActorArgs` that the entrypoint cloudpickles into the actor.
2. **Actor startup**: `SingleControllerActor` launches `_rollout_pump` and `_train_pump` concurrently as asyncio tasks; both share the same `TQReplayBuffer` and `StalenessSampler`.
3. **Rollout pump loop**: `sampler.admit` gates dispatch against the current trainer version (returning a `target_step` for `in_order`); the pump then reserves a buffer slot, drives `RolloutManager.generate_and_push`, and commits with the observed `start_weight` / `end_weight`.
4. **Train pump loop**: `sampler.evict` drops out-of-window groups, `sampler.select` picks the next batch, `_value_stage` and `_value_train` run the critic forward and its optimizer step on a PPO run, `_advantage_stage` computes advantages, and the TQPolicy split API runs one optimizer step per RL step on GRPO, or `ppo.ppo_epochs` of them on PPO.
4. **Train pump loop**: `sampler.evict` drops out-of-window groups and `sampler.select` picks the next batch. On PPO, `_value_stage` runs the critic forward, `_advantage_stage` computes advantages, and `_value_train_epochs` runs `ppo.critic_ppo_epochs` critic updates. The TQPolicy split API then runs one optimizer step per RL step on GRPO, or `ppo.ppo_epochs` policy updates on PPO.
5. **Weight sync**: after each optimizer step the pump bumps the trainer version, clears rollout permission, calls the weight synchronizer, and re-opens the rollout pump for the next version.

## Relation to Legacy Async GRPO
Expand Down
4 changes: 3 additions & 1 deletion examples/configs/ppo_math_1B.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ ppo:
max_rollout_turns: 1
max_num_epochs: 100000
max_num_steps: 100000
ppo_epochs: 4
ppo_epochs: 4 # actor passes over each rollout batch
# Critic passes over each rollout batch; follows ppo_epochs unless overridden.
critic_ppo_epochs: ${ppo.ppo_epochs}
policy_training_start_step: 0 # number of PPO steps of critic-only warmup before policy training begins
# step_<n> dir of a critic-pretrain run whose value/ seeds the critic.
# Only a fresh run reads it; a resume ignores it and restores the critic from
Expand Down
Loading
Loading