Justin Huang

Back

SFT / OPD / RL 的第一性原理#

SFT、RL、OPD 最底层都在改模型的 next-token 条件分布 πθ(ytht)\pi_\theta(y_t\mid h_t),但它们区别在于:训练 prefix 从哪里来、target token 是谁生成的、每个 token 的权重/方向由什么决定。

在 slime 里,OPD 不是单独写成一个“纯 KL loss”替代 RL,而是把 teacher 对 student rollout token 的 logprob 差异转成一个 dense token-level advantage shaping 信号,再走和 PPO/GRPO 类似的 policy loss 更新路径。slime README 也把它定位成一个 Megatron training + SGLang rollout + Data Buffer 的统一 RL/dataflow 框架,rollout 侧生成数据和 verifier/reward,training 侧从 Data Buffer 读数据并更新模型。(GitHub)


1. 先从大模型 next-token 建模说起#

大模型本质上学的是一个条件分布:

πθ(ytht)\pi_\theta(y_t \mid h_t)

其中:

ht=(x,y<t)h_t = (x, y_{<t})

也就是当前 prompt + 已经生成的前缀。

整条 response 的概率是:

πθ(y1:Tx)=t=1Tπθ(ytx,y<t)\pi_\theta(y_{1:T}\mid x) = \prod_{t=1}^{T} \pi_\theta(y_t\mid x,y_{<t})

所以 logprob 是加和:

logπθ(y1:Tx)=t=1Tlogπθ(ytht)\log \pi_\theta(y_{1:T}\mid x) = \sum_{t=1}^{T} \log \pi_\theta(y_t\mid h_t)

所有 SFT、RL、OPD 最后都会落到这件事:

在某些 prefix hth_t 下,让某些 token 的 logπθ(ytht)\log \pi_\theta(y_t\mid h_t) 上升或下降。

区别只是:

  • SFT:prefix 来自 expert trajectory,target 是 expert token,永远正向提升;
  • RL:prefix 来自 student rollout,target 是 student 自己采样的 token,advantage 正则提升、负则压低;
  • OPD:prefix 来自 student rollout,target 也是 student 采样 token,但 teacher 会评价这个 token 在该 prefix 下是否合理。

2. SFT 到底在更新什么?#

SFT loss 是:

LSFT=tlogπθ(ytht)\mathcal{L}_{SFT} = -\sum_t \log \pi_\theta(y_t^{*}\mid h_t^{*})

其中 yty_t^{*} 是 expert/human/teacher 数据里的 gold token。

slime 里的 sft_loss_function 也是这个逻辑:先用当前模型 logits 计算 response tokens 的 log probabilities,然后 loss 是负的 mean logprob:

loss=mean(logpθ(yt))loss = - \text{mean}(\log p_\theta(y_t))

源码里 sft_loss_function 明确写了它是 response tokens 上的 negative log-likelihood,并且 loss = -sum_of_sample_mean(log_probs)。(GitHub)

从 logit 梯度看 SFT#

模型在某个位置输出 logits:

zRVz \in \mathbb{R}^{|V|}

经过 softmax:

pj=ezjkezkp_j = \frac{e^{z_j}}{\sum_k e^{z_k}}

如果 gold token 是 yy,cross entropy 是:

L=logpy\mathcal{L} = -\log p_y

它对 logit 的梯度是经典结果:

Lzj=pj1[j=y]\frac{\partial \mathcal{L}}{\partial z_j} = p_j - \mathbb{1}[j=y]

所以:

  • 对 gold token yy:梯度是 py1<0p_y-1 < 0,梯度下降会让 zyz_y 变大;
  • 对其他 token jj:梯度是 pj>0p_j > 0,梯度下降会让 zjz_j 变小。

因此你说的“加大目标 token 在当前条件下的 logit/概率”对 SFT 是完全准确的。

但注意:SFT 只在 expert prefix 上这样做。

它学的是:

πθ(ytexperthtexpert)\pi_\theta(y_t^{expert}\mid h_t^{expert})

而不是:

πθ(correct actionhtstudenterror)\pi_\theta(\text{correct action}\mid h_t^{student-error})

这就是 exposure bias 的根源。


3. RL / GRPO 到底在更新什么?#

RL 不是给 gold token,而是让模型自己 rollout:

y1:Tπθ(x)y_{1:T} \sim \pi_\theta(\cdot\mid x)

然后 verifier/reward 给一个分数:

R(y1:T)R(y_{1:T})

最原始 policy gradient 是:

LRL=tAtlogπθ(ytht)\mathcal{L}_{RL} = -\sum_t A_t \log \pi_\theta(y_t\mid h_t)

这里 yty_t 是 student 自己采样出来的 token,AtA_t 是 advantage。

如果:

At>0A_t > 0

说明这个 token/action 所在轨迹比预期好,那么 loss 会推动:

logπθ(ytht)\log \pi_\theta(y_t\mid h_t) \uparrow

如果:

At<0A_t < 0

说明这个 token/action 所在轨迹比预期差,那么 loss 会推动:

logπθ(ytht)\log \pi_\theta(y_t\mid h_t) \downarrow

所以你说“RL 本质上是不是也像 SFT 一样增加目标 token 概率”——答案是:

形式上类似,都是在 selected token 的 logprob 上反传;但 RL 的 token 不一定是正确 token,而且方向由 advantage 决定:好 token 增强,坏 token 压低。

在 slime 里,policy loss 会先计算当前模型在 response tokens 上的 log_probs,然后根据 old log_probs 和 current log_probs 得到 PPO-style ratio,再用 advantage 计算 clipped policy loss。源码中 policy_loss_function 里会计算当前 log_probs、旧 old_log_probsppo_kl = old_log_probs - log_probs,再调用 compute_policy_loss。(GitHub)

compute_policy_loss 里核心就是:

ratio=elogpθlogpoldratio = e^{\log p_{\theta} - \log p_{old}}

然后:

pgloss=ratioApg_loss = -ratio \cdot A

并带 PPO clip。源码里对应 ratio = (-ppo_kl).exp(),因为 ppo_kl = old_log_probs - log_probs,所以 -ppo_kl = log_probs - old_log_probs。(GitHub)


4. OPD 到底在做什么?#

OPD 的基本思想是:

让 student 先走自己的轨迹,然后 teacher 在 student 自己产生的 prefix 上给监督。

普通 off-policy distillation / SFT 是:

ht=htteacherh_t = h_t^{teacher}

OPD 是:

ht=htstudenth_t = h_t^{student}

这就是“on-policy”的含义。

已有 OPD 文献也基本这样定义:student 在自己生成的 rollouts 上接受 token-level teacher supervision。近期一些工作也指出,OPD 在长程任务中会遇到 student prefix drift、teacher signal 失效等问题。(arXiv)


5. 经典 KL distillation 是怎么工作的?#

先说最标准的“全分布 KL”。

在某个 student prefix hth_t 下:

  • student 分布:
pθ(v)=πθ(vht)p_\theta(v)=\pi_\theta(v\mid h_t)
  • teacher 分布:
pT(v)=πT(vht)p_T(v)=\pi_T(v\mid h_t)

5.1 Forward KL#

DKL(pTpθ)=vpT(v)[logpT(v)logpθ(v)]D_{KL}(p_T|p_\theta) = \sum_v p_T(v) \left[ \log p_T(v)-\log p_\theta(v) \right]

对 student 来说,第一项 vpT(v)logpT(v)\sum_v p_T(v)\log p_T(v) 是常数,所以优化等价于 soft-label cross entropy:

L=vpT(v)logpθ(v)\mathcal{L} = -\sum_v p_T(v)\log p_\theta(v)

它的梯度会把 student 的整个 vocab 分布往 teacher 靠:

Lzj=pθ(j)pT(j)\frac{\partial \mathcal{L}}{\partial z_j} = p_\theta(j)-p_T(j)

所以:

  • 如果 teacher 在某 token 上概率比 student 高:
pT(j)>pθ(j)p_T(j)>p_\theta(j)

梯度下降会提高这个 token 的 logit。

  • 如果 teacher 在某 token 上概率比 student 低:
pT(j)<pθ(j)p_T(j)<p_\theta(j)

梯度下降会降低这个 token 的 logit。

这是真正意义上的“让学生 logits 分布靠近 teacher logits 分布”。

5.2 Reverse KL#

DKL(pθpT)=vpθ(v)[logpθ(v)logpT(v)]D_{KL}(p_\theta|p_T) = \sum_v p_\theta(v) \left[ \log p_\theta(v)-\log p_T(v) \right]

它更像:

student 当前认为可能的 token,如果 teacher 不认可,就压下去;teacher 认可,就保留。

Reverse KL 通常更 mode-seeking,也就是更倾向于让 student 集中到 teacher 支持的高概率模式上。


6. slime 里的 OPD 更像哪一种?#

slime 当前源码里实现的 OPD 更接近 sampled-token reverse-KL shaping,不是完整 full-vocab KL。

关键数据字段是:

teacher_log_probs
rollout_log_probs
tokens
loss_masks
python

slime 的 rollout 侧会把 teacher_log_probs 作为 rollout_data 的一部分传到训练侧;_ROLLOUT_DATA_TENSOR_DTYPES 里也明确包含 teacher_log_probsrollout_log_probs。(GitHub)

参数上,slime 支持:

  • --use-opd
  • --opd-type sglang
  • --opd-type megatron
  • --opd-kl-coef

源码说明 sglang 模式是在 rollout 时从外部 SGLang teacher server 获取 teacher log-probs;megatron 模式是在训练时加载 teacher checkpoint 做 forward,并且 teacher 要和 policy/ref model 架构一致。(GitHub)

6.1 slime OPD 的核心代码#

核心函数是:

apply_opd_kl_to_advantages(...)
python

它做的是:

reverse_klt=logπθ(atht)logπT(atht)\text{reverse\_kl}_t = \log \pi_\theta(a_t\mid h_t) - \log \pi_T(a_t\mid h_t)

然后修改 advantage:

At=Atβreverse_kltA'_t = A_t - \beta \cdot \text{reverse\_kl}_t

源码 docstring 也直接写了:计算 reverse KL,即 student_logp - teacher_logp,并把加权 penalty 加到 advantages 里。(GitHub)

所以,在 slime 里,OPD 不是一个单独的:

LOPD=DKL(pTpθ)\mathcal{L}_{OPD} = D_{KL}(p_T|p_\theta)

而是:

AtAtβ[logπθ(atht)logπT(atht)]A_t \leftarrow A_t - \beta \left[ \log \pi_\theta(a_t\mid h_t) - \log \pi_T(a_t\mid h_t) \right]

然后继续走 policy loss。

源码里也明确说 OPD 是 orthogonal to advantage estimators,可以叠加在 GRPO、PPO 等 estimator 上,通过给 advantage 加 KL penalty 来实现。(GitHub)


7. slime OPD 中,学生到底学到了什么信号?#

假设某个 prefix 是:

h_t = "Task ... previous commands ... stderr ..."
text

student 采样了一个 token/action:

a_t = "rm"
text

student 自己给这个 token 的 logprob 是:

logπθ(atht)=1.0\log \pi_\theta(a_t\mid h_t) = -1.0

teacher 给这个 token 的 logprob 是:

logπT(atht)=5.0\log \pi_T(a_t\mid h_t) = -5.0

那么:

reversekl=1.0(5.0)=4.0reverse_kl = -1.0 - (-5.0)=4.0

如果 OPD 系数 β=0.2\beta=0.2,基础 advantage At=0A_t=0,则:

At=00.2×4=0.8A'_t=0-0.2\times 4=-0.8

于是 policy loss 会压低这个 token 的概率。

直觉是:

student 很想输出这个 token,但 teacher 很不认可,所以这个 token 应该被压下去。

反过来,如果:

logπθ(atht)=3.0\log \pi_\theta(a_t\mid h_t)=-3.0 logπT(atht)=0.5\log \pi_T(a_t\mid h_t)=-0.5

那么:

reversekl=2.5reverse_kl=-2.5 At=00.2×(2.5)=+0.5A'_t=0-0.2\times(-2.5)=+0.5

于是 policy loss 会提高这个 token 概率。

直觉是:

student 采样到了一个 token,但自己信心不高;teacher 很认可,所以应该加强。

这就是 slime OPD 最核心的学习信号:

teacher 不直接告诉学生“应该输出哪个替代 token”,而是评价 student 已经采样出来的 token 是否合理。

如果 teacher_logprob 高,就强化。 如果 teacher_logprob 低,就抑制。


8. 这和“靠近 teacher logits 分布”有什么区别?#

你问得非常准。

完整 KL distillation 是:

student 的整个 vocab 分布靠近 teacher 的整个 vocab 分布。

也就是你真的拿到了:

pT(vht)p_T(v\mid h_t)

对所有 vocab token vv 的概率。

但 slime 这个实现更像:

对 student rollout 中实际出现的 token ata_t,拿 teacher 对这个 token 的 logprob,然后做 sampled reverse-KL / advantage shaping。

也就是说它只知道:

logπT(atht)\log \pi_T(a_t\mid h_t)

而不一定知道 teacher 对其他 token 的完整分布。

所以它不能直接说:

teacher 更想让你输出 pytest,而不是 rm
text

它只能说:

你刚才输出 rm,teacher 给 rm 的概率很低,所以降低 rm。
text

当然,降低 rm 的概率后,softmax 概率质量会重新分配给其他 token;如果模型本来知道 pytestlscat 等替代 token,那么这些替代 token 的相对概率可能上升。

但这是间接的。

所以我们要区分两种 OPD:

Full-distribution OPD#

L=DKL(πT(ht)πθ(ht))\mathcal{L} = D_{KL} \left( \pi_T(\cdot\mid h_t) | \pi_\theta(\cdot\mid h_t) \right)

优点:teacher 明确告诉 student 整个分布应该长什么样。 缺点:需要 full logits,成本高。

Sampled-token OPD#

signalt=logπT(atht)logπθ(atht)\text{signal}_t = \log \pi_T(a_t\mid h_t) - \log \pi_\theta(a_t\mid h_t)

优点:便宜,只需要 teacher 给 student 采样 token 的 logprob。 缺点:teacher 没有显式给出替代 token,长程 drift 后信号可能变弱。

近期 OPD 论文也在讨论 sampled-token OPD 的不稳定性:比如 student 走到坏 prefix 后,teacher 可能局部也给不出有效纠错信号,出现 low-KL agreement trap;或者 sampled-token signal 太局部,不足以表达 teacher 真正的替代分布。(arXiv)


9. slime 里 RL 和 OPD 是怎么合在一起的?#

在 slime 的 compute_advantages_and_returns 中,先根据 advantage_estimator 算出基础 advantage/returns。支持的 estimator 包括 GRPO、GSPO、CISPO、PPO、REINFORCE++ 等。然后如果 args.use_opd,就调用 apply_opd_kl_to_advantages 修改 advantages。(GitHub)

所以整体逻辑是:

AtRL=advantage from reward/verifier/group baselineA_t^{RL} = \text{advantage from reward/verifier/group baseline} AtOPD=β[logπθ(atht)logπT(atht)]A_t^{OPD} = -\beta \left[ \log \pi_\theta(a_t\mid h_t) - \log \pi_T(a_t\mid h_t) \right] Atfinal=AtRL+AtOPDA_t^{final} = A_t^{RL} + A_t^{OPD}

然后 policy loss 用:

L=tratiotAtfinal\mathcal{L} = -\sum_t ratio_t A_t^{final}

其中:

ratiot=πθ(atht)πold(atht)ratio_t = \frac{ \pi_\theta(a_t\mid h_t) }{ \pi_{old}(a_t\mid h_t) }

再带 PPO clip。

所以 slime OPD 的本质不是“另开一个 OPD loss 分支”,而是:

把 teacher 对 token 的认可度变成 dense advantage correction。


10. 从第一性原理看:update 后模型会变成什么样?#

我们考虑某个 token ata_t

训练时 policy loss 近似是:

Lt=Atlogπθ(atht)\mathcal{L}_t = -A_t \log \pi_\theta(a_t\mid h_t)

为了直觉先忽略 PPO ratio 和 clip。

对 logits 的梯度是:

Ltzj=At[pθ(jht)1[j=at]]\frac{\partial \mathcal{L}_t}{\partial z_j} = A_t \left[ p_\theta(j\mid h_t)-\mathbb{1}[j=a_t] \right]

如果 At>0A_t>0

  • ata_t 的 logit 上升;
  • 其他 token 的 logit 下降。

如果 At<0A_t<0

  • ata_t 的 logit 下降;
  • 其他 token 的概率质量相对上升。

因此,RL/OPD 的统一理解是:

advantage 决定了 student 自己生成的 token 是被强化还是被惩罚。

OPD 只是把 teacher logprob 引入 advantage:

At=Atβ(logpSlogpT)A_t' = A_t - \beta (\log p_S - \log p_T)

于是:

  • teacher 比 student 更认可该 token:
logpT>logpS\log p_T > \log p_S

则:

AtA'_t \uparrow

这个 token 更可能被强化。

  • teacher 比 student 更不认可该 token:
logpT<logpS\log p_T < \log p_S

则:

AtA'_t \downarrow

这个 token 更可能被抑制。

这就是从参数反传角度看的 OPD。


11. 长序列里,为什么 OPD 比 SFT 更接近推理?#

SFT 的问题是:

ht=htexperth_t = h_t^{expert}

训练时看到的是 expert prefix。

但部署时:

ht=htstudenth_t = h_t^{student}

student 可能早就犯错了。

例如 terminal agent:

cd wrong_dir
pytest -q
sed -i ...
bash

这时候后续 context 已经变了。SFT 可能从来没有在这种 prefix 上训练过。

OPD 改变的是 prefix 分布:

htπθh_t \sim \pi_\theta

也就是 student 自己生成的 prefix。

所以 OPD 学的是:

在我自己真实会走到的状态下,teacher 如何评价我当前 token。

这就是它相比 SFT 的核心优势。

但长程 agent 里也有问题:如果 student 走到非常坏的 prefix,teacher 对 sampled token 的 logprob 可能只能告诉你“这个 token 不好”,但不能构造一个可恢复路径。这也是为什么我们前面讨论 terminal agent 时更倾向于 Anchor-Replay SFT / recovery suffix

不只是让 teacher 给坏 prefix 上的 token 打 logprob,而是让 teacher 从可 replay 的失败状态继续修复,给出正向 repair trajectory。

这比 sampled-token OPD 更强,因为它提供了替代动作序列。


12. 为什么说 RL、SFT、OPD 底层是同一个 logprob 机械?#

可以把三者统一成:

L=twtlogπθ(atht)\mathcal{L} = -\sum_t w_t \log \pi_\theta(a_t\mid h_t)

区别在于 ht,at,wth_t,a_t,w_t

方法prefix hth_t 来自哪里token/action ata_t 来自哪里权重 wtw_t 是什么
SFTexpert trajectoryexpert token恒为正,通常 1
RL/GRPOstudent rolloutstudent sampled tokenreward/advantage,可正可负
OPDstudent rolloutstudent sampled tokenteacher-student logprob gap + RL advantage
Full KL distillationteacher/student prefixfull vocab distributionteacher probability pT(v)p_T(v)
Anchor-Replay SFTstudent failed stateteacher repair token恒为正,但 context 是失败状态

所以:

是的,它们都在通过反向传播改变 next-token logits;但“加大哪个 token、压低哪个 token、在哪个 prefix 上做”完全不同。

这三个维度决定了训练范式的本质。


13. 和我们 terminal-agent idea 的关系#

现在看,我们的 Anchor-Replay SFT 不是 full OPD,但它抓住了 OPD 最关键的点:

训练 context 应该来自 student 自己会进入的状态,而不是只来自 clean expert trajectories。

区别是:

  • OPD:student prefix 上 teacher 给 token logprob;
  • 我们:student failed executable state 上 teacher 给 repair continuation。

也就是:

OPD signal=logπT(atstudenthtstudent)\text{OPD signal} = \log \pi_T(a_t^{student}\mid h_t^{student})

而我们想要的是:

Recovery signal=at:t+kteacherrepairststudentfailure\text{Recovery signal} = a_{t:t+k}^{teacher-repair} \mid s_t^{student-failure}

这个更适合 terminal,因为 terminal 的错误不是纯文本 prefix 的错误,而是 workspace/environment state 的错误。

所以我们的 publication-ready 说法可以是:

Standard OPD provides dense token-level feedback on student-generated prefixes. For terminal agents, however, student mistakes change the executable environment state. We therefore move from token scoring to state recovery: we replay student-induced failure states and distill verifier-validated teacher repairs as SFT data.

这就把 OPD 和我们的 idea 关系讲清楚了。


14. 最后用一个极简图式总结#

SFT:
expert prefix + expert token
loss = -log p_student(expert token)

RL:
student prefix + student token + reward/advantage
loss = -A * log p_student(student token)

OPD in slime:
student prefix + student token + teacher logprob
reverse_kl = log p_student(token) - log p_teacher(token)
A' = A - beta * reverse_kl
loss = PPO/GRPO policy loss with A'

Our Anchor-Replay SFT:
student failed executable state + teacher repair suffix
loss = -log p_student(teacher repair token | failed-state context)
text

所以你可以这样理解:

SFT 是“模仿正确答案”;RL 是“强化带来好结果的自己动作”;OPD 是“让老师评价自己动作是否像老师会做的动作”;Anchor-Replay SFT 是“在自己犯错后的状态里,学习老师怎么修回来”。

大模型算法总结
https://justinhuang.top/blog/2026-06-12---llm-algorithms/post
Author Justin Huang
Published at 2026年6月12日