Skip to content

feat(sc): run the distillation teacher in the SingleController train pump - #3846

Open
tianyi-zhang-02 wants to merge 9 commits into
NVIDIA-NeMo:mainfrom
tianyi-zhang-02:feat-sc-distillation
Open

feat(sc): run the distillation teacher in the SingleController train pump#3846
tianyi-zhang-02 wants to merge 9 commits into
NVIDIA-NeMo:mainfrom
tianyi-zhang-02:feat-sc-distillation

Conversation

@tianyi-zhang-02

@tianyi-zhang-02 tianyi-zhang-02 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Wires distillation into the SingleController train pump. It builds and parks a teacher TQPolicy, scores each chunk, writes the teacher top-k tensors to TransferQueue, narrows the student train fields, and skips the reward/advantage work that distillation does not use.

The same change also closes the setup gaps exposed by the first real run: teacher schema registration and shutdown, teacher train_iters, tokenizer compatibility, distillation-safe algorithm validation, and the missing setup timing.

This is the middle PR in the #3843#3846#3849 stack. The recipe and checkpoint/restore functional test stay in #3849.

Ownership

The stack is on hold because its core teacher top-k entry point overlaps the still-open #2580. I am keeping this branch tested and current, but not extending it until that ownership question is resolved.

Validation

@tianyi-zhang-02
tianyi-zhang-02 requested review from a team as code owners August 26, 2026 16:32
@copy-pr-bot

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

…troller

Distillation is the last algorithm with a rollout loop that SingleController
cannot run. Its teacher is a Policy, not a separate model class, so the
SingleController side needs no new driver -- only the missing top-k
entrypoint on the two layers every other forward already has:

  - TQWorkerMixin.get_topk_logits_presharded: per-rank fetch -> forward ->
    write-back. Unlike its siblings it writes back two tensors, and both
    carry a third axis ([B, S, k]); the write-back validates only the batch
    dimension, so that axis passes through unchanged.
  - TQPolicy.get_topk_logits_from_meta: the 1-hop dispatch, reusing
    LP_SEED_FIELDS because a teacher forward needs exactly what a logprob
    forward needs.

Nothing calls these yet -- wiring the teacher into the train pump is a
follow-up. Splitting it out keeps this piece independently testable.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
…pump

Builds on the teacher top-k forward and makes SingleController able to run
a distillation step end to end.

Distillation turns out to fit the existing shape almost exactly. Its
teacher is a Policy, so it is a second TQPolicy rather than a class of its
own, built and parked exactly the way the PPO critic is -- serially, with
the trainer offloaded, because both worker groups sit on the training GPUs.

What is different is the batch. DistillationLossFn reads only the sequence
columns and the teacher's top-k: no importance ratio, no reference KL, no
advantages. So the train pump skips both logprob forwards and the whole
advantage stage, and the fetched column set is narrowed accordingly --
DP_TRAIN_FIELDS names advantages and the logprob columns, and fetching a
column nobody wrote errors out rather than reading zeros. That narrowing
needs train_microbatches_from_meta to take the train_fields argument
train_from_meta already had.

MasterConfig now admits a third algorithm block, still exactly one at a
time, and a `teacher` block is required by distillation and rejected on
every other path -- a teacher nobody reads is the same silent no-op this
path already rejects unsupported algorithm knobs for.

The recipe and the functional test are a follow-up.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
Two things a distillation run hits before any model loads.

The legacy-async check read `config.grpo.async_grpo` on everything that was
not PPO. `grpo` is None on a distillation run, so the launcher died with an
AttributeError. DistillationConfig has no legacy async block at all --
distillation never had a v1 async path -- so there is nothing to reject.

And the teacher's vocabulary is now checked against the student's. The
teacher writes top-k *indices*, which the loss reads back as student
vocabulary ids: a teacher on a different vocabulary produces indices that
are silently wrong rather than an error. distillation.py already runs this
check; SC skipped it. Same helper, same NRL_SKIP_DISTILLATION_TOKENIZER_CHECK
opt-out, and it runs before the first model load so a mismatch costs
seconds instead of a full spin-up.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
@tianyi-zhang-02

Copy link
Copy Markdown
Contributor Author

Heads-up on merge order, with the resolution written out.

Four of my open PRs edit the same twelve-line block — the unsupported knob list in _validate_algo_settings:

I checked every pair by actually merging them. #3786 × #3787 is clean now — they only ever conflicted on the comment wording and on both adding a test before the same anchor, and both are fixed. The remaining four pairs are a genuine textual conflict on that block, and no restructuring avoids it: extracting or reordering the list conflicts just as hard.

So rather than force a stack, here is the resolution. Whichever order they land in, this is the merged form:

    # An enabled one here describes shaping this run does not do. An entry
    # leaves this list when the SC path starts implementing it -- rejecting a
    # knob is only right while nobody honours it.
    #
    # DistillationConfig defines none of them: there is no reward to shape or
    # filter on, so the list cannot even be evaluated on that path.
    unsupported = (
        []
        if is_distillation_run(master_config)
        else [
            name
            for name, enabled in (
                ("use_dynamic_sampling", algo_cfg.use_dynamic_sampling),
                ("reward_scaling", algo_cfg.reward_scaling.enabled),
                ("reward_shaping", algo_cfg.reward_shaping.enabled),
            )
            if enabled
        ]
    )

I built that merge locally and ran the SC and config suites against it — 1084 passed. Happy to rebase whichever ones are left once the first lands; just say which order you want.

The guard used to wrap that list in a conditional, which put this PR on the
same twelve lines as NVIDIA-NeMo#3786 and NVIDIA-NeMo#3787 -- each of those removes an entry from
it. Four pairwise conflicts on nothing but placement.

Returns early instead. DistillationConfig defines none of the knobs the list
names, so the check does not apply and the comprehension could not be
evaluated anyway; everything after it is PPO-specific and returns early on
this path already. The colocated requirement is the one thing that does
apply, so it moves into a helper and the guard calls it directly rather than
falling through.

The list itself is now untouched by this PR, so all four conflicts go away
and none of the three needs to land before the others.

Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
# Conflicts:
#	nemo_rl/algorithms/single_controller.py
#	nemo_rl/algorithms/single_controller_utils/setup.py
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Aug 29, 2026
tianyi-zhang-02 and others added 4 commits August 28, 2026 21:03
validate_single_controller_config routed distillation around
_validate_algo_settings entirely, to dodge one check inside it -- the
reward shaping and filtering knobs, which DistillationConfig does not
declare, so reading them raises AttributeError.

That dropped three checks that are not about GRPO at all and whose own
comments say so: the max_num_epochs<=0 guard (the rollout pump gates on
it whatever the algorithm), the warmup_lookahead_versions capacity guard
('Capacity is sized from the peak window whatever the algorithm'), and
the value-without-ppo guard. A distillation config setting any of them
was silently accepted where GRPO raises -- the same silently-accepted
no-op this file's shaping check exists to prevent.

Read the four shaping knobs defensively instead, so distillation stays
on the one code path and only the check that cannot apply is skipped.

Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>

# Conflicts:
#	nemo_rl/algorithms/single_controller.py
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request waiting-on-maintainers Waiting on maintainers to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants