
强化学习训练框架这两年更新得很快从最早大家各自手写 PPO 训练循环到后来 DeepSpeed、Megatron 这些并行方案逐步成熟再到 verl 这种专门为 RL 后训练设计的框架出现整个链路的重心已经从把算法写对转移到了把工程跑通。verl 是字节跳动开源的一套强化学习训练框架核心定位是给大语言模型做 RLHF 和推理增强训练支持 PPO、GRPO 这类主流的策略优化算法同时把训练和推理的并行调度做了比较彻底的解耦。我最近花了两周时间从零把 verl 跑通中间踩了不少坑这篇文章就把整个流程拆开讲清楚包括环境准备、数据格式、配置项含义、单机多卡跑通、常见报错排查以及 PPO 和 GRPO 在配置上的差异。适合已经了解强化学习基本概念、想上手 verl 做实际训练的读者也适合被各种环境问题卡住、想找一份能直接抄的配置的人。1. 为什么值得花时间跑通 verl1.1 手写 PPO 训练循环的天花板在哪如果你自己写过 PPO 训练大模型的代码应该对那种痛苦有印象。一个完整的 PPO 训练循环里至少涉及四个模型角色Actor策略模型、Critic价值模型、Reward Model奖励模型、Reference Model参考模型。这四个模型在训练过程中对显存、并行策略、通信模式的要求完全不同。Actor 需要前向和反向Critic 需要前向和反向Reward Model 只做前向推理Reference Model 也只做前向推理。如果全部塞进同一套并行策略里显存直接爆炸如果拆开部署又要处理跨进程的权重同步和通信。手写代码最麻烦的地方在于推理阶段和训练阶段的并行方式往往不一致。推理时你可能用张量并行加流水并行训练时又换成 ZeRO 或者 FSDP两边的权重格式对不上每次更新都要做一次转换。这个转换逻辑写起来不难但调试起来极其痛苦尤其是当并行度不是 2 的幂次时各种边界情况能把人逼疯。verl 解决的核心问题就是这个。它把推理引擎和训练引擎分开管理推理侧可以挂 vLLM 或者 SGLang 这类高吞吐推理框架训练侧用 FSDP 或者 Megatron 做并行两边通过一套权重重分片机制做同步。你不需要自己写转换逻辑框架帮你处理了。1.2 verl 的架构分层与关键抽象verl 的代码结构大致分三层。最底层是并行执行层负责把模型切分到不同 GPU 上处理通信和显存管理。中间层是 RL 算法层实现了 PPO、GRPO 等算法的训练循环包括 rollout 生成、优势估计、策略更新这些步骤。最上层是配置和入口层通过 Hydra 管理配置用 YAML 文件描述整个训练任务。关键抽象有几个。Worker是执行单元每个 Worker 管理一部分模型参数。WorkerGroup是一组 Worker 的集合负责协调它们之间的通信。Rollout负责生成训练数据也就是让 Actor 模型和环境交互产生轨迹。AdvantageEstimator负责计算优势函数PPO 用 GAEGRPO 用组内归一化。这些抽象在配置文件里都有对应的字段理解它们之间的关系是读懂配置的前提。我一开始没搞清 Worker 和 WorkerGroup 的区别配置里看到actor_rollout_wg这种字段完全不知道在指什么。后来翻源码才明白wg就是 WorkerGroup 的缩写actor_rollout_wg指的是同时承担 Actor 和 Rollout 职责的那组 Worker。verl 里 Actor 和 Rollout 经常是同一组 Worker因为 rollout 本质上就是 Actor 做前向推理没必要单独开一组。1.3 和 DeepSpeed、Megatron 的关系verl 不是要替代 DeepSpeed 或 Megatron而是建在它们之上。训练侧的并行策略可以直接用 FSDP也可以用 Megatron-LM 的张量并行和流水并行。verl 做的是把这些并行方案封装成统一的接口让 RL 算法层不用关心底层用的是哪种并行。这个设计的好处是灵活。小规模训练用 FSDP 就够了配置简单显存利用率也不错。大规模训练再切到 Megatron虽然配置复杂但能撑住更大的模型。我这次跑通用的是 FSDP因为手头只有单机 8 卡FSDP 在这个规模下性价比最高。2. 环境准备里那些文档不会告诉你的细节2.1 硬件与驱动的最低要求verl 对硬件的要求取决于你要跑多大的模型。如果只是跑通流程用 7B 级别的模型单机 8 卡 A100 或者 H100 是最舒服的配置。显存方面7B 模型做 PPO 训练Actor 和 Critic 各占一份加上 Reward 和 Reference四份模型权重加上优化器状态和激活值每卡至少需要 40GB 显存才能比较从容。如果显存紧张可以开梯度检查点但会拖慢训练速度。驱动和 CUDA 版本这块我建议直接用 CUDA 12.1 以上。PyTorch 2.1 之后的版本对 CUDA 12 的支持比较完善FSDP 的稳定性也好了很多。我试过 CUDA 11.8 加 PyTorch 2.0 的组合FSDP 在某些配置下会出现通信 hang 住的问题换成 CUDA 12.1 加 PyTorch 2.2 之后就再没遇到过。提示如果你的机器上有多套 CUDA 版本务必确认nvcc --version和torch.version.cuda输出一致。不一致的时候编译自定义算子会出各种奇怪的链接错误。2.2 Python 依赖的版本陷阱verl 的依赖不算多但版本敏感。我列一下我实测能跑通的版本组合依赖包版本说明Python3.103.11 也可以但 3.10 兼容性最好PyTorch2.2.12.1 也能跑2.2 更稳CUDA12.1和 PyTorch 版本对应transformers4.40.0太新的版本可能有 API 变动vLLM0.4.2推理引擎版本要和 PyTorch 匹配flash-attn2.5.8注意力加速编译比较耗时hydra-core1.3.2配置管理flash-attn 的编译是环境准备里最耗时的一步。它需要编译 CUDA 算子在 8 卡机器上如果并行编译大概要 20 到 30 分钟。我建议用MAX_JOBS8 pip install flash-attn --no-build-isolation这种方式装把编译并行度拉满。如果编译过程中报显存不足把MAX_JOBS降到 4 再试。vLLM 的版本和 PyTorch 版本强绑定。vLLM 0.4.2 要求 PyTorch 2.1 以上但如果你用的是 PyTorch 2.2需要确认 vLLM 的 wheel 是针对 CUDA 12.1 编译的。装错了版本会在导入时直接报undefined symbol错误排查起来很费时间。2.3 从源码安装 verl 的正确姿势verl 可以直接 pip 安装但我建议从源码装因为有些配置项在 pip 版本里可能还没更新。步骤是这样的git clone https://github.com/volcengine/verl.git cd verl pip install -e .-e是 editable 模式装完之后你改源码里的配置或者算子不用重新安装。verl 的 setup.py 里会检查一些依赖如果缺了会提示你装。我遇到过一次 setup.py 里写的依赖版本和实际能跑通的版本不一致装完之后手动降级了 transformers 才跑通。装完之后验证一下import verl print(verl.__version__)如果这行能正常输出说明基础环境没问题。接下来可以跑一个最小示例确认端到端能通。3. 数据格式与配置文件跑通的第一道门槛3.1 训练数据的组织方式verl 的训练数据格式取决于你用的数据集类型。做 RLHF 的时候数据通常是 prompt 加 chosen/rejected 的形式但 PPO 训练只需要 prompt奖励由 Reward Model 实时打分。所以 PPO 的数据集本质上就是一个 prompt 列表。verl 支持从 HuggingFace datasets 加载数据也支持从本地 parquet 文件加载。我建议用 parquet因为加载速度快而且格式可控。数据文件里至少要有prompt字段如果做多轮对话还需要messages字段。一个典型的 parquet 数据长这样import pandas as pd data { prompt: [ 请解释一下什么是强化学习, 写一段 Python 代码实现快速排序, 介绍一下 Transformer 的注意力机制 ] } df pd.DataFrame(data) df.to_parquet(train.parquet)这个格式看起来简单但有个坑verl 在加载数据时会做 tokenize如果你的 prompt 里包含特殊字符或者超长文本tokenize 之后可能超过模型的最大长度。verl 默认会截断但截断策略在配置里可以调。我建议在准备数据阶段就先过滤掉超长的样本避免训练时出现莫名其妙的长度错误。3.2 配置文件的结构与关键字段verl 用 Hydra 管理配置主配置文件在verl/trainer/config目录下。PPO 的配置叫ppo_trainer.yamlGRPO 的配置叫grpo_trainer.yaml。这两个文件的结构类似但算法相关的字段有差异。配置大致分几块data数据路径、batch size、最大长度、截断策略actor_rollout_refActor 和 Rollout 的模型路径、并行策略、推理引擎配置criticCritic 模型的配置GRPO 不需要 Critic这块可以忽略reward_modelReward Model 的配置algorithm算法相关的参数比如 GAE 的 lambda、gamma、KL 系数trainer训练轮数、保存频率、日志频率我一开始被actor_rollout_ref这个命名搞晕了后来才理解它把 Actor、Rollout、Reference 三个角色的配置放在了一起因为它们共享同一个基础模型。actor_rollout_ref.actor下面是 Actor 的训练配置actor_rollout_ref.rollout下面是推理配置actor_rollout_ref.ref下面是 Reference Model 的配置。3.3 单机 8 卡的最小配置跑通优先我给一份单机 8 卡、7B 模型的最小配置。这份配置我实测能跑通显存占用在每卡 60GB 左右。data: train_files: /path/to/train.parquet val_files: /path/to/val.parquet train_batch_size: 64 max_prompt_length: 512 max_response_length: 512 actor_rollout_ref: model: path: /path/to/Qwen2-7B actor: optim: lr: 1e-6 ppo_mini_batch_size: 16 ppo_micro_batch_size: 2 use_kl_loss: true kl_loss_coef: 0.001 rollout: name: vllm tensor_model_parallel_size: 2 gpu_memory_utilization: 0.6 temperature: 1.0 ref: fsdp_config: param_offload: true critic: model: path: /path/to/Qwen2-7B optim: lr: 1e-5 ppo_mini_batch_size: 16 ppo_micro_batch_size: 2 reward_model: enable: true model: path: /path/to/reward-model algorithm: gamma: 1.0 lam: 0.95 adv_estimator: gae kl_penalty: kl kl_ctrl: type: fixed kl_coef: 0.001 trainer: total_epochs: 15 save_freq: 5 test_freq: 5 n_gpus_per_node: 8 nnodes: 1这份配置里有几个字段需要重点解释。ppo_micro_batch_size是实际送进 GPU 的 batch sizeppo_mini_batch_size是一个 PPO 更新周期里累积的 batch size。mini batch 会被切成多个 micro batch 做梯度累积。我设成 16 和 2意味着每个 PPO 更新周期处理 16 条样本分 8 次前向反向每次 2 条。gpu_memory_utilization是 vLLM 的显存占用比例。设成 0.6 意味着 vLLM 最多用 60% 的显存做 KV cache。这个值不能设太高因为训练侧还要留显存给 FSDP。我试过设 0.8结果训练侧 OOM 了。param_offload是 Reference Model 的参数卸载开关。Reference Model 只做前向不需要优化器状态把参数卸载到 CPU 能省不少显存。代价是每次前向都要从 CPU 拷参数到 GPU速度会慢一些。如果显存够可以关掉。4. 启动训练与首轮跑通的完整链路4.1 启动命令与日志解读verl 的启动入口是verl/trainer/main_ppo.py用 torchrun 启动。单机 8 卡的命令是torchrun --nproc_per_node8 \ -m verl.trainer.main_ppo \ --config-pathconfig \ --config-nameppo_trainer \ data.train_files/path/to/train.parquet \ actor_rollout_ref.model.path/path/to/Qwen2-7B \ critic.model.path/path/to/Qwen2-7B \ reward_model.model.path/path/to/reward-model命令行参数会覆盖 YAML 里的配置这样你可以在不改配置文件的情况下做实验。我习惯把常用配置写在 YAML 里命令行只覆盖需要频繁调整的字段。启动之后日志会先打印配置然后是模型加载信息。模型加载阶段最耗时7B 模型加载四份Actor、Critic、Reward、Reference加上 FSDP 分片大概要 3 到 5 分钟。加载完之后会打印Rollout开始的日志这时候 vLLM 开始生成样本。第一轮 rollout 通常比较慢因为 vLLM 要做 warmup编译 CUDA 图。我实测第一轮 rollout 花了 8 分钟后面每轮稳定在 2 到 3 分钟。如果你发现第一轮特别慢不用慌这是正常的。4.2 从 rollout 到策略更新的数据流verl 的训练循环大致是这样的先让 Actor 用 vLLM 生成一批 response然后 Reward Model 给每个 response 打分接着 Critic 计算每个 token 的价值最后用 GAE 算优势更新 Actor 和 Critic。这个流程里有个容易忽略的点rollout 生成的 response 长度是不固定的。verl 会把它们 padding 到同一长度但 padding 部分不参与 loss 计算。如果你发现 loss 曲线抖动很大可能是 padding 处理有问题。检查一下max_response_length是不是设得太小导致大量 response 被截断。另一个点是 KL 散度的计算。PPO 里 KL 散度是 Actor 和 Reference Model 之间的差异用来约束策略不要偏离太远。verl 支持两种 KL 计算方式kl和mse。kl是标准的 KL 散度mse是均方误差近似。我建议用kl虽然计算慢一点但更准确。4.3 首轮跑通后该看哪些指标首轮跑通之后不要急着调参先确认几个关键指标是否正常。第一个是reward的均值。如果 reward 一直不涨可能是 Reward Model 有问题或者学习率太低。我第一轮跑的时候 reward 从 0.3 涨到 0.5 用了 5 个 epoch这个速度算正常。第二个是kl散度。KL 散度应该在一个合理范围内波动如果持续增大说明策略偏离 Reference Model 太远需要增大 KL 系数。如果 KL 一直是 0说明 KL 惩罚没生效检查use_kl_loss是不是设成了 false。第三个是response_length。这个指标反映模型生成的 response 平均长度。如果长度一直很短可能是max_response_length设得太小或者模型倾向于生成短回复。可以通过调整 temperature 或者加长度惩罚来改善。5. PPO 和 GRPO 的配置差异与选型5.1 GRPO 为什么不需要 CriticGRPO 是 DeepSeek 提出的一种策略优化算法核心思想是用组内归一化代替 Critic 的价值估计。具体来说对于同一个 promptGRPO 会生成一组 response然后用这组 response 的 reward 均值作为基线每个 response 的优势就是它的 reward 减去组均值。这个设计的好处是省掉了 Critic 模型。Critic 在 PPO 里是个大头7B 的 Critic 加上优化器状态显存占用和 Actor 差不多。GRPO 去掉 Critic 之后显存直接省了一半训练速度也快了不少。代价是 GRPO 需要为每个 prompt 生成多个 response。配置里有个num_generations字段控制每个 prompt 生成几个 response。我一般设 4 到 8设太小优势估计不准设太大显存吃不消。5.2 两种算法的配置字段对照我把 PPO 和 GRPO 的关键配置差异整理成表格配置字段PPOGRPO说明adv_estimatorgaegrpo优势估计方式critic需要不需要GRPO 无 Criticnum_generations14-8GRPO 需要多个生成kl_penaltyklkl两者都支持ppo_mini_batch_size1632GRPO 可以更大GRPO 的ppo_mini_batch_size可以设得比 PPO 大因为没有 Critic 占显存。我试过设到 64显存还有富余。5.3 什么场景选 PPO什么场景选 GRPOPPO 适合 reward 信号比较稠密的场景也就是每个 token 都能得到有意义的奖励。比如做代码生成编译器能给出逐行的错误信息这种稠密信号用 PPO 效果更好。GRPO 适合 reward 信号稀疏的场景比如数学推理只有最终答案对错中间步骤没有奖励。这种情况下 Critic 很难学到准确的价值函数GRPO 的组内归一化反而更稳。我实测下来数学推理任务上 GRPO 比 PPO 收敛快大概 30%而且显存占用低单机 8 卡能跑更大的模型。代码生成任务上两者差距不大但 PPO 的 reward 曲线更平滑。6. 踩坑实录那些让我卡了半天的报错6.1 vLLM 初始化时的显存冲突最常见的报错是 vLLM 初始化时 OOM。日志里会看到CUDA out of memory然后跟一堆显存分配信息。这个问题的根因是 vLLM 默认会占用大量显存做 KV cache而训练侧的 FSDP 也要占显存两边抢资源。解决办法是调低gpu_memory_utilization。我一开始设的 0.9直接 OOM。降到 0.6 之后稳定了。如果还不行可以开enable_gradient_checkpointing用时间换显存。另一个相关的坑是 vLLM 的tensor_model_parallel_size和 FSDP 的并行度不匹配。vLLM 用张量并行FSDP 用数据并行两者的通信组不一样。如果tensor_model_parallel_size设成 2意味着 vLLM 把模型切成 2 份但 FSDP 可能把模型切成 8 份。这种情况下权重同步会出问题。我建议tensor_model_parallel_size设成 1 或者 2不要设太大。6.2 FSDP 权重同步的 hang 住问题FSDP 权重同步 hang 住是个很隐蔽的问题。表现是训练卡在某个步骤不动日志也不报错GPU 利用率降到 0。我遇到过一次排查了半天才发现是param_offload和optimizer_offload同时开启导致的。FSDP 的 offload 机制在参数卸载和优化器卸载同时开启时会出现死锁。解决办法是只开一个或者都不开。我最后是只开了param_offloadoptimizer_offload关掉问题就消失了。如果遇到 hang 住可以用py-spy抓一下堆栈py-spy dump --pid 训练进程PID堆栈会显示卡在哪个函数上。如果是卡在all_gather或者reduce_scatter基本可以确定是通信问题。6.3 reward 不涨的排查链路reward 不涨是最让人头疼的问题因为可能的原因太多。我总结了一个排查链路按顺序检查第一步确认 Reward Model 本身是否正常。单独拿几条样本喂给 Reward Model看打分是否合理。如果 Reward Model 打分都是 0.5 左右说明它没学到东西需要重新训练。第二步检查学习率。学习率太大会导致策略震荡太小会导致不更新。我一般从 1e-6 开始试如果 reward 不涨就调到 5e-6。第三步检查 KL 系数。KL 系数太大会把策略锁死在 Reference Model 附近reward 自然不涨。可以先把 KL 系数设成 0看 reward 能不能涨。如果能涨说明是 KL 约束太强逐步增大 KL 系数找到平衡点。第四步检查 advantage 的计算。如果 advantage 全是正数或者全是负数说明基线估计有问题。PPO 里基线是 Critic 的输出GRPO 里是组均值。可以打印 advantage 的均值和方差确认。6.4 多轮对话场景下的特殊处理多轮对话的 PPO 训练比单轮复杂。主要复杂在 reward 的分配上。单轮场景下整个 response 对应一个 reward。多轮场景下每一轮都有一个 reward需要决定怎么把多轮 reward 分配到每个 token 上。verl 支持多轮对话但配置里需要额外设置multi_turn相关的字段。我试过一次多轮训练发现 reward 分配策略对结果影响很大。简单地把最后一轮的 reward 广播到所有轮次效果不如按轮次加权。加权系数需要根据具体任务调没有通用值。另一个坑是多轮对话的 rollout 长度不好控制。每一轮的长度不固定总长度可能超过max_response_length。verl 会截断但截断位置如果落在某一轮中间会导致对话不完整。我建议把max_response_length设大一些或者限制最大轮数。7. 让训练更稳的几个工程技巧7.1 梯度累积与 batch size 的平衡batch size 的设置直接影响训练稳定性。batch size 太小梯度噪声大reward 曲线抖动batch size 太大显存不够而且可能过拟合。我的经验是ppo_micro_batch_size设成 2 到 4ppo_mini_batch_size设成 micro batch 的 8 到 16 倍。这样梯度累积的次数在 8 到 16 之间既能平滑梯度又不会太慢。如果显存实在不够可以开gradient_accumulation_steps但要注意这和ppo_mini_batch_size是两套机制。gradient_accumulation_steps是 FSDP 层面的梯度累积ppo_mini_batch_size是 PPO 算法层面的。两者叠加时实际 batch size 是两者相乘。7.2 checkpoint 保存与恢复的注意事项verl 的 checkpoint 保存分两种Actor 的 checkpoint 和 Critic 的 checkpoint。保存频率由save_freq控制单位是 epoch。保存 checkpoint 时有个坑如果save_freq设得太小保存太频繁会拖慢训练。我试过设成 1每个 epoch 都保存结果训练速度慢了 20%。后来改成 5速度就正常了。恢复训练时需要指定resume_from_checkpoint路径。verl 会从该路径加载 Actor 和 Critic 的权重以及优化器状态。注意优化器状态也要加载否则学习率调度会从头开始影响训练连续性。7.3 日志与监控的配置verl 默认用 wandb 做日志也支持 tensorboard。我建议用 tensorboard因为不需要联网而且本地查看方便。配置里加一行trainer: logger: tensorboard日志会写到output_dir下的tensorboard目录。用tensorboard --logdir启动就能看。关键监控指标我列一下critic/rewardsreward 均值应该逐步上升actor/klKL 散度应该稳定在合理范围actor/pg_loss策略梯度 loss应该震荡下降critic/vf_loss价值函数 loss应该逐步下降response_lengthresponse 平均长度应该稳定如果pg_loss一直不降可能是学习率太小或者 advantage 估计有问题。如果vf_loss不降可能是 Critic 的学习率太小或者网络结构不合适。7.4 显存优化的几个实用开关显存不够的时候可以按顺序尝试这几个开关第一个是enable_gradient_checkpointing用时间换显存能省 30% 到 40% 的激活值显存。代价是训练速度慢 20% 左右。第二个是param_offload把 Reference Model 的参数卸载到 CPU。Reference Model 只做前向卸载影响不大。第三个是optimizer_offload把优化器状态卸载到 CPU。这个影响比较大训练速度会明显变慢但能省很多显存。注意不要和param_offload同时开会死锁。第四个是减小ppo_micro_batch_size。这是最直接的办法但会拉长训练时间。我一般按这个顺序试先开 gradient checkpointing不够再开 param offload还不够就减 micro batch size。optimizer offload 是最后手段。8. 从跑通到跑好的下一步跑通 verl 只是第一步真正要做出效果还需要在数据和奖励设计上下功夫。我这两周最大的体会是框架层面的问题都是可以解决的无非是查文档、看源码、调参数。但数据和奖励的问题没有标准答案需要根据具体任务反复实验。如果你刚开始跑 verl我建议先用一个小数据集比如 1000 条样本把整个流程跑通确认 reward 能涨。然后再换大数据集调并行策略和 batch size。不要一上来就用全量数据那样出问题很难定位。另外verl 的社区比较活跃遇到问题可以去 GitHub 提 issue或者在讨论区搜一下有没有人遇到过类似的问题。我遇到的几个报错都是在 issue 里找到的解决方案。最后分享一个我踩过的坑verl 的配置文件里有些字段是互斥的比如use_kl_loss和kl_penalty不能同时设成冲突的值。配置加载时不会报错但训练时会出问题。建议改配置之后先跑一个 epoch 确认没问题再跑完整训练。