Tianyun Zhao$^{\dagger }$, Ao Sun$^{\dagger }$, Changlong Li, Yinghao Chen, Haoxuan Pan, Jinqian Zhang, Zekai Qu, Bingxiang He, ChaoJun Xiao, Xu Han

Github: https://github.com/OpenBMB/Meshy

$\dagger$ : Project lead

$^*$: Core contributors

<aside> ✨

1. 引言

在过去的几年里,LLM RL 系统的工作负载发生了巨大的变化。早期的 RLHF 围绕同步训练展开:模型按固定顺序进行生成、打分和训练,每个阶段之间有着明确的同步边界。如今,系统还需要处理异步生成、持续训练、多轮 Agent 以及与外部环境的交互——那些在同步执行中原本隐含的状态、版本和故障边界,现在都必须显式地进行管理。

以 verl[1] 为典型代表的 Single-Controller 架构,用顺序程序表达异构的分布式计算,有效解决了同步 RLHF 时代最重要的编排问题。但一旦执行不再严格同步、任务开始跨越多轮训练,Single-Controller 就会逐渐从简化系统的抽象,变成数据传输和任务调度的阻碍。

越来越多的系统正在把通用的分布式编排框架从核心执行引擎的默认依赖中剥离,类似的解耦也发生在推理基础设施中。vLLM 在 V1 引擎中为多机张量并行和流水线并行提供了不依赖 Ray 的原生执行路径:各节点分别启动 vLLM 进程,由 PyTorch torch.distributed 建立跨节点进程组。

Meshy 正是在这一背景下提出的:我们将 Inference、Training、Rollout 和 Teacher 等角色建模为独立服务,不再将分布式编排框架作为 RL 系统的中心。所有样本数据通过统一的 TransferQueue[4] 数据面在服务之间流动,控制流程由数据来驱动,拓扑则由各进程在启动时在本地按相同配方推导,因此使得 RL 的流程更接近预训练框架常用的 SPMD 设计。

Meshy 结构天然支持全异步训练,同时兼容传统的同步训练。相比 Single Controller 设计,Meshy 避免了诸多的 RPC 调用,摆脱了沉重的分布式框架 Ray,在故障排查、性能、持续集成和代码简洁度上,都具有较大的优势。

2. 背景

2.1 经典 RLHF 流水线

以 PPO 为例,一轮传统的 RLHF 训练大致如下:

Prompts
   |
   v
Actor Rollout ──> Reference / Reward / Critic Forward
   |
   v
Advantage Estimation ──> Actor & Critic Update ──> Next Iteration

这条流水线有三个重要特征。首先,各个阶段之间的同步是显式的,整个 batch 的 rollout 结束后才计算 reward 和 advantage,参数更新完成后下一轮才使用新权重。其次,同步 RLHF 的控制流固定,每一轮跑哪些阶段、阶段之间如何依赖,在训练开始前就已确定。第三,轨迹是一次性生成的,绝大多数样本是单次生成的结果,无需跨轮维护环境状态。因此,传统 RLHF 的计算组件虽然是异构的(Actor、Critic 和 Reward Model 可能使用不同的引擎和并行策略),但它的控制流是同步、规则、可预测的。换句话说,系统在任意时刻都处于某个确定的阶段——系统的状态变化遵循一条全局统一的执行时序

2.2 Single-Controller:以顺序程序表达多机同步RL

训练和推理引擎内部通常遵循 SPMD 范式:各个进程运行同一份程序,进程间通过集合通信进行协作。SPMD 很适合表达单个模型的前向传播、反向传播,却难以表达顶层的 RL 流程——Rollout, Inference, Trainer 分属不同引擎、不同并行布局,SPMD 程序难以描述复杂的数据流向和执行顺序。因此,RL 框架通常需要在引擎内部的 SPMD 执行之上,再增加一层负责跨角色编排的控制面。

Single-Controller 给出的答案非常直接:既然系统遵循单一的全局执行时序,那就用一个顺序程序来编写它。Google 的 Pathways[3](arXiv:2203.12533)最早系统地提出这一设计,verl 的 HybridFlow 将其与 SPMD 执行结合,成为 RL 训练框架的主流形态:顶层控制流集中在一个进程里,每个角色对外表现为一个可调用的对象

for step in range(num_steps):
    sequences  = rollout.generate(prompts)     # 一次“函数调用”
    rewards    = reward.compute(sequences)
    advantages = estimate_advantage(sequences, rewards)
    actor.update(advantages)