Chapter 09

算法:PPO / GRPO / DAPO 及变体

📌 源码引用基于 verl/trainer/ppo/core_algos.py 、 examples/<algo>_trainer/ · commit 9c38b8bb1876 。

9.1 verl 算法谱系

verl 把所有 PPO 家族算法通过 不同的 algorithm.adv_estimator + 不同的 loss 实现。examples/ 下每个 trainer 都对应一种算法:

examples 目录算法关键 override
ppo_trainer/ 经典 PPO adv_estimator=gae
grpo_trainer/ GRPO(DeepSeek-R1 同款)adv_estimator=grpo
dppo_trainer/ Decoupled PPO adv_estimator=gae + decouple
gdpo_trainer/ GDPO
gmpo_trainer/ GMPO
gspo_trainer/ GSPO (group sequence PO)
gpg_trainer/ Group Policy Gradient
reinforce_plus_plus_trainer/ REINFORCE++ adv_estimator=reinforce_plus_plus
rloo_trainer/ RLOO(leave-one-out)adv_estimator=rloo
remax_trainer/ ReMax
cispo_trainer/ CISPO
sapo_trainer/ SAPO
otb_trainer/ OTB
mtp_trainer/ Multi-Token Prediction

看着多,本质都是 PPO 的变体,区别在 advantage 估计、KL 项、ratio 计算这三件事上。

9.2 PPO(最经典)

$$ \mathcal{L}_\text{PPO}(\theta) = -\mathbb{E}_t\!\left[\min\!\big(\rho_t \hat A_t,\;\mathrm{clip}(\rho_t,\,1{-}\epsilon,\,1{+}\epsilon)\hat A_t\big)\right] - c_v \mathcal{L}_v + c_e \mathcal{H}[\pi_\theta] $$

verl 中 PPO 启动:

algorithm.adv_estimator=gae \
algorithm.gamma=1.0 \
algorithm.lam=0.95 \
critic.optim.lr=1e-5

9.3 GRPO(DeepSeek-R1 同款)

GRPO 的招牌操作:用同一 prompt 采样 $G$ 个 response,组内归一化做 advantage。不再需要 critic。

$$ \hat A_i = \frac{r_i - \mathrm{mean}(\{r_j\}_{j=1}^G)}{\mathrm{std}(\{r_j\}_{j=1}^G)} $$

$$ \mathcal{L}_\text{GRPO}(\theta) = -\mathbb{E}\!\left[\tfrac{1}{G}\sum_{i=1}^G \min\!\big(\rho_i \hat A_i,\;\mathrm{clip}(\rho_i, 1{-}\epsilon, 1{+}\epsilon)\hat A_i\big)\right] - \beta\,\mathrm{KL}(\pi_\theta \| \pi_\text{ref}) $$

关键配置:

algorithm.adv_estimator=grpo
algorithm.norm_adv_by_std_in_grpo=true     # 启用 std 归一
algorithm.use_kl_in_reward=false            # KL 走 loss,不走 reward
actor_rollout_ref.actor.use_kl_loss=true
actor_rollout_ref.actor.kl_loss_coef=0.001
actor_rollout_ref.rollout.n=5               # 每 prompt 采样 5 个

9.4 DAPO(高效 R1 风格)

DAPO 是 ByteDance 提出的 GRPO 升级版,主要改动:

  1. 动态采样:动态判断每个 prompt 需要多少 response;
  2. clip-higher:clip 上下界非对称,缓解 entropy collapse;
  3. token-level loss:对长 sequence 的每 token 单独算 loss,避免长 response 主导梯度。

verl 通过 GRPO + 特殊参数实现 DAPO 风格:

algorithm.adv_estimator=grpo
algorithm.use_dapo_clip_higher=true
algorithm.clip_ratio_low=0.2
algorithm.clip_ratio_high=0.28
algorithm.use_token_level_loss=true

具体 recipe 在子仓 verl-recipedapo/ 目录。

9.5 REINFORCE++

简化版无 critic 算法:

$$ \hat A_t = r - \tau \cdot \mathrm{normalize}(r) $$

等同于"reward 归一化 + REINFORCE 梯度"。优点:实现极简、显存最省。缺点:方差大,需要更多样本。

algorithm.adv_estimator=reinforce_plus_plus

9.6 RLOO(Leave-One-Out)

RLOO 给同一 prompt 采 $G$ 个 response,每个 response 的 advantage = 自己的 reward 减其他 $G-1$ 个的平均

$$ \hat A_i = r_i - \frac{1}{G-1}\sum_{j\neq i} r_j $$

$$\Leftrightarrow\; \hat A_i = \frac{G}{G-1}\big(r_i - \bar r\big)$$

与 GRPO 区别:不做 std 归一。更稳定但 sample efficiency 略低。

algorithm.adv_estimator=rloo
actor_rollout_ref.rollout.n=4

9.7 KL 项的三种放法

配置含义
algorithm.use_kl_in_reward=true把 $-\beta\,\mathrm{KL}$ 加到 reward 上(OpenAI 经典)
actor.use_kl_loss=true 把 KL 作为 loss 的额外项
两个都 false 不做 KL 约束(仅 clip 限制策略漂移)

GRPO / DAPO 默认走第二种(KL 进 loss),更稳。

9.8 KL 估计的几种方式

实际计算 $\mathrm{KL}(\pi_\theta \| \pi_\text{ref})$ 在 token level,verl 提供:

algorithm.kl_penalty公式特点
kl $\log\pi_\theta - \log\pi_\text{ref}$ 无偏但方差大
abs $|\log\pi_\theta - \log\pi_\text{ref}|$ 稳定但有偏
mse $\tfrac{1}{2}(\log\pi_\theta - \log\pi_\text{ref})^2$ 常用
low_var_kl $r - \log r - 1,\; r = \pi_\text{ref}/\pi_\theta$ 无偏 + 低方差(推荐)
full 所有词表上的 KL(不需要采样) 精确但贵

推荐 low_var_kl,这是 John Schulman 博客提出的无偏低方差估计

9.9 选哪个算法

业务诉求推荐
"我有可信打分(编译通过 / 单测 / GSM8K 答案)"GRPO
"我训长思考链 R1 风格" GRPO + DAPO 改进
"我已有 reward model + 偏好数据" 经典 PPO
"显存极紧 / 不想训 critic" REINFORCE++ 或 RLOO
"想要最 SOTA 的算法" 看 verl-recipe 的最新 recipe
"想做学术对比" 跑 GRPO / RLOO / REINFORCE++ 各一遍

9.10 多种算法在同一 verl 入口下的切换

四种算法的切换:

# PPO
algorithm.adv_estimator=gae

# GRPO
algorithm.adv_estimator=grpo
actor_rollout_ref.rollout.n=8

# RLOO
algorithm.adv_estimator=rloo
actor_rollout_ref.rollout.n=4

# REINFORCE++
algorithm.adv_estimator=reinforce_plus_plus

verl 在 core_algos.py 里把 advantage 估计抽成一个 dispatch 函数,新增算法只要加一个 case

9.11 完整的 AdvantageEstimator 枚举

verl/trainer/ppo/core_algos.pyAdvantageEstimator(line 88–111)现在已经 13 项。逐个简介,配公式:

estimator函数(core_algos.py 行号)核心公式 / 特征是否需要 critic
gae compute_gae_advantage_return (216) $A_t = \sum_l (\gamma\lambda)^l \delta_{t+l}$,标准 GAE
grpo compute_grpo_outcome_advantage (268) $A_i = (r_i - \mu_\text{group}) / (\sigma_\text{group}+\varepsilon)$,组内归一
grpo_vectorized 335 GPU 友好版 grpo(无 Python 循环)
gdpo compute_gdpo_outcome_advantage (362) 每维 reward 各自归一后加权聚合,再 batch-level whitening
grpo_passk 472 只有 group 内最好的一条拿非零 advantage:$r_\max - r_\text{2nd}$
rloo compute_rloo_outcome_advantage (588) $A_i = \tfrac{n}{n-1}(r_i - \mu_{-i})$,leave-one-out 无偏估计
rloo_vectorized 同 rloo,向量化版
opo compute_opo_outcome_advantage (640) length-normalized advantage(arXiv:2505.23585)
reinforce_plus_plus 694 $G_t = \sum_{t'\geq t}\gamma^{t'-t} r_{t'}$ + batch-level whitening
reinforce_plus_plus_baseline R++ 但减去 group baseline
remax compute_remax_outcome_advantage (733) cumulative reward - 贪心 baseline 的 cumulative reward否(但要并行 greedy rollout)
gpg compute_gpg_outcome_advantage (769) group-relative + f-norm 加权 + α 缩放
optimal_token_baseline 870 token-level 最优 baseline(长 horizon)
tir_optimal_token_baseline 989 同上但 multi-turn / tool-in-reasoning 版本

注册机制:ADV_ESTIMATOR_REGISTRY(line 113)+ @register_adv_est(name_or_enum) 装饰器(line 116)。新增自定义算法只要写一个函数 + 一行装饰器,不用动主循环

9.12 完整的 POLICY_LOSS_REGISTRY

同文件的 POLICY_LOSS_REGISTRY(line 50)注册了 11 种 policy loss。这是 ch08.12 那张 YAML 表的算法侧对应:

名字函数 (行号)核心思路
vanilla 1278 标准 PPO clip:$\min(rA, \text{clip}(r)A)$,带 dual-clip 下界
dppo_tv 1372 DPPO + Total Variation 正则
dppo_kl 1453 DPPO + KL 正则
gspo 1539 序列级 IS × token 级 ratio;seq-mean-token-mean 聚合(2025-11)
sapo 1614 $\text{sigmoid}(\tau(r-1))\cdot\tfrac{4}{\tau}$ soft-clip(2025-11,arXiv:2511.20347)
gpg 1699 group PG + 选择性 clip
clip_cov 1735 PPO + 协方差正则
kl_cov 1840 KL + 协方差跟踪
geo_mean 1920 $\exp(\text{mean}(\log r))$ 几何均值,数值更稳
cispo 2007 Clipped IS PPO,rollout IS 直接进 ratio
bypass_mode 2351 off-policy 矫正路径:REINFORCE-style 或 PPO-clip with IS 重加权

注册装饰器:@register_policy_loss(name)(line 53);检索:get_policy_loss_fn(name)(line 70)。ch10 会讲怎么自己写一个。

9.13 KL 函数细节

9.4 列了几种 KL 估计名字,对应函数都在 core_algos.py:2126kl_penalty() 里 dispatch:

名字公式用途
"kl" $\log p - \log q$ 定义式,有正负
"abs" $|\log p - \log q|$ 绝对值,单调正
"mse" $(\log p - \log q)^2$ 平方
"low_var_kl" $\exp(\log q - \log p) - (\log q - \log p) - 1$ 低方差近似(Schulman),实战默认
"full" full distribution KL(需要完整 logits) 计算贵但精确

9.14 GDPO:多维 reward 的归一

2026-Q1 引入的 GDPO 解决的是多 reward 来源信号互相淹没的问题。比如同时要 reward = 0.5·正确性 + 0.3·简洁性 + 0.2·安全性,三个维度的量纲完全不同。GRPO 直接聚合后归一会让小维度信号被大维度淹掉。

GDPO 的做法(compute_gdpo_outcome_advantage(), line 362):

  1. 对每一维 reward单独做 group 归一化;
  2. gdpo_reward_weights 加权聚合归一化后的 advantage;
  3. 最后做一次 batch-level whitening 控制整体方差。

YAML 写法:

algorithm:
  adv_estimator: gdpo
  gdpo_reward_keys: [accuracy, brevity, safety]    # 三维 reward 字段名
  gdpo_reward_weights: [0.5, 0.3, 0.2]

对应 reward 函数要返回 non_tensor_batch 里有这三个字段而不是只返回 scalar。

9.15 DAPO 的两个具体改进

9.7 简单提了 DAPO 的两个 idea,这里给真实落地字段:

字段(algorithm 段)含义
filter_groups.enabled 开 group 过滤
filter_groups.metric 过滤准则:"std"(reward 方差太小=无信号)/ "all_correct"(全对/全错 group 不学)
filter_groups.max_num_gen_batches每个 prompt 最多重采样多少次直到能产生有信号 group

"Clip-Higher" 在 vanilla policy_loss 里通过 clip_ratio_high / clip_ratio_low 两个字段控制(非对称 PPO clip)。GRPO 的 Dr.GRPO 变体只要把 algorithm.norm_adv_by_std_in_grpo: false 即可。

9.16 rollout correction 的三个预设

训练和 rollout 一旦不在同一 forward pass(off-policy),就要校正。看 verl/trainer/config/algorithm.pyRolloutCorrectionConfig

预设构成适用
decoupled_token_is() 3 策略(actor / old / rollout)+ token-level IS常规 off-policy
decoupled_seq_is() 3 策略 + sequence-level IS 长序列,token IS 方差太大
bypass_ppo_clip_geo_rs() 2 策略 + PPO clip + 几何拒绝采样 rollout / training 偏离巨大时

没开 correction 时(默认)算法本质是 on-policy(同步 rollout-training);开了 correction 才能放心做 fully_async / one_step_off_policy。

9.17 这章你需要带走什么