Skip to content

feat(sc): support PPO in single controller - #3773

Merged
yuki-97 merged 31 commits into
mainfrom
yukih/sc-ppo
Aug 26, 2026
Merged

feat(sc): support PPO in single controller#3773
yuki-97 merged 31 commits into
mainfrom
yukih/sc-ppo

Conversation

@yuki-97

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

Copy link
Copy Markdown
Contributor

What does this PR do ?

Ports PPO — including the critic/value model — to the SingleController (SC) path, so an async PPO run can be driven by examples/run_grpo_single_controller.py instead of run_ppo.py. Megatron critic only, async only.

What lands here

Config. MasterConfig.grpo becomes optional and a ppo: block is added beside it; the two are mutually exclusive and every read goes through algo_config(). The advantage estimator config moves from dict to a BaseModel with name pinned to a Literal. _validate_algo_settings rejects the misconfigurations listed below at setup time, each message naming the fix.

Value model on the data plane. New TQValue (driver) and get_values_presharded (worker), with TQDriverMixin extracted from tq_policy.py so the policy and the critic share one copy of the packing / data-plane helpers. PPO_VALUE_FIELDS is deliberately kept out of DP_TRAIN_FIELDS so a GRPO run never fetches a column nobody wrote.

Train pump. _value_stage (critic forward) and _value_train (critic optimizer step) around the existing advantage stage, plus ppo.ppo_epochs (N optimizer steps per RL step, the policy offloaded between epochs — closes the "multiple mini-steps inside a single RL step" item in #2625), ppo.policy_training_start_step (critic warmup: weight sync skipped, policy optimizer not checkpointed, step excluded from best-checkpoint tracking), and async_rl.sampler.warmup_lookahead_versions (the SC equivalent of legacy warmup_generation_lead_steps; buffer capacity is sized from the peak window). The critic forward runs after the policy and reference logprobs rather than before as in ppo.py, the policy is parked on CPU across both critic stages, and the two checkpoint saves are serialized critic-first.

Setup. The train cluster is sized for two worker groups on a PPO run, the critic is built and resumed alongside the policy, and the Megatron tick budget is scaled by ppo_epochs.

Constraints enforced at config time

  • ppo.async_ppo: null — SC drives the async loop itself (rejected in the entrypoint).
  • ppo and grpo are mutually exclusive, and exactly one must be set.
  • value and value_loss_fn are present iff the ppo block is, and value.megatron_cfg.enabled: true.
  • ppo.ppo_epochs >= 1.
  • async_rl.sampler.name: in_order.
  • min_groups_for_streaming_train == ppo.num_prompts_per_step.
  • value.train_global_batch_size == num_prompts_per_step * num_generations_per_prompt, so one RL step is one critic optimizer.step.
  • async_rl.rollout_failure.max_skipped_prompts and max_consecutive_dropped_prompts are both 0.
  • warmup_lookahead_versions requires policy_training_start_step > 0.
  • ckpt_assume_constant_structure is rejected together with critic warmup + optimizer checkpointing.

policy.offload_optimizer_for_logprob: true is required but not validated — nothing else on the SC path takes the policy optimizer off the GPU.

Not covered (intentional, tracked)

Rejected at setup rather than silently skipped, except where noted.

  • ppo.overlong_filtering, reward_shaping, reward_scaling, use_dynamic_sampling — implemented on neither algorithm block on the SC path ([Single Controller / Async RL] cleanup tracking issue #2625).
  • Samplers other than in_orderwindowed, weight_fifo and custom are not supported under PPO.
  • Streaming critic training — the value workers have no split begin/microbatch/finish train API yet ([Single Controller / Async RL] cleanup tracking issue #2625), so one RL step must be one chunk.
  • Rollout drop budgets — a drop shortens the step, and the critic shards it against the configured value.train_global_batch_size rather than its actual size, so the first short step fails a divisibility assert inside the value workers. The resiliency layer (feat(sc): tolerate and replace dropped rollouts in the SingleController #3665) stays available on GRPO.
  • DTensor critic — out of scope, Megatron only.
  • adv_estimator.name: raw_reward under PPO — yields no returns column; ppo.py behaves the same way. Not rejected.
  • total_flops is reported from the last epoch only, so it under-reports N-fold under ppo_epochs > 1; throughput here is not comparable to the v1 path.
  • The exemplar's default sizing OOMs (PPO default yaml OOM #3793) — pre-existing across the PPO exemplar family, not introduced here. The functional and nightly runs both pass with their own sizing.
  • Renaming run_grpo_single_controller.py to run_single_controller.py — separate PR, to keep this diff reviewable.

Issues

Advances #2625 (does not close it). The SC exemplar added here inherits the exemplar-sizing OOM tracked in #3793.

Usage

uv run examples/run_grpo_single_controller.py --config examples/configs/recipes/llm/ppo-qwen2.5-1.5b-gsm8k-2n8g-megatron-valuetp2sp-dynbatch-noncolocated-async-single-controller.yaml

examples/configs/ppo_math_1B_megatron_single_controller.yaml is the exemplar (schema documentation / starting point). Running it as-is OOMs — see #3793, which covers all three PPO exemplars. The functional test drives it with the usual smoke-test overrides (Qwen/Qwen2.5-0.5B, num_prompts_per_step=2, train_global_batch_size=8, 2 GPUs) and passes, as does the 2-node nightly recipe.

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?
  • Did you add or update any necessary documentation?

Additional Information

Tests added. Unit: test_ppo_setup.py, tests/unit/models/value/test_tq_value.py and test_sampler_interface.py are new, plus PPO coverage in test_single_controller_actor.py and test_checkpointing.py — the epoch loop, the warmup path, the value stage, pad-target isolation, and save/resume across the warmup boundary. Functional: tests/functional/ppo_async_single_controller.sh, registered in the L1 list. Nightly: the 2n8g recipe above, added to nightly.txt; its thresholds come from this recipe's own 40-step run, not the legacy recipe's — at 256 prompts/step it has consumed a quarter of the legacy run's data by step 40.

Docs. docs/guides/single-controller.md now covers the PPO path: the ppo: / value: / value_loss_fn: blocks, the constraints above, warmup_lookahead_versions, and the legacy ppo.async_ppo.*async_rl.* migration table.

Test Results. yellow: legacy with same settings; red: v2, this PR; blue: v2, this PR. resume from ckpt 20.
image

@copy-pr-bot

copy-pr-bot Bot commented Aug 23, 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.

@yuki-97 yuki-97 added the CI:L1 Run doctests, unit tests, and functional tests label Aug 23, 2026
@yuki-97

yuki-97 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test a61654e

@yuki-97

yuki-97 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test aa5bfb3

@yuki-97 yuki-97 added CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) and removed CI:L1 Run doctests, unit tests, and functional tests labels Aug 24, 2026
@yuki-97

yuki-97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 5026969

@tianyi-zhang-02

tianyi-zhang-02 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Four places this touches what I have open. Flagging them so the ordering is yours, not something we hit in a conflict.

1. scale_rewards does not reach SC. For PPO that is a regression — ppo.py calls it in ppo_train, and grep -c scale_rewards nemo_rl/algorithms/single_controller.py on this branch is 0. So grpo.reward_scaling silently stops working when a PPO run moves to SC. #3787 applies it at the top of _advantage_stage, after _value_stage and before compute_advantage, so GAE sees scaled rewards. (advantage_clip_low/high is in that same PR but is GRPO-only, so not a PPO regression.)

2. payload.py:83 still reads sample_mask = torch.ones(n, ...). grpo.py builds that from DatumSpec.loss_multiplier and zeroes it for env-flagged and, under overlong_filtering, truncated rows. Both signals are on the Completion already; nothing reads them. So the PPO path inherits it — rows every v1 driver drops get a value target and a GAE advantage. #3786 fixes it.

3. advantage_estimator.py#3512 is the other half of this. You type the config side; #3512 types the return and the contract (AdvantageResult, plus a Protocol both _create_advantage_estimator functions are annotated with). Same direction, and they will conflict — both rewrite the estimator __init__s and compute_advantage signatures. Yours is the feature and mine has sat since 2026-08-06 with no reviewer, so I would rather adapt: rebase #3512 on top of this once it settles, or close it and hand you the AdvantageResult change to fold in. Your call.

4. The test renames. This moves test_single_controller.pytest_single_controller_actor.py, where #3787 adds its tests. Nothing for you to do — I will rebase after this lands.

1 and 2 are pre-existing and open separately; no action needed for this PR to be correct. Mentioning them because "SC never applied it" is the same shape as your #3770, and the PPO path picks both up :)

@yuki-97

yuki-97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

hi @tianyi-zhang-02 , thanks for your interest. this is the init implementation of single controller + PPO and is aim to get into main in recent days.
it's intended that there will be many missing features. critical features will be updated in this PR, others may record in #2625 and may or may not need implementing.

many things is still subject to change, feel free to follow this PR to watch.

@tianyi-zhang-02

tianyi-zhang-02 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Went through the whole diff rather than just my overlaps. One finding, one note, and four things that hold up — recording those too, since "I looked and it is fine" is worth as much to you as a list.

Finding, low severity: OPDAdvantageEstimator is the only estimator left out of the typed-config migration.

Five of six are converted — advantage_estimator.py:80, :124, :218 to AdvEstimatorConfig, :292, :345 to GAEConfig. :589 is still def __init__(self, estimator_config: dict, loss_config: dict), and the only one whose loss_config is still dict.

It matters a little more than an annotation. This PR narrows the name to Literal["grpo", "gdpo", "opd", "reinforce_plus_plus"], making opd first-class — while grpo.py:2275 still constructs it as OPDAdvantageEstimator({"name": "opd"}, loss_config), a literal dict, and the __init__ body ignores both arguments. So setting adv_estimator.name: opd alongside any other adv_estimator.* field drops those fields silently. Typing the signature and passing the real config through would make that visible.

Note: this closes a #2625 item. "Support multiple mini-steps inside a single RL step" is still unchecked there, and self._ppo_epochs / for epoch in range(self._ppo_epochs) in single_controller.py is that. Worth ticking off when this lands.

Checked and clean — listing these so you know where I did not find anything:

  • ppo.py parity. Enumerated every ppo.* key v1 reads. policy_training_start_step is honoured (11 sites), ppo_epochs is honoured. batch_multiplier is absent but gated on use_dynamic_sampling, already tracked in [Single Controller / Async RL] cleanup tracking issue #2625.
  • The dict → model migration. estimator_config[ and estimator_config.get( both return nothing tree-wide, so no subscript survived.
  • The fix(sc): reduce and finalize gradients once per step, not per chunk #3660 shape on the value model. Went looking for the critic's gradient being reduced per chunk instead of per step. You avoided it deliberately and said why in the _train_pump docstring.
  • And that invariant is enforced, not assumed — the thing I came to check. _validate_algo_settings in config.py hard-errors when min_groups_for_streaming_train != num_prompts_per_step under PPO, with a message naming the critic's per-chunk optimizer step. Right place for it.

Three lenses I did not get to: the GAE recursion itself (mask handling at sequence ends, VAPO decoupled λ, normalize_advantages's axis), driver_mixin.py versus the 77 lines leaving tq_policy.py, and whether the six-file rename dropped a test. Happy to take any — say which and I will do it against the branch rather than guess :)

@tianyi-zhang-02

tianyi-zhang-02 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closing two of the three lenses. Both clean, no action needed.

The six-file rename drops no test. Collected tests/unit/single_controller/ on both trees and diffed the ID sets with the filename stripped, so a test moving between renamed files does not register as a change:

clean main (ffbf33f3): 357 collected
this branch:           424 collected
main-only:             (empty)

67 net new, nothing lost.

driver_mixin.py is a real extraction, not a parallel implementation. tq_policy.py loses exactly _stamp_pad_seqlen, read_from_dataplane, write_to_dataplane, _packing_args; the mixin provides exactly those four plus a new _isolated_meta. Two consumers — TQPolicy(TQDriverMixin, Policy) and TQValue(TQDriverMixin, Value) — so it earns being shared.

That leaves the GAE recursion: mask handling at sequence ends, the decoupled gae_lambda_value / gae_lambda_policy with length_adaptive_alpha, and which axis normalize_advantages reduces over. That one I would want to run rather than read — it is where reading is least reliable, and I would rather say so than give you a confident-sounding opinion about a recursion I only eyeballed :)

@yuki-97 yuki-97 mentioned this pull request Aug 24, 2026
@yuki-97

yuki-97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 9d61ad0

@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Aug 24, 2026

@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.

Self-review pass over the whole diff, done with a team of specialised agents plus an adversarial verification round. 14 inline comments below: 3 correctness bugs, 9 suggestions, 1 batched nit.

Rebase needed -- the PR is currently CONFLICTING against main. Worth doing before the comments below, since a few of them touch files that will move.

What held up well, so it does not get lost among the findings: test coverage is genuinely strong -- ppo_epochs > 1, the critic warmup path, the value stage, the pad-target isolation and save/resume across the warmup boundary all have real unit coverage, and three reviewers independently said so. _validate_algo_settings is the right pattern, with seven config-time rejections whose messages each name the fix. Keeping PPO_VALUE_FIELDS out of DP_TRAIN_FIELDS so a missing column errors rather than reading zeros is the correct call. driver_mixin.py is a real two-consumer extraction. The Megatron loss-averaging counteraction from 6e103d0 is fully preserved, VPP is asserted off, PP is handled correctly, and every Megatron API call checks out against the pinned SHA.

Checked and cleared, listed so nobody re-derives them: the GAE recursion, masking and normalize_advantages axis are unchanged at the merge base; the logprobs -> logprobs_policy rename is a fix rather than a break (SC already used the new names, so its KL kwargs were being swallowed by **kwargs); train_iters = max_num_steps * ppo_epochs matches ppo.py verbatim; old logprobs, values and advantages are computed once before the epoch loop, which is correct PPO; the six-file rename drops no test and leaves no stale reference anywhere in the tree; both new test files genuinely collect in their CI shards.

Two candidate findings died in verification and are recorded here rather than posted. The nightly GPU-hour cap going to 4190 when the measured total is 4155 looked like slack that should be tightened -- but git log -L on that assertion shows the cap has never been an exact fit, so 35 hours of headroom is normal practice. And adv_estimator.name: raw_reward reaching the critic with no returns column is real, but ppo.py:1580 does the identical thing, so it is ported behaviour, not something this PR introduces.

One scope note: roughly 60 lines of the diff are unrelated churn -- pyrefly.toml re-sorted end to end, the L1 driver reordered (which is what carried away the 42 comments flagged below), and two of the six renames are pure no-ops. Splitting the rename and re-sort commits out would make this easier to review.

Generated by Claude Code

Comment thread nemo_rl/algorithms/single_controller_utils/setup.py Outdated
Comment thread nemo_rl/models/value/tq_value.py
Comment thread pyrefly.toml
Comment thread nemo_rl/algorithms/single_controller.py
Comment thread tests/functional/L1_Functional_Tests_SingleController.sh
Comment thread tests/unit/single_controller/test_ppo_setup.py
Comment thread nemo_rl/algorithms/single_controller_utils/config.py Outdated
@yuki-97

yuki-97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test b5b1bb8

@yuki-97

yuki-97 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 90c48c9

@yuki-97
yuki-97 marked this pull request as ready for review August 24, 2026 16:02
@yuki-97
yuki-97 requested review from a team as code owners August 24, 2026 16:02
@tianyi-zhang-02

tianyi-zhang-02 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Looked at the five reds. They are two separate things, and I could only pin one down.

The three L0_Unit_Tests_Megatron_Policy_* shards are one missing line in a test fixture.

All three fail the same way:

if self._first_train_step_forward_pre_hook_disabled and update_successful:
E   AttributeError: 'MegatronPolicyWorkerImpl' object has no attribute '_first_train_step_forward_pre_hook_disabled'
megatron_policy_worker.py:1657

bea00ff added that read in _finish_train_step_body. The attribute is only set in __init__, at megatron_policy_worker.py:599. And test_megatron_split_state.py:113 builds the worker with object.__new__(MegatronPolicyWorkerImpl) to skip __init__, then hand-sets what it needs — there is already a comment there saying exactly that about another attribute:

# Normally set from get_rank_safe() in __init__, which object.__new__ skips.

So the fixture needs one more line next to it, w._first_train_step_forward_pre_hook_disabled = False, matching the __init__ default. Nothing wrong with the production change.

fast_L1_Functional_Tests_Megatron_4 is unrelated, and I could not attribute it.

A threshold miss in grpo_megatron_generation_topp_topk:

FAIL  max(data["train/token_mult_prob_error"]) < 1.06   ->  1.0856791734695435

2.4% over. With no main baseline I cannot tell whether this PR moved it or it was already close. One thing that might help you decide: git log -L on that assertion shows it has been raised once already, 1.05 → 1.06 in #3392, so it has a history of being tight. If you have a recent green main run of that job, its max value would settle it.

CI quality check is just the roll-up of the four :)

@yuki-97

yuki-97 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 8fda593

Signed-off-by: Yuki Huang <yukih@nvidia.com>
… modules back in pyrefly project-includes, add the value stages to the data-flow diagram

Signed-off-by: Yuki Huang <yukih@nvidia.com>
… hoist the warmup-lookahead guard onto the GRPO path, unprefix shared validation messages, retune the nightly thresholds

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…r-identical groups

Removed:

- test_ppo_setup.py TestPPOValidation::test_the_ppo_schema_rejects_a_non_ppo_estimator
  asserted exactly what test_ppo.py::test_ppo_schema_rejects_unsupported_estimator_name
  already does, and it covers the PPOConfig schema rather than SC setup.
- test_single_controller_actor.py::test_train_pump_loads_and_offloads_the_critic_around_each_stage
  asserted a critic call sequence that is a strict subsequence of the interleaved
  list in test_train_pump_parks_the_policy_on_cpu_across_the_critic_stages.
- test_single_controller_actor.py::test_train_pump_steps_both_optimizers_once_per_ppo_epoch
  asserted per-epoch counts that follow from the exact call list in
  test_train_pump_offloads_the_policy_between_ppo_epochs; its two unique
  assertions, one refit and one version bump per RL step, moved there.
- test_sampler_interface.py TestWarmupLookaheadWindow::test_set_gate_window_retunes_admission
  asserted the private _gate_window; the setter's observable effect is covered by
  test_capacity_does_not_shrink_when_the_gate_is_retuned and by TestLookaheadSchedule.

Parametrized, with the same set of cases as before:

- TestIsPPORun's three tests differed only in the config handed to a one-line
  predicate.
- TestTrainClusterSizesForTheCritic's five tests were one body over
  (colocated, backend, algorithm); the _groups helper goes with them.
- TestMegatronTrainIters' two injection tests differed only in ppo_epochs.
- test_train_pump_runs_every_critic_epoch_during_warmup folds into
  test_train_pump_freezes_the_policy_during_critic_warmup as ppo_epochs=2, with
  the critic-train count asserted against ppo_epochs.
- test_init_leaves_the_critic_handles_unset_on_a_grpo_run folds into
  test_init_picks_up_the_critic_handles as the handles-absent case.
- TestPPOWarmupCheckpoint's two optimizer-path tests differed only in
  is_policy_training_step. The metric pair stays split: one expects a warning,
  the other an exception.
- test_tq_value.py's two get_values_from_meta tests shared a patch stack; the
  packing-budget check is now one assertion in the surviving test.

test_rejects_a_config_with_neither_block and test_rejects_a_config_with_both_blocks
move to TestPPOValidation, since both exercise validate_single_controller_config
rather than algo_config.

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…orker, cover the finish-path re-enable

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…PO-only drop-budget constraint

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…w guards, make the warmup top-k assertion observable, tighten the nightly GPU-hour cap to 4149

Signed-off-by: Yuki Huang <yukih@nvidia.com>
Signed-off-by: Yuki Huang <yukih@nvidia.com>
…, guard ppo.async_ppo=null in run_ppo.py, make the advantage-estimator logprob args keyword-only, document the colocated branch as unreachable

Signed-off-by: Yuki Huang <yukih@nvidia.com>
…b is required

Signed-off-by: Yuki Huang <yukih@nvidia.com>
@yuki-97

yuki-97 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 6d303d3

terrykong
terrykong previously approved these changes Aug 25, 2026
Signed-off-by: Yuki Huang <yukih@nvidia.com>
@yuki-97

yuki-97 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test b60aee0

@yuki-97
yuki-97 merged commit d938da3 into main Aug 26, 2026
85 checks passed
@yuki-97
yuki-97 deleted the yukih/sc-ppo branch August 26, 2026 02:11
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 26, 2026
Rebased onto NVIDIA-NeMo#3773, which added overlong_filtering to the unsupported list --
"SC reads none of these on either path, so an enabled one describes shaping
this run does not do". That reasoning is right while nobody implements it, and
this PR implements it: build_sample_mask drops truncated rows from the loss,
from the flag the Completion already carries. So the rejection comes off the
list and its test becomes the opposite assertion.

Also fixes a real break from the rebase: the TQReplayBuffer wiring read
grpo_config.overlong_filtering, and NVIDIA-NeMo#3773 made master_config.grpo optional, so
that name no longer exists. It now goes through algo_config(master_config) like
everything else in that file. test_setup_forwards_latest_resume_paths catches
it.

Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
asolergi-nv added a commit that referenced this pull request Aug 27, 2026
14 upstream commits; four of them produced seven conflicts. Each resolution below.

#3612 feat(sglang): megatron backend weight refit for sglang rollouts
  - weight_sync/factory.py: it rewrote the train_cluster/inference_cluster/
    refit_buffer_size_gb docstrings (SGLang owns its own process group, so it needs
    neither cluster handle). Took its wording and kept our refit_timeout_s entry, which
    it never saw. refit_timeout_s still reaches NcclReshardWeightSynchronizer and
    CollectiveWeightSynchronizer; the new SGLang synchronizer does not take it, which is
    correct -- our watchdog bounds a JOINT communicator and SGLang does not build one.
  - base_policy_worker.py: it added _refit_transport_state and
    connect_sglang_rollout_engines at the same insertion point as our
    stand_down_refit_watchdog. Disjoint additions; kept both.
  - pyrefly.toml: it swapped http_weight_synchronizer for sglang_weight_synchronizer.
    Corroborated by the merge deleting http_weight_synchronizer.py outright.

#3773 feat(sc): support PPO in single controller
  - single_controller_utils/setup.py: the SC path is no longer GRPO-only, so it renamed
    grpo_config to algo_cfg. Kept our nccl_reshard precondition guard and applied the
    rename to the val-period line inside it; grpo_config no longer appears anywhere.
  - L1_Functional_Tests_SingleController.sh: it added a ppo_async run_test and padded
    every non-fast entry to align with "run_test fast". Kept our annotation -- it says
    which of skip-vs-pass a green lane actually means, which its one-line version does
    not -- and adopted the alignment, including on our seven recovery entries, so the
    file does not end up half-converted.
  - pyrefly.toml: it re-sorted the list, moving vllm_remote_sparse_weight_synchronizer to
    its correct alphabetical slot. Our side had added membership.py AND held that entry in
    the old position, so taking our block verbatim would have duplicated it. Kept
    membership.py only; verified the result is sorted and has no duplicates.

#3545 fix(vllm): support native BF16 FlashInfer TRTLLM refit
  - vllm_backend.py: its _nrl_layerwise_reload_* class attributes landed where our
    model_update_group declaration is. Disjoint; kept both.
  - tests/unit/models/generation/test_vllm_backend.py: its layerwise-reload suite against
    our init_collective release tests plus the _RecordingGroup fixture. Disjoint; kept
    both. 53 tests collect.

#3768 feat: add MOPD to single-controller text path
  - Touched setup.py alongside #3773; no separate resolution needed.

Submodule: the merge advances Megatron-Bridge to d352aced (#3824). Verified the STAGED
pointer is upstream's and not our stale 8c46dc42 -- staging the local one is what breaks
the fast-forward check and `uv lock --check` together. Gym is untouched by the merge.

Verified after resolving: no conflict markers remain, all four lint hooks clean (the one
pyrefly error is the pre-existing unrelated transfer_queue import), and 841 unit tests pass
across single_controller, refit_watchdog, worker_refit_signatures and weight_sync -- up
from 725, because #3773 brings a large new SC suite that passes alongside ours.

Signed-off-by: asolergibert <asolergibert@nvidia.com>
asolergi-nv added a commit that referenced this pull request Aug 27, 2026
…o PR4

Brings PR3's sync with upstream/main down the stack. Three conflicts, all between PR4's
own additions and changes that arrived from main:

  pyrefly.toml
    #3773 re-sorted the list and moved generation/fleet_health.py to its correct
    alphabetical slot. PR4's block added engine_supervisor.py AND held fleet_health.py in
    the old position, so taking it verbatim would have duplicated the entry -- the same
    trap this file set on PR3 with vllm_remote_sparse_weight_synchronizer. Kept
    engine_supervisor.py only, placed where it sorts (after dynamo/, before fleet_health).
    Verified the whole nemo_rl list is sorted and duplicate-free.

  nemo_rl/algorithms/single_controller.py
    PR4's EngineSupervisor wiring against #3768's MOPD TQTeacherLogprobCoordinator, both
    landing in the same __init__ region. Disjoint, so both kept, with the coordinator
    first because it installs a post-write enricher on the buffer.

    Also removed a duplicate this merge would otherwise have introduced: main relocated
    the `_rollout_manager._tq_buffer = self._buffer` rebind next to the assignment it
    guards, so PR4's copy at the old site became redundant. Confirmed main carries it
    exactly once before deleting the second.

  tests/functional/L1_Functional_Tests_SingleController.sh
    PR4's RESTART_DEAD_SHARDS entry against the column alignment #3773 introduced. Kept
    the entry and applied the alignment to it and to every other unpadded line, so the
    file is not left half-converted.

Submodules: both pointers match upstream/main exactly (Megatron-Bridge d352aced from
#3824, Gym c3bac963), so the fast-forward check and `uv lock --check` both see a clean
state. The locally dirty submodule working trees were deliberately not staged.

Verified: no markers remain, all four lint hooks clean (the single pyrefly error is the
pre-existing unrelated transfer_queue import), 849 unit tests pass.

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.

4 participants