[OPD] Add Qwen3.5-35B-A3B single-node self-distillation example - #1488
Conversation
Two-phase on-policy-distillation example for Qwen3.5-35B-A3B on a single
8xH200 node using the in-process Megatron teacher (--opd-type megatron):
Phase 1 (phase1_rlvr_teacher.sh): RLVR-train the base into a teacher that is
measurably better and more concise (eval 0.83 -> 0.89, length ~14k -> ~6k).
Phase 2 (phase2_opd_selfdistill.sh): distill that teacher into the base
student. Pure mode (reward=0, only reverse-KL) cleanly attributes the change
to OPD; grounded mode keeps a correctness reward so raw_reward is meaningful.
Unlike run-qwen3-8B-opd-megatron.sh (teacher == base, reverse-KL ~ 0), this
example trains a genuinely diverged teacher first, which is what makes OPD move
the student. Self-distillation is required: Qwen3.5's tokenizer (vocab 248320)
is not compatible with the smaller Qwen3 models (vocab 151936).
Includes the single-node parallelism derivation (TP2/PP1/CP2/EP8/ETP1, world=8),
a format-agnostic reward that avoids the deepscaler/math/dapo grader pitfalls, a
seeded disjoint train/eval split, and a README documenting the gotchas
(context-length truncation, --opd-teacher-load parent-dir requirement, the
ref-model memory interaction, and teacher divergence vs learning rate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a reproducible two-phase self-distillation pipeline for the Qwen3.5-35B-A3B MoE model on a single node, including training scripts, evaluation configurations, dataset splitting utilities, and custom format-agnostic reward functions. The feedback suggests improving the robustness of the prompt parsing logic in make_split.py to handle missing keys or malformed structures, and double-quoting array expansions in the Bash scripts (phase1_rlvr_teacher.sh and phase2_opd_selfdistill.sh) to prevent unexpected word splitting.
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.
| def prompt_text(d): | ||
| p = d["prompt"] | ||
| return "\n".join(m.get("content", "") for m in p) if isinstance(p, list) else str(p) |
There was a problem hiding this comment.
To improve robustness and adhere to defensive programming practices, handle cases where d might not contain the "prompt" key, or where elements in the prompt list are None or not dictionaries (which would cause AttributeError or KeyError).
| def prompt_text(d): | |
| p = d["prompt"] | |
| return "\n".join(m.get("content", "") for m in p) if isinstance(p, list) else str(p) | |
| def prompt_text(d): | |
| p = d.get("prompt", "") | |
| if isinstance(p, list): | |
| return "\\n".join(m.get("content", "") if isinstance(m, dict) else str(m) for m in p if m is not None) | |
| return str(p) |
| ${MODEL_ARGS[@]} ${CKPT_ARGS[@]} ${ROLLOUT_ARGS[@]} ${OPTIMIZER_ARGS[@]} ${GRPO_ARGS[@]} \ | ||
| ${WANDB_ARGS[@]} ${PERF_ARGS[@]} ${EVAL_ARGS[@]} ${SGLANG_ARGS[@]} ${MISC_ARGS[@]} ${RM_ARGS[@]} |
There was a problem hiding this comment.
In Bash, expanding arrays without double quotes (e.g., ${MODEL_ARGS[@]}) can lead to unexpected word splitting and glob expansion if any of the arguments contain spaces or special characters. Always use double quotes around array expansions: "${ARRAY[@]}".
| ${MODEL_ARGS[@]} ${CKPT_ARGS[@]} ${ROLLOUT_ARGS[@]} ${OPTIMIZER_ARGS[@]} ${GRPO_ARGS[@]} \ | |
| ${WANDB_ARGS[@]} ${PERF_ARGS[@]} ${EVAL_ARGS[@]} ${SGLANG_ARGS[@]} ${MISC_ARGS[@]} ${RM_ARGS[@]} | |
| "${MODEL_ARGS[@]}" "${CKPT_ARGS[@]}" "${ROLLOUT_ARGS[@]}" "${OPTIMIZER_ARGS[@]}" "${GRPO_ARGS[@]}" \\ | |
| "${WANDB_ARGS[@]}" "${PERF_ARGS[@]}" "${EVAL_ARGS[@]}" "${SGLANG_ARGS[@]}" "${MISC_ARGS[@]}" "${RM_ARGS[@]}" |
| ${MODEL_ARGS[@]} ${CKPT_ARGS[@]} ${OPD_ARGS[@]} ${ROLLOUT_ARGS[@]} ${OPTIMIZER_ARGS[@]} ${GRPO_ARGS[@]} \ | ||
| ${WANDB_ARGS[@]} ${PERF_ARGS[@]} ${EVAL_ARGS[@]} ${SGLANG_ARGS[@]} ${MISC_ARGS[@]} ${RM_ARGS[@]} |
There was a problem hiding this comment.
In Bash, expanding arrays without double quotes (e.g., ${MODEL_ARGS[@]}) can lead to unexpected word splitting and glob expansion if any of the arguments contain spaces or special characters. Always use double quotes around array expansions: "${ARRAY[@]}".
| ${MODEL_ARGS[@]} ${CKPT_ARGS[@]} ${OPD_ARGS[@]} ${ROLLOUT_ARGS[@]} ${OPTIMIZER_ARGS[@]} ${GRPO_ARGS[@]} \ | |
| ${WANDB_ARGS[@]} ${PERF_ARGS[@]} ${EVAL_ARGS[@]} ${SGLANG_ARGS[@]} ${MISC_ARGS[@]} ${RM_ARGS[@]} | |
| "${MODEL_ARGS[@]}" "${CKPT_ARGS[@]}" "${OPD_ARGS[@]}" "${ROLLOUT_ARGS[@]}" "${OPTIMIZER_ARGS[@]}" "${GRPO_ARGS[@]}" \\ | |
| "${WANDB_ARGS[@]}" "${PERF_ARGS[@]}" "${EVAL_ARGS[@]}" "${SGLANG_ARGS[@]}" "${MISC_ARGS[@]}" "${RM_ARGS[@]}" |
Grounded OPD (correctness reward + teacher reverse-KL): rollout/raw_reward climbs 0.637 -> 0.910 in one step while the student adopts the teacher's concise responses (18.8k -> 7.7k) and opd_reverse_kl shrinks 0.045 -> 0.014. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: "Run on GB200/GB300 (CUDA 13, Blackwell)" — 2×4 tiling, the flashinfer_cutlass MoE runner + trtllm_mha attn + flex dispatcher (default triton fused-MoE mis-shards routed experts on the megatron->sglang weight sync; FA3 is SM<=90 only), NCCL_NVLS_ENABLE=0, and the PROMETHEUS_PORT k8s-Service collision. - README: "Run Phase 2 only (skip Phase 1)" — point --opd-teacher-load at an existing torch_dist teacher; HF->torch_dist via convert_gb200.sh. - phase2_gb200.sh: GB200 variant of phase2_opd_selfdistill.sh (2 nodes x 4 GPU, Blackwell sglang/MoE backends, NVLS-off + PROMETHEUS_PORT in the Ray runtime env). convert_gb200.sh: convert_hf_to_torch_dist wrapper carrying the Qwen3.5 MODEL_ARGS. - mbridge/qwen3_5.py: autodetect unfused per-expert main-layer weights (mirrors the existing MTP-expert autodetect) so a teacher exported with split experts converts without manual re-fusing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the public HF checkpoint (cm00cm/Qwen3.5-35B-A3B-DAPO-RLVR-teacher, weights only) in the Phase-1 results and References, so the trained teacher used in the example is directly available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
A reproducible two-phase on-policy-distillation example for Qwen3.5-35B-A3B on a single 8×H200 node, using the in-process Megatron teacher (
--opd-type megatron).phase1_rlvr_teacher.sh): RLVR-train the base 35B into a teacher that is measurably better and more concise —eval/dapo_heldout 0.828 → 0.887, response length~14k → ~6k(lr 1e-5).phase2_opd_selfdistill.sh): distill that teacher into the base student.−57%(14k→6.1k) with accuracy preserved/slightly up (0.840→0.852),opd_reverse_kl 0.045→0.013(student converging onto the teacher).rollout/raw_rewardis meaningful and climbs) plus the teacher reverse-KL.Why it's distinct from the existing examples
run-qwen3-8B-opd-megatron.shuses teacher == base — a mechanism demo whereopd_reverse_kl ≈ 0. This example trains a genuinely diverged teacher first, which is the prerequisite for OPD to actually move the student.TP2/PP1/CP2/EP8/ETP1, world=8),--colocate+--optimizer-cpu-offload, ≈124/143 GB per GPU.Contents
README.mdphase1_rlvr_teacher.shphase2_opd_selfdistill.shMODE)rm.pymake_split.pyeval_dapo_heldout.yamlGotchas documented (each cost a wasted run)
--rm-type deepscalerneeds a</think>tag → scores Qwen3.5 (inline reasoning) as 0;math/dapoeach read only one answer format. The example uses a format-agnostic reward and always sets--label-key label.--opd-teacher-loadmust point at the checkpoint parent dir (withlatest_checkpointed_iteration.txt), not aniter_*subdir — otherwise it silently falls back to base andopd_reverse_kl ≈ 0.with_ref = (--use-kl-loss or --kl-coef≠0)— dropping--use-kl-losskeeps only student+teacher (2×35B) in memory and avoids a 3rd-model OOM.🤖 Generated with Claude Code