verl 源码解读(五):Rollout 生成与训练推理混合引擎
本文是《verl 源码解读》系列第 5 篇。前四篇讲清了“数据流怎么编排、数据怎么流转、算法怎么算”。本篇聚焦 verl 最有工程含量的部分:生成(Rollout)如何用 vLLM/SGLang 加速,以及训练后的新权重如何同步回推理引擎。对应代码在
verl/workers/rollout/。系列目录:1 架构 · 2 HybridFlow · 3 DataProto · 4 PPO/GRPO · 5 Rollout 混合引擎 · 6 RL 算法详解
1. 为什么 Rollout 要单独用一套引擎
RL 训练每个 step 都要“用当前策略生成一批回答”。生成是自回归解码,和训练(一次 forward+backward)是完全不同的负载:
- 训练引擎(FSDP / Megatron):为反向传播和参数分片优化,直接拿来做逐 token 解码非常慢。
- 推理引擎(vLLM / SGLang):有 PagedAttention、continuous batching、KV cache 复用等,生成吞吐能快一个数量级。
所以 verl 让 Actor 用训练引擎存在,但生成时切换到 vLLM/SGLang。这就带来了全篇的核心难题:同一份 Actor 权重,要在两套引擎之间来回同步。 这正是 HybridFlow 论文里 “3D-HybridEngine” 要解决的问题。
2. BaseRollout:统一的生成抽象
所有生成后端都实现 verl/workers/rollout/base.py 的 BaseRollout,它只有几个抽象方法,把“生成 + 权重/显存管理”抽象得非常干净:
class BaseRollout(ABC):
@abstractmethod
async def resume(self, tags: list[str]):
"""把权重或 KV cache 恢复到 GPU 显存(tags: 'weights' / 'kv_cache')"""
@abstractmethod
async def update_weights(self, weights, wire_format="named_tensors", **kwargs):
"""用训练侧传来的新权重更新推理引擎"""
@abstractmethod
async def release(self):
"""释放显存里的权重和 KV cache"""
def generate_sequences(self, prompts: DataProto) -> DataProto:
"""同步批量生成"""
这四个方法恰好对应一个 RL step 里 Rollout 的生命周期: resume(唤醒)→ generate(生成)→ release(释放显存给训练用)→ update_weights(训练后更新权重)。
3. 后端注册表:vLLM / SGLang / TRT-LLM
verl 用注册表选择后端,默认都是 async(server)模式:
_ROLLOUT_REGISTRY = {
("vllm", "async"): "verl.workers.rollout.vllm_rollout.ServerAdapter",
("sglang", "async"): "verl.workers.rollout.sglang_rollout.sglang_rollout.ServerAdapter",
("trtllm", "async"): "verl.workers.rollout.trtllm_rollout.trtllm_rollout.ServerAdapter",
}
def get_rollout_class(rollout_name, mode="async"):
fqdn = _ROLLOUT_REGISTRY[(rollout_name, mode)]
module_name, class_name = fqdn.rsplit(".", 1)
return getattr(importlib.import_module(module_name), class_name)
配置 actor_rollout_ref.rollout.name=vllm(或 sglang)就切换后端。rollout/ 目录下还有 naive/(纯 HF)、hf_rollout.py 等用于调试/对拍。
4. 两种工作模式:sync vs server(async)
4.1 同步模式
driver 调 actor_rollout_wg.generate_sequences(gen_batch),走第 2 篇的 DP_COMPUTE_PROTO:prompt 被切分到各 rollout worker,并行生成后 concat 回来。适合“一问一答”的单轮场景。
4.2 Server / async 模式(多轮 Agent 必需)
对于多轮对话、工具调用、Agent,一次“生成”中间可能要调外部工具、再继续生成,无法简单地一次性批处理。verl 提供 server 模式:把推理引擎拉起成一个服务(llm_server.py / replica.py / vllm_async_server.py),用 AgentLoop 之类的驱动按 OpenAI-like 接口多轮交互。
主循环里对应的是 async_rollout_manager.generate_sequences(...)(见第 4 篇 fit() 片段),底层用 replica.py 管理多个推理副本(replica),实现请求级的并发调度。
5. 权重同步:整篇的核心
训练一个 step 后,Actor 权重变了,必须把新权重灌进推理引擎,否则下一轮还在用旧策略采样。难点在于:
- 训练侧权重可能是 FSDP 分片 或 Megatron 的 TP/PP 切分;
- 推理侧(vLLM)有自己的并行与权重布局;
- 两者的 sharding(分片方式)通常不一致,需要“重新分片 / resharding”。
5.1 ShardingManager:训练布局 ↔ 推理布局
verl 为每种组合提供了 sharding manager(如 FSDP↔vLLM、Megatron↔vLLM),负责在生成前后转换权重布局,并处理设备 mesh 的映射。它是“3D-HybridEngine 消除内存冗余、降低通信开销”的落地点。
训练侧 FSDP / Megatron 的分片方式(ZeRO 切参数/梯度/优化器、TP/PP 切模型),详见《Megatron-LM 分布式训练》系列。
5.2 update_weights 的传输格式
update_weights(weights, wire_format=...) 接收一个 生成器,逐个 yield (name, tensor):
async def update_weights(self, weights: Generator[tuple[str, torch.Tensor], None, None],
wire_format="named_tensors", **kwargs): ...
wire_format="named_tensors":传完整张量(默认)。wire_format="delta_flush":只传增量(稀疏 delta,SGLang 专用),进一步省带宽。
用生成器而非一次性字典,是为了边生成边传输、边释放,避免把整份权重同时驻留显存。相关实现见 vllm_rollout/weight_update_utils.py、bucketed_weight_transfer.py(分桶传输)。
5.3 在主循环里的落点
第 4 篇 fit() 里的 self.checkpoint_manager.update_weights(self.global_steps) 以及生成前后的 sleep_replicas() / resume,就是这条链路的触发点。
6. 显存错峰:sleep / wake / offload
Actor(训练)、Critic、Ref、Rollout 常常 colocate 在同一组 GPU(第 2 篇)。显存不可能同时装下所有引擎的全部状态,verl 用“错峰”策略:
┌── 生成阶段 ─────────────────────────────┐
│ resume rollout 权重 + KV cache 到 GPU │
│ 训练引擎的优化器状态 offload 到 CPU │
│ → 生成 │
└─────────────────────────────────────────┘
│ 生成完成
▼
┌── 训练阶段 ─────────────────────────────┐
│ release / sleep rollout(吐出显存) │
│ 训练引擎权重/优化器状态 reload 回 GPU │
│ → 前向/反向/更新 │
└─────────────────────────────────────────┘
│ 更新完成
▼
update_weights → 新权重灌回 rollout
BaseRollout.resume("weights"/"kv_cache") 和 release() 就是这套机制的接口。vLLM 的 sleep/wake、FSDP 的 CPU offload 都服务于此。理解了这个“显存一会儿给生成、一会儿给训练”的错峰,就理解了 verl 能在有限 GPU 上塞下这么多模型的关键。
7. 一个完整 step 的引擎视角
把前五篇串成一条时间线(单机单组 GPU,colocate):
[t0] resume rollout 权重 → vLLM 就绪
[t1] generate_sequences:vLLM 高速解码出 n 条回答 (推理引擎)
[t2] release rollout / sleep,吐出显存
[t3] reload 训练引擎 → compute_log_prob / ref / values (训练引擎)
[t4] driver 本地算 reward + advantage (driver)
[t5] update_critic / update_actor:反向传播更新参数 (训练引擎)
[t6] ShardingManager 转换布局 → update_weights 灌回 vLLM
[t7] 下一个 step,回到 t0(vLLM 已是最新策略)
- t1 用推理引擎,t3/t5 用训练引擎,t6 做布局转换 + 权重同步;
- t0/t2 的 resume/release 是显存错峰;
- 全程数据用 DataProto 承载,调用用 WorkerGroup 分发——正好把整个系列串起来。
8. 小结
- Rollout 之所以单独成体系,是因为“生成”适合用 vLLM/SGLang,而“训练”适合用 FSDP/Megatron,两者负载与分片方式都不同。
BaseRollout用resume / generate / release / update_weights四个方法,把“生成 + 显存管理 + 权重同步”抽象得很干净;后端通过注册表可插拔。- 权重同步(ShardingManager + update_weights 生成器 + 分桶/增量传输) 和 显存错峰(sleep/wake/offload) 是 verl 工程上最核心、最能体现 HybridEngine 价值的两块。
- server/async 模式让多轮 Agent、工具调用这类复杂生成成为可能。
系列回顾
| 篇 | 回答的问题 |
|---|---|
| 01 | verl 是什么、整体架构与数据流 |
| 02 | 复杂 RL 数据流怎么写成几行代码(HybridFlow / WorkerGroup / Dispatch) |
| 03 | 数据怎么统一承载与流转(DataProto) |
| 04 | PPO/GRPO 具体怎么算(fit 主循环 + core_algos) |
| 05 | 训练与推理两套引擎怎么协同(Rollout + 权重同步 + 显存错峰) |
至此,verl 的核心脉络——single-controller 编排、DataProto 数据总线、分布式分发、RL 算法、训练推理混合引擎——就完整地串起来了。接下来可以挑一个 examples/ 脚本实际跑一遍,对照这五篇把每个环节落到实处。