本文为 RL 系列的第六篇。在上几篇中我们推导了 GRPO 的核心思想并将其应用于图像生成。本文将介绍 GRPO 的工程增强版——DAPO(Decoupled Clip and Dynamic sAmpling Policy Optimization),它是字节跳动 Seed 团队与清华 AIR 联合提出的大规模 LLM 强化学习算法,用 Qwen2.5-32B 基座模型在 AIME 2024 上达到 50 分(超过 DeepSeek-R1-Zero 的 47 分),且训练步数减少 50%。

论文:DAPO: An Open-Source LLM Reinforcement Learning System at Scale(2025.03)

先看问题:GRPO 在大规模训练中遇到了什么?

还是用上一篇的数学题例子开场。 假设我们用 GRPO 训练一个推理模型解数学竞赛题(AIME 级别),每道题让模型生成 \(G = 16\) 个回答,然后用规则判题:对了奖励 \(+1\),错了奖励 \(-1\)

先看一组理想情况下的训练数据:

题目 回答数 正确/错误 奖励分布 \(\sigma_R\)
竞赛题 A(模型完全不会) 16 0/16 全是 \(-1\) \(0\)
竞赛题 B(模型基本掌握) 16 12/4 12 个 \(+1\),4 个 \(-1\) \(>0\)
竞赛题 C(模型轻松拿下) 16 16/0 全是 \(+1\) \(0\)

问题暴露了

  1. 题 A 和题 C 的 \(\sigma_R = 0\)(所有回答的奖励完全相同),GRPO 的优势公式 \(\hat{A}_i = \frac{r_i - \mu_R}{\sigma_R}\) 中分母为零,梯度信号为零——这些样本完全浪费了!这不只是效率问题:大量的零梯度让有效 batch size 缩水,训练不稳定。

  2. 熵崩溃(Entropy Collapse):PPO/GRPO 使用对称裁剪 \([1 - \varepsilon, 1 + \varepsilon]\)。对于一个概率仅 \(\pi_{\theta_{\text{old}}}(o_t | q) = 0.01\) 的低概率 Token,即使 GRPO 发现它出现在正确回答中应被鼓励,裁剪上界也只允许概率增长到 \(0.01 \times (1 + 0.2) = 0.012\)——几乎动不了。探索性 Token 被压制,模型越来越"保守",策略熵持续下降,最终只会生成少数固定模式的回答。

  3. 长回答的奖励噪声:数学推理题目的回答动辄数千 Token。如果模型生成超长但错误的回答(比如陷入循环推理),传统的 \(+1/-1\) 奖励一视同仁,导致训练信号噪声极大。

用原始 GRPO 训练 Qwen2.5-32B,团队在 AIME 2024 上只拿到了 30 分——远低于 DeepSeek-R1-Zero 的 47 分。DAPO 的四个技术正是为解决以上三个问题而生


DAPO 的四大核心技术

技术一:Clip-Higher(非对称裁剪)

问题复盘:对称裁剪 \([1-\varepsilon, 1+\varepsilon]\) 在抑制概率下降和鼓励概率上升时使用相同的力度。但"探索"需要让小概率 Token 涨上去,而"维稳"需要限制大概率 Token 不要突变。这两件事不应该用同一个阈值。

用例子理解:模型解题时正确用了"换元法"(概率仅 0.01),而"直接暴力展开"(概率 0.8)虽然也对但不够优雅。对称裁剪下,"换元法"的概率上界只能到 \(0.01 \times 1.2 = 0.012\)(增长 20%),而"暴力展开"的概率上界可到 \(0.8 \times 1.2 = 0.96\)(绝对增量 0.16)。低概率探索路径几乎无法获得实质性增长。

Clip-Higher 的解决方案:将裁剪范围解耦为两个独立参数 \(\varepsilon_{\text{low}}\)\(\varepsilon_{\text{high}}\)

\[ \mathcal{J}_{\text{DAPO}}(\theta) = \mathbb{E}\left[\frac{1}{\sum_{i=1}^{G}|o_i|} \sum_{i=1}^{G}\sum_{t=1}^{|o_i|} \min\Big(r_{i,t}(\theta)\hat{A}_{i,t},\ \text{clip}\big(r_{i,t}(\theta),\, 1-\varepsilon_{\text{low}},\, 1+\varepsilon_{\text{high}}\big)\hat{A}_{i,t}\Big)\right] \]

其中 \(r_{i,t}(\theta) = \frac{\pi_\theta(o_{i,t} | q, o_{<t})}{\pi_{\theta_{\text{old}}}(o_{i,t} | q, o_{<t})}\) 是 Token 级别的重要性采样比率。

关键参数选择:DAPO 论文中使用 \(\varepsilon_{\text{low}} = 0.2\)\(\varepsilon_{\text{high}} = 0.28\)

为什么这能防止熵崩溃? 考虑策略梯度对一个 Token \(o_t\) 的更新。当 \(\hat{A}_t > 0\)(这个 Token 出现在好的回答中),PPO 裁剪目标的有效梯度为:

\[ \nabla_\theta J \propto \begin{cases} \hat{A}_t \cdot \nabla_\theta \log \pi_\theta(o_t) & \text{if } r_t(\theta) < 1 + \varepsilon_{\text{high}} \\ 0 & \text{if } r_t(\theta) \geq 1 + \varepsilon_{\text{high}} \end{cases} \]

对于低概率的探索性 Token(\(\pi_{\theta_{\text{old}}}(o_t)\) 很小),\(r_t\) 达到上界 \(1 + \varepsilon_{\text{high}}\) 时对应的新概率为 \(\pi_{\theta_{\text{old}}}(o_t) \cdot (1 + \varepsilon_{\text{high}})\)。提高 \(\varepsilon_{\text{high}}\) 意味着这个"天花板"更高,梯度信号能持续更多步更新才被截断——低概率 Token 获得了更充分的增长机会。

而对于 \(\hat{A}_t < 0\)(坏回答中的 Token),概率下降的底线仍然由 \(\varepsilon_{\text{low}} = 0.2\) 控制——惩罚力度不变。这种不对称设计的本质是:鼓励多探索、同等力度惩罚错误

直觉对比

对称裁剪(PPO/GRPO) Clip-Higher(DAPO)
裁剪范围 \([1-\varepsilon, 1+\varepsilon] = [0.8, 1.2]\) \([1-\varepsilon_{\text{low}}, 1+\varepsilon_{\text{high}}] = [0.8, 1.28]\)
概率 0.01 的 Token 可涨到 \(0.012\) \(0.0128\)
经过 10 次更新后 \(0.012^{10} \approx 0.062\) \(0.0128^{10} \approx 0.112\)
效果 探索被压制,熵崩溃 探索空间更大,策略保持多样性

与 GRPO 中 KL 惩罚的关系:GRPO 使用 \(\beta \cdot D_{\text{KL}}(\pi_\theta \| \pi_{\text{ref}})\) 来约束策略不偏离太远,但 KL 惩罚是全局性的——它惩罚所有概率变化,包括有益的探索。Clip-Higher 则是局部性的约束——只在比率超出范围时截断梯度,保留了范围内的自由度。DAPO 论文发现,Clip-Higher 足以替代 KL 惩罚的约束功能,同时避免了 KL 惩罚对探索的抑制,因此完全移除了 KL 正则项和参考模型。这进一步减少了一个大模型的显存占用。


技术二:Dynamic Sampling(动态采样)

问题复盘:回到开头的例子——题 A(全错)和题 C(全对)的 16 个回答没有产生任何梯度信号。在大规模训练中,这种"零方差"样本可能占到 batch 的一大半,导致有效训练数据大幅缩水。

Dynamic Sampling 的解决方案:只保留组内奖励方差大于零(\(\sigma_R > 0\))的 Prompt 组,过滤掉全对或全错的无效样本,然后累积有效样本直到达到目标 batch size。

用例子理解:假设目标 batch 需要 512 个 Prompt,每轮我们采样 \(512 \times 3 = 1536\) 个 Prompt(batch_multiplier = 3):

步骤 操作 结果
1 采样 1536 个 Prompt,每个生成 16 个回答 共 24576 个回答
2 对每组计算 \(\sigma_R\) 发现 400 组 \(\sigma_R > 0\),1136 组 \(\sigma_R = 0\)
3 只保留 \(\sigma_R > 0\) 的 400 组存入缓存 缓存有 400 组
4 缓存不足 512 组,继续采样新一批 Prompt 新增 150 组有效样本
5 缓存达到 550 组(≥ 512),截取 512 组训练 开始梯度更新

核心约束:DAPO 同时要求每个保留的 Prompt 组中必须同时包含正确和错误的回答:

\[ 0 < |\{o_i \mid \text{is\_correct}(a, o_i)\}| < G \]

这确保了每个 Prompt 都能提供"做对了的回答应该被鼓励、做错了的回答应该被抑制"的双向梯度信号。

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cache = []  # 缓存通过动态采样筛选后的有效 Prompt 组(含 G 条回答与奖励)

while len(cache) < target_batch_size: # 有效组数未达目标前持续过采样
prompts = sample_prompts(batch_multiplier * target_batch_size) # 一次采多倍题目,为过滤零方差组留余量

for prompt in prompts: # 遍历本批中的每一道题目(或对话上下文)
responses = model.generate(prompt, num_return=G) # 对每题并行采样 G 条推理轨迹供组内对比
rewards = judge(responses) # 规则判题等得到回答级标量奖励,用于后续优势估计

if rewards.std() > 0: # 仅保留组内奖励有差异的 Prompt(排除全对/全错,保证 GRPO 优势可分)
cache.append((prompt, responses, rewards)) # 将该题及其 G 条回答与奖励加入有效缓存

if generation_rounds >= max_gen_batches: # 超过最大采样轮次仍凑不齐 batch 则中止,避免训练挂死
raise RuntimeError("无法累积足够的有效样本") # 动态采样失败时显式报错,便于调大 multiplier 或 max_rounds

train_batch = cache[:target_batch_size] # 取前 target_batch_size 组作为本步训练 batch


技术三:Token-Level Policy Gradient Loss(Token 级损失)

问题复盘:标准 GRPO 使用回答级(Sequence-Level)的损失归一化——每个回答的贡献权重相同,不论长短。但在数学推理场景中,不同回答的长度差异极大(短回答 50 Token,长回答 5000 Token)。如果按回答级归一化,一个 5000 Token 的长回答和一个 50 Token 的短回答对损失的贡献一样大,但长回答中每个 Token 分到的梯度只有短回答的 1/100。

用例子理解

回答 Token 数 正确性 回答级权重(GRPO) Token 级权重(DAPO)
\(o_1\)(简洁解法) 50 正确 \(\frac{1}{2}\) \(\frac{50}{5050} \approx 1\%\)
\(o_2\)(长推导) 5000 正确 \(\frac{1}{2}\) \(\frac{5000}{5050} \approx 99\%\)

GRPO 给了两个回答相同的权重,这意味着长回答的 5000 个 Token 平均每个只分到 \(\frac{1}{2 \times 5000} = 0.0001\) 的梯度——信号太弱了。

Token-Level Loss 的解决方案:归一化因子从"回答数"改为"总 Token 数":

\[ \text{GRPO 归一化因子} = \frac{1}{G} \sum_{i=1}^{G} \left(\frac{1}{|o_i|}\sum_{t=1}^{|o_i|} L_{i,t}\right) \quad \xrightarrow{\text{DAPO}} \quad \text{DAPO 归一化因子} = \frac{1}{\sum_{i=1}^{G} |o_i|} \sum_{i=1}^{G} \sum_{t=1}^{|o_i|} L_{i,t} \]

其中 \(L_{i,t} = \min\big(r_{i,t}\hat{A}_{i,t},\ \text{clip}(r_{i,t},\, 1-\varepsilon_{\text{low}},\, 1+\varepsilon_{\text{high}})\hat{A}_{i,t}\big)\)

直觉:GRPO 先求每个回答的"平均 Token 损失",再求回答间平均——长短回答贡献相同。DAPO 直接对所有 Token 做全局平均——每个 Token 的贡献相同,长回答中的推理步骤得到了应有的梯度信号。


技术四:Overlong Reward Shaping(超长回答奖励塑形)

问题复盘:推理模型有时会陷入"循环推理"(重复验证或反复改写),生成远超必要长度的回答。在纯粹的 \(+1/-1\) 二值奖励下,一个 500 Token 的正确解和一个 15000 Token 的正确解获得相同的 \(+1\) 奖励——模型没有任何动力去简洁作答,甚至可能越来越啰嗦(因为长回答中"凑巧"碰到正确答案的概率更高)。

Overlong Reward Shaping 的解决方案:对超过长度阈值 \(L_{\text{max}}\) 的回答施加长度惩罚:

\[ r_{\text{shaped}} = \begin{cases} r_{\text{original}} & \text{if } |o| \leq L_{\text{max}} - L_{\text{buffer}} \\ \min\left(r_{\text{original}},\ -\frac{|o| - (L_{\text{max}} - L_{\text{buffer}})}{L_{\text{buffer}}} \cdot p\right) & \text{if } |o| > L_{\text{max}} - L_{\text{buffer}} \end{cases} \]

其中 \(L_{\text{buffer}}\) 是缓冲区长度(论文中设为 4096),\(p\) 是每超出一个 Token 的惩罚系数(设为 1.0)。

用例子理解\(L_{\text{max}} = 20480\)\(L_{\text{buffer}} = 4096\)):

回答 Token 数 原始奖励 是否超阈值(\(20480 - 4096 = 16384\) 塑形后奖励
\(o_1\)(简洁正确) 500 \(+1\) \(+1\)
\(o_2\)(长但正确) 10000 \(+1\) \(+1\)
\(o_3\)(超长正确) 18000 \(+1\) (超出 1616) \(\min(+1, -0.395) = -0.395\)
\(o_4\)(超长错误) 19000 \(-1\) (超出 2616) \(\min(-1, -0.639) = -1\)

效果:超长且正确的回答反而被惩罚,迫使模型学习更简洁高效的推理路径。


完整 DAPO 算法与代码实现

算法全貌

将四项技术整合起来,DAPO 的完整训练目标为:

\[ \mathcal{J}_{\text{DAPO}}(\theta) = \mathbb{E}\left[\frac{1}{\sum_{i=1}^{G}|o_i|} \sum_{i=1}^{G}\sum_{t=1}^{|o_i|} \min\Big(r_{i,t}(\theta)\hat{A}_{i,t},\ \text{clip}\big(r_{i,t}(\theta),\, 1-\varepsilon_{\text{low}},\, 1+\varepsilon_{\text{high}}\big)\hat{A}_{i,t}\Big)\right] \]

\[ \text{s.t.} \quad 0 < |\{o_i \mid \text{is\_correct}(a, o_i)\}| < G \]

其中优势函数仍然使用 GRPO 的组内相对计算:\(\hat{A}_i = \frac{r_i - \mu_R}{\sigma_R + \epsilon}\),不依赖 Critic 网络。

DAPO 与 GRPO 的差异对比

特性 GRPO DAPO
裁剪方式 对称 \([1-\varepsilon, 1+\varepsilon]\) 非对称 \([1-\varepsilon_{\text{low}}, 1+\varepsilon_{\text{high}}]\)
采样策略 固定 batch,包含零方差组 动态采样,过滤零方差组
损失归一化 回答级(每个回答等权) Token 级(每个 Token 等权)
长度控制 超长奖励惩罚
KL 正则 \(\beta \cdot \text{KL}(\pi_\theta \| \pi_{\text{ref}})\) 移除 KL(依赖裁剪约束策略)

注意 DAPO 移除了 KL 散度正则项——通过 Clip-Higher 和动态采样的协同作用,策略已经被有效约束在合理范围内,不再需要显式的参考模型约束。

完整实现代码

以下是 DAPO 的完整 PyTorch 实现,将四个技术组合为一个端到端的训练流程。

Step 1: 模型定义与超参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch  # PyTorch:前向、反向与分布式训练的基础
import torch.nn.functional as F # 函数式 API,常用于 logits/损失中的激活与归一化
from transformers import AutoModelForCausalLM, AutoTokenizer # 加载 HuggingFace 因果语言模型与分词器

# 模型定义: DAPO 移除了 KL 惩罚,不需要参考模型!
actor = AutoModelForCausalLM.from_pretrained("Qwen2.5-32B-SFT") # 可训练策略 π_θ,用于 rollout 与重要性采样比率
# 对比: GRPO 需要 Actor + Reference; DAPO 只需要 Actor!
# (但实际工程中通常仍保留 ref_model 用于监控 KL 漂移)

tokenizer = AutoTokenizer.from_pretrained("Qwen2.5-32B-SFT") # 与 Actor 词表对齐,编码 prompt/response
optimizer = torch.optim.AdamW(actor.parameters(), lr=1e-6) # 仅更新策略参数;RL 中常用较小 lr 抑制策略突变

# DAPO 超参数(与论文/开源实现对应,控制裁剪、采样与长度塑形)
G = 16 # 每个 Prompt 的采样数 (DAPO 论文用 16)
eps_low = 0.2 # 裁剪下界 (与标准 PPO 相同)
eps_high = 0.28 # 裁剪上界 (Clip-Higher: 比 PPO 的 0.2 更宽)
K_epochs = 2 # 每批数据的更新轮数;多 epoch 提高旧轨迹利用率
target_batch_size = 512 # 目标有效 Prompt 数;一步优化所依据的组内对比规模
batch_multiplier = 3 # 动态采样的过采样倍率;越大越易凑满有效组,但 rollout 成本更高
max_len = 20480 # 最大回答长度;与超长塑形阈值配合,限制生成上限
buffer_len = 4096 # 超长惩罚缓冲区;控制从“不罚”到“重罚”的过渡宽度

Step 2: 四大核心函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def overlong_reward_shaping(rewards, response_lengths, max_len=20480,
buffer_len=4096, penalty=1.0):
"""技术四: 超长奖励塑形"""
threshold = max_len - buffer_len # 超过该长度开始线性惩罚,缓冲区内平滑过渡;对应论文中 L_max 与 buffer
shaped = rewards.clone() # 不原地修改原始奖励,便于对比与调试
over_mask = response_lengths > threshold # 标记超长回答,供塑形与后续优势计算使用
excess = (response_lengths[over_mask] - threshold).float() # 超出阈值部分的 token 数
penalty_values = -(excess / buffer_len) * penalty # 在 buffer 上归一化的负向塑形项,抑制循环啰嗦
shaped[over_mask] = torch.min(rewards[over_mask], penalty_values) # 正确但过长时可能被压成负奖励
return shaped # 返回进入组内标准化前的 shaped 奖励


def dynamic_sampling(model, dataset, target_size, G, batch_multiplier=3,
max_rounds=10):
"""技术二: 动态采样 — 只保留有区分度的 Prompt 组"""
cache = [] # 累积满足方差与对错混合约束的轨迹,供后续 PPO 式更新
for round_idx in range(max_rounds): # 多轮过采样直到凑够有效 batch 或达到轮次上限
prompts = dataset.sample(target_size * batch_multiplier) # 一次采 multiplier 倍,抵消被过滤的零方差组
for prompt, answer in prompts: # 题目与标准答案(或用于判分的元信息)
responses = model.generate(prompt, num_return_sequences=G,
max_new_tokens=max_len, do_sample=True) # 组内 G 条独立样本,估计相对优势
raw_rewards = judge_responses(responses, answer) # 任务奖励(如对错 ±1),尚未含长度塑形
lengths = torch.tensor([len(r) for r in responses]) # 各条回答长度,用于超长惩罚
rewards = overlong_reward_shaping(raw_rewards, lengths) # 技术四:把过长正确回答拉低奖励

n_correct = (rewards > 0).sum().item() # 塑形后仍用 >0 区分“有效正确”条数,满足混合约束
if 0 < n_correct < G: # 约束:组内既有正例也有负例,GRPO 优势分母 σ_R 非零
old_logps = compute_token_log_probs(model, prompt, responses) # 采样时策略 π_old 的 token 对数概率,算比率用
cache.append({
"prompt": prompt, "responses": responses, # 输入上下文
"rewards": rewards, "old_log_probs": old_logps, # 塑形奖励与旧策略对数概率
"lengths": lengths, # 各条生成长度,便于 mask 与日志
}) # 存整条轨迹,供多 epoch 重算新策略下 log π_θ

if len(cache) >= target_size: # 已收集足够有效组,提前返回避免无效开销
return cache[:target_size]

return cache[:target_size] # 未凑满时返回已有部分(工程上可配合告警或丢弃该步)


def compute_group_advantages(rewards_list, G):
"""GRPO 组内相对优势 (与 GRPO 完全相同)"""
rewards = torch.stack(rewards_list).reshape(-1, G) # (batch, G):每个 prompt 一行、G 列 rollout 奖励
mean_r = rewards.mean(dim=1, keepdim=True) # 组内基线 μ_R,去价值网络
std_r = rewards.std(dim=1, keepdim=True) # 组内标准差 σ_R;动态采样保证其 >0
advantages = (rewards - mean_r) / (std_r + 1e-8) # 标准化优势 Â_i,同组内相对比较好坏
return advantages.reshape(-1) # 展平为 (batch*G,) 与逐条 response 对齐


def dapo_loss(log_probs, old_log_probs, advantages, loss_mask,
eps_low=0.2, eps_high=0.28):
"""技术一 + 技术三: Clip-Higher + Token-Level 归一化"""
ratio = torch.exp(log_probs - old_log_probs) # 重要性采样比 r_t = π_θ/π_old,token 级 trust region

# 技术一: 非对称裁剪
surr1 = ratio * advantages # 未裁剪代理目标,鼓励按优势方向更新策略
surr2 = torch.clamp(ratio, 1.0 - eps_low, 1.0 + eps_high) * advantages # Clip-Higher:上界更宽利探索
token_loss = -torch.min(surr1, surr2) # PPO 式 pessimistic bound,取较小者抑制过大策略步长

# 技术三: Token-Level 归一化 (除以有效 Token 总数)
total_tokens = loss_mask.sum() # 全 batch 有效生成 token 数,长短回答按 token 等权
loss = (token_loss * loss_mask).sum() / total_tokens # 全局平均:长链推理每步梯度不再被稀释
return loss # 标量目标,无 KL 项,仅靠裁剪约束偏离

Step 3: 完整训练循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
for step in range(total_steps):  # 外层训练步:每步先采样一批有效轨迹再做多轮策略更新
# === 阶段 1: 动态采样 (技术二 + 技术四) ===
# 反复采样直到积累够 target_batch_size 个有效 Prompt 组
batch = dynamic_sampling(actor, dataset, target_batch_size, G, batch_multiplier) # 在线 rollout + 过滤零方差组

# === 阶段 2: 计算组内相对优势 ===
all_rewards = [item["rewards"] for item in batch] # 每条 trajectory 的(已塑形)回答级奖励列表
advantages = compute_group_advantages(all_rewards, G) # GRPO:同题 G 条间标准化,得到无 critic 的优势信号
# 将序列级优势广播到每个 Token(同一回答内各 token 共享该序列的 Â)
# advantages shape: (batch*G,) → 需要展开到 (batch*G, T);与 GRPO 一致再喂入 token 级裁剪目标

# === 阶段 3: 多 Epoch 更新 (技术一 + 技术三) ===
for epoch in range(K_epochs): # 对同一批数据重复利用,提高样本效率(仍受 clip 约束)
for mini_batch in create_minibatches(batch, minibatch_size=64): # 小批量降低显存、稳定梯度
# 用当前策略重新计算 Token 级对数概率
new_log_probs = compute_token_log_probs(actor,
mini_batch["prompts"],
mini_batch["responses"]) # 当前 θ 下各生成 token 的 log π_θ
old_log_probs = mini_batch["old_log_probs"] # 采样时冻结的 log π_old,构造比率
adv = mini_batch["advantages"].unsqueeze(-1).expand_as(new_log_probs) # 回答级 Â 广播到每个 token 位置
mask = mini_batch["loss_mask"] # 仅对实际生成的 token 累计损失,忽略 padding

# DAPO 损失 (Clip-Higher + Token-Level)
# 注意: 没有 KL 惩罚项! (对比 GRPO 的 loss = policy_loss + β·kl)
loss = dapo_loss(new_log_probs, old_log_probs, adv, mask,
eps_low=eps_low, eps_high=eps_high) # 联合实现非对称裁剪与 token 归一化

optimizer.zero_grad() # 清空上一轮梯度
loss.backward() # 反传得到 ∇_θ L,更新鼓励高优势 token、抑制低优势 token
torch.nn.utils.clip_grad_norm_(actor.parameters(), max_norm=1.0) # 全局梯度裁剪,抑制 RL 不稳定
optimizer.step() # AdamW 一步,策略向 DAPO 目标前进

# === 阶段 4: 监控 (可选但推荐) ===
with torch.no_grad(): # 监控不参与反传,节省计算与显存
mean_reward = torch.stack(all_rewards).mean().item() # 批次平均回报,观察整体是否在提升
entropy = compute_policy_entropy(actor, batch) # 策略熵,诊断是否熵崩溃
# 如果 entropy 持续下降,可能需要增大 eps_high

与 GRPO 训练循环的关键区别: 1. 动态采样替代了固定采样——过滤无效组,每次更新都有充分的梯度信号。 2. dapo_loss 使用非对称裁剪 + Token 级归一化——不再是 torch.clamp(ratio, 1-ε, 1+ε)。 3. 没有 KL 惩罚项——loss = policy_loss 而不是 loss = policy_loss + β * kl_penalty。这是 DAPO 与 GRPO 最显著的差异。 4. 奖励在采样时就经过了超长塑形——长回答在进入优势计算前已被惩罚。

开源代码参考: DAPO 的官方实现基于 verl 框架,NVIDIA NeMo RL 也提供了 DAPO 训练指南


DAPO 的训练效果

DAPO 在 Qwen2.5-32B 基座模型上的 AIME 2024 成绩:

方法 AIME 2024 分数 训练步数
原始 GRPO 30 ~10000
DeepSeek-R1-Zero-Qwen-32B 47 ~10000
DAPO 50 ~5000

四项技术的消融实验(Ablation)显示每项技术都有独立贡献,其中 Clip-Higher 和 Dynamic Sampling 对性能提升最为显著。


更远的视野:2026 年 RL 前沿

DAPO 之后,强化学习领域仍在快速演进:

  • f-GRPO:将 GRPO 推广到通用 f-散度框架,不局限于 KL 散度,适用于安全对齐等更广泛的任务。
  • 2-GRPO:研究发现仅用 2 个 rollout(而非 16 个)就能保留 GRPO 98.1% 的性能,训练时间降至 21%。
  • GIFT:融合 GRPO 的在线采样和 DPO 的隐式奖励,将优化转化为稳定的 MSE 损失。
  • SuperFlow:将 DAPO 类似的思想引入图像生成,使用方差感知采样和步级优势,在 SD3.5 上取得 4.6%-47.2% 的性能提升。
  • Flow-Factory / GenRL:统一的图像/视频生成 RL 框架,支持 T2I、T2V、I2V 多种模态。
  • TRL v1.0(Hugging Face, 2026.04):生产级 RL 框架,统一 SFT → Reward Modeling → Alignment(DPO/GRPO/KTO)流水线。

强化学习已经从一个"理论优美但工程复杂"的技术,演变为大模型训练不可或缺的核心环节。从 REINFORCE 的简单直觉,到 PPO 的步长控制,到 GRPO 的去 Critic 化,再到 DAPO 的工程最佳实践——每一步都在让 RL 变得更简单、更高效、更可规模化。

参考资料:

  1. DAPO: An Open-Source LLM Reinforcement Learning System at Scale
  2. NVIDIA NeMo RL DAPO Guide
  3. Hugging Face TRL v1.0
  4. f-GRPO: Divergence-Based RL for General LLM Alignment

下一篇:笔记|生成模型(二十二):GRPO 的三重面孔——从 2-GRPO 到 f-GRPO 与 GIFT