Skip to content

Add AsyncDistillationTrainer - #6705

Open
kashif wants to merge 8 commits into
huggingface:mainfrom
kashif:async-distillation-trainer
Open

Add AsyncDistillationTrainer#6705
kashif wants to merge 8 commits into
huggingface:mainfrom
kashif:async-distillation-trainer

Conversation

@kashif

@kashif kashif commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Async on-policy distillation, architected like AsyncGRPOTrainer: a background rollout worker generates the student's own completions and scores them against a teacher served over HTTP, so generation and training overlap instead of alternating. The teacher is never loaded locally, just a vLLM server URL.

Also supports MOPD (multi-teacher on-policy distillation) — pass more than one entry in teacher_server_urls and route each sample to a teacher via a teacher_id column (e.g. a math teacher and a code teacher, each served independently).

This went through several rounds of GPU validation in a staging fork before landing here, and the multi-teacher routing design was cross-checked against how NeMo RL, verl, and miles do the same kind of per-sample teacher routing.

Adds:

  • trl/experimental/async_distillation/ (config, trainer, rollout worker, vLLM client, weight transfer)
  • tests/experimental/test_async_distillation_trainer.py
  • examples/scripts/async_distillation.py (single-teacher) and async_distillation_mopd.py (math+code MOPD demo)
  • docs/source/async_distillation_trainer.md + toctree entry, one-line cross-ref from distillation_trainer.md

make precommit passes. Test suite passes (26 passed / 8 GPU-gated skipped without a GPU); also ran the GPU-gated tests via srun --gres=gpu:1 — one pre-existing failure reproduces there, but it's unrelated to this PR (it also reproduces on unmodified async_grpo tests on the same node, looks like a tiny-test-fixture/environment issue rather than anything this PR changes).

Happy to open a tracking issue first if that's the preferred process for a new experimental trainer — let me know.


Note

Medium Risk
Large new experimental training path (async queues, NCCL weight sync, distributed FSDP2-only) with complex loss math; well-tested but operationally sensitive to vLLM/transformers versions and multi-process failures.

Overview
Introduces AsyncDistillationTrainer under trl/experimental/async_distillation/, modeled on AsyncGRPOTrainer: a spawned rollout worker generates student completions via vLLM while the main loop trains on queued samples, overlapping generation and updates.

The teacher is not loaded in-process—only trl vllm-serve URLs and /get_sequence_logprobs/ sparse top-k scoring. Loss is generalized JSD (same family as sync distillation / server distillation), with chunked lm_head projection and beta-dependent support narrowing. Student weights sync to vLLM over NCCL on a schedule; max_staleness drops outdated rollouts.

MOPD is supported via multiple teacher_server_urls and a per-row teacher_id column (strict routing, no silent fallback). Adds AsyncDistillationConfig, rollout worker, vLLM client, weight transfer client, 646-line test suite, single- and multi-teacher example scripts, dedicated docs + toctree entry, paper index MOPD blurb, and a cross-link from the sync distillation doc.

Reviewed by Cursor Bugbot for commit d4060f3. Bugbot is set up for automated code reviews on this repo. Configure here.

Async on-policy distillation: a background rollout worker generates
the student's own completions and scores them against a teacher
served over HTTP, decoupling generation from training the way
AsyncGRPOTrainer does for GRPO. Also supports MOPD (multi-teacher
on-policy distillation) via per-sample teacher_id routing across
several teacher_server_urls entries.
@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a09d0bfb5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread trl/experimental/async_distillation/async_distillation_trainer.py Outdated
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py Outdated
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
- Remove the teacher vocab_size check: it called GET /v1/models on
  the teacher's server, but trl vllm-serve doesn't expose that route
  (it's the standard vLLM OpenAI server's endpoint, not this custom
  FastAPI app). No way to fix it without adding new shared server
  infra, so reverting rather than shipping a check that always fails.
- Fix the paper citation: it was misattributed to the MiniLLM authors
  instead of Agarwal et al. (the actual 2306.13649 authors), which
  would have shown up as a false citation on every model card.
- Note that async execution and MOPD aren't described in 2306.13649
  (a synchronous, single-teacher paper); add the actual MOPD paper
  citation and a paper_index.md entry for it.
- Document the TokenBudgetBatcher/max_steps interaction as a known,
  pre-existing limitation shared with AsyncGRPOTrainer rather than
  fixing it in isolation here.
- Drop a dangling reference to a planning doc that doesn't exist in
  this repo.
Comment thread docs/source/paper_index.md Outdated
kashif added 3 commits August 11, 2026 10:14
MOPD is Stage 3 of a 3-stage pipeline (SFT, per-domain RL experts,
then fusion) — AsyncDistillationTrainer only implements the fusion
stage, with pre-trained teachers. The paper's Stage 3 also uses
reverse KL (beta=1.0), not this trainer's default beta=0.0.
…mory

Project hidden states through lm_head in fixed-size chunks (checkpointed)
instead of materializing the full [seq_len, vocab_size] logits tensor at
once, mirroring DistillationTrainer's own chunked JSD path. Also fixes a
missing autocast around the bypassed base_model forward call, and a stale
test fixture bug (tokenizer.vocab_size excludes added special tokens like
eos_token_id, causing out-of-bounds indices in synthetic teacher data).
Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
kashif added 2 commits August 11, 2026 14:32
Calling unwrapped_model.base_model(...) directly to bypass lm_head skips
the wrapper's forward, so DDP's prepare_for_backward and FSDP's parameter
materialization never run for the loss. Mirrors DistillationTrainer's
identical need for its own chunked JSD path.
jsd/{teacher_id} collided with the flat jsd metric name in dashboards
that treat / as a grouping separator; teacher_entropy/{id} already used
the teacher_ prefix, so jsd/{id} was the odd one out.
if len(self.teacher_server_urls) == 1:
((teacher_id, url),) = self.teacher_server_urls.items()
return teacher_id, url
teacher_id = row.get("teacher_id")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this couples the mapping teacher_id->vllm_server_url to the dataset having a specific column teacher_id in the format ?
I wonder if we can check/enforce this somehow at runtime like checking that the dataset has this columns and the unique values map correctly to the teacher 🤔 ? wdyt ?

Not a strong requirement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, with more than one teacher each row needs a teacher_id matching a key in teacher_server_urls. I kept it the same as AsyncGRPO, which routes multi-env runs off an environment column and checks it per row in the worker rather than validating the dataset upfront. A missing or unknown id raises instead of silently picking a teacher. I did align the error with the GRPO one (ValueError, same wording), so it reads the same in both.

"response_format": "json",
}
output = await _retry_on_http_error(
lambda: self._post(teacher_server_url, "/get_sequence_logprobs/", payload, self.request_timeout),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: I was unaware of /get_sequence_logprobs endpoint. Is this in stock vllm ? :o

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that one is ours, not stock vllm. It only exists on trl vllm-serve (trl/scripts/vllm_serve.py), wrapping vllm prompt_logprobs for teacher forcing. Same endpoint sdft/sdpo/server_distillation already use.

Comment thread trl/experimental/async_distillation/async_distillation_trainer.py

@AmineDiro AmineDiro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good to me. Well done @kashif 👏🏼

The only design question is about separating the RolloutWorker in a separate process. We've done this in AsyncGRPOTrainer in PR mainly because recursive_parse / accuracy_reward were locking the GIL. In this AsyncDistillationTrainer I dont see anything similar to that so maybe a simple threaded Worker is the simpler + remove serialization/deserialization cost ?

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d4060f3. Configure here.

Comment thread trl/experimental/async_distillation/async_distillation_trainer.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants