Skip to content

DAPO-style 训练:GRPO 主线上的 recipe 组合

DAPO 容易被初学者误解成“一个全新的 trainer”。在当前 verl 主仓库里,更准确的读法是:DAPO-style 训练主要沿用 GRPO/RLVR 主线,然后组合一组 recipe 来解决长 CoT 数学训练里的稳定性问题。

它的名字来自 Decoupled Clip and Dynamic Sampling Policy Optimization。放到源码里看,关键不是某个 compute_dapo_loss(),而是这些组件一起工作:

text
GRPO advantage + clipped actor loss + 不对称 clip + token-level loss aggregation
+ dynamic sampling / group filtering + overlong reward shaping

DAPO 想解决什么

RLVR 训练常见的坏味道有四个:

  • 同一题采样全对或全错,GRPO 组内优势没有信息。
  • response 越写越长,模型靠拖长推理碰运气。
  • PPO clip 太保守或方向不合适,正向学习被限制。
  • 长短 response 的 token 数不同,loss 聚合方式带来长度偏置。

DAPO-style recipe 的作用,就是把这些问题拆开处理。

组件一:Clip-Higher / 不对称 clip

PPO 的 ratio 是:

rt(θ)=exp(logπθlogπold)

DAPO 文档里的 separated clip epsilons 对应:

clip(rt,1ϵlow,1+ϵhigh)

verl 源码落点仍是 core_algos.compute_policy_loss_vanilla()

text
clip_ratio_low = config.clip_ratio_low or config.clip_ratio
clip_ratio_high = config.clip_ratio_high or config.clip_ratio
pg_losses2 = -advantages * clamp(ratio, 1 - clip_ratio_low, 1 + clip_ratio_high)

常见 DAPO-style 配置:

text
actor_rollout_ref.actor.clip_ratio_low=0.2
actor_rollout_ref.actor.clip_ratio_high=0.28

直觉是:对提升好样本概率的一侧给更宽空间,对另一侧保持更强约束。源码没有 DAPO 专属 loss;它复用普通 PPO clip 的不对称参数。

组件二:Dynamic Sampling / Group Filtering

GRPO 需要同 prompt 的多条 response 有差异。如果同一组全对或全错:

Riμg0

这一组就几乎没有学习信号。DAPO 的 dynamic sampling 会反复生成,过滤掉“组内指标全一样”的 prompt,直到凑够训练 batch 或达到 max_num_gen_batches

官方文档中的配置是:

text
data.gen_batch_size=1536
data.train_batch_size=512
algorithm.filter_groups.enable=True
algorithm.filter_groups.metric=acc
algorithm.filter_groups.max_num_gen_batches=10

本地源码里 AlgoConfigfilter_groups 字段,并标注用于 DAPO/Entropy。主仓库同步 RayPPOTrainer.fit() 当前可读到的主线包含 GRPO advantage、KL、loss、rollout correction;dynamic sampling 的完整复现实验更多出现在 recipe/fully-async 配置与脚本里,例如 verl/experimental/fully_async_policy/shell/dapo_30b_a3b_base_math_fsdp.sh

组件三:Token-level loss aggregation

DAPO 文档强调 token-level loss。verl 的具体实现是 core_algos.agg_loss()

text
token-mean:
    sum(loss over all valid tokens) / number_of_valid_tokens
seq-mean-token-sum:
    mean over sequences of sum(token losses)
seq-mean-token-mean:
    mean over sequences of mean(token losses)
seq-mean-token-sum-norm:
    mean sequence token-sum, then divide by fixed horizon/loss_scale_factor

这不是纯工程细节。LLM response 长短差很多时,聚合方式会改变“长回答”和“短回答”在梯度中的权重。DAPO/Dr.GRPO-style 经常要和 loss_agg_mode 一起读,不能只看 adv_estimator=grpo

组件四:Overlong reward shaping

主仓库里 DAPO reward manager 的同步实现是 verl/workers/reward_manager/dapo.py,reward loop 版本在 verl/experimental/reward_loop/reward_manager/dapo.py。它的核心逻辑是:

text
expected_len = max_resp_len - overlong_buffer.len
exceed_len = valid_response_length - expected_len
overlong_reward = min(-exceed_len / overlong_buffer.len * penalty_factor, 0)
reward = score + overlong_reward
reward_tensor[i, valid_response_length - 1] = reward

变量含义:

源码变量含义
valid_response_length这条 response 的有效 token 数
max_resp_len允许的最大 response 长度
overlong_buffer.len从多长开始线性惩罚的缓冲区
penalty_factor走完整个 buffer 时最多扣多少分
reward_tensor[..., valid_response_length - 1]outcome reward 写在最后一个有效 token 上

直觉:如果 max_response_length=20480、buffer 为 4096,那么超过 16384 后开始线性扣分,越接近硬上限扣得越多。

源码实现怎么读

读 DAPO-style 不要找单点入口,按下面顺序更稳:

  1. 先读 core_algos.compute_grpo_outcome_advantage():DAPO-style 大多仍是 GRPO 分组优势。
  2. 再读 core_algos.compute_policy_loss_vanilla():不对称 clip 在这里生效。
  3. 再读 core_algos.agg_loss():理解 token-level / sequence-level 聚合。
  4. 再读 verl/workers/reward_manager/dapo.py:overlong penalty 如何变成最后一个 token 的 reward。
  5. 最后读 ray_trainer.py 的 reward、KL、advantage 顺序:先 reward shaping,再 use_kl_in_reward,再 advantage。

RayPPOTrainer.fit() 中,DAPO-style 主线仍然是:

text
reward_tensor -> token_level_scores
if use_kl_in_reward:
    token_level_rewards = token_level_scores - beta * KL
else:
    token_level_rewards = token_level_scores
compute_advantage(... adv_estimator=grpo ...)
actor.update_actor(...)

所以 overlong penalty 会先进入 token_level_scores,再参与后续 KL reward penalty 和 GRPO advantage。

本仓库与 recipe 的边界

当前主仓库能直接读到并复用的通用能力:

  • GRPO/Dr.GRPO advantage。
  • 不对称 PPO clip。
  • loss_agg_mode 的多种聚合。
  • DAPO reward manager 的 overlong penalty。
  • AlgoConfig.filter_groups 配置结构。
  • fully-async / experimental shell 里的 DAPO-style 组合示例。

需要注意的边界:

  • DAPO 复现脚本和部分 dynamic sampling 逻辑在 verl-recipe 或 experimental/fully_async_policy 路径里更完整。
  • MTP、fully-async、one-step-off-policy 是系统/执行路径能力,不等于 DAPO 算法本体。
  • reward.reward_manager.name=dapo 只是 reward shaping 组件,不代表整个训练就自动拥有 DAPO paper 的全部 recipe。

哪些地方不适合初学者硬啃

  • 不要把 DAPO 当成和 PPO/GRPO 平级的单个 loss。它更像 GRPO/RLVR 上的一套 recipe。
  • 不要只抄 clip_ratio_high=0.28。如果 reward、采样、长度、KL 设置不同,效果可能完全不同。
  • 不要混淆 overlong filtering 和 overlong reward shaping。官方 DAPO docs 明确说主实验多用 shaping,本仓库实现也主要是 reward penalty。
  • 不要忽略 dynamic sampling 的算力代价。过滤组会让实际生成次数超过一次 train batch。

本节参考与延伸阅读

  • verl 源码:verl/trainer/ppo/core_algos.pycompute_grpo_outcome_advantage()compute_policy_loss_vanilla()agg_loss()
  • verl 源码:verl/trainer/ppo/ray_trainer.py 的 reward、KL、advantage、actor update 主线。
  • verl 源码:verl/workers/reward_manager/dapo.pyverl/experimental/reward_loop/reward_manager/dapo.py
  • verl 配置:verl/trainer/config/algorithm.pyfilter_groupsrollout_correctionAlgoConfig
  • verl 官方文档:docs/algo/dapo.mddocs/algo/grpo.md
  • 示例脚本:examples/grpo_trainer/run_qwen3_30b_a3b_megatron.shexamples/grpo_trainer/run_deepseek_v3_671b_megatron.shexamples/mtp_trainer/run_mimo_7b_mtp_megatron.shverl/experimental/fully_async_policy/shell/dapo_30b_a3b_base_math_fsdp.sh
  • 论文:DAPO: Decoupled Clip and Dynamic Sampling Policy Optimization
  • 数据与模型:BytedTsinghua-SIA DAPO collection

面向源码阅读的 verl 学习文档。