Skip to content

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 抽象:

python
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() 这个名字,要追:

text
RayPPOTrainer.init_workers()
  -> LLMServerManager.create(...)
  -> async_rollout_manager = AgentLoopManager.create(...)
  -> RayPPOTrainer.fit()
  -> async_rollout_manager.generate_sequences(batch)
  -> AgentLoopWorker / AgentLoop
  -> LLMServerClient.generate(...)
  -> vLLMHttpServer 或 SGLangHttpServer

LLMServerManager:rollout 服务的控制面

verl/workers/rollout/llm_server.py 里有三个关键对象:

对象职责
LLMServerManager创建 rollout replicas,记录 server 地址,提供 client
LLMServerClient给 agent loop 用的客户端,向某个 server 发 generate 请求
GlobalRequestLoadBalancer多 server 之间做 sticky session 和 least-loaded 选择

伪代码是:

python
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

模式直觉工程动机
HYBRIDrollout 与 actor worker 在同一组资源中协作同步 PPO/GRPO 常见,省资源
COLOCATEDrollout server 和训练资源 colocate减少跨进程/跨节点调度,但要精细管显存
STANDALONErollout 使用独立资源池异步、fully async、reward model/teacher server 更灵活

colocate 不是为了代码好看,而是为了吞吐和成本:同一批 GPU 可以在“训练阶段”和“生成阶段”交替使用。但代价是显存必须可释放、可恢复,否则训练状态和 KV cache 会互相 OOM。

vLLM 路径

vLLM async 路径主要看:

  • verl/workers/rollout/vllm_rollout/vllm_rollout.py
  • verl/workers/rollout/vllm_rollout/vllm_async_server.py
  • verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py
  • verl/workers/rollout/vllm_rollout/utils.py

ServerAdapter 是 actor worker 侧持有的 rollout adapter。它负责:

text
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.py
  • verl/workers/rollout/sglang_rollout/async_sglang_server.py
  • verl/workers/rollout/sglang_rollout/http_server_engine.py
  • verl/workers/rollout/sglang_rollout/utils.py

ServerAdapter.update_weights() 走 tensor bucket:

text
actor.engine.get_per_tensor_param()
  -> get_named_tensor_buckets(...)
  -> sglang.srt.weight_sync.update_weights(...)
  -> SGLang server update_weights_from_tensor

SGLang 的 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 权重:

text
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 路径有一个很典型的显存顺序:

text
如果 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 资源彻底拆开:

text
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/

面向源码阅读的 verl 学习文档。