vLLM 在 RLHF 训练中的角色:权重同步、Pause/Resume 与异步强化学习实战指南
在 RLHF(基于人类反馈的强化学习)等在线强化学习工作流中,推理引擎需要与训练引擎高频协作:策略模型每更新一轮,推理侧的权重就必须同步刷新,同时生成请求不能因此中断。vLLM 为此提供了可插拔的权重传输系统(Weight Transfer)、飞行中安全换权的 Pause/Resume API 以及一批官方 RLHF 示例脚本。读完本文,你将掌握如何在 vLLM 中配置权重传输后端、如何在训练循环中驱动权重同步,以及如何用异步流水线提升 GPU 利用率。
vLLM 在 RLHF 工作流中的定位
Reinforcement Learning from Human Feedback (RLHF) 是一种利用人类生成的偏好数据微调语言模型、使模型输出与期望行为对齐的技术。在 RLHF 及其他在线 RL 方法(如 GRPO)中,策略模型在训练过程中被迭代更新,而 rollout(采样生成)阶段必须由推理引擎高效完成,并且更新后的权重必须实时反映到推理引擎中,否则 rollout 使用的仍是旧策略,训练将失去意义。
vLLM 承担的就是其中的高速 rollout 角色。当前开源生态中,多个主流 RL 库都使用 vLLM 来加速 rollout(按字母序,非穷举):Cosmos-RL、ms-swift、NeMo-RL、Open Instruct、OpenRLHF、PipelineRL、Prime-RL、SkyRL、TRL、Unsloth、verl 等。vLLM 官方文档同时提供了 GRPO 的在线训练实践 notebook(例如基于 TRL 的高效在线训练、基于 Unsloth + vLLM 的 Qwen-3 4B GRPO),可作为入门参考(参见 docs/training/rlhf.md)。
要让 vLLM 真正融入 RL 训练循环,需要解决两个核心问题:
- 权重如何从训练进程同步到推理引擎? —— vLLM 的 Weight Transfer 系统 解决这一点,提供 NCCL(多卡分离部署)与 IPC(同卡共置部署)等可插拔后端;
- 生成过程中如何安全地换权? —— Async Reinforcement Learning 指南介绍的
pause_generation/resume_generationAPI 解决这一点,实现生成与训练的流水线化。
权重传输系统:架构与四阶段协议
vLLM 的权重传输系统位于 vllm/distributed/weight_transfer/,其核心设计是两个进程各持一个引擎、且两者对称:
| 训练进程(Trainer process) | 推理工作进程(Inference workers) | |
|---|---|---|
| 类 | TrainerWeightTransferEngine |
WeightTransferEngine |
| 构建者 | WeightTransferTrainerFactory.trainer_init(...) |
vLLM 内部,从 WeightTransferConfig 构建 |
| 驱动方式 | send_weights() |
下方的四阶段协议 |
| 持有物 | 通信器、传输计划、wire 参数 | 通信器、目标模型 |
训练侧引擎是有状态的:它持有自己的通信器与 wire 参数,从 WeightSource 中拉取权重,并通过 VLLMWeightSyncClient 驱动推理侧。训练代码无需了解底层传输协议,每个同步轮次只需一次 send_weights() 调用。
每一轮同步在底层都走同一套四阶段协议:
- 初始化(
init_weight_transfer_engine):在训练循环开始前调用一次(从trainer_init触发),建立训练进程与推理工作进程之间的通信通道; - 开始(
start_weight_update):让推理引擎为一轮权重更新做好准备; - 权重更新(
update_weights):实际传输更新后的权重,可调用一次或多次(例如分块传输); - 完成(
finish_weight_update):收尾(例如对 checkpoint 格式的权重做后处理),所有权重传完后调用一次。
可选的传输后端
| 后端 | 传输机制 | 适用场景 |
|---|---|---|
nccl |
NCCL broadcast | 训练与推理分布在不同 GPU 上(可跨节点) |
ipc |
CUDA IPC handles | 训练与推理共置在同一 GPU 上 |
sparse_nccl |
NCCL broadcast | 基于 checkpoint 坐标的稀疏权重补丁 |
sharded_rdt |
NIXL / Ray Direct Transport(拉取式) | 超大规模模型,每个 worker 只需自己那片分片(如专家并行的 MoE) |
推理侧的配置只暴露一个后端选择器。WeightTransferConfig 的定义见 vllm/config/weight_transfer.py,其中 backend 字段为字符串选择器(默认 "nccl"),在引擎创建时对照 WeightTransferEngineFactory 注册表做校验;其余所有传输细节( rendezvous 参数、打包参数等)都由训练侧决定并在初始化握手中下发,推理侧不需要也不能与之“不一致”。
快速上手:两侧各写多少代码?
推理侧:只声明后端
from vllm import LLM
from vllm.config import WeightTransferConfig
llm = LLM(
model="my-model",
weight_transfer_config=WeightTransferConfig(backend="nccl"), # 或 "ipc"、"sparse_nccl"、"sharded_rdt"
)
在线服务场景则直接用命令行:
vllm serve my-model \
--weight-transfer-config '{"backend": "nccl"}'
训练侧:建一次引擎,每轮调一次 send_weights()
from vllm.distributed.weight_transfer import (
ModuleSource,
HTTPVLLMWeightSyncClient,
WeightTransferTrainerFactory,
)
from vllm.distributed.weight_transfer.nccl_engine import NCCLTrainerInitInfo
# 在训练循环开始前,只构建一次。
engine = WeightTransferTrainerFactory.trainer_init(
init_info=NCCLTrainerInitInfo(
master_address=master_address,
master_port=master_port,
world_size=world_size, # 训练进程 + 全部推理 worker 的总数
rank=0, # 本训练进程的 rank;rank 0 是发送方
packed=True,
),
client=HTTPVLLMWeightSyncClient("http://localhost:8000"),
source=ModuleSource(model),
)
# 每个权重同步轮次调用一次。
for step in range(num_steps):
train_one_step(model)
engine.send_weights()
send_weights() 会同时在推理侧驱动 start → update → finish,并完成数据面传输,包括后端所需的并发编排(例如 NCCL 后端要求 worker 的 update_weights 与训练侧的 broadcast 同时执行,因为两侧在同一个 NCCL 集合调用内汇合/rendezvous)。训练侧没有 backend= 参数:每个 TrainerInitInfo 子类自带一个 ClassVar 形式的 backend,工厂按它分发。
NCCL 后端:分离部署与打包广播
NCCL 引擎(vllm/distributed/weight_transfer/nccl_engine.py)适用于训练与推理分卡(甚至跨节点)的场景。详细文档见 docs/training/weight_transfer/nccl.md。
工作原理:
- 训练进程与所有推理 worker 通过
StatelessProcessGroup(vLLM 提供的、不依赖 torch.distributed 全局组的抽象)加入同一个 NCCL 进程组。训练侧是 rank 0,worker 从rank_offset(1)开始编号; - 训练侧向所有 worker 同时广播权重,每个 worker 接收并加载;
- 可选的 packed tensor 广播:把多个小张量打包进大缓冲,配合双/三重缓冲与 CUDA stream 重叠,减少 NCCL 调用次数、提升吞吐。
关键参数在 NCCLTrainerInitInfo 上:
| 字段 | 默认值 | 说明 |
|---|---|---|
master_address |
— | rendezvous 主机地址 |
master_port |
— | rendezvous 端口 |
world_size |
— | 训练侧 + 全部 worker 的 NCCL 组大小 |
rank |
— | 本训练进程的 rank(关键字参数);0 是发送方 |
packed |
True |
是否使用打包广播 |
packed_buffer_size_bytes |
1 GiB | 打包缓冲大小 |
packed_num_buffers |
2 | 轮换缓冲数量(双/三重缓冲) |
从源码结构看,这里有一条重要的设计不变量:wire 参数(如 packed)故意不放在 WeightTransferConfig 或每轮的 update_weights 载荷里——训练侧在 trainer_init 时下发,worker 只读取被告知值。这样两侧 packed 标志不一致的状态是“不可表达的”,而不是“被不鼓励的”。NCCL 是少数同时读取 WeightSource 的 metadata() 与迭代字节流两条通道的后端:引擎按 metadata() 构造每轮 update info 先发给 worker 用于确定接收缓冲尺寸与分块边界,字节本身来自迭代 source;发送端会逐参数比对迭代结果与声明元数据,一旦发散立即抛出异常(指明首个不一致的参数),而不是让坏数据上网络。ModuleSource 天然满足该不变量;自写 WeightSource(如 Megatron 导出、MoE 重融合)时应把它作为首要测试项。
内存方面:轮换缓冲在整个传输期间都存活,两侧各占用 packed_buffer_size_bytes * packed_num_buffers(默认配置下约 2 GiB),显存紧张时调小 packed_buffer_size_bytes。
Sparse NCCL:只传变化的元素
sparse_nccl 后端(vllm/distributed/weight_transfer/sparse_nccl_engine.py)用于按 checkpoint/Hugging Face 坐标表达稀疏、扁平索引的权重补丁:每个推理 rank 收到相同的 checkpoint 全局补丁,再由模型原生 load_weights() 映射到本 rank 的 TP/EP 与打包运行时参数。它是 delta 型后端——每次调用携带的是替换补丁而非模型参数的稳定流,因此不接受 WeightSource,补丁直接传给 send_weights(patches),空补丁列表是 no-op,单次调用即完整走一遍 start / update / finish。传输量为 O(nnz)(只发索引与值),但 checkpoint 应用本身仍走原生 loader 的 O(N) staging。支持张量并行且无需与训练侧布局一致。示例见 examples/rl/rlhf_sparse_nccl.py(Qwen3 MoE 逐专家 checkpoint 更新,1 个训练 GPU + 2 个 TP2/EP2 推理 GPU)。
IPC 后端:同卡共置、零拷贝
IPC 引擎(vllm/distributed/weight_transfer/ipc_engine.py)通过 CUDA IPC 句柄让训练与推理进程在同一 GPU 上直接共享显存,避免任何数据拷贝,是共置部署下最高效的选择;多 GPU 场景(如 FSDP)也支持,每个 GPU 会先 all-gather 出完整权重,再由正确的共置进程提取。详细文档见 docs/training/weight_transfer/ipc.md。
流程概述:
- 训练侧为每个权重创建 CUDA 张量并用
torch.multiprocessing.reductions.reduce_tensor生成 IPC 句柄;多 rank 时(FSDP)每个 rank 先用ModuleSource在自己的 GPU 上物化完整张量; - 所有训练 rank 的句柄经默认进程组 all-gather 合并(按 GPU UUID 映射),由发送方通过 client 下发,每个 worker 只读取自己 GPU 对应的句柄;
- 推理 worker 用
rebuild_cuda_tensor从句柄重建张量,直接读训练侧显存。
训练侧代码与 NCCL 几乎同构,只是换成 IPCTrainerInitInfo:
from vllm.distributed.weight_transfer.ipc_engine import IPCTrainerInitInfo
engine = WeightTransferTrainerFactory.trainer_init(
init_info=IPCTrainerInitInfo(rank=0, packed=False), # rank 0 是发送方
client=HTTPVLLMWeightSyncClient("http://localhost:8000"),
source=ModuleSource(model),
)
engine.send_weights() # 每个同步轮次调用一次
IPCTrainerInitInfo 的字段:
| 字段 | 默认值 | 说明 |
|---|---|---|
rank |
— | 本训练进程的 rank(关键字参数);0 是发送方 |
packed |
False |
启用分块、内存有界的传输 |
packed_buffer_size_bytes |
1 GiB | packed=True 时的分块大小 |
两个实战要点:
-
分块传输:默认所有权重在一次
update_weights中发完,要求两侧 GPU 同时装得下完整模型。packed=True后权重被拼接进固定大小缓冲、按块分多次update_weights发送(仍在一对 start / finish 括号内,层式重载路径只初始化一次、收尾一次),每块被消费后即可释放显存:engine = WeightTransferTrainerFactory.trainer_init( init_info=IPCTrainerInitInfo( rank=0, packed=True, packed_buffer_size_bytes=256 * 1024 * 1024, # 256 MB 分块 ), client=client, source=ModuleSource(model), )多 rank 训练时,生产侧各块之间带跨 rank 屏障,防止某 rank 覆盖缓冲时共置 worker 仍在读取当前块,该逻辑内建在
send_weights()中。 -
HTTP 传输警告:IPC 句柄是序列化 Python 对象,走 HTTP 时必须在服务端与客户端都设置
VLLM_ALLOW_INSECURE_SERIALIZATION=1(句柄会被 pickle 并 base64 编码传输)。
HTTP 服务端的权重同步端点
以 HTTP server 方式运行 vLLM 时,权重传输暴露以下端点(HTTPVLLMWeightSyncClient 会替你调用前四个):
| 端点 | 方法 | 说明 |
|---|---|---|
/init_weight_transfer_engine |
POST | 用后端特定信息初始化权重传输引擎 |
/start_weight_update |
POST | 开始一轮权重更新 |
/update_weights |
POST | 传输一批带后端特定元数据的权重 |
/finish_weight_update |
POST | 完成更新,可选择提交 weight_version |
/update_weight_version |
POST | 不改变模型权重、只更新 weight_version |
/weight_info |
GET | 查询最新已提交的权重版本 |
/pause |
POST | 权重同步前暂停生成,处理在途请求 |
/resume |
POST | 权重同步后恢复生成 |
/get_world_size |
GET | 获取推理 worker 数量(便于计算 NCCL world size) |
注意:HTTP 权重同步端点要求设置 VLLM_SERVER_DEV_MODE=1。此外,Rust 前端的可选 gRPC Control 服务为可信 sidecar 暴露同样的 pause、sleep、权重传输与权重版本生命周期;ServerInfo.rl_capabilities 响应会报告是否配置了权重传输与 sleep mode。无论走 HTTP 还是 gRPC,后端特定的 init_info / update_info 都是 JSON 元数据,张量本身始终走配置的 NCCL、IPC、sparse-NCCL 或 sharded-RDT 传输。
多 rank 训练器:FSDP / TP / PP / EP 下如何同步
对于分片式训练器(FSDP、TP/PP/EP),每一个训练 rank 都要构建引擎并调用 send_weights():
- Rank 0 是发送方:只有它持有通信器、与 client 对话、真正把字节放上网络;
- 非发送 rank 也必须迭代
WeightSource——因为物化一个参数本身通常就是集合通信(如 FSDP 的full_tensor()all-gather、Megatron 导出),若某些 rank 跳过就会死锁。
每个进程在 init info 中传入自己的 rank。它是显式参数而非从全局进程组读取,因为同时存在 FSDP / TP / PP / EP 多个进程组时,全局组语义是模糊的:
engine = WeightTransferTrainerFactory.trainer_init(
init_info=NCCLTrainerInitInfo(..., rank=torch.distributed.get_rank()),
client=client,
source=ModuleSource(model),
)
engine.send_weights() # 在所有 rank 上调用
多 rank 场景的完整可运行示例:examples/rl/rlhf_nccl_fsdp_ep.py(NCCL + FSDP2 + 专家并行,每个 FSDP rank 都构建引擎并参与 full_tensor() gather,仅 rank 0 触网)与 examples/rl/rlhf_ipc_fsdp_ep.py(IPC + FSDP2 共置于 4 张 GPU,带 packed 分块与传输前后的 sleep/wake)。
异步 RL:Pause/Resume API 与典型循环
在标准 RL 循环中,生成与训练顺序执行:策略生成 rollout → 训练 → 循环。生成期间训练卡闲置,反之亦然。**一次性流水线(one-off pipelining)**方案把生成与训练拆成两个并行协程——一边用旧数据训练,一边用当前策略生成新样本,从而提升 GPU 利用率与训练吞吐。
但重叠引入了一个难题:必须在推理引擎仍在处理请求时、"飞行中"(mid-flight)更新权重。vLLM 的解法是 pause_generation 与 resume_generation(见 docs/training/async_rl.md)。
pause_generation
await engine.pause_generation(mode="keep", clear_cache=True)
mode 决定在途请求如何处置:
| Mode | 行为 |
|---|---|
"abort" |
立即中止所有在途请求并返回部分结果(默认) |
"wait" |
等待所有在途请求完成后再暂停 |
"keep" |
将请求冻结在队列中,调用 resume_generation 时继续 |
clear_cache 决定暂停后是否清空 KV cache 与前缀缓存。
resume_generation
await engine.resume_generation()
暂停后恢复调度器。以 mode="keep" 冻结的请求将继续生成。
HTTP 端点
设置 VLLM_SERVER_DEV_MODE=1 后,vLLM HTTP server 通过以下方式暴露相同能力:
POST /pause?mode=keep— 暂停生成POST /resume— 恢复生成POST /abort_requests— 中止在途请求但不暂停调度器(发{}中止全部,或{"request_ids": [...]}指定)GET /weight_info— 返回最新已提交的weight_version
关于数据并行的一个重要区分:使用 vLLM 内建负载均衡器(data_parallel_backend="ray")时,pause/resume 会自动在所有 DP rank 上生效,调用一次即可;使用外部负载均衡器(多个独立 vLLM 实例置于代理之后)时,必须在权重更新前后向每一个引擎实例单独发送 pause 与 resume 请求。
典型异步 RL 循环
- 用当前策略开始生成 rollout;
- 训练器产生新权重后,以
mode="keep"暂停生成; - 将训练器侧的新权重同步到推理引擎(Weight Transfer 系统);
- 恢复生成——在途请求继续,且带上新权重;
- 重复。
这里的关键洞察是:以 mode="keep" 暂停的请求,在暂停前产出的 token 来自旧权重,恢复后产出的 token 来自新权重。clear_cache 决定 KV cache 是否被失效:clear_cache=True 时之前缓存的键值对全部丢弃,恢复后生成的每个 token 都完全由新权重计算;clear_cache=False 时保留既有 KV cache 条目,上下文中部分 token 仍反映旧权重(陈旧 KV cache)。是否可接受取决于算法对一致性的要求——GRPO 类方法通常按 rollout 批次边界对齐权重版本,此时 clear_cache 的取舍直接影响同一请求内新旧 token 的混合程度。
官方 RLHF 示例:从哪份代码入手
examples/rl/ 目录下有一组端到端可运行的 RLHF 示例,覆盖各后端与部署形态,是最直接的实战入口:
| 示例 | 说明 |
|---|---|
| rlhf_http_nccl.py | 分离部署的起点。训练器占 1 张 GPU,2× 张量并行 fp8 服务占另外 2 张;HTTP 控制面 + NCCL 数据面,脚本自行拉起并销毁 server |
| rlhf_http_ipc.py | 共置部署的起点。server 与训练模型共享单张 GPU;HTTP 控制面 + CUDA IPC 数据面,同样自管理 server 生命周期 |
| rlhf_async_new_apis.py | 异步权重同步的完整演示:vllm.AsyncLLMEngine + NCCL 传输 + 飞行中暂停/换权/恢复,并开启 batch invariance 做确定性校验——把换权后的输出与一个直接加载训练模型的新 vLLM 实例逐 token 比对(NVIDIA 要求 100% 精确匹配,ROCm 放宽到 90%) |
| rlhf_nccl_fsdp_ep.py | NCCL + FSDP2 + 专家并行的多 rank 训练器示例 |
| rlhf_ipc_fsdp_ep.py | IPC + FSDP2,4 张 GPU 上与 --data-parallel-size 4 server 共置 |
| rlhf_sparse_nccl.py | sparse NCCL 的逐专家 checkpoint 补丁(Qwen3 MoE) |
以 rlhf_async_new_apis.py 为例,其核心编排清晰展示了异步 RL 的骨架:训练模型(Qwen3-1.7B)与推理引擎(Qwen3-1.7B-Base)分别放在不同 GPU 的 Ray actor 中,训练侧用 RayVLLMWeightSyncClient 直连引擎对象;生成一批请求后,任一请求达到 token 阈值即 pause_generation(mode="keep"),调用 broadcast_weights()(内部即 engine.send_weights()),再 resume_generation();脚本随后打印每个请求换权前后分别产出的 token 段,并用新模型实例验证换权后的 token 与直接加载新权重的输出完全一致。
延伸阅读与扩展点
- 架构细节、四阶段协议与“每个配置项该放在哪一侧”的完整表格:docs/training/weight_transfer/README.md
- 基类(
WeightSource、VLLMWeightSyncClient、两个引擎 ABC 及其工厂注册表,全部可替换):docs/training/weight_transfer/base.md - 超大模型的拉取式分片传输:docs/training/weight_transfer/sharded_rdt.md
- NCCL / IPC 后端的参数与注意事项:nccl.md、ipc.md
- 入口 API 的导出列表:vllm/distributed/weight_transfer/init.py
整个系统的所有部件都可替换:要发送的权重(WeightSource)、控制面传输(VLLMWeightSyncClient,内置 HTTP 与 Ray 两种 client,也可自行适配)、以及传输本身(两个引擎 ABC 各自带工厂注册表)。对于自建 RL 基础设施的团队,这意味着可以把 vLLM 的权重同步层无缝嵌入自有的训练框架——只需实现自己的 WeightSource 或 TrainerInitInfo 子类,即可复用四阶段协议、多 rank 语义与全部 HTTP/gRPC 端点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00