Rollout:为什么 verl 把生成做成推理服务
rollout 就是让当前 policy 根据 prompt 生成 response。小模型里它可能只是 model.generate(),但在 verl 这种 post-training 框架里,rollout 更接近一个高吞吐推理服务系统:它要处理 KV cache、采样参数、multi-turn agent、工具调用、权重热更新、显存释放和多副本负载均衡。
先补一点先验
训练 forward/backward 和推理生成的资源形态完全不同:
| 阶段 | 需要什么 | 不需要什么 |
|---|---|---|
| actor 训练 | 参数、梯度、优化器状态、反向图 | 大量 KV cache |
| rollout 生成 | 推理 engine、KV cache、采样调度、prefix cache | 梯度、优化器 |
所以 verl 把 actor 训练和 rollout 推理拆开。actor 更新完以后,再把最新权重同步给 rollout server。
BaseRollout:统一生命周期,不统一生成实现
verl/workers/rollout/base.py 定义了 rollout 抽象:
class BaseRollout:
async def resume(self, tags: list[str]): ...
async def update_weights(self, weights, ...): ...
async def release(self): ...
def generate_sequences(self, prompts): ...注意:当前 async server 是主路径。generate_sequences() 在 BaseRollout 里没有实现,vLLM/SGLang 的 ServerAdapter.generate_sequences() 也会明确报错,提示用 async server interface。也就是说,学习 rollout 不要只追 generate_sequences() 这个名字,要追:
RayPPOTrainer.init_workers()
-> LLMServerManager.create(...)
-> async_rollout_manager = AgentLoopManager.create(...)
-> RayPPOTrainer.fit()
-> async_rollout_manager.generate_sequences(batch)
-> AgentLoopWorker / AgentLoop
-> LLMServerClient.generate(...)
-> vLLMHttpServer 或 SGLangHttpServerLLMServerManager:rollout 服务的控制面
verl/workers/rollout/llm_server.py 里有三个关键对象:
| 对象 | 职责 |
|---|---|
LLMServerManager | 创建 rollout replicas,记录 server 地址,提供 client |
LLMServerClient | 给 agent loop 用的客户端,向某个 server 发 generate 请求 |
GlobalRequestLoadBalancer | 多 server 之间做 sticky session 和 least-loaded 选择 |
伪代码是:
manager = LLMServerManager.create(config, worker_group, rollout_resource_pool)
client = manager.get_client()
output = await client.generate(
request_id=...,
prompt_ids=...,
sampling_params=...,
)sticky session 的意义是:同一个多轮对话或工具调用链尽量落在同一个 server,减少上下文/cache 迁移。least-loaded 则避免所有请求挤到同一副本。
replica:HYBRID、COLOCATED、STANDALONE
verl/workers/rollout/replica.py 定义 RolloutMode:
| 模式 | 直觉 | 工程动机 |
|---|---|---|
HYBRID | rollout 与 actor worker 在同一组资源中协作 | 同步 PPO/GRPO 常见,省资源 |
COLOCATED | rollout server 和训练资源 colocate | 减少跨进程/跨节点调度,但要精细管显存 |
STANDALONE | rollout 使用独立资源池 | 异步、fully async、reward model/teacher server 更灵活 |
colocate 不是为了代码好看,而是为了吞吐和成本:同一批 GPU 可以在“训练阶段”和“生成阶段”交替使用。但代价是显存必须可释放、可恢复,否则训练状态和 KV cache 会互相 OOM。
vLLM 路径
vLLM async 路径主要看:
verl/workers/rollout/vllm_rollout/vllm_rollout.pyverl/workers/rollout/vllm_rollout/vllm_async_server.pyverl/workers/rollout/vllm_rollout/bucketed_weight_transfer.pyverl/workers/rollout/vllm_rollout/utils.py
ServerAdapter 是 actor worker 侧持有的 rollout adapter。它负责:
resume(tags)
-> server_handle.wake_up(tags=...)
release()
-> server_handle.sleep()
update_weights(...)
-> 让 vLLM server 先准备 update_weights_from_ipc
-> BucketedWeightSender 通过 CUDA IPC 或 shared memory 分桶发送 tensor
-> vLLM worker extension 把权重写入推理模型vLLMHttpServer.sleep() 会根据模式选择 sleep level。LoRA adapter 场景通常只释放 KV cache 或 adapter 相关内存;完整权重同步可能释放 weights + KV cache。官方 vLLM 的 Sleep Mode 文档也把这个设计讲成内存管理能力:sleep 释放推理 engine 占用,wake_up 再恢复。
SGLang 路径
SGLang async 路径主要看:
verl/workers/rollout/sglang_rollout/sglang_rollout.pyverl/workers/rollout/sglang_rollout/async_sglang_server.pyverl/workers/rollout/sglang_rollout/http_server_engine.pyverl/workers/rollout/sglang_rollout/utils.py
ServerAdapter.update_weights() 走 tensor bucket:
actor.engine.get_per_tensor_param()
-> get_named_tensor_buckets(...)
-> sglang.srt.weight_sync.update_weights(...)
-> SGLang server update_weights_from_tensorSGLang 的 release_memory_occupation(tags=["weights", "kv_cache"]) 和 resume_memory_occupation(...) 对应 verl 里的 release() / resume(tags)。LoRA adapter 不 merge 时,sleep_level=1 只释放 KV cache,保留 base weights;merge 或普通全量权重路径则可以释放更多内存。
weight sync:rollout 必须使用新 actor
一次标准同步训练里,actor update 后必须同步 rollout 权重:
update_actor(batch)
-> actor engine optimizer step
-> checkpoint_manager.update_weights(global_steps)
-> ActorRolloutRefWorker.update_weights(...)
-> rollout.update_weights(...)如果不同步,下一批 response 就是旧 policy 生成的。PPO/GRPO 仍然可以带 importance sampling,但 stale policy 会让训练更难解释,也会让 on-policy 假设变弱。
ActorRolloutRefWorker.update_weights() 的 colocated naive 路径有一个很典型的显存顺序:
如果 free_cache_engine:
rollout.resume(tags=["weights"])
actor.engine.get_per_tensor_param(...)
rollout.update_weights(...)
如果 actor 参数 offload:
actor.engine.to("cpu", model=True, optimizer=False, grad=False)
如果 free_cache_engine:
rollout.resume(tags=["kv_cache"])这个顺序不是随意的:同步权重时需要 weights 在推理 engine 侧可写;同步完再恢复 KV cache,避免权重和 cache 同时顶满显存。
sleep / release / resume 的区别
学习时可以这样粗略理解:
| 方法 | 在 verl 里的语义 |
|---|---|
release() | rollout 阶段结束,释放推理侧内存,让训练侧有空间 |
resume(tags=["weights"]) | 权重同步前恢复权重相关内存 |
resume(tags=["kv_cache"]) | 权重同步后恢复生成需要的 KV cache |
server sleep() | vLLM/SGLang 后端具体释放内存 |
server wake_up() / resume_memory_occupation() | 后端具体恢复内存 |
vLLM、SGLang、TRTLLM 的 API 名字不同,verl 用 BaseRollout 把生命周期抽成相似接口。
schemas.py 和 tokenizer.py:multi-turn 不是“字符串拼接”
schemas.py 里的 AsyncRolloutRequest 会处理 raw prompt、messages、tools、多模态输入、loss mask、position ids 和 tokenization sanity check。多轮 rollout 不是把对话字符串简单相加,因为训练时只应该对 assistant 生成的新 token 算 loss。
docs/sglang_multiturn/multiturn.rst 解释了 delta tokenization:每次把新增 assistant message 与上一轮 chat template 的差分 token 拿出来,作为 loss mask 的有效部分。tokenization_sanity_check_mode 用来防止训练模板和推理模板悄悄不一致。
fully async rollout 的额外一层
verl/experimental/fully_async_policy/fully_async_rollouter.py 继承了这套 server 思路,但把 Trainer 和 Rollouter 资源彻底拆开:
Rollouter 持续逐样本生成 -> MessageQueue
Trainer 从队列取样本训练
ParameterSynchronizer 定期同步权重这里 async_training.staleness_threshold 控制允许多少旧参数样本进入训练,partial_rollout=True 时,rollouter 可以在参数同步时中断未完成生成,保存状态,更新权重后继续生成。
这页原本不适合学习的地方
- 只说 rollout 是推理服务,但没有把
RayPPOTrainer -> AgentLoopManager -> LLMServerClient -> backend server的调用链写出来。 - 没区分
BaseRollout.generate_sequences()和当前 async server 主路径,容易让读者追错入口。 - 对 sleep/release/resume 只有概念解释,缺少“权重同步前恢复 weights、同步后恢复 KV cache”的具体顺序。
- 缺少 vLLM 与 SGLang 的差异:vLLM 侧偏 IPC/shared-memory bucket,SGLang 侧偏 tensor bucket + HTTP/server weight-sync API。
- 没解释 colocate 的核心矛盾:省 GPU,但必须精细管显存生命周期。
本节参考与延伸阅读
- 源码:
verl/workers/rollout/base.py - 源码:
verl/workers/rollout/llm_server.py - 源码:
verl/workers/rollout/replica.py - 源码:
verl/workers/rollout/schemas.py - 源码:
verl/workers/rollout/tokenizer.py - 源码:
verl/workers/rollout/vllm_rollout/vllm_rollout.py - 源码:
verl/workers/rollout/vllm_rollout/vllm_async_server.py - 源码:
verl/workers/rollout/sglang_rollout/sglang_rollout.py - 源码:
verl/workers/rollout/sglang_rollout/async_sglang_server.py - 源码:
verl/experimental/fully_async_policy/fully_async_rollouter.py - 官方文档:
docs/workers/sglang_worker.rst - 官方文档:
docs/workers/trtllm_worker.rst - 官方文档:
docs/sglang_multiturn/multiturn.rst - 官方文档:
docs/advance/fully_async.md - 外部文档:vLLM Sleep Mode,
https://docs.vllm.ai/en/latest/features/sleep_mode.html - 外部文档:SGLang 官方文档,
https://docs.sglang.ai/