跳到主要内容

数据截至 (上游 commit cfacd76a0bdd)

verl — 架构与原理

30 秒导读: verl 是字节 Seed 团队开源的 LLM 强化学习训练库(HybridFlow 论文的开源实现)。它解决的核心矛盾是:RL 训练一步里要先用推理引擎(vLLM/SGLang)高速生成一堆回答,再用训练引擎(FSDP/Megatron)对这些回答做反向传播——两套框架的并行方式、显存布局、进程模型完全不同,还常常挤在同一批 GPU 上。verl 的答案是把「怎么调度」和「怎么算」彻底分开:算法作者在 driver 上写一段顺序代码,底层自动展开成跨百卡的分布式执行。


1. 这是什么(零基础也能懂)

一句话定义

verl 是一个给大语言模型做强化学习后训练(post-training)的框架:你给它一个基座模型、一批题目、一个打分函数,它反复「让模型答题 → 打分 → 按分数更新模型」,直到模型答得更好。

它要解决谁的什么问题

设想你要复现 DeepSeek-R1 那种「靠 RL 把数学能力练上去」的训练:

  • 你有一个 32B 的模型,要在 64 张 H800 上训。
  • 每一步要让模型对 512 道题各采样 8 个答案 —— 这是推理任务,得用 vLLM 才够快。
  • 拿到 4096 条答案后要算 loss、反向传播 —— 这是训练任务,得用 FSDP 或 Megatron 才装得下。
  • 这两件事在同一步里交替发生,而且共用同一批 GPU

于是你会撞上一堆基础设施问题:推理引擎和训练引擎的模型分片方式不一样,权重怎么搬过去?训练时推理引擎占的显存怎么让出来?一批答案长短差 10 倍,怎么让各张卡的负载不失衡?

verl 就是把这些问题一次性解决掉的那一层。 算法研究者只写「算 advantage、算 loss」,工程细节由框架承担。

它能做什么

能力具体支持
RL 算法PPO、GRPO、REINFORCE++、RLOO、REMAX、GSPO、DAPO、Dr.GRPO、CISPO、GPG 等(verl/trainer/ppo/core_algos.py
训练后端FSDP / FSDP2、Megatron-LM、VeOmni、TorchTitan、Automodel、MindSpeed(verl/workers/engine/
推理后端vLLM、SGLang、TRT-LLM、HF Transformers(verl/workers/rollout/
奖励来源规则函数(gsm8k / math / 代码沙箱)、判别式奖励模型、生成式奖励模型(verl/utils/reward_score/verl/experimental/reward_loop/
Agent 训练多轮对话 + 工具调用的轨迹级 RL(verl/experimental/agent_loop/tool_agent_loop.py
多模态图像 / 视频 / 音频输入的 VLM RL
规模支持到 671B MoE、上百张卡;专家并行、LoRA RL

用起来什么样

最小可用形态就是一条命令行——所有配置通过 Hydra 覆盖:

# 摘自 examples/grpo_trainer/run_qwen3_4b_fsdp.sh(已精简)
python3 -m verl.trainer.main_ppo \
algorithm.adv_estimator=grpo \
data.train_files=$HOME/data/gsm8k/train.parquet \
data.train_batch_size=512 \
actor_rollout_ref.model.path=Qwen/Qwen3-4B \
actor_rollout_ref.actor.ppo_mini_batch_size=256 \
actor_rollout_ref.rollout.name=vllm \
actor_rollout_ref.rollout.n=5 \
trainer.n_gpus_per_node=8 trainer.nnodes=1

读这条命令就能读出 verl 的心智模型:

  • actor_rollout_ref —— 一个进程里同时装着「要训的模型(actor)」「用来生成的推理引擎(rollout)」「算 KL 用的参考模型(ref)」。
  • rollout.n=5 —— 每道题采 5 个答案,GRPO 靠组内比较算优势。
  • train_batch_size=512 / ppo_mini_batch_size=256 —— 一步收 512 道题,切成 mini-batch 做多次梯度更新。

入口在 verl/trainer/main_ppo.py:166main),它根据 trainer.use_v1 分流到 V1 或已废弃的 V0 trainer(verl/trainer/main_ppo.py:183)。本 commit 里 use_v1 默认为 trueverl/trainer/config/ppo_trainer.yaml:222),V0 的 RayPPOTrainer 已标注 v0.9.0 移除(verl/trainer/ppo/ray_trainer.py:285)。本文档主线讲 V1。

一句话直觉

把 verl 想成一个「乐队指挥」。 乐手(GPU 进程)各自会演奏,但谁在什么时候演奏什么、乐谱怎么分页发下去,全由指挥(driver 上的单控制器)说了算。指挥手里的谱子是一段顺序的、能一行行读懂的 Python;乐手收到的是被自动切好的分谱。


2. 顶层全景(它大概怎么转)

2.1 进程拓扑

先看「谁是谁」。整套系统跑在一个 Ray 集群上:

┌──────────────────────────────────┐
│ TaskRunnerV1 (Ray actor) │
│ ── 单控制器 / driver ── │
main_ppo.py ───────► │ PPOTrainer.fit() 顺序驱动全流程 │
(hydra 配置) └───┬───────────┬──────────┬───────┘
│ │ │
┌───────────────┘ │ └────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌────────────────────┐ ┌───────────────────┐
│ WorkerGroup │ │ AgentLoopManager │ │ TransferQueue │
│ (训练侧 N 进程) │ │ + LLM 推理副本 │ │ (全局 KV 数据面) │
│ FSDP / Megatron │ │ vLLM / SGLang │ │ 存轨迹、只传 key │
└──────────────────┘ └────────────────────┘ └───────────────────┘
▲ │
└──── CheckpointEngine ─────┘
(训练权重 → 推理副本)

怎么读这张图: 中间那个 driver 是唯一的「大脑」,它自己不碰 GPU 重活;左右两侧是两片 GPU 集群资源(默认还共用同一批卡);底下的 TransferQueue 是数据落脚点,driver 之间传的只是 key。

2.2 部件职责

部件干什么在哪个文件
PPOTrainer单控制器主体,fit() / step() 顺序编排一整步 PPOverl/trainer/ppo/v1/trainer_base.py:118
@register + Dispatch声明式地说明「这个方法怎么把数据切给各 rank、怎么把结果收回来」verl/single_controller/base/decorator.py:398
RayWorkerGroup把 N 个 Ray actor 包成一个「像单机对象一样调用」的组verl/single_controller/ray/base.py:418
ActorRolloutRefWorkerGPU 进程:同一进程里持有 actor 训练引擎、ref 引擎、rollout 引擎verl/workers/engine_workers.py:446
TrainingWorker通用训练进程(critic 用它),暴露 Tinker 风格粗粒度 APIverl/workers/engine_workers.py:76
EngineRegistry / BaseEngine训练后端抽象层:(model_type, backend, device, vendor) → 引擎类verl/workers/engine/base.py:339
RolloutReplica一个推理服务副本(可跨节点),三种部署模式verl/workers/rollout/replica.py:70
AgentLoopManager / AgentLoopBase一条轨迹的生成逻辑:单轮、或多轮工具调用状态机verl/experimental/agent_loop/agent_loop.py:1139:196
ReplayBuffer从 TransferQueue 里挑出「整组已完成」的轨迹组成 batchverl/trainer/ppo/v1/replay_buffer.py:63
CheckpointEngineManager训练权重 → 推理副本的搬运与显存编排verl/checkpoint_engine/base.py:381
RewardLoopManager打分:规则函数 / 判别式 RM / 生成式 RMverl/experimental/reward_loop/reward_loop.py:273
core_algos优势估计器 + 策略损失的双注册表verl/trainer/ppo/core_algos.py

2.3 主线走一遍(一个训练 step)

下面这条线对应 PPOTrainer.step()verl/trainer/ppo/v1/trainer_base.py:509)。①~⑩ 与源码里的 # 1. ~ # 10. 十条编号注释一一对应;另有两处没有编号的动作——后台生成、同步权重——不在 step() 函数体内,下面用旁注标出。

① 投喂 prompt _add_batch_to_generate()
dataloader 取一批题 → 给每题分配 uid → 在 TQ 里登记 status=pending
→ AgentLoopManager.generate_sequences() (非阻塞,发完就走)

├── 旁注(不在 step() 里):~ 生成在后台异步进行 ~
│ AgentLoopWorker 为每道题起 n 个 asyncio 任务 → 打到推理副本
│ → 轨迹写回 TransferQueue,整组做完把 uid 标成 finished

② 取一批 replay_buffer.sample()
轮询 TQ 元数据,等到「finished 的题数 ≥ batch_size」,挑最老的那些
→ 返回 KVBatchMeta(只有 key + tag,没有真数据)


③ 打分 → ④ 负载均衡 → ⑤ old_log_prob → ⑥ ref_log_prob → ⑦ values
(③ 若用 colocate RM 才在这里做;否则生成时就已流式打分)
(④ 按序列长度重排,让各 DP rank token 数接近)
(⑥⑦ 分别在开了 ref / critic 时才做)


⑧ 算优势 _compute_advantage()
driver 上做的轻量计算:GRPO 组内减均值除标准差 / GAE 等


⑨ 更新 critic → ⑩ 更新 actor
wg.update_actor(batch) —— 一行调用,底层展开到所有训练 rank


旁注(不在 step() 里):同步权重 on_step_end()
step() 返回后由 fit() 调用(trainer_base.py:372)
→ CheckpointEngineManager.update_weights() → 推理副本换上新权重

这条线最值得记住的两点:

  1. driver 只做「轻量算子 + 调度」。 优势估计这种 O(batch) 的小计算就跑在 driver 进程里:_compute_advantageverl/trainer/ppo/v1/trainer_base.py:1588)用 tq.kv_batch_get 把需要的几列拉下来,本地算完再写回,全程不发 worker 调用;重活(前反向、生成)全部下沉到 worker。
  2. 数据不跟着调用走。 ② 之后 driver 手上拿的是 KVBatchMeta——一串 key,不是几个 GB 的张量。真正的数据在 TransferQueue 里,由各 worker 自己按 key 取(详见第 2 章)。

2.4 一步之内 GPU 在干什么(sync 模式)

默认的 sync 模式下,训练和推理共用同一批 GPU,靠「睡/醒」切换:

时间 ──────────────────────────────────────────────────────►

推理副本 [醒着,全速生成]────►[睡:释放权重+KV cache]─────────────►[醒]
│ ▲
训练引擎 [显存让给推理] └►[前反向传播 训练]───────┘
权重同步

对应的三个动作:on_sample_end()sleep_replicas()on_step_end()update_weights()verl/trainer/ppo/v1/trainer_sync.py:35-42)。这就是 HybridFlow 论文里 3D-HybridEngine 的落地方式。


3. 阅读地图(建议顺序)

七章由浅入深。如果时间有限,读 01 → 02 → 04 就能抓住 verl 区别于其他 RL 库的全部要害。

顺序章节讲什么适合谁
101-single-controller.md@register / Dispatch / WorkerGroup —— HybridFlow 编程模型的实现所有人必读,这是 verl 的立身之本
202-data-plane.mdDataProto 与 TransferQueue 两代数据面,以及为什么要换想看懂 V1 代码里满屏 tq.kv_batch_get 的人
303-rollout-agent-loop.md推理副本部署、负载均衡、AgentLoop 状态机、partial rollout做 agent RL / 多轮工具调用的人
404-weight-sync-and-modes.mdCheckpointEngine 权重搬运;sync / colocate_async / separate_async关心吞吐和显存的人
505-training-engine.mdEngineRegistry、micro-batch、序列长度均衡、loss 归一化要接新训练后端、或调性能的人
606-algorithms.md优势估计器 / 策略损失注册表、GRPO、rollout correction算法研究者
707-insights-boundaries-map.md巧妙之处、边界、横向对比、全局代码地图想带走「精华」和想快速定位源码的人

4. 代码地图(入口级)

每章末尾都有自己的细粒度地图,这里只列从零开始读源码的四个入口

主题文件路径符号名
命令行入口、V0/V1 分流verl/trainer/main_ppo.pymainrun_ppoTaskRunnerV1
单控制器主循环verl/trainer/ppo/v1/trainer_base.pyPPOTrainer.fitPPOTrainer.step
分布式调用的魔法verl/single_controller/base/decorator.pyregisterDispatchDISPATCH_MODE_FN_REGISTRY
GPU 侧 workerverl/workers/engine_workers.pyActorRolloutRefWorkerTrainingWorker