同一个 reward 断崖,可能来自算法层的分布变窄,也可能来自工程层的概率口径、数值或显存故障。DAPO §3.1 直接描述了 entropy collapse;verl 的代码把非有限梯度和零方差组暴露在不同的位置。先做 0/1 的工程检查,再谈裁剪、采样和学习率。
GRPO 没有 critic,学习信号来自同一 prompt 的一组回答。组内 reward 全同时,advantage 的分子为 0(core_algos.py:326);这是没有信号,不是崩溃。策略坍塌是分布逐步变窄:DAPO §3.1 写的是 entropy 快速下降、组内回答趋同;训练崩溃是某个 step 的工程/数值事故:非有限梯度、KL 爆炸、OOM、训练侧与推理侧的概率口径对不上。
两类会互相转化:坍塌的末端可能以 reward 断崖出现;崩溃也可能渐进 工程判断。所以不要用「渐进 vs 突发」直接定性,先读 rollout,再做概率一致性、mask、数值路径这些 0/1 检查。前一层不通过,后一层的曲线没有解释意义。
DAPO 的处置是 Clip-Higher、Dynamic Sampling、Token-Level Loss、Overlong Shaping;verl 的 filter_groups 默认关闭(algorithm.py:57),rollout.n 默认 1(rollout.yaml:135),actor/entropy 来自更新前的 old_log_prob 前向(ray_trainer.py:1300)。这些默认值会改变你看到的曲线。
先建立共同认知:坍塌是分布变窄,崩溃是某一步的工程/数值事故;两者的证据不在同一张曲线上。
GRPO 用同一 prompt 的一组回答做基线,而不是训练一个 critic。对第 i 条回答,组内 advantage 是:
ε);verl 的实现见 verl/trainer/ppo/core_algos.py:308-326,分母另加 ε,默认 1e-6(:272)减均值决定「谁比同组平均更好」,除以 std 决定「这组差异按多大音量进入训练」。两个操作合起来有一个硬边界:组内所有 reward 相同,分子就是 0,不管 std 是不是 0,advantage 都是 0(core_algos.py:326)。这时这一组没有任务梯度;它既可能是「模型已经全对」,也可能是「模型已经全错」。
DAPO §3.1 的原文术语是 entropy collapse:训练推进时 entropy 快速下降,某些组的采样回答「tend to be nearly identical」,原文把这种状态描述为有限的探索和过早确定的策略。本页沿用笔记的「策略坍塌」指这一类算法层故障,但判据只用原文能支持的部分:entropy 持续下降 + 组内回答趋同。
「训练崩溃」是笔记的工程分类,不是 verl 的代码术语。它对应的是可定位的 0/1 故障:两侧 logprob 对不上、loss mask 写错、log-ratio 溢出、非有限 grad、OOM。表现为 reward 断崖、输出乱码/复读、KL 或 grad norm 突然跳高。它没有「更合适的学习率」,只有写对和写错。
DAPO §3.1 处理的是 entropy 快速下降,做法是放宽上界 εhigh;§4.3 同时写「excessively low entropy」会失去探索能力,「excessively high entropy」常伴随 gibberish 和 repetitive generation。两处并列:低和高的处置方向不同,原文没有给一个统一阈值。DeepSeekMath §5.2.2 也只报告 RL 提升 Maj@K、没有提升 Pass@K(Figure 7)——单看一条准确率上涨,不能证明分布没有变窄。
还有一个容易混淆的配置边界:rollout.n 默认是 1(verl/trainer/config/rollout/rollout.yaml:135)。当一组只有一条样本时,verl 把该组的 mean 设为 0、std 设为 1(core_algos.py:316-317),advantage 退化成原始 reward,组内基线消失。这不是策略坍塌,也不是数值崩溃,而是配置错误:你以为在跑 GRPO,实际没有组内比较。
已有工作给的是两种镜头:一种解释算法层为什么会变窄,一种暴露工程层具体哪个张量坏了;两者都不能单独完成诊断。
DeepSeekMath 给出 GRPO 的目标、组内归一化 advantage 和原始训练设置:G=64、KL 系数 0.04、max length 1024、batch 1024、每个探索阶段只更新一次(§4.2)。DAPO 在 naive GRPO 上直接观察到 entropy collapse(§3.1),并给出四件套:Clip-Higher(§3.1)、Dynamic Sampling(§3.2)、Token-Level Policy Gradient Loss(§3.3)、Overlong Reward Shaping(§3.4)。
卡在:它能解释「分布为什么变窄」,但不能告诉你 loss mask 哪一位写错、推理引擎和训练框架的 logprob 差在哪、显存为什么爆。
verl 把关键故障点拆成了可检查的代码:log-ratio 在 exp 前 clamp 到 [-20,20](core_algos.py:1256);非有限 grad norm 会跳过 optimizer step(workers/engine/fsdp/transformer_impl.py:765-769);KL 有 reward 和 loss 两条路径;零方差组过滤是 algorithm.filter_groups,默认关闭;OOM 有 micro-batch 配置。
卡在:代码能告诉你「这个值坏了」,不能告诉你 reward 是否在测对的东西,也不能替你判断「全对组变多」是数据太简单还是策略已经变窄。
所以顺序不是二选一,而是先工程、后算法:工程层是 0/1,能被穷尽排除;算法层是目标函数的倾向,只能设计上缓解。这个顺序来自笔记的工程实践,原论文没有把它写成流程;本页把它和代码里的实际故障点对齐。
GRPO 的 advantage 公式给出一个硬边界;其余症状要沿 rollout → logprob → advantage → update 的信号链分层。
把「策略坍塌」和「训练崩溃」落到可操作判据,先记住三件事:
rollout.n=1 时 verl 走 len(id2score[idx]) == 1 分支,mean=0、std=1(core_algos.py:316-317),advantage 就是原始 reward。GRPO 文档明确要求把 rollout.n 设成大于 1(docs/algo/grpo.md:29)。norm_adv_by_std_in_grpo 默认 True(verl/trainer/config/algorithm.py:737);Dr. GRPO 把它设为 False(docs/algo/grpo.md:62)。关掉只减均值,advantage 落在原始 reward 差的范围内;这不是「修复」,是换一种权重结构。表 1(本页归纳,不是论文表格):症状到类别的映射。「坍塌 / 崩溃」是笔记的工程分类;每一行的判据都指向可查的原文或源码,不把分类本身当成论文结论。
| 症状 | 先归到哪类 | 判据(原文 / 代码) | 先做什么 |
|---|---|---|---|
| entropy 持续下降,组内多条回答几乎相同 | 策略坍塌(算法层) | DAPO §3.1:entropy decreases quickly;sampled responses of certain groups tend to be nearly identical;Figure 2(b) | 查 upper clip 和数据多样性;不要先降 lr(本页建议) |
| 组内 reward 全同 / 方差=0 / advantage=0 | 信号消失,不是崩溃 | core_algos.py:326:分子为 0;DAPO §3.2:advantage zero → zero policy gradients | 统计全对 / 全错构成;需要时开 algorithm.filter_groups(默认关,algorithm.py:57-58) |
| reward 断崖到 0,输出乱码 / 复读 | 训练崩溃优先(工程 / 数值) | 先读 rollout;非有限 grad 的跳过逻辑 transformer_impl.py:765-769;log-ratio clamp core_algos.py:1256 | 锁第一个异常 step;查概率口径、mask、数值 |
| grad norm 尖刺 | 训练崩溃优先 | verl 记录的是裁剪前 norm(transformer_impl.py:735、748-752);指标入口 ray_trainer.py:1788 | 重放该 batch;看 pre/post clip、advantage 尺度、loss 分母 |
| KL 爆炸 | 训练崩溃优先(口径 / 更新) | KL 来自 old_log_probs vs ref_log_prob(ray_trainer.py:100-104);actor/ppo_kl 来自 log_prob - old_log_prob(core_algos.py:1254-1258) | 先查两侧 logprob 一致性;确认 KL 走 reward 还是 loss |
| NaN / Inf | 训练崩溃(数值) | log-ratio 在 exp 前 clamp 到 [-20,20](core_algos.py:1256-1257);k3 KL 也 clamp(:2239-2245);非有限 grad 跳过更新(transformer_impl.py:765-769) | 沿计算图找第一个非有限值;先修上游 |
| 显存 OOM | 工程 / 基础设施(不是算法坍塌) | docs/start/quickstart.rst:157-162;docs/algo/grpo.md:23:micro batch 配置不应改变算法 / 收敛行为 | 降 ppo_micro_batch_size_per_gpu;查 rollout 显存 |
| 指标 NaN(score / reward / advantage) | 工程 / 数据(可能是全 abort) | metric_utils.py:468-499:全 abort 时 score/reward 在 468-488、advantage 在 490-499 返回 NaN;response/aborted_ratio 在 :511、587 | 先看 aborted_ratio 和 rollout stop reason,不要先怀疑 exp 爆炸 |
rollout.n=1 但以为在跑 GRPO | 配置错误(无组基线) | rollout.yaml:135 默认 1;core_algos.py:316-317 单样本组 mean=0、std=1 → advantage=raw score | 设 n>1(docs/algo/grpo.md:29);确认 group index 正确 |
| 全同组在某个 step 后突然出现 | 先按崩溃查权重同步 / 概率口径 | actor update 后才执行 update_weights(ray_trainer.py:1715-1716);同步失败时 rollout 仍是旧策略 推断 | 抽层 checksum / 首 token logits;记录 policy version |
表里的 § 指 DAPO / DeepSeekMath 章节;代码行号对应 verl@7de8f2e。最后一行的因果链是工程推断,不是原文结论。
DAPO §3.2 要过滤 accuracy 为 0/1 的组(Eq.11);全对组 advantage 为 0 是原文点名的例子,全错组由同一公式同样为 0。§4.1 用 G=16、每批 512 个 prompt。把二值 reward 近似成「每条回答独立、以概率 p 正确」,一组全同的概率就是 pG + (1−p)G。默认 G=16、p=0.90 时:全对组 18.53%,全错组 0.0000%,零方差合计 18.53%;512 个 prompt 中约 417.1 个仍有组内比较信号。
公式:全对 pG,全错 (1−p)G,零方差为两者之和;有效 prompts = 512 × (1 − 零方差)。512 是 DAPO §4.1 的 prompt batch size;G=16 也来自 §4.1。
组 1:G=16,p=0.90。
0.916 = 0.1853020189 → 18.53%0.116 = 0.0000000000000001 → 0.0000%0.1853020189 + 0.0000000000000001 = 0.1853020189 → 18.53%512 × (1 − 0.1853020189) = 417.125366 → 417.1组 2:G=8,p=0.75。
0.758 = 0.1001129150 → 10.01%0.258 = 0.0000152588 → 0.0015%0.1001129150 + 0.0000152588 = 0.1001281738 → 10.01%512 × (1 − 0.1001281738) = 460.734375 → 460.7覆盖与未覆盖:这个模型覆盖「二值 reward、同组独立同分布」下的零方差比例;它不覆盖连续 reward 的近似相等、同组回答之间的相关性、题目难度异质,也不模拟动态采样后的分布变化。它是本页的概率模型,不是论文测量;真实训练要以自己的题集难度分布做基线。分项按四舍五入显示、合计按未舍入值计算,所以显示的分项相加可能与合计差 0.01 个百分点。
为什么不是固定 50%:同一个 p,G 从 8 变到 16,零方差占比会变;同一个 G,p 从 0.9 变到 0.99,全对组会明显变多。固定百分比无法跨 G、跨题集使用。verl 的过滤条件是精确 std==0(verl/trainer/ppo/v1/replay_buffer.py:312),不是百分比阈值。
compute_data_metrics 在整批样本都 abort(response length 为 0)时,会把 score、reward、advantage 直接设成 NaN 并打 warning(score/reward:metric_utils.py:468-488;advantage::490-499)。看到 NaN 先看 response/aborted_ratio,再沿计算图找第一个非有限值;不要一上来就收紧 log-ratio clamp。
全对组和全错组的 advantage 都是 0,但处置方向不同:全对多说明数据对当前模型太简单;全错多可能是验证器过严或模型能力不够。DAPO §3.2 的动态采样把两类都过滤;实际排查要先拆构成,再决定换数据、查 verifier,还是开过滤。
actor/entropy、actor/ppo_kl、actor/grad_norm 各自来自不同阶段;默认配置还会让某些检查静默失效。
verl 一个 step 的顺序是:rollout 生成(ray_trainer.py:1508-1526)→ reward(:1560-1567)→ old_log_prob 前向并算 entropy(:1584-1600)→ ref_log_prob(:1620-1622)→ advantage(:1627-1667)→ update_actor(:1689-1690)→ update_weights(:1715-1716)。这决定了每个指标对应哪一版策略。
actor/entropy 是更新前旧策略的熵在默认 recompute 路径下,_compute_old_log_prob 把 calculate_entropy=True 固定传给 worker(ray_trainer.py:1300),返回值在 :1590-1600 聚合成 actor/entropy。它衡量的是这一批 rollout 所对应的旧策略,不是 update 之后的新策略。actor.calculate_entropy 默认 false(actor.yaml:96)只控制 update 前向是否返回 entropy(ray_trainer.py:1336-1338),不影响上面这条曲线。注意 algorithm.rollout_correction.bypass_mode=True 时 _compute_old_log_prob 会被跳过(ray_trainer.py:1571-1584),该曲线不产生。
actor/ppo_kl 不是到 reference 的 KL:它是 mean(-(log_prob - old_log_prob)),且 log-ratio 在 exp 前被 clamp 到 [-20,20](core_algos.py:1254-1258)。到 reference 的 KL 有两条路:algorithm.use_kl_in_reward=True 时记 actor/reward_kl_penalty(ray_trainer.py:100-115);actor.use_kl_loss=True 时记 kl_loss(verl/workers/utils/losses.py:181-182)。三个名字不能混着读。
actor/grad_norm 是裁剪前的 norm(transformer_impl.py:735 的 docstring;:748-752 调 clip 函数并拿到返回值),由 trainer 在 ray_trainer.py:1788 读取。非有限时 FSDP engine 会 zero_grad 并跳过 optimizer.step(:765-769)——进程不一定挂,但这一步没有更新。日志里只出现一次 WARN 和连续出现 WARN,含义完全不同。
表 2(本页归纳):verl@7de8f2e 的指标与代码位置;不是论文表格。
| 指标 | 实际含义 | 代码位置 |
|---|---|---|
actor/entropy | old_log_prob 前向的 token 熵(更新前旧策略) | ray_trainer.py:1290-1318、1590-1600 |
actor/ppo_kl | mean(-(log_prob-old_log_prob)),log-ratio 先 clamp [-20,20] | core_algos.py:1254-1258、1372-1374 |
actor/pg_clipfrac / _lower | 上界 / 下界 clip 命中比例 | core_algos.py:1271-1277、1372-1374 |
actor/grad_norm | 裁剪前的梯度 norm;非有限则跳过更新 | transformer_impl.py:735、748-752、765-769;ray_trainer.py:1788 |
critic/score|rewards|advantages | 批次统计(mean / max / min);全 abort 时 NaN | metric_utils.py:554-565、468-499 |
response_length/clip_ratio、response/aborted_ratio | 撞 max length 的比例 / 空回答比例 | metric_utils.py:576、511、587 |
actor/reward_kl_penalty / kl_loss | KL 在 reward 路径 / loss 路径的两种记录 | ray_trainer.py:100-115;verl/workers/utils/losses.py:181-182 |
training/filter_groups/filtered_reward_counts | 被过滤组的 reward 值分布(要开 filter_groups) | utils/tracking.py:939;replay_buffer.py:312 |
表 3(本页归纳):会改变诊断结论的默认值;行号对应 verl@7de8f2e。
| 配置 | 默认 | 代码 / 文档 | 对排查的影响 |
|---|---|---|---|
rollout.n | 1 | rollout.yaml:135;docs/algo/grpo.md:29 | GRPO 要 >1;n=1 时单样本组 advantage 退化为原始 reward(core_algos.py:316-317) |
clip_ratio | 0.2 | actor.yaml:36 | DAPO 用 εlow=0.2、εhigh=0.28(§4.1) |
entropy_coeff | 0 | actor.yaml:93 | 默认没有 entropy bonus;不要把它当成熵坍塌的修复开关 |
calculate_entropy | false | actor.yaml:96 | 只影响 update 前向;old_log_prob 的 actor/entropy 仍会记录 |
use_kl_loss / kl_loss_coef | false / 0.001 | actor.yaml:103、113;docs/algo/grpo.md:45、47 | GRPO 文档要求设 True;DAPO 则直接移除 KL(§2.3) |
norm_adv_by_std_in_grpo | true | algorithm.py:737 | Dr. GRPO 设 False(docs/algo/grpo.md:62) |
filter_groups.enable / metric | false / None | algorithm.py:57-58 | DAPO 动态采样默认没开;过滤条件是精确 std==0(replay_buffer.py:312) |
loss_agg_mode | token-mean | actor.yaml:86;docs/algo/grpo.md:41 | 原 GRPO 用 sample-level;文档提示长 CoT 下可能不稳定 |
诊断本身也有算力代价。actor/entropy 不需要额外采样:它和 old_log_prob 在同一个前向里算(ray_trainer.py:1300)。动态采样则要额外生成:DAPO §3.2 说每批的采样成本是动态的,要一直采样到 batch 被非 0/1 正确率的组填满;verl 的 filter_groups 需要 refill_fn 替换被淘汰的组(replay_buffer.py:178-179),不是免费的开关。DAPO §4.2 / Figure 6 报告在其设置下总训练时间没有显著受影响、收敛所需 step 更少;这个结论依赖「生成被长尾样本主导、同步、非流水线」等条件,不能直接搬到所有异步系统。
最后一个正常 step、第一个异常 step、异常之后的一个 step。每个 step 落盘 rollout 原文、reward 分项、old/current logprob、ratio 分位数、attention/loss mask、clip fraction、裁剪前后 grad norm、batch ID 与随机种子。没有这个最小故障窗口,后面的「改一个变量重放」都是噪音。这是笔记的工程流程,不是论文实验设计。
一张图把症状分流:先读 rollout 原文,再验概率一致性与数值路径,最后才动 clip、采样和学习率。
图上的 0/1 检查顺序可以展开成:概率一致性(同一批 token 在训练侧两次、推理侧一次)→ 梯度累积等价性 → 逐 token mask → 数值路径的第一个 NaN/Inf → optimizer / reward。这个顺序的关键不是「优雅」,而是每一层都有通过条件:ratio 的分子分母不是同一套概率口径时,后面的 clip fraction、KL、lr 扫描都失去原义。
如果只能做一件事,先做权重没变时的 ratio 检查(笔记的 A/B/C 检查):冻结权重、关 dropout、固定 RNG,同一批 token 在训练侧前向两次、在推理侧前向一次。训练侧两次不一致,查训练框架自身的可重复性;训练侧和推理侧不一致,查两套实现的概率口径。权重没动但大量 token 已经撞 PPO clip,说明训练目标把实现差异误读成了策略更新 工程判断——这时应先停训修口径,而不是调 clip。
Table 1 是逐项叠加的结果,能说明这些处置改变了训练,不说明每一项都独立有效;DeepSeekMath 的 Maj@K / Pass@K 则说明单看一条准确率不够。
表 4(本页转录 DAPO Table 1):AIME24 avg@32。原文的 progressive techniques 按行叠加;「较上一行」是本页按相邻两行算的差值,不是原文的独立消融。
| 设置 | AIME24 avg@32 | 较上一行 |
|---|---|---|
| DeepSeek-R1-Zero-Qwen-32B(参考行,不属于叠加序列) | 47 | — |
| Naive GRPO | 30 | — |
| + Overlong Filtering | 36 | +6 |
| + Clip-Higher | 38 | +2 |
| + Soft Overlong Punishment | 41 | +3 |
| + Token-level Loss | 42 | +1 |
| + Dynamic Sampling(DAPO) | 50 | +8 |
Naive GRPO → DAPO 共 +20 分(50 − 30)。每一行是在前一行基础上加一项,所以「+2」是 Clip-Higher 在这个顺序、这组数据上的边际结果,不是它单独的因果效应 推断(依据:Table 1 的 progressive 叠加结构)。
原文的主结果是 DAPO 50 对 DeepSeek-R1-Zero-Qwen-32B 47(§1、§4.2、Figure 1),但 47 来自另一套 R1 配方,50 来自 DAPO 的叠加配方,两者不是同一组控制变量。Table 1 内部是 progressive 叠加:naive GRPO 30 → 最终 50;50 与 47 的跨配方比较不能当成受控消融。
表 5(本页转录 DAPO §4.1):复现实验设置。这些条件不能从数字里拆掉。
| 项 | 值 |
|---|---|
| prompt batch size | 512 |
| 每 prompt 采样数 G | 16 |
| 训练 mini-batch size | 512 |
| 每 rollout step 的梯度更新数 | 16 |
| 最大生成长度 | 20,480(16,384 + 4,096 cache) |
| 学习率 | 1e-6 |
| clip 参数 | εlow=0.2,εhigh=0.28 |
| 评测 | AIME24 avg@32,temperature 1.0,top_p 0.7 |
DeepSeekMath §5.2.2 / Figure 7 报告:RL 后的模型在 GSM8K 和 MATH 上 Maj@K 提升,Pass@K 没有提升(原文措辞是 not Pass@K)。原文的解释是 RL 让输出分布更稳、把正确回答从 TopK 里抬出来,而不是提升基础能力。对排查的含义是:训练 reward 或 Maj@K 上涨,不能单独证明分布没有变窄;如果 Pass@K 相对基线变差,才更像坍塌方向的证据(这是本页对 Figure 7 的延伸解读,不是原文的 collapse 判据)。
DAPO §4.3 把 response length、reward、generation entropy、mean probability 列为监控指标:entropy 过低表示分布过尖、失去探索;过高常伴随 gibberish 和重复生成。Figure 7(c) 是 generation entropy,Figure 7(d) 是 mean probability。原文没有给一个跨模型通用的 entropy 数值,只给了方向性判断。
代码侧还有两个反直觉的「效果」:非有限 grad 不一定让训练挂掉——FSDP engine 会跳过这一步的 optimizer update(transformer_impl.py:765-769),所以日志里可能只有一行 WARN,曲线表现为「少更新了一步」;指标 NaN 不一定是数值爆炸——整批 abort 时 compute_data_metrics 会主动返回 NaN(score/reward:metric_utils.py:468-488;advantage::490-499)。这两种情况都应该先看原始日志和 aborted ratio,再决定是否改算法。
什么时候别用这套二分:只有一条曲线、无法重放、或者把 OOM / 全 abort 当成算法坍塌。
你有原始 rollout 和 reward 分项,能保存最后正常/首个异常/异常后一步,能在冻结权重下重放同一批 token。此时二分法能快速决定先查工程还是先查算法,零方差计算能给出题集难度的自然基线。
只有 reward / entropy 两条曲线、没有 rollout 原文,无法区分「输出变窄」和「验证器坏了」;reward 是学习出来的 RM 且没有独立复核时,reward 上涨可能只是 hacking。多轮 agent 的工具 token / loss mask 也是高发点,不能只用 GRPO 的组内公式解释。
std==0(replay_buffer.py:312),不是「超过多少就报警」。先在自己的干净配置上量 baseline,再比较故障 run。εlow=0.2 / εhigh=0.28(§4.1;无 KL 见 §2.3;规则 reward 见 §2.4)。搬到 RLHF、agent 或不同模型规模前,要重新验证;Table 1 的 +8 也不是「开动态采样就一定 +8」。docs/algo/grpo.md:23;docs/start/quickstart.rst:157-162)。修 OOM 是恢复可训练性,不会修掉分布变窄。工程判断transformer_impl.py:765-769 的 zero_grad 只是防止坏更新写进参数;如果 WARN 连续出现,根因仍在概率口径、mask 或数值路径上。如果组内还有 reward 方差,就先看 advantage 和 clip;如果组内已经全同,就先统计全对/全错;如果出现 NaN / Inf / 非有限 grad,就先沿计算图找第一个非有限值。顺序对了,降 lr 才不会变成把 bug 推迟到视野之外的稀释手段。