Skip to content

feat(ppo): warm-start the value model from a critic-pretrain checkpoint - #3821

Merged
terrykong merged 6 commits into
mainfrom
yukih/sc-warm-start-critic
Aug 28, 2026
Merged

feat(ppo): warm-start the value model from a critic-pretrain checkpoint#3821
terrykong merged 6 commits into
mainfrom
yukih/sc-warm-start-critic

Conversation

@yuki-97

@yuki-97 yuki-97 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

Adds ppo.warm_start_value_checkpoint: a fresh PPO run seeds its critic from a separately pretrained value checkpoint, so critic warmup can be paid once offline instead of inside every PPO run.

Today the only way to reach a usable critic is ppo.policy_training_start_step: N — N steps during which generation and environment scoring run at full cost while only the critic trains. Where generation dominates the step cost (SWE, long-horizon agentic runs), that is the most expensive way to buy a value function. Pretraining the critic on already-collected rollouts and starting PPO from it moves that cost off the PPO allocation entirely.

How it works. The value component's resume paths are resolved independently of the policy's (CheckpointManager.get_resume_paths(..., model_component="value")), so the whole feature is a choice of which checkpoint that call reads:

warm_start = ppo_config.warm_start_value_checkpoint
if last_checkpoint_path is None and warm_start is not None:
    validate_warm_start_checkpoint(warm_start)
    print(f"🔥 Warm-starting the value model from {warm_start}")
value_weights_path, value_optimizer_path = checkpointer.get_resume_paths(
    last_checkpoint_path or warm_start,
    model_component="value",
)

validate_warm_start_checkpoint rejects an empty string and a path with no value/weights. get_resume_paths never stats what it is given, so without it a typo prints the 🔥 line over a cold critic.

The seed is a step_<n> directory holding a value/ subtree — what a PPO or critic-pretraining run already checkpoints — and the critic restores its weights, plus optimizer moments and LR-scheduler state whenever the seed carries them. On Megatron the seed and this run must then agree on the value scheduler — docs/guides/ppo.md lists the fields. Nothing else is read: the policy starts from the base model, the dataloader from the beginning, and the step counter at 0. Those are a fresh run's defaults, so no extra handling is needed for them.

Fresh runs only, deliberately. When the run already has a checkpoint of its own, that checkpoint wins and the setting is not read. This is what lets the key stay in the config across resumes: a requeued run restores its own critic rather than re-seeding from the pretrained one, with no config edit between submissions. Making the warm start win instead would silently discard the critic's progress on every resume.

Both paths. The knob lives on PPOConfig, and SC's algo_config() returns that same object on a PPO run, so legacy ppo.setup and setup_single_controller are both covered by one field.

Issues

None closed.

Usage

ppo:
  warm_start_value_checkpoint: /path/to/critic_pretrain_run/step_370
  policy_training_start_step: 0  # the critic is already warm

policy_training_start_step: 0 is the natural pairing, but keeping a short online warmup is equally valid — it calibrates the seeded critic on this run's own rollout distribution before the policy starts moving. A nonzero value is also what async_ppo.warmup_generation_lead_steps requires (async_rl.sampler.warmup_lookahead_versions on the SingleController path); both must be null when it is 0.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you run the unit tests and functional tests locally? Visit our Testing Guide for how to run tests
  • Did you add or update any necessary documentation? Visit our Document Development Guide for how to write, build and test the docs.

Additional Information

  • Byte-identical on a PPO run when unset. The default is null, so last_checkpoint_path or warm_start reduces to last_checkpoint_path.

  • One shape change on the SC path. Value resume-path resolution now sits inside if is_ppo_run(master_config), so a GRPO run no longer resolves value paths it never uses. Side effect on the GRPO path: a GRPO resume previously warned Optimizer state not found about a value model it does not have. Warning text only.

  • Tests. test_ppo_setup.py::TestValueWarmStart covers the near end on the SC path — the seeded paths reach _build_value while the trainer's weights_path stays None, so the policy is not seeded, plus test_ppo.py::test_ppo_setup_rejects_a_warm_start_that_does_not_resolve for the legacy ppo.setup copy. test_{megatron,dtensor}_value_worker.py::test_value_worker_checkpoint_save_and_load covers the far end: a restored value worker reproduces the saved model's predictions exactly, so a wrong checkpoint or a partial restore fails.

    uv run --group test pytest tests/unit/single_controller/test_ppo_setup.py tests/unit/algorithms/test_ppo.py tests/unit/utils/test_checkpoint.py
    # 2 GPUs + HF_TOKEN
    uv run --group test --extra mcore pytest tests/unit/models/value/test_megatron_value_worker.py::test_value_worker_checkpoint_save_and_load
  • One unrelated fix to resolve #3773 (comment). SC has no -1 convention — the rollout pump gates on _current_epoch < max_num_epochs, so an async-PPO user following docs/guides/ppo.md and carrying max_num_epochs: -1 onto SC got zero optimizer steps and exit status 0. _validate_algo_settings now rejects a non-positive value and names the remedy; _clamp_max_num_steps no longer needs its own <= 0 early return.

  • No functional test. A seeded run and a cold control do not share rollouts (async vLLM is not reproducible even at a fixed seed), so a cross-run critic-loss comparison is a statistical claim, not an identity check. The one property e2e would add — a real SC PPO run writing a loadable step_<n>/value/ subtree — is already asserted by tests/functional/ppo_async_single_controller.sh.

@copy-pr-bot

copy-pr-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Aug 25, 2026
@yuki-97
yuki-97 force-pushed the yukih/sc-warm-start-critic branch from 220a173 to 6adf1a4 Compare August 25, 2026 12:03
@yuki-97 yuki-97 added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Aug 25, 2026
@yuki-97

yuki-97 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 6adf1a4

@yuki-97
yuki-97 force-pushed the yukih/sc-drop-incomplete-target branch from 2fcc765 to 2ccfec2 Compare August 25, 2026 16:30
@yuki-97
yuki-97 force-pushed the yukih/sc-warm-start-critic branch from 6adf1a4 to 129ed0e Compare August 25, 2026 16:32

@yuki-97 yuki-97 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Team review of the critic warm-start path, run across five agents (RL/guidelines, bug-finder, test, design, devil's advocate) with an adversarial pass over every finding.

15 raw findings deduped to 11 clusters; 4 survived confidence + impact filtering. Seven were dropped as confirmed-but-not-worth-a-round-trip, and one (a claim that the new test fixture models an unreachable checkpoint layout) was disputed outright — get_resume_paths returns the identical tuple on both branches, so the fixture is fine.

No lint/type results: this host has no uv or pre-commit, and nothing was executed except two standalone stdlib repros. Test suggestions are unverified locally and flagged as such.

Generated by Claude Code

Comment thread examples/configs/ppo_math_1B.yaml
Comment thread nemo_rl/algorithms/single_controller_utils/setup.py
Comment thread nemo_rl/algorithms/single_controller_utils/setup.py
Comment thread tests/unit/single_controller/test_ppo_setup.py
@yuki-97

yuki-97 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test c14539c

@yuki-97
yuki-97 marked this pull request as ready for review August 25, 2026 17:33
@yuki-97
yuki-97 requested review from a team as code owners August 25, 2026 17:33
@yuki-97
yuki-97 force-pushed the yukih/sc-drop-incomplete-target branch from 85ad93c to 7b09c83 Compare August 26, 2026 02:11
@yuki-97
yuki-97 force-pushed the yukih/sc-warm-start-critic branch from c14539c to 6e8790c Compare August 26, 2026 02:11
…y to the megatron reference config

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…ent the value-scheduler constraint

Signed-off-by: Yuki Huang <yukih@nvidia.com>
Signed-off-by: Yuki Huang <yukih@nvidia.com>
@yuki-97
yuki-97 force-pushed the yukih/sc-warm-start-critic branch from 6e8790c to 7107664 Compare August 28, 2026 08:15
@yuki-97
yuki-97 requested review from a team as code owners August 28, 2026 08:15
@github-actions github-actions Bot added the CI Relating to CI label Aug 28, 2026
@yuki-97
yuki-97 changed the base branch from yukih/sc-drop-incomplete-target to main August 28, 2026 08:15
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: 7107664 (PR #3821 from yukih/sc-warm-start-critic)

✅ Submodules that are properly updated:

Megatron-Bridge: ✅ PR branch is ahead of yukih/sc-drop-incomplete-target branch (fast-forward)

All submodule changes look good! ✨

@terrykong terrykong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Team review of the critic warm-start path: seven agents (RL/guidelines, Megatron-Bridge, bug-finder, test, design, comment-audit, devil's advocate), with an adversarial pass over every finding including my own.

Nice feature — moving critic warmup off the PPO allocation is a real saving where generation dominates, and the resume-wins precedence is the right call.

Three comments below. Eleven candidate findings were cut to three: four were disputed outright by the adversarial pass, and the rest folded into the surviving comments. Two design calls are worth affirming rather than questioning:

  • Routing the seed through the resume path instead of checkpointing.pretrained_checkpoint is correct, and non-obviously so. That path auto-sets finetune=True in megatron-bridge, which suppresses optimizer/RNG state and triggers the value worker's hide_loss_modules hook that drops output_layer.* — so it would restore the trunk and randomly re-initialize the value head, discarding exactly what critic pretraining produces. megatron_value_worker.py:97 already states the rule.
  • Gating value-path resolution on is_ppo_run incidentally fixes a pre-existing GRPO wart — a GRPO resume previously stat'd step_N/value/optimizer, missed, and emitted a spurious "Optimizer state not found" warning about a value model the run does not have. Worth a line in the PR body, since it is a behavior change on the GRPO path in a PR titled feat(ppo).

Not verified — no runnable environment. Everything here is static reading plus upstream source at the pinned SHAs (Megatron-LM 14346b65, Megatron-Bridge 8c46dc42). No test and no linter was executed: this tree's uv.lock is linux-only and uv run / pre-commit both fail on macOS with Failed to parse entry: nemo-gym. I make no claim about lint status, and the suggested test is unrun.

FYI, no action: the PR description's "How it works" snippet is stale — it still shows the call site before validate_warm_start_checkpoint was added in c14539c.

Generated by Claude Code

Comment thread docs/guides/ppo.md Outdated
Comment thread nemo_rl/algorithms/ppo.py
Comment thread nemo_rl/utils/checkpoint.py
… path, add ppo.py setup coverage

Signed-off-by: Yuki Huang <yukih@nvidia.com>
@github-actions github-actions Bot removed the CI Relating to CI label Aug 28, 2026
@yuki-97

yuki-97 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 95c03dc

@yuki-97

yuki-97 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test c39b179

@terrykong
terrykong enabled auto-merge (squash) August 28, 2026 19:13
@terrykong
terrykong merged commit 725a704 into main Aug 28, 2026
85 checks passed
@terrykong
terrykong deleted the yukih/sc-warm-start-critic branch August 28, 2026 19:57
asolergi-nv added a commit that referenced this pull request Aug 29, 2026
PR3 (#3591) was SQUASH-merged into main as b3b6713, so none of its commits are
ancestors of main while PR4 still carries all of them. Git therefore sees PR3's whole
diff as independently added on both sides, which is why all 16 conflicts name b3b6713
and why the PR showed CONFLICTING despite the content being identical.

That made the classification, not the content, the work. For each conflicted file: is
main's version byte-identical to PR3's head (3d9ce21), and does PR4 add anything beyond
it? Three groups fell out.

GROUP A -- pure squash artefacts, resolved by taking OURS (10 files)
  fleet_health.py, collective_weight_synchronizer.py, membership.py,
  nccl_reshard_weight_synchronizer.py, grpo_sc_generation_shard_recovery.sh,
  test_watchdog_pump.py, test_membership.py, test_reconcile_communicator.py,
  test_reshard_rebuild.py, test_weight_synchronizer.py
  main == PR3 exactly and no other PR touched them, so PR4's side is main's content plus
  PR4's delta. Taking ours loses nothing.

GROUP B -- PR4 contributes nothing, resolved by taking THEIRS (2 files)
  single_controller_utils/setup.py  (#3480, #3727, #3821 on top of PR3)
  tests/unit/single_controller/test_refit_recovery.py  (#3480 on top of PR3)

GROUP C -- genuine merges (4 files), one per upstream PR below.

The six upstream PRs that contributed real content, and what each needed:

  #3480 recover replay buffer from native TQ checkpoints
        single_controller.py: rollout_recovery imports. Kept alongside ours.
        setup.py, test_refit_recovery.py, L1 harness: group B / additive.
  #3765 log toolcall and thinktag violation rate
        single_controller.py: VIOLATION_TAG_KEYS. Auto-merged, verified present.
  #3727 support non-colocated MInf
        single_controller.py: MegatronGeneration import, kept alongside ours.
        L1 harness: grpo_megatron_generation_gym_single_controller.sh entry.
  #3821 warm-start the value model from a critic-pretrain checkpoint
        config.py: the max_num_epochs validator. Ours only adds restart_dead_shards to
        FleetHealthConfig, so both survive; verified the field landed in the right class
        and the validator is intact.
  #3655 nemo-lens telemetry
        vllm_generation.py: the @trace_fn decorator on generate. Ours adds restart_shard
        in a different region; both kept.
  #3839 pause generation during in-flight refit
        vllm_generation.py: pause_generation_for_refit / resume_generation_after_refit.
        Auto-merged, verified present -- worth knowing it exists, since it pauses engines
        around a refit and this PR restarts them.

Verified after resolving: no conflict markers; all four lint hooks clean (the single
pyrefly error is the pre-existing unrelated transfer_queue import); 1122 unit tests pass;
both submodule pointers and uv.lock/pyproject byte-identical to main.

Both sides' work was checked individually rather than assumed: EngineSupervisor wiring,
restart_dead_shards, restart_shard, recreate_worker, desired_membership and the report_refit
call on our side; the six items above on main's.

Note for anyone reproducing locally: #3655 adds a nemo-lens dependency that the pre-merge
container image does not carry, so tests fail at import with ModuleNotFoundError: nemo
until the venv is refreshed. Plain upstream/main fails the same way in that image; it is
not a merge defect.

Signed-off-by: asolergibert <asolergibert@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants