Skip to content

feat(mopd): support multiple teacher checkpoints in full-vocabulary o… - #4045

Merged
yfw merged 13 commits into
mainfrom
ruit/full_vocab_v2_multi_teacher
Sep 21, 2026
Merged

yfw merged 13 commits into
mainfrom
ruit/full_vocab_v2_multi_teacher

Conversation

@RayenTian

Copy link
Copy Markdown
Contributor

…pd_full

Drops the "exactly one unique teacher checkpoint" restriction on on_policy_distillation.full's hidden_states path. Each physical teacher gets a stable index; teacher worker groups tag every payload row they write with it (new OPD_FULL_TEACHER_INDEX_FIELD column); the student loads one LM-head shard per teacher (dict keyed by index, same offload/evict lifecycle as before) and groups a microbatch's rows by teacher_index to project each group through its own shard rather than requiring a single teacher for the whole run.

What does this PR do ?

Add a one line overview of what this PR aims to accomplish.

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

  • ...

@copy-pr-bot

copy-pr-bot Bot commented Sep 8, 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.

@RayenTian
RayenTian force-pushed the ruit/full_vocab_v2_multi_teacher branch from 7384eba to 5dc5548 Compare September 8, 2026 14:57
@RayenTian
RayenTian force-pushed the ruit/mopd_full_vocabulary_v2 branch 2 times, most recently from b4c4f6e to 0823c83 Compare September 8, 2026 21:09
@RayenTian
RayenTian force-pushed the ruit/full_vocab_v2_multi_teacher branch from 5dc5548 to 6305f72 Compare September 8, 2026 21:38
@RayenTian
RayenTian force-pushed the ruit/mopd_full_vocabulary_v2 branch from 0823c83 to 8603a46 Compare September 9, 2026 19:41
@RayenTian
RayenTian force-pushed the ruit/full_vocab_v2_multi_teacher branch from b8bafa3 to 15839dc Compare September 9, 2026 21:18
Base automatically changed from ruit/mopd_full_vocabulary_v2 to main September 10, 2026 08:10
@RayenTian
RayenTian force-pushed the ruit/full_vocab_v2_multi_teacher branch from 15839dc to 63c2754 Compare September 10, 2026 18:07
@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label Sep 10, 2026
@RayenTian RayenTian added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Sep 10, 2026
@RayenTian

Copy link
Copy Markdown
Contributor Author

/ok to test 63c2754

@RayenTian
RayenTian marked this pull request as ready for review September 11, 2026 17:54
@RayenTian
RayenTian requested review from a team as code owners September 11, 2026 17:54
RayenTian and others added 3 commits September 11, 2026 16:29
Squashes four commits from this branch's iteration on top of #3978's
opd_full feature:

- Multiple teacher checkpoints on the hidden_states path: each physical
  teacher gets a stable index (OPD_FULL_TEACHER_INDEX_FIELD), the student
  loads one LM-head shard per teacher, and a microbatch's rows are grouped
  by teacher_index and projected through the matching shard.
- Student pipeline parallelism on the hidden_states path: earlier PP
  stages hold no teacher LM-head shard (only the last stage runs the
  loss), but still join the whole-world dist_checkpointing.load collective
  with an empty sharded request so the collective stays balanced.
- Review-finding fixes: repaired 16 test-double failures the teacher_index
  plumbing left behind, raised instead of silently falling through to the
  single-teacher projection when a routing column is missing, fixed
  pipeline-rank-asymmetric eviction tracking, and caught heterogeneous
  teacher hidden sizes/dtypes at load time instead of an opaque jagged-
  collate error later.
- Rebuilt the multi-teacher routing after a silent rebase-onto-main
  conflict: main reordered reconstruct_opd_full_teacher_logits into
  validate -> narrow -> project (so the LM-head matmul only ever sees this
  rank's CP window), but this branch's multi-teacher commits were written
  against the older project-then-narrow order and the rebase merged both
  textually into an incoherent function. Kept main's order and slotted the
  routing checks into the validate phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: ruit <ruit@nvidia.com>
…_size

chunk_size on the opd_full divergence kernels
(ChunkedDistributedReverseKLToFixedLogits / CrossEntropyToFixedLogits /
Entropy) only bounded the transient fp32 working set inside each kernel's
internal forward/backward loop over sequence chunks -- the single
autograd.Function node wrapping that loop still called
ctx.save_for_backward() on the whole, un-chunked [B, S, V_local] student
and teacher logits. So backward's peak retained memory scaled with the
full local vocabulary width regardless of chunk_size, which is what an
OOM during opd_full backward traced back to.

Restructures the three kernels into thin dispatchers over a new
_ChunkedStudentTeacherChunk autograd.Function: one node per sequence
chunk, each saving only its own chunk (materialized via .clone(), not
.contiguous() -- a middle-dim slice of an already-contiguous tensor can
itself report is_contiguous() == True, so .contiguous() would be a no-op
that still aliases the full parent's storage). torch.cat's own backward
routes the upstream gradient back to each chunk node. External
ClassName.apply(...) call sites are unchanged.

Verified forward/backward numerics against a from-scratch reference
(reverse_kl, cross_entropy, entropy; chunk_size in {1, 2, 3, 100}) plus
self-distillation (KL ~= 0), teacher-no-gradient, misaligned-shape
rejection, and bf16 round-trip -- all within rtol=1e-4. Confirmed
structurally that the autograd graph now holds one independent
_ChunkedStudentTeacherChunkBackward node per chunk, each fed by its own
CloneBackward0, rather than one node holding the whole tensor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: ruit <ruit@nvidia.com>
… padding/truncation

materialize()'s pad_to_seqlen step forced teacher_full_hidden_states /
teacher_full_logits to pad to the global cross-DP max sequence length even
though they never feed the model forward (only the per-sequence loss, which
reads each row's own real length) -- at 98304 tokens x hidden_size wide
across a whole DP-rank's batch this produced 100+ GiB single allocations
and was the direct cause of opd_full OOMs on long-context full-vocabulary
runs.

Exempts OPD_FULL_FIELDS from that padding in codec.py::materialize, and
adds the matching exemption to the two downstream checks that assumed every
tensor field shares one padded seqlen: megatron/data.py's
get_and_validate_seqlen (hit live once the payload was allowed to keep its
own shorter length) and batched_data_dict.py's truncate_tensors (the
dynamic_batching microbatch path's analog, not yet exercised but same gap).

Signed-off-by: ruit <ruit@nvidia.com>
@RayenTian
RayenTian force-pushed the ruit/full_vocab_v2_multi_teacher branch from 63c2754 to 549953d Compare September 11, 2026 23:31
@RayenTian
RayenTian requested a review from a team as a code owner September 11, 2026 23:31
@RayenTian

Copy link
Copy Markdown
Contributor Author

/ok to test 549953d

…ption

test_materialize_pads_a_3d_payload_on_the_seq_dim_only used
OPD_FULL_LOGITS_FIELD as its example 3-D column, so it broke when
materialize() started exempting OPD_FULL_FIELDS from pad_to_seqlen (see
549953d). Swaps it to a synthetic field name to keep covering the
generic 3-D pad-spec construction it actually tests, and adds a dedicated
regression test asserting the new exemption itself.

Signed-off-by: ruit <ruit@nvidia.com>
@RayenTian

Copy link
Copy Markdown
Contributor Author

/ok to test 7db9f0d

Comment thread tests/unit/data_plane/test_codec_jagged.py Outdated
Comment thread nemo_rl/distributed/model_utils.py Outdated
Comment thread nemo_rl/distributed/batched_data_dict.py Outdated
Comment thread nemo_rl/algorithms/opd.py Outdated
Comment thread nemo_rl/algorithms/loss/utils.py Outdated
Comment thread tests/unit/models/policy/test_teacher_worker_group.py
Comment thread tests/unit/algorithms/test_opd_full.py
Comment thread tests/unit/algorithms/test_opd_full.py
Comment thread tests/unit/models/policy/test_split_api_wrappers.py
Comment thread tests/unit/models/policy/test_megatron_worker.py
The hang the non-last pipeline stages guard against comes from
Megatron-Bridge's read_train_state broadcast during path resolution, not
from dist_checkpointing.load, which is rank-local under
validate_access_integrity=False. Reword the six spots that claimed
otherwise.

Also make the opd_full transport keys required rather than .get-with-
fallback in TQPolicy, and drop the teacher_index default on
load_opd_full_teacher_lm_head: both silently degrade to "teacher 0" on a
caller bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
bg51717 and others added 5 commits September 19, 2026 03:45
…allbacks

Three silent-corruption paths in full-vocabulary MOPD:

- teacher_index was numbered from the sorted alias list, but which alias
  represents a checkpoint shared by several agents is first-seen-wins
  over teacher_model_by_agent_name. The index outlives a run inside a
  data-plane checkpoint, so reordering the YAML repointed already-tagged
  rows at another teacher's LM head. Number by the deduplicated
  checkpoint instead, record the per-index checkpoint paths in the
  data-plane checkpoint metadata, and reject a restore that would
  renumber them. Stored as a list ordered by index rather than a map,
  since the metadata round trips through JSON.

- reconstruct_opd_full_teacher_logits branched on how many LM heads were
  loaded, so a single-teacher run ignored the routing column entirely and
  projected a row tagged for an unloaded teacher through the only head it
  had. Route whenever the column is present.

- truncate_tensors exempted the teacher payload from dynamic batching's
  narrow, which made every microbatch shorter than the fetch raise in the
  loss. Clamp to min(truncated_len, its own width) instead: materialize
  leaves the payload at its natural width, which can already be shorter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
…ycle

Every raise and lifecycle branch added for multi-teacher opd_full was
invisible to the suite: deleting the guard, the row-count check, or the
evict bookkeeping left it green.

- teacher worker: a routing column configured without an index is
  rejected before the forward runs.
- reconstruct/prepare: several heads without a routing column, and an
  index column whose length does not match the payload.
- TQPolicy: prepare_step, prepare_val_partition and train_from_meta all
  request the teacher-index column, not just train_microbatches_from_meta.
- megatron worker: evict reloads every teacher once per release and
  resets the flag, ranks off the last pipeline stage re-enter the reload
  from their recorded paths, offload parks every head, a second teacher
  with another hidden size is rejected at load, a shardless rank still
  records its path, and owner resolution returns None only off the last
  stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
This reverts commit a6e20cc.

Splitting the backward into per-chunk autograd nodes did not bound the
retained memory: the per-chunk clones sum to exactly one full copy each
of the student and teacher logits, so the retained set never varied with
chunk_size. Each chunk's SliceBackward0 then materializes another full
[B, S, V_local] zero tensor that the engine accumulates into a second
full buffer, and the forward loop briefly holds the originals alongside
their clones.

Measured peak (B=2, S=1024, V_local=8192, chunk 128): fp32 232 -> 296
(+28%), bf16 unchanged. defer_fp32_logits defaults to False, so the fp32
row is the default path. Backward also got slower in both dtypes. The
two designs agree numerically and the kernel tests pass either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
torch.empty_like(..., dtype=torch.float32) held a full fp32
[B, S, V_local] buffer that autograd immediately cast back down to the
leaf's dtype. Allocate it in the logits' dtype instead and move both
multiplies ahead of the copy_, so the arithmetic still happens in fp32
and bf16 avoids an extra rounding.

Measured peak (B=2, S=1024, V_local=8192): bf16 168 -> 144, fp32
unchanged, no forward cost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
Signed-off-by: bg51717 <biguo@nvidia.com>

# Conflicts:
#	nemo_rl/algorithms/single_controller_utils/setup.py
#	nemo_rl/data_plane/codec.py
#	nemo_rl/models/megatron/data.py
#	nemo_rl/models/policy/tq_policy.py
@bg51717

bg51717 commented Sep 19, 2026

Copy link
Copy Markdown
Member

/ok to test 5db3ec9

bg51717 and others added 2 commits September 19, 2026 08:53
Defaulting it to 0 tags every teacher's payload rows as teacher 0, and both
lookups that key on the attribute -- setup.py's per-checkpoint RPC dict and
opd.py's checkpoint-by-index map -- are dict comprehensions, so the collision
collapses them to a single entry instead of raising. The student would then
project every row through one teacher's LM head at legal shapes.

Also fix the attribute's comment: create_teacher_worker_groups has ordered the
index by checkpoint since teacher_configs_by_index landed, not by alias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
Buffered rows in a data-plane checkpoint carry a teacher_index each and nothing
re-tags them on restore, so _maybe_restore_native_data_plane_checkpoint refuses
a checkpoint whose opd_full teacher list differs from the one this run would
number. That guard shipped without a test -- no test in the repo referenced
opd_full_teacher_checkpoints at all.

Covers renumbering, a dropped teacher, both directions of the untagged/tagged
transition, and the matching case that must still restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
@bg51717

bg51717 commented Sep 19, 2026

Copy link
Copy Markdown
Member

/ok to test 105b4f6

…estore fixture

This test builds SingleControllerActor via object.__new__, bypassing
__init__, so it never gets the _teacher_coordinator attribute __init__
always sets (to a coordinator or None). The data-plane checkpoint-save
path added by this PR reads self._teacher_coordinator unconditionally,
so this fixture's controller raised AttributeError instead of running.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: bg51717 <biguo@nvidia.com>
@bg51717

bg51717 commented Sep 20, 2026

Copy link
Copy Markdown
Member

/ok to test c6fdfc3

@bg51717
bg51717 requested a review from yfw September 20, 2026 09:58
@yfw
yfw merged commit 93b1c1d into main Sep 21, 2026
94 of 95 checks passed
@yfw
yfw deleted the ruit/full_vocab_v2_multi_teacher branch September 21, 2026 17:33
lauradang added a commit to lauradang/RL that referenced this pull request Sep 22, 2026
Bring main a9c3178 (25 commits since the previous merge base 702cd79) into
the branch on top of the #5 submodule bump (00621c2).

Conflicts:
- nemo_rl/algorithms/single_controller_utils/setup.py and
  tests/unit/single_controller/test_setup.py: keep main's sharded Gym stacks
  (NVIDIA-NeMo#3373), multi-teacher MOPD (NVIDIA-NeMo#4045), critic optimizer rebuild (NVIDIA-NeMo#4213) and
  Mooncake storage checkpoints (NVIDIA-NeMo#3898) next to the Megatron token-capture
  setup guards and backend wiring; the vLLM-only capture guard is replaced by
  the backend-aware one.
- tests/unit/models/generation/test_vllm_token_capture_hosting.py: keep main's
  stripped staged token arrays (NVIDIA-NeMo#4206) on top of the shared vLLM chain-cache
  prefix resolution.
- pyproject.toml (semantic conflict, auto-merged by git): main NVIDIA-NeMo#3566 removed
  `exclude-dependencies = ["nvidia-cutlass-dsl-libs-base"]` in favour of
  per-version dependency-metadata overrides for CUTLASS DSL 4.5.x, while this
  branch's mcore extra pinned nvidia-cutlass-dsl[cu13]==4.8.0.dev0 (68b63da).
  Without the exclusion that pin drags in the pre-release
  nvidia-cutlass-dsl-libs-base==4.8.0.dev0, which uv refuses for a transitive
  dependency, so `uv lock` no longer resolves. Pin 4.6.2 instead: it is the
  stable release flashinfer-python 0.6.18.post1 actually requires
  (>=4.6.2a0), and it is the pin NVIDIA-NeMo#4139 uses for the same
  Megatron-Bridge bump on main.
- uv.lock: regenerated from main's copy with uv 0.11.28 (the docker/Dockerfile
  pin), so it carries main's new git pins (Model-Optimizer, sglang, Lens,
  TransferQueue, nvidia-resiliency-ext, torch_memory_saver) together with the
  Gym and Megatron-Bridge main pins and the flashinfer index spelling from #5.
  `uv lock --check` passes with uv 0.11.28 and 0.12.5.

Submodule pins: Automodel follows main (72daceffa); Gym (9fc05c0ff) and
Megatron-Bridge (fcea83a85) stay at the #5 pins, which descend from main's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Laura Dang <laurad@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.

3 participants