Recipe: 解耦裁剪和动态采样策略优化 (DAPO)

上次更新时间:2025年6月19日。

开源算法实现与实验运行:童宇轩盛光明

🏠 主页 | 📝 论文@arXiv | 🤗 数据集&模型@HF | 🐱 代码@GitHub | 🐱 仓库@GitHub

我们提出了Decoupled Clip and Dynamic sAmpling Policy Optimization (DAPO) 算法。通过公开我们的工作,我们为更广泛的研究社区和社会提供了可扩展强化学习的实际应用途径,使所有人都能从这些进步中受益。我们的系统基于优秀的 verl 框架。感谢他们的辛勤付出!将 DAPO 训练应用于 Qwen2.5-32B 基模型,在 AIME 2024 上相较于之前的 SOTA 模型 DeepSeek-R1-Zero-Qwen-32B,在训练步数减少 50% 的情况下,准确率达到了 50%

dapo-main-result

快速开始

  1. 在 Ray 集群上准备数据集:

bash prepare_dapo_data.sh # 默认将数据集下载到 ${HOME}/verl/data
  1. 从任何机器将作业提交到 Ray 集群:

cd verl # 仓库根目录
export RAY_ADDRESS="http://${RAY_IP:-localhost}:8265" # 要连接的 Ray 集群地址
export WORKING_DIR="${PWD}" # 要打包到 Ray 集群的本地目录
# 在 yaml 中为 Ray 集群设置运行时环境,如环境变量和 pip 包
export RUNTIME_ENV="./recipe/dapo/runtime_env.yaml" # 这将为 Ray 集群设置环境变量
bash recipe/dapo/run_dapo_qwen2.5_32b.sh # 或其他脚本

复现运行

设置

AIME 2024 准确率

硬件

镜像

提交

环境变量

训练脚本

训练记录

DAPO

52%

16x8xH800

hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.3-flashinfer0.2.2-cxx11abi0

4f80e4

runtime_env.yaml

run_dapo_qwen2.5_32b.sh

W&B

DAPO w/o Dynamic Sampling

50%

16x8xH800

hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.3-flashinfer0.2.2-cxx11abi0

4f80e4

runtime_env.yaml

run_dapo_wo_ds_qwen2.5_32b.sh

W&B

DAPO w/o Token-level Loss & Dynamic Sampling

44%

16x8xH20

hiyouga/verl:ngc-th2.5.1-cu120-vllm0.7.4-hotfix

4f80e4

runtime_env.yaml

run_dapo_early_qwen2.5_32b.sh

W&B

[!IMPORTANT]

📢 贡献征集!

欢迎提交您的复现运行和配置!

配置

分离的 Clip Epsilons (-> Clip-Higher)

配置示例:

actor_rollout_ref:
  actor:
    clip_ratio_low: 0.2
    clip_ratio_high: 0.28

clip_ratio_lowclip_ratio_high 指定了 DAPO objective 中的 $\varepsilon_{\text {low }}$ 和 $\varepsilon_{\text {high }}$。

核心相关代码:

pg_losses1 = -advantages * ratio
pg_losses2 = -advantages * torch.clamp(ratio, 1 - cliprange_low, 1 + cliprange_high)
pg_losses = torch.maximum(pg_losses1, pg_losses2)

动态采样 (带分组过滤)

配置示例:

data:
  gen_batch_size: 1536
  train_batch_size: 512
algorithm:
  filter_groups:
    enable: True
    metric: acc # score / seq_reward / seq_final_reward / ...
    max_num_gen_batches: 10 # 非正值表示无上限

filter_groups.enable 设置为 True 将过滤掉其输出 metric 全都相同的组,例如,对于 acc,会过滤掉输出准确率全为 1 或 0 的组。

训练器将以 gen_batch_size 重复采样,直到有足够的合格组用于 train_batch_size 或达到 max_num_gen_batches 指定的上限。

核心相关代码:

prompt_bsz = self.config.data.train_batch_size
if num_prompt_in_batch < prompt_bsz:
    print(f'{num_prompt_in_batch=} < {prompt_bsz=}')
    num_gen_batches += 1
    max_num_gen_batches = self.config.algorithm.filter_groups.max_num_gen_batches
    if max_num_gen_batches <= 0 or num_gen_batches < max_num_gen_batches:
        print(f'{num_gen_batches=} < {max_num_gen_batches=}. 继续生成...')
        continue
    else:
        raise ValueError(
            f'{num_gen_batches=} >= {max_num_gen_batches=}. 生成过多。请检查您的数据。'
        )
else:
    # 对齐批次
    traj_bsz = self.config.data.train_batch_size * self.config.actor_rollout_ref.rollout.n
    batch = batch[:traj_bsz]

Flexible Loss Aggregation Mode (-> Token-level Loss)

配置示例:

actor_rollout_ref:
  actor:
    loss_agg_mode: "token-mean" # / "seq-mean-token-sum" / "seq-mean-token-mean"
    # 注意:“token-mean”是默认行为

loss_agg_mode 设置为 token-mean 意味着计算小批量中所有序列的所有 token 的(策略梯度)损失。

核心相关代码:

if loss_agg_mode == "token-mean":
    loss = verl_F.masked_mean(loss_mat, loss_mask)
elif loss_agg_mode == "seq-mean-token-sum":
    seq_losses = torch.sum(loss_mat * loss_mask, dim=-1)  # token-sum
    loss = torch.mean(seq_losses)  # seq-mean
elif loss_agg_mode == "seq-mean-token-mean":
    seq_losses = torch.sum(loss_mat * loss_mask, dim=-1) / torch.sum(loss_mask, dim=-1)  # token-mean
    loss = torch.mean(seq_losses)  # seq-mean
else:
    raise ValueError(f"无效的 loss_agg_mode: {loss_agg_mode}")

Overlong Reward Shaping (过长奖励塑形)

配置示例:

data:
  max_response_length: 20480 # 16384 + 4096
reward_model:
  overlong_buffer:
    enable: True
    len: 4096
    penalty_factor: 1.0

overlong_buffer.enable 设置为 True 将会惩罚那些虽然超长但仍落在硬上下文限制内的输出。

具体来说,当输出长度超过 max_response_length 段落 0overlong_buffer.len 个 token 时,惩罚将从 0 线性增加到 overlong_buffer.penalty_factor

核心相关代码:

if self.overlong_buffer_cfg.enable:
    overlong_buffer_len = self.overlong_buffer_cfg.len
    expected_len = self.max_resp_len - overlong_buffer_len
    exceed_len = valid_response_length - expected_len
    overlong_penalty_factor = self.overlong_buffer_cfg.penalty_factor
    overlong_reward = min(-exceed_len / overlong_buffer_len * overlong_penalty_factor, 0)
    reward += overlong_reward

FAQ

论文中的“Overlong Filtering”在哪里?

论文中的大多数实验,包括表现最佳的实验,都没有启用 Overlong Filtering,因为在恰当学习最长输出方面,它与 Overlong Reward Shaping 有些重叠。因此,我们没有在此处实现它。

main 分支中的 recipe/dapo 目录recipe/dapo 分支 有什么区别?

recipe/dapo 分支 用于原样复现,因此不会更新新功能。

main 分支中的 recipe/dapo 目录 作为示例,展示了如何扩展最新的 verl 来实现算法 recipe,该目录将随新功能进行维护。

为什么修改后我无法产生相似的结果?

如今的 RL 基础设施仍然存在固有的不稳定性,我们正在努力改进。

我们强烈建议一次只修改一个地方。

我们在此列出了一些已知的问题:

  1. 启用 CUDA Graph (enforce_eager=False) 可能会导致模型性能下降,其原因仍在调查中。