On-Policy Distillation(OPD):为什么大模型后训练要在学生自己的轨迹上蒸馏?

前言:学生走偏以后,标准答案还够用吗?

知识蒸馏通常让强教师先写出高质量答案,再让小模型模仿。但模型上线后,只能沿着自己生成的内容继续走。

比如同一道几何题,教师习惯构造辅助线,学生却先建立坐标系。离线蒸馏只教过教师轨迹,学生进入自己的坐标法前缀后,便可能来到训练中没有见过的位置。

这就是序列模型中的 exposure bias(暴露偏差):训练时,模型通常沿着正确答案继续预测;推理时,却只能沿着自己生成的内容继续走。一旦前面生成错误,后续便可能进入训练中很少见过的状态。

这种方法简称 OPD,可以理解为"基于当前策略的蒸馏":让学生先生成,再让教师在学生实际到达的位置上指导它。

text 复制代码
SFT(Supervised Fine-Tuning,监督微调):老师先写,学生模仿。
RL(Reinforcement Learning,强化学习):学生先做,奖励打分。
OPD:学生先做,老师逐步指导。

OPD 的关键并不是发明了一个新的 KL(Kullback-Leibler divergence,KL 散度)公式,而是改变了训练状态从哪里来

它保留了 on-policy RL 的状态分布对齐,又获得了知识蒸馏逐 token、连续且稠密的监督。

下面我们从算法、verl 源码和 Qwen3、DeepSeek-V4 的实践三个层面,把这件事讲清楚。

一、SFT、RL 与 OPD,根本区别是什么?

先看一个简单例子。

问题是:解方程 2(x-3)=10

教师提供的标准轨迹可能是:

text 复制代码
2(x-3)=10 → x-3=5 → x=8

学生自己生成时,却可能写成:

text 复制代码
2x-3=10 → 2x=13 → ...

SFT:继续强化标准答案

SFT 或传统 response distillation 会继续提高教师正确轨迹的概率。

它能告诉学生"正确展开应该怎么写",却没有直接回答:当前缀已经变成 2x-3=10 时,下一步怎样恢复?

因为训练时的状态来自教师,部署时的状态来自学生,两者并不完全一致。

RL:在学生状态上看最终结果

RL 会让学生先生成,再根据最终答案是否正确给 reward。

这样训练状态与推理状态是一致的,但信号往往比较稀疏:一整条推理最终得 0 分,模型不一定知道到底从哪一步开始错。

OPD:在学生状态上提供逐 token 指导

OPD 也先让学生生成。

不同的是,它把学生前缀交给教师,请教师给出下一 token 的概率分布。在刚才的错误前缀上,教师可能提高"检查展开""这里应为 2x-6"等修正方向的概率。

学生学到的不只是复述标准答案,还包括:按自己的方式走到这里以后,怎样回到合理方向。

一条 1000-token 的轨迹只有一个序列奖励,却能产生数百到上千个教师 next-token 信号,因此 OPD 的监督更稠密。但标点、连接词和风格差异也会进入目标,仍需用任务 reward、mask、loss coefficient 或数据筛选控制模仿强度。

可以用两条轴理解这三类方法:

text 复制代码
状态来自哪里?
教师固定轨迹 / 学生当前策略

监督有多细?
固定答案 token / 序列 reward / 教师 next-token 分布

因此,on-policy 说的是训练状态来自学生当前策略,不等于一定使用 PPO(Proximal Policy Optimization,近端策略优化)或 GRPO(Group Relative Policy Optimization,组相对策略优化)。

OPD 是一种采样和监督协议;KL、直接反传或 policy gradient,才是具体的优化协议。

二、OPD 到底怎样训练?

OPD 的训练过程可以先概括成四步:

  1. 学生模型自己生成回答;
  2. 教师模型阅读学生已经生成的前缀;
  3. 教师判断学生接下来生成的 token 是否合理;
  4. 训练程序根据教师与学生的意见差异更新学生。

对于学生刚生成的 token,师生都会给出概率判断:教师更认可就提高其概率,更不认可就降低其概率。

在后文介绍的 PG-OPD(Policy-Gradient On-Policy Distillation,基于策略梯度的 OPD)路线中,这个判断可以写成一个 token 级训练信号:

At=stopgrad(log⁡ν(yt∣st)−log⁡πθ(yt∣st)) A_t=\mathrm{stopgrad}(\log \nu(y_t\mid s_t)-\log \pi_\theta(y_t\mid s_t)) At=stopgrad(logν(yt∣st)−logπθ(yt∣st))

这里不必急着理解所有符号,只需要抓住三件事:

  • πθ 是学生模型;
  • ν 是教师模型;
  • A_t 表示教师相对学生有多认可当前 token。

A_t > 0 时提高该 token 的概率,A_t < 0 时降低。例如学生给"检查"的概率是 0.2、教师给 0.6,它会获得正向信号;若学生给 0.5、教师只给 0.1,则会被抑制。

OPD 可以直观地理解为:学生负责走到自己真实会遇到的位置,教师负责告诉它,这一步走得对不对。

把教师的意见变成训练信号,主要有两种做法:

  • GKD(Generalized Knowledge Distillation,广义知识蒸馏):教师告诉学生"下一个 token 的概率分布应该是什么样"。学生直接对齐教师返回的多个候选 token 概率,因此它更像传统的知识蒸馏。
  • PG-OPD(基于策略梯度的 OPD) :教师只评价学生已经采样出来的 token,再把教师与学生的概率差构造成 A_t。训练程序把它当作 advantage,通过策略梯度更新学生。

简单记忆就是:GKD 学教师的整组答案分布,PG-OPD 学教师对学生这一步的评价。

上面只是在解释训练直觉。两种方法具体怎样计算、为什么分别对应 forward KL 和 reverse KL,放到下一节再展开。

三、两条 OPD 路线:直接匹配分布,还是用策略梯度?

verl 当前主要提供两条 OPD 路径:直接匹配教师分布,以及把 reverse-KL 样本当成 policy-gradient 信号。

路线一:GKD,直接匹配教师 top-k 分布

forward KL 写成:

KL(ν∥πθ)= ∑v∈V ν(v∣st)log⁡ ν(v∣st) πθ(v∣st) \mathrm{KL}(\nu\Vert\pi_\theta) =\sum_{v\in V}\nu(v\mid s_t) \log\frac{\nu(v\mid s_t)}{\pi_\theta(v\mid s_t)} KL(ν∥πθ)=∑v∈Vν(v∣st)logπθ(v∣st)ν(v∣st)

它会同时观察教师认为可能的多个 token。

例如教师认为下一步既可以写"检查",也可以写"重新展开",forward KL 可以同时提高这两个合理方向,而不是只监督学生恰好采到的一个 token。

工程上通常不传完整词表,而只传教师 top-k:

text 复制代码
teacher 在每个位置返回:
top-k token ids + top-k logprobs

student 在相同 token ids 上取出自己的 logprobs
然后计算截断的 forward KL

优点是监督信息丰富,一次反传能调整多个候选 token。

代价也很直接:教师返回的数据量从每个位置 1 个 logprob,增加为 k 个 token id 和 k 个 logprob;学生侧还要计算或分块处理大词表 logits。

这条路线在 verl 中对应:

text 复制代码
loss_mode=forward_kl_topk
use_policy_gradient=False

loss 直接通过学生 logits 反向传播,不需要把蒸馏包装成 RL reward。

路线二:PG-OPD,用 k1 构造 advantage

reverse KL 为:

KL(πθ∥ν)= E a∼πθ log⁡πθ(a∣s)−log⁡ν(a∣s) \mathrm{KL}(\pi_\theta\Vert\nu) =\mathbb E_{a\sim\pi_\theta} \left \\log\\pi_\\theta(a\\mid s)-\\log\\nu(a\\mid s) \\right KL(πθ∥ν)=Ea∼πθlogπθ(a∣s)−logν(a∣s)

学生已经采样出了 token y_t,因此可以只查询这个 token 在教师侧的 logprob。

对应的单样本估计量是:

k1=log⁡ π θold (yt∣st)−log⁡ν(yt∣st) k_1= \log\pi_{\theta_{\mathrm{old}}}(y_t\mid s_t) -\log\nu(y_t\mid s_t) k1=logπθold(yt∣st)−logν(yt∣st)

这里的 k1 不是 top-1,而是 John Schulman 在 KL approximation 讨论中使用的一组简称。令 r = 教师概率 / 学生概率,常见写法是:

text 复制代码
k1 = -log r
k2 = 1/2 × (log r)²
k3 = r - 1 - log r
  • k1 最简单,单样本有正有负,取期望后得到 KL;
  • k2 是非负的平方近似,KL 较小时容易使用,但估计值有偏;
  • k3 逐样本非负,取期望仍得到 KL,通常比 k1 波动小。

verl 中的 klk1mse(Mean Squared Error,均方误差)与 k2low_var_klk3 分别是别名,另外还提供 abs。这些名字只是估计公式的标签,不是不同的 KL 方向;真正计算什么,仍要看公式以及概率比值的方向。

单个 k1 可以为负;只有对学生采样求期望时,它才等于非负的 reverse KL。

因为 token 来自学生分布:

E yt∼π log⁡π(yt∣st)−log⁡ν(yt∣st)=KL(π∥ν) \mathbb E_{y_t\sim\pi} \left \\log\\pi(y_t\\mid s_t)-\\log\\nu(y_t\\mid s_t) \\right =\mathrm{KL}(\pi\Vert\nu) Eyt∼πlogπ(yt∣st)−logν(yt∣st)=KL(π∥ν)

单次抽到教师更喜欢的 token,k1 可能为负;抽到学生过度自信的 token,k1 可能很大。只有许多学生 token 的均值才是在估计整体分布距离,所以不应要求每个 token 的 k1 都非负。

clamp 即截断:把信号限制在安全区间。例如区间为 [-5, 5] 时,85 处理,-7-5 处理,避免少数异常 token 产生过大梯度。

监控时应看整个 batch 的均值、绝对值、分位数和截断比例。若大量 token 触发 clamp,应检查采样温度、低概率 token 和师生对齐。

例子一:教师比学生更认可这个 token

假设学生给当前 token 的概率是 0.10,教师给 0.50

k1=log⁡0.10−log⁡0.50≈−1.61 k_1=\log 0.10-\log 0.50\approx-1.61 k1=log0.10−log0.50≈−1.61

PG-OPD 使用它的负数作为 advantage:

A=−k1≈+1.61 A=-k_1\approx+1.61 A=−k1≈+1.61

advantage 为正,更新会提高这个 token 的概率。

例子二:学生过度偏爱一个教师不认可的 token

假设学生给它 0.70,教师只给 0.10

k1=log⁡0.70−log⁡0.10≈+1.95 k_1=\log 0.70-\log 0.10\approx+1.95 k1=log0.70−log0.10≈+1.95

于是:

A=−k1≈−1.95 A=-k_1\approx-1.95 A=−k1≈−1.95

advantage 为负,更新会压低这个 token 的概率。

一句话概括:

text 复制代码
teacher 概率 > student 概率:鼓励这个 token
teacher 概率 < student 概率:抑制这个 token

为什么必须 stop-gradient?

理解这件事,关键是区分两份学生概率。它们不是两个学生模型,而是同一个模型在两个时间点的状态:

  • 旧学生概率:模型生成这批回答时记录的概率,作为固定的评分基准;
  • 当前学生概率:模型正在训练时重新计算的概率,梯度通过它更新参数。

刚开始训练这批数据时,二者通常相同。训练几步后,当前学生不断变化,旧学生仍保持为生成回答时的快照。

例如,学生生成 token"检查"时:

text 复制代码
教师 logprob:     -0.2
旧学生 logprob:   -0.8
教师评分 A_t:     -0.2 - (-0.8) = +0.6

为了说明更新方向,可以先把训练目标简化为:

python 复制代码
score = (teacher_logprob - old_student_logprob).detach()  # 固定为 +0.6
simple_loss = -score * current_student_logprob             # 梯度只走这里

这里的 current_student_logprob 仍然是同一个已采样 token"检查"的 logprob,只是改用当前学生重新计算。token 和它前面的文本都不变,变化的是学生模型的参数。实际训练会对回答中的每个 token 分别计算,再汇总 loss。

这只是帮助理解的简化写法,不是 verl 最终执行的完整 loss。PG-OPD 实际会比较当前学生与旧学生的概率,计算 PPO ratio,并通过 clipping 限制单次更新幅度。

detach() 不改变 +0.6,只告诉自动求导系统:分数已经算完,不再追踪其计算过程。 否则梯度还会沿 A_t 内的学生概率反传,让同一份概率既"给自己打分"又"根据分数学习",偏离预期的策略梯度。

verl 中的 advantages = -distillation_losses.detach(),正是先固定蒸馏分数。

一句话概括:旧概率负责算分,当前概率负责学习;stop-gradient 把两件事分开。

这条路线在 verl 中对应:

text 复制代码
loss_mode=k1
use_policy_gradient=True

两条路线该怎样选?

可以先记住这组取舍:

text 复制代码
GKD / top-k forward KL
  优点:一次看到多个教师候选,分布信号更完整
  代价:教师通信量和学生 logits 显存更高

PG-OPD / k1
  优点:每个位置只需要 sampled-token logprob
  代价:单样本方差更高,要依靠大量 on-policy token 平均

如果教师能高效返回 top-k,并且训练显存充足,可以优先尝试直接 GKD。

如果教师服务或网络通信是瓶颈,k1 + PG 更容易扩展。

这里涉及三个脚本名缩写:全分片数据并行(Fully Sharded Data Parallel,FSDP)、多教师 OPD(Multi-Teacher On-Policy Distillation,MOPD)和视觉语言(Vision-Language,VL)。

verl 两条路线都支持,但当前 OPD 示例更常把 PG-OPD 设为默认:Qwen3 的 FSDP、VeOmni、MOPD 和 VL 脚本均默认 k1 + use_policy_gradient=True;Megatron 示例默认 forward_kl_topk + use_policy_gradient=False,走 GKD。

因此,本文后面的 run_qwen3_8b_fsdp.sh 主要沿 PG-OPD 展开。

k3 和任务奖励是另外两项选择

确定 GKD 或 PG-OPD 路线后,还要分别决定使用哪种 KL 估计器,以及是否混合任务奖励。

verl 还支持 k3。它不是"第三个 token"或"top-3",而是一种 KL 单样本估计公式,用来把师生概率差变成非负距离。

先计算教师概率与学生概率的比值:

r= ν(yt∣st) π(yt∣st) r=\frac{\nu(y_t\mid s_t)}{\pi(y_t\mid s_t)} r=π(yt∣st)ν(yt∣st)

再计算:

k3=r−1−log⁡r k_3=r-1-\log r k3=r−1−logr

看三个例子:

text 复制代码
教师概率 = 学生概率:r = 1,k3 = 0
教师概率是学生的 2 倍:r = 2,k3 ≈ 0.31
教师概率是学生的一半:r = 0.5,k3 ≈ 0.19

k3 在师生概率相同时为零,差异越大通常越大。它相当于给 k1 加上一个期望为零的修正项,平均后仍估计 reverse KL,但单样本更稳定。

代价是 k3 只表示"差多少",不表示"该鼓励还是抑制"。它与 k1 + policy gradient 不是同一更新规则,选择时必须同时检查 loss_modeuse_policy_gradient

OPD 还可以与任务奖励联合:

L= Ltask +λ Ldistill \mathcal L= \mathcal L_{\mathrm{task}} +\lambda\mathcal L_{\mathrm{distill}} L=Ltask+λLdistill

纯 OPD 关闭任务 reward,完全学习教师;RL+OPD 则保留正确性、格式或偏好 reward,让环境反馈有机会纠正教师。

四、verl 是怎样把 OPD 跑起来的?

verl(Volcano Engine Reinforcement Learning)是字节跳动 Seed 团队发起、社区维护的大模型 RL 训练框架。它不是具体算法或推理引擎,而是把训练、rollout、奖励和分布式资源编排成可执行数据流。

在 OPD 中,verl 让学生生成、教师评分,再更新学生。负责训练的学生模型叫 actor(策略模型);rollout 引擎用其当前权重生成回答,actor 计算 loss、反向传播并更新权重。两者属于同一学生策略,只负责不同阶段。

先抓住一轮训练的主线:

text 复制代码
student rollout
  ↓
teacher 对 prompt + student response 评分
  ↓
teacher_ids / teacher_logprobs 写入 batch
  ↓
distillation_ppo_loss
  ↓
actor(学生训练模块)反向传播 / 更新参数

第一步:创建独立教师资源池

OPD 配置入口位于:

text 复制代码
verl/trainer/config/distillation/distillation.yaml

对应的数据结构位于:

text 复制代码
verl/workers/config/distillation.py

配置项很多,但第一次阅读只需要抓住四组参数。下面是 run_qwen3_8b_fsdp.sh 中最关键的 PG-OPD 配置:

bash 复制代码
distillation.enabled=True
distillation.n_gpus_per_node=4
distillation.nnodes=1

distillation.teacher_models.teacher_model.model_path=Qwen/Qwen3-32B
distillation.teacher_models.teacher_model.inference.name=vllm
distillation.teacher_models.teacher_model.inference.tensor_model_parallel_size=2
distillation.teacher_models.teacher_model.inference.gpu_memory_utilization=0.4
distillation.teacher_models.teacher_model.inference.max_model_len=3073

distillation.distillation_loss.loss_mode=k1
distillation.distillation_loss.use_policy_gradient=True
distillation.distillation_loss.use_task_rewards=False
distillation.distillation_loss.loss_max_clamp=10.0
distillation.distillation_loss.log_prob_min_clamp=-10.0

第一组:是否启用,以及给教师多少资源。

  • distillation.enabled=True:打开 OPD;关闭时不会创建教师服务。
  • n_gpus_per_node=4nnodes=1:为教师资源池准备 1 个节点、每个节点 4 张图形处理器(Graphics Processing Unit,GPU),总计 4 张。这部分资源运行教师,不是训练 Qwen3-8B 学生的 GPU。

RayPPOTrainer 发现 OPD 已启用后,会为 Role.TeacherModel 创建独立资源池,再建立 MultiTeacherModelManager 管理教师请求。

第二组:教师是谁,以及怎样运行。

  • model_path=Qwen/Qwen3-32B:指定教师模型权重。
  • inference.name=vllm:使用 vLLM 启动教师推理服务。
  • tensor_model_parallel_size=2:一套教师服务使用 2 张 GPU 做张量并行(Tensor Parallelism,TP)。
  • gpu_memory_utilization=0.4:限制教师 vLLM 可使用的显存比例。
  • max_model_len=3073:容纳 prompt、完整 response 和额外 1 个生成位置。

replica一套可独立接收请求的教师服务副本,逻辑上拥有完整模型,权重可以拆到多张 GPU。TP 和 replica 解决不同问题:

  • TP 决定一套教师服务内部使用几张 GPU:TP=2 表示两张 GPU 合作完成同一次教师推理。
  • replica 决定启动几套独立教师服务:replica 越多,可以同时处理的教师请求通常越多。

计算 replica 数可以拆成两步。这里的 DP 是数据并行(Data Parallelism),PP 是流水线并行(Pipeline Parallelism)。

第一步,计算每个 replica 占用多少张 GPU:

text 复制代码
每个 replica 占卡数 = TP × DP × PP
                    = 2 × 1 × 1
                    = 2 张 GPU

第二步,用教师资源池的总 GPU 数除以每个 replica 的占卡数:

text 复制代码
replica 数 = 4 张教师 GPU ÷ 2 张 GPU
            = 2

因此,这 4 张卡会被分成两组,每组启动一套独立的教师服务:

text 复制代码
GPU 0 + GPU 1 → teacher replica 1
GPU 2 + GPU 3 → teacher replica 2

每个 replica 内的两张 GPU 共同承载 Qwen3-32B,两个 replica 可并行处理不同评分请求。增加 TP 主要解决单份模型的显存与计算,增加 replica 主要提高并发吞吐。

第三组:使用哪种蒸馏更新。

  • loss_mode=k1:只比较学生采样出的 token 在师生模型下的 logprob。
  • use_policy_gradient=True:把 -k1 当作 advantage,通过策略梯度更新学生。这两个参数组合起来才是前面介绍的 PG-OPD。
  • 如果改成 GKD,通常使用 loss_mode=forward_kl_topk,同时设置 use_policy_gradient=False,直接反向传播教师 top-k 分布的蒸馏 loss。
  • topk 只在 forward_kl_topk 等需要教师候选分布的模式下生效。

第四组:是否混合任务奖励,以及怎样保证数值稳定。

  • use_task_rewards=False:关闭正确性等奖励,只学习教师信号,属于纯 OPD。
  • 打开后,distillation_loss_coef 控制蒸馏信号与任务奖励的相对权重。
  • loss_max_clamp=10.0:把过大的蒸馏 loss 截断到 10。
  • log_prob_min_clamp=-10.0:避免极低概率产生绝对值过大的 logprob 差。

第二步:学生生成以后,教师只负责评分

学生 rollout 完成后,agent_loop.py 中的 _compute_teacher_logprobs 会把下面这段序列送给教师:

text 复制代码
prompt_ids + response_ids

teacher_manager.py 使用的关键请求参数是:

text 复制代码
max_tokens=1
temperature=1.0
prompt_logprobs=0 或 topk

max_tokens=1 只是为了复用生成服务并取得输入序列的 prompt logprobs,不是用教师新生成的 token 训练学生。计算 k1 时,只需查询教师对学生已采样 token 给出的 logprob;计算 forward_kl_topk 时,还需要教师在各位置返回 top-k token ids 和对应的 logprobs。

这里有两个工程前提。

第一,师生通常必须共享 tokenizer 和词表,确保同一 token id 表示同一内容。跨 tokenizer 需要额外的重分词、位置映射或投影目标,当前 OPD 路径默认同词表对齐。

第二,教师上下文必须容纳完整学生序列:

教师的 max_model_len 至少需要覆盖:

text 复制代码
student prompt length
+ student response length
+ 1 dummy token

学生能生成 8K response,不代表教师的默认上下文也能完整评分;verl 会在初始化时校验这一点。

第三步:actor 切换到蒸馏 loss

开启 OPD 后,verl/workers/engine_workers.py 会把普通 ppo_loss 替换为 distillation_ppo_loss

主要逻辑位于:

text 复制代码
verl/trainer/distillation/losses.py

两条核心函数分别是:

text 复制代码
compute_distillation_loss_reverse_kl_estimator
compute_forward_kl_topk

前者计算 student_logprob - teacher_logprob,在 PG 模式下取负并 detach 为 advantage。

后者使用教师 top-k 分布直接计算 forward-KL 近似,再反向传播。

OPD 是不是 GRPO?

不是。示例脚本虽然设置:

text 复制代码
algorithm.adv_estimator=grpo

这是因为 OPD 复用了 PPO/GRPO 的 rollout、batch 和 actor-update 框架。但纯 OPD 同时设置:

text 复制代码
use_task_rewards=False

此时任务 reward 的 policy loss 被置零,更新来自教师的 distillation advantage:

text 复制代码
OPD 可以借用 GRPO/PPO 的训练框架,
但 OPD 本身不是 GRPO。

VeOmni 与全分片数据并行是什么关系?

VeOmni 不是一种并行方式,而是学生训练引擎;全分片数据并行(Fully Sharded Data Parallel,FSDP)及其第二代实现 FSDP2,才是它用于训练学生的并行策略。run_qwen3_0.6b_opd_veomni.sh 里的分工是:

text 复制代码
学生 rollout / 生成:vLLM
学生训练 / 反向传播:VeOmni → FSDP2
教师在线评分:vLLM → TP(如有需要)

脚本中的 model_engine=veomni 选择 VeOmni。它使用第二代全分片数据并行(FSDP2)来训练学生;FSDP2 才是把学生训练状态分到多张 GPU 的并行策略。TP 则用于让一份推理模型跨多张 GPU 运行,常用于 vLLM rollout 或教师服务。

FSDP 要解决的是:训练时除了模型参数,还要保存梯度和优化器状态;普通数据并行会让每张 GPU 都保存完整的一份,模型一大就容易放不下。FSDP 则把这三类状态切分到多张 GPU:

text 复制代码
普通数据并行:每张 GPU 都保存完整模型
FSDP:GPU 0、1、2、3 各保存约 1/4 的参数、梯度和优化器状态

计算到某一层时,各 GPU 临时聚合这一层所需的参数;这一层计算结束后,再把它们分开保存。这样单张 GPU 的常驻训练显存更小,学生模型可以更大。

除了训练编排,VeOmni 还提供 fused kernel。在 forward_kl_topk 下,它可分块计算 lm_head 和蒸馏 loss,避免完整物化 [B,L,V] logits。其中 B 表示 batch size,L 表示序列长度,V 表示词表大小:

text 复制代码
B × L × V

Qwen3 词表超过 15 万,长 response 下最后一层 logits 可能造成显存峰值。chunked/fused 路径逐段通过 lm_head 并立即提取 top-k 所需值,以更多 kernel 调度换取更低显存,主要适合长上下文或 OOM(Out of Memory,内存或显存不足)场景。

教师仍然由:

text 复制代码
distillation.teacher_models.*.inference.name=vllm

启动。

这个脚本默认使用 7 张学生训练/rollout 卡和 1 张教师 vLLM 卡,因此它也不是单卡配置。

五、Qwen3 与 DeepSeek-V4 怎样使用 OPD?

工业实践最容易混淆官方披露、社区推测和开源复现,下面严格区分三者。

Qwen3:小模型通过蒸馏复用大模型的后训练能力

Qwen3 的旗舰模型先经历完整的四阶段后训练:

text 复制代码
Long-CoT cold start
  ↓
Reasoning RL
  ↓
Thinking / non-thinking mode fusion
  ↓
General-domain RL

轻量模型不再把这四个阶段从头各跑一遍,而是让完成后训练的强模型充当教师。这就是 strong-to-weak:用能力更强的模型,把推理和回答能力迁移给更小、更便宜的学生。

报告对小模型蒸馏公开了三点:

  • 既做离线蒸馏:学生学习教师预先生成好的内容;
  • 也做在线蒸馏:学生先生成,再由教师在学生走到的位置给出 token 级指导;
  • 不只学习教师文本:还学习教师对候选 token 的打分(output logits)。

报告称,相比完整四阶段流程,这种做法的训练 GPU-hours 约为十分之一,同时改善 Pass@1(每题生成一次的通过率)和 Pass@64(每题生成 64 次、至少一次通过的比例)。

因此,公开资料只能确认"离线蒸馏 + 在线蒸馏"的方向;教师组合、数据比例、KL、top-k 和任务奖励等具体配方并未公开。

verl 的 Qwen3-8B ← Qwen3-32B 脚本只是用同一模型家族演示 OPD 的工程示例,不是 Qwen 团队发布的官方训练命令。

DeepSeek-V4:先培养专才,再用 OPD 合并

DeepSeek-V4 的官方模型卡公开了清晰的两阶段后训练流程:先培养领域专家,再用 on-policy distillation 合并为统一模型

text 复制代码
第一阶段:按领域训练专家
数学 / 代码 / Agent / 指令跟随
每个专家:SFT → GRPO
          ↓
第二阶段:统一模型做 OPD
学生在自己的 rollout 上向领域专家学习

第一阶段的目标不是直接训练一个万能模型,而是先把每项能力练到足够强。每个领域专家先用高质量数据做监督微调(Supervised Fine-Tuning,SFT),再按该领域的奖励信号做组相对策略优化(Group Relative Policy Optimization,GRPO)。

第二阶段才是 OPD:统一学生模型先生成自己的回答;然后在这些学生生成的前缀上,让对应领域的专家给出 token 级指导。这样合并的不是多份模型权重,而是把多个专家的行为能力压进同一个学生模型。

这正是 Multi-Teacher OPD 想解决的问题:数学、代码、Agent 和指令跟随分别优化更容易;最终模型又必须同时具备这些能力。DeepSeek 官方模型卡确认了这条"专才 → 统一模型"的路线,但没有公开完整训练 recipe。

例如,以下细节仍无法从公开材料逐项核验:

text 复制代码
实际训练了多少个专家、各自的数据规模
一个样本如何选择或混合多个教师
教师返回完整词表 logits 还是 top-k 概率
不同领域的训练比例、学习率和稳定化参数

因而,verl 的多教师 OPD 可以复现其核心机制,但不能声称逐项复刻 DeepSeek-V4 的内部配置。

六、怎样把 OPD 跑起来?

把 OPD 从公式变成一次训练,先后解决三个问题:谁生成、谁评分、教师怎样供给。

建议先用一位教师打通闭环;确认链路正确后,再按数据领域扩展为多教师。脚本和 GPU 配置只是实现这条顺序的工具。

先用一位教师打通训练闭环

最小实验只验证:学生能否在自己的生成轨迹上,拿到教师的 token 级评分并完成一次更新。不追求最终成绩,也不叠加任务 reward。

text 复制代码
题目 → Qwen3-0.6B(学生)生成回答
     → Qwen3-1.7B(教师)评价学生实际写出的每个 token
     → k1 生成固定训练信号
     → 更新学生;教师始终冻结

下面只保留影响 OPD 数据流的核心覆盖项。实际运行时,应从仓库里的示例脚本启动,让脚本补齐 batch、学习率、日志和保存等基础配置。

bash 复制代码
python3 -m verl.trainer.main_ppo \
  algorithm.adv_estimator=grpo \
  actor_rollout_ref.model.path=Qwen/Qwen3-0.6B \
  actor_rollout_ref.rollout.name=vllm \
  data.train_files="$HOME/data/gsm8k/train.parquet" \
  data.max_prompt_length=512 \
  data.max_response_length=1024 \
  trainer.n_gpus_per_node=7 trainer.nnodes=1 \
  distillation.enabled=True \
  distillation.n_gpus_per_node=1 distillation.nnodes=1 \
  distillation.teacher_models.teacher_model.model_path=Qwen/Qwen3-1.7B \
  distillation.teacher_models.teacher_model.inference.name=vllm \
  distillation.teacher_models.teacher_model.inference.max_model_len=1537 \
  distillation.distillation_loss.loss_mode=k1 \
  distillation.distillation_loss.use_policy_gradient=True \
  distillation.distillation_loss.use_task_rewards=False

这些参数按一次训练的流动顺序分为四组。

学生怎样生成?

actor_rollout_ref.model.path 是学生模型;它既生成 rollout,也接收反向传播。actor_rollout_ref.rollout.name=vllm 指定由 vLLM 做学生 rollout,而不是指定教师。

data.train_files 给出训练数据。max_prompt_length=512max_response_length=1024 分别限制输入与生成长度;response 越长,学生生成、教师评分和显存成本都会增加。

教师怎样评分?

distillation.enabled=True 打开教师服务和蒸馏训练。teacher_model.model_path 指定冻结教师的权重,teacher_model.inference.name=vllm 指定教师的推理引擎。

教师的 max_model_len=1537 是一次请求可容纳的最大序列长度:512 个 prompt token、1024 个 response token 和 1 个 dummy token。

学生依据什么更新?

algorithm.adv_estimator=grpo 选择训练器的 GRPO advantage 管线。

loss_mode=k1use_policy_gradient=True 把教师和旧学生对实际采样 token 的 logprob 差,变成当前学生的更新信号。use_task_rewards=False 表示不根据最终答案对错再追加奖励。

GPU 怎样分配?

trainer.n_gpus_per_node=7 给学生训练和 rollout;distillation.n_gpus_per_node=1 给教师服务。两者是独立资源池,因此这个示例合计需要 8 张 GPU。

再按领域扩展为多教师 OPD

多教师 OPD(Multi-Teacher On-Policy Distillation,MOPD)不是让多位教师对每个 token 投票。

它先按样本所属领域选出一位教师,再由该教师完成 token 级评分。数学题可路由给数学教师,代码题路由给代码教师;最后仍只更新同一个学生模型。

text 复制代码
math 样本 → math_teacher → 同一个 student
code 样本 → code_teacher → 同一个 student

在 verl 中,样本字段 data_source 负责路由,值必须与教师的 key 一致:

例如,一条数学训练样本带有 data_source="math",一条代码样本带有 data_source="code"。设置 distillation.teacher_key=data_source 后,训练器会从当前样本读出该字段的值,再按这个值查找教师:

text 复制代码
样本 data_source="math"
        ↓
teacher_key=data_source
        ↓
路由值为 "math"
        ↓
找到 key="math" 的 math_teacher

注意:math_teacher 是配置里的名字,真正参与查找的是它的 key="math"。因此它们可以不同;但样本的 data_source 必须等于 key,而不是等于 math_teacher

bash 复制代码
distillation.teacher_key=data_source \
distillation.n_gpus_per_node=4 distillation.nnodes=1 \
+distillation.teacher_models.math_teacher.key=math \
+distillation.teacher_models.math_teacher.model_path=/models/qwen3-math-32b \
+distillation.teacher_models.math_teacher.num_replicas=1 \
+distillation.teacher_models.math_teacher.inference.tensor_model_parallel_size=2 \
+distillation.teacher_models.code_teacher.key=code \
+distillation.teacher_models.code_teacher.model_path=/models/qwen3-code-32b \
+distillation.teacher_models.code_teacher.num_replicas=1 \
+distillation.teacher_models.code_teacher.inference.tensor_model_parallel_size=2

这组多教师参数分别控制:

  • teacher_key=data_source :从样本的 data_source 字段读取路由值。
  • *.key :该教师可接收的路由值,例如 mathcode
  • *.model_path:该领域教师加载的权重。
  • *.num_replicas:该教师启动多少份相同副本;副本越多,可同时处理的请求越多,但占用更多 GPU。
  • *.tensor_model_parallel_size=2:每一份副本内部用两张 GPU 协同推理。

本例中,每个教师的 num_replicas=1,并使用张量并行(Tensor Parallelism,TP)=2。

若数据并行(Data Parallelism,DP)=1、流水线并行(Pipeline Parallelism,PP)=1,数学教师占 2 张 GPU,代码教师占 2 张 GPU,总共占 4 张。

热门领域的样本更多时,应增加该教师的 replica;否则学生会等教师排队。

Hydra 默认已有 teacher_model 占位项。添加多教师时全部使用 math_teachercode_teacher 这类新名字,不要与默认项混用。

shell 脚本只是拼装配置

这类脚本没有依次启动六个程序。它只是把一条很长的启动命令拆开写,方便阅读和修改。

例如脚本里可能有:

bash 复制代码
DATA=(data.max_prompt_length=512)
EXTRA=(distillation.enabled=True)

结尾的 "${DATA[@]}" 会展开为 DATA 数组中的所有参数。所以下面这条:

bash 复制代码
python3 -m verl.trainer.main_ppo \
  "${DATA[@]}" "${MODEL[@]}" "${ACTOR[@]}" \
  "${ROLLOUT[@]}" "${TRAINER[@]}" "${EXTRA[@]}" \
  "$@"

等价于把 DATAMODELACTORROLLOUTTRAINEREXTRA 中的每一项,按顺序都写在 python3 -m verl.trainer.main_ppo 后面。Hydra 再把这些 字段=值 参数合并成最终配置。

末尾的 "$@" 表示:把你运行脚本时额外写在后面的参数,也追加到同一条命令。例如:

bash 复制代码
bash run_qwen3_0.6b_opd_veomni.sh trainer.total_epochs=1

这里的 trainer.total_epochs=1 会覆盖脚本内的默认值。

例如从 PG-OPD 改为 top-k GKD 时,要同时切换 loss 和梯度方式:

bash 复制代码
bash examples/on_policy_distillation_trainer/run_qwen3_0.6b_opd_veomni.sh \
  trainer.total_epochs=1 \
  distillation.distillation_loss.loss_mode=forward_kl_topk \
  distillation.distillation_loss.use_policy_gradient=False

这两个字段必须同时改:top-k GKD 要直接对教师给出的多个 token 分布反向传播;PG-OPD 则只对学生实际采样的 token 使用 policy gradient。

结语

离线蒸馏问的是:教师的好答案应该怎样续写?

OPD 问的是:学生已经这样写了,下一步应该怎样纠正?

两者的差别不在于有没有 KL,而在于训练状态来自谁。前者沿着教师答案学习;后者沿着学生实际会走到的前缀学习。因此,当长回答中前面一步的失误会不断影响后续时,OPD 能让教师在错误刚出现的位置给出更密集的信号。

代价也很直接:训练时要反复让教师评价学生的新 rollout,成本更高;而且教师在异常前缀上不一定仍然可靠。OPD 不是"用了就更强"的配方,它适合解决的是学生生成轨迹与教师数据不一致的问题。

所以判断一个 OPD 方案时,先问三个问题:学生实际会生成哪些前缀?教师能否在这些前缀上给出可信的 token 级判断?学生的容量能否吸收这份指导?这三个问题比先选 k1、top-k 或某个框架更根本。

参考资料

相关推荐
朱峥嵘(朱髯)1 小时前
数据库如何根据全表 NDV 估算子集的 NDV
数据库·算法
jjjava2.01 小时前
牛客算法题(第四期)
算法
雪碧聊技术1 小时前
力扣 回溯法 | LCR 020. 回文子串
javascript·算法·leetcode
wabs6661 小时前
关于哈希表【力扣454.四数相加II的思考】
数据结构·算法·leetcode·散列表
我能坚持多久2 小时前
优选算法——专题一双指针(上):附四道例题详解
c++·学习·算法
Tim_102 小时前
【C++】023、移动语义&深拷贝
开发语言·c++·算法
月光船幽幽2 小时前
锁死后干预有效性的关键突破
人工智能·python·算法
2501_906565122 小时前
哥德巴赫猜想
算法
ShineWinsu2 小时前
对于C++中unordered_map的详细介绍
数据结构·c++·算法·面试·stl·哈希表·unordered_map