Skip to content

feat: add MOPD to single-controller text path - #3768

Merged
terrykong merged 8 commits into
mainfrom
yifu/mopd-single-controller-text
Aug 26, 2026
Merged

feat: add MOPD to single-controller text path#3768
terrykong merged 8 commits into
mainfrom
yifu/mopd-single-controller-text

Conversation

@yfw

@yfw yfw commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

Adds text-only Multi-Teacher On-Policy Distillation (MOPD) support to the new SingleController GRPO path.

Previously, MOPD was supported only through the legacy controller. This ports the feature to the TransferQueue-based SingleController flow while keeping teacher inference non-colocated.

Implementation

  • Validates the SingleController MOPD configuration before allocating resources:
    • on_policy_distillation.enabled=true
    • grpo.adv_estimator.name=opd
    • NeMo Gym rollouts
    • non-colocated teachers
  • Reserves topology-constrained policy and teacher resources before other workers can consume them.
  • Creates and attaches teacher worker groups to the SingleController data plane.
  • Routes each completed prompt group to its teacher using NeMo Gym agent_ref metadata.
  • Runs teacher inference as a required post-write TQ enrichment stage:
    1. rollout tensors are written to TQ;
    2. teacher workers fetch their presharded rows;
    3. teachers write teacher_reference_logprobs back to the same rows;
    4. the rollout becomes available for training.
  • Builds sequence-packing or dynamic-batching plans globally on the driver using logprob_mb_tokens.
  • Serializes requests targeting the same physical teacher while allowing distinct teachers to run concurrently.
  • Handles teacher DP padding, cleanup, cancellation, rollback, and grouped enrichment failures.
  • Feeds student and teacher logprobs into the OPD advantage estimator.
  • Pools OPD metrics exactly across unequal streaming chunks using sum, sum-of-squares, and token count.
  • Warms the long-lived TQ partition schema before concurrent writers start.

Limitations

  • This PR supports text-only MOPD. Multimodal/VLM teacher inputs are not yet forwarded through the SingleController TQ path.
  • MOPD currently requires NeMo Gym and non-colocated teacher workers.
  • Reference-logprob field narrowing is handled separately by fix(sc): gate reference logprobs on KL penalty #3742.

Issues

List issues that this PR closes (syntax):

Usage

  • You can potentially add a usage example below
# Add a code snippet demonstrating how to use this

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

  • ...

yfw added 2 commits August 21, 2026 02:52
Route teacher logprob inference through TransferQueue with deduplicated per-teacher serialization and wire OPD advantages into the single controller. Add configuration validation, failure cleanup, unit coverage, and a same-model nightly recipe.

Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
Build teacher dynamic-batching metadata globally, warm the TransferQueue schema, and make teacher enrichment cleanup and cancellation safe. Pool OPD metrics from exact sufficient statistics and expand topology, routing, recipe, and lifecycle coverage.

Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 22, 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 22, 2026
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
@yfw

yfw commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 5804832

Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
@yfw

yfw commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 7e7d721

@yfw yfw 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 by 5 coordinated agents (rl-expert, bug-finder, test-agent, design-reviewer, devil's advocate) at head 7e7d721.

Overall: high-quality port — recommend addressing the two bugs before merge; nothing here is release-blocking. All 173 affected unit tests pass locally on this head, lint is green (thanks for the quick import-sort fix), recipe/nightly registration follows the conventions, and docstrings are complete.

Worth calling out as good design, explicitly: the typed PostWriteEnrichmentError enrichment seam (the buffer stays OPD-agnostic, failures classify by exception type, and the rollout retry budget is correctly not spent on post-write failures); TQTeacherLogprobCoordinator being fully unit-testable without Ray/GPUs; reuse of the legacy teacher reserve/create path with the init-ordering discipline pinned by a test; and the fail-fast MOPD validation before any resource allocation.

Findings summary: 2 bugs on config paths adjacent to (not hit by) the shipped recipe — a teacher dynamic-batching crash (executed repro, verified fix attached) and a router-replay gate that breaks MoE-student MOPD — plus 9 low-severity suggestions (1 perf-evidence ask, 2 dead-code notes, 1 stall note, 1 error-type nit, 2 design/tracking notes, 2 verified test additions). Devil's-advocate pass: 10 findings confirmed, 1 dropped as unreachable, 1 folded.

Generated by Claude Code

Comment thread nemo_rl/models/policy/teacher_worker_group.py
Comment thread nemo_rl/data_plane/worker_mixin.py
Comment thread nemo_rl/algorithms/single_controller_utils/setup.py
Comment thread nemo_rl/algorithms/opd.py Outdated
Comment thread nemo_rl/algorithms/opd.py Outdated
Comment thread nemo_rl/algorithms/single_controller.py Outdated
Comment thread nemo_rl/data_plane/schema.py
Comment thread tests/unit/models/policy/test_teacher_worker_group.py
Comment thread tests/unit/single_controller/test_single_controller.py Outdated
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
@yfw yfw added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Aug 24, 2026
@yfw

yfw commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 359c895

@yfw
yfw marked this pull request as ready for review August 24, 2026 04:21
@yfw
yfw requested review from a team as code owners August 24, 2026 04:21
@yfw yfw changed the title feat(mopd): add single-controller text path feat: add MOPD to single-controller text path Aug 24, 2026

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

Important

This PR now has merge conflicts with main and needs a rebase. It was MERGEABLE when this review started and went CONFLICTING while the review was being written — four commits landed on main in between, including #3771 (fix(sc): skip reference logprobs when KL is disabled), which is the most likely overlap. None of them touched opd.py or tests/unit/algorithms/test_opd.py, so the findings below still apply as written, but the line numbers will shift after you rebase.

Re-review at 359c8958 by a team of 6 coordinated agents (rl-expert, bug-finder, test-agent, design-reviewer, an architecture tracer, and a devil's advocate).

All 9 findings from the previous review are verified genuinely fixed in b0b91a3 — three lanes checked each fix independently, including sibling branches, and found no partial fixes. Nothing below re-raises them. The 5-step legacy-vs-SC convergence comparison is accepted; nothing below re-requests performance or convergence evidence.

Overall this is a well-shaped port, and the hard parts are handled carefully: post-write TQ enrichment ordering, per-physical-teacher serialization with cross-teacher concurrency, DP padding with rollback under cancellation, driver-side global packing plans that keep DP collectives in sync, and exact pooled metrics across unequal streaming chunks. 4 low-severity items follow.

Design choices we examined and think are right — please don't churn these:

  • set_post_write_enricher is the right seam and the right size. A single optional callback invoked inside commit()'s existing try, so the enricher inherits the put_samples rollback path for free. We looked for a second post-write consumer and could not name one (multimodal MOPD is this same enricher widened, not a second one; reference logprobs are a train-pump stage). Turning it into a stage list would force ordering and partial-failure semantics that nothing today can pin down.
  • The metric accumulators should stay inline. They score 2 of 3 on extraction — closed state and low reach-back, but no independent lifecycle. A TeacherMetrics class here would be an anemic helper. The coordinator itself earns being a class: it owns the per-teacher locks and a real stateful protocol.
  • The setup ordering is pinned executably, not just in comments: assert events == ["reserve_teacher", "build_generation", "create_teacher"] turns "claim teacher GPUs before NeMo-Gym can take them, but load checkpoints after the student to avoid the shared HF-to-Megatron cache race" into a regression guard.

Two findings were raised and then killed by the adversarial pass, noted so they don't resurface: a proposed consistency guard in SingleControllerActor.__init__ (unreachable — validation enforces OPD-implies-teachers three times over) and a concern about the prev_logprobs/logprobs_policy kwarg branch (safe — validation is bidirectional and no estimator consumes both).

Caveat on verification: this review ran on macOS with no GPU, and the repo's uv.lock is linux-only, so no test and no linter was executed. Every suggestion below is marked unverified and should be run before merging.

Generated by Claude Code

Comment thread tests/test_suites/llm/mopd-qwen3-1.7b-3n8g-megatron-pack-single-controller.sh Outdated
Comment thread nemo_rl/algorithms/opd.py Outdated
Comment thread tests/unit/models/policy/test_teacher_worker_group.py Outdated
Comment thread nemo_rl/algorithms/async_utils/replay_buffer.py
Comment thread tests/unit/algorithms/test_opd.py
yfw added 2 commits August 25, 2026 22:57
Strengthen the SingleController MOPD nightly and batching/concurrency coverage. Reuse routing metric computation and omit cardinality values for idle enrichment intervals.

Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
…roller-text

Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>

# Conflicts:
#	nemo_rl/algorithms/single_controller.py
#	nemo_rl/algorithms/single_controller_utils/config.py
#	nemo_rl/algorithms/single_controller_utils/setup.py
#	nemo_rl/data_plane/schema.py
#	nemo_rl/data_plane/worker_mixin.py
#	tests/unit/single_controller/test_setup.py
#	tests/unit/single_controller/test_single_controller_actor.py
#	tests/unit/test_recipes_and_test_suites.py
@yfw

yfw commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 37f9fed

@terrykong
terrykong merged commit 7909525 into main Aug 26, 2026
85 checks passed
@terrykong
terrykong deleted the yifu/mopd-single-controller-text branch August 26, 2026 20:49
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 26, 2026
NVIDIA-NeMo#3768 added on_policy_distillation validation that reads
algo_cfg.adv_estimator. DistillationConfig has no such field -- its teacher
signal is the loss, not an advantage -- so a distillation config now dies with
an AttributeError inside the validator.

Reads it defensively and rejects the on_policy_distillation + distillation
pairing explicitly, the same way the PPO pairing is already rejected. The two
are different algorithms that both happen to use a teacher: MOPD routes
per-agent teachers on their own nodes and turns their logprobs into an
advantage, this path colocates one teacher with the trainer and feeds
DistillationLossFn.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 26, 2026
NVIDIA-NeMo#3768 added on_policy_distillation validation that reads
algo_cfg.adv_estimator. DistillationConfig has no such field -- its teacher
signal is the loss, not an advantage -- so a distillation config now dies with
an AttributeError inside the validator.

Reads it defensively and rejects the on_policy_distillation + distillation
pairing explicitly, the same way the PPO pairing is already rejected. The two
are different algorithms that both happen to use a teacher: MOPD routes
per-agent teachers on their own nodes and turns their logprobs into an
advantage, this path colocates one teacher with the trainer and feeds
DistillationLossFn.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 26, 2026
NVIDIA-NeMo#3768 added on_policy_distillation validation that reads
algo_cfg.adv_estimator. DistillationConfig has no such field -- its teacher
signal is the loss, not an advantage -- so a distillation config now dies with
an AttributeError inside the validator.

Reads it defensively and rejects the on_policy_distillation + distillation
pairing explicitly, the same way the PPO pairing is already rejected. The two
are different algorithms that both happen to use a teacher: MOPD routes
per-agent teachers on their own nodes and turns their logprobs into an
advantage, this path colocates one teacher with the trainer and feeds
DistillationLossFn.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 27, 2026
Not every algorithm block defines it. The OPD test doubles main added in
NVIDIA-NeMo#3768 build a bare namespace, and DistillationConfig has no reward to scale
at all, so a hard attribute read turns this into an AttributeError on paths
that never asked for scaling.

A missing block is a disabled one, which is the shipped default anyway.

Found by merging all twenty of my open PRs together: this passed alone and
failed against main's MOPD tests.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 27, 2026
…kens guard

The rebase onto main left the new AdvantageResult call sitting *after* main's
guarded block instead of replacing it, so the estimator ran twice and ran even
when the sequence mask had removed the whole chunk -- which is exactly what
test_advantage_stage_skips_estimator_when_seq_mask_removes_whole_chunk
asserts must not happen.

Also migrates the four estimator doubles main added since this PR opened
(NVIDIA-NeMo#3768's OPD ones plus the GAE-like tuple) onto AdvantageResult, which is the
point of the contract: a double that still returns a bare tensor or a tuple is
modelling an interface that no longer exists.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 27, 2026
…t-PPO

Same class as the reward_scaling read one commit up. Only GRPOConfig carries
advantage_clip_low/high; DistillationConfig does not, and the OPD test doubles
main added in NVIDIA-NeMo#3768 build a bare namespace. "Not PPO" is not the same as
"has the knob", so the not-is_ppo guard alone turns this into an
AttributeError on paths that never configured clipping.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 27, 2026
…ge reads

Nine controller stubs in this file predate that attribute, so they pass here
and fail once merged with current main -- NVIDIA-NeMo#3768 made _advantage_stage read it
unconditionally. Same shape as the _rollout_manager stub gap in NVIDIA-NeMo#3783.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 27, 2026
Two gaps that only show once this branch is merged with anything else:

  - nine controller stubs predate _teacher_logprobs_required, which main's
    NVIDIA-NeMo#3768 made _advantage_stage read unconditionally;
  - the estimator double returns a bare tensor, which NVIDIA-NeMo#3512 replaces with
    AdvantageResult.

The double now resolves AdvantageResult through the module rather than
importing it, so it returns whichever shape the checkout actually has and this
file does not depend on a name that exists only on the other branch.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
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>
tianyi-zhang-02 added a commit to tianyi-zhang-02/RL that referenced this pull request Aug 28, 2026
NVIDIA-NeMo#3768 added on_policy_distillation validation that reads
algo_cfg.adv_estimator. DistillationConfig has no such field -- its teacher
signal is the loss, not an advantage -- so a distillation config now dies with
an AttributeError inside the validator.

Reads it defensively and rejects the on_policy_distillation + distillation
pairing explicitly, the same way the PPO pairing is already rejected. The two
are different algorithms that both happen to use a teacher: MOPD routes
per-agent teachers on their own nodes and turns their logprobs into an
advantage, this path colocates one teacher with the trainer and feeds
DistillationLossFn.

Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
ZhiyuLi-Nvidia added a commit that referenced this pull request Sep 4, 2026
The three nightly entries this branch adds bring the suite from 4138 to
4266 GPU hours:

  +32  vlm_grpo-nemotron-omni-30ba3b-clevr-2n8g-megatron-tp8ep8.v1
  +32  vlm_grpo-nemotron-omni-30ba3b-clevr-2n8g-...v1-tq_mooncake
  +64  vlm_grpo-qwen3.5-35ba3b-geo3k-2n8g-automodel-ep16-tq_simple

Two of those are the VLM data-plane coverage itself; the third is the
2-node base the mooncake wrapper delegates to, which exists because the
recipe does not fit on one node.

Follows the convention for adding nightly coverage -- the cap has moved
4048 -> 4139 -> 4149 -> 4157 -> 4181 -> 4197 as recipes landed (#2744,
#3773, #3768, #3727, #3678).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Zhiyu Li <zhiyul@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