
上一篇解释了 GRPO 为什么能省掉 critic:它用同一 prompt 的多条 response 做组内相对 baseline,而不是训练一个 value model。这篇继续向前走一步:省掉 critic 之后,训练里最容易被低估的问题是什么?
本文的核心判断是:DAPO-style recipe 和 Dr. GRPO 关心的不是“再加一个模型角色”,而是 reward 形状、advantage 归一化和 loss 聚合方式。它们都在处理同一个工程事实:LLM 后训练里的 response 长度不是中性变量。长答案会影响 reward、advantage 广播、token loss 聚合、显存和 rollout 长尾。
先看整体组合图。读这张图时注意:在 verl 示例里,DAPO-style recipe 仍然设置 algorithm.adv_estimator=grpo,真正变化的是 reward manager、overlong buffer、actor clip、loss 聚合和动态 batch 这些训练旋钮。

DAPO-style recipe 的组合地图
这张图对应 examples/grpo_trainer/run_qwen3_30b_a3b_megatron.sh的配置组合:算法仍是 adv_estimator=grpo,in-reward KL 关闭;reward manager 设成 dapo,并开启 overlong buffer;数据侧设置最大 prompt/response 长度;actor 侧设置 dynamic batch、clip ratio 和 loss_agg_mode(examples/grpo_trainer/run_qwen3_30b_a3b_megatron.sh:100-147)。
DAPO 在这个仓库里的直接代码入口是 DAPORewardManager。它注册名是 "dapo",初始化时接收 max_resp_len和 overlong_buffer_cfg;如果启用 overlong buffer,会要求 max_resp_len存在,并要求 buffer 长度为正(verl/workers/reward_manager/dapo.py:25-56)。
reward 计算时,manager 会解码 prompt/response,调用 compute_score()得到基础 score,然后如果启用 overlong buffer,就按 response 的有效长度计算额外惩罚:expected_len = max_resp_len - overlong_buffer_len,超过 expected_len 的部分按比例扣分,最后把 reward 写到最后一个有效 response token 上(verl/workers/reward_manager/dapo.py:71-132)。
下面这张图把 overlong buffer 的形状画出来。它不是简单地把 response 截掉,而是在 reward 层对过长答案施加连续负反馈。

DAPO overlong buffer 的 reward 形状
这个设计的系统意义是:长度管理不只发生在 tokenizer 或 rollout max length 上。data.max_response_length决定生成上限,overlong buffer 决定接近上限时的 reward 曲线,二者共同影响模型学到的“应该多长才合适”。
Dr. GRPO 在当前 examples/grpo_trainer/README.md里不是一个新 worker,也不是一个单独 trainer。README 给出的启用方式是在 canonical GRPO 上叠加三个配置:actor_rollout_ref.actor.loss_agg_mode=seq-mean-token-sum-norm、actor_rollout_ref.actor.use_kl_loss=False、algorithm.norm_adv_by_std_in_grpo=False(examples/grpo_trainer/README.md:30-38)。
这三个配置分别落到两段源码。第一段是 GRPO advantage:compute_grpo_outcome_advantage()默认会把 score - group_mean再除以组内 std;当 norm_adv_by_std_in_grpo=False时,它只做 score - group_mean(verl/trainer/ppo/core_algos.py:294-328)。第二段是 loss 聚合:agg_loss()对 seq-mean-token-sum-norm会先按序列求 token-sum,再按 global batch 做 seq-mean,最后除以固定 scale factor 或 response horizon(verl/trainer/ppo/core_algos.py:1141-1187)。
下面这张图把两个关键开关放在一起。它补充的是:Dr. GRPO 不改变 actor/ref/reward/rollout 的角色图,而是改变同一批 token loss 如何被缩放。

Dr. GRPO 的两个关键开关
这里要谨慎区分源码事实和工程解释。源码事实是:一个开关控制 GRPO advantage 是否除以组内 std,另一个开关控制 actor loss 的 token/sequence 聚合方式。工程解释是:这两个开关都会改变长度、方差和梯度尺度之间的平衡,因此它们属于长度偏置和稳定性管理的一部分。
长度偏置不是一个单点 bug,而是三条路径叠加出来的系统效应。
第一条是 reward 路径。DAPO overlong buffer 明确把 response 长度纳入 reward:超过 expected_len 后,reward 会被额外扣分(verl/workers/reward_manager/dapo.py:121-132)。
第二条是 advantage 路径。GRPO 的 outcome score 是标量,但 compute_grpo_outcome_advantage()会把这个标量乘以 response_mask广播到所有有效 response token 上(verl/trainer/ppo/core_algos.py:329-331)。同一个 scalar advantage 作用在多少 token 上,取决于 response 长度。
第三条是 loss 聚合路径。agg_loss()支持 token-mean、seq-mean-token-sum、seq-mean-token-sum-norm、seq-mean-token-mean等模式;不同模式对长短 response 的权重不同(verl/trainer/ppo/core_algos.py:1168-1197)。actor 默认配置也把 loss_agg_mode作为一等配置项,并说明它支持这些模式(verl/trainer/config/actor/actor.yaml:81-86)。
下面这张图把三条路径合在一起。它和前两张图互补:前面分别讲 reward 和 Dr. GRPO 开关,这张图说明为什么它们最终都落到长度偏置。

长度偏置的三个入口
所以写 DAPO/Dr. GRPO 时,不能只说“用了 GRPO”。更完整的描述应该同时说明 reward 形状、advantage 归一化、loss 聚合方式和 max response length。否则同样是 adv_estimator=grpo,训练语义可能已经不同。
长度问题还会影响性能。GRPO/DAPO 场景常见长上下文和多 response,短样本与长样本混在一起时,padding、micro-batch token 数和 DP rank 负载都会变成系统问题。
示例脚本里,actor 侧会启用 actor_rollout_ref.actor.use_dynamic_bsz=True,并设置 ppo_max_token_len_per_gpu;rollout/ref logprob 也经常跟随 dynamic batch 配置(examples/grpo_trainer/run_qwen3_30b_a3b_megatron.sh:134-147,examples/grpo_trainer/run_qwen3_8b_fsdp.sh:148-172)。actor 配置文件也说明 ppo_max_token_len_per_gpu通常要和 prompt/response 长度相关(verl/trainer/config/actor/actor.yaml:26-33)。
trainer 侧还有 balance_batch:它会按 attention mask 的有效 token 数计算 workload,再对 batch 重排,让 DP rank 上的 token 数更接近;源码注释也提醒,这通常会改变样本顺序,但 advantage 依赖 uid,所以不影响 advantage 计算(verl/trainer/ppo/ray_trainer.py:1060-1128,verl/trainer/ppo/ray_trainer.py:1408-1415)。
这些是性能侧配套,不是算法替代。它们解决的是“长短样本如何高效进 worker”,而不是“长度本身该被奖励还是惩罚”。后者仍由 reward manager、advantage 和 loss 聚合共同决定。
把第 08、09 篇连起来看,第二组的逻辑是:
PPO 需要 critic baseline
GRPO 用组内相对 reward 替代 critic baseline
DAPO / Dr. GRPO 继续管理 reward 长度、advantage 归一化和 loss 聚合尺度
DAPO-style recipe 在 verl 里不是换掉 GRPO 主线,而是在 GRPO 上调整 reward 和训练配置。Dr. GRPO 也不是新增 worker,而是通过 norm_adv_by_std_in_grpo和 loss_agg_mode改变 advantage 与 token loss 的尺度。
下一篇可以继续写 KL、clip、entropy:当 reward、advantage 和长度都进入系统后,还需要一组限速器来防止 actor 更新跑得太快。
examples/grpo_trainer/run_qwen3_30b_a3b_megatron.sh:100-147:DAPO-style recipe 中 GRPO estimator、DAPO reward、overlong buffer、actor clip 与 loss aggregation 配置。examples/grpo_trainer/run_qwen3_8b_fsdp.sh:130-172:常规 GRPO 示例中的 data、actor、rollout/ref dynamic batch 配置。examples/grpo_trainer/README.md:30-38:Dr. GRPO 的三个配置开关。verl/workers/reward_manager/dapo.py:25-56:DAPORewardManager的注册与 overlong buffer 参数校验。verl/workers/reward_manager/dapo.py:71-132:DAPO reward 计算、overlong penalty 和 reward 写入位置。verl/trainer/ppo/core_algos.py:294-328:GRPO advantage 是否按组内 std 归一。verl/trainer/ppo/core_algos.py:329-331:GRPO scalar advantage 如何广播到 response tokens。verl/trainer/ppo/core_algos.py:1141-1199:agg_loss()的 token/sequence 聚合模式。verl/trainer/config/actor/actor.yaml:26-33:dynamic batch 与 token 长度上限配置。verl/trainer/config/actor/actor.yaml:81-86:actor loss aggregation 配置。verl/trainer/ppo/ray_trainer.py:1060-1128:按有效 token 数做 batch balance。verl/trainer/ppo/ray_trainer.py:1408-1415:fit()中 response mask 与 balance_batch 的位置。