在前段时间基于VeRL的Search-R1仓库进行了一些Agentic Search相关的探索,也算是通过一些实践来积累了一点Agentic RL的基本常识。之前朋友推荐了几个帖子来讨论了pg_loss在训练开始时为0的分析,自己阅读之后发现过去对于Agentic RL的理论推导还有些生疏,借此机会重新梳理一下。
单轮与多轮SFT
首先,我们先来回顾一下传统SFT的参数更新策略,我们从最基本的单轮对话开始,假设输入的prompt为$p:=[p_1, p_2,\dots, p_m]$,对应的标准答案为$\mathbf a^*:=[a_1^*,\dots, a_n^*]$,而模型$\theta$针对问题$p$输出的分布为$\pi_\theta(\cdot \mid p)$,此时SFT的目标就是最大化条件似然:
$$ \mathcal{L}_{\text{SFT}}=-\sum_{t=1}^n\log \pi_\theta(a_t^*|p,a_{\lt t}^*). $$注意在训练的时候模型并不会针对$p$直接输出一整串完整答案$\mathbf{a}$,而是采用teacher forcing的方式逐token来进行监督,通过展开上述$\mathcal{L}_{\text{SFT}}$的公式即可说明。在这里,每一个输出的token都有对应的label予以监督,因此也不存在credit assignment的问题。在目前大多数实现中,对于一个样本会将其问题和回答拼接成一条回复$[p_1,\dots, p_m, a_1^*,\dots, a_n^*]$,然后用$p$预测$a_1$,用$(p,a_1^*)$预测$a_2$等等,在一个forward过程中得到$a_1^*,\dots, a_n^*$每一个位置的logit,并将loss累加。
上面我们讨论了单轮对话的情况,如果将其扩展为多轮,那么 SFT 的目标其实仍然没有本质变化:仍然是在teacher forcing下,对所有需要监督的assistant token做条件似然最大化;只是这里的条件前缀不再只是单个prompt $p$,而是截至当前轮为止的整个对话历史。设一个多轮对话一共有$K$轮,我们将第$i$轮用户的输入记为$u_i$,模型在这一轮的回复记为$\mathbf a^{(i)*}$,那么整段对话可以写成一条轨迹$\tau=(u_1,\mathbf a^{(1)*},\dots, u_K, \mathbf a^{(K)*})$,而其中每一个assistant的回复都可以进一步写成一个token序列$\mathbf a^{(i)*}=[a^{(i)*}_1,\dots, a^{(i)*}_{n_i}]$。当模型在生成第$i$轮的回复时,它所条件化的上下文是此前的全部历史:
$$ h_i:=[u_1,\mathbf a^{(1)*},u_2,\mathbf a^{(2)*},\dots, u_i]. $$因此,第$i$轮第$t$个token的条件分布就可以写为$\pi_\theta(a_t^{(i)*}|h_i,a_{\lt t}^{(i)*})$,目标函数可以理解为单轮SFT公式的直接推广:
$$ \mathcal{L}_{\text{SFT}}=-\sum_{i=1}^K\sum_{t=1}^{n_i}\log \pi_\theta(a^{(i)*}_t|h_i,a^{(i)*}_{\lt t}). $$在训练的时候和单轮一样,不是按轮一轮轮独立生成再分别backward,而是通常把整段对话直接拼成一个长序列,一次forward得到所有位置的logits。需要注意的是在多轮SFT中,后续轮次虽然仍然条件化于此前完整对话历史,但该历史由数据集中的gold trajectory给定,而非由当前模型rollout得到;因此,轮次之间不存在由模型早先动作诱导出的状态转移依赖,也就不涉RL意义下对前序离散动作的credit assignment。
单轮与多轮RL
在完成了对于SFT的基本理解之后,我们进入到RL的部分;由于过去一段时间的大多数实验使用了GRPO,因此后续分析也以此为例。首先,我们依然从单轮的情况开始考虑:依然给定输入prompt为$p=[p_1,p_2,\dots,p_m]$,此时模型不会再进行teacher forcing,而是直接按照当前策略$\pi_\theta$自行采样得到完整回复。记一次采样得到的回复为$\mathbf a:=[a_1,\dots,a_n]\sim \pi_\theta(\cdot \mid p)$,在RL的状态下模型在生成第$t$个token时所条件化的前缀$(p,a_{\lt t})$本身就是由模型之前的采样结果构成的,而非SFT中的golden trajectory。对于单轮任务,环境通常只会在模型生成完整回复$\mathbf a$之后给一个标量奖励$r(\mathbf a,p)$。由此,我们希望优化的目标写为
$$ J(\theta)=\mathbb{E}_{\mathbf a\sim \pi_\theta(\cdot\mid p)}[r(\mathbf a,p)]. $$进一步地,由于整条回复的生成概率可以按token进行分解,我们有
$$ \pi_\theta(\mathbf{a}|p)=\prod_{t=1}^n \pi_\theta(a_t|p,a_{\lt t}). $$因此就有了
$$ \log \pi_\theta(\mathbf{a}|p)=\sum_{t=1}^n\log\pi_\theta(a_t|p,a_{\lt t}). $$如果我们使用REINFORCE这个最经典的policy gradient的方法,可以将目标函数的梯度写为
$$ \nabla_\theta J(\theta)=\mathbb{E}_{\mathbf{a}\sim \pi_\theta(\cdot|p)}[r(\mathbf{a},p)\nabla_\theta \log\pi_\theta(\mathbf{a}|p)]=\mathbb{E}_{\mathbf{a}\sim \pi_\theta(\cdot|p)}\left[r(\mathbf{a},p)\sum_{t=1}^n\nabla_\theta \log\pi_\theta(a_t|p,a_{\lt t})\right]. $$注意到在这种情况下,虽然奖励只在整条回复完成之后才给出,但它最终会共同作用在整条回复的所有token上,作为一个序列级的reward signal,这与传统RL并无差异。当然,在具体实现时为了降低方差,我们会使用Advantage而非原始奖励作为信号。而GRPO则是抛弃了PPO中的value model,使用旧策略$\pi_{\theta_{\text{old}}}$采样出一组回复$\mathcal{A}=\{\mathbf{a}^{(1)},\dots, \mathbf{a}^{(G)}\}$,并分别计算它们对应的奖励$r^{(g)}:=r(\mathbf{a}^{(g)},p)$,并计算group-relative advantage
$$ A^{(g)}=\frac{r^{(g)}-\text{mean}\left(\{r^{(j)}\}_{j=1}^G\right)}{\text{std}\left(\{r^{(j)}\}_{j=1}^G\right)} $$最终的优化目标写为PPO形式的
$$ \mathcal{L}_{\text{GRPO}}=-\frac{1}{G}\sum_{g=1}^G\frac{1}{n_g}\sum_{t=1}^{n_g}\left(\min\left(\frac{\pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)})}{\pi_{\theta_{\text{old}}}(a_t^{(g)}|p,a_{\lt t}^{(g)})}A^{(g)}, \text{clip}\left(\frac{\pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)})}{\pi_{\theta_{\text{old}}}(a_t^{(g)}|p,a_{\lt t}^{(g)})}, 1-\epsilon ,1+\epsilon\right)A^{(g)}\right)\right) $$其中$n_g$表示第$g$个样本的回复长度,$\epsilon$为clipping的系数。这个公式我们自然是见过很多次了,但是在这里希望进一步补充的是它在backward时的具体含义。对于一条固定采样得到的回复$\mathbf{a}^{(g)}\in\mathcal{A}$,训练时并不是沿着rollout的过程逐步回传,而是会将整条sampled sequence固定下来,重新拼接成$[p_1,\dots, p_m, a_1^{(g)},\dots, a_n^{(g)}]$做一次并行forward,并在每一个assistant token所对应的位置上重新计算logprob。因而,对于单个位置$t$来说,其局部loss本质上依然是一个交叉熵项,只是target不再是SFT中的gold token,而是当前策略采样出的token带上advantage项作为权重。下面我们可以做一个简单的推导:如果我们设重要性采样$\rho_t^{(g)}$,假设不考虑clipping激活的场景,则这条sequence第$t$个token的loss可以近似为
$$ \ell_t^{(g)}=-\frac{1}{Gn_g}\rho_t^{(g)}A^{(g)}. $$对应位置的梯度便可以表示为
$$ \begin{aligned} \nabla_\theta \ell_t^{(g)}&=-\frac{1}{Gn_g}A^{(g)}\nabla_\theta\rho_t^{(g)}\\ &=-\frac{1}{Gn_g}A^{(g)}\nabla_\theta\left[\frac{\pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)})}{\pi_{\theta_{\text{old}}}(a_t^{(g)}|p,a_{\lt t}^{(g)})}\right]\\ &=-\frac{1}{Gn_g}A^{(g)}\rho_t^{(g)}\nabla_\theta \log \pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)}).\\ \end{aligned} $$注意最后一步使用了RL中最常用的一个变换函数
$$ \nabla_\theta f(\theta)=f(\theta)\nabla \log f(\theta) $$这说明对于单个token而言,它所接收到的梯度方向,本质上依然是提高或者压低当前token的logprob的方向。这个形式和普通的交叉熵函数是完全一致的,只是前面乘上了一个$\rho_t^{(g)}A^{(g)}$的权重项。因此,从backward的角度看,AR policy model的训练逻辑其实并不神秘:虽然 reward 是在整条回复结束后才给出的,但在真正更新参数时,它会被转化为每一个token位置上的一个加权交叉熵梯度;模型仍然是在每个位置输出一个词表分布、计算对应logprob,再将这些位置上的梯度累加到同一套共享参数上。与SFT的根本区别并不在于backward的计算图结构,而在于这里的监督信号不再来自token-level的 gold label,而是来自sequence-level reward所诱导出的advantage。
接下来我们也将其推广到多轮对话的场景,假设一轮完整的轨迹一共有$K$轮,第$i$轮的用户输入记为$u_i$,模型在该轮生成的回复记为$\mathbf{a}^{(i)}=[a_1^{(i)},\dots, a_{n_i}^{(i)}]$,则整条轨迹可以写为
$$ \tau=(u_1,\mathbf{a}^{(1)},u_2,\mathbf{a}^{(2)},\dots, u_K, \mathbf{a}^{(K)}). $$和多轮SFT不同的是,这里的历史并不是数据集中给定的gold trajectory,而是由当前的policy逐轮rollout的sampled trajectory。我们设模型生成第$i$轮回复时,它所条件化的上下文为此前全部的交互历史:
$$ h_i:=[u_1,\mathbf{a}^{(1)},u_2,\mathbf{a}^{(2)},\dots, u_i]. $$由此,整条多轮轨迹的概率可以分解为如下形式,其中$n_i$为第$i$轮assistant回复的token数量:
$$ \pi_\theta(\tau)=\prod_{i=1}^K\pi_\theta(\mathbf{a}^{(i)}|h_i)=\prod_{i=1}^K\prod_{t=1}^{n_i}\pi_\theta(a_t^{(i)}|h_i, a_{\lt t}^{(i)}) $$如果环境在整条轨迹结束之后给出一个最终奖励$r(\tau)$,那么对应的优化目标也就可以写为
$$ J(\theta)=\mathbb{E}_{\tau\sim \pi_\theta}[r(\tau)] $$如果使用REINFORCE算法,其梯度形式为
$$ \nabla_\theta J(\theta)=\mathbb{E}_{\tau\sim \pi_\theta}[r(\tau)\nabla_\theta\log\pi(\tau)]=\mathbb{E}_{\tau\sim \pi_\theta}\left[r(\tau)\sum_{i=1}^K\sum_{t=1}^{n_i}\nabla_\theta \log\pi_\theta(a_t^{(i)}|h_i,a_{\lt t}^{(i)})\right] $$这个式子和单轮情形在形式上几乎完全一致,只不过求和对象从单条回复中的各个 token,扩展成了整条多轮轨迹中所有assistant token。它所表达的含义也非常直接:如果最终奖励只在整条轨迹结束后给出,那么这个sequence-level reward就会共同作用在整条轨迹上的所有动作token 上。
在具体实现中,我们同样不会沿着rollout过程逐轮逐token地单独回传,而是会将整条sampled trajectory固定下来,拼接为一个长序列后统一做一次forward,并在所有assistant token对应的位置上重新计算logprob。于是,从backward的角度看,多轮RL和单轮RL并没有本质区别:对于轨迹中任意一个位置 $(i,t)$,其局部 loss 仍然可以理解为“该位置 sampled token 的对数概率乘上一个由整条轨迹表现决定的权重”;差别只在于,这里的前缀$h_i$本身也已经包含了模型在更早轮次作出的决策,因此后续奖励会间接归因到这些更早的动作上。
对于GRPO而言,我们依然是对同一个初始输入采样一组完整轨迹$\{\tau^{(1)},\dots, \tau^{(G)}\}$,并计算每一条轨迹对应的奖励$r^{(g)}:=r(\tau^{(g)})$以及group-relative advantage
$$ A^{(g)}=\frac{r^{(g)}-\text{mean}(\{r^{(j)}\}_{j=1}^G)}{\text{std}(\{r^{(j)}\}_{j=1}^G)}. $$而多轮的GRPO的优化目标就可以写成
$$ \mathcal{L}_{\text{GRPO}}=-\frac{1}{G}\sum_{g=1}^G\frac{1}{N_g}\sum_{i=1}^{K_g}\sum_{t=1}^{n_i^{(g)}}\min\left(\rho_{i,t}^{(g)}A^{(g)},\text{clip}(\rho_{i,t}^{(g)},1-\epsilon,1+\epsilon)A^{(g)}\right). $$其中$N_g=\sum_{i=1}^{K_g}n_i^{(g)}$表示第$g$条轨迹中的assistant token总数,$K_g$为总轮数,$n_i^{(g)}$则表示该采样轨迹在第$i$轮的token数,而重要性采样
$$ \rho_{i,t}^{(g)}=\frac{\pi_\theta(a_t^{(g,i)}|h_i^{(g)},a_{\lt t}^{(g,i)})}{\pi_\text{old}(a_t^{(g,i)}|h_i^{(g)},a_{\lt t}^{(g,i)})}. $$因此,从形式上看,多轮 GRPO 相比单轮并没有引入新的目标结构,只是把单条回复替换成了完整轨迹,把回复长度$n_g$替换成了轨迹中所有assistant token的总数$N_g$。但从建模含义上看,两者有一个根本差异:在单轮场景下,某个token只会影响当前这条回复本身;而在多轮场景下,某一轮生成出的token还会进入后续轮次的上下文,进而改变之后整条rollout的演化路径。而从credit assignment的角度考虑,每条轨迹 $\tau^{(g)}$ 中所有assistant token共享同一个scalar advantage $A^{(g)}$,包括第1轮的思考token、第3轮的工具调用token、以及最终summary的每个token——它们都被等权重地上移或下压。这意味着:如果一条轨迹最终得到高奖励,模型根本无法区分"第2轮搜索策略好"还是"第5轮总结写得好",两者都得到相同的正向强化。
最后,在梯度计算上,多轮情形依然可以类比单轮形式进行理解:对每一个assistant token而言,其局部梯度本质上仍然对应于该sampled token的log-prob,再乘上一个由整条轨迹最终表现决定的advantage-related coefficient。RL下的多轮对话,往往是应用在Agentic场景之下,此时的user message转化为环境或者工具调用给出的observation。
相关分析
Question: 解释一下batch、micro_batch、mini_batch这三者之间的关系,并分析它们取值变化所可能带来的后果?
在VeRL的训练框架下,一次完整的GRPO训练并不是直接rollout一批数据,然后立刻对整批数据同时更新一次。我们会进一步拆成三个层次来理解,即batch、mini_batch与micro_batch。它们分别对应着数据采样的范围、一次optimizer step所使用的数据量,以及为了适配显存而进行的梯度累积粒度。
首先,在每一个training step开始时,会先从数据集中取出一个batch的prompt。若记batch_size为$B$,GRPO中的采样数量为$G$,则真正进入GRPO更新阶段的trajectory总数自然为$N_{\text{rollout}}=B\times G$。对于这$N_{\text{rollout}}$条序列,会首先基于当前的旧策略$\pi_{\text{old}}$计算并冻结它们的old_logprobs。也就是说,这一整批的rollout数据共同对应同一个固定的旧策略$\pi_{\text{old}}$,后续的所有GRPO更新都会围绕这批冻结数据予以展开。
接下来,在真正做参数更新时,这$N_{\text{rollout}}$条序列并不会一次性全部送入optimizer,而是会进一步切分成若干个mini_batch,记每一个mini_batch中包含的序列数为$M$,则mini_batch的总数则为$N_{\text{rollout}}/M$,也对应$N_{\text{rollout}}/M$次参数更新。需要注意的是,这$N_{\text{rollout}}/M$次参数更新虽然使用的都是同一轮rollout得到的数据,但是随着每次mini_batch更新之后参数$\theta$发生变化,当前策略$\pi_\theta$会逐渐偏离$\pi_{\theta_{\text{old}}}$,这也正是重要性采样会逐渐偏离1并需要使用clipping进行约束的原因。
然而,即使一个mini_batch中只有$M$条序列,在大模型训练的过程中往往也无法一次性完成forward与backward,因此还需要进一步切分成micro_batch。在VeRL的实现中,micro_batch的相关参数定义为actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu,即每一张卡每一次梯度累积的样本数量。从数学上来看,micro_batch只是把一个mini_batch拆开来适配显存大小,并不改变最终的优化目标,梯度多次累积与一次性forward/backward在不考虑工程误差的情况下,数学上是等价的。
综合上述分析,一个典型的batch优化pipeline如下所示
Rollout阶段 (π_θ_old固定):
256 prompts × 5 samples → 1280条序列
→ 计算 old_logprobs, advantages (全部冻结)
PPO更新阶段 (π_θ被更新):
for each minibatch of 128: # 1280/128 = 10次optimizer step
for each microbatch of 32: # 128/32 = 4次梯度累积
forward: 计算 new_logprobs # 用当前π_θ
backward: 累积梯度
optimizer.step() # π_θ更新, old_logprobs不变
下一轮:
π_θ_old ← π_θ (同步), 重新rollout
在取值变化所带来的后果上,这三者的影响也并不相同。增大batch会使每轮rollout包含更多prompt与trajectory,从而让reward和advantage的统计更稳定、方差更小,但代价是rollout成本线性上升、单轮迭代变慢;反之,batch过小则数据量不足,reward统计波动更大,训练更容易不稳定。增大mini_batch会让每次参数更新基于更多trajectory,梯度估计更平滑、噪声更小,但会提高单次step的显存与算力压力,并减少固定rollout数据上的optimizer step次数;减小mini_batch则会让step更频繁,但单步噪声更大,同时同一批旧数据会被重复用于更多次更新,使当前策略更快偏离$\pi_{\theta_{\text{old}}}$,importance ratio更容易偏离1,训练稳定性也会变差。相比之下,micro_batch更多是工程层面的显存切分参数:增大它可以减少梯度累积次数、提升吞吐,但受显存约束;减小它虽然不改变理论上的梯度期望,却会带来更多累积step、额外通信与更低的硬件利用率,从而拖慢训练。
Question: 在实际Agentic RL的训练过程中,pg_loss在一开始可能为0,试解释这种现象?
在VeRL的训练设定之下,pg_loss表示policy gradient loss,也即上述分析中$\mathcal{L}_{\text{GRPO}}$,不包括大多数时候会添加的KL散度约束。我们可以将这个问题分成两个维度来分析:
- 区分
pg_loss为什么一开始时可能为0 - 为什么它等于0时并不一定意味着这一轮没有梯度,模型不能更新
在进入具体公式之前,首先需要结合前述batch、mini_batch、micro_batch的流程来理解:对于某一轮fresh rollout得到的数据,所有trajectory的old_logprobs都是由同一个旧策略$\pi_{\theta_{\text{old}}}$ 计算并冻结的;而在随后的PPO/GRPO更新中,当前参数$\theta$会在这些固定数据上重新计算new_logprobs并进行优化。也就是说,所谓的“训练一开始”更准确地说,并不是指整个训练任务的最开头,而是指针对某一批fresh rollout数据,刚开始做第一个optimizer step的时刻。
我们先忽略clipping激活的细节,使用单轮对话的GRPO作为案例,那么有
$$ \mathcal{L}_{\text{pg}}(\theta)=-\frac{1}{G}\sum_{g=1}^G\frac{1}{n_g}\sum_{t=1}^{n_g}\rho_t^{(g)}A^{(g)}. $$其中$\rho^{(g)}$为重要性采样的系数,在这里重新补充一遍
$$ \rho_t^{(g)}=\frac{\pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)})}{\pi_{\theta_{\text{old}}}(a_t^{(g)}|p,a_{\lt t}^{(g)})}. $$在优化开始时,对于这一批刚刚rollout完成并冻结了old_logprob的数据,在处理第一个mini_batch时,当前策略实际上尚未发生更新,因此有$\theta=\theta_{\text{old}}$,此时$\rho_t^{(g)}=1$;将其代入上式,自然有
而由于GRPO的advantage天然就满足组内均值为0,因此$\sum_{g=1}^GA^{(g)}=0$,也就得到了$\mathcal{L}_{\text{pg}}(\theta_{\text{old}})=0$。因此总结而言,pg_loss在“一开始”为0,首先可能只是一个非常直接的数值结果:对于某一批fresh rollout数据,在第一个minibatch的第一个optimizer step开始之前,当前策略与旧策略完全一致,故importance ratio恒为1;而GRPO的advantage又经过了零均值归一化,因此最终的policy loss在数值上会恰好抵消为0。
不过,这里最容易产生误解的地方在于:loss取值为0,并不等价于梯度为0,继续对上式求导,我们有
$$ \nabla_\theta\mathcal{L}_{\text{pg}}(\theta)=-\frac{1}{G}\sum_{g=1}^G\frac{1}{n_g}\sum_{t=1}^{n_g}A^{(g)}\nabla_\theta\rho_t^{(g)} $$依然使用上文提到的经典变换公式:$\nabla f(x)=f(x)\nabla \log f(x)$,我们有
$$ \nabla_\theta \rho_t^{(g)}=\rho_t^{(g)}\nabla_\theta \log \pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)}) $$因此当$\theta=\theta_{\text{old}}$也即$\rho_t^{(g)}=1$时,我们有
$$ \nabla_\theta\mathcal{L}_\text{pg}(\theta)\Big|_{\theta=\theta_{\text{old}}}=-\frac{1}{G}\sum_{g=1}^G\frac{1}{n_g}\sum_{t=1}^{n_g}A^{(g)}\nabla_\theta\log\pi_\theta(a_t^{(g)}|p,a_{\lt t}^{(g)}). $$也就是说,这样这些样本的$A^{(g)}$并不全为0,那么即使此时pg_loss的标量值恰好为0,梯度通常仍然不为0。而当模型完成了一个mini_batch转入新的mini_batch之后,由于policy已经被更新过一次,因此重要性采样的值往往就不再为1,pg_loss也就一般不为0了。
回到一开始的问题,在我们进行的实验中,并没有观察到step = 1时pg_loss为0的情况,这是因为VeRL在wandb记录数据时是按照step level来进行平均的,而我们mini_batch的大小并不等于batch的大小,即使在mini_batch的第一步出现了pg_loss为0的情况,后续非0的结果也会使得最终记录的均值偏离0。事实上,pg_loss在数值上是否为0,与是否处于全局训练的step = 1并没有直接关系,而是取决于当前更新是否仍然处在某一批fresh rollout数据的第一次optimizer step之前。而如果按照GRPO原本的one-iteration-per-step完全on policy的配置,也即$M=B$,在ppo_epoch = 1(也即mini batch只把batch中的每一个样本过一遍)的情况下得到的结论是:pg_loss始终为0,但这并不影响最终的梯度。