719 lines
12 KiB
Markdown
719 lines
12 KiB
Markdown
# CISPO 这段代码怎么理解
|
||
|
||
本文只解释第 24 课里 MiniMind 的 CISPO 分支:
|
||
|
||
```text
|
||
trainer/train_grpo.py:135-138
|
||
```
|
||
|
||
源码:
|
||
|
||
```python
|
||
if args.loss_type == "cispo":
|
||
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
||
per_token_loss = -(
|
||
clamped_ratio
|
||
* advantages.unsqueeze(1)
|
||
* per_token_logps
|
||
- args.beta * per_token_kl
|
||
)
|
||
```
|
||
|
||
## 0. 一句话
|
||
|
||
CISPO 的想法是:
|
||
|
||
```text
|
||
不要让 clipped ratio 变成一个切断梯度的目标;
|
||
把 clipped ratio 当成固定权重;
|
||
让真正的策略梯度从 log pi_theta(a|s) 走。
|
||
```
|
||
|
||
也就是:
|
||
|
||
```text
|
||
ratio 负责告诉模型“这次更新权重多大”
|
||
logprob 负责提供“往哪个方向更新”的梯度路径
|
||
```
|
||
|
||
## 1. 先看 GRPO 的问题
|
||
|
||
MiniMind 的 GRPO 分支是:
|
||
|
||
```python
|
||
clipped_ratio = torch.clamp(ratio, 1 - args.epsilon, 1 + args.epsilon)
|
||
per_token_loss1 = ratio * advantages.unsqueeze(1)
|
||
per_token_loss2 = clipped_ratio * advantages.unsqueeze(1)
|
||
per_token_loss = -(torch.min(per_token_loss1, per_token_loss2) - args.beta * per_token_kl)
|
||
```
|
||
|
||
对应公式:
|
||
|
||
$$
|
||
\mathcal{L}_{\mathrm{GRPO}}
|
||
=
|
||
-
|
||
\mathbb{E}
|
||
\left[
|
||
\min
|
||
\left(
|
||
r_t A,
|
||
\operatorname{clip}(r_t, 1-\epsilon, 1+\epsilon) A
|
||
\right)
|
||
-
|
||
\beta \operatorname{KL}_t
|
||
\right]
|
||
$$
|
||
|
||
其中:
|
||
|
||
$$
|
||
r_t
|
||
=
|
||
\frac{
|
||
\pi_{\theta}(a_t \mid s_t)
|
||
}{
|
||
\pi_{\mathrm{old}}(a_t \mid s_t)
|
||
}
|
||
$$
|
||
|
||
当 ratio 超过 clip 范围时,`clipped_ratio` 变成常数边界:
|
||
|
||
```text
|
||
1 + epsilon
|
||
```
|
||
|
||
或者:
|
||
|
||
```text
|
||
1 - epsilon
|
||
```
|
||
|
||
这能限制更新幅度,但也带来一个副作用:
|
||
|
||
```text
|
||
被 clip 的那一侧,梯度可能直接被截断。
|
||
```
|
||
|
||
README 里也说了这个问题:
|
||
|
||
```text
|
||
ratio 被 clip 之后,梯度流直接就被硬截断了。
|
||
```
|
||
|
||
源码证据:
|
||
|
||
文件:`README.md:1156-1170`
|
||
|
||
看它是为了理解:CISPO 为什么不是重新设计一套 RL 算法,而是改 GRPO 的 loss 形式。
|
||
|
||
代码/文本摘录:
|
||
|
||
```text
|
||
CISPO 的关注点并不是重新设计 group baseline,
|
||
而是用非常小的 loss 改动,更直接地修正这个问题。
|
||
```
|
||
|
||
这段说明:
|
||
|
||
- GRPO 的 group reward / advantage 仍然保留。
|
||
- CISPO 主要改策略项。
|
||
- 目标是避免 ratio clip 后梯度路径被截断。
|
||
|
||
### 1.1 clip 为什么会让梯度变成 0
|
||
|
||
先只看 `torch.clamp`:
|
||
|
||
$$
|
||
y
|
||
=
|
||
\operatorname{clamp}(x, l, u)
|
||
$$
|
||
|
||
它的导数大致是:
|
||
|
||
$$
|
||
\frac{\partial y}{\partial x}
|
||
=
|
||
\begin{cases}
|
||
1, & l < x < u \\
|
||
0, & x < l \text{ or } x > u
|
||
\end{cases}
|
||
$$
|
||
|
||
原因很简单:
|
||
|
||
```text
|
||
x 在区间内:clamp(x) = x,输出会跟着 x 变。
|
||
x 超过边界:clamp(x) = 边界常数,输出不再跟着 x 变。
|
||
```
|
||
|
||
所以当 ratio 超过 clip 边界时:
|
||
|
||
```python
|
||
clipped_ratio = torch.clamp(ratio, 1 - epsilon, 1 + epsilon)
|
||
```
|
||
|
||
如果:
|
||
|
||
```text
|
||
ratio = 1.5
|
||
1 + epsilon = 1.2
|
||
```
|
||
|
||
那么:
|
||
|
||
```text
|
||
clipped_ratio = 1.2
|
||
```
|
||
|
||
这时把 `ratio` 从 `1.5` 改成 `1.6`,`clipped_ratio` 仍然是 `1.2`。也就是说:
|
||
|
||
```text
|
||
clipped_ratio 对 ratio 不敏感了;
|
||
梯度就不能从 clipped_ratio 继续传回 ratio。
|
||
```
|
||
|
||
再放回 GRPO/PPO 的 clipped surrogate:
|
||
|
||
$$
|
||
\min
|
||
\left(
|
||
r_t A_t,
|
||
\operatorname{clip}(r_t, 1-\epsilon, 1+\epsilon)A_t
|
||
\right)
|
||
$$
|
||
|
||
这里“梯度被截断”不是所有情况都会发生,而是发生在 `min` 选中了 clipped 分支、并且 `clamp` 已经落到边界时。
|
||
|
||
两个典型情况:
|
||
|
||
| advantage | ratio 状态 | 被限制的方向 | 结果 |
|
||
|---|---|---|---|
|
||
| $A_t > 0$ | $r_t > 1+\epsilon$ | 好 token 概率已经涨太多 | clipped 分支是常数边界,继续增大 ratio 没有梯度 |
|
||
| $A_t < 0$ | $r_t < 1-\epsilon$ | 坏 token 概率已经降太多 | clipped 分支是常数边界,继续减小 ratio 没有梯度 |
|
||
|
||
所以这句话的准确含义是:
|
||
|
||
```text
|
||
clip 限制了 policy 相对 old policy 变化太大;
|
||
但当 clipped 分支被选中时,策略项对 ratio 的梯度可能变成 0。
|
||
```
|
||
|
||
这就是 CISPO 想缓解的问题:不要把被 clip 的 ratio 本身当作主要梯度路径。
|
||
|
||
## 2. CISPO 的公式
|
||
|
||
README 写的 CISPO 损失是:
|
||
|
||
$$
|
||
\mathcal{L}_{\mathrm{CISPO}}
|
||
=
|
||
-
|
||
\mathbb{E}
|
||
\left[
|
||
\min(r_t, \epsilon_{\max})
|
||
\cdot
|
||
A_t
|
||
\cdot
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
-
|
||
\beta \operatorname{KL}_t
|
||
\right]
|
||
$$
|
||
|
||
MiniMind 源码里的变量对应:
|
||
|
||
```text
|
||
min(r_t, epsilon_max) -> clamped_ratio
|
||
A_t -> advantages.unsqueeze(1)
|
||
log pi_theta(a_t | s_t) -> per_token_logps
|
||
beta * KL_t -> args.beta * per_token_kl
|
||
```
|
||
|
||
所以源码:
|
||
|
||
```python
|
||
per_token_loss = -(
|
||
clamped_ratio
|
||
* advantages.unsqueeze(1)
|
||
* per_token_logps
|
||
- args.beta * per_token_kl
|
||
)
|
||
```
|
||
|
||
就是:
|
||
|
||
$$
|
||
\operatorname{loss}_{t}
|
||
=
|
||
-
|
||
\left[
|
||
\bar{r}_t
|
||
A
|
||
\ell^{\theta}_{t}
|
||
-
|
||
\beta \operatorname{KL}_t
|
||
\right]
|
||
$$
|
||
|
||
其中:
|
||
|
||
$$
|
||
\bar{r}_t
|
||
=
|
||
\min(r_t, \epsilon_{\max})
|
||
$$
|
||
|
||
$$
|
||
\ell^{\theta}_{t}
|
||
=
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
## 3. 为什么要乘 logprob
|
||
|
||
policy gradient 的基础形式是:
|
||
|
||
$$
|
||
\nabla_{\theta} J(\theta)
|
||
\approx
|
||
A_t
|
||
\nabla_{\theta}
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
所以如果要让模型:
|
||
|
||
```text
|
||
A_t > 0 时提高 token 概率
|
||
A_t < 0 时降低 token 概率
|
||
```
|
||
|
||
最直接的可微路径就是:
|
||
|
||
```text
|
||
log pi_theta(a_t | s_t)
|
||
```
|
||
|
||
CISPO 让损失里显式出现:
|
||
|
||
```python
|
||
advantages * per_token_logps
|
||
```
|
||
|
||
这样梯度就从 `per_token_logps` 走。
|
||
|
||
直观上:
|
||
|
||
```text
|
||
如果 advantage > 0:
|
||
loss = -positive_weight * logprob
|
||
最小化 loss 会提高 logprob。
|
||
|
||
如果 advantage < 0:
|
||
loss = -negative_weight * logprob
|
||
最小化 loss 会降低 logprob。
|
||
```
|
||
|
||
## 4. ratio 在 CISPO 里做什么
|
||
|
||
ratio 仍然有用。
|
||
|
||
源码:
|
||
|
||
```python
|
||
ratio = torch.exp(per_token_logps - old_per_token_logps)
|
||
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
||
```
|
||
|
||
它对应:
|
||
|
||
$$
|
||
r_t
|
||
=
|
||
\exp
|
||
\left(
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
-
|
||
\log \pi_{\mathrm{old}}(a_t \mid s_t)
|
||
\right)
|
||
$$
|
||
|
||
也就是:
|
||
|
||
$$
|
||
r_t
|
||
=
|
||
\frac{
|
||
\pi_{\theta}(a_t \mid s_t)
|
||
}{
|
||
\pi_{\mathrm{old}}(a_t \mid s_t)
|
||
}
|
||
$$
|
||
|
||
CISPO 不把 ratio 当成主要梯度路径,而是把它当作权重:
|
||
|
||
$$
|
||
\bar{r}_t
|
||
=
|
||
\min(r_t, \epsilon_{\max})
|
||
$$
|
||
|
||
如果某个 token 的当前概率相对 old policy 变大,ratio 大,那么这个 token 的策略项权重也变大。
|
||
|
||
但上限是:
|
||
|
||
```text
|
||
epsilon_high
|
||
```
|
||
|
||
避免权重爆炸。
|
||
|
||
## 5. 为什么要 detach
|
||
|
||
这行最关键:
|
||
|
||
```python
|
||
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
||
```
|
||
|
||
`detach()` 的意思是:
|
||
|
||
```text
|
||
clamped_ratio 参与前向计算;
|
||
但不从 clamped_ratio 反传梯度。
|
||
```
|
||
|
||
也就是说,CISPO 的策略项:
|
||
|
||
```python
|
||
clamped_ratio * advantages * per_token_logps
|
||
```
|
||
|
||
反向传播时:
|
||
|
||
```text
|
||
clamped_ratio 被当成常数权重
|
||
advantages 被当成常数权重
|
||
per_token_logps 提供梯度
|
||
```
|
||
|
||
数学上近似看:
|
||
|
||
$$
|
||
\nabla_{\theta}
|
||
\left(
|
||
\bar{r}_t^{\mathrm{detach}}
|
||
A_t
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
\right)
|
||
=
|
||
\bar{r}_t^{\mathrm{detach}}
|
||
A_t
|
||
\nabla_{\theta}
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
这就是 CISPO 的核心:
|
||
|
||
```text
|
||
用 ratio 控制权重;
|
||
用 logprob 保留梯度;
|
||
不要让梯度穿过 ratio 本身。
|
||
```
|
||
|
||
如果不 detach,梯度还会穿过:
|
||
|
||
```text
|
||
ratio = exp(new_logp - old_logp)
|
||
```
|
||
|
||
那策略项会同时从 ratio 和 logprob 两条路径反传,更新会更复杂、更容易偏离 CISPO 想表达的“裁剪权重 × logprob”。
|
||
|
||
## 6. 和 GRPO 的差别
|
||
|
||
GRPO 策略项:
|
||
|
||
$$
|
||
\min(r_t A_t, \bar{r}_t A_t)
|
||
$$
|
||
|
||
它的核心是:
|
||
|
||
```text
|
||
ratio 自己是优化目标的一部分。
|
||
```
|
||
|
||
CISPO 策略项:
|
||
|
||
$$
|
||
\bar{r}_t^{\mathrm{detach}}
|
||
A_t
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
它的核心是:
|
||
|
||
```text
|
||
ratio 只是权重;
|
||
logprob 才是梯度路径。
|
||
```
|
||
|
||
对比:
|
||
|
||
| 项目 | GRPO | CISPO |
|
||
|---|---|---|
|
||
| advantage 来源 | group reward | group reward |
|
||
| 是否需要 critic | 不需要 | 不需要 |
|
||
| 是否使用 old logprob | 使用 | 使用 |
|
||
| ratio 作用 | 进入 clipped surrogate | 作为 detached weight |
|
||
| 梯度主路径 | ratio / clipped surrogate | logprob |
|
||
| KL 约束 | token KL | token KL |
|
||
|
||
### 6.1 PPO/GRPO 更新 policy 到底靠谁
|
||
|
||
PPO/GRPO 更新 policy 不是“只靠 ratio”,也不是“只靠 advantage”。更准确的说法是:
|
||
|
||
```text
|
||
ratio/logprob 提供梯度路径;
|
||
advantage 提供方向和权重。
|
||
```
|
||
|
||
先看没有 clip 的策略目标:
|
||
|
||
$$
|
||
J_t
|
||
=
|
||
r_t A_t
|
||
$$
|
||
|
||
其中:
|
||
|
||
$$
|
||
r_t
|
||
=
|
||
\exp
|
||
\left(
|
||
\ell_t^\theta
|
||
-
|
||
\ell_t^{\mathrm{old}}
|
||
\right)
|
||
$$
|
||
|
||
$$
|
||
\ell_t^\theta
|
||
=
|
||
\log \pi_\theta(a_t \mid s_t)
|
||
$$
|
||
|
||
`old_per_token_logps` 是 rollout 时保存下来的旧 policy logprob,在训练当前 step 时当常数用。所以:
|
||
|
||
$$
|
||
\nabla_\theta J_t
|
||
=
|
||
A_t r_t
|
||
\nabla_\theta \ell_t^\theta
|
||
$$
|
||
|
||
也就是:
|
||
|
||
$$
|
||
\nabla_\theta J_t
|
||
=
|
||
A_t r_t
|
||
\nabla_\theta
|
||
\log \pi_\theta(a_t \mid s_t)
|
||
$$
|
||
|
||
这条公式说明三件事:
|
||
|
||
```text
|
||
1. 真正连到 policy net 参数的是 current logprob。
|
||
2. ratio 里面包含 current logprob,所以 ratio 是一条梯度路径。
|
||
3. advantage 自己通常不反传梯度,但它乘在梯度前面,决定方向和强度。
|
||
```
|
||
|
||
所以 advantage 不是“只和 reward model / critic 有关,然后和 policy 更新没关系”。它的来源确实可能来自 reward model、critic 或 group reward,但最后会直接进入 policy loss。
|
||
|
||
PPO 里:
|
||
|
||
```text
|
||
reward / reward_model -> reward
|
||
critic_model -> value
|
||
reward + value -> GAE advantage
|
||
policy_loss -> ratio * advantage 的 clipped 形式
|
||
```
|
||
|
||
GRPO 里:
|
||
|
||
```text
|
||
reward_model / rule -> reward
|
||
同一个 prompt 的多条 rewards -> group mean/std
|
||
group 标准化 -> advantage
|
||
policy_loss -> ratio * advantage 的 clipped 形式
|
||
```
|
||
|
||
两者的共同点:
|
||
|
||
```text
|
||
advantage 不一定接收梯度;
|
||
但 advantage 一定控制 policy gradient。
|
||
```
|
||
|
||
如果:
|
||
|
||
$$
|
||
A_t > 0
|
||
$$
|
||
|
||
优化会倾向于提高这个 token 的概率。
|
||
|
||
如果:
|
||
|
||
$$
|
||
A_t < 0
|
||
$$
|
||
|
||
优化会倾向于降低这个 token 的概率。
|
||
|
||
CISPO 的变化是:
|
||
|
||
```text
|
||
GRPO/PPO:
|
||
ratio 既是权重,也是梯度路径的一部分。
|
||
|
||
CISPO:
|
||
clamped_ratio.detach() 只当固定权重;
|
||
per_token_logps 明确承担策略梯度路径。
|
||
```
|
||
|
||
## 7. 用正负 advantage 看方向
|
||
|
||
假设忽略 KL,只看 CISPO 策略项:
|
||
|
||
$$
|
||
\mathcal{L}
|
||
=
|
||
-
|
||
\bar{r}_t
|
||
A_t
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
### 7.1 advantage 大于 0
|
||
|
||
如果:
|
||
|
||
$$
|
||
A_t > 0
|
||
$$
|
||
|
||
那么:
|
||
|
||
$$
|
||
\mathcal{L}
|
||
=
|
||
-
|
||
\text{positive}
|
||
\cdot
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
最小化 loss 会让:
|
||
|
||
```text
|
||
log pi_theta(a_t | s_t) 变大
|
||
```
|
||
|
||
也就是提高这个 token 的概率。
|
||
|
||
### 7.2 advantage 小于 0
|
||
|
||
如果:
|
||
|
||
$$
|
||
A_t < 0
|
||
$$
|
||
|
||
那么:
|
||
|
||
$$
|
||
\mathcal{L}
|
||
=
|
||
+
|
||
\text{positive}
|
||
\cdot
|
||
\log \pi_{\theta}(a_t \mid s_t)
|
||
$$
|
||
|
||
最小化 loss 会让:
|
||
|
||
```text
|
||
log pi_theta(a_t | s_t) 变小
|
||
```
|
||
|
||
也就是降低这个 token 的概率。
|
||
|
||
这和 policy gradient 的直觉一致。
|
||
|
||
## 8. 源码证据:MiniMind 的 CISPO 分支
|
||
|
||
文件:`trainer/train_grpo.py:132-143`
|
||
|
||
看它是为了理解:GRPO 和 CISPO 共用 group advantage、ratio、reference KL,只切换 policy loss 形式。
|
||
|
||
代码摘录:
|
||
|
||
```python
|
||
kl_div = ref_per_token_logps - per_token_logps
|
||
per_token_kl = torch.exp(kl_div) - kl_div - 1
|
||
ratio = torch.exp(per_token_logps - old_per_token_logps)
|
||
if args.loss_type == "cispo":
|
||
clamped_ratio = torch.clamp(ratio, max=args.epsilon_high).detach()
|
||
per_token_loss = -(clamped_ratio * advantages.unsqueeze(1) * per_token_logps - args.beta * per_token_kl)
|
||
else:
|
||
clipped_ratio = torch.clamp(ratio, 1 - args.epsilon, 1 + args.epsilon)
|
||
per_token_loss1 = ratio * advantages.unsqueeze(1)
|
||
per_token_loss2 = clipped_ratio * advantages.unsqueeze(1)
|
||
per_token_loss = -(torch.min(per_token_loss1, per_token_loss2) - args.beta * per_token_kl)
|
||
policy_loss = ((per_token_loss * completion_mask).sum(dim=1) / completion_mask.sum(dim=1).clamp(min=1)).mean()
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `per_token_kl` 两种 loss 共用。
|
||
- `ratio` 两种 loss 都要计算。
|
||
- GRPO 使用 `torch.min(ratio*A, clipped_ratio*A)`。
|
||
- CISPO 使用 `clamped_ratio.detach() * A * per_token_logps`。
|
||
- `completion_mask` 只让有效 response token 参与 loss。
|
||
|
||
## 9. 记忆方式
|
||
|
||
```text
|
||
GRPO:
|
||
clipped ratio objective
|
||
ratio 是优化目标
|
||
|
||
CISPO:
|
||
clipped importance weight * logprob
|
||
ratio 是权重
|
||
logprob 是梯度路径
|
||
```
|
||
|
||
一句话记:
|
||
|
||
```text
|
||
CISPO = GRPO 的 group advantage + reference KL + detached clipped ratio weighted logprob。
|
||
```
|
||
|
||
## 10. 检查问题
|
||
|
||
1. CISPO 为什么还需要 old logprob?
|
||
2. `clamped_ratio.detach()` 的作用是什么?
|
||
3. CISPO 的策略梯度主要从哪个变量走?
|
||
4. `advantages.unsqueeze(1)` 为什么可以乘到每个 token 上?
|
||
5. CISPO 和 GRPO 哪些部分是共用的?
|
||
6. 为什么 `torch.clamp` 到边界以后,对 ratio 的梯度可能是 0?
|
||
7. PPO/GRPO 里 advantage 不反传梯度,为什么仍然会影响 policy 更新?
|