ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GRPO参考实现全解析:从PPO到组相对策略优化的工程实践

GRPO参考实现全解析:从PPO到组相对策略优化的工程实践 1. GRPO到底是什么为什么最近大家都在聊GRPO全称Group Relative Policy Optimization组相对策略优化是最近强化学习训练大模型这条赛道里热度蹿升最快的一个算法。如果你关注过DeepSeekMath、DeepSeek-R1或者Kimi k1.5这类推理模型的技术报告大概率都见过这个词。它本质上是PPOProximal Policy Optimization近端策略优化的一种变体核心改动非常激进直接去掉Critic价值网络改用一组采样结果的相对比较来估计优势。我最早看到GRPO这个思路时第一反应是“去掉价值网络不会方差爆炸吗”。后来在数学推理任务上实测了几轮发现它非但不是偷工减料反而把PPO最棘手的两个问题——价值网络训练不稳定、优势估计偏差大——给绕过去了。这个算法的设计动机很朴素在生成式任务里同一个提示词让模型反复采样采样结果之间的相对好坏本身就比一个绝对的价值预测更可靠。打个比方你让一个学生反复做同一道题你不知道他“绝对水平”是多少分但你至少能看出哪几次作答更完整、推理链条更清晰。GRPO做的就是这件事不看绝对分只看相对排名。从工程角度看GRPO还有一个非常现实的优势显存占用低。PPO需要同时维护Actor策略模型、Critic价值模型和参考模型三个模型一起跑微调7B以上规模模型时显存压力非常大。GRPO干掉Critic之后训练阶段只需要策略模型和参考模型Multi-GPU部署的工程复杂度直接降了一个档次。这也是很多团队从PPO迁移到GRPO的直接原因不是PPO不好而是GRPO在当前生成式RL场景下更轻、更稳、更容易落地。这篇文章不讲花哨的理论推导我会把GRPO的参考实现按模块拆开从采样策略、优势估计、KL约束到损失函数一步步给你讲清楚每一步在做什么、为什么这么做、以及我踩过哪些坑。内容主要面向两类人一类是准备在自研模型上试RL的工程师另一类是想读懂GRPO源码但被各种符号劝退的算法同学。看完你应该能直接照着一套参考实现把训练流程跑起来。2. GRPO的核心机制拆解去掉Critic之后发生了什么2.1 PPO的优势估计和它的两个老大难问题要理解GRPO必须先从PPO说起。PPO的目标函数是裁剪后的策略比率目标L E[min(r_t(θ)·A_t, clip(r_t(θ), 1-ε, 1ε)·A_t)]其中r_t(θ)是新旧策略的概率比A_t是优势函数。PPO的经典做法是用GAEGeneralized Advantage Estimation广义优势估计来算A_t而GAE依赖一个Critic网络输出状态价值V(s)。这里就容易出现两个问题。第一个是Critic网络自身的训练不稳定。生成式RL里状态空间是离散的token序列价值函数极度非平滑Critic在短短几步内可能从V1.0跳到V-0.5导致优势估计的噪声远大于真实的奖励信号。第二个问题是Critic和Actor的“协作成本”。Actor和Critic必须保持同步更新任何一方的延迟都会导致整体训练曲线震荡。我见过不少团队在PPO里花70%的调试时间在调Critic的学习率、网络结构和价值裁剪范围真正留给策略优化的时间反而不多。2.2 GRPO的分组相对优势把绝对估值换成相对比较GRPO的思路非常直接不再预测一个绝对值而是用同龄人做参照系。具体做法是对同一个提示词x从当前策略π_θ中采样G个完整回复{y1, y2, ..., yG}对每个回复用奖励模型打分得到{r1, r2, ..., rG}然后做标准化A_i (r_i - mean(r)) / std(r)这个标准化后的值就是第i条样本的优势。它的含义是这条回复在本次采样的G条结果中相对位置如何。如果r_i高于组内平均优势为正策略更新时会鼓励这条轨迹如果低于平均优势为负策略更新时会抑制这条轨迹。这个设计巧妙的核心在于不需要精确的价值估计只需要组内排序正确。奖励模型的绝对分数漂移、尺度不一致、不同提示词之间的分数不可比——这些PPO时代让人头秃的问题在GRPO里被一次标准化全部消解。你可以理解为GRPO把“预测分数”这件很难的事替换成了“排队”这件容易的事。2.3 KL散度约束没有Critic之后如何防止策略跑飞去掉价值网络只是GRPO改动的一半另一半是和KL散度约束配合使用。PPO时代策略更新主要靠clip机制限制单步更新幅度GRPO在clip之外还加了一个逐token的KL惩罚项L E[min(r_t(θ)·A_t, clip(r_t(θ), 1-ε, 1ε)·A_t)] - β·KL(π_θ || π_ref)这里π_ref是参考模型通常是SFT阶段训好的模型权重在RL训练期间冻结。KL项的作用是惩罚策略偏离参考模型太远防止模型在奖励模型短板的地方钻空子。比如数学推理任务里奖励模型可能对“过程冗长但答案正确”的回复给出高分如果没有KL约束策略很快会学会刷长度把所有能写的步骤都堆上去。我在参考实现里用的是逐token的KL估计在生成每个token时同时记录策略模型和参考模型的对数概率二者相减就是该token的KL贡献。相比整句KL逐token的粒度更细能让模型在保持整体风格的同时逐步优化推理质量。2.4 为什么GRPO在生成式任务上比PPO更“皮实”GRPO在很多生成式RL场景下表现更稳本质原因有三点。第一方差控制好。PPO的Critic初始时预测误差很大优势估计前期基本是噪声GRPO的优势完全来自组内相对比较不依赖任何尚未训练好的模块从第一个step开始就是有效信号。第二超参数更少。PPO要调Critic学习率、GAE的λ、价值裁剪范围、价值损失系数GRPO把这些全砍了剩下主要是采样组数G、KL系数β、clip范围ε。第三显存占用低。这个前面说过少一个模型同样batch size下可以跑更大的策略模型或者把训练速度提上去。值得一提的是GRPO并不是银弹。在小样本场景或者奖励信号极其稀疏的任务里组内相对比较可能全是噪声——G条回复得分差不多标准化之后的优势被压缩到很小策略几乎不更新。这也是我后文会重点讲的“采样组数G”这个关键参数为什么如此重要。3. GRPO参考实现的完整流程从采样到参数更新3.1 整体训练循环长什么样一套标准的GRPO参考实现训练循环可以拆成六个阶段采样、打分、优势计算、损失计算、反向传播、周期性的KL监测。我用伪代码把主循环写出来后面每个阶段再展开# 伪代码GRPO主训练循环 for step in range(total_steps): # 1. 从当前策略采样 prompts sample_batch(prompt_dataset, batch_sizeB) rollouts policy_model.generate(prompts, num_return_sequencesG) # 2. 奖励模型打分 rewards reward_model.score(rollouts) # shape: [B, G] # 3. 组内相对优势 advantages (rewards - rewards.mean(dim-1, keepdimTrue)) / (rewards.std(dim-1, keepdimTrue) eps) # 4. 逐token KL散度 logprobs policy_model.compute_logprobs(rollouts) ref_logprobs ref_model.compute_logprobs(rollouts) kl logprobs - ref_logprobs # shape: [B, G, seq_len] # 5. 策略损失 ratios torch.exp(logprobs - logprobs_old) loss grpo_loss(ratios, advantages, kl, clip_eps0.2, beta0.01) # 6. 反向传播并更新 loss.backward() clip_grad_norm_(policy_model.parameters(), max_norm1.0) optimizer.step() # 7. 周期性记录KL和reward统计 if step % log_interval 0: log_metrics(logprobs, ref_logprobs, rewards)3.2 采样阶段num_return_sequences和分组策略的讲究采样阶段的核心参数是G即每个提示词生成几条回复。这个参数直接决定了优势估计的质量。我实测下来的经验是G4时训练能跑通但噪声偏大G8时质量明显改善G16往上提升开始边际递减且采样成本翻倍。参考实现里我的默认值是8如果你的奖励模型比较准G4也可以如果奖励噪声大建议至少设到12。采样时还有一个容易忽略的细节温度参数。GRPO在采样和优化之间的反馈回路要求采样分布能覆盖当前策略的探索范围。温度太低采样结果全部集中在高概率区域组内方差小标准化之后优势趋近于零训练没有信号温度太高采样结果里大量无关token干扰KL约束。我的做法是采样温度固定为1.0不做annealing让探索自然发生。这和我早期尝试过的“先高温探索再低温收敛”策略相比训练曲线更平滑。另一个细节是max_new_tokens的设置。参考实现里我用的是分段限制比如推理任务中提示词本身可能几百token回复限制为1024 token。这个值不能太短否则很多推理链被截断奖励模型面对半截答案打分不稳定但也不能太长否则KL偏差累计过大策略容易在长尾区域游走。如果某个任务的回复长度天然差异大可以考虑按长度做分桶采样让组内样本的长度分布尽量接近。3.3 奖励打分与组内标准化为什么mean/std就能稳住优势采样完成后每个提示词的G条回复会被送到奖励模型打分。奖励模型可以是训练好的RM也可以是一个基于规则的验证器比如代码任务里的单元测试通过率、数学任务里的答案正确性。打分完成后关键操作就是组内标准化mean_r rewards.mean(dim-1, keepdimTrue) std_r rewards.std(dim-1, keepdimTrue) advantages (rewards - mean_r) / (std_r 1e-8)这个标准化有两个作用。第一是尺度对齐不同提示词的奖励分布可能差异巨大有的任务所有回复得分都在0.8到0.9之间有的任务得分可能在0.1到0.9之间大幅度波动不做标准化的话前者优势几乎是0后者优势能到1.0以上训练会被高分差任务主导。第二是奖励模型漂移免疫同一个奖励模型在训练过程中分数分布可能整体上移如果不做标准化策略会认为“模型在变好”实际上只是奖励模型变得更宽容了。标准化之后模型关注的只是组内排序不会被绝对分数欺骗。实际操作中我还会在标准化之后做一次clip把advantage限制在[-2, 2]区间。原因很简单偶尔会出现极端奖励比如一条回复触发器触发了一个bug导致奖励暴涨如果不clip这条样本的梯度会淹没整个batch的梯度。clip范围不是固定值如果你发现训练早期方差过大可以收紧到[-1.5, 1.5]后期再放宽。3.4 逐token KL散度参考模型的冻结与计算细节KL惩罚项是GRPO防止策略“跑飞”的保险丝参考实现里用逐token KL来近似。具体做法是对采样得到的每条回复序列分别计算当前策略模型和参考模型在每个token位置上的对数概率差求和取平均得到序列级KL估计。在工程实现上有一个重要细节计算logprobs时策略模型和参考模型需要在同一套tokenizer下、以完全相同的输入格式推理。我在参考实现里踩过一个坑参考模型用的是和策略模型相同的基座模型但SFT之后tokenizer的special token行为变了导致逐token的对齐错位KL值虚高。后来统一用基座模型的tokenizer做RL训练所需的全部生成和打分才彻底解决。KL系数β的选择也有一套经验法则。β太大策略更新会被KL惩罚压制训练效果接近参考模型原地踏步β太小策略快速偏离参考模型出现重复、绕弯、回答过长的现象。我常用的初始值是0.01然后根据KL散度的实际变化做自适应调整如果KL超过预设阈值比如0.5 nats把β乘1.5如果KL长期低于0.05把β除以1.5。这个自适应策略在参考实现里只用了十几行代码却省去了大量手动调参时间。4. grpo_loss的参考实现与关键参数解析4.1 一个可以直接用的grpo_loss实现下面这段是参考实现里grpo_loss的核心部分基于PyTorch风格编写。代码里我刻意保留了变量名和注释方便你对照公式理解import torch import torch.nn.functional as F def grpo_loss( logprobs, # 当前策略对数概率 [B, G, seq_len] old_logprobs, # 采样时的策略概率快照 [B, G, seq_len] ref_logprobs, # 参考模型对数概率 [B, G, seq_len] advantages, # 组内标准化的优势 [B, G] clip_eps0.2, beta0.01, ): B, G, S logprobs.shape # 概率比当前策略 / 旧策略 log_ratio logprobs - old_logprobs ratio torch.exp(log_ratio) # 对每个token计算KL惩罚逐token per_token_kl logprobs - ref_logprobs # 优点扩展成序列级每条回复所有token共享同一个advantage adv advantages.unsqueeze(-1).expand(B, G, S) # [B, G, seq_len] # 裁剪策略目标 pg_losses -adv * ratio pg_clipped -adv * torch.clamp(ratio, 1.0 - clip_eps, 1.0 clip_eps) pg_loss torch.max(pg_losses, pg_clipped) # 加入KL惩罚参考DeepSeekMath论文KL按token平均后求和 kl_loss per_token_kl * adv * 0 # 注意KL不乘advantage loss (pg_loss beta * per_token_kl).mean() return loss实际在参考实现里我稍微做了一点调整KL惩罚项没有用逐token直接相加而是先对每条序列做token级平均再做batch级平均这样能防止长序列累计过高的KL惩罚。标准做法之间差异不大但如果你复现时发现KL项除以G之后训练反而更不稳可以对比一下是“序列平均”还是“全量求和”造成的。4.2 旧概率快照为什么必须缓存采样时的logprobsgrpo_loss里用到的old_logprobs是采样阶段就缓存下来的。这个值的意义是策略更新时我们需要知道“当前策略相对于采样时策略提高了多少”——如果模型已经更新了好几步再去计算old_logprobs就失去了对照基准。工程上我推荐在采样阶段就把每条生成的回复的logprobs存下来存成和rollout序列对齐的tensor而不是重新前向计算一遍。原因有两个一是节省计算量重新前向一次等于又跑了一遍生成模型二是避免结果不一致dropout、模型同步之类的细微差异会导致old_logprobs和实际采样时分布有偏差削弱clipping机制的保护效果。如果你用的是HuggingFace transformers库可以在generate时开启return_dict_in_gestureTrue和output_logitsTrue拿到逐步logits再手动转为logprobs。但要注意这个logits是包含所有vocab的显存开销比较大。另一种省显存的做法是采样时单独做一次前向只取采样token位置上的logits来计算logprobs存成紧凑tensor。参考实现里用的是后者。4.3 clip范围ε的取值0.2是默认值但不总是最优PPO系算法里ε0.2是一个广为接受的默认值GRPO参考实现也沿用这个值。但这个值并不是放之四海皆准的。ε控制的是单次更新步长ε越大允许的更新幅度越大收敛快但可能震荡甚至发散ε越小更新越保守训练稳定但速度慢。我在数学推理数据集上测试过一组对比ε0.1时训练约500步后reward还在缓慢上升但上升速度和0.2几乎一致ε0.3时训练前100步曲线很猛到300步时开始能看到reward的高频抖动。最终我保留0.2的默认值只在奖励噪声特别大的任务里调到0.1。需要说明的是GRPO因为去掉了Critic本身更新信号就比PPO纯粹所以对ε的敏感度比PPO低一些这也是为什么我们能容忍0.2这种PPO默认值。5. GRPO参考实现中的模型选型与显存策略5.1 策略模型、参考模型、奖励模型之间的显存分配一套GRPO训练任务涉及三个模型策略模型可训练、参考模型冻结、奖励模型冻结。参考实现里它们的关系是策略模型和参考模型共享相同的基座结构和tokenizer奖励模型可以不同但最好用同一个基座继续训练得到。显存分配策略上我推荐“混合精度参数冻结梯度检查点”三件套。策略模型用fp16或bf16训练参考模型用fp16推理且完全冻结奖励模型如果输入序列长度大可以考虑offload到CPU只在打分时加载到GPU。DeepSeekMath原论文的配置是13B策略模型加奖励模型权重转换和逐batch调度很讲究我自己试下来7B模型用单卡A100-80G可以勉强跑训练13B模型建议至少双卡一张放策略模型一张放参考奖励模型。5.2 参考模型的选择SFT模型还是原始基座这里有一个很多人会搞混的地方参考模型到底用SFT之后的模型还是原始基座模型GRPO论文里的做法是用SFT阶段训练好的模型作为初始策略同时这个初始策略的权重拷贝一份冻结作为参考模型。也就是说参考模型应该和策略模型的起点完全一致这样KL项衡量的是“RL训练带来的偏移量”。我见过一些团队直接用原始基座模型当参考模型这在效果上会有明显问题如果参考模型和策略模型起点不一致KL项从一开始就是非零的大值KL惩罚会干扰策略优化。正确的做法是SFT完成后把SFT权重复制一份一份作为策略模型的初始化权重进入RL训练另一份冻结作为参考模型。5.3 奖励模型处理技巧长度正则与奖励归一化奖励模型在GRPO里起到的作用远比PPO里的Critic重要因为它直接决定了“相对好坏”的来源。参考实现里我加入了一个长度正则项用来缓解奖励模型偏好长回答的问题final_reward raw_reward - length_penalty * (seq_len - target_len) ** 2这个正则不是必须的但如果你的奖励模型在训练数据上对长序列有偏好不加这个正则GRPO会非常快地学会“把所有回答拉长”。我在一次代码生成任务里就吃过这个亏最终KL项报警检查rollout发现回复平均长度从200 token涨到了800 token回答质量并没有提升。6. GRPO训练中的常见问题与排查技巧实录6.1 训练早期loss爆炸reward快速飙升后骤降这个现象我在GRPO初期实验里遇到得最多。典型特征是前50步loss涨得很快reward跟着起飞然后第60步左右突然崩盘。排查思路基本上是三步先看优势分布如果标准化后的advantage出现大量大于2的极值检查是否有奖励异常比如某个提示词的某条回复触发了奖励模型的bug打分再看KL值如果KL已经超过0.5甚至1.0说明β太小策略跑得太快最后看梯度范数如果clip_grad_norm之前梯度范数已经超过5把max_norm收紧到0.5或1.0同时降低学习率。我的经验是GRPO的loss绝对值本身没有太大参考价值重点看两个比值优势均值与优势标准差之比、KL散度与step变化之比。前者反映信号强度后者反映策略漂移速度。这两组值正常loss波动大一点不用太紧张。6.2 采样组数G到底设多大合适成本和质量的平衡这个问题没有通用答案但有一个参考策略先在固定预算下做小规模消融。具体做法是选一个1000条prompt的子集分别用G4/8/16跑100步看reward的均值与方差变化。如果G4的reward上升速度和G16相同你完全可以省掉采样成本如果G4的reward曲线抖动明显优先增加G而不是调其他超参。从成本角度看G每翻一倍生成计算量翻一倍但优势估计方差的减少是递减的。工程上我还会考虑“G个采样之间的多样性”如果温度设太高G个采样内容过于发散组内比较的另一条极端——最好的结果可能只是碰巧抽中了一个极端样本——又会出现。所以G的选择要配合温度一起看别单独调。6.3 奖励信号稀疏时GRPO的优势失效怎么办GRPO对奖励质量极端敏感。如果你的任务奖励非常稀疏比如90%的采样得0分、只有10%得1分那么组内标准化后的优势分布会极度偏斜多数样本的优势是负的只有少数是正的策略可能长时间停留在惩罚区域不动。这种情况下GRPO的组内标准化反而成了一个劣势。我的应对方案有两类。第一类是改奖励把稀疏奖励改成稠密奖励比如数学题里不只看最终答案正确还看中间步骤的格式、关键公式是否出现。第二类是混合目标在GRPO损失基础上叠加一个行为克隆损失强制策略向SFT数据中的参考行为靠拢等奖励信号逐渐丰富后再退去行为克隆项。这个做法虽然不纯粹但在工程上很有效尤其是在早期冷启动阶段。6.4 训练后期模型输出退化重复、套话、绕弯这是GRPO训练很长一段时间后容易出现的现象。原因基本是KL约束变弱、奖励模型被策略“欺骗输入”后给出了虚高奖励。排查时先看KL曲线如果后期KL值还在持续上升说明β衰减得太快或自适应调整策略过激进。如果KL值没有异常但输出还是退化基本可以断定是奖励模型的问题需要重新校准奖励模型或者加入规则过滤器。我在参考实现里加了一个简单但实用的措施每N步做一次人工抽检把当前策略的生成结果和参考模型的生成结果放在一起盲评后给奖励模型打质量分。这个抽检不是为了训练而是为了尽早发现“策略学会了刷奖励”的苗头。自动化指标再漂亮也架不住模型在评价维度上钻空子毕竟这就是RL的本质——它优化的是你给的目标不是你真的想要的东西。7. GRPO参考实现之外算法的边界与扩展方向7.1 GRPO适合什么任务不适合什么任务从我的实践经验来看GRPO最适合的场景是“采样成本可控、奖励信号可比较”的生成式任务典型代表是数学推理、代码生成、问答质量优化。这些任务的共同点是同一个提示词可以多次采样采样结果之间的质量差异可以被奖励模型有效区分。不太适合的场景包括奖励信号稀疏且不可比较的环境交互式任务比如机器人控制、多智能体博弈。这类任务里单次采样的奖励强依赖环境状态不同episode之间直接比较意义不大GRPO的组内标准化会丧失参照系。如果你的项目正好是这类建议去看基于价值估计的算法比如offline RL里的IQL、在线RL里的PPO或者做Causal RL这类把因果推断引入强化学习流程的探索——把因果关系嵌入奖励建模能部分缓解奖励噪声问题。至少从我了解到的方向来看因果强化学习在奖励分解和credit assignment上是比GRPO更有潜力的方向。7.2 从参考实现到生产级训练框架还要补哪些模块参考实现能跑通实验但要做生产级RL训练还差几个模块。第一是采样与训练的异步管道生成是逐个batch的如果同步等待生成完成GPU利用率会惨不忍睹参考实现里用两个线程池分别跑生成和训练。第二是rollout存储管理因为GRPO需要一次性存G条回复的完整序列、logprobs和奖励存储格式和shuffle策略会直接影响训练吞吐。第三是训练监控可视化至少要有reward均值、KL散度、生成长度、优势分布四张曲线图缺一张你都很难判断训练是否健康。还有一个容易被忽略的模块是评估集。RL训练过程中reward上升不代表真实能力上升你需要一个独立的评估集定期测试模型在标准 benchmark上的表现。参考实现里我每50步跑一次eval把eval结果单独记录和训练reward区分开。7.3 未来方向GRPO的改进变体和与其他RL思路的融合GRPO还很年轻改进空间不小。一个方向是自适应KL系数把β从固定值改成基于KL目标自动调节的PID控制器这个方法在PPO领域已经很成熟可以直接移植到GRPO。另一个方向是优势估计的平滑比如对组内reward做nash均衡求解而非简单标准化这在部分论文里被称为“基于博弈论的优势建模”特别适合奖励模型存在对抗性偏差的情况。还有一个思路是把GRPO的行为克隆混合扩展成更一般的框架先用SFT数据做预热然后对部分prompt以更高概率采样高质量参考让策略在“参考行为”和“探索行为”之间灵活切换。这个做法有点类似离线RL里的混合策略初始化实现在参考实现里改动不大但能显著提升训练初期的稳定性。最后如果你做的是多智能体强化学习或者多AGV路径规划这类高维决策任务GRPO那种“组内相对比较”的思想也可以借鉴过来设计多智能体之间的奖励归一化只是复杂任务里不建议直接用原版需要配合环境建模一起改。写在后面这套GRPO参考实现我前前后后迭代了三轮从最初照搬DeepSeekMath论文里的公式到后来逐步加入自己的工程化修改最大的体会是GRPO真正好的地方不在于“去掉Critic”这个形式创新而在于它把“绝对价值估计”这个学习问题简化成了“组内相对排序”这个比较问题——在生成式RL这种奖励信号天然嘈杂的场景里比大小永远比估数值更可信。如果你正打算在自己模型上试GRPO我的建议是先别急着上大规模训练拿一个1000条的小数据集把采样、打分、优势计算、KL约束这一整套流程跑通多打印一些统计量看看分布长什么样再扩大规模。这个流程里的每个环节看着都不复杂但串起来之后隐藏的坑一个比一个多。好在GRPO的工程复杂度已经比PPO低了一大截这也是它能够在短时间里成为主流选择的重要原因。希望这篇文章能帮你少走一些弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进