1. GRPO算法核心定位与设计动机
1.1 从PPO的痛点说起:为什么需要GRPO
搞强化学习的人都知道,PPO(Proximal Policy Optimization)在过去几年几乎成了策略优化的默认选择。但真正在语言模型对齐、推理能力增强这些场景里跑过PPO的人,心里都清楚它有多“重”——你需要同时维护策略模型、价值模型(Critic)、奖励模型、参考模型,显存占用直接翻倍不说,训练稳定性还特别依赖Critic的估计质量。一旦Critic估计偏差大,优势函数就失真,整个策略更新方向就跑偏了。
GRPO(Group Relative Policy Optimization)的核心动机就一句话:把Critic干掉,用组内相对比较来估计优势。这个思路其实不新鲜,类似的思想在REINFORCE with baseline、RLOO里都有影子,但GRPO把它做成了一个端到端可落地、在大模型上真正work的方案。DeepSeekMath那篇工作里首次系统性地提出了GRPO,后来DeepSeek-R1系列把它推到了推理增强的核心位置。
我第一次接触GRPO是在做一个数学推理增强的项目,当时用PPO跑7B模型,4张A100 80G,batch size开到64就OOM了,而且训练曲线抖得厉害。换成GRPO之后,同样的硬件,batch size能开到256,训练稳定性肉眼可见地提升。这个对比让我意识到,GRPO不是“又一个PPO变体”,而是从计算范式和优化目标上都做了实质性简化的方案。
1.2 GRPO到底解决了什么问题
传统PPO的目标函数里,优势函数Â_t是通过GAE(Generalized Advantage Estimation)算出来的,而GAE依赖Critic网络对每个状态的价值估计V(s_t)。这意味着:
- 你需要额外训练一个和策略模型同量级的Critic,参数量翻倍;
- Critic的估计误差会直接传导到策略梯度,导致高方差;
- 在语言模型场景下,状态空间是离散的token序列,Critic很难准确估计中间状态的价值。
GRPO的做法是:对同一个prompt,采样一组(Group)输出,用这组输出的奖励均值作为baseline,每个输出的奖励减去均值就是它的优势。公式上就是:
Â_i = (r_i - mean(r_1, r_2, ..., r_G)) / std(r_1, r_2, ..., r_G)
其中G是组大小,r_i是第i个输出的奖励。这个优势估计完全不依赖Critic,纯粹靠组内比较。你可以把它理解成:同一个题目,让模型做G遍,谁做得好、谁做得差,相对差距就是优势信号。
这个设计的好处非常直接:
| 对比维度 | PPO | GRPO |
|---|---|---|
| Critic网络 | 必须,参数量与策略模型相当 | 不需要 |
| 显存占用 | 高(4个模型) | 低(3个模型:策略、参考、奖励) |
| 优势估计 | GAE,依赖V(s) | 组内相对奖励,无偏估计 |
| 训练稳定性 | 对Critic质量敏感 | 对组大小和奖励尺度敏感 |
| 适用场景 | 通用RL | 语言模型生成任务优势明显 |
但GRPO也不是没有代价。组内采样意味着每个prompt要生成G个完整输出,推理成本是PPO的G倍。所以实际用的时候,G一般取4到16之间,太大推理扛不住,太小优势估计方差大。
1.3 GRPO在算法谱系中的位置
如果把强化学习算法按“是否依赖价值函数”来分,GRPO属于无Critic的策略梯度方法这一支。它的近亲包括:
- REINFORCE with baseline:用滑动平均奖励做baseline,但baseline是全局的,不是组内的;
- RLOO(REINFORCE Leave-One-Out):用留一法估计baseline,和GRPO的组内均值思路非常接近;
- DPO:完全绕开在线采样,用偏好数据直接优化,但缺乏在线探索能力。
GRPO的独特之处在于它把“组内相对比较”和“PPO的clip机制”结合了起来。目标函数里保留了PPO的clip ratio,保证策略更新不会一步迈太大,同时用组内标准化奖励替代GAE。这个组合让它在语言模型上既能稳定训练,又能有效利用在线采样。
实操心得:如果你之前跑PPO总是调不好Critic,或者显存不够同时装4个模型,GRPO是值得认真考虑的替代方案。但前提是你的任务能定义清晰的标量奖励,并且推理成本可控。
2. GRPO目标函数拆解与数学原理
2.1 目标函数的完整形式
GRPO的目标函数看起来和PPO很像,但细节上有本质区别。先看完整形式:
J_GRPO(θ) = E[q~P(Q), {o_i}~π_θ_old(O|q)] * (1/G) * Σ_i [ min( ratio_i * Â_i, clip(ratio_i, 1-ε, 1+ε) * Â_i ) - β * KL(π_θ || π_ref) ]
逐项拆解:
- q~P(Q):从prompt分布中采样一个问题q;
- {o_i}~π_θ_old(O|q):用旧策略对q生成G个输出,组成一个组;
- ratio_i = π_θ(o_i|q) / π_θ_old(o_i|q):重要性采样比率,衡量新旧策略对同一个输出的概率变化;
- Â_i:第i个输出的组内相对优势;
- clip(ratio_i, 1-ε, 1+ε):PPO的clip机制,防止策略更新过猛;
- β * KL(π_θ || π_ref):KL散度惩罚项,约束新策略不要偏离参考模型太远。
和PPO的关键差异在Â_i的计算上。PPO的Â_t来自GAE,依赖Critic;GRPO的Â_i来自组内奖励标准化,不依赖任何价值网络。
2.2 组内优势估计的数学细节
假设对prompt q采样了G个输出,奖励分别为r_1, r_2, ..., r_G。GRPO的优势计算分两步:
第一步,计算组内均值和标准差:
μ = (1/G) * Σ_i r_i σ = sqrt( (1/G) * Σ_i (r_i - μ)^2 )
第二步,标准化得到优势:
Â_i = (r_i - μ) / (σ + ε)
其中ε是一个很小的常数(通常1e-8),防止除零。
这个标准化操作有几个重要性质:
- 零均值:Σ_i Â_i = 0,组内优势之和为零,意味着策略更新是“有升有降”的,不会整体推高所有输出的概率;
- 单位方差:Â_i的尺度被归一化到1附近,不受奖励绝对大小影响,这对奖励尺度变化大的任务特别重要;
- 无偏性:在组大小G足够大时,组内均值是真实期望奖励的无偏估计,所以Â_i是真实优势的无偏估计。
但这里有个坑:如果组内所有输出的奖励都相同(比如全对或全错),σ=0,Â_i全部为0,这个组就不产生任何梯度。这在训练初期或任务太简单/太难时经常发生。解决办法后面会讲。
2.3 KL惩罚项的作用与调参
KL惩罚项β * KL(π_θ || π_ref)是GRPO里最需要小心调的参数之一。它的作用是约束新策略不要偏离参考模型太远,防止:
- 策略为了刷奖励而输出乱码或重复无意义内容;
- 策略遗忘预训练阶段学到的语言能力;
- 奖励模型被“钻空子”(reward hacking)。
KL散度的计算方式有两种常见选择:
- 精确KL:KL(π_θ || π_ref) = Σ π_θ(o|q) * log(π_θ(o|q) / π_ref(o|q)),需要对整个词表求和,计算量大;
- 采样近似KL:用采样到的token做蒙特卡洛估计,k3估计量:KL ≈ (π_ref/π_θ - log(π_ref/π_θ) - 1),这个估计量无偏且方差较小。
实际实现里,大多数GRPO代码用的是k3估计量,因为它只需要采样token的概率,不需要遍历词表。
β的取值经验:
| β值 | 效果 | 适用场景 |
|---|---|---|
| 0 | 无约束,容易reward hacking | 不推荐 |
| 0.001-0.01 | 弱约束,策略自由度大 | 奖励信号可靠、任务明确 |
| 0.01-0.1 | 中等约束,平衡探索与稳定 | 大多数场景的默认选择 |
| 0.1-1.0 | 强约束,策略接近参考模型 | 奖励信号噪声大、需要保守更新 |
注意:β不是越大越好。β太大时,策略几乎不更新,训练失去意义;β太小时,KL散度爆炸,输出质量崩坏。建议从0.04开始试,根据KL散度的实际值动态调整。
2.4 clip机制在GRPO中的特殊考量
PPO的clip机制在GRPO里同样保留,但ratio的计算方式和PPO略有不同。PPO是token级别的ratio,GRPO通常也是token级别,但优势是序列级别的(整个输出的奖励算出来的)。
这就带来一个细节:同一个输出里的所有token共享同一个Â_i。这意味着如果一个输出整体奖励高,它里面所有token的概率都会被推高;反之亦然。这个设计是合理的,因为在语言模型里,我们很难精确知道哪个token贡献了最终奖励,用序列级奖励做token级更新是一种粗粒度但有效的信用分配。
clip的ε通常取0.2,和PPO一致。但在GRPO里,由于优势是标准化的,ratio的波动范围可能更大,有些实现会把ε调到0.1或0.3。我的经验是:如果训练初期ratio经常撞到clip边界,说明学习率太大或β太小,先调这两个,再动ε。
3. 完整实操流程与关键环节实现
3.1 环境准备与依赖安装
先列一下我实际跑GRPO用的环境配置,这套配置在单机8卡A100 80G上验证过,7B模型训练稳定:
# 基础环境 Python 3.10+ PyTorch 2.1+ CUDA 12.1+ transformers 4.40+ trl 0.8+ # HuggingFace的RL训练库,内置GRPO支持 accelerate 0.28+ deepspeed 0.14+ # 多卡训练必备如果你不想自己从头写GRPO,HuggingFace的TRL库已经内置了GRPOTrainer,可以直接用。但如果你想深入理解细节或者做定制化修改,建议自己实现一遍核心逻辑。我下面会给出关键代码片段。
# 核心依赖 import torch import torch.nn.functional as F from transformers import AutoModelForCausalLM, AutoTokenizer from trl import GRPOTrainer, GRPOConfig3.2 数据准备与奖励函数设计
GRPO的训练数据格式很简单:每条数据就是一个prompt,不需要标注答案,只需要一个能打分的奖励函数。这比DPO需要偏好对、PPO需要奖励模型要轻量得多。
数据格式示例(JSONL):
{"prompt": "计算 3x + 7 = 22 中 x 的值。"} {"prompt": "解释什么是梯度下降,并给出一个生活中的类比。"} {"prompt": "写一个Python函数,判断一个字符串是否是回文。"}奖励函数是GRPO的灵魂。它决定了模型往哪个方向优化。我一般把奖励函数设计成多个维度的加权和:
def reward_function(completions, prompts, **kwargs): rewards = [] for completion, prompt in zip(completions, prompts): score = 0.0 # 维度1:格式正确性(0或1) if has_valid_format(completion): score += 1.0 # 维度2:答案正确性(0或1) if check_answer_correct(completion, prompt): score += 2.0 # 维度3:长度惩罚(避免过长或过短) length = len(completion.split()) if 20 <= length <= 200: score += 0.5 elif length > 500: score -= 0.5 # 维度4:重复惩罚 if has_repetition(completion): score -= 1.0 rewards.append(score) return rewards实操心得:奖励函数的设计比算法本身更重要。我踩过的最大坑是奖励函数太复杂,多个维度互相冲突,导致模型学出一个“四不像”策略。建议初期只用1-2个核心维度,跑通了再逐步加。
3.3 组采样与优势计算的核心代码
这是GRPO最核心的部分。假设我们已经用旧策略对每个prompt采样了G个输出,并算出了奖励,接下来计算组内优势:
def compute_grpo_advantages(rewards, group_size, eps=1e-8): """ rewards: shape (batch_size * group_size,) 每group_size个奖励属于同一个prompt """ rewards = rewards.view(-1, group_size) # (batch_size, group_size) # 组内均值和标准差 mean = rewards.mean(dim=1, keepdim=True) std = rewards.std(dim=1, keepdim=True) # 标准化优势 advantages = (rewards - mean) / (std + eps) return advantages.view(-1) # 展平回 (batch_size * group_size,)这段代码看起来简单,但有几个细节要注意:
- std的计算:PyTorch的std默认是unbiased(除以G-1),但GRPO论文里用的是biased(除以G)。实际差异不大,但为了和论文一致,可以用
rewards.std(dim=1, unbiased=False, keepdim=True); - eps的位置:eps加在std后面,不是加在分母整体上,防止std为0时除零;
- 组大小为1的情况:如果G=1,std=0,优势全为0,训练完全无效。所以G至少为2,实际建议4以上。
3.4 训练循环与关键参数配置
完整的训练循环大致长这样:
# 配置 config = GRPOConfig( output_dir="./grpo_output", num_train_epochs=3, per_device_train_batch_size=4, # 每个设备处理的prompt数 gradient_accumulation_steps=8, num_generations=8, # 组大小G max_new_tokens=512, # 每个输出的最大长度 learning_rate=1e-6, kl_coef=0.04, # β clip_range=0.2, # ε temperature=0.9, # 采样温度 top_p=0.95, logging_steps=10, save_steps=100, ) # 训练 trainer = GRPOTrainer( model=model, reward_funcs=reward_function, args=config, train_dataset=dataset, ) trainer.train()关键参数的经验值:
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| num_generations (G) | 4-16 | 太小优势方差大,太大推理成本高 |
| learning_rate | 1e-7 ~ 5e-6 | 比PPO小,因为组内标准化后梯度尺度更稳定 |
| kl_coef (β) | 0.01-0.1 | 从0.04开始,根据KL实际值调 |
| clip_range (ε) | 0.1-0.3 | 默认0.2,ratio撞边界频繁时调小 |
| temperature | 0.7-1.0 | 太低组内输出太相似,优势信号弱 |
| max_new_tokens | 256-1024 | 根据任务输出长度定 |
3.5 显存优化与多卡训练
GRPO虽然省掉了Critic,但组采样意味着推理时的batch size是训练batch size的G倍。7B模型、G=8、max_new_tokens=512的情况下,单卡推理显存大概需要40-50G。如果显存不够,有几个优化方向:
- 梯度检查点:
model.gradient_checkpointing_enable(),省显存但慢20%左右; - DeepSpeed ZeRO-2/3:多卡场景下用ZeRO-3把模型参数分片,单卡显存能降到1/8;
- vLLM推理:把生成阶段交给vLLM,速度提升3-5倍,显存效率也更高;
- 减小G:从8降到4,显存减半,但优势估计方差增大。
我实际用的组合是:DeepSpeed ZeRO-3 + gradient checkpointing + G=8,8卡A100 80G,7B模型,per_device_batch_size=2,gradient_accumulation=16,总batch size=256。训练速度大概每步3-4秒,3个epoch跑完10万条数据大概需要2天。
4. 常见问题排查与避坑指南
4.1 训练不收敛或奖励不上升
这是最常见的问题。排查顺序如下:
第一步,检查奖励函数是否有信号。把模型输出和奖励值打印出来,人工看几条。如果奖励全是0或者全是同一个值,说明奖励函数没区分度。我遇到过一次,奖励函数里的格式检查写错了,所有输出都判为格式错误,奖励恒为-1,模型完全学不动。
第二步,检查组内是否有差异。如果G个输出的奖励完全一样,优势全为0,这个组不产生梯度。统计一下训练过程中“零优势组”的比例,如果超过30%,说明任务太简单或太难,或者temperature太低导致输出多样性不足。
第三步,检查KL散度。如果KL散度在训练初期就爆炸(比如超过10),说明β太小或学习率太大。把β调大、学习率调小,重新跑。
第四步,检查ratio分布。如果ratio大量撞到clip边界(比如超过50%的token被clip),说明策略更新太猛。调小学习率或调大β。
4.2 输出重复、乱码或语言能力退化
这是reward hacking的典型表现。模型发现某种重复模式能骗到高奖励,就疯狂输出重复内容。解决办法:
- 加重复惩罚:在奖励函数里检测n-gram重复,重复就扣分;
- 加KL约束:调大β,让策略不要偏离参考模型太远;
- 加长度惩罚:过长或过短的输出扣分;
- 人工审查:定期抽样看模型输出,发现异常及时调整。
我踩过最坑的一次是奖励函数里有个“包含关键词就给分”的维度,结果模型学会了在输出末尾堆砌关键词,前面全是乱码。后来把关键词维度改成“关键词出现在正确位置才给分”,问题才解决。
4.3 显存溢出(OOM)的排查与解决
OOM是工程上最烦人的问题。按以下顺序排查:
| 排查项 | 可能原因 | 解决办法 |
|---|---|---|
| 生成阶段OOM | batch_size * G * max_new_tokens太大 | 减小per_device_batch_size或G |
| 前向传播OOM | 模型参数量大、序列长 | 开gradient checkpointing |
| 反向传播OOM | 梯度累积步数多、优化器状态大 | 用ZeRO-2/3、8-bit Adam |
| KL计算OOM | 同时加载策略和参考模型 | 参考模型用CPU offload或量化 |
提示:KL计算需要同时拿到策略模型和参考模型对同一批token的概率。如果显存紧张,可以把参考模型量化到8-bit或4-bit,精度损失很小但显存省一半。
4.4 组大小G的选择与权衡
G的选择是一个典型的工程权衡:
- G太小(2-4):优势估计方差大,训练不稳定,但推理成本低;
- G太大(16-32):优势估计准确,但推理成本线性增长,训练速度慢;
- G=8:大多数场景的甜点区,方差和成本平衡较好。
但这不是绝对的。如果你的任务奖励信号很稀疏(比如只有0和1),G需要大一些(12-16)才能保证组内有正有负。如果奖励是连续的、区分度高,G=4就够用。
还有一个技巧:动态组大小。训练初期用大G(16)保证优势估计质量,训练后期策略逐渐收敛,用小G(4)节省推理成本。这个在TRL里可以通过callback实现。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 奖励不上升 | 奖励函数无区分度 | 打印奖励分布 | 重新设计奖励函数 |
| 奖励上升但输出变差 | reward hacking | 人工看输出 | 加KL约束、加惩罚项 |
| 训练loss震荡 | 学习率太大 | 看ratio和KL | 调小学习率、调大β |
| 零优势组比例高 | 任务太难/太简单 | 统计奖励方差 | 调整任务难度、调temperature |
| 生成速度慢 | 推理未优化 | profile生成阶段 | 用vLLM、减小max_new_tokens |
| KL散度爆炸 | β太小 | 监控KL值 | 调大β、调小学习率 |
| 显存OOM | batch或序列太长 | 看显存峰值 | 减batch、开checkpointing、ZeRO |
5. GRPO的变体与扩展方向
5.1 GRPO与因果强化学习的结合点
最近因果强化学习(Causal RL)是个热词,核心思路是把因果推断工具嵌入RL流程,解决“相关不等于因果”的问题。GRPO和因果RL的结合点在于优势估计的因果校正。
标准GRPO的组内优势Â_i = (r_i - μ) / σ,这个估计假设组内输出是可交换的,即没有混淆变量。但在实际场景里,prompt的难度、长度、领域都会影响奖励,这些是混淆变量。如果不控制,优势估计会有偏。
一个可能的改进方向是:在组内比较时,对混淆变量做分层。比如按prompt难度分层,在每个层内做组内标准化,再加权汇总。这样得到的优势估计更接近因果效应,而不是简单的相关性。
这个方向目前还在研究中,但思路是清晰的。如果你在做GRPO的定制化改进,这是一个值得探索的点。
5.2 离线GRPO与IQL的结合
IQL(Implicit Q-Learning)是离线RL的代表算法,核心是用expectile回归估计Q函数,避免查询分布外动作。GRPO目前主要是在线算法,需要实时采样。如果把GRPO和IQL结合,一个可能的方案是:
- 用离线数据预训练一个Q函数(IQL风格);
- 在线阶段用Q函数辅助组内优势估计,而不是纯靠奖励均值;
- 这样可以在组大小G较小时,仍然得到低方差的优势估计。
这个思路的本质是用离线价值估计来弥补在线组采样的方差。工程上实现起来有一定复杂度,但理论上很有吸引力。
5.3 多模态场景下的GRPO适配
GRPO目前主要在纯文本任务上验证。多模态场景(图像+文本)下,组采样的成本更高(图像编码+文本生成),奖励函数也更复杂(视觉质量+文本质量)。适配方向包括:
- 分层组采样:图像层面采样少量,文本层面采样多量;
- 多维度奖励分解:视觉奖励和文本奖励分别标准化,再加权合并;
- 跨模态KL约束:分别约束视觉编码器和文本解码器的偏离程度。
这些目前还没有成熟方案,但GRPO的组内比较框架是通用的,适配多模态只是奖励设计和采样策略的调整。
6. 个人实操体会与建议
GRPO是我过去一年里用得最多的RL算法,从7B到32B模型都跑过。最大的体会是:算法本身不复杂,复杂的是奖励函数设计和工程调优。GRPO的目标函数一页纸就能写完,但要让它在实际任务上work,80%的精力花在奖励函数迭代和显存优化上。
几个具体的建议:
第一,先跑通小规模再放大。用1B模型、G=4、100条数据先跑通全流程,确认奖励能上升、输出不崩,再换7B、G=8、全量数据。我见过太多人直接上大模型全量数据,跑了一天发现奖励函数写错了,浪费大量算力。
第二,奖励函数从简到繁。初期只用1个核心维度(比如答案正确性),跑通了再加格式、长度、重复惩罚。每加一个维度都要重新验证,确保新维度不会和旧维度冲突。
第三,监控三个核心指标:奖励均值、KL散度、零优势组比例。这三个指标能覆盖90%的训练异常。奖励均值不涨说明奖励函数或学习率有问题;KL散度爆炸说明β太小;零优势组比例高说明任务难度或temperature需要调。
第四,组大小G不要死守8。根据任务调整:奖励稀疏就调大,奖励密集就调小;训练初期调大,后期调小。动态G是一个很实用的技巧。
第五,KL系数β用自适应。固定β很难适应训练全程。一个简单的自适应策略是:如果KL > 2 * target_KL,β *= 1.5;如果KL < 0.5 * target_KL,β /= 1.5。target_KL一般设0.01-0.05。
最后分享一个小技巧:在奖励函数里加一个“格式奖励”作为稳定器。即使任务奖励信号很稀疏,格式奖励也能提供持续的梯度信号,防止模型在训练初期完全学不动。等任务奖励逐渐起来后,再降低格式奖励的权重。这个技巧在我做数学推理任务时特别管用,训练初期模型连“把答案放在\boxed{}里”都学不会,加了格式奖励后很快就稳定了。