昇腾后训练强化学习最佳实践
发表于 2025/11/07
引言
大模型时代,强化学习正从“锦上添花”变成“不可或缺”。参数膨胀后,大模型训练中的难题从“写出答案”变成了“写出人类想要的答案”,强化学习用人类偏好把模型对齐到真实需求,同时抑制幻觉,并把下一token预测升级为多步规划,使大模型既安全又会推理。
但真要把大模型 RL 训练跑起来,往往存在几个痛点:
1.环境踩坑:多个库版本稍有不对就报错。
2.流程复杂:RL 训练的流程复杂度往往让很多人望而却步,不敢迈出步伐。
3.参数理解:RL 训练配置参数太多,又难理解,不清楚每个参数的作用。
这篇实践文档就是用来解决这些痛点的。我们基于4台Atlas 800T A3卡,完整跑通 verl + dapo 框架下的 Qwen3-32B RL训练,并把每一步可复现,可扩展的细节全部开源出来。读完本文,你将获得:
1.开箱即用的镜像:提供一键 docker pull 的镜像,所有训练和评测相关环境已预装,迅速完成环境拉起,告别版本地狱。
2.训练-评估全链路脚本:提供整个流程中每一步的脚本,通过简单的复制粘贴即可完成复杂的模型训练及评估。
3.训练参数及日志结果解读:提供 RL 训练中复杂的各种参数及训练过程中的日志指标的说明,帮助大家理解各个参数的含义,让调参不再玄学。
4.性能调优锦囊:提供常用的性能调优指南,帮助大家在资源有限的情况下,成功拉起大模型训练并提高吞吐量。
5.扩展定制:通过扩展内容可以学习到大模型 RL 训练中的自定义流程,不再仅限于“跑通“,而是可以真正的动手修改。
如果想要快速上手 RL 训练,少踩坑,少调参,那么请详细的跟着这篇最佳实践文档操作!
1.硬件要求
当前支持Atlas 800T A3 与 Atlas 900 A3 SuperPoD。完成跑完本次最佳实践需要 4台Atlas 800T A3。
2.大模型强化学习介绍
2.1 强化学习基础概念
强化学习是一种通过试错学习来优化策略的方法,其目标是最大化累积奖励。在大语言模型(LLM)的训练中,RL 框架通常包括以下几个核心组件:
l 状态空间:输入序列的分布。
l 动作空间:所有可能的输出 token(即词汇表中的词)。
l 策略函数:根据当前状态选择下一个动作(token)的函数。
l 价值函数:评估在给定状态下采取特定动作的价值。
整体而言,RL 训练通过策略函数生成动作,价值函数评估动作收益,环境反馈奖励信号,以此循环迭代,最终使模型学会生成高奖励的优质回答。
2.2 强化学习算法及训练流程介绍
算法 | 核心改进点 | 训练流程 |
PPO | • 引入裁剪机制保证训练稳定性 • 支持多轮策略更新 • 更好的样本效率 | 1. 采样生成回答 2. 奖励模型评分 3. 计算优势函数 4. 裁剪策略梯度更新 5. 迭代优化 |
GRPO | • 无需显式奖励模型 • 将RL问题转化为分类问题 • 直接偏好优化 | 1. 定义规则奖励函数 2. 生成回答并计算奖励 3. 策略梯度更新 4. 参考模型正则化 |
DAPO | • 动态权重调整 • 多目标平衡优化 • 自适应偏好对齐 | 1. 多维度奖励计算 2. 动态权重分配 3. 自适应策略更新 4. 多目标平衡 |
例如下图简要介绍了 GRPO 与 PPO 的整体流程及区别:

其中,q 表示问题数据集,o 表示旧策略模型中采样的问题和输出,r 表示奖励模型计算的收益,v 表示价值模型预测得到的收益,A 表示最终实际的收益 advantage。图中黄色的是需要在训练过程中更新参数的模型,蓝色的是不需要更新参数的模型,GRPO 算法相对于 PPO 算法少训练了一个价值模型,而且大大简化了优势函数的计算,节约了计算资源。
2.3 RL 相较于 SFT 的区别
在大模型训练中,监督微调 SFT 和强化学习 RL 是两种不同的模型训练方法,分别用于不同的阶段和目的。以下是它们的主要区别:
特性 | 监督微调SFT | 强化学习RL |
数据 | 高质量输入-输出配对数据 | 奖励信号或偏好数据。 |
优化目标 | 最小化模型在标注数据上的损失函数,使模型在特定任务上表现更好。 | 最大化模型在特定任务上的奖励函数,使输出更符合人类的期望。 |
适用场景 | 适用于有明确标注数据的任务,如图像分类、文本分类、机器翻译等。 | 适用于需要高质量生成输出的任务,如对话系统、文本生成等。 |
训练复杂度 | 相对较低,主要依赖于标注数据和传统监督学习算法。 | 相对较高,需要结合人类反馈和强化学习算法。 |
2.4 典型数据集格式
[
{
"prompt": [
{
"content": "内容一般为提出的问题",
"role": "user"
}
],
"reward_model": {
"ground_truth": "内容为问题的答案,单选题例如 A"
"style": "rule"
...
}
},
...
]
veRL 仓库的 /verl/examples/data_preprocess 路径下会提供数据处理的脚本,一般会按如下方式处理数据集:
instruction_following = "Let's think step by step and output the final answer within \\boxed{}."
def make_map_fn(split):
def process_fn(example, idx):
question = example.pop("problem")
question = question + " " + instruction_following
answer = example.pop("solution")
solution = extract_solution(answer)
data = {
"data_source": data_source,
"prompt": [{"role": "user", "content": question}],
"ability": "math",
"reward_model": {"style": "rule", "ground_truth": solution},
"extra_info": {"split": split, "index": idx},
}
return data
return process_fn
# 其中extract_solution表示从solution中提取boxed{}里面的答案,作为ground_truth用于后续rewards的计算
3. 镜像环境导入
3.1 关键软件版本
软件 | 版本 |
Python | 3.10.0 |
PyTorch | 2.7.1 |
Transformers | main commit id 8365f70e925 |
vLLM | main commit id 38217877aa7004 |
vLLM-ascend | main commit id 1de16ead8eecfec |
veRL | main commit id 796871d7d092f7c |
CANN |
3.2 下载镜像
镜像归档在昇腾社区的昇腾镜像仓库,命令行下载方式如下:
docker pull swr.cn-south-1.myhuaweicloud.com/ascendhub/verl_pt27_2025rc3:latest-arm3.3 启动容器
执行以下命令即可启动容器:
eth_name=`ifconfig |grep "$(hostname -I |awk '{print $1}'|awk -F '.' '{print $0}')" -B 1|awk -F ':' '{print$1}' | head -1 | tail -1`
docker run -it \
--net host \
--shm-size 720g \
--privileged \
--device=/dev/davinci0 \
--device=/dev/davinci1 \
--device=/dev/davinci2 \
--device=/dev/davinci3 \
--device=/dev/davinci4 \
--device=/dev/davinci5 \
--device=/dev/davinci6 \
--device=/dev/davinci7 \
--device=/dev/davinci8 \
--device=/dev/davinci9 \
--device=/dev/davinci10 \
--device=/dev/davinci11 \
--device=/dev/davinci12 \
--device=/dev/davinci13 \
--device=/dev/davinci14 \
--device=/dev/davinci15 \
--device=/dev/davinci_manager \
--device=/dev/devmm_svm \
--device=/dev/hisi_hdc \
-e LD_LIBRARY_PATH=/usr/local/Ascend/driver/lib64/driver:/usr/local/Ascend/driver/lib64/common:/usr/local/lib \
-e HCCL_SOCKET_IFNAME=$eth_name \
-e GLOO_SOCKET_IFNAME=$eth_name \
-v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
-v /usr/bin/msnpureport:/usr/bin/msnpureport \
-v /root/.cache:/root/.cache \
-v /tmp:/tmp \
swr.cn-south-1.myhuaweicloud.com/ascendhub/verl_pt27_2025rc3:latest-arm \
/bin/bash
建议通过 -v 挂载模型权重目录,使用 -v 参数挂载目录时,不可以挂载 /home 目录,会覆盖掉镜像中的文件。
以下所有命令均在容器中执行。
3.4 激活 conda 环境
执行以下命令,查看 conda 环境。
conda env list其中 aisbench_env 为模型评估环境,包含 benchmark 评测工具,verl_env 为模型训练环境,包含 veRL强化学习框架及依赖。
执行以下命令,激活训练环境。
conda activate verl_env3.5 激活 CANN
source /usr/local/Ascend/ascend-toolkit/set_env.sh
source /usr/local/Ascend/nnal/atb/set_env.sh
在每次新开一个窗口时,都需要激活conda环境和CANN包。
4. 模型训练与评估
以下将基于 veRL 仓库中的示例脚本,使用 Qwen3-32B 模型及 DAPO-Math-17k 数学领域数据集,详细介绍整体的强化学习训练流程,并基于 AISBenchmark 评估套件,比较强化学习训练前后在 Math 测试数据集上的效果。
4.1 DAPO 算法及 Ray 介绍
4.1.1 DAPO 算法简介
由字节跳动、清华大学和香港大学联合研发的一种创新的开源解决方案,Decoupled Clip and Dynamic sAmpling Policy Optimization。
优化函数:

关键技术点:
- Clip-Higher:通过解耦上下剪辑范围,提高低概率 token 生成的概率,增加系统多样性,避免了开源社区复现中出现的熵崩溃现象。
- Dynamic Sampling:通过过采样和过滤掉准确率为0或1的样本,确保每个批次中的样本都有有效的梯度,提高训练效率和稳定性。
- Token-Level Policy Gradient Loss:在长推理链 RL 场景中,通过在 token 级别计算策略梯度损失,而不是样本级别的损失计算,解决了样本长度不一致导致的问题。
- Overlong Reward Shaping:为过长的样本设计惩罚机制,减少奖励噪声,稳定训练过程。
4.1.2 Ray 分布式计算框架介绍
Ray 在 VeRL 中扮演着分布式计算的角色,它负责管理所有计算资源和任务调度,让 VeRL 可以专注于强化学习算法的实现。Ray 在 VeRL 中的作用主要体现在三个层面:
角色抽象与资源管理
VeRL 中的每个RL角色(Actor、Critic、Reward Model、Generator)都被映射为 Ray Actor - 有状态的工作进程。
Ray 通过 Placement Group 机制将不同的 RL 角色精确地分配到特定的 GPU 或 CPU 资源上,实现复杂的colocate策略。
这种设计使得多个模型可以在同一组计算卡上按时间片交替运行。
异步执行与流水线
Ray 的异步执行机制让VeRL中的不同角色能够并行工作。
通过 ray.get() 和 ray.wait() 方法,VeRL 可以灵活控制任务的同步点,实现高效的资源利用。
分布式通信与数据交换
Ray的Object Store作为分布式内存存储,为VeRL中的各个角色提供高效的数据交换机制。
不同RL角色之间通过传递对象引用而非数据本身,减少了不必要的数据拷贝和传输开销。
4.2 DAPO-Math-17k 数据集概要及准备
数据集概要:
DAPO-Math-17k 是由字节跳动、清华大学人工智能产业研究院、香港大学和清华大学AIR-SIA实验室联合创建的一个包含17000个数学问题及其整数答案的数据集。数据集构建过程中,研究者们首先从AoPS网站和竞赛主页收集问题和答案,然后通过人工标注和转换,将答案统一为整数形式,便于模型进行强化学习训练。
数据集特点:
- 高质量标注:所有问题都经过人工标注和转换,确保答案的准确性和一致性。
- 整数答案:通过将答案转换为整数形式,简化了奖励信号的计算,减少了错误。
- 大规模:包含17000个样本,为大规模强化学习提供了丰富的训练样本。
数据集下载:
执行以下命令,即可下载 DAPO-Math-17k 数据集。
git clone https://www.modelscope.cn/datasets/AI-ModelScope/DAPO-Math-17k.git数据集样例:
{
"data_source": "math_dapo",
"prompt": [
{
"content": "Solve the following math problem step by step. The last line of your response should be of the form Answer: $Answer (without quotes) where $Answer is the answer to the problem.\n\nIn triangle $ABC$, $\\sin \\angle A = \\frac{4}{5}$ and $\\angle A < 90^\\circ$. Let $D$ be a point outside triangle $ABC$ such that $\\angle BAD = \\angle DAC$ and $\\angle BDC = 90^\\circ$. Suppose that $AD = 1$ and that $\\frac{BD}{CD} = \\frac{3}{2}$. If $AB + AC$ can be expressed in the form $\\frac{a\\sqrt{b}}{c}$ where $a, b, c$ are pairwise relatively prime integers, find $a + b + c$.\n\nRemember to put your answer on its own line after \"Answer:\".",
"role": "user"
}
],
"ability": "MATH",
"reward_model": {
"ground_truth": "34",
"style": "rule-lighteval/MATH_v2"
},
"extra_info": {
"index": "9a9b6eb4-a1cb-49d1-8c1e-62eaf2f74079"
}
}
每个样本由多个字段组成,其中 data_source 字段用于映射 veRL 中为该数据集实现的 reward 函数,prompt 字段是问题内容,reward_model 字段中的 ground_truth 为正确答案。
4.3 DAPO-Math-17k数据集 reward 函数解析
DAPO-Math-17k 数据集的 reward 函数内置于 /home/verl/verl/utils/reward_score/math_dapo.py,其中 compute_score 函数内容如下:
def compute_score(
solution_str: str,
ground_truth: str,
strict_box_verify: bool = False,
pause_tokens_index: Optional[list[int]] = None,
) -> float:
"""Compute the reward score for a solution.
Args:
solution_str: The solution string
ground_truth: The ground truth answer
strict_box_verify: Whether to use strict box verification
pause_tokens_index: Indices of pause tokens
Returns:
Reward score (1.0 for correct, -1.0 for incorrect)
"""
# Limit solution length for efficiency
solution_str = solution_str[-300:] # The longest answer in MATH-500 has 159 characters
# Verify the solution
correct, pred = verify(solution_str, ground_truth, strict_box_verify, pause_tokens_index)
reward = 1.0 if correct else -1.0
acc = correct
return {
"score": reward,
"acc": acc,
"pred": pred,
}
- 文件中共有多个函数,其中 compute_score 为计算奖励函数的接口,是最核心的函数。
- compute_score 接收多个参数,其中 solution_str 是推理后端的输出结果,ground_truth 是数据集中的正确答案。
- verify 函数负责从推理结果中提取最终答案和与正确答案的比较结果并返回。
- 最后判断提取的答案是否正确,相同则返回1分,不同则返回-1分。
verify 函数的输入输出样例。
{
"solution": " is of multiplicity three when $k = 6$.\n\nWe can factor the cubic polynomial:\n\n$$\n8x^3 + 12x^2 + 6x + 1 = (2x + 1)^3\n$$\n\nThis confirms the **triple root** at $x = -\\frac{1}{2}$, meaning **only one distinct complex (in the algebraic sense) solution**.\n\n---\n\n### Final Answer\n\n$$\n\\boxed{6}\n$$\n\nAnswer: 6",
"ground_truth": "6",
"correct": true,
"pred": "6",
}
solution_str 和 ground_truth 为 verify 函数输入,分别是推理输出结果和数据集自带正确答案。correct 和 pred 为 verify 函数输出,分别是提取答案与正确答案比较结果和从输出结果中提取的答案。
4.4 模型下载
执行以下命令,即可开始下载 Qwen3-32B 模型。
export HF_ENDPOINT=https://hf-mirror.com
huggingface-cli download --resume-download Qwen/Qwen3-32B --local-dir /path/to/local_dir--local-dir:模型保存路径。
4.5 启动训练
使用多机训练时,训练配置要完全相同,包括模型路径等。
打开 /home/verl/ray_start.sh 文件,该脚本文件用于配置各类环境变量以及 ray 节点拉起,进行如下修改:
pkill -9 python
ray stop --force
rm -rf /tmp/ray
export RAY_DEDUP_LOGS=0
export HYDRA_FULL_ERROR=1
# TASK_QUEUE_ENABLE,下发优化,图模式设置为1,非图模式设置为2
export TASK_QUEUE_ENABLE=1
export HCCL_ASYNC_ERROR_HANDLING=0
export HCCL_EXEC_TIMEOUT=3600
export HCCL_CONNECT_TIMEOUT=3600
# 修改为当前需要跑的用例路径
DEFAULT_SH="./test_dapo_qwen3_32b_fsdp2_A3.sh"
echo "Use $DEFAULT_SH"
ulimit -n 32768
mkdir logs
NNODES=4
NPUS_PER_NODE=16
# 修改为对应主节点IP
MASTER_ADDR="IP FOR MASTER NODE"
# 修改为当前节点的通信网卡
SOCKET_IFNAME="Your SOCKET IFNAME"
export HCCL_SOCKET_IFNAME="SOCKET IFNAME FOR CURRENT NODE"
export GLOO_SOCKET_IFNAME="SOCKET IFNAME FOR CURRENT NODE"
# 获取当前IP
CURRENT_IP=$(ifconfig $SOCKET_IFNAME | grep -Eo 'inet (addr:)?([0-9]{1,3}\.){3}[0-9]{1,3}' | awk '{print $NF}')
if [ "$MASTER_ADDR" = "$CURRENT_IP" ]; then
# 主节点启动
ray start --head --port 6766 --dashboard-host=$MASTER_ADDR --node-ip-address=$CURRENT_IP --dashboard-port=8260 --resources='{"NPU": '$NPUS_PER_NODE'}'
while true; do
ray_status_output=$(ray status)
npu_count=$(echo "$ray_status_output" | grep -oP '(?<=/)\d+\.\d+(?=\s*NPU)' | head -n 1)
npu_count_int=$(echo "$npu_count" | awk '{print int($1)}')
device_count=$((npu_count_int / $NPUS_PER_NODE))
# 判断device_count 是否与 NNODES 相等
if [ "$device_count" -eq "$NNODES" ]; then
echo "Ray cluster is ready with $device_count devices (from $npu_count NPU resources), starting Python script."
ray status
bash $DEFAULT_SH
break
else
echo "Waiting for Ray to allocate $NNODES devices. Current device count: $device_count"
sleep 5
fi
done
else
# 子节点尝试往主节点注册 ray 直到成功
while true; do
# 尝试连接 ray 集群
ray start --address="$MASTER_ADDR:6766" --resources='{"NPU": '$NPUS_PER_NODE'}' --node-ip-address=$CURRENT_IP
# 检查连接是否成功
ray status
if [ $? -eq 0 ]; then
echo "Successfully connected to the Ray cluster!"
break
else
echo "Failed to connect to the Ray cluster. Retrying in 5 seconds..."
sleep 5
fi
done
fi
sleep 600
- DEFAULT_SH:修改为训练所用配置 sh 文件路径。在此案例中中修改为 /home/verl/recipe/dapo/test_dapo_qwen3_32b_fsdp2_A3.sh。
- NNODES 和 NPUS_PER_NODE:修改为使用节点数量和每个节点 NPU 数量。在此案例中分别为4和16。
- MASTER_ADDR:修改为对应主节点 IP。即所有节点的 MASTER_ADDR 应该相同。
- SOCKET_IFNAME, HCCL_SOCKET_IFNAME, GLOO_SOCKET_IFNAME: 修改为对应通信网卡,通信网卡可以通过以下命令获取:
ifconfig |grep "$(hostname -I |awk '{print $1}'|awk -F '.' '{print $0}')" -B 1|awk -F ':' '{print$1}' | head -1 | tail -1结果如下所示:

打开 /home/verl/recipe/dapo/test_dapo_qwen3_32b_fsdp2_A3.sh,修改 sh 配置文件:
#!/usr/bin/env bash
set -xeuo pipefail
project_name='DAPO'
exp_name='DAPO-Qwen3-32B'
adv_estimator=grpo
use_kl_in_reward=False
kl_coef=0.0
use_kl_loss=False
kl_loss_coef=0.0
clip_ratio_low=0.2
clip_ratio_high=0.28
max_prompt_length=$((1024 * 2))
max_response_length=$((1024 * 20)) # 20
enable_overlong_buffer=True
overlong_buffer_len=$((1024 * 4))
overlong_penalty_factor=1.0
loss_agg_mode="token-mean"
use_token_level_loss=True
enable_filter_groups=False
filter_groups_metric=acc
max_num_gen_batches=10
train_prompt_bsz=64
gen_prompt_bsz=$((train_prompt_bsz * 1))
n_resp_per_prompt=16
train_prompt_mini_bsz=32
# Ray
WORKING_DIR=${WORKING_DIR:-"${PWD}"}
RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
NNODES=4
# Paths
MODEL_PATH=/path/to/Qwen3-32B
CKPTS_DIR=/path/to/Qwen3-32B-save
TRAIN_FILE=/path/to/dapo-math-17k.parquet
TEST_FILE=/path/to/dapo-math-17k.parquet
# Algorithm
temperature=1.0
top_p=1.0
top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
val_top_p=0.7
# Performance Related Parameter
sp_size=4
use_dynamic_bsz=True
actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) / sp_size))
infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) / sp_size))
offload=True
gen_tp=4
python3 -m recipe.dapo.main_dapo \
data.train_files="${TRAIN_FILE}" \
data.val_files="${TEST_FILE}" \
data.prompt_key=prompt \
data.truncation='left' \
data.max_prompt_length=${max_prompt_length} \
data.max_response_length=${max_response_length} \
data.gen_batch_size=${gen_prompt_bsz} \
data.train_batch_size=${train_prompt_bsz} \
actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
algorithm.adv_estimator=${adv_estimator} \
algorithm.use_kl_in_reward=${use_kl_in_reward} \
algorithm.kl_ctrl.kl_coef=${kl_coef} \
actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
actor_rollout_ref.actor.clip_ratio_c=10.0 \
algorithm.filter_groups.enable=${enable_filter_groups} \
algorithm.filter_groups.max_num_gen_batches=${max_num_gen_batches} \
algorithm.filter_groups.metric=${filter_groups_metric} \
actor_rollout_ref.model.use_remove_padding=True \
actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
actor_rollout_ref.model.path="${MODEL_PATH}" \
actor_rollout_ref.model.enable_gradient_checkpointing=True \
actor_rollout_ref.actor.optim.lr=1e-6 \
actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
actor_rollout_ref.actor.optim.weight_decay=0.1 \
actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
actor_rollout_ref.actor.fsdp_config.param_offload=${offload} \
actor_rollout_ref.actor.fsdp_config.optimizer_offload=${offload} \
actor_rollout_ref.actor.entropy_coeff=0 \
actor_rollout_ref.actor.grad_clip=1.0 \
actor_rollout_ref.rollout.name=vllm \
actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
actor_rollout_ref.rollout.gpu_memory_utilization=0.70 \
actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
actor_rollout_ref.rollout.enable_chunked_prefill=True \
actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
actor_rollout_ref.rollout.temperature=${temperature} \
actor_rollout_ref.rollout.top_p=${top_p} \
actor_rollout_ref.rollout.top_k="${top_k}" \
actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
actor_rollout_ref.rollout.val_kwargs.do_sample=True \
actor_rollout_ref.rollout.val_kwargs.n=1 \
actor_rollout_ref.ref.fsdp_config.param_offload=${offload} \
actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
actor_rollout_ref.actor.fsdp_config.fsdp_size=-1 \
actor_rollout_ref.ref.strategy=fsdp2 \
actor_rollout_ref.actor.strategy=fsdp2 \
reward_model.reward_manager=dapo \
reward_model.overlong_buffer.enable=${enable_overlong_buffer} \
reward_model.overlong_buffer.len=${overlong_buffer_len} \
reward_model.overlong_buffer.penalty_factor=${overlong_penalty_factor} \
trainer.logger='["console"]' \
trainer.project_name="${project_name}" \
trainer.experiment_name="${exp_name}" \
trainer.n_gpus_per_node=16 \
trainer.nnodes="${NNODES}" \
trainer.val_before_train=False \
trainer.save_freq=100 \
trainer.test_freq=-1 \
trainer.val_before_train=False \
trainer.total_epochs=10 \
trainer.default_local_dir="${CKPTS_DIR}" \
trainer.resume_mode=auto \
trainer.balance_batch=True \
actor_rollout_ref.rollout.enforce_eager=False \
actor_rollout_ref.actor.use_torch_compile=False \
actor_rollout_ref.ref.use_torch_compile=False \
trainer.device=npu 2>&1 | tee "logs/verl_qwen3_32b_$(date +%Y%m%d_%H%M).log"
- MODEL_PATH:修改为 Qwen3-32B 模型路径。
- CKPTS_DIT:修改为保存模型权重路径。
- TRAIN_FILE:修改为训练数据集本地路径。
- TEST_FILE:修改为验证数据集本地路径。
主节点与从节点(共四个节点)都执行以下命令,即可启动镜像中提供的 DAPO 训练流程。
cd /home/verl
bash ray_start.sh当需要重新启动训练时,可以先通过以下命令关闭 ray 的进程。
ray stop --force4.6 分析训练日志
在训练模型的过程中,可以通过输出的日志,分析模型的训练过程。输出日志的格式如下所示。关于日志指标,更详细的内容请看5.2节的日志内容解析。
step:1 - global_seqlen/min:33676 - global_seqlen/max:326471 - global_seqlen/minmax_diff:292795 - global_seqlen/balanced_min:120597 - global_seqlen/balanced_max:120614 - global_seqlen/mean:120608.5 - actor/entropy:0.31534484028816223 - actor/pg_loss:0.06752552084314327 - actor/pg_clipfrac:9.875712657958502e-05 - actor/ppo_kl:-3.158243763815941e-05 - actor/pg_clipfrac_lower:0.0 - actor/grad_norm:0.04106094129383564 - perf/mfu/actor:0.0 - perf/max_memory_allocated_gb:60.253610610961914 - perf/max_memory_reserved_gb:79.15234375 - perf/cpu_memory_used_gb:533.7807006835938 - actor/lr:0.0 - critic/score/mean:-0.4141845703125 - critic/score/max:1.0 - critic/score/min:-2.0 - critic/rewards/mean:-0.4141845703125 - critic/rewards/max:1.0 - critic/rewards/min:-2.0 - critic/advantages/mean:-0.058039188385009766 - critic/advantages/max:3.7499923706054688 - critic/advantages/min:-2.6211581230163574 - critic/returns/mean:-0.058039188385009766 - critic/returns/max:3.7499923706054688 - critic/returns/min:-2.6211581230163574 - response_length/mean:7385.140625 - response_length/max:20480.0 - response_length/min:1190.0 - response_length/clip_ratio:0.0390625 - prompt_length/mean:152.890625 - prompt_length/max:606.0 - prompt_length/min:75.0 - prompt_length/clip_ratio:0.0 - timing_s/start_profile:0.0003245100001549872 - timing_s/generate_sequences:1081.803466796875 - timing_s/reshard:8.664657592773438 - timing_s/gen:1315.2451746799998 - timing_s/reward:2.444457609999972 - timing_s/old_log_prob:116.21133135000036 - timing_s/adv:0.07527759000004153 - timing_s/update_actor:303.92059642999993 - timing_s/step:1738.0274970199998 - timing_s/stop_profile:0.00010573000008662348 - timing_per_token_ms/gen:0.17391938503519522 - timing_per_token_ms/adv:9.75231715634179e-06 - timing_per_token_ms/update_actor:0.03937333868855635 - perf/total_num_tokens:7718944 - perf/time_per_step:1738.0274970199998 - perf/throughput:69.3938963605546 - train/num_gen_batches:1在理想情况下,critic/rewards/mean 指标大体趋势应随着训练的进行逐步提升(允许在一定范围内有些许波动)。
如下图所示,当 critic/rewards/mean 指标整体呈现上升趋势,说明训练正常。

4.7 FSDP 模型合并
在之前的模型训练中,使用了 FSDP 后端训练模型,因此需要将各个卡上碎片化的权重,重新合并为完整的模型权重。执行以下命令,即可调用 veRL 仓库中提供的 FSDP 模型合并脚本,获取完整的模型权重。(多机训练的情况,需要先将多个节点上的模型权重汇总到一个节点上的同一个目录下。)
python -m verl.model_merger merge \
--backend fsdp \
--local_dir /path/to/actor \
--target_dir /path/to/qwen3-32b-after-rl- backend:训练后端,选择 fsdp。
- local_dir:FSDP 模型权重本地路径,可以在训练配置的保存路径中找到。
- target_dir:合并后模型权重本地保存路径。
4.8 AISBenchmark 评估模型
跑通以下模型评测,单机 A3 * 16卡即可。
4.8.1 下载 Math 数据集
cd /home/benchmark/ais_bench/datasets
wget http://opencompass.oss-cn-shanghai.aliyuncs.com/datasets/data/math.zip
unzip math.zip
rm math.zip
在 /home/benchmark/ais_bench/datasets 目录下执行 tree aime/ 查看目录结构,若目录结构如下所示,则说明数据集部署成功。
math
├── convert_jsonl2json.py
├── math.json
├── test.jsonl
├── test_prm800k_500.json # MATH500
├── test_prm800k_500.jsonl # MATH500
└── train.jsonl
4.8.2 修改 vLLM 推理配置
打开 /home/benchmark/ais_bench/benchmark/configs/models/vllm_api/vllm_api_general.py 文件,进行如下修改。
from ais_bench.benchmark.models import VLLMCustomAPI
models = [
dict(
attr="service",
type=VLLMCustomAPI,
abbr='vllm-api-general',
path="/path/to/Qwen3-32B",
model="qwen3-32b",
max_seq_len = 2048,
request_rate = 0,
rpm_verbose = False,
retry = 2,
host_ip = "localhost", # 推理服务的IP
host_port = 6380, # 推理服务的端口
enable_ssl = False,
max_out_len = 20480, # 最大输出tokens长度
batch_size=48, # 推理的最大并发数
generation_kwargs = dict( # 后处理参数参考https://docs.vllm.ai/en/latest/api/inference_params.html#sampling-params 中的Parameters
temperature = 0,
seed = 1234,
)
)
]- path:修改为 Qwen3-32B 模型路径。
4.8.3 启动 vLLM 推理服务
创建 vllm_server.sh,用于启动 vLLM 服务:
# NPU服务端
python -m vllm.entrypoints.openai.api_server \
--model="/修改为模型对应路径/Qwen3-32B" \
--served-model-name qwen3-32b \
--gpu-memory-utilization 0.9 \
--max-num-seqs 24 \
--max-model-len 22528 \
--max-num-batched-tokens 22528 \
--enforce-eager \
--trust-remote-code \
--distributed_executor_backend=mp \
--tensor-parallel-size 4 \
--data-parallel-size 4 \
--generation-config vllm \
--port 6380
- model:修改为 Qwen3-32B 模型路径。
执行以下命令,即可启动 vLLM 服务。
# 运行脚本
bash vllm_server.sh- 不要在 /home 目录下执行该脚本,会因为路径问题报错。
如下所示,则 vllm 服务启动成功。

4.8.4 启动评测
另起一个新窗口,执行以下命令进入容器:
docker exec -it container_id bash- container_id:修改为对应容器 id,可以通过docker ps命令查看正在运行的容器 id。
切换为 aisbench_env 环境,并启动评测:
conda activate aisbench_env
source /usr/local/Ascend/ascend-toolkit/set_env.sh
source /usr/local/Ascend/nnal/atb/set_env.sh
ais_bench --models vllm_api_general --datasets math_prm800k_500_0shot_cot_gen
4.8.5 评测结果
训练前:

训练后:

- 经过 DAPO 训练后模型在 Math 数据集上的准确率提升了3.6%。
- 可以在 /home/benchmark/outputs/default/ 目录下查看推理结果和评估结果。
5. 训练参数及日志结果解析
5.1 训练参数
以下将对训练配置文件中的参数进行详细的解释,更多内容请参考官方文档中的配置说明。
参数名称 | 参数说明 | 默认值 |
data.train_files | 训练数据集本地路径。 | ~/data/rlhf/gsm8k/train.parquet |
data.val_files | 验证数据集本地路径。 | ~/data/rlhf/gsm8k/test.parquet |
data.prompt_key | 数据集中提示信息所在的字段。 | prompt |
data.truncation | 若输入的 input_ids 或提示词长度超过了 max_prompt_length,则将其截断。默认值为 error,表示不允许超过 max_prompt_length。如果抛出该错误,用户可以选择增加 max_prompt_length 的值,或设置为 left、right 和 middle,表示在哪侧进行截断。 | error |
data.max_prompt_length | 最大提示词长度,所有提示词都将左对齐填充至该长度。 | 512 |
data.max_response_length | 最大推理长度,rollout 生成的推理长度最多可达此值。 | 512 |
data.gen_batch_size | 训练器将重复使用 gen_batch_size 进行采样,直到获得足够数量的合格组以匹配 train_batch_size,或者达到 max_num_gen_batches 指定的上限。 | ${data.train_batch_size} |
data.train_batch_size | 在一次训练迭代中采样的批量大小。 | 1024 |
actor_rollout_ref.rollout.n | 对于每个提示,采样 n 个 response(即采样次数)。当使用 grpo 和 rloo 算法时,需将其设置为大于1的值。 | 1 |
algorithm.adv_estimator | 优势估计方法。 | gae |
algorithm.use_kl_in_reward | 是否启用奖励中的 kl penalty。 | False |
algorithm.kl_ctrl.kl_coef | 奖励内 kl_penalty 的(初始)系数。 | 0.001 |
actor_rollout_ref.actor.use_kl_loss | 设置为 True 时,在 Actor 中应用 KL 散度,而不是在奖励函数中应用 KL 散度。 | False |
actor_rollout_ref.actor.kl_loss_coef | kl 损失系数。 | 0.001 |
actor_rollout_ref.actor.clip_ratio_low | 非对称裁剪的下界(用于 dual-clip PPO)。 | 0.2 |
actor_rollout_ref.actor.clip_ratio_high | 非对称裁剪的上界(用于 dual-clip PPO)。 | 0.2 |
actor_rollout_ref.actor.clip_ratio_c | Dual-clip PPO 中的常数 C。 | 3.0 |
algorithm.filter_groups.enable | 是否使用样本过滤,若为 True,则过滤所有生成结果全对或全错的样本。 | False |
algorithm.filter_groups.max_num_gen_batches | 最大采样次数。 | 0 |
algorithm.filter_groups.metric | 样本过滤标准。 | null |
actor_rollout_ref.model.use_remove_padding | 是否在模型中使用移除填充功能。如果设置为True,模型将移除input_ids和response_ids中的填充标记。这有助于显著提高模型的运行效率。 | False |
actor_rollout_ref.actor.use_dynamic_bsz | 是否启用 Dynamic Batch Size,当此项为 True 时,系统会忽略基于样本数的 micro_batch_size_per_gpu 参数,转而使用基于 Token 数的 max_token_len_per_gpu 参数来构建 batch。 | False |
actor_rollout_ref.ref.log_prob_use_dynamic_bsz | 是否启用 Dynamic Batch Size。 | False |
actor_rollout_ref.rollout.log_prob_use_dynamic_bsz | 是否启用 Dynamic Batch Size。 | False |
actor_rollout_ref.actor.ppo_max_token_len_per_gpu | ppo_micro_batch_size_per_gpu 的替代方案,与 use_dynamic_bsz 配合使用。系统会自动打包样本,直到总 Token 量(prompt_len + response_len)接近这个阈值,形成一个动态的 micro batch size,从而稳定计算效率;无论长短样本,每个微批次的计算量都相对恒定。 | 16384 |
actor_rollout_ref.ref.log_prob_max_token_len_per_gpu | 一个 micro-batch 的 token 最大数量。 | 16384 |
actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu | 一个 micro-batch 的 token 最大数量。 | 16384 |
actor_rollout_ref.model.path | 模型路径,可以是本地路径或HDFS路径。 | ~/models/deepseek-llm-7b-chat |
actor_rollout_ref.model.enable_gradient_checkpointing | 仅限于FSDP,是否使能梯度检查点功能。 | True |
actor_rollout_ref.actor.optim.lr |
| 1e-6 |
actor_rollout_ref.actor.optim.lr_warmup_steps | 学习率预热步数。 | -1 |
actor_rollout_ref.actor.optim.weight_decay | 权重衰减系数,控制训练过程中对权重施加的 L2 正则化的强度。 | 0.01 |
actor_rollout_ref.actor.ppo_mini_batch_size | 一个样本被分割成多个子批次,每个子批次的批量大小。 | 256 |
actor_rollout_ref.actor.fsdp_config.param_offload | 仅限于 FSDP,是否卸载 actor 权重。 | False |
actor_rollout_ref.actor.fsdp_config.optimizer_offload | 仅限于 FSDP,是否卸载 actor 优化器参数。 | False |
actor_rollout_ref.actor.entropy_coeff | 计算 PPO 损失时的熵权重。 | 0 |
actor_rollout_ref.actor.grad_clip | actor 更新的梯度裁剪。 | 1.0 |
actor_rollout_ref.rollout.name | 使用的推理后端。 | 无 |
actor_rollout_ref.actor.loss_agg_mode | 损失聚合模式:token-mean, seq-mean-token-sum, 或 seq-mean-token-mean。 | token-mean |
actor_rollout_ref.actor.ulysses_sequence_parallel_size | 序列并行大小。 | 1 |
actor_rollout_ref.rollout.gpu_memory_utilization | 用于 vllm 实例的 GPU 总内存比例。 | 0.5 |
actor_rollout_ref.rollout.tensor_model_parallel_size | 张量并行数,只针对 vllm 有效。 | 2 |
actor_rollout_ref.rollout.enable_chunked_prefill | 是否启用 chunked prefill,对于非常长的 prompt,可以将其分块处理,减少显存峰值,但是降低吞吐量。 | True |
actor_rollout_ref.rollout.max_num_batched_tokens | 调用 vllm 生成 rollout 时,一个 batch 里所有 prompt + 已生成 token 的总数上限。 | 8192 |
actor_rollout_ref.rollout.temperature | temperature 值越高,概率分布越平滑,生成结果更多样、更随机;值越低,分布越尖锐,生成结果更倾向于高概率词元,更确定、更保守。 | 1.0 |
actor_rollout_ref.rollout.top_p | 从概率最高的 token 开始累加,直到它们的总概率达到 p,然后从这个 nucleus token 集合中进行采样。是一种动态选择采样范围的方法。top_p=1.0 表示不限制。 | 1 |
actor_rollout_ref.rollout.top_k | 在每一步生成时,只考虑概率最高的 k 个 token 进行采样。 | -1 |
actor_rollout_ref.ref.fsdp_config.param_offload | FSDP 中是否卸载参数。 | False |
actor_rollout_ref.ref.ulysses_sequence_parallel_size | 序列并行大小。 | 1 |
actor_rollout_ref.actor.fsdp_config.fsdp_size | 每个 FSDP 分片组中的 GPU 数量;-1 表示自动。 | -1 |
actor_rollout_ref.ref.strategy | reference 模型的配置 | ${actor_rollout_ref.actor.strategy} |
actor_rollout_ref.actor.strategy | actor 模型的配置 | 无 |
reward_model.reward_manager | 定义计算基于规则的奖励和处理不同奖励源的机制。 | naive |
reward_model.overlong_buffer.enable | 将 overlong_buffer.enable 设置为 True 将会对那些长度过长但仍未超出硬上下文限制的输出进行惩罚。 | False |
reward_model.overlong_buffer.penalty_factor reward_model.overlong_buffer.len | 当输出的长度超过 max_response_length - overlong_buffer.len 时,惩罚值会从0线性增加到overlong_buffer.penalty_factor ,增加的幅度取决于超出部分的长度,即0到 overlong_buffer.len 个 token。 | 0.0 0 |
trainer.logger | 支持 console、WandB、SwanLab、MLFlow、TensorBoard 和 TrackIO。 | ["console", "wandb"] |
trainer.project_name | 项目名称。 | verl_examples |
trainer.experiment_name | 实验名称。 | gsm8k |
trainer.n_gpus_per_node | 每个节点的 NPU 数量。 | 8 |
trainer.nnodes | 训练中使用的节点数量。 | 1 |
trainer.val_before_train | 是否在开始训练前启动验证。 | True |
trainer.save_freq | 保存 actor 和 critic 模型检查点的频率(按迭代次数)。 | -1 |
trainer.test_freq 是否在训练开始前进行验证。 | 验证频率(按迭代次数)。 | -1 |
trainer.total_epochs | 总训练轮数。 | 30 |
trainer.default_local_dir | 用于保存检查点的默认本地目录。 | checkpoints/${trainer.project_name}/${trainer.experiment_name} |
trainer.resume_mode | 恢复模式:auto, disable, 或 resume_path。 | auto |
trainer.balance_batch | 是否在分布式工作节点间平衡批次大小。 | True |
actor_rollout_ref.rollout.enforce_eager | 是否开启图模式。 | False |
actor_rollout_ref.actor.use_torch_compile | 是否使用 torch.compile。 | True |
tainer.device | 设备型号。 | cuda |
5.2 日志内容解析
以下将对日志中的指标进行详细的解释。
指标名称 | 指标说明 |
global_seqlen/(max/min/mean) | 均衡前整个训练批次中原始序列长度的分布情况。 |
global_seqlen/(balanced_max/balanced_min) | 均衡后分配给 worker 的最大和最小的负载情况,“负载”指的是每个 worker 所分到的所有序列的长度之和。 |
global_seqlen/minmax_diff | balanced_max - balanced_min 。越小说明不同 worker 之间的负载差距越小,训练效率也就越高。 |
actor/entropy | 策略分布的熵,衡量了策略的随机性或探索程度。 |
actor/kl_loss | KL 散度惩罚项的损失值。 |
actor/kl_coef | KL 惩罚项的系数。可以是固定的,也可以是自适应调整的。自适应 kl_coef 会根据 ppo_kl 的大小动态调整,以使其保持在目标范围内。 |
actor/pg_loss | Policy Gradient Loss,Actor 的核心损失值。 |
actor/pg_clipfrac | 在一个训练批次中,有多少比例的样本的概率比率超出了裁剪范围而被裁剪。 |
actor/ppo_kl | 新旧策略之间的 KL 散度。 |
actor/pg_clipfrac_lower | 是 pg_clipfrac 的一个细分,表示有多少比例的样本是因为概率比率小于而被裁剪。 |
actor/grad_norm | Actor 模型在反向传播后计算出的梯度的L2范数。 |
actor/lr | Actor 的学习率。 |
perf/mfu/actor | 模型浮点运算利用率, 衡量了 NPU 的实际计算效率。 |
perf/max_memory_allocated_gb | 训练过程中 NPU 显存「已分配」的峰值。 |
perf/max_memory_reserved_gb | 训练过程中 NPU 显存「已保留」的峰值。 |
perf/cpu_memory_used_gb | 进程所使用的 CPU 内存量。 |
training/global_step | 当前训练的步数。 |
training/epoch | 当前训练的轮数。 |
critic/score/(mean,max,min) | 从奖励函数或奖励模型中获得的原始、未经修改的奖励分数,是计算最终 rewards 的基础。 |
critic/rewards/(mean,max,min) | 最终用于计算优势函数的奖励信号。它由奖励模型打出的原始分数(score)经过可选的 KL 惩罚项调整后得到。公式可以理解为:rewards = scores - β * kl_divergence。 |
critic/advantages/(mean,max,min) | 优势函数,它衡量了在状态 s 下采取动作 a 相对于平均水平有多好。 |
critic/returns/(mean,max,min) | 对未来奖励的折扣累加和。 |
response_length/(mean,max,min) | 在当前批次中,response 的长度分布情况。 |
response_length/clip_ratio | 在当前批次中,有多少比例的 response 因为长度超过了预设的最大长度而被截断。 |
response_length_non_aborted/(mean,max,min) | 在当前批次中,未被异常中断的样本的 response 的长度分布情况。 |
response_length_non_aborted/clip_ratio | 在当前批次中,未被异常中断的样本中有多少比例的 response 因为长度超过了预设的最大长度而被截断。 |
response/aborted_ratio | 在当前批次中,被异常中断样本的比例。 |
prompt_length/(mean,max,min) | 在当前批次中,prompt 的长度分布情况。 |
prompt_length/clip_ratio | 在当前批次中,有多少比例的 prompt 因为长度超过了预设的最大长度而被截断。 |
timing_s/start_profile | 开始性能分析操作的耗时。 |
timing_s/generate_sequences | 纯模型推理阶段的耗时。 |
timing_s/reshard | 模型重新切片的耗时。 |
timing_s/generation_timing/max | 同一个 rollout 批次里,最慢一条样本的生成耗时。 |
timing_s/generation_timing/min | 同一个 rollout 批次里,最快一条样本的生成耗时。 |
timing_s/generation_timing/topk_ratio | 表示(前 k% 最长样本的耗时之和)/ (批次总耗时)。 |
timing_s/gen | 端到端的生成耗时,也是训练日志里最常被拿来做“每步 rollout 多快” 的宏观指标。 |
timing_s/reward | reward 计算阶段的耗时。 |
timing_s/old_log_prob | 计算 old policy 对数概率的耗时。 |
timing_s/ref | 计算 reference policy 的对数概率的耗时。 |
timing_s/adv | 优势估计阶段的耗时。 |
timing_s/update_actor | 策略更新阶段的耗时。 |
timing_s/step | 当前step总耗时。 |
timing_s/stop_profile | 停止性能分析操作的耗时。 |
timing_per_token_ms/gen | 在 rollout 阶段,生成一个 token 平均需要花费的毫秒数,是衡量推理性能的核心指标。 |
timing_per_token_ms/update_actor | 更新 Actor 模型 时,处理一个 token 平均需要花费的毫秒数。 |
timing_per_token_ms/ref | 计算 reference policy 的对数概率时,处理一个 token 平均需要花费的毫秒数。 |
timing_per_token_ms/adv | 计算优势函数(advantage) 时,处理一个 token 平均需要花费的毫秒数。 |
perf/total_num_tokens | 在当前训练步骤中处理的 token 总数。这包括了批次中所有样本的 prompt 和 response 的有效 token 数量。 |
perf/time_per_step | 完成一个完整训练步骤所花费的时间,单位为秒。这个时间覆盖了从数据生成、奖励计算、优势估计到 Actor 和 Critic 模型更新的全过程。 |
perf/throughput | 端到端的吞吐量。 |
train/num_gen_batches | rollout 阶段的推理次数,最多不会超过 data.gen_batch_size 。 |
6. 性能调优指南
针对如何优化 veRL 中各阶段的性能,有以下几种优化思路,更详细的内容请参考官方文档中的性能调优指南。
参数名称 | 参数说明 |
gpu_memory_utilization | 对于 vLLM v0.7.0 及更高版本,vLLM 实例只会使用总内存中的gpu_memory_utilization 部分。通常,0.5 到 0.7 之间的值可以在高吞吐量和避免内存不足之间取得良好的平衡。 |
max_num_seqs 或 max_num_batched_tokens | 如果日志中显示 NPU 缓存利用率相对较低,增大该参数,可以扩大解码阶段的有效批处理大小,从而允许每个批处理中处理更多的并发请求。建议将 max_num_batched_tokens 设置为大于 2048,以获得更高的吞吐量。 |
tensor_parallel_size | 当 NPU 资源允许时,较小的张量并行大小可以生成更多的 vLLM 副本。数据并行(DP)相比张量并行(TP)可以获得更高的吞吐量,但也会增加 kv 缓存的消耗。 |
use_remove_padding | 通过设置为 True 来使用 Transformers 库提供的序列打包实现来提高性能。 |
enable_gradient_checkpointing | 通过设置为 True 启用梯度检查点,可以有效降低显存占用,允许更大的 micro_batch_size。 |
actor_rolloput_ref.ref.log_prob_micro_batch_size_per_gpu, actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu, critic.forward_micro_batch_size_per_gpu, actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu, critic.ppo_micro_batch_size_per_gpu | 仅用于前向传播的参数,可以比训练相关的微批量大小更大(例如,2倍) |
actor_rollout_ref.model.enable_activation_offload, critic.model.enable_activation_offload | 通过设置为 True 启用激活值卸载,可以有效降低显存占用,允许更大的 micro_batch_size。 |
use_dynamic_bsz | 通过设置为 True 来使用动态批次大小,允许模型在单次前向传播中处理相似数量的token,可以显著提高训练效率并减少显存占用。 |
ulysses_sequence_parallel_size | 通过设置序列并行,可以有效降低显存占用,支持训练长序列。 |
fsdp_config.forward_prefetch | 通过设置为 True 启用 FSDP 中的前向预取功能,会在当前前向计算之前预取下一层的模型权重,从而将通信与计算重叠,提高效率。 |
7. 扩展内容
完整跑通本章节需1台 A3 * 8卡。
本章节将介绍如何对以上的模型训练流程做更多的自定义修改,内容如下。
- 模型修改、数据集修改、自定义 reward 函数修改
- 算法修改
7.1 自定义训练模型及数据
7.1.1 模型修改
训练配置文件中的 actor_rollout_ref.model.path 参数代表 Actor 模型权重路径,修改该参数,即可更换 Actor 模型。同理,若需要更换 Reward 和 Critic 模型权重,分别修改 reward_model.model.path 和 critic.model.path 即可。
actor_rollout_ref.model.path=/path/to/model
# reward_model.model.path=/path/to/model
# critic.model.path=/path/to/model- 在7.1.4节中,已提供对应配置文件,不需要手动添加。
本节以 DianJin-R1-Data 金融领域数据集作为演示,详细介绍数据集准备流程。
7.1.2 数据集修改
执行以下命令,即可从 ModelScope 下载 DianJin-R1-Data 数据集。
git clone https://www.modelscope.cn/datasets/tongyi_dianjin/DianJin-R1-Data.git原始数据集样例:
{
"instruction": "假设你是一位金融行业专家,请回答下列问题。\n注意:题目是单选题,只需要返回一个最合适的选项,若有多个合适的答案,只返回最准确的即可。\n\n甲公司为制造企业,20×4年发生现金流量:(1)将销售产生的应收账款申请保理,取得现金1200万元,银行对于标的债券具有追索权;(2)购入作为交易性金融资产核算的股票支付现金200万元;(3)收到保险公司对存货损毁的赔偿款120万元;(4)收到所得税返还款260万元;(5)向其他方提供劳务收取现金400万元。不考虑其他因素,甲公司20×4年经营活动产生的现金流量净额是( )。\nA. 1980万元\nB. 980万元\nC. 2180万元\nD. 780万元\n\n请一步步思考,并把答案选项放到\\boxed{}中,如\\boxed{A}。\n",
"output": "<think>\n好,我现在要解决这个关于甲公司20×4年经营活动现金流量净额的问题。题目给出了五个现金流量的事项,我需要逐一分析每个事项属于经营活动、投资活动还是筹资活动,然后计算经营活动的净额。选项是四个可能的答案,我需要选择正确的一个。\n\n首先,我得回忆一下现金流量表中各个活动的分类标准。一般来说,经营活动涉及企业日常的运营,比如销售商品、提供劳务、收到的税费返还等。投资活动涉及长期资产的买卖和投资,比如购买固定资产、交易性金融资产等。筹资活动则和企业的资本结构有关,比如借款、还款、股东投资等。\n\n现在来看题目中的五个事项:\n\n1. 将销售产生的应收账款申请保理,取得现金1200万元,银行有追索权。这里的关键是保理业务如何处理。如果有追索权的保理,按照会计准则,这应该视为一种融资行为,也就是属于筹资活动,而不是经营活动。因为企业并没有完全转移应收账款的风险和报酬,所以收到的现金属于筹资活动的现金流入。那这个1200万元应该计入筹资活动的现金流量,而不是经营活动。\n\n2. 购入作为交易性金融资产核算的股票支付现金200万元。交易性金融资产通常属于短期投资,而根据现金流量表的规定,购买这类金融资产属于投资活动的现金流出。所以这200万属于投资活动,不影响经营活动的净额。\n\n3. 收到保险公司对存货损毁的赔偿款120万元。存货相关的赔偿应该和日常经营相关,所以这部分赔偿款属于经营活动的现金流入。因此,这120万应计入经营活动。\n\n4. 收到所得税返还款260万元。税费返还,无论是所得税还是其他税种的返还,都属于经营活动现金流量的一部分。所以这260万属于经营活动的流入。\n\n5. 向其他方提供劳务收取现金400万元。这显然是经营活动,因为提供劳务是公司的主营业务之一,所以这400万应计入经营活动。\n\n现在,把所有属于经营活动的部分加起来:第3项的120万,第4项的260万,第5项的400万。这三个数相加,120+260=380,380+400=780万元。那么经营活动产生的现金流量净额应该是780万,对应的选项是D选项。\n\n不过,我需要再仔细核对第一个事项的处理是否正确。因为保理业务是否有追索权会影响分类。如果是有追索权的保理,按照会计准则,企业并没有终止确认应收账款,因此这笔交易应视为以应收账款为质押的借款,属于筹资活动。这样处理的话,这1200万不属于经营活动,而属于筹资活动的现金流入。所以之前的分析是正确的,这个金额不计入经营活动,所以经营活动的净额确实是三个项的总和,即780万元。所以正确选项是D选项。\n</think>\n<answer>\n甲公司20×4年经营活动产生的现金流量净额需通过分析各事项的现金流量分类确定: \n1. **应收账款保理(1200万元)**:由于银行具有追索权,该交易实质为融资行为,属于**筹资活动**,不计入经营活动。 \n2. **购入交易性金融资产(200万元)**:购买短期金融资产属于**投资活动**,不影响经营活动。 \n3. **存货损毁赔偿(120万元)**:与日常经营相关,属于**经营活动**现金流入。 \n4. **所得税返还款(260万元)**:税费返还属于**经营活动**现金流入。 \n5. **提供劳务收款(400万元)**:主营业务收入属于**经营活动**现金流入。 \n\n**经营活动现金流量净额** = 120 + 260 + 400 = **780万元**。 \n\n答案选项为\\boxed{D}。\n</answer>"
}
每个样本由 instruction 和 output 两个部分组成,其中 instruction 是问题内容,output 中则包括思考过程及答案,答案放在最后的 \\boxed{} 里,例如,\\boxed{D} 。
创建 data_preprocess.py 文件,用于做数据集预处理。预处理主要是将数据集中的最终答案抽取出来,用于训练过程中的 reward 计算。
import argparse
import os
import re
import datasets
def extract_solution(solution_str):
box_pattern = r"\\boxed\{(.*?)\}"
answer_pattern = r"<answer>(.*?)</answer>"
answer_matches = re.findall(answer_pattern, solution_str, re.DOTALL)
box_matches = re.findall(box_pattern, answer_matches[0], re.DOTALL)
choices = "ABCDEFG"
answer = ""
for c in choices:
if c in box_matches:
answer += c
return answer
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--dataset_file")
parser.add_argument("--save_dir")
args = parser.parse_args()
data_source = args.dataset_file
local_dir = args.save_dir
dataset = datasets.load_dataset("json", data_files=data_source)
train_dataset = dataset["train"]
def make_map_fn(split):
def process_fn(example, idx):
question= example.pop("instruction")
answer_raw = example.pop("output")
solution = extract_solution(answer_raw)
data = {
"data_source": data_source,
"prompt": [
{
"role": "user",
"content": question,
}
],
"ability": "finantial",
"reward_model": {"style": "rule", "ground_truth": solution},
"extra_info": {
"split": split,
"index": idx,
"answer": answer_raw,
"question": question,
},
}
return data
return process_fn
train_dataset = train_dataset.map(function=make_map_fn("train"), with_indices=True)
train_dataset.to_parquet(os.path.join(local_dir, "train.parquet"))
执行以下命令,即可将数据集转换为 veRL 适配的格式。
python data_preprocess.py --dataset_file /path/to/DianJin-R1-Data/data/train/cflue_mcq.json --save_dir /path/to/save_dir- dataset_file:下载到本地的 DianJin-R1-Data 原始数据集文件中 cflue_mcq.json 文件的路径。
- save_dir:数据集保存路径。
预处理后数据集样例:
{
"data_source": "./cflue_mcq.json",
"prompt": [
{
"content": "假设你是一位金融行业专家,请回答下列问题。\n注意:题目是单选题,只需要返回一个最合适的选项,若有多个合适的答案,只返回最准确的即可。\n\n甲公司为制造企业,20×4年发生现金流量:(1)将销售产生的应收账款申请保理,取得现金1200万元,银行对于标的债券具有追索权;(2)购入作为交易性金融资产核算的股票支付现金200万元;(3)收到保险公司对存货损毁的赔偿款120万元;(4)收到所得税返还款260万元;(5)向其他方提供劳务收取现金400万元。不考虑其他因素,甲公司20×4年经营活动产生的现金流量净额是( )。\nA. 1980万元\nB. 980万元\nC. 2180万元\nD. 780万元\n\n请一步步思考,并把答案选项放到\\boxed{}中,如\\boxed{A}。\n",
"role": "user"
}
],
"ability": "finantial",
"reward_model": {
"ground_truth": "D",
"style": "rule"
},
"extra_info": {
"answer": "<think>\n好,我现在要解决这个关于甲公司20×4年经营活动现金流量净额的问题。题目给出了五个现金流量的事项,我需要逐一分析每个事项属于经营活动、投资活动还是筹资活动,然后计算经营活动的净额。选项是四个可能的答案,我需要选择正确的一个。\n\n首先,我得回忆一下现金流量表中各个活动的分类标准。一般来说,经营活动涉及企业日常的运营,比如销售商品、提供劳务、收到的税费返还等。投资活动涉及长期资产的买卖和投资,比如购买固定资产、交易性金融资产等。筹资活动则和企业的资本结构有关,比如借款、还款、股东投资等。\n\n现在来看题目中的五个事项:\n\n1. 将销售产生的应收账款申请保理,取得现金1200万元,银行有追索权。这里的关键是保理业务如何处理。如果有追索权的保理,按照会计准则,这应该视为一种融资行为,也就是属于筹资活动,而不是经营活动。因为企业并没有完全转移应收账款的风险和报酬,所以收到的现金属于筹资活动的现金流入。那这个1200万元应该计入筹资活动的现金流量,而不是经营活动。\n\n2. 购入作为交易性金融资产核算的股票支付现金200万元。交易性金融资产通常属于短期投资,而根据现金流量表的规定,购买这类金融资产属于投资活动的现金流出。所以这200万属于投资活动,不影响经营活动的净额。\n\n3. 收到保险公司对存货损毁的赔偿款120万元。存货相关的赔偿应该和日常经营相关,所以这部分赔偿款属于经营活动的现金流入。因此,这120万应计入经营活动。\n\n4. 收到所得税返还款260万元。税费返还,无论是所得税还是其他税种的返还,都属于经营活动现金流量的一部分。所以这260万属于经营活动的流入。\n\n5. 向其他方提供劳务收取现金400万元。这显然是经营活动,因为提供劳务是公司的主营业务之一,所以这400万应计入经营活动。\n\n现在,把所有属于经营活动的部分加起来:第3项的120万,第4项的260万,第5项的400万。这三个数相加,120+260=380,380+400=780万元。那么经营活动产生的现金流量净额应该是780万,对应的选项是D选项。\n\n不过,我需要再仔细核对第一个事项的处理是否正确。因为保理业务是否有追索权会影响分类。如果是有追索权的保理,按照会计准则,企业并没有终止确认应收账款,因此这笔交易应视为以应收账款为质押的借款,属于筹资活动。这样处理的话,这1200万不属于经营活动,而属于筹资活动的现金流入。所以之前的分析是正确的,这个金额不计入经营活动,所以经营活动的净额确实是三个项的总和,即780万元。所以正确选项是D选项。\n</think>\n<answer>\n甲公司20×4年经营活动产生的现金流量净额需通过分析各事项的现金流量分类确定: \n1. **应收账款保理(1200万元)**:由于银行具有追索权,该交易实质为融资行为,属于**筹资活动**,不计入经营活动。 \n2. **购入交易性金融资产(200万元)**:购买短期金融资产属于**投资活动**,不影响经营活动。 \n3. **存货损毁赔偿(120万元)**:与日常经营相关,属于**经营活动**现金流入。 \n4. **所得税返还款(260万元)**:税费返还属于**经营活动**现金流入。 \n5. **提供劳务收款(400万元)**:主营业务收入属于**经营活动**现金流入。 \n\n**经营活动现金流量净额** = 120 + 260 + 400 = **780万元**。 \n\n答案选项为\\boxed{D}。\n</answer>",
"index": 0,
"question": "假设你是一位金融行业专家,请回答下列问题。\n注意:题目是单选题,只需要返回一个最合适的选项,若有多个合适的答案,只返回最准确的即可。\n\n甲公司为制造企业,20×4年发生现金流量:(1)将销售产生的应收账款申请保理,取得现金1200万元,银行对于标的债券具有追索权;(2)购入作为交易性金融资产核算的股票支付现金200万元;(3)收到保险公司对存货损毁的赔偿款120万元;(4)收到所得税返还款260万元;(5)向其他方提供劳务收取现金400万元。不考虑其他因素,甲公司20×4年经营活动产生的现金流量净额是( )。\nA. 1980万元\nB. 980万元\nC. 2180万元\nD. 780万元\n\n请一步步思考,并把答案选项放到\\boxed{}中,如\\boxed{A}。\n",
"split": "train"
}
}
其中 ground_truth 是从 output 中抽取出的答案,是在下节中要讲到的 reward 函数的关键参数。
修改 data.train_files 参数,即可完成训练数据集的更换。若需要更换验证数据集,修改 data.val_files 参数即可。
data.train_files=$DATASET_PATH/train.parquet
data.val_files=$DATASET_PATH/val.parquet在7.1.4节中,已提供对应配置文件,不需要手动添加。
7.1.3 自定义 reward 函数
本节以上节得到的预处理后数据集作为参考,详细介绍自定义 reward 函数过程。
创建 reward_score.py 文件,用于提供自定义计算 reward 函数。
import re
def get_choices(text):
choices = "ABCDEFG"
answer = ""
for c in choices:
if c in text:
answer += c
return answer
def extract_answer(solution_str):
box_pattern = r"\\boxed\{(.*?)\}"
box_matches = re.findall(box_pattern, solution_str, re.DOTALL)
if len(box_matches) == 0:
return None
return get_choices(box_matches[-1])
def compute_score(data_source, solution_str, ground_truth, extra_info=None):
answer = extract_answer(solution_str)
if answer is None:
return 0.0
else:
gt = get_choices(ground_truth)
if answer == gt:
return 1.0
else:
return 0.0
- 其中 compute_score 是计算 reward 的核心函数,四个入参分别为 data_source,solution_str,ground_truth,extra_info。除去 solution_str,其余三个参数对应上节预处理后的数据集中的字段。在自定义 reward 函数时,大部分情况下只需要关注两个参数,solution_str 和 ground_truth。
- solution_str 为模型推理的结果,extract_answer 函数从中提取出最终答案,并与正确答案 ground_truth 进行比较。无论是单选题还是多选题当两者完全相同时,获得1分,否则漏选错选,获得0分。
- 在自定义 reward 方法时,至少要实现 compute_score 方法。
添加 custom_reward_function.path 和 custom_reward_function.name 两个参数,即可在训练过程中使用自定义的函数计算 reward。两个参数分别为 reward 函数文件路径和 reward 函数名称,reward 函数名称在此处即为compute_score。
custom_reward_function.path=$REWARD_PATH \
custom_reward_function.name=$REWARD_NAME \- 在7.1.4节中,已提供对应配置文件,不需要手动添加。
7.1.4 启动训练
基于以上修改,提供了示例配置文件,创建 run_qwen3-8b.sh 文件,进行如下修改。
set -x
export VLLM_USE_V1=1
export BASE_MODEL=/path/to/Qwen3-8B
export EXPERIMENT_NAME=Qwen3-8B-dapo
export REWARD_PATH=/path/to/reward_score.py
export REWARD_NAME=compute_score
export DATASET_DIR=/path/to/train.parquet
export SAVE_DIR=/path/to/checkpoints/$EXPERIMENT_NAME
python3 -m recipe.dapo.main_dapo \
algorithm.adv_estimator=grpo \
data.train_files=$DATASET_DIR \
data.val_files=$DATASET_DIR \
data.train_batch_size=128 \
data.max_prompt_length=1024 \
data.max_response_length=10000 \
data.filter_overlong_prompts=True \
data.truncation='error' \
data.shuffle=True \
actor_rollout_ref.actor.use_torch_compile=False \
actor_rollout_ref.model.path=$BASE_MODEL \
actor_rollout_ref.model.enable_gradient_checkpointing=True \
actor_rollout_ref.model.use_remove_padding=True \
actor_rollout_ref.actor.optim.lr=1e-6 \
actor_rollout_ref.actor.ppo_mini_batch_size=32 \
actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=2 \
actor_rollout_ref.actor.use_kl_loss=True \
actor_rollout_ref.actor.kl_loss_coef=0.001 \
actor_rollout_ref.actor.kl_loss_type=low_var_kl \
actor_rollout_ref.actor.entropy_coeff=0 \
actor_rollout_ref.actor.fsdp_config.param_offload=True \
actor_rollout_ref.actor.fsdp_config.optimizer_offload=True \
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=2 \
actor_rollout_ref.rollout.tensor_model_parallel_size=2 \
actor_rollout_ref.rollout.name=vllm \
actor_rollout_ref.rollout.max_num_batched_tokens=11024 \
actor_rollout_ref.rollout.gpu_memory_utilization=0.8 \
actor_rollout_ref.rollout.temperature=0.6 \
actor_rollout_ref.rollout.top_p=0.95 \
actor_rollout_ref.rollout.n=8 \
actor_rollout_ref.rollout.enforce_eager=False \
actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=2 \
actor_rollout_ref.ref.fsdp_config.param_offload=True \
algorithm.kl_ctrl.kl_coef=0.001 \
custom_reward_function.path=$REWARD_PATH \
custom_reward_function.name=$REWARD_NAME \
trainer.critic_warmup=0 \
trainer.logger=['console'] \
trainer.n_gpus_per_node=16 \
trainer.nnodes=1 \
trainer.save_freq=5 \
trainer.test_freq=-100 \
trainer.default_local_dir=$SAVE_DIR \
trainer.val_before_train=False \
trainer.device=npu \
trainer.max_actor_ckpt_to_keep=2 \
trainer.max_critic_ckpt_to_keep=2 \
trainer.total_epochs=5 \
data.gen_batch_size=256 \
actor_rollout_ref.actor.clip_ratio_low=0.2 \
actor_rollout_ref.actor.clip_ratio_high=0.28 \
actor_rollout_ref.actor.clip_ratio_c=10.0 \
algorithm.filter_groups.enable=True \
algorithm.filter_groups.max_num_gen_batches=5 \
algorithm.filter_groups.metric=acc \
actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
actor_rollout_ref.actor.optim.weight_decay=0.01 \
reward_model.overlong_buffer.enable=True \
reward_model.overlong_buffer.len=2048 \
reward_model.overlong_buffer.penalty_factor=1.0 \
reward_model.reward_manager=dapo
- BASE_MODEL:修改为 Qwen3-8B 模型本地路径。
- REWARD_PATH:修改为 reward 函数文件路径。
- DATASET_DIR:修改为经过预处理的数据集文件路径。
- SAVE_DIR:修改为模型保存路径。
执行以下命令,即可启动训练流程:
cd /home/verl
bash /path/to/run_qwen3-8b.shrewards曲线如下所示:

7.2 算法修改
当需要更换算法时,需要了解算法的特点,可以通过veRL官方文档及示例了解到不同算法的参数配置。本章节将算法从 DAPO 更换为 GRPO 举例说明。不同算法的训练配置示例可以参考 /home/verl/examples 目录。
DAPO 与 GRPO 的区别:
维度 | DAPO | GRPO |
采样策略 | 强制 n 条 response 里同时含正确与错误样本,否则整组丢弃重采。 | 对同一 prompt 随机采 n 条 response,无额外约束。 |
损失裁剪范围 | 提高剪切上限至 0.28,扩大低分区域探索空间;下限保持 0.2。 | PPO 式对称剪切 ε=0.2。 |
KL惩罚 | 直接去掉 KL 散度,允许分布大幅偏移,适配长 CoT 推理。 | 保留 KL 散度,防止偏离初始模型。 |
过长惩罚 | 线性长度塑形奖励,缓解“长但正确”样本被误惩罚的问题。 | 超过最大长度即截断并给 −1 奖励。 |
采样策略相关参数:
参数名称 | 参数说明 |
data.gen_batch_size | 训练器将重复使用 gen_batch_size 进行采样,直到获得足够数量的合格组以匹配 train_batch_size,或者达到 max_num_gen_batches 指定的上限。 |
alogorithm.filter_groups.enable | 是否使用样本过滤,若为 True,则过滤所有生成结果全对或全错的样本。 |
alogorithm.filter_groups.metric | 样本过滤标准。 |
alogorithm.filter_groups.max_num_batches | 最大采样次数。 |
- 以上参数是 DAPO 算法的采样策略相关参数,在 GRPO 算法中是没有的,因此删除相关参数即可。
损失裁剪相关参数:
参数名称 | 参数说明 |
actor_rollout_ref.actor.clip_ratio_low | 裁剪范围的下限,默认值为 0.2。 |
actor_rollout_ref.actor.clip_ratio_high | 裁剪范围的上限,默认值为 0.2。 |
- DAPO 算法的配置中一般会将裁剪范围的上限设置为 0.28,在 GRPO 中一般都为 0.2,因此删除相关参数即可。
KL惩罚相关参数:
参数名称 | 参数说明 |
actor_rollout_ref.actor.use_kl_loss | 是否启用kl惩罚 |
- DAPO 中一般不使用 KL 散度惩罚,而 GRPO 中为了防止偏离初始模型过多,因此需要设置该参数为 True 。
过长惩罚相关参数:
参数名称 | 参数说明 |
reward_model.overlong_buffer.enable | 将 overlong_buffer.enable 设置为 True 将会对那些长度过长但仍未超出硬上下文限制的输出进行惩罚。 |
reward_model.overlong_buffer.penalty_factor reward_model.overlong_buffer.len | 当输出的长度超过 max_response_length - overlong_buffer.len时,惩罚值会从0线性增加到overlong_buffer.penalty_factor,增加的幅度取决于超出部分的长度,即0到overlong_buffer.len个 token。 |
- 以上参数是 DAPO 算法的过长惩罚策略相关参数,在 GRPO 算法中是没有的,因此删除相关参数即可。
GRPO 算法的控制流与 DAPO 算法不同,当使用 GRPO 算法时入口文件为 verl.trainer.main_ppo。经过以上修改,可以将 DAPO 算法更换为 GRPO 算法启动模型训练。



