Skip to content

Repository files navigation

🧪 OPD 实验复现项目

On-Policy Distillation of Large Language Models — 复现、实验与深度分析

Python PyTorch vLLM Ray verl LlamaFactory

Student: Qwen3-1.7B-Base · Teacher: Qwen3-8B · Data: GSM8K · Hardware: 4× NVIDIA L20 48GB


📋 目录


📖 项目概述

本项目基于 verl 框架和 LlamaFactory 工具,复现并深入探索 On-Policy Distillation (OPD) 算法在大语言模型训练中的实际表现。

🎯 研究动机

flowchart LR
    A[传统离线 SFT] -->|"Teacher 固定输出<br>→ 存在分布偏移"| C{问题}
    B[On-Policy 蒸馏] -->|"Student 自己 rollout<br>→ Teacher 在线评分"| C
    C -->|"哪个更有效?"| D[🔬 系统性实验]
    D --> E[📊 定量结论]
    D --> F[💡 实践指南]
Loading
核心问题 描述
❓ OPD 的保护机制是否真的有效? Top-K 过滤、交集策略、概率加权、KL 惩罚
❓ 离线 SFT vs 在线 OPD,谁更优? 监督信号强度与分布匹配度的权衡
❓ 关键超参数是什么? 通过消融实验定位核心因素

🏗️ 系统架构

┌──────────────────────────────────────────────────────────┐
│                    Ray Cluster (4 GPUs)                   │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │  GPU 0   │  │  GPU 1   │  │  GPU 2   │  │  GPU 3   │ │
│  │ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │ │
│  │ │Actor │ │  │ │Actor │ │  │ │Actor │ │  │ │Actor │ │ │
│  │ │FSDP  │ │  │ │FSDP  │ │  │ │FSDP  │ │  │ │FSDP  │ │ │
│  │ └──────┘ │  │ └──────┘ │  │ └──────┘ │  │ └──────┘ │ │
│  │ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │ │
│  │ │Ref   │ │  │ │Ref   │ │  │ │vLLM  │ │  │ │TchRM │ │ │
│  │ │FSDP  │ │  │ │FSDP  │ │  │ │Roll  │ │  │ │FSDP  │ │ │
│  │ └──────┘ │  │ └──────┘ │  │ └──────┘ │  │ └──────┘ │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
└──────────────────────────────────────────────────────────┘

🔬 实验设计

🧩 实验矩阵

实验 范式 top_k strategy weight KL 目的
Exp 1 🏆 离线 SFT 建立上界基线
Exp 2 💥 Naive On-Policy 0 only_stu none 确认崩溃
Exp 3 🎯 OPD 核心 16 intersection student_p 0.005 验证保护机制
4a1 🔍 top_k=8 8 intersection student_p 0.005 更严格过滤
4a2 🔍 top_k=32 32 intersection student_p 0.005 更宽松过滤
4b 🔍 kl=0.001 16 intersection student_p 0.001 弱 KL 惩罚
4c 🔍 only_stu 16 only_stu student_p 0.005 无交集策略

🛡️ OPD 4 大保护机制

机制 参数 作用
Top-K 过滤 LOG_PROB_TOP_K=16 只保留 Student 高置信度的 token,过滤低质量噪声信号
交集策略 TOP_K_STRATEGY=intersection 只保留 Student 和 Teacher 都高置信度的 token
学生概率加权 REWARD_WEIGHT_MODE=student_p 按 Student 概率加权,避免 Teacher 主导
KL 惩罚 USE_KL=True, COEF=0.005 防止 Student 偏离 Reference 模型过远

📐 核心公式

Token 级奖励信号(OPD 的核心):

$$r_t = \log \frac{p_{\text{student}}(a_t | s_t)}{p_{\text{teacher}}(a_t | s_t)}$$

PPO 损失函数

$$\mathcal{L}_{\text{PPO}} = \mathbb{E}\left[\min\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} \cdot A(s,a), \text{clip}\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}, 1-\epsilon, 1+\epsilon\right) \cdot A(s,a)\right)\right] + \alpha_{\text{KL}} \cdot \mathcal{L}_{\text{KL}}$$


⚡ 关键修改与优化

🐛 Bug 修复:KL 计算变量未捕获

dp_actor.pyupdate_policy 方法中,top-k 分支下的 log_prob 变量未被捕获,导致 UnboundLocalError

# ❌ 原版代码
if advantages.dim() == 3:
-    entropy, _, _, topk_log_probs = self._forward_micro_batch(...)
+    entropy, log_prob, _, topk_log_probs = self._forward_micro_batch(...)
    log_prob_for_loss = topk_log_probs
# 后面 kld = kl_penalty(logprob=log_prob, ...) ← 不再报错

🚀 训练效率优化(Exp 3 vs Exp 2)

参数 Exp 2 Exp 3 优化效果
N_RESPONSES 4 3 rollout 减少 25%
test_freq 20 40 验证次数减半
每步时间 ~120s ~55s ↓ 54%
总耗时 ~18h ~12h ↓ 32%
Throughput 250-336 tok/s ~310 tok/s ↑ ~15%

🔧 OOM 排查与解决

💥 现象: batch_size=32, Step 3 崩溃
   CUDA OutOfMemory: 尝试分配 17.17 GiB
   GPU 0: 30.30/44.52 GiB 已用

🔍 根因: Qwen3 词表大小 151,936
   logits shape = (total_nnz, 151936)
   log_softmax(logits, dim=-1) 需要 17 GB 额外显存

✅ 解决方案:
   1. MINI_BATCH_SIZE 回退到 16(安全值)
   2. 减少 N_RESPONSES=3 弥补吞吐量
   3. 增加 test_freq=40 减少验证开销

⚠️ 额外踩坑:
   expandable_segments:True 与 vLLM CuMemAllocator 不兼容
   → AssertionError: Expandable segments are not compatible

📊 实验结果

📈 奖励曲线总览

%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#FF6B35', 'textColor': '#333' }}}%%
xychart-beta
  title "各实验 reward/mean@8 对比"
  x-axis ["Exp 1<br>SFT", "Exp 2<br>Naive", "Exp 3<br>OPD", "4a1<br>topk=8", "4a2<br>topk=32", "4b<br>kl=0.001", "4c<br>only_stu"]
  y-axis "GSM8K Acc / Reward (%)" 0 --> 60
  bar [55.27, 0.76, 0.86, 0.43, 0.38, 0.32, 0.45]
Loading

🏆 Exp 1:离线 SFT 基线

指标
GSM8K Test Accuracy 55.27% (729/1319)
No Answer Rate 35.48% (468/1319)
训练 Loss 0.2768 → 0.215
验证 Loss 0.3364
训练步数 318 (3 epoch)
耗时 1h 36min

✅ 成功建立上界基线。Teacher (8B) 生成的 CoT 轨迹有效,Student (1.7B) 可模仿学习。

💥 Exp 2:Naive On-Policy 崩溃

指标
reward/mean@8 0.76%(全程无上升)
KL Loss 稳定 ~0.0057(未暴涨)
输出长度 1000-1600(未膨胀)
总步数 467
总耗时 ~18h

✅ 确认无保护机制的 Naive On-Policy 训练有效信号几乎为零。

🎯 Exp 3:OPD 核心算法

指标 Step 1 → 467
reward/mean@8 0.39% → 0.86%
reward/best@8/mean 2.02% → 3.93%
KL Loss 0 → 0.0113
Grad Norm 5.56 → 3.72
每步时间 ~55s
总耗时 12h 11min

时间分布(单步 ~55s)

阶段 耗时 占比
generate_sequences (vLLM rollout) 16.0s 29%
compute_log_prob 2.5s 5%
compute_rm_score (Teacher forward) 8.2s 15%
compute_distillation 2.4s 4%
ref forward 3.2s 6%
adv computation 0.6s 1%
update_actor 13.4s 24%
验证等其他 ~9s 16%

🔍 Exp 4:消融实验(@ Step 80)

消融 reward/mean@8 vs 基线
🥇 4c: only_stu 0.45% +0.06pp
🥈 4a1: top_k=8 0.43% +0.04pp
🥉 Exp 3 基线 0.39%
4a2: top_k=32 0.38% -0.01pp
4b: kl=0.001 0.32% -0.07pp
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#FF6B35', 'textColor': '#333' }}}%%
xychart-beta
  title "消融实验 reward/mean@8 @ Step 80"
  x-axis ["kl=0.001❌", "topk=32", "基线", "topk=8🥈", "only_stu🥇"]
  y-axis "Reward (%)" 0 --> 0.5
  bar [0.32, 0.38, 0.39, 0.43, 0.45]
Loading

💡 核心发现

🥇 发现 1:离线 SFT >> 在线蒸馏

SFT (55.27%) >>>>>>>>>>>>>>> OPD (0.86%) > Naive (0.76%)

差 64 倍的根因:
1. 📶 信号强度:监督信号(正确 token 序列)vs 相对信号(概率差 ~0.006)
2. 🎯 信用分配:每个 token 有明确目标 vs 中间 token 信号模糊
3. 🧠 模型容量:1.7B 小模型难以从微弱相对信号中学习推理

🥈 发现 2:OPD 保护机制有效防止崩溃

机制 效果
KL 惩罚 KL Loss 仅 0.011,Grad Norm 5.8→3.7
Top-K 过滤 过滤约 27% 的低质量 token(overlap ~73%)
概率加权 训练全程稳定,未出现崩溃

🥉 发现 3:KL 惩罚是最关键的消融参数

KL 惩罚 (0.005) >>> Top-K 阈值 ≈ 交集策略

kl=0.005 → 0.39%  ✅ 基线
kl=0.001 → 0.32%  ❌ 显著下降(-18%)

⚠️ 发现 4:交集策略可能非必需

only_stu     → 0.45%  🥇 最佳
intersection → 0.39%  📉 基线

对 1.7B 小模型,仅用 Student 自己的 top-k 过滤已足够
交集策略反而可能过滤掉有用信号

🎯 推荐配置

参数 推荐值
LOG_PROB_TOP_K 8
TOP_K_STRATEGY only_stu
REWARD_WEIGHT_MODE student_p
USE_KL True
KL_LOSS_COEF 0.005

⚠️ 重要建议:OPD 从 SFT checkpoint 开始训练,而非 Base 模型,效果可能显著提升。


🚀 快速开始

1️⃣ 环境准备

# OPD / RL 环境
conda create -n verl python==3.12
conda activate verl
cd verl/
USE_MEGATRON=0 bash scripts/install_vllm_sglang_mcore.sh
pip install math-verify

# SFT 环境
conda create -n sft python==3.11
cd LlamaFactory/
pip install -e .
pip install -r requirements/metrics.txt

2️⃣ 数据准备

# 模型软链接
ln -sf /path/to/qwen3_1.7b  model/qwen3-1.7b-base
ln -sf /path/to/qwen3_8b    model/qwen3-8b

# GSM8K 预处理
python verl/examples/data_preprocess/gsm8k_local.py \
  --local_save_dir datasets/gsm8k_verl

3️⃣ 运行实验

# Exp 1:离线 SFT 基线
bash exp/run_exp1.sh

# Exp 2:Naive On-Policy
bash exp/run_exp2.sh

# Exp 3:OPD 核心算法
bash exp/run_exp3.sh

# Exp 4:消融实验
bash exp/run_exp4.sh

# Exp 3 + 4 一键运行
bash exp/run_all.sh

📊 评估

# 生成预测
python exp/eval_gsm8k.py \
  --model /path/to/checkpoint \
  --input datasets/gsm8k_verl/test.parquet \
  --output preds.jsonl

# 打分
python exp/grade_gsm8k.py \
  --input preds.jsonl \
  --output metrics.json

📁 项目结构

OPD_exp/
├── verl/                          # verl RL 框架(editable install)
│   └── verl/trainer/main_ppo.py   # PPO/OPD 训练入口
├── LlamaFactory/                  # LlamaFactory SFT 框架
│   ├── data/dataset_info.json     # 数据集注册表
│   └── examples/                  # 训练配置和脚本
├── model/                         # 模型软链接
│   ├── qwen3-1.7b-base/           # Student (1.7B)
│   └── qwen3-8b/                  # Teacher (8B)
├── datasets/                      # 预处理数据
│   ├── gsm8k_verl/                # GSM8K (verl 格式)
│   └── test_data/                 # 各类测试集
├── exp/                           # 🧪 实验核心
│   ├── EXPERIMENT_LOG.md          # 完整实验记录
│   ├── Exp1_操作记录.md            # Exp 1 详细记录
│   ├── Exp2_详细记录.md            # Exp 2 详细记录
│   ├── Exp3_详细记录.md            # Exp 3 详细记录
│   ├── Exp4_详细记录.md            # Exp 4 详细记录
│   ├── eval_gsm8k.py             # GSM8K 评估
│   ├── grade_gsm8k.py            # 打分
│   ├── run_exp*.sh                # 一键运行脚本
│   ├── logs/                      # 训练日志
│   └── checkpoint/                # 模型 checkpoint
├── outputs/                       # SwanLab 输出
├── figs/                          # 图片资源
├── scripts/                       # 辅助脚本
├── on_policy_distillation.sh      # OPD 训练脚本
├── grpo.sh                        # GRPO 训练脚本
└── 简历项目描述.md                  # 简历版本总结

🛠️ 环境配置

硬件环境

项目 配置
GPU 4× NVIDIA L20 (48GB/卡)
CPU 128 核
内存 ~500 GB
驱动 NVIDIA 550.135, CUDA 12.4

软件版本

环境 版本
ppo (verl/OPD) Python 3.10.0
torch 2.8.0+cu128
vllm 0.11.0
ray 2.55.1
verl 0.7.0.dev0
sft (LlamaFactory) Python 3.11.0
torch 2.6.0+cu124
transformers 5.2.0
LlamaFactory 0.9.5.dev0

⚠️ sft 环境注意事项:需手动设置 LD_LIBRARY_PATH 以正确加载 CUDA 库。



📚 参考论文: Rethinking On-Policy Distillation of Large Language Models: Phenomenology, Mechanism, and Recipe

🔗 原始仓库: thunlp/OPD


If you find this project helpful, please star the original repo!

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages