vLLM/SGLang/TRT-LLM在verl中的抽象机制解析
时间:2026-07-06 16:23
verl通过五层抽象(rollout name、BaseRollout、LLMServerManager等)将vLLM、SGLang、TRT-LLM等后端包装成对RL流程友好的契约,保持薄抽象,支持生成参数、多模态、logprobs、PD分离、休眠唤醒及权重同步。
上一篇阐明了 rollout 是训练系统中的推理服务边界。本文继续深入探讨:如果 rollout 是一个服务层,那么 vLLM、SGLang、TRT-LLM 这些推理后端在 verl 框架中究竟是如何被选择、启动、调用以及同步的?
核心观点很明确:verl 并没有让 trainer 直接绑定某个推理引擎的 API,而是通过 `rollout.name`、`BaseRollout`、`LLMServerManager`、`RolloutReplica` 和 backend adapter 这五个层级,将它们封装成一个对强化学习(RL)流程友好的契约(contract)。不过,这个抽象层刻意保持着“薄”的特点——生成参数、多模态支持、logprobs、PD 分离、休眠/唤醒以及权重同步等差异,依然会暴露出来。
先来看看整个抽象层次。阅读下面的示意图时,请重点关注:trainer 端只需选择 rollout backend;而真正将名称翻译为 server replica 和 engine API 的,则是 registry、server manager 和 replica 这三个层。

rollout backend 抽象层
图中第一层是配置:在 `rollout.yaml` 中,`name` 可选 `hf/vllm/sglang/trtllm`,`mode` 默认采用异步模式(async),后面还包含各个后端专属的 `engine_kwargs.vllm/sglang/trtllm`(详见 `verl/trainer/config/rollout/rollout.yaml:1-8` 以及 `verl/trainer/config/rollout/rollout.yaml:130-140`)。配置并非细节——它决定了 trainer 随后会进入哪条 server 适配路径。
### 1. `BaseRollout` 定义的是最小契约
`BaseRollout` 的接口非常精简:`resume(tags)`、`update_weights(weights, **kwargs)`、`release()`,以及同步模式的 `generate_sequences(prompts)`(见 `verl/workers/rollout/base.py:29-80`)。异步模式的 backend registry 则将 `("vllm", "async")`、`("sglang", "async")`、`("trtllm", "async")` 分别映射到对应的 `ServerAdapter`(`verl/workers/rollout/base.py:83-104`)。
这说明 verl 的抽象目标并不是消除所有后端的行为差异,而是让 trainer 能够稳定地表达:“我需要一个 rollout 角色,它能够恢复状态、接收权重、释放资源并生成序列。”至于该角色背后是 vLLM、SGLang 还是 TRT-LLM,则由 server manager 和 replica 负责处理。
`LLMServerManager` 会根据 `rollout_config.name` 和分离部署(disaggregation)配置解析 replica class,计算每个 replica 的 world size 和 replica 数量,随后在混合部署(hybrid)、TRT-LLM 共置(colocated)或独立部署(standalone)三种路径下初始化 server,并创建全局负载均衡器(见 `verl/workers/rollout/llm_server.py:222-340`)。`RolloutReplicaRegistry` 则将 `vllm`、`sglang`、`trtllm` 注册到具体的 replica loader;如果 SGLang 启用了 PD 分离,则会进入 `SGLangPDReplica` 分支(`verl/workers/rollout/replica.py:302-402`)。
### 2. 共同的 generate 契约长什么样
下面这张图将 backend adapter 的共同输入输出压缩在一起。看图时请注意:共同点并非“调用同一个库”,而是都必须从 prompt ids、sampling params、多模态输入和 request id 出发,最终返回 token ids、logprobs、stop reason 等 RL 可消费的字段。

共同的 generate 契约
三条后端路径都呈现出类似的形态。vLLM server 的 `generate()` 接收 `prompt_ids`、`sampling_params`、`request_id`、image/video/audio 以及 processor kwargs,计算 `max_tokens`,构造 vLLM 的 `SamplingParams` 和 `TokensPrompt`,再调用推理引擎生成(`verl/workers/rollout/vllm_rollout/vllm_async_server.py:447-535`)。SGLang server 的 `generate()` 接收相似输入,将 `max_tokens/max_new_tokens` 转换为 SGLang 请求,构造 `GenerateReqInput`,通过 tokenizer manager 发起生成(`verl/workers/rollout/sglang_rollout/async_sglang_server.py:496-606`)。TRT-LLM server 也接收同样类别的输入,构造 TensorRT-LLM 的 `SamplingParams`,然后调用 `llm.generate_async()`(`verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:313-382`)。
由此可见,抽象的核心收益是让上层的 RL 流水线获得稳定的 token output,而无需在 trainer 中到处编写 vLLM/SGLang/TRT-LLM 的分支逻辑。
### 3. 这层抽象在哪里会变薄
下面这张图列出了后端差异会泄漏的地方,与上一张图互为补充:上一张图展示了共同契约,而本图则说明为何不能将共同契约误读为“所有 backend 完全等价”。

后端差异泄漏地图
vLLM 路径使用 `max_tokens`,但也兼容 SGLang 风格的 `max_new_tokens`;它会将 `logprobs` 映射到 vLLM 的 `SamplingParams`,并通过 `TokensPrompt` 组织多模态输入和可选的 LoRA request(`verl/workers/rollout/vllm_rollout/vllm_async_server.py:461-526`)。
SGLang 路径默认采用 `max_new_tokens`,同时兼容 vLLM 风格的 `max_tokens`;它包含 PD 分离分支,prefill 侧会创建 bootstrap room,并同时执行本地 prefill 和远端 decode;此外还会处理 prompt logprobs 和 routing replay(`verl/workers/rollout/sglang_rollout/async_sglang_server.py:507-606`)。
TRT-LLM 路径将 `top_k == -1` 转换为 0,logprobs 在 torch sampler 和 TRTLLM sampler 下的映射方式不同;在 VLM 场景中,会构造包含 `multi_modal_data` 的 input dict,但目前对于 audio 直接抛出 `NotImplementedError`(`verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:325-382`)。
这些差异并非抽象的失败,而是抽象边界真实含义的体现:trainer 不需要了解每个后端的 API 细节,但系统工程师在进行性能调优、错误排查、logprobs 处理或涉及多模态/PD 分离时,必须清楚差异发生在哪一层。
### 4. backend 抽象还包括生命周期
rollout backend 不仅要负责生成,还需要处理训练与推理之间的状态切换。下面这张图将部署模式(placement mode)、生命周期 API 以及权重路径综合在一起。

rollout 生命周期与权重同步契约
`RolloutMode` 将部署模式分为 hybrid、colocated 和 standalone:hybrid 表示 rollout 引擎与训练引擎共享 GPU,并通过权重同步切换上下文;colocated 表示在同一 placement group 中的独立进程;standalone 表示使用单独的 GPU 资源,常用于解耦或 off-policy 场景(`verl/workers/rollout/replica.py:54-68`)。
replica 层提供了 `wake_up()`、`sleep()`、`clear_kv_cache()`、`release_kv_cache()`、`resume_kv_cache()` 等控制方法(`verl/workers/rollout/replica.py:245-299`)。worker 层的 `update_weights()` 进一步决定同步方式:naive 路径从 actor engine 获取 per tensor params 并调用 rollout 的 `update_weights()`;非 naive 路径则交由 checkpoint engine 的 `send_weights()` 处理(`verl/workers/engine_workers.py:663-740`)。
这表明 backend abstraction 的最后一层并非 generate API,而是训练推理状态机。只有权重、KV cache 和 server 生命周期全部衔接起来,rollout 才能成为训练系统的有效组成部分。
### 小结:backend 抽象是稳定契约,而非消除差异
本文从配置、接口、server manager、replica、adapter 和生命周期这五个层次剖析了 rollout backend。可以将其压缩为一句话:
rollout.name 选择后端 -> registry 找到 adapter / replica -> manager 启动 server 并分发请求 -> backend generate 返回 TokenOutput -> lifecycle / weight sync 保证下一轮 rollout 使用正确状态
读完本篇后,读者应该能理解:verl 的 trainer 不直接编写 vLLM/SGLang/TRT-LLM 分支,目的是保持 RL 流水线的稳定性;但具体后端差异仍然会影响 token budget、多模态能力、logprobs、PD 分离、显存状态以及权重同步。下一篇将继续推进到 KV cache、sleep/resume 以及显存复用。
### 本文源码索引
- `verl/trainer/config/rollout/rollout.yaml:1-8`:rollout backend 名称和异步模式配置。
- `verl/trainer/config/rollout/rollout.yaml:130-140`:vLLM、SGLang、TRT-LLM 的 backend kwargs。
- `verl/workers/rollout/base.py:29-104`:`BaseRollout` 接口和异步 backend registry。
- `verl/workers/rollout/llm_server.py:222-340`:`LLMServerManager` 的 replica 创建与负载均衡器初始化。
- `verl/workers/rollout/replica.py:54-68`:hybrid、colocated、standalone 三种 rollout mode。
- `verl/workers/rollout/replica.py:245-299`:replica 生命周期控制方法。
- `verl/workers/rollout/replica.py:302-402`:rollout replica registry 与 SGLang PD 分支。
- `verl/workers/rollout/vllm_rollout/vllm_async_server.py:447-535`:vLLM async server 的 generate 适配。
- `verl/workers/rollout/sglang_rollout/async_sglang_server.py:496-606`:SGLang async server 的 generate 适配与 PD 分支。
- `verl/workers/rollout/trtllm_rollout/trtllm_async_server.py:313-382`:TRT-LLM async server 的 generate 适配。
- `verl/workers/engine_workers.py:663-740`:训练权重同步到 rollout 的路径。