verl训练全流程拆解:从rollout到advantage计算
强化学习在大语言模型后训练中的应用正变得越来越关键,而verl作为专为LLM设计的高效RL框架,其核心流程——尤其是rollout生成与advantage计算——是理解整个训练逻辑的钥匙。本文不讲抽象理论,不堆砌公式,而是带你一步步拆解verl中真实运行的每一步:数据怎么分、模型怎么调、log_prob怎么算、reward怎么来、advantage怎么出。所有内容均基于verl源码实际执行路径,聚焦GRPO这一典型无critic、无reward model的轻量级PPO变体,用可验证的代码片段和明确的数据流向,还原一个完整训练step的真实面貌。
1. 全局视角:一次训练step到底发生了什么
在verl中,一次完整的训练step不是“先rollout再算advantage最后更新”,而是一个高度协同、设备感知、分片驱动的数据流。我们先看整体骨架,再逐层深入。
以单机6卡(trainer.n_gpus_per_node=6)、每步处理60条prompt(data.train_batch_size=60)为例,整个step的核心阶段如下:
阶段一:Prompt分发与并行rollout
60条原始prompt被划分为3组,每组20条,分别送入3个vLLM推理引擎(每个引擎绑定2张GPU)。每个引擎对20条prompt各生成12条响应(rollout.n=12),产出240条完整序列。3个引擎汇总后,共得720条rollout样本。阶段二:多策略log_prob并行计算
这720条样本同时被送入两个计算通道:
▪ 旧策略(actor)通道:计算每条序列中每个token由当前actor模型生成的概率对数(old_log_prob);
▪ 参考策略(ref)通道:计算同一序列由固定参考模型生成的概率对数(ref_log_prob)。阶段三:规则化reward与advantage生成
对每条序列,按预设规则(如答案正确性、长度合规性、格式匹配度)打分,得到token-level reward;
基于该reward,结合折扣因子γ和GAE参数λ,直接计算出每条序列的advantage(无需critic模型输出value)。阶段四:Actor模型梯度更新
利用advantage指导actor模型参数更新,完成一次policy优化。
整个过程没有独立的“价值网络”前向传播,也没有外部reward model调用——这正是GRPO的精简所在。下面,我们从最前端的rollout开始,一层层剥开它的实现细节。
2. Rollout:如何把60条prompt变成720条高质量响应
rollout不是简单地让模型“多生成几次”,而是在分布式环境下,对计算资源、内存带宽、通信开销进行精细编排的结果。verl通过ActorRolloutRefWorker统一调度rollout行为,其核心在于设备网格(device mesh)的构建与分片策略的协同。
2.1 设备网格划分:让GPU各司其职
在_build_rollout方法中,verl根据配置创建了专门用于rollout的二维设备网格:
infer_tp = self.config.rollout.tensor_model_parallel_size # =2 dp = self.world_size // infer_tp # =6//2=3 rollout_device_mesh = init_device_mesh('cuda', mesh_shape=(dp, infer_tp), mesh_dim_names=['dp', 'infer_tp'])这行代码定义了一个3×2的GPU网格:
dp维度(Data Parallel)负责将60条prompt切分为3份,每份20条;infer_tp维度(Inference Tensor Parallel)表示每份20条prompt由2张GPU协作完成vLLM推理。
最终设备网格结构为:
DeviceMesh('cuda', [[0, 1], [2, 3], [4, 5]], mesh_dim_names=('dp', 'infer_tp'))即GPU 0&1组成第1个推理单元,GPU 2&3组成第2个,GPU 4&5组成第3个。每个单元独立运行一个vLLM实例,互不干扰。
2.2 Rollout执行:generate_sequences的三段式流水线
generate_sequences是rollout的入口函数,它被@register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)装饰,意味着它天然支持数据并行下的自动分发与聚合。其执行逻辑可清晰划分为三段:
(1)预处理:数据对齐与元信息注入
prompts = prompts.to(torch.cuda.current_device()) # 将batch移至当前GPU meta_info = { 'eos_token_id': self.tokenizer.eos_token_id, 'pad_token_id': self.tokenizer.pad_token_id, } prompts.meta_info.update(meta_info) # 注入tokenizer关键ID,供vLLM使用此时输入的prompts是一个包含60条prompt的DataProto对象,但尚未分片。它会被rollout_sharding_manager.preprocess_data()自动切分,并分发至3个vLLM单元。
(2)vLLM推理:每个单元生成240条响应
每个vLLM单元收到20条prompt后,执行n=12次采样:
- 输入:20条prompt(如“请写一首关于春天的五言绝句”)
- 输出:20×12=240条完整响应(含
output_ids,logprobs,prompt_token_ids等)
注意:vLLM本身不返回token-level logprob,它只返回每个token的top-k logprob。verl后续会用这些logprob重建完整序列的log_prob链。
(3)后处理与聚合:从240×3到720
rollout_sharding_manager.postprocess_data(output)负责:
- 收集3个vLLM单元各自产出的240条响应;
- 将它们按原始顺序拼接,形成统一的720条响应batch;
- 补充必要的元数据(如
prompt_lengths,response_lengths),为后续log_prob计算做准备。
最终返回的output是一个标准DataProto,其batch['prompt_token_ids']形状为[720, 8192],与ray_trainer.py中打印结果完全一致。
关键洞察:rollout的“批量”不是静态的,而是动态组合的。
data.train_batch_size=60是输入粒度,rollout.n=12是采样倍率,tensor_model_parallel_size=2是硬件约束,三者共同决定了最终的720这个数字。它不是魔法,而是可推导、可复现的工程结果。
3. Log Prob计算:为什么需要两套log_prob?它们怎么算?
rollout生成的720条响应只是“文本”,要进入RL训练,必须知道:这些文本在当前策略下有多大概率被生成?在参考策略下又有多大概率?这就是log_prob计算的意义——它把文本变成了可微分、可比较的数学量。
3.1 Actor log_prob:衡量当前策略的“自信程度”
self.actor_rollout_wg.compute_log_prob(batch)调用的是actor模型的前向传播,目标是为每条响应中的每个token计算: $$ \log \pi_{\text{old}}(a_t \mid s_t, \tau_{<t}) $$ 即:在给定历史τ_<t的前提下,当前策略选择动作a_t(即当前token)的对数概率。
在verl中,这一计算通过以下方式高效完成:
- 输入:720条响应的
input_ids(已拼接好prompt+response)及其attention_mask - 模型:FSDP封装的actor模型(如Llama-3-8B)
- 输出:一个与
input_ids同长的log_prob张量,仅对response部分有效(prompt部分mask掉)
由于actor模型采用FSDP,计算在6张GPU上并行展开,但verl通过dispatch_mode=DP_COMPUTE_PROTO确保每个GPU只处理自己分片内的样本,避免冗余计算。
3.2 Ref log_prob:提供稳定的KL散度基准
self.ref_policy_wg.compute_ref_log_prob(batch)调用的是一个冻结的参考模型(通常为SFT后的基座模型),其作用不是参与更新,而是提供一个稳定的概率分布基准: $$ \log \pi_{\text{ref}}(a_t \mid s_t, \tau_{<t}) $$
为什么需要它?因为RLHF/GRPO训练中,必须防止actor模型过度偏离原始能力,导致胡说八道。KL散度惩罚项: $$ \mathcal{L}{\text{KL}} = \mathbb{E}[\log \pi{\text{old}} - \log \pi_{\text{ref}}] $$ 就是靠这对log_prob计算出来的。
ref模型同样走FSDP流程,但它全程不更新参数,且通常配置更轻量(如param_offload=True),以节省显存。
3.3 数据对齐:确保两个log_prob能相减
这是极易被忽略却至关重要的细节。actor和ref模型的tokenizer必须完全一致,且input_ids的padding、truncation策略必须严格同步。verl通过以下方式保障:
- 所有worker共享同一个
self.tokenizer实例; compute_log_prob内部调用prepare_inputs_for_generation,确保prompt和response的拼接逻辑完全一致;- 最终log_prob张量的shape、mask位置、有效token索引完全对齐。
若这两套log_prob无法对齐,KL惩罚就失去意义,训练会迅速崩溃。
4. Advantage计算:没有Critic,Advantage从何而来?
这是GRPO区别于标准PPO的核心。PPO需要critic模型预测每个状态的价值V(s),再通过GAE(Generalized Advantage Estimation)计算advantage: $$ A_t^{\text{GAE}} = \delta_t + (\gamma\lambda)\delta_{t+1} + (\gamma\lambda)^2\delta_{t+2} + \cdots $$ 其中δ_t = r_t + γV(s_{t+1}) - V(s_t)。
而GRPO彻底抛弃critic,其advantage直接由规则化reward驱动: $$ A_t = r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + \cdots - b_t $$ 其中b_t是baseline(常取rolling average reward),r_t是token-level reward。
4.1 Token-level Reward:规则即模型
reward_fn(batch)是用户自定义函数,verl不提供默认实现,但给出了典型范式:
def reward_fn(batch): # batch.batch['responses'] 是720条字符串列表 responses = batch.batch['responses'] scores = [] for resp in responses: score = 0.0 if "春天" in resp and "花开" in resp: # 关键词匹配 score += 1.0 if len(resp) > 20 and len(resp) < 50: # 长度合规 score += 0.5 if resp.count("。") == 2: # 标点规范 score += 0.3 scores.append(score) return torch.tensor(scores, dtype=torch.float32)该函数输出一个[720]的reward张量,verl将其广播为token-level形式(即每个token获得相同reward),存入batch.batch['token_level_rewards']。
4.2 GAE计算:verl的compute_advantage函数
compute_advantage是verl中高度优化的advantage计算器,其签名如下:
def compute_advantage( batch, adv_estimator=None, # 此处为None(GRPO不使用) gamma=0.99, lam=0.95, num_repeat=12 # 每条prompt生成的响应数 ):在GRPO模式下,adv_estimator为None,函数直接走“纯reward”路径:
- 读取
batch.batch['token_level_rewards'](shape:[720, seq_len]) - 对每条响应,按
gamma和lam进行GAE衰减计算,得到[720, seq_len]的advantage张量 - 将advantage按token归一化(如除以响应长度),存入
batch.batch['advantages']
整个过程不涉及任何神经网络前向,纯CPU/GPU张量运算,毫秒级完成。
关键对比:PPO的advantage依赖critic预测的
V(s),易受critic训练不稳定影响;GRPO的advantage直接锚定规则reward,更稳定、更可控,代价是reward设计需足够鲁棒。
5. 训练循环:从advantage到actor更新的最后一步
当720条响应拥有了old_log_prob、ref_log_prob和advantages,训练循环就进入了最关键的actor更新阶段。
5.1 KL散度惩罚:防止策略坍缩
在apply_kl_penalty中,verl将KL项融入reward:
# token_level_rewards 已包含原始reward # 现在减去 KL 散度项 kl_div = old_log_prob - ref_log_prob # shape: [720, seq_len] kl_penalty = self.config.algorithm.kl_penalty # 如0.01 batch.batch['token_level_rewards'] = batch.batch['token_level_rewards'] - kl_penalty * kl_div这使得actor在追求高reward的同时,被强制靠近ref策略,避免过拟合规则或生成低质量文本。
5.2 Actor更新:PPO-style policy gradient
self.actor_rollout_wg.update_actor(batch)执行标准的PPO loss计算: $$ \mathcal{L}_{\text{PPO}} = -\mathbb{E}\left[ \min\left( r_t \cdot A_t,\ \text{clip}(r_t, 1-\epsilon, 1+\epsilon) \cdot A_t \right) \right] $$ 其中r_t = \frac{\pi_{\text{new}}(a_t|s_t)}{\pi_{\text{old}}(a_t|s_t)}是重要性采样比。
verl在此处做了关键优化:
π_new由当前actor模型实时计算;π_old即前面算好的old_log_prob,无需重复计算;- clip操作在CUDA kernel中完成,避免Python循环。
整个更新过程在FSDP下完成梯度同步,6张GPU合力完成一次参数更新。
5.3 实际效果:一次step的耗时分布(实测参考)
在A100×6环境上,一次完整step的耗时大致分布为:
- Rollout(vLLM生成):~3.2s(占总耗时65%)
- Log_prob计算(actor+ref):~0.9s(18%)
- Advantage计算:~0.1s(2%)
- Actor更新:~0.7s(14%)
- 其他(I/O、metric收集等):~0.1s(1%)
可见,rollout是绝对瓶颈。这也是verl强调与vLLM/sglang深度集成的原因——优化推理,就是优化整个RL训练的天花板。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。