Skip to content

[trainer, fsdp, worker, algo, cfg, recipe] feat: add reverse-KL on-policy distillation with FSDP teacher - #6920

Open
YangxSong wants to merge 17 commits into
verl-project:mainfrom
YangxSong:feature/reverse-kl-opd
Open

YangxSong wants to merge 17 commits into
verl-project:mainfrom
YangxSong:feature/reverse-kl-opd

Conversation

@YangxSong

Copy link
Copy Markdown

What does this PR do?

Adds reverse-KL on-policy distillation with student-top-K support (verl issue #6676), following the formulation in Rethinking On-Policy Distillation: the loss is KL(Q_student_topk || P_teacher_topk), which the teacher log-probs are evaluated at the student's own top-K tokens rather than the teacher's. Because the query IDs are student-specific and change every step, the teacher is served as a frozen forward-only FSDP worker (teacher_backend='fsdp') that gathers log-probs at arbitrary IDs via a chunked gather kernel. The existing vLLM teacher path is unchanged.

Checklist Before Starting

Test

Unit tests

The following unit tests have been verified:

python -m pytest tests/workers/test_reverse_kl_topk_on_cpu.py \
                 tests/workers/teacher/test_chunked_gather_on_cpu.py -v

End-to-end test

Run the script: examples/on_policy_distillation_trainer/run_dsr1_1.5b_reverse_kl_fsdp.sh. Use reverse-kl-topk to distill knowledge from JustRL-DeepSeek-1.5B into DeepSeek-R1-Distill-Qwen-1.5B on DAPO-17k.

score_amc23 score_aime24 score_aime25

loss overlap_ratio overlap_token_advantage

API and Usage Example

New opt-in DistillationConfig fields (defaults preserve current behavior): distillation.teacher_backend ("vllm" default / "fsdp"), teacher_chunk_size, teacher_fsdp_config. Under the FSDP backend distillation_loss.topk refers to the student top-K width.

# Full recipe: examples/on_policy_distillation_trainer/run_dsr1_1.5b_reverse_kl_fsdp.sh
python3 -m verl.trainer.main_ppo ... \
    distillation.enabled=True \
    distillation.teacher_backend=fsdp \
    distillation.teacher_models.teacher_model.model_path=hbx/JustRL-DeepSeek-1.5B \
    +distillation.teacher_fsdp_config.strategy=fsdp \
    +distillation.teacher_fsdp_config.dtype=bfloat16 \
    +distillation.teacher_fsdp_config.param_offload=True \
    distillation.distillation_loss.loss_mode=reverse_kl_topk \
    distillation.distillation_loss.topk=16

Design & Code Changes

New files

  • verl/workers/teacher/fsdp_teacher_worker.py: TeacherFSDPWorker — a forward-only FSDP worker that loads and hosts the frozen teacher model, with get_teacher_logprobs_at_ids(student_topk_ids) as the main entry point.
  • verl/workers/teacher/utils.py: chunked_gather_logprobs — streams log-prob gathering in chunks along (B*T), using gather(logits, ids) - logsumexp(logits) to avoid materializing [B, T, V].
  • verl/experimental/teacher_loop/teacher_fsdp_manager.py: TeacherManagerFSDP — thin single-teacher manager mirroring the vLLM teacher-manager interface.
  • verl/workers/teacher/__init__.py: package init for the teacher module.
  • examples/on_policy_distillation_trainer/run_dsr1_1.5b_reverse_kl_fsdp.sh: end-to-end recipe for DSR1-1.5B distillation.
  • examples/on_policy_distillation_trainer/run_qwen3_reverse_kl_fsdp.sh: Qwen3 variant recipe.
  • tests/workers/test_reverse_kl_topk_on_cpu.py: CPU unit tests for the reverse-KL loss computation and renormalization.
  • tests/workers/teacher/test_chunked_gather_on_cpu.py: CPU unit tests for the chunked gather kernel.

Modified files

  • verl/trainer/distillation/fsdp/losses.py: add compute_reverse_kl_topk — gathers student log-probs at student_topk_ids, renormalizes both sides to the shared student-top-K support, computes KL(Q_K || P_K), and emits raw mass + overlap diagnostics.
  • verl/trainer/distillation/losses.py: register reverse_kl_topk loss mode; add top-level wrapper that re-pads per-micro-batch tensors, collects diagnostics, and clamps ≥0.
  • verl/workers/config/distillation.py: add teacher_backend, teacher_chunk_size, teacher_fsdp_config fields to DistillationConfig with post-init validation.
  • verl/workers/config/engine.py: add FSDP engine config entries needed by the teacher worker.
  • verl/trainer/ppo/ray_trainer.py: branch teacher creation on teacher_backend; in the FSDP path, run a lightweight student forward (compute_student_topk_only=True) to collect student_topk_ids, query the teacher, and stamp the result into the batch.
  • verl/workers/engine_workers.py: expose teacher worker integration on the actor rollout worker group.
  • verl/workers/engine/fsdp/transformer_impl.py: support compute_student_topk_only flag — short-circuits after computing student top-K without running policy loss.
  • verl/workers/utils/padding.py: add pad/unpad helpers for nested tensor handling in the logits processor pipeline.
  • verl/experimental/agent_loop/agent_loop.py: wire FSDP teacher manager into the agent loop.
  • verl/trainer/config/distillation/distillation.yaml & generated trainer configs: add teacher_backend, teacher_chunk_size, teacher_fsdp_config fields.

Checklist Before Submitting

  • Read the Contribute Guide.
  • Apply pre-commit checks: pre-commit install && pre-commit run --all-files --show-diff-on-failure --color=always
  • Add / Update the documentation. Documentation is not included in this PR. If this feature is accepted and merged, I will submit a follow-up PR to update the documentation.
  • Add CPU unit tests for the loss and gather kernel. The full FSDP+vLLM pipeline needs multi-GPU and cannot run in CI; validated by the end-to-end run above.

YangxSong and others added 15 commits July 1, 2026 17:29
…nfig (verl#6676)

Introduce three opt-in fields on DistillationConfig that scaffold the upcoming
student-top-K reverse-KL pipeline (verl issue verl-project#6676):

- teacher_backend ("vllm" | "fsdp"): selects whether the teacher is served by
  the existing vLLM/SGLang rollout (default, unchanged) or a frozen FSDP
  worker that gathers logprobs at student-top-K IDs each step.
- teacher_chunk_size: token-axis chunk size for the FSDP gather kernel.
- teacher_fsdp_config: reuses FSDPEngineConfig so the existing engine builder
  can construct the teacher model.

When teacher_backend='fsdp' the post-init now skips the vLLM-only
prompt_logprobs / max_logprobs / resource-pool-sizing checks (those assume a
rollout deployment) and pins loss_mode to 'reverse_kl_topk' so misuse is
caught early. The default vllm path is unchanged.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the mode-seeking objective requested in verl issue verl-project#6676:

    L_t = sum_{i in TopK_student}  q_i * (log q_i - log p_i)

with the support taken on the *student*'s own top-K and the expectation
under the student distribution. The teacher log-probabilities at those
student-top-K IDs are precomputed by the upcoming FSDP teacher worker
(P3), so this loss only consumes the gathered (B, T, K) tensor and does
not need to materialize a full [B, T, V] log-softmax.

Changes:
- verl/trainer/distillation/fsdp/losses.py: add reverse_kl_divergence
  helper and compute_reverse_kl_topk, mirroring compute_forward_kl_topk's
  nested-tensor / ulysses-SP unwrap path. Returns distillation_losses,
  student_mass (≈ 1, sanity), and teacher_mass (the key mode-seeking
  diagnostic — should rise during training).
- verl/trainer/distillation/losses.py: dispatch compute_topk_loss by
  loss_mode; route 'reverse_kl_topk' to the fsdp/veomni implementation
  (megatron raises NotImplementedError pending a vocab-parallel port);
  register the top-level wrapper that re-pads outputs, aggregates the
  mass diagnostics over the response mask, and clamps minor negative
  drift introduced by log_prob_min_clamp.
- tests/workers/test_reverse_kl_topk_on_cpu.py: CPU-only unit tests for
  non-negativity, zero at distribution match, gradient flow into the
  student logits, agreement with the manual reference formula, and
  registry/metric-aggregation contracts.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce the worker-side scaffold for the FSDP teacher backend:

- verl/workers/teacher/utils.py: chunked_gather_logprobs — kernel that
  computes log_softmax(logits).gather(idx) without materializing the
  full log_softmax tensor, by using
  log_softmax(x).gather(idx) == x.gather(idx) - logsumexp(x)
  in fp32 over chunks of the leading axis. Peak memory becomes
  chunk_size * V * 4 bytes per side instead of N * V * 4.

- verl/workers/teacher/fsdp_teacher_worker.py: TeacherFSDPWorker —
  subclasses TrainingWorker and pins engine_config.forward_only=True so
  FSDPEngine builds without optimizer/grad. Adds compute_logprobs_at_ids
  which reuses FSDPEngineWithLMHead.prepare_model_inputs (matching the
  actor's rmpad / nested-tensor layout), runs a single forward, and
  delegates to chunked_gather_logprobs at trainer-supplied topk_ids. The
  trainer is responsible for the roll-by-1 alignment; Ulysses SP on the
  teacher is gated off until a follow-up.

- verl/experimental/teacher_loop/teacher_fsdp_manager.py:
  FSDPTeacherManager and MultiTeacherFSDPManager — mirror the public
  surface of TeacherModelManager / MultiTeacherModelManager so the
  trainer's resource-pool plumbing can switch on teacher_backend without
  diverging downstream consumers. Worker-group spawning and main-loop
  dispatch are explicitly NotImplementedError-marked for P4.

- tests/workers/teacher/test_chunked_gather_on_cpu.py: CPU-only unit
  tests covering chunk_size sweep including > N rows, 2D rmpad input,
  shape/dtype preservation, validation errors, and fp32-stable behavior
  on large-magnitude logits.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…676)

Connect the post-rollout student-top-K -> teacher-gather pipeline into
the trainer's RayPPOTrainer fit loop. With teacher_backend='fsdp':

- agent_loop.py: gate the per-trajectory vLLM teacher logprob hook on
  use_vllm_teacher (False under the FSDP backend) so rollouts no longer
  attempt to reach a non-existent LLMServerClient.

- workers/engine/fsdp/transformer_impl.py: relax the per-key shape
  assertion in prepare_model_outputs from `v.shape == log_probs.shape`
  to `v.shape[0] == log_probs.shape[0]`, allowing the logits processor
  to emit per-token, top-K outputs (e.g. (total_nnz, K)) which the
  reverse-KL pipeline needs for student_topk_log_probs / student_topk_ids.

- trainer/distillation/losses.py: add a `compute_student_topk_only`
  data-flag fast path on compute_topk_loss that returns the student's
  own top-K log-probs and IDs, used by the actor's first-pass forward
  before the teacher is queried.

- workers/engine_workers.py: add ActorRolloutRefWorker.compute_student_topk
  which stamps the data flag above and runs a no-grad infer_batch on
  the actor engine, returning the per-token student top-K as a TensorDict
  the trainer can hand to the teacher worker.

- experimental/teacher_loop/teacher_fsdp_manager.py: rewrite as a thin
  manager (single-teacher only for now) that holds a Ray worker group
  handle installed by the trainer after the unified spawn. Multi-teacher
  raises NotImplementedError until the per-teacher resource-pool split
  is finalized.

- trainer/ppo/ray_trainer.py: branch teacher manager creation by
  teacher_backend; pre-spawn, register TeacherFSDPWorker on the teacher
  resource pool with a TrainingWorkerConfig assembled from
  distillation.teacher_fsdp_config; post-spawn, install the spawned
  handle on MultiTeacherFSDPManager and reset the worker. Add helpers
  _build_fsdp_teacher_training_config and _inject_fsdp_teacher_data, and
  call the latter inside the fit loop's update_actor block (under a
  dedicated marked_timer) so update_actor sees student_topk_ids /
  teacher_on_student_logp on every step. The vLLM path is unchanged.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l#6676)

Single-teacher OPD recipe that runs the verl#6676 student-top-K
reverse-KL pipeline end-to-end:

- distillation.teacher_backend=fsdp routes the teacher through the
  TeacherFSDPWorker instead of a vLLM rollout replica.
- distillation.distillation_loss.loss_mode=reverse_kl_topk drives the
  student-top-K reverse-KL loss, with topk repurposed as the
  *student*-top-K width.
- teacher_fsdp_config exposes the FSDPEngineConfig defaults the
  trainer now wires through (param_offload + bf16 + fsdp_size=-1).

Defaults are sized for a single 8-GPU node with a 1.7B student and an
8B FSDP teacher; the script's user-adjustable block exposes the knobs
needed to retarget at larger pairs.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#6676)

- losses.py: short-circuit policy loss on the FSDP-teacher first pass
  (compute_student_topk_only) so the student top-K emit step does not
  trigger the final loss path.
- ray_trainer.py: wrap TeacherFSDPWorker with ray.remote, materialize the
  non-blocking compute_logprobs_at_ids future, and pass temperature=1.0 so
  the FSDP engine emits the teacher's raw distribution.
- engine.py: allow runtime override of EngineRouterReplayConfig.mode and
  type max/micro batch fields as Optional[int].
- fsdp_teacher_worker.py: move cu_seqlens to the values device before
  nested_tensor_from_jagged.
- teacher/utils.py: move topk_ids to logits.device for torch.gather.
- distillation.yaml: add teacher_backend / teacher_chunk_size /
  teacher_fsdp_config knobs for the FSDP teacher path; regenerate the
  trainer yaml variants.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… padding (verl#6676)

- ray_trainer.py: Phase 3 now scatters student_topk_ids /
  teacher_on_student_logp back to left-right padded dense
  (B, max_seq_len, K) using the Phase-1 ``indices`` / ``max_seq_len``,
  instead of no_padding_2_padding (which sliced response-only and
  left-shifted, breaking input_ids alignment).
- padding.py: left_right_2_no_padding now also re-nests student_topk_ids
  and teacher_on_student_logp aligned to input_ids, matching the
  teacher_logprobs / teacher_ids handling so the reverse_kl_topk logits
  processor sees aligned jagged tensors.
…erl#6676)

Track the student/teacher top-K intersection during reverse-KL OPD,
following "Rethinking On-Policy Distillation of Large Language Models".
The FSDP teacher now also emits its own top-K ids, carried through the
left_right padding glue, so compute_reverse_kl_topk can report
overlap_ratio and overlap_token_advantage. Also default
use_policy_gradient to False.



Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (verl#6676)

The student-top-K reverse-KL loss summed q_i*(log q_i - log p_i) over the
student's own top-K without renormalizing. Once the student starts to de-peak,
that truncated sum can be minimized by leaking probability mass *outside* the
top-K (every q_i shrinks, so every term shrinks) instead of by matching the
teacher. This drives a self-reinforcing collapse: entropy explodes, student_mass
falls from ~0.97 to ~0.4 and overlap_ratio drops sharply, while the reported loss
keeps decreasing. The onset is governed by the deterministic optimization drift,
not batch noise, so it reproduces at the same step regardless of train batch size.

Fix: renormalize both student and teacher log-probs onto the shared
student-top-K support before the divergence, so the per-token objective is a
proper KL(Q_K || P_K). student_mass / teacher_mass remain un-normalized as
de-peaking diagnostics. Add a regression test asserting the loss is invariant to
mass leaked outside the top-K, and update the reference-formula test.
…ture (verl#6676)

The reverse-KL OPD loss compares the student and teacher distributions, so
both must be taken at the same temperature. The FSDP teacher was queried at a
fixed T=1.0 while the student rollout uses rollout.temperature, so the divergence
was computed between two distributions at mismatched temperatures. Pass
rollout.temperature through to the teacher forward to align them.
# Conflicts:
#	verl/workers/config/distillation.py
#	verl/workers/engine/fsdp/transformer_impl.py
The top-K distillation path may produce tensors of shape (total_nnz,)
or (total_nnz, K) for student_topk_log_probs / student_topk_ids.
The old strict assert required v.shape == (total_nnz,), which would
fail for 2D outputs. Change it to only compare the first dimension.
@CLAassistant

CLAassistant commented Jul 2, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for student-top-K reverse-KL on-policy distillation (OPD) using a frozen FSDP-served teacher backend. Key additions include the TeacherFSDPWorker and a memory-efficient chunked_gather_logprobs utility to compute teacher log-probabilities at student-selected token IDs without materializing full vocabulary-sized tensors. The PR also adds the reverse_kl_topk loss mode, a dedicated FSDPTeacherManager, configuration extensions, padding utilities, integration tests, and example bash scripts for running reverse-KL distillation. I have no feedback to provide as there are no review comments to assess.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

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