feat(mopd): add full-vocabulary on-policy distillation to SingleContr… - #3978
Draft
RayenTian wants to merge 2 commits into
Draft
feat(mopd): add full-vocabulary on-policy distillation to SingleContr…#3978RayenTian wants to merge 2 commits into
RayenTian wants to merge 2 commits into
Conversation
…oller
Adds `on_policy_distillation.full`, which replaces MOPD's sampled-token
log-probability gap with the exact full-vocabulary reverse KL
L_t = sum_v p_student(v) * (log p_student(v) - log p_teacher(v))
This is the K=V limit of the top-k MOPD estimator: with the support spanning
the whole vocabulary the score-function tail term vanishes, so the objective
is exact, deterministic, and free of that estimator's off-policy bias.
Because the loss now needs the teacher's whole distribution rather than one
scalar per token, the teacher/student boundary changes. Two payload paths are
supported, selected by `full.teacher_payload`:
* `hidden_states` (default): the teacher ships its pre-LM-head hidden states
and the student projects them with an output-layer weight shard loaded from
the teacher checkpoint. The payload is hidden_size wide, so teacher and
student parallelism stay fully decoupled.
* `logits`: the teacher ships full-vocabulary logits and the student needs no
teacher LM head. The payload is vocab_size wide -- roughly 74x larger for a
2k-hidden / 152k-vocab model -- so this is a numerical-reference and fallback
path, not a production configuration.
Implementation notes:
* Two chunked tensor-parallel autograd functions sit beside
ChunkedDistributedEntropy and share one backward: for both weights,
dw/dlog p_s is constant and cancels against the sum_v p_s = 1 normalization,
leaving dL/dz = p_s * (w - L). Both recompute their log-softmaxes in backward
rather than caching the teacher's, matching the neighbouring kernel.
* The reconstruction and divergence run in prepare_loss_input, the only place
holding the vocab- and context-parallel groups; the loss receives a plain
[B, S-1] differentiable tensor and reduces it the same way the policy-gradient
path does, including the nested per-sequence average under token_level_loss=false.
* Teacher hidden states are captured with a forward pre-hook on `output_layer`
rather than a Megatron fork. Sequence parallelism shards that input, so the
capture gathers it back -- without this a TP=1 run passes while TP>1 is
silently wrong.
* The payload rides a new per-token TransferQueue column, written from the
pipeline stage that produced it rather than broadcast back to the replica
leader, which for a per-token payload would move gigabytes for nothing.
* The teacher LM-head shard is a plain tensor on the worker, so it stays
invisible to checkpoint saving and to HF/vLLM refit conversion. Its checkpoint
path is resolved from the teacher's own model name: TeacherWorkerGroup copies
the student policy config wholesale, so a student `pretrained_checkpoint` would
otherwise win and the student would be distilled into itself.
* Loss knobs with no code path under this objective (ratio clipping, CISPO,
importance-sampling correction, truncated IS, VAPO) are rejected at
construction rather than silently ignored.
First version limits: exactly one teacher checkpoint, and the hidden-state path
additionally requires student pipeline_model_parallel_size=1, because Megatron
builds output_layer only on the last pipeline stage while the LM-head load is a
whole-world collective -- earlier stages would fail while the last stage hangs.
The `logits` path has no such restriction. The payload column is shaped so a
per-row teacher index can be added later without reworking it.
Ships a Qwen3-1.7B self-distillation recipe whose divergence must sit at ~0,
landing in disabled.txt until it has a real end-to-end run and the nightly
GPU-hour budget has room for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: ruit <ruit@nvidia.com>
…d payload guard Review follow-ups on the full-vocabulary MOPD commit. All four are config surface or comment fixes; no behavior changes on a correctly configured run. - Reject policy.sequence_packing.fuse_loss in _validate_opd_full_config. The fused packing path routes through prepare_packed_loss_input, which only supports LossInputType.LOGPROB and never reaches the opd_full branch, so the run died inside the first training forward instead of at startup -- after the whole cluster and every teacher had already come up. The analogous megatron_cfg.use_fused_linear_logprobs was already rejected here; this closes the asymmetry. examples/nemo_gym/nemotron-3-ultra/mopd.yaml sets fuse_loss: true, so one extra switch on a real production config was enough to hit this. - Reject loss_fn.use_on_policy_kl_approximation. It reweights the reference-KL term by a ratio only the policy-gradient branch computes, so opd_full silently ignored it. The fullvocab recipe inherited true from its grandparent, which is why it also has to set it false explicitly -- the same reason it already overrides use_importance_sampling_correction and truncated_importance_sampling_type. - Drop max_payload_bytes_per_batch, estimate_opd_full_payload_bytes and is_opd_full_enabled. The first documented a guard that refuses to start on an oversized teacher payload; nothing ever read it, and the other two had no callers at all. A promise in a comment with no implementation behind it is worse than no comment, because it stops the next reader from checking. The logits-path advisory stays, described as the print it actually is. - Correct the load_teacher_output_layer_weight docstring. Re-sharding a teacher saved at a different tensor-parallel width is dist_checkpointing's ordinary by-offset load and needs no special flag. allow_shape_mismatch covers a different case -- a teacher whose padded vocabulary differs -- where mcore zero-initializes and partially loads rather than raising. Also documents the TypeError the function already raises. Fixes three lint violations this branch introduced, all verified with ruff 0.16.6 against the repo config: one ruff-format wrap in megatron/train.py and two I001 import orderings (pre-commit runs `ruff check --select I` as its own hook, and line-length = 120 sits under [tool.black], so ruff-format uses its 88 default). Signed-off-by: ruit <ruit@nvidia.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: ruit <ruit@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…oller
Adds
on_policy_distillation.full, which replaces MOPD's sampled-token log-probability gap with the exact full-vocabulary reverse KLThis is the K=V limit of the top-k MOPD estimator: with the support spanning the whole vocabulary the score-function tail term vanishes, so the objective is exact, deterministic, and free of that estimator's off-policy bias.
Because the loss now needs the teacher's whole distribution rather than one scalar per token, the teacher/student boundary changes. Two payload paths are supported, selected by
full.teacher_payload:hidden_states(default): the teacher ships its pre-LM-head hidden states and the student projects them with an output-layer weight shard loaded from the teacher checkpoint. The payload is hidden_size wide, so teacher and student parallelism stay fully decoupled.logits: the teacher ships full-vocabulary logits and the student needs no teacher LM head. The payload is vocab_size wide -- roughly 74x larger for a 2k-hidden / 152k-vocab model -- so this is a numerical-reference and fallback path, not a production configuration.Implementation notes:
output_layerrather than a Megatron fork. Sequence parallelism shards that input, so the capture gathers it back -- without this a TP=1 run passes while TP>1 is silently wrong.pretrained_checkpointwould otherwise win and the student would be distilled into itself.First version limits: exactly one teacher checkpoint, and the hidden-state path additionally requires student pipeline_model_parallel_size=1, because Megatron builds output_layer only on the last pipeline stage while the LM-head load is a whole-world collective -- earlier stages would fail while the last stage hangs. The
logitspath has no such restriction. The payload column is shaped so a per-row teacher index can be added later without reworking it.Ships a Qwen3-1.7B self-distillation recipe whose divergence must sit at ~0, landing in disabled.txt until it has a real end-to-end run and the nightly GPU-hour budget has room for it.
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
# Add a code snippet demonstrating how to use thisBefore your PR is "Ready for review"
Pre checks:
Additional Information