☰
彻底搞懂UniRL训练循环:rollout→reward→advantage→train→sync五步闭环深度图解
2026/10/16 14:57:23 网站建设 项目流程

【免费下载链接】UniRL

UniRL is a Framework for Unified Multimodal Model Reinforcement Learning

项目地址:https://gitcode.com/gh_mirrors/uni/UniRL
点击查看免费下载

UniRL(A Framework for Unified Multimodal Model Reinforcement Learning)是一个面向统一多模态模型的强化学习训练框架。本文用最直白的方式,带你一图一文搞懂 UniRL 的训练循环:每一步在做什么、由哪个模块负责、新手如何跑通第一个 RL 训练任务。无需读懂全部源码,10 分钟即可建立完整的架构认知。

🎯 一句话总结:rollout 生成样本 → reward 打分 → advantage 算优势 → train 更新策略 → sync 回灌权重,如此循环往复,模型逐轮变强。

UniRL 是什么:先看整体架构

UniRL 的核心理念是"一个 RL 后训练循环,跑遍所有多模态模型家族"——无论是 LLM(Qwen3)、VLM(Qwen-VL)、图像扩散(SD3.5、FLUX.2-Klein)、视频扩散(WAN 2.1/2.2、HunyuanVideo),还是统一 AR+Diffusion 模型(BAGEL、HunyuanImage3),用的都是同一套闭环。

从架构上看,系统自顶向下分为四层:

  • 入口层(Entrypoints):train_diffusion、train_ar、train_pe等,负责加载 Hydra 配方并创建 Trainer;
  • 编排层(Trainers):DiffusionTrainer/ARTrainer等"总指挥",决定每个组件放在哪块 GPU 上,并驱动整个训练循环;
  • 组件层:rollout 引擎、reward 服务、算法(GRPO/FlowDPPO/DRPO…)、训练栈;
  • 基础设施层:Ray DevicePool、FSDP、张量传输与权重同步。

下面这张图展示的是"入口如何把一份 YAML 配方变成一支 Worker 舰队",可以帮你快速定位每个组件的代码归属:

组件与源码位置的对应关系一目了然:

组件职责代码位置
Trainer编排五步闭环unirl/trainer/
Rollout 引擎生成样本unirl/rollout/
Reward 服务打分数奖励unirl/reward/
算法库定义损失函数unirl/algorithms/
训练栈优化器 + FSDPunirl/train/
权重同步回灌新权重unirl/distributed/weight_sync/

一图看懂:UniRL 训练循环每一步的数据流动

在进入分步详解之前,先看全景。下面这张图是一次 train_step 内所有数据的完整流动,强烈建议放大对照阅读:

整个循环的骨架其实非常短。以参考实现 DiffusionTrainer 为例,循环主体(train_step)本质上就是这几行顺序调用:

rollout.generate(sample) # ① 生成 reward.score_and_attach(sample) # ② 打分 part.compute_advantages(...) # ③ 算优势 stack.train_track(...) # ④ 训练 weight_sync.sync() # ⑤ 回灌权重

官方模块文档 对"运行时数据流"的官方描述同样是这五步。下面我们逐步拆解。

第一步 Rollout:模型"做题"生成候选样本

Rollout(采样/生成)是整个循环的起点:把一批 prompt 喂给当前策略模型,让它"答一题",产出图像/文本/视频等候选样本。

这一步在 UniRL 中分四个小动作:

  1. 取数据:数据源(data_source)从数据集里取一批 prompt(batch_size个);
  2. 构建请求 Sample:Trainer 把每个 prompt 按samples_per_prompt派生(fork)出 N 个"分支",并携带采样参数(步数、guidance scale、σ 调度、初始噪声配方等)。同一 prompt 的一组 N 个样本就是后面算优势用的"GRPO 组";
  3. 引擎生成:rollout 引擎执行text_embed → diffuse → vae_decode之类的流水线,把空壳 Sample 填上真实的生成结果;
  4. 回传:填好的 Sample 回到驱动端,等待打分。

一个容易被忽略的细节:σ 校验边界

注意图中红框"THE σ-VERIFICATION BOUNDARY":UniRL 会把采样时使用的 σ(噪声)调度钉死在生成的 Sample 上,并要求每个引擎回传校验。为什么这么较真?因为强化学习训练时要重放 rollout 重新计算对数概率,如果引擎和训练端走的轨迹哪怕有微小数值漂移,GRPO 的重要性比率就会悄悄偏离 1,训练目标就错了。所以"不一致就大声报错"比"悄悄训错"好得多。

三种部署模式,同一个 generate 接口

rollout 引擎由配方中的_target_选择,常见有:

  • trainside:训练进程内直接采样,采样权重就是训练权重本身(无需 sync);
  • SGLang / SGLang-Diffusion:独立推理服务器,生成速度更快;
  • vLLM-Omni:面向 BAGEL、HunyuanImage3、SD3.5 等多模态/过程模型的专用后端;
  • composed:串联"AR 改写 + 扩散生成"的编排器(Prompt-Enhancer 场景)。

无论选哪种,接口都是同一个BaseRolloutEngine.generate(sample)(见 unirl/rollout/engine/base.py),对上层 Trainer 完全透明——这就是"可插拔"的含义。

第二步 Reward:把生成结果变成每样本分数

样本生成了,怎么知道好坏?Reward(奖励)服务负责把"图/文/视频"变成一个标量分数,这是 RL 的信号来源。

如上图,RewardService.score_and_attach只走一条后端,两种可选:

  • local(进程内):一个后端一个打分器,比如 PickScore、CLIP、HPSv2、OCR、GenEval2、ImageReward、VideoAlign 等,全部实现在 unirl/reward/local/;
  • remote(HTTP 服务):调一个远程 reward 面板,多个奖励模型加权聚合(weighted_sum / mean / min / max),返回一个总奖励 + 每个模型的明细。配套服务在仓库的 unirl-reward-service/ 目录。

打分结果直接挂回 Sample 上(part.rewards,外加可选的component_rewards分项明细)。图中底部那句总结值得记住:reward 是信号,advantage 才是真正用来训练的。

第三步 Advantage:GRPO 组内 z-score 把分数变成"优势"

有了每样本分数,还不能直接训练——分数有偏置(这个任务本来就难,大家分都低)。GRPO 的做法是组内相对评价:同一个 prompt 生成的 N 个样本为一组,各自减去组均值、除以组标准差:

advantage = (reward - group_mean) / (group_std + eps)

也就是说:比同组兄弟平均好 → 正优势,鼓励这个方向;比兄弟差 → 负优势,抑制。这一步由 Part.compute_advantages 完成,支持两种作用域:

  • scope="group"(默认):每个 prompt 组内归一化,是标准 GRPO 语义;
  • scope="global":整批归一化,可通过adv_use_global_std微调。

💡 这也解释了为什么第二步的 fork 必须"同组样本连续排列"——组内 z-score 依赖样本的父子血缘关系(sample id 里就编码了 prompt 谱系)。

优势算完,交给哪个损失函数?

UniRL 算法层的设计很巧妙:算法 = 一个损失函数 + 几个声明式"旋钮"。四个损失家族可在同一训练栈上互换:

算法核心思想源码
GRPOPPO 裁剪比率 + 组优势unirl/algorithms/grpo.py
Flow-DPPO面向 Flow Matching 的散度信任域掩码unirl/algorithms/flowdppo.py
DRPOtoken 级平滑二次正则unirl/algorithms/drpo.py
DiffusionNFT双适配器重构 MSE(无需重放)unirl/algorithms/diffusionnft.py

图中上半部分还标出了训练时的关键输入:重放(replay)用的冻结锚点 π_old + 每样本优势,输出就是标准的策略梯度损失 → 反向传播 →optimizer.step()。这些"旋钮"(如requires_ema_rollout、supports_multi_update)由训练栈在启动时读取,决定循环如何被重配——新手只需知道:换算法基本等于换配方里的一个_target_。

第四步 Train:FSDP 分片下的 mini-batch 更新

拿到带优势的 Sample,进入唯一的"动权重"环节:训练栈TrainStack把生成的 Part 通过DP_SCATTER切分到各训练 worker,每个 worker 持有同一模型的 FSDP 分片、一个优化器和一个损失算法。

这步的三个关键点:

  • 重放对齐:stack.train_track(...)会先prepare_segment(冻结 π_old 锚点)再前向,保证重放轨迹与 rollout 时逐位一致——呼应第一步说的 σ 校验;
  • mini-batch 多次更新:stack.num_updates_per_batch = N会把本批 rollout 切成 N 个不相交的 mini-batch 各跑一次 optimizer step(不是跑 N 个 epoch),用于提高样本效率;
  • 跨 rollout 累积:accumulate_rollouts可让 M 次 rollout 只走一次 optimizer step(面向多域任务循环的 MOPD 场景)。

训练完成后,wandb_logger.log_rollout_step会记录损失、奖励均值、步耗时等指标,maybe_save_checkpoint按save_interval落盘检查点(支持断点续训)。

第五步 Sync:把新权重推回 rollout 引擎

如果 rollout 用的是独立推理引擎,训练完的"新大脑"必须装回引擎,否则下一轮还是在用旧权重生成。这一步就是虚线"回边"(见架构总览图),也是分布式上最精细的一步。

上图浓缩了三个必知事实:

  1. gather + push 两阶段:FSDP 下读取完整权重是全体 rank 参与的集合通信(all-gather),所以sync()在每个训练 rank 上都会执行(BROADCAST 分发);实际"搬运"只有 rank 0 负责,其余 rank 聚合后丢弃。这也是为什么"只在 rank 0 上 sync"会死锁;
  2. 五种 handler 按 [推什么 × 推去哪] 组合:LoRA 只推小适配器(LocalLoraWeightSync/RemoteLoraWeightSync),Full 推完整权重(NCCLWeightSync跨节点、TensorWeightSync/IPCWeightSync同卡进程内);
  3. trainside 模式没有这一步:它本来就在训练进程内采样,第 0 次更新时比率恒为 1,权重天然新鲜。

同步频率由weight_sync_interval控制(每隔几个 rollout 推一次;async 模式下则结合版本滞后预算自动发布)。

部署模式与分布式拓扑:三种 GPU 摆法

五步闭环的"第⑤步存在与否"取决于 GPU 布局。UniRL 提供三种拓扑,一图看懂:

模式GPU 布局需要 sync?典型场景
trainside(direct)rollout 与 train 同一进程❌ 不需要上手最省事
colocate独立引擎与训练共享 GPU,靠 offload/onload 腾挪显存✅ 需要单卡显存紧张时
separaterollout 与 train 各占一块独立 GPU slab✅ 需要(跨 slab NCCL 握手)吞吐最优

由顶层layout(colocate/separate)+train_fraction/reward_fraction(GPU 切分比例)控制。还有一个新手少踩的坑:重张量永远 worker→worker 直传,绝不经由 driver,所以跨节点跑时记得把save_dir、模型路径放在所有节点都可见的存储上。

新手速查:训练循环关键配置项

结合 examples/README.md 的启动指南,以下是循环层面最常用的旋钮(配方示例见 examples/diffusion/sd3/sd3_trainside.yaml):

配置项含义对应循环步骤
batch_size每轮 prompt 数rollout
sampling.samples_per_prompt每个 prompt 的组大小(GRPO 组)rollout / advantage
num_rollouts总训练轮数(总预算)全局
reward:/reward_stack:打分器或远程奖励面板reward
eval_interval周期性评测(含生成图预览上传)循环外
stack.num_updates_per_batch每批 rollout 的 optimizer step 数train
weight_sync_interval每几个 rollout 向引擎推一次权重sync
save_interval/save_dir检查点节奏与目录train
train_resident/rollout_residentcolocate 下各角色权重是否常驻显存部署

单节点最简启动方式:

# 先干跑校验配方,再真正启动 python -m unirl.train_diffusion --config-name=diffusion/sd3/sd3_trainside --cfg job --resolve bash examples/run_experiment_single_node.sh diffusion/sd3/sd3_trainside

多节点启动与更多入口(AR、agentic、async 等)详见 examples/README.md 与 examples/run_experiment_multinode.sh。

写在最后:回到那 10 行循环

学完这五个环节,回头看 unirl/trainer/ 的总结会发现一切豁然开朗:每个模块都故意"看不见"其他模块(rollout 不知道 reward 是谁、算法不知道引擎是谁),唯一知道全局、负责把五步串起来并决定"谁跑在哪块卡上"的就是 Trainer——这正是循环主体能保持约 10 行、而每个组件都能靠_target_自由替换的原因。

记住这张心智地图,UniRL 的任一新特性(async 训练、agentic 多轮、PE 双栈、检查点续训)都只是在这五步闭环上做重排与解耦,不会再让你迷路 🚀

步骤一句话关键入口
① rollout模型做题,产出 Samplerollout.generate
② reward把输出变成每样本分数reward.score_and_attach
③ advantage组内 z-score 出优势part.compute_advantages
④ trainFSDP mini-batch 更新stack.train_track
⑤ sync新权重推回引擎weight_sync.sync

【免费下载链接】UniRL

UniRL is a Framework for Unified Multimodal Model Reinforcement Learning

项目地址:https://gitcode.com/gh_mirrors/uni/UniRL
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询