数据截至 (上游 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:166(main),它根据 trainer.use_v1 分流到 V1 或已废弃的 V0 trainer(verl/trainer/main_ppo.py:183)。本 commit 里 use_v1 默认为 true(verl/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() 顺序编排一整步 PPO | verl/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 |
ActorRolloutRefWorker | GPU 进程:同一进程里持有 actor 训练引擎、ref 引擎、rollout 引擎 | verl/workers/engine_workers.py:446 |
TrainingWorker | 通用训练进程(critic 用它),暴露 Tinker 风格粗粒度 API | verl/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 里挑出「整组已完成」的轨迹组成 batch | verl/trainer/ppo/v1/replay_buffer.py:63 |
CheckpointEngineManager | 训练权重 → 推理副本的搬运与显存编排 | verl/checkpoint_engine/base.py:381 |
RewardLoopManager | 打分:规则函数 / 判别式 RM / 生成式 RM | verl/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() → 推理副本换上新权重
这条线最值得记住的两点:
- driver 只做「轻量算子 + 调度」。 优势估计这种 O(batch) 的小计算就跑在 driver 进程里:
_compute_advantage(verl/trainer/ppo/v1/trainer_base.py:1588)用tq.kv_batch_get把需要的几列拉下来,本地算完再写回,全程不发 worker 调用;重活(前反向、生成)全部下沉到 worker。 - 数据不跟着调用走。 ② 之后 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 库的全部要害。
| 顺序 | 章节 | 讲什么 | 适合谁 |
|---|---|---|---|
| 1 | 01-single-controller.md | @register / Dispatch / WorkerGroup —— HybridFlow 编程模型的实现 | 所有人必读,这是 verl 的立身之本 |
| 2 | 02-data-plane.md | DataProto 与 TransferQueue 两代数据面,以及为什么要换 | 想看懂 V1 代码里满屏 tq.kv_batch_get 的人 |
| 3 | 03-rollout-agent-loop.md | 推理副本部署、负载均衡、AgentLoop 状态机、partial rollout | 做 agent RL / 多轮工具调用的人 |
| 4 | 04-weight-sync-and-modes.md | CheckpointEngine 权重搬运;sync / colocate_async / separate_async | 关心吞吐和显存的人 |
| 5 | 05-training-engine.md | EngineRegistry、micro-batch、序列长度均衡、loss 归一化 | 要接新训练后端、或调性能的人 |
| 6 | 06-algorithms.md | 优势估计器 / 策略损失注册表、GRPO、rollout correction | 算法研究者 |
| 7 | 07-insights-boundaries-map.md | 巧妙之处、边界、横向对比、全局代码地图 | 想带走「精华」和想快速定位源码的人 |
4. 代码地图(入口级)
每章末尾都有自己的细粒度地图,这里只列从零开始读源码的四个入口:
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 命令行入口、V0/V1 分流 | verl/trainer/main_ppo.py | main、run_ppo、TaskRunnerV1 |
| 单控制器主循环 | verl/trainer/ppo/v1/trainer_base.py | PPOTrainer.fit、PPOTrainer.step |
| 分布式调用的魔法 | verl/single_controller/base/decorator.py | register、Dispatch、DISPATCH_MODE_FN_REGISTRY |
| GPU 侧 worker | verl/workers/engine_workers.py | ActorRolloutRefWorker、TrainingWorker |