feat(ppo): warm-start the value model from a critic-pretrain checkpoint - #3821
Conversation
220a173 to
6adf1a4
Compare
|
/ok to test 6adf1a4 |
2fcc765 to
2ccfec2
Compare
6adf1a4 to
129ed0e
Compare
yuki-97
left a comment
There was a problem hiding this comment.
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
|
/ok to test c14539c |
85ad93c to
7b09c83
Compare
c14539c to
6e8790c
Compare
Signed-off-by: Yuki Huang <yukih@nvidia.com>
…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>
6e8790c to
7107664
Compare
terrykong
left a comment
There was a problem hiding this comment.
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_checkpointis correct, and non-obviously so. That path auto-setsfinetune=Truein megatron-bridge, which suppresses optimizer/RNG state and triggers the value worker'shide_loss_moduleshook that dropsoutput_layer.*— so it would restore the trunk and randomly re-initialize the value head, discarding exactly what critic pretraining produces.megatron_value_worker.py:97already states the rule. - Gating value-path resolution on
is_ppo_runincidentally fixes a pre-existing GRPO wart — a GRPO resume previously stat'dstep_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 titledfeat(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
… path, add ppo.py setup coverage Signed-off-by: Yuki Huang <yukih@nvidia.com>
|
/ok to test 95c03dc |
Signed-off-by: Yuki Huang <yukih@nvidia.com>
|
/ok to test c39b179 |
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>
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:validate_warm_start_checkpointrejects an empty string and a path with novalue/weights.get_resume_pathsnever 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 avalue/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.mdlists 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'salgo_config()returns that same object on a PPO run, so legacyppo.setupandsetup_single_controllerare both covered by one field.Issues
None closed.
Usage
policy_training_start_step: 0is 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 whatasync_ppo.warmup_generation_lead_stepsrequires (async_rl.sampler.warmup_lookahead_versionson the SingleController path); both must be null when it is 0.Before your PR is "Ready for review"
Pre checks:
Additional Information
Byte-identical on a PPO run when unset. The default is
null, solast_checkpoint_path or warm_startreduces tolast_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 warnedOptimizer state not foundabout a value model it does not have. Warning text only.Tests.
test_ppo_setup.py::TestValueWarmStartcovers the near end on the SC path — the seeded paths reach_build_valuewhile the trainer'sweights_pathstaysNone, so the policy is not seeded, plustest_ppo.py::test_ppo_setup_rejects_a_warm_start_that_does_not_resolvefor the legacyppo.setupcopy.test_{megatron,dtensor}_value_worker.py::test_value_worker_checkpoint_save_and_loadcovers the far end: a restored value worker reproduces the saved model's predictions exactly, so a wrong checkpoint or a partial restore fails.One unrelated fix to resolve
#3773 (comment). SC has no-1convention — the rollout pump gates on_current_epoch < max_num_epochs, so an async-PPO user followingdocs/guides/ppo.mdand carryingmax_num_epochs: -1onto SC got zero optimizer steps and exit status 0._validate_algo_settingsnow rejects a non-positive value and names the remedy;_clamp_max_num_stepsno longer needs its own<= 0early 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 bytests/functional/ppo_async_single_controller.sh.