Chapter 05

HybridFlow 架构详解

📌 本章基于 HybridFlow 论文( arXiv:2409.19256 )+ verl/single_controller/ 、 verl/workers/ 、 verl/trainer/ppo/ray_trainer.py 源码 · commit 9c38b8bb1876 。

5.1 HybridFlow 想解决的问题

之前的 RLHF 框架(DeepSpeed-Chat、OpenRLHF)有两个共同痛点:

  1. 算法和系统耦合。算法工程师改 PPO loss,往往要懂 Ray Actor / DeepSpeed engine 才能改;
  2. 计算与采样切换时显存浪费。训练时 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 的最大设计差异:

维度OpenRLHFverl 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 选择把它们放在同一片卡上分时复用

sequenceDiagram participant GPU as GPU 0-7 Note over GPU: 训练阶段 GPU->>GPU: FSDP/Megatron actor
(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 单步)

sequenceDiagram participant C as Controller participant A as Actor (training) participant R as Rollout (vLLM) participant Ref as Reference participant Rwd as Reward C->>A: 1. 切到推理模式(resharding) A-->>R: weight resharding 完成 C->>R: 2. generate_sequences(prompts) R-->>C: rollouts(responses) C->>R: 3. 释放 KV cache R-->>A: weight 回到训练布局 C->>Ref: 4. compute_log_prob(rollouts) Ref-->>C: ref_logp C->>Rwd: 5. compute_reward(rollouts) Rwd-->>C: rewards C->>C: 6. compute advantages (GRPO 组内归一) loop ppo_epochs C->>A: 7. update_actor(rollouts, advantages) end

整套流程对应 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 的具体性能差异

对比项OpenRLHFverl
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 / checkpointactor / rollout 异机分离 actor.engine → checkpoint engine(NCCL/Mooncake/NIXL)→ 跨节点送到 rollout

naive 模式的 5 步流程(engine_workers.py:689–734):

  1. rollout.resume(tags=["weights"])(line 699)—— 让 vLLM/SGLang server 从 sleep 醒回来,给"weights"留位置;
  2. actor.engine.get_per_tensor_param()(line 703–704)—— 一个生成器,逐张量 yield (name, tensor)。FSDP 这里做 DTensor all-gather,Megatron 做 PP broadcast;
  3. rollout.update_weights(per_tensor_param, peft_config=..., global_steps=...)(line 721)—— 流式推给 server adapter;
  4. (可选)actor.engine offload 到 CPU 释放 GPU(param_offload=True 时);
  5. 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_levelresume(tags) 的语义

读 rollout 配置时经常碰到 sleep_level: 1 / sleep_level: 2,意思常被误解:

sleep_levelrelease 时释放什么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:150BaseEngine 定义:

def get_per_tensor_param(self) -> Generator[Tuple[str, Tensor], None, None]:
    """yield (name, tensor) one by one; 各后端各自实现 all-gather / broadcast"""

FSDP 实现要点:

Megatron 实现要点:

5.13 实验性:fully_async_policy

5.7 已经提到 fully_async,这里展开它的真实数据流(verl/experimental/fully_async_policy/):

flowchart LR RolloutCluster["Rollout cluster
(独立 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 路径的关键差异:

  1. rollout 和 train 不共卡——分两套 ResourcePool;
  2. trajectory 流式产出——rollout 持续往 TransferQueue 推,train 持续从 ReplayBuffer 拉;
  3. 权重同步走 NCCL 直接 broadcast 到 rollout 进程——不经过 checkpoint engine,延迟比 disaggregated 模式低;
  4. 可设 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_asyncone_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 这章给你留下什么