首页 / 资讯中心 / 文章详情

GRPO强化学习实战:从PPO显存痛点到组内优势与训练排坑

GRPO强化学习实战:从PPO显存痛点到组内优势与训练排坑 ★ FEATURED ARTICLE
如果你在 2025 年之后动手做过大模型强化学习训练大概率会在同一个地方卡住PPO 明明跑通了但显存怎么都省不下来。Actor 模型要占一份Critic 模型又要占一份再加 Reference 模型7B 的模型跑 RLHF 时显存直接翻三倍。更难受的是Critic 学得不准优势估计就会跑偏训练曲线也就跟着一起偏离预期。这个痛点正是 GRPOGroup Relative Policy Optimization要解决的事情。它的思路很直接既然 Critic 的作用只是提供一个基线baseline那我干脆不要 Critic改成对同一个问题采样一组回答用这组回答内部的奖励均值当作基线、奖励标准差当作尺度直接算出优势。显存省下来了超参也少了一半。这篇文章是强化学习系列的其中一篇我不打算把 GRPO 讲成论文翻译而是按照“为什么需要它 → 公式里每一项在代码里是什么 → 怎么跑起来 → 哪里容易出问题”的顺序走一遍。读完之后你应该能自己实现一版 GRPO loss也能用现成框架跑通一次小规模训练并且知道训练不收敛时该优先检查哪几个地方。1. 为什么 GRPO 值得单独拆开讲在 GRPO 出现之前大模型对齐的主流路线是 PPO。PPO 本身没问题它稳定、通用、经过大量工程验证但它有一个隐含前提你必须有一个能给出准确价值估计的 Critic。在传统强化学习任务里这个前提不算过分状态空间有限Critic 可以用较小的网络拟合。但在 LLM 场景下Critic 面对的是“一句话的每一个 token 之后的期望回报”输入的维度、序列长度、语义复杂度都和 Actor 完全一致所以业界普遍把 Critic 设成和 Actor 同规模。代价立刻体现出来。一个 7B 模型做 PPO 训练需要同时驻留 Actor、Critic、Reference、Reward 四个模型显存峰值轻松越过单卡极限。而且 Critic 是从零开始学的早期价值估计几乎全是噪声优势函数 A R - V 里的 V 一旦偏梯度方向就跟着偏。很多团队调到后面发现不是策略不行是 Critic 拖了后腿。GRPO 的破局点在于重新审视“基线”这个概念。基线的本质作用是降低方差它不一定要是一个神经网络。如果同一个 prompt 采样出 8 个回答这 8 个回答的平均奖励天然就是“当前策略在这个问题上的期望表现”完全可以当基线用。这个想法在 REINFORCE with baseline、RLOO 等工作中都有影子GRPO 把它做成了完整的、可大规模训练的算法并在 DeepSeekMath 的论文里系统化地给出后续 DeepSeek-R1 的推理能力训练也采用了这条路线。所以判断很明确GRPO 降低的是“RL 训练的显存门槛和调参复杂度”不是“RL 的理论难度”。它让中小团队有可能在有限卡数上做真正的在线强化学习而不是只能退而求其次去做 DPO 这类离线偏好优化。但它也带来了新的问题后面第 7 节会重点讲尤其是“组内奖励全一样导致优势全为 0”这个几乎人人都踩的坑。这篇文章最适合三类读者已经了解 PPO 基本流程、想搞清楚 GRPO 到底改了什么的人准备用框架跑一次 GRPO、但被参数和奖励函数卡住的人以及训练跑起来了、但曲线不动或者崩了、需要排查思路的人。2. GRPO 的核心概念用组内比较替代价值网络2.1 从 PPO 的架构差异说起先把两套流程摆在同一个视角下看。PPO 的一次迭代大概是这样给定 prompt也就是状态Actor 生成一个回答Reward 模型打分Critic 对每一步给出价值 V(s)然后用 TD 误差或者 GAE 算出优势最后用裁剪的重要性采样比率更新 Actor同时用回归损失更新 Critic。GRPO 的流程是给定 promptActor 一次性生成 G 个回答G 通常取 8、16、64 这样的值Reward 模型或者规则函数给这 G 个回答分别打分然后直接在这 G 个分数内部做标准化得到每个回答的优势接着用和 PPO 完全一样的裁剪目标更新 Actor。整个过程里没有 Critic也没有价值回归损失。这个改动看起来只是“换了个基线”但影响是连锁的。第一显存结构变了省掉一整个和 Actor 同规模的模型。第二训练目标变了从“拟合价值 更新策略”两个目标变成了单一的策略目标超参搜索空间缩小。第三也是最容易被忽略的一点训练信号的语义变了PPO 的优势是“这个回答比 Critic 预期的好多少”GRPO 的优势是“这个回答比同批次其他回答好多少”后者是相对的场景内比较前者是绝对的跨场景可比。2.2 组内归一化到底在做什么很多人第一次看 GRPO 的公式会疑惑为什么要除以标准差只减均值不行吗原因在于不同 prompt 的奖励尺度差异极大。有些数学题的奖励分布在 0 到 1 之间有些代码题的奖励函数可能输出 0 到 100还有的 prompt 组内分数全是 0.9 附近的小波动。如果只用 (r - mean) 做基线那么高方差组的梯度会主导整个 batch低方差组几乎不贡献学习信号。除以标准差相当于给每一组做了归一化让不同 prompt 的梯度量级回到同一个尺度上。这也解释了 GRPO 的一个隐含假设同一个 prompt 下的多个回答之间必须存在分数差异否则标准差接近 0归一化会把噪声放大成无效信号。DeepSeekMath 论文里给出的处理是在分母上加一个很小的常数数学上写成 std ε工程实现里通常会写成 1e-4 或者 1e-8。这个常数的取值其实有讲究太小会在全同分时放大浮点误差太大会压缩正常组的优势幅度实践中 1e-4 是比较常见的折中。2.3 GRPO 与 PPO、DPO、RLOO 的定位对比维度PPOGRPORLOODPO是否需要 Critic需要不需要不需要不需要基线来源价值网络 V(s)组内奖励均值留一法均值无离线采样方式在线在线每题 G 个在线每题 K 个离线用现成偏好对显存占用高中中低训练稳定性依赖 Critic 质量依赖组内方差依赖 K 的大小依赖偏好数据质量典型场景通用 RLHF可验证奖励的推理任务轻量 RLHF对齐微调RLOO 和 GRPO 的区别值得单独说一句因为两者经常被混为一谈。RLOO 用的是留一法第 i 个样本的优势等于它的奖励乘以 K 再减去组内总奖励本质上算的是“去掉我自己之后其他人的平均值”。GRPO 则是直接用全体均值并且额外做了标准差归一化。数学上 RLOO 的优势是 (K/(K-1))·(r_i - mean_{-i})GRPO 是 (r_i - mean)/std。前者无偏但尺度不归一后者有偏但尺度统一。两者都有效选择哪个更多取决于奖励函数的分布特征。3. 目标函数拆解公式每一项在代码里长什么样GRPO 的目标函数可以拆成三块我按照“从外到内”的顺序讲每一块都对应后面代码里的几行。3.1 优势估计组内标准化的具体实现对于一个问题 q从旧策略 π_θold 采样 G 个输出 {o_1, ..., o_G}用奖励函数得到 {r_1, ..., r_G}第 i 个输出的优势定义为Â_i (r_i - mean(r_1..r_G)) / (std(r_1..r_G) ε)这里有一个工程细节必须强调mean 和 std 是在整个组内算的而且这个组是“同一个 prompt 的 G 个采样”不是整个 batch。如果实现时把 batch 里所有样本混在一起算均值和方差那就不叫 GRPO 了样本难度差异会被抹平训练效果会明显变差。这也是很多手写实现最常见的 bug。另一个细节是优势必须 detach。优势是常量不参与梯度回传。如果忘记 detach梯度会顺着优势回流到奖励函数里轻则训练不稳定重则直接报错。3.2 裁剪的重要性采样和 PPO 同源的稳定性机制优势算完之后就是标准的 PPO 式目标J E[ (1/G) Σ_i min( ratio_i · Â_i , clip(ratio_i, 1-ε, 1ε) · Â_i ) ]其中 ratio_i π_θ(o_i|q) / π_θold(o_i|q)也就是新旧策略在同一个输出上的概率比。裁剪的作用是限制单次更新的幅度当 ratio 偏离 1 太多时目标函数会截断梯度归零避免一次更新把策略推到不可恢复的地方。这里有个比 PPO 更微妙的地方。PPO 的 ratio 是逐 token 算的GRPO 在 LLM 场景下同样是逐 token 算但优势是序列级的。换句话说一个序列里所有 token 共享同一个 Â_i。这就带来一个问题长回答的 token 数量多对 loss 的贡献自然更大模型会倾向于生成更长的回答而长度往往和正确性无关。这个问题在 DAPO 等工作里被明确提出来解决方案是把序列级 loss 改成 token 级归一化或者对超长输出做惩罚。第 8 节会展开。3.3 KL 正则与 k3 估计量最后一个部分是 KL 惩罚用来约束新策略不要偏离参考模型太远D_KL(π_θ || π_ref) 的近似DeepSeekMath 用的是无偏的 k3 估计量形式是 exp(log_ratio) - log_ratio - 1其中 log_ratio log(π_ref) - log(π_θ)。和常见的 k1 估计量 log(π_θ/π_ref) 相比k3 永远非负方差更小而且不会出现“KL 估计值为负”的反直觉情况。系数 β 控制约束强度。DeepSeekMath 论文中使用的典型值是 0.04但这个值强依赖于奖励模型的质量。如果奖励函数是规则型的、几乎不可能被 hackβ 可以调小甚至设为 0如果奖励来自一个学出来的 RMβ 通常要调大一些否则模型很快会找到钻空子的输出。4. 环境准备与前置条件动手之前先把环境说清楚。这一节不给死版本号因为 GRPO 相关库的迭代速度极快写死版本反而会误导人重点讲清需要哪些组件、以及怎么确认它们装对了。硬件方面最小可跑通的配置是单张 24GB 显存的卡配 0.5B 到 1.5B 的模型G 取 4 到 8序列长度控制在 512 以内。如果你想复现论文级别的效果那需要多卡和更大的 G但那是另一回事本文的目标是先跑通再谈效果。软件方面需要几个组件。第一是 PyTorch建议 2.1 以上并且确认 CUDA 版本和显卡驱动匹配。第二是 transformers用于加载模型和 tokenizer。第三是训练框架选择有两个方向用 TRL 的GRPOTrainer快速起步或者用 verl、OpenRLHF 这类面向大规模分布式训练的框架。本文的示例代码基于 TRL 的接口因为它的抽象层次最适合理解算法本身。第四是数据集和奖励函数。这是最容易被低估的部分。GRPO 的效果上限几乎完全由奖励函数决定如果没有可靠的、可自动判定的奖励信号跑再久也学不出东西。所以准备阶段一定要先确认你的任务能不能写出一个确定性、低延迟、不易被 hack 的判分函数。最后安装完成后建议先做一次环境自检确认关键组件版本和 GPU 可用性避免训练跑到一半才发现某个依赖没装。# 环境自检脚本保存为 check_env.py import torch import transformers import trl print(PyTorch:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) if torch.cuda.is_available(): print(GPU:, torch.cuda.get_device_name(0)) print(Total VRAM (GB):, round(torch.cuda.get_device_properties(0).total_memory / 1024**3, 2)) print(Transformers:, transformers.__version__) print(TRL:, trl.__version__) print(BF16 supported:, torch.cuda.is_bf16_supported() if torch.cuda.is_available() else False)运行结果是打印一串版本号和建议开启的精度类型。如果BF16 supported是 False训练脚本里就要把bf16换成fp16并且加fp16True对应的 grad scaler否则可能出现 loss 为 NaN。5. 手写一版最小 GRPO loss在调用框架之前我强烈建议先自己写一遍 loss。这是理解 GRPO 最快的方式代码不到 40 行但它把第 3 节的三个公式块全部落实了。# 文件路径grpo_core.py import torch def compute_grpo_loss( policy_logps: torch.Tensor, # [G] 当前策略对每个采样回答的平均 log prob带梯度 old_logps: torch.Tensor, # [G] 采样时策略的 log probdetach ref_logps: torch.Tensor, # [G] 参考模型 log probdetach rewards: torch.Tensor, # [G] 每个回答的标量奖励 beta: float 0.04, epsilon: float 0.2, eps_std: float 1e-4, ): 返回 (总 loss, 策略 loss, kl loss)G 为同一个 prompt 的采样数 # ---------- 1) 组内标准化得到优势 ---------- rewards rewards.float().detach() mean_r rewards.mean() std_r rewards.std(unbiasedFalse) advantages (rewards - mean_r) / (std_r eps_std) advantages advantages.detach() # 优势是常量绝不参与反传 # ---------- 2) 重要性采样比率 ---------- log_ratio policy_logps - old_logps ratio torch.exp(log_ratio) # ---------- 3) 裁剪的 PPO 目标 ---------- surr1 ratio * advantages surr2 torch.clamp(ratio, 1.0 - epsilon, 1.0 epsilon) * advantages policy_loss -torch.min(surr1, surr2).mean() # ---------- 4) k3 形式的 KL 估计 ---------- kl_per_sample torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1.0 kl_loss beta * kl_per_sample.mean() return policy_loss kl_loss, policy_loss.detach(), kl_loss.detach()这段代码有四行值得单独解释。第一行rewards.float().detach()注意这里用的是.detach()后再算均值和标准差保证优势的梯度链是断的。第二行torch.clamp(ratio, 1.0 - epsilon, 1.0 epsilon)epsilon 通常取 0.2这个值在 GRPO 里一般不需要像 PPO 那样精细调节因为优势已经归一化到 1 左右梯度幅度本身就比较可控。第三行 KL 估计torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1.0这个表达式看起来有点吓人但它其实就是 exp(x) - x - 1 在 x0 附近的最小值为 0保证了 KL 非负。最后一行policy_loss kl_loss有些实现会把 KL 直接加进奖励里reward shaping两种做法等价但梯度路径不同加进 loss 更简洁。给这个函数喂一组构造数据可以快速验证数值行为# 文件路径test_grpo_core.py import torch from grpo_core import compute_grpo_loss # 构造一个组4个采样其中两个答对奖励1两个答错奖励0 rewards torch.tensor([1.0, 1.0, 0.0, 0.0]) old_logps torch.tensor([-1.0, -1.0, -1.0, -1.0]) policy_logps torch.tensor([-0.8, -1.1, -0.9, -1.2], requires_gradTrue) ref_logps torch.tensor([-1.0, -1.0, -1.0, -1.0]) total, pl, kl compute_grpo_loss(policy_logps, old_logps, ref_logps, rewards) print(ftotal{total.item():.6f} policy{pl.item():.6f} kl{kl.item():.6f}) total.backward() print(policy_logps.grad , policy_logps.grad)预期输出里policy loss 约为 -0.9 左右因为答对的样本概率上升、答错的下降KL 项接近 0因为 policy_logps 和 ref_logps 很接近。如果你看到 KL 是负数说明 k3 写错了如果 grad 全是 0说明优势没有正确传到 ratio 上检查一下是不是把 logps 也 detach 了。这一步的意义不只是验证代码而是建立直觉GRPO 的梯度方向完全由“组内谁比谁好”决定而不是由“绝对分数多高”决定。当你在真实训练中发现 loss 不下降时第一反应应该是去看奖励的组内分布而不是去调学习率。6. 用框架跑通一次完整训练手写 loss 解决的是“懂不懂”跑框架解决的是“能不能用”。这一节用 TRL 的GRPOTrainer走一遍完整流程接口以较新版本为准参数名如果有差异请对照你本地安装版本的文档调整。6.1 数据集格式GRPO 需要的数据是 prompt 标准答案如果是规则奖励或者 prompt 任意可以被奖励函数用到的字段。最小数据集长这样# 文件路径build_dataset.py from datasets import Dataset raw [ {prompt: 计算 12 加 35 等于多少只输出数字。, answer: 47}, {prompt: 计算 8 乘 7 等于多少只输出数字。, answer: 56}, {prompt: 计算 144 除以 12 等于多少只输出数字。, answer: 12}, {prompt: 计算 9 加 17 等于多少只输出数字。, answer: 26}, {prompt: 计算 25 减 13 等于多少只输出数字。, answer: 12}, ] dataset Dataset.from_list(raw) dataset dataset.train_test_split(test_size0.2, seed42) print(dataset)字段名不重要重要的是answer会被透传到奖励函数里作为 kwargs。如果你的数据有多个辅助字段奖励函数的签名里写上对应的参数名就能拿到。6.2 奖励函数GRPO 的真正胜负手奖励函数是 GRPO 训练中唯一提供学习信号的地方写坏了整套流程就废了。下面的例子做一个按答案完全匹配的判分并且做了格式容错能处理模型输出里的额外文本和空白。# 文件路径reward_funcs.py import re import ast def _to_text(completion): 兼容两种 completions 形态str 或 [{role:..,content:..}] if isinstance(completion, str): return completion if isinstance(completion, list) and completion and isinstance(completion[0], dict): return completion[0].get(content, ) return str(completion) def _extract_number(text): 从文本里抽出最后一个数字覆盖 47 / 47.0 / -3 等形态 nums re.findall(r-?\d(?:\.\d)?, text) if not nums: return None try: return float(nums[-1]) except ValueError: return None def accuracy_reward(completions, answer, **kwargs): 逐条打分命中答案给1.0否则给0.0 rewards [] for comp, gold in zip(completions, answer): text _to_text(comp) pred _extract_number(text) try: gold_val float(str(gold).strip()) except ValueError: rewards.append(0.0) continue if pred is None: rewards.append(0.0) elif abs(pred - gold_val) 1e-6: rewards.append(1.0) else: rewards.append(0.0) return rewards def format_reward(completions, **kwargs): 格式分输出长度合理且只含数字时给 0.2抑制啰嗦 rewards [] for comp in completions: text _to_text(comp).strip() if 0 len(text) 16 and _extract_number(text) is not None: rewards.append(0.2) else: rewards.append(0.0) return rewards这里用「准确率 格式」两个奖励做加权和是实践中很常见的组合。加格式分的目的是给那些“答错了但格式对”的样本一点正向信号避免训练早期所有样本都是 0 分、优势全为 0、模型完全学不到东西。这个技巧在推理类任务里几乎是必备的。但格式分也有反作用如果权重给太高模型会学会“只输出一个数字”哪怕这个数字是错的因为它能稳定拿到格式分。所以权重最好控制在主奖励的 20% 以内。6.3 训练脚本把数据、奖励、配置拼起来就是完整的训练入口。# 文件路径train_grpo.py from datasets import Dataset from trl import GRPOConfig, GRPOTrainer from reward_funcs import accuracy_reward, format_reward raw [ {prompt: 计算 12 加 35 等于多少只输出数字。, answer: 47}, {prompt: 计算 8 乘 7 等于多少只输出数字。, answer: 56}, {prompt: 计算 144 除以 12 等于多少只输出数字。, answer: 12}, {prompt: 计算 9 加 17 等于多少只输出数字。, answer: 26}, {prompt: 计算 25 减 13 等于多少只输出数字。, answer: 12}, {prompt: 计算 63 除以 9 等于多少只输出数字。, answer: 7}, {prompt: 计算 18 加 26 等于多少只输出数字。, answer: 44}, {prompt: 计算 100 减 37 等于多少只输出数字。, answer: 63}, ] train_dataset Dataset.from_list(raw) training_args GRPOConfig( output_diroutputs/grpo-demo, learning_rate1e-6, per_device_train_batch_size8, # 必须能被 num_generations 整除 gradient_accumulation_steps2, num_generations8, # 每个 prompt 采样 8 条 max_prompt_length64, max_completion_length32, temperature1.0, beta0.04, epsilon0.2, num_train_epochs3, logging_steps1, save_steps50, bf16True, report_tonone, ) trainer GRPOTrainer( modelQwen/Qwen2.5-0.5B-Instruct, reward_funcs[accuracy_reward, format_reward], argstraining_args, train_datasettrain_dataset, ) trainer.train() trainer.save_model(outputs/grpo-demo/final)有三个参数必须连起来看。num_generations是每个 prompt 的采样数 Gper_device_train_batch_size是单卡一个 micro batch 里能放下的样本总数它必须是 G 的整数倍因为一个 prompt 的 G 条采样必须落在同一个 forward 里才能算组内优势。如果设成 batch4、G8框架会直接报错或者静默地把 G 降级两种结果都很坑。gradient_accumulation_steps只影响梯度累积不改变组内结构。temperature也是一个关键值。GRPO 依赖组内多样性温度太低会导致 G 条采样几乎一模一样奖励相同优势全为 0温度太高又会让输出乱码、奖励全为 0。1.0 是一个合理的起点。6.4 启动与验证# 启动训练 python train_grpo.py # 如果显存吃紧先降 G 再降长度不要一上来就上 Deepspeed # 观察训练日志中的 reward 和 kl 两项判断训练是否正常看三件事。第一reward的均值应该缓慢上升即使只上升 0.1 到 0.2 也说明有学习信号。第二kl应该从接近 0 开始缓慢增长如果一开始就跳到很大说明 β 太小或者初始策略和 ref 差太多。第三reward_std如果日志里有应该保持在 0.1 以上如果长期接近 0说明组内没有分歧训练其实在空转。如果日志里只有 loss 没有 reward说明奖励函数返回的格式不对检查是不是返回了 tensor 而不是 list或者长度和 completions 对不上——这是最常见的报错来源。7. 常见问题与排查思路GRPO 的坑集中在几个固定位置我把高频问题整理成表后面几条展开说。问题现象可能原因排查方式解决方案优势全为 0loss 不动组内奖励全相同打印每个 prompt 的 G 个奖励提高 temperature、加格式分、换更难的数据报错 batch 不能被整除num_generations 与 batch 不匹配看报错里的两个数字让 per_device_batch 是 G 的整数倍KL 迅速飙升后崩溃beta 过小或奖励被 hack打印 kl 曲线和采样输出调大 beta检查奖励函数是否有漏洞输出越来越长序列级 loss 的长度偏置统计 completion 平均长度改 token 级归一化或加超长惩罚显存 OOMG 太大或序列太长看 max_memory_allocated降 G、降 max_completion_length、开梯度检查点reward 有值但 loss 为 NaNfp16 溢出或 logps 出现 -inf检查是否用了 bf16换 bf16或对 logps 做 clamp训练集 reward 涨、验证集不涨过拟合到奖励函数对比训练/验证的准确率换更多样的 prompt放宽奖励判定关于第一条我想多说两句因为它是最隐蔽的坑。假设你的数据全是简单加减法0.5B 模型冷启动时可能 8 条采样全答对奖励全是 1.0标准差为 0归一化后优势全是 0梯度为 0训 100 步和训 0 步没区别。反过来如果题目太难8 条全错奖励全是 0同样没有信号。所以 GRPO 对数据难度的分布极其敏感理想状态是每个 prompt 的通过率在 20% 到 80% 之间。这也是 DAPO 提出「动态采样」的动机把那些全对或全错的 prompt 直接丢掉不浪费算力。关于第四条长度偏置成因在 3.2 节已经说过。实践中有两种修法一是在 loss 里除以该序列的 token 数让每条序列的贡献归一二是在奖励里对超过某个长度的输出扣分。前者更优雅但会改变梯度尺度后者更粗暴但见效快。如果你的任务对长度不敏感比如数学题答案本来就是短输出也可以先不管等观察到明显变长再处理。关于第六条logps 出现 -inf 通常是因为某个 token 的概率被算成了 0。这在 fp16 下极易发生因为 softmax 后的极小概率会下溢。解决办法是统一用 bf16或者在计算 logps 时加上一个很小的 eps或者用 log_softmax 而不是先 softmax 再 log。8. 最佳实践与工程建议跑通之后真正决定训练效果的是一堆工程细节。下面这些是我认为优先级最高的几条。第一奖励函数要分层设计不要只有一个总开关。推荐拆成「正确性」「格式」「长度」三个独立函数分别加权。好处是调试时能单独看每一项的分布出了问题能快速定位。比如你发现模型答案越来越短但准确率不涨那就是长度惩罚权重给高了。第二先做离线评估再上线训练。拿 100 条数据让模型每种采样 8 次统计通过率和组内方差。如果通过率低于 10% 或高于 90%直接换数据或换模型规模不要硬训。这个步骤花 10 分钟能省掉几小时的无效训练。第三β 和 epsilon 的调节顺序是先 β 后 epsilon。β 控制的是「允不允许偏离参考模型」是策略行为的上限epsilon 控制的是「单步更新幅度」是训练稳定性的下限。如果发现模型输出开始胡说八道先调大 β如果发现 loss 抖得厉害再调小 epsilon。不要同时改两个。第四监控指标要包含组内统计量。除了 reward 均值至少还要记录 reward 的标准差、每个 prompt 的通过率分布、completion 的平均长度、以及被 clip 的样本比例。clip 比例长期高于 30% 说明学习率太大或者 epsilon 太小长期低于 5% 说明学习率太小训练效率低。第五生产环境必须做奖励函数的对抗测试。让一个已经训好的模型去攻击你的奖励函数看它能不能在答错的情况下拿到高分。规则型奖励相对安全但依然可能被绕过比如正则提取最后一个数字模型就会学会在末尾补一个随机数字来碰运气。学出来的 RM 更危险一定要有 held-out 的人工评估。第六参数规模和 G 的取舍要算清楚。G 增大能降低优势估计的方差但采样成本线性增长。经验上 G8 是性价比拐点G64 是论文级配置中间值收益递减。如果你的显存只够 G4那就把 batch 开大一点用更多 prompt 换更少的组内采样整体梯度噪声反而可能更小。第七训练和推理要保持一致。GRPO 采样的 distribution 受 temperature、top_p、stop token 影响如果训练时用 temperature1.0评估时用 greedy那策略学到的行为在推理时可能表现不出来。至少保证采样参数在训练和验证阶段对齐。9. 收尾GRPO 的边界在哪里回到最开始的问题。GRPO 把 RL 训练的门槛从「必须有四张卡跑四个模型」降到「一张卡跑三个模型甚至两个」这个变化对想做在线强化学习但资源有限的团队来说是实打实的。它也把「优势估计」这件事从「训一个网络去猜」变成了「采样一组去看」逻辑更透明出问题时更容易定位。但它不是万能的。GRPO 的适用前提是「同一个 prompt 的多次采样能被可靠地区分好坏」如果你的任务没有可自动判定的奖励或者奖励噪声极大GRPO 的优势估计会非常不稳这时候还不如老老实实做 DPO。另外它对组内方差高度敏感数据难度分布不平滑时大量算力会浪费在全对或全错的样本上这也是后续 DAPO、Dr.GRPO 等工作要补的洞。如果你要接着往下走我建议的路径是先把第 5 节的手写 loss 吃透理解每一行的梯度来源然后用第 6 节的脚本在自己的数据上跑一次重点看 reward_std 和 clip 比例这两个指标等跑稳了再去读 DAPO 和 Dr.GRPO 的改进点那时候你会明显感觉到论文里那些看起来像 trick 的改动其实都是在填你在日志里看到的那些坑。
阅读完成 · 觉得有帮助?
咨询建站