HybridFlow 架构详解
5.1 HybridFlow 想解决的问题
之前的 RLHF 框架(DeepSpeed-Chat、OpenRLHF)有两个共同痛点:
- 算法和系统耦合。算法工程师改 PPO loss,往往要懂 Ray Actor / DeepSpeed engine 才能改;
- 计算与采样切换时显存浪费。训练时 actor 用 fp32 master、采样时 actor 转 fp16 + KV cache,两套权重并存。
HybridFlow 给出的两件武器:
| 武器 | 对应代码 |
|---|---|
| Hybrid Controller | controller / WorkerGroup 抽象(第 4 章已讲) |
| 3D-HybridEngine | actor / rollout 共置 + 自动 weight resharding |
5.2 Hybrid Controller:算法和系统解耦
这是 verl 区别于 OpenRLHF 的最大设计差异:
| 维度 | OpenRLHF | verl HybridFlow |
|---|---|---|
| 控制流 | 每个角色(actor/critic/rollout)各自跑 Ray Actor,互发消息 | 单进程 controller 调度 |
| 算法代码 | 看起来像 Ray actor 间消息流 | 看起来像单机顺序代码 |
| 典型 PPO 主循环 | 多文件 / 多 Actor 协同 | 单个 RayPPOTrainer.fit() |
| 添加新算法 | 要懂 Ray | 只动 controller 单一文件 |
实际看 verl/trainer/ppo/ray_trainer.py · RayPPOTrainer.fit(),PPO 主循环就是一个 for 循环 + 几个 RPC 调用。这就是 hybrid controller 的承诺。
5.3 3D-HybridEngine:actor / rollout 同卡共存
普通 RLHF 框架:
$$ \text{actor 占显存} = P_\text{train}\ (\text{含 fp32 master, m, v}) \qquad \text{rollout 占显存} = P_\text{infer}\ (\text{含 fp16 + KV cache}) $$
如果 actor 和 rollout 各占一片卡,总显存 = $P_\text{train} + P_\text{infer}$。verl 选择把它们放在同一片卡上分时复用:
(fp32 master + bf16) Note over GPU: ★ Resharding ★ GPU->>GPU: 卸载 optimizer state
把 actor 权重 → vLLM 内存布局 Note over GPU: Rollout 阶段 GPU->>GPU: vLLM engine 跑 generate() Note over GPU: ★ Resharding 回去 ★ GPU->>GPU: vLLM KV cache 释放
恢复训练 actor Note over GPU: 训练阶段 GPU->>GPU: 训练 micro batch
关键技术挑战:"训练的 actor 权重布局" ≠ "vLLM 推理的权重布局",必须做权重 resharding。
5.4 权重 resharding 的"3D"
所谓 3D:
| 维度 | 训练侧 | 推理侧 |
|---|---|---|
| Tensor Parallel | Megatron-Core 风格 TP | vLLM 自家 TP |
| Pipeline Parallel | Megatron PP | vLLM 不分 stage |
| Data Parallel | FSDP / Megatron DP | 每个 DP rank 独立 generate |
每次训练 → rollout 切换,必须做三维布局变换,把分片的训练权重凑成 vLLM 期望的格式。verl 的 verl/workers/sharding_manager/ 就在干这件事。
5.5 完整数据流(GRPO 单步)
整套流程对应 RayPPOTrainer.fit() 里的一个 for 循环。这就是 HybridFlow 的"看起来像单机"的代码体验。
5.6 ResourcePool 的放置策略
verl 支持三种放置:
| 放置 | 谁和谁共置 | 典型场景 |
|---|---|---|
| Colocated(默认) | actor + rollout 同卡 | 显存允许的常规训练 |
| Disaggregated | actor 一片卡,rollout 另一片 | 大模型 + 高吞吐 rollout |
| Hybrid | 部分共置部分分离 | 专家路由 / 中等规模 |
在 YAML 里通过 ResourcePool 配置实现:
actor_rollout_ref:
hybrid_engine: true # colocated
# 或
actor_rollout_ref:
hybrid_engine: false # disaggregated
rollout:
n_gpus_per_node: 4
actor:
n_gpus_per_node: 4
5.7 异步采样(fully_async)
同步路径:rollout 完后才能训。verl/experimental/fully_async_policy/ 实验性支持异步——rollout 持续跑、actor 间断更新,吞吐显著提升但要容忍 stale data。
开启方式:
actor_rollout_ref.rollout.mode=async
新手不用碰,了解概念即可。
5.8 HybridFlow 与 OpenRLHF 的具体性能差异
| 对比项 | OpenRLHF | verl |
|---|---|---|
| actor / rollout 是否共置 | 默认分离 | 默认共置(hybrid engine) |
| 显存峰值 | $P_\text{train} + P_\text{infer}$ | $\max(P_\text{train}, P_\text{infer})$ |
| 训练 → rollout 切换开销 | 0(不切,靠 weight sync) | 有 resharding,但通过 streaming 隐藏 |
| 训练后端 | DeepSpeed | FSDP / Megatron |
| 采样后端 | vLLM | vLLM / SGLang / TensorRT-LLM |
在同样硬件上,verl 通常显存使用更少,但单步训练 wall time 不一定更短(resharding 有成本)。
5.9 学术贡献与生态
| 论文 / 项目 | 说明 |
|---|---|
| HybridFlow (EuroSys'25) | 核心方法论文 |
| verl-recipe | 各种业界 R1 / DAPO recipe 集合(已 submodule) |
| verl-omni | 扩散与全模态后训练 |
| vexact | 零失配的 HF rollout 后端 |
| Mind Lab + Megatron-bridge 训练万亿模型 | 2025/12 实战案例 |
5.10 update_weights():训推权重同步真路径
5.3 那张时序图里的 "★ Resharding ★" 在源码里对应 ActorRolloutRefWorker.update_weights()(verl/workers/engine_workers.py:659 起)。它是 hybrid engine 的核心,每次 rollout 前都会被 controller 调一次。
方法签名(简化):
@register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
def update_weights(self, global_steps: int):
if self.config.weight_sync.mode == "naive":
self._update_weights_naive(global_steps)
else:
self._update_weights_via_checkpoint_engine(global_steps)
两种 mode 的差异:
| mode | 典型用在 | 路径 |
|---|---|---|
naive | actor / rollout 同卡(默认) | actor.engine → DTensor all-gather → 同进程内推给 vLLM/SGLang server adapter |
async / checkpoint | actor / rollout 异机分离 | actor.engine → checkpoint engine(NCCL/Mooncake/NIXL)→ 跨节点送到 rollout |
naive 模式的 5 步流程(engine_workers.py:689–734):
rollout.resume(tags=["weights"])(line 699)—— 让 vLLM/SGLang server 从 sleep 醒回来,给"weights"留位置;actor.engine.get_per_tensor_param()(line 703–704)—— 一个生成器,逐张量 yield(name, tensor)。FSDP 这里做 DTensor all-gather,Megatron 做 PP broadcast;rollout.update_weights(per_tensor_param, peft_config=..., global_steps=...)(line 721)—— 流式推给 server adapter;- (可选)actor.engine offload 到 CPU 释放 GPU(
param_offload=True时); rollout.resume(tags=["kv_cache"])(line 734)—— 让 server 再申请 KV cache 准备 generate。
blocking=False 这一点很关键:controller 发起 update_weights 后立刻拿到 future,可以并行做别的(比如启动数据 prefetch、统计 timer),等到真正调 generate_sequences() 时再 await。
5.11 sleep_level 与 resume(tags) 的语义
读 rollout 配置时经常碰到 sleep_level: 1 / sleep_level: 2,意思常被误解:
| sleep_level | release 时释放什么 | resume 时要"装回"什么 | 典型场景 |
|---|---|---|---|
| 0 | 什么都不放 | — | 不开 hybrid engine |
| 1 | 只释放 KV cache,base 权重留 GPU | 只 resume KV cache | LoRA 训练(base 不变,只换 adapter) |
| 2 | KV cache + 权重都释放(全 sleep) | weights + KV cache 都 resume | 全参 RL(默认) |
resume(tags=...) 是带标签的——只 wake up 指定 tag。所以才会看到 resume(tags=["weights"]) + 后面 resume(tags=["kv_cache"]) 这种分两步唤醒的写法:第一步先腾出权重位置准备接 update,第二步再申请 KV 准备 generate。两步之间,旧 KV 已经被释放,让 update_weights 拷贝有最大可用空间。
对应源码:vLLM 这边是 ServerAdapter._execute_method("wake_up", tags=[...])(vllm_rollout.py:150–162);SGLang 这边是 HTTP 调 /release_memory_occupation 与 /resume_memory_occupation(sglang_rollout.py:270 起)。
5.12 get_per_tensor_param():训练侧的权重提取
这是 hybrid engine "魔法" 的真实施工方。verl/workers/engine/base.py:150 的 BaseEngine 定义:
def get_per_tensor_param(self) -> Generator[Tuple[str, Tensor], None, None]:
"""yield (name, tensor) one by one; 各后端各自实现 all-gather / broadcast"""
FSDP 实现要点:
- 对每个 FSDP-wrapped 参数调
summon_full_params()context 让 DTensor 在 rank 0 拼齐; - 结合"layered summon"——大模型不能一次性 gather 全部参数,按 layer 分组,每组 yield 完之后释放;
base_sync_doneflag 跟踪"base 权重是否已经同步过"——LoRA 路径下,base 同步一次后续只增量推 adapter。
Megatron 实现要点:
- 每个 PP stage 只持有自己的 layer 参数;先 PP broadcast 让所有 PP rank 都拿到本 stage 之外的权重;
- broadcast 完后 rollout 拓扑只需要 DP,TP 部分由 vLLM/SGLang 自己重切;
- 用完立刻释放非本 stage 的参数,避免显存翻倍。
5.13 实验性:fully_async_policy
5.7 已经提到 fully_async,这里展开它的真实数据流(verl/experimental/fully_async_policy/):
(独立 GPU 池)"] TrainCluster["Train cluster
(独立 GPU 池)"] TQ["TransferQueue
(experimental/transfer_queue/)"] Buf["ReplayBuffer"] RolloutCluster -->|trajectory stream| TQ TQ -->|sample mini-batch| Buf Buf -->|train step| TrainCluster TrainCluster -->|NCCL param broadcast| RolloutCluster
三条与默认 hybrid 路径的关键差异:
- rollout 和 train 不共卡——分两套 ResourcePool;
- trajectory 流式产出——rollout 持续往 TransferQueue 推,train 持续从 ReplayBuffer 拉;
- 权重同步走 NCCL 直接 broadcast 到 rollout 进程——不经过 checkpoint engine,延迟比 disaggregated 模式低;
- 可设
staleness_threshold控制 train 用多旧的 rollout 数据。
README 公布的 benchmark:128 卡 Qwen2.5-7B DAPO,相对 colocate sync 提速 2.35×(FSDP)/ 2.67×(Megatron)。代价是 rollout 数据有 1-2 步延迟,对收敛影响很小但调参更难。
5.14 实验性:one_step_off_policy
fully_async 太激进时还有个温和版:verl/experimental/one_step_off_policy/。它把训练和 rollout 的并行度限制为一步——train 用上一步的 rollout,rollout 跑下一步的,永不会更落后。
| 特性 | fully_async | one_step_off_policy |
|---|---|---|
| rollout 延迟 | 可达数十步 | 固定 1 步 |
| 权重同步 | NCCL streaming | NCCL broadcast,一步一次 |
| 资源占用 | 两套 GPU 池 | 两套 GPU 池,但自动 partition |
| 实测加速(Qwen2.5-Math-7B DAPO) | — | +23-40%(Megatron 65.7 vs colocate 60.5 AIME 准确率) |
实战推荐顺序:colocate sync → one_step_off_policy → fully_async。前者最稳,后者最快但需要更细的资源分配能力。
5.15 verl-omni / vexact 概要
2026 上半年 verl 衍生出几个独立仓,本书不展开但需要知道存在:
| 仓库 | 定位 | 什么时候用 |
|---|---|---|
verl-project/verl-omni | 扩散 + 文本 + 图像 + 音频统一 RL 后训练 | 多模态 RL 项目 |
verl-project/vexact | "零失配" rollout 后端:batch-invariant kernel,HF 直接做 rollout | 调试时排除 rollout / training 数值差异 |
verl-project/verl-recipe | 业界 R1 / DAPO / RetTool 等完整 recipe 集合 | 查复现脚本、看怎么搭长 CoT |
vexact 的核心 idea:训练侧 forward 和 rollout 侧 forward 数值上差异(即"训练 logits ≠ rollout logits")是 PPO 不收敛的常见根因——vexact 用同一份 HF transformer 实现两侧 forward,逐 token logits 完全一致,让 importance ratio 永远 ≈ 1。代价是吞吐不如 vLLM/SGLang。建议把它当作"金标",怀疑训推不一致时切过去对比一次。
5.16 这章给你留下什么
- 理解 HybridFlow 的"hybrid" 双关:hybrid controller + hybrid engine;
- 知道为什么 verl 显存更省(actor / rollout 共置);
- 知道权重 resharding 是什么、为什么需要;
- 知道 colocated / disaggregated / hybrid 三种放置如何选;
- 读
RayPPOTrainer.fit()时不会迷失方向。