Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
cc99a3b
feat(swe_bench): add GB200 (hsg) SWE2 generation-scaling launcher and…
RayenTian Jul 1, 2026
9519e04
feat(swe_bench): self-contained hsg SWE launcher (baked-gym image + t…
RayenTian Jul 1, 2026
1392b13
chore(swe_bench): don't source personal env script; document required…
RayenTian Jul 1, 2026
d5ee3df
chore(swe_bench): default model/data to SWE1 step_230 + r2e_gym_subset
RayenTian Jul 1, 2026
a6039a0
chore(swe_bench): revert default data to balanced_language + swe_publ…
RayenTian Jul 1, 2026
7195900
feat(swe_bench): nano V3.5 SWE scale-gen launcher with gen-benchmark …
RayenTian Jul 2, 2026
81f5242
try to make nano3.5 swe work on main
Jul 7, 2026
b66cb73
fix: declare missing build deps for custom vLLM container builds
Jul 7, 2026
c88955d
fix tools/build-custom-vllm.sh
HeyyyyyyG Jul 7, 2026
afce932
fix tools/build-custom-vllm.sh, nanobind
HeyyyyyyG Jul 7, 2026
c68f226
fix: restrict tool.uv.environments to aarch64 only
HeyyyyyyG Jul 7, 2026
dec7b08
Revert "fix: restrict tool.uv.environments to aarch64 only"
HeyyyyyyG Jul 8, 2026
64ad3bc
Revert "fix tools/build-custom-vllm.sh, nanobind"
HeyyyyyyG Jul 8, 2026
dffe6a1
Revert "fix tools/build-custom-vllm.sh"
HeyyyyyyG Jul 8, 2026
2b199b7
Revert "fix: declare missing build deps for custom vLLM container bui…
HeyyyyyyG Jul 8, 2026
dc7e4f5
use cudagraph_mode: PIECEWISE in vllm 0.20.0
HeyyyyyyG Jul 8, 2026
3ae4c5f
default mtp training on
HeyyyyyyG Jul 9, 2026
9a474c4
Merge origin/main into jiaqiz/ppo-dev
HeyyyyyyG Jul 9, 2026
11b1240
bump gym
HeyyyyyyG Jul 9, 2026
cbb4265
feat(ppo): add non-colocated generation and train/inference mismatch …
HeyyyyyyG Jul 10, 2026
04011ac
feat: add mask_sample
sdevare-nv Jul 9, 2026
0a5adde
fix hsg config
HeyyyyyyG Jul 11, 2026
165a41e
add cmh swe config
HeyyyyyyG Jul 11, 2026
a3d2a49
fix gym merge
HeyyyyyyG Jul 11, 2026
ea57ce0
feat(ppo): asynchronous PPO with non-colocated generation
HeyyyyyyG Jul 11, 2026
4a8acb7
fix(ppo): drop survivorship-biased incomplete replay-buffer targets o…
HeyyyyyyG Jul 11, 2026
102da6c
Merge branch 'jiaqiz/ppo-dev' of github.com:NVIDIA-NeMo/RL into jiaqi…
HeyyyyyyG Jul 12, 2026
c41b702
feat(ppo): NeMo-Gym rollout support (sync + async)
HeyyyyyyG Jul 13, 2026
82af102
fix(async ppo): cycle the collector dataloader across epochs
HeyyyyyyG Jul 13, 2026
f70c08c
bump gym
HeyyyyyyG Jul 14, 2026
3ee78be
fix survivorship bias for grpo and add dapo grpo baseline
HeyyyyyyG Jul 14, 2026
021b0ad
fix yaml
HeyyyyyyG Jul 16, 2026
af6b876
PPO: add token-level logging (#3252)
d-molinari Jul 16, 2026
cb18224
privileged critic
HeyyyyyyG Jul 16, 2026
5da66d0
Merge branch 'jiaqiz/ppo-dev' of github.com:NVIDIA-NeMo/RL into jiaqi…
HeyyyyyyG Jul 16, 2026
d93381a
fix ray launching on cmh
HeyyyyyyG Jul 16, 2026
dada509
fix genrm index
HeyyyyyyG Jul 20, 2026
575ab5b
Merge branch 'jiaqiz/ppo-dev' of github.com:NVIDIA-NeMo/RL into jiaqi…
HeyyyyyyG Jul 20, 2026
9be0f8f
chore: ignore local Claude Code state and test scratch artifacts
HeyyyyyyG Jul 31, 2026
1a6c298
feat(critic): offline rollout collection for the decoupled critic pip…
HeyyyyyyG Jul 31, 2026
47f8a99
feat(critic): offline critic pretraining on collected rollout shards
HeyyyyyyG Jul 31, 2026
4fb64f8
feat(ppo): support the privileged critic in async PPO
HeyyyyyyG Jul 31, 2026
f750f02
feat(critic): calibration diagnostics and a fail-loud privilege preco…
HeyyyyyyG Jul 31, 2026
2519a03
fix: crashes and false preconditions hit in the SWE / 1B runs
HeyyyyyyG Jul 31, 2026
b1912be
feat(logging): log mtp_metrics from both PPO loops
HeyyyyyyG Jul 31, 2026
4be6d57
config: SWE PPO run config + long-sequence memory headroom for 1B meg…
HeyyyyyyG Jul 31, 2026
380346b
config: run-config tuning for the ultra and 1B math sweeps
HeyyyyyyG Jul 31, 2026
ac59649
chore: bump Gym submodule (rollout /run retry + sandbox DNS fix)
HeyyyyyyG Jul 31, 2026
1fa70a8
ppo: let the critic train for more epochs than the actor
HeyyyyyyG Aug 5, 2026
d8ec189
examples: add PPO nano v3.5 SWE config for the hsg cluster
HeyyyyyyG Aug 5, 2026
eb028ce
data: import decord lazily in load_media_from_message
HeyyyyyyG Aug 5, 2026
4c1626b
ppo: load critic weights only when resuming from a warm-start seed
HeyyyyyyG Aug 5, 2026
0003f1b
examples: cap SWE test execution separately from agent time (cmh)
HeyyyyyyG Aug 5, 2026
e7ac37b
ppo: report critic/explained_var from the pre-update values
HeyyyyyyG Aug 5, 2026
afc2154
Log pre-whitening advantage statistics from GAE
HeyyyyyyG Aug 7, 2026
b5eb09e
Record pre-whitening advantage scale in the PPO rollout dump
HeyyyyyyG Aug 7, 2026
2f5ff50
feat(ppo): turn-level critic and turn-level GAE for agentic RL
HeyyyyyyG Aug 8, 2026
76c49ed
behaviour kl loss between pi_gen and pi_curr
HeyyyyyyG Aug 11, 2026
9738599
build: re-lock to pick up boto3 required by the pinned Gym
HeyyyyyyG Aug 13, 2026
9e6748e
style: apply ruff format to ppo.py and critic_pretrain.py
HeyyyyyyG Aug 13, 2026
c747b6b
fix(megatron): don't set the deprecated MoE SM-count knob on new mcore
HeyyyyyyG Aug 13, 2026
ed0068a
feat(critic): decomposed group baseline + residual critic
HeyyyyyyG Aug 13, 2026
f239239
feat(critic): SWE reference block for the privileged critic
HeyyyyyyG Aug 13, 2026
a1d23f4
Merge origin/jiaqiz/ppo-dev into the residual/privileged-critic work
HeyyyyyyG Aug 13, 2026
42452ef
fix(critic): report pre-update EV in both return spaces, on the ancho…
HeyyyyyyG Aug 13, 2026
dab69b1
config: add the critic keys to the hsg SWE config
HeyyyyyyG Aug 13, 2026
ed119cb
test: cover the two-space return of _pooled_explained_var
HeyyyyyyG Aug 13, 2026
78a12dc
fix(critic): read the required train_micro_batch_size without a fallback
HeyyyyyyG Aug 13, 2026
edfce82
test: match the turn_gae-aware unsupported-estimator message
HeyyyyyyG Aug 13, 2026
fb2f2a1
feat(ppo): report adv_raw/* on the turn-level path too
HeyyyyyyG Aug 13, 2026
7d85a64
fix privilege critic for r2egym
HeyyyyyyG Aug 28, 2026
f338853
feat(precision): fp32 LM head on both engines (opt-in), ported from s…
HeyyyyyyG Aug 30, 2026
23229cd
fix(vllm-http): return 400 for engine-side context-length ValueError …
HeyyyyyyG Sep 10, 2026
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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ code_snapshots*/

# Claude Code review memory (local only)
.claude/review-memory/

# Local Claude Code / MCP state
.claude/
!.claude/review-memory/
.mcp.json

# Local test + scratch artifacts
tests/unit/unit_results.json
tests/unit/unit_results/
classifier.model
known_elements.txt
output.txt
perm*.txt
2 changes: 1 addition & 1 deletion 3rdparty/Gym-workspace/Gym
Submodule Gym updated 141 files
58 changes: 52 additions & 6 deletions docs/guides/ppo.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,34 @@ We define a [ValueInterface](../../nemo_rl/models/value/interfaces.py) that cont

The value model supports the **Megatron-Core backend** (`value.megatron_cfg.enabled: true`) and the **DTensor backend** (`value.dtensor_cfg.enabled: true`). It uses the same architecture and tokenizer as the policy (configured via `value.model_name`), but is trained with a separate MSE loss on GAE returns.

### Colocated Architecture
### Colocated and Non-Colocated Architecture

PPO uses a colocated architecture where the **policy**, **value model**, and **vLLM generation engine** share the same set of GPUs. GPU memory is managed by offloading models to CPU between stages: the value model is loaded to GPU only during its inference and training phases, then offloaded to make room for other components.
By default, PPO uses a colocated architecture where the **policy**, **value model**, and **vLLM/SGLang generation engine** share the same set of GPUs. GPU memory is managed by offloading models to CPU between stages: the value model is loaded to GPU only during its inference and training phases, then offloaded to make room for other components.

PPO also supports **non-colocated generation**, where the generation engine runs on a dedicated set of GPUs separate from training (`policy.generation.colocated.enabled: false`, with `policy.generation.colocated.resources.{gpus_per_node,num_nodes}` sizing the dedicated generation resources). In this mode the policy and value model still share a single `train_cluster` (offloaded to CPU between each other as in the colocated case), while generation runs concurrently on its own `inference_cluster`; weights are synchronized via an NCCL broadcast collective spanning both clusters instead of the CUDA-IPC/ZMQ path used when colocated. Non-colocated generation currently requires the vLLM backend — `setup()` asserts this explicitly, since SGLang does not yet implement the collective rendezvous or weight refit needed for non-colocated mode.

### Asynchronous PPO

PPO supports an **asynchronous** training mode (`ppo.async_ppo.enabled: true`) that mirrors [async GRPO](grpo.md): a background `AsyncTrajectoryCollector` continuously generates trajectories into a replay buffer while the driver trains, so generation and training overlap instead of running in lockstep. Async PPO requires non-colocated vLLM generation (`policy.generation.colocated.enabled: false`, `vllm_cfg.async_engine: true`) and importance-sampling correction (`loss_fn.use_importance_sampling_correction: true`) — all enforced by asserts in `async_ppo_train`. Enable it via `examples/run_ppo.py`, which dispatches to `async_ppo_train` when `ppo.async_ppo.enabled` is set.

Each async step: sample a fixed batch from the replay buffer → compute fresh critic **values** at train time → compute fresh policy/reference logprobs → compute GAE advantages → run the `ppo_epochs` inner loop (critic then actor), preceded by any extra `critic_ppo_epochs` critic-only passes → perform a **single** weight refit to the generation engine and bump the replay-buffer weight version. Computing values at train time (rather than stashing them when the trajectory was generated) keeps the critic side as fresh as possible.

**Critic-staleness caveat.** Off-policy trajectories are corrected on the *actor* side by importance sampling (`exp(prev_logprobs - generation_logprobs)`), exactly as in async GRPO. PPO's GAE, however, recursively bootstraps value estimates across each trajectory, so bias from a stale trajectory's actions/rewards compounds along the sequence in a way GRPO's memoryless reward-only advantage does not, and there is **no** corresponding critic-side correction. This bias is *bounded* (not eliminated) by keeping `ppo.async_ppo.max_trajectory_age_steps: 1` (at most one policy version stale), which is the recommended and validated setting. Values above 1 are permitted but only warned about; a rigorous fix (folding importance ratios into the GAE recursion, V-trace style) is future work.

**Critic warmup** (`ppo.policy_training_start_step > 0`) is supported: during warmup the policy is frozen, so each step's weight refit skips the actual weight transfer (generation already holds the correct initial weights) but still advances the replay-buffer weight version so the async pipeline keeps making progress.

Because the actor is frozen at its initial policy `π₀` all the way **through** step `W = ppo.policy_training_start_step` (it first trains *during* step `W`, and the refit that publishes `π₁` to generation happens at the *end* of step `W`), **every rollout — whatever its generation-version tag — was produced by the same model `π₀`**. The generation-version counter still increments each step (so the collector's lead can advance), but it does *not* track policy staleness during warmup: gen-version `g ≤ W` ⇒ policy `π₀`, and `g > W` ⇒ policy `π_{g−W}`. This lets the collector bank cheap frozen rollouts far ahead for free: set `ppo.async_ppo.warmup_max_trajectory_age_steps` above `max_trajectory_age_steps` (`A_t`) and it generates that far ahead while the critic pretrains.

The snap-back is governed by **two distinct boundaries** (this is what makes the knob correct and hang-free):

- The **collector's generation-lead** drops to `A_t` at step `W`, so from step `W+1` it stops over-banking `π₀` and regenerates its lead targets against the freshly-trained policy (`π₁`, `π₂`, …).
- The **buffer's eviction age** stays elevated through step `W + A_t`. A frozen (`π₀`) rollout is within `A_t` *policy*-steps of the actor for every step `s ≤ W + A_t` (its policy-age is `s − W`), so those banked rollouts are admitted as legitimate lag-≤`A_t` data — IS-corrected at train time exactly like normal async — and are only evicted once the actor has genuinely moved more than `A_t` steps past `π₀`. (Snapping the eviction age at `W` instead would discard the still-on-policy boundary batch and **deadlock**, since the collector's lead has already advanced past that target and never regenerates it.)

Note this only improves *throughput* when warmup is **generation-bound** (the collector can actually bank ahead). When critic training is the bottleneck it is a throughput no-op — but it is correct and hang-free either way.

**Reading the age metrics.** `avg_trajectory_age` reports the *generation-version* age (`current_weight_version − gen_version`), which **overcounts** off-policyness across the warmup boundary: every gen-version `≤ W` is the same frozen `π₀`, so a rollout banked at gen-version 0 and consumed at step `W+1` shows `avg_trajectory_age ≈ A_w` even though it is only **1 policy step** off-policy. The metric that actually bounds the importance-sampling correction is `avg_trajectory_policy_age` / `max_trajectory_policy_age` = `max(0, s−W) − max(0, g−W)`, which stays **≤ `max_trajectory_age_steps`** at every step (equal to the gen-version age when there is no warmup). A spike in `avg_trajectory_age` at the boundary with `max_trajectory_policy_age` still at the bound is expected and safe; a `max_trajectory_policy_age` *above* the bound would indicate a real regression. Absent/`null` ⇒ same as `max_trajectory_age_steps`.

**v1 limitations.** Async PPO currently requires the vLLM backend (no SGLang/Megatron generation, no colocated inference). DAPO-style dynamic sampling, reward scaling, and reward shaping are also unsupported in async mode (rejected by `examples/run_ppo.py`).

### Value Model Configuration

Expand Down Expand Up @@ -151,18 +176,37 @@ The PPO training loop, [ppo_train](../../nemo_rl/algorithms/ppo.py), follows thi
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

Steps 6–7 repeat `ppo_epochs` times per rollout before generating new responses.
Steps 6–7 repeat `ppo_epochs` times per rollout before generating new responses. Step 6 additionally runs `critic_ppo_epochs - ppo_epochs` extra critic-only passes up front.

### 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 # Train the actor 4 times on each rollout batch
```

Each step trains both the critic and the actor on the same advantage estimates computed from the initial rollout.
Every pass trains on the same advantage estimates, computed once from the initial rollout.

#### Training the critic for more epochs

The critic often benefits from more passes over a rollout batch than the actor: extra actor epochs push the policy further off the data that produced the advantages, while extra critic epochs just fit a regression target harder. Set `critic_ppo_epochs` to give the critic its own epoch count:

```yaml
ppo:
ppo_epochs: 1 # one actor update per rollout batch
critic_ppo_epochs: 4 # four critic updates on the same batch
```

`critic_ppo_epochs: null` (or omitting it) keeps the critic coupled to `ppo_epochs`, which is the default behavior. It must be `>= ppo_epochs`: the critic already trains once per shared epoch, so the setting only adds passes.

The extra critic-only passes run before the shared critic/actor loop, which is otherwise unchanged. This is safe because every pass consumes the same returns and advantages, computed once per step before any update — the critic never sees the actor's within-step updates, so the ordering does not change the result. For Megatron backends the LR-schedule budget (`train_iters`) is derived per model from its own epoch count, so a longer critic loop does not truncate the value model's decay schedule.

Two caveats when reading the plots:

- The critic's LR scheduler ticks once per pass, so with `critic_ppo_epochs: 4` it advances 4x faster per PPO step than the actor's. `lr_warmup_iters` on the value optimizer is counted in those ticks — multiply it by the same factor to keep warmup spanning the same number of PPO steps.
- `critic/explained_var` is computed from the rollout-time values — the exact tensors GAE consumed — so it describes the critic **before any update that step**, regardless of `critic_ppo_epochs` (it will sit near the positional `critic/ev_early|mid|late` diagnostics, which use the same values). Setting `ppo.log_post_update_critic_metrics: true` adds `critic/explained_var_post_update` and `critic/loss_post_update`, which re-score the same batch **after** every update that step, at the cost of one extra forward-only critic pass per step. A large post-vs-pre gap that does not lift the pre-update curve over training means the critic is memorizing each batch rather than generalizing. `critic/loss` and `critic/grad_norm` come from the last training pass.

### Critic Warmup

Expand Down Expand Up @@ -210,6 +254,7 @@ ppo:
max_num_epochs: 100000
max_num_steps: 100000
ppo_epochs: 4
critic_ppo_epochs: null
policy_training_start_step: 0
val_period: 20
val_at_start: true
Expand Down Expand Up @@ -255,7 +300,8 @@ value_loss_fn:
```

**PPO-specific parameters:**
- **`ppo.ppo_epochs`**: Number of training updates per rollout batch
- **`ppo.ppo_epochs`**: Number of actor updates per rollout batch
- **`ppo.critic_ppo_epochs`**: Number of critic updates per rollout batch (`null` = same as `ppo.ppo_epochs`)
- **`ppo.policy_training_start_step`**: Number of critic-only warmup steps before policy training begins
- **`ppo.adv_estimator.name`**: Set to `"gae"` for GAE advantage estimation (PPO default)
- **`ppo.adv_estimator.gae_lambda`**: GAE $\lambda$ parameter (bias-variance tradeoff, typically 0.95)
Expand Down
35 changes: 35 additions & 0 deletions examples/configs/grpo_math_1B_dapo_megatron.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# GRPO (Megatron-Core) on DAPO-Math-17K — async baseline for the PPO async run
# (repro_ppo_mcore_7b_math_async.sh / ppo_math_1B.yaml).
#
# Inherits grpo_math_1B_megatron.yaml and swaps the `data:` and `env:` blocks so
# the data setup AND the reward scorer are byte-for-byte the same as
# ppo_math_1B.yaml (apple-to-apple):
# - full DAPOMath17K train set. DAPOMath17KDataset takes **kwargs and ignores
# the inherited split_validation_size, so NO 5% train-side split happens —
# the whole training set is used, exactly like PPO.
# - explicit DAPOMathAIME2024 validation (overrides the parent's
# `validation: null`, which would otherwise fall back to a train split).
# - prompt_file: null (no CoT template) to match PPO; the parent grpo config
# uses examples/prompts/cot.txt.
# - math_verify_impl: dapo_math_verify to match PPO. The grpo default is the
# more lenient hf_math_verify, which scores the SAME completions higher and
# was inflating GRPO's reward/accuracy vs PPO (~0.45 vs ~0.29 val at step 0).
# Everything else (loss_fn, policy/megatron, cluster, grpo sizes, async_grpo) is
# inherited and overridden on the command line by the launch script.
defaults: "grpo_math_1B_megatron.yaml"

data:
train:
dataset_name: DAPOMath17K
split_validation_size: null # no train-side split; use the explicit validation set below (matches PPO)
validation:
dataset_name: DAPOMathAIME2024
default:
prompt_file: null
system_prompt_file: null
processor: "math_hf_data_processor"
env_name: "math"

env:
math:
math_verify_impl: "dapo_math_verify" # match PPO's stricter verifier (grpo default is hf_math_verify)
Loading
Loading