
在 LLM 对齐领域的强化学习实践里PPO 长期以来是默认选项。但真正把 RLHF 跑起来的人会发现PPO 是一个相当“重”的方案需要额外训练一个 Critic 价值网络需要为价值损失维护一堆超参稍不注意训练就会抖动甚至发散。后来 DeepSeek 团队在 DeepSeekMath 等工作中公开了 GRPOGroup Relative Policy Optimization组相对策略优化把 RLHF 的复杂度大幅降了下来。本文是 H17 系列的一篇独立完整教程围绕 GRPO 的算法原理、数学推导、代码实现和工程坑点展开适合刚接触强化学习的同学也适合正在做大模型对齐工程的开发者参考。1. 背景与核心概念1.1 RLHF 的基本流程RLHFReinforcement Learning from Human Feedback基于人类反馈的强化学习是让大语言模型对齐人类偏好的核心手段。常规流程分三阶段先做监督微调SFT让模型学会任务形式再训练奖励模型Reward Model用来近似人类对回答质量的打分最后通过强化学习算法优化策略让模型在给定 Prompt 时生成的回答获得更高奖励。在第三阶段PPO 是早期最常用的算法。PPO 的流程是对当前策略采样一批回答奖励模型打分后用 Critic 网络估计状态价值计算 Advantage优势再按照近端更新规则对策略做多次梯度更新。PPO 的工程稳定性是有保证的它在 2017 年被 OpenAI 提出后迅速成为深度强化学习领域最通用的策略优化基线。1.2 从 PPO 到 GRPO为什么要去掉 CriticPPO 的缺点也很明显。第一Critic 是一个与策略同规模的价值网络。对 7B、70B 甚至更大规模的语言模型而言这个网络会带来几乎翻倍的显存开销与训练耗时。第二价值网络与策略网络共享输入特征但收敛速度往往不同步。如果你观察过 PPO 训练曲线会发现价值损失忽高忽低Advantage 估计噪声很大最终表现为策略梯度信号不稳定。第三PPO 的 GAEGeneralized Advantage Estimation需要大量超参如 lambda、gamma一旦设错训练就容易陷入 reward 不涨或 KL 爆炸的困境。GRPO 则从设计层面规避了这些问题。它不再训练 Critic而是对同一个 Prompt 采样出 G 个回答组成一个 group在该 group 内部对奖励做标准化直接用标准化后的相对分数作为 Advantage。这个过程既不需要价值网络也不需要 GAE 的超参理论上更省显存也更容易稳定。1.3 GRPO 的核心概念与适用范围GRPO 的核心概念有三个组内相对优势Group Relative Advantage、近端更新Clipping、组内标准化优势Normalized Group Advantage。组内相对优势解决的是“奖励绝对值不可比”的问题近端更新解决的是策略更新步长过大导致崩溃的问题标准化优势解决的是不同 Prompt 之间奖励尺度差异的问题。适用场景上GRPO 目前最成熟的应用是大语言模型的 RLHF 训练例如 DeepSeekMath、DeepSeek-R1 系列。强化学习的应用范围远不止大模型还包括机器人控制、机械臂操作、交通信号灯控制、游戏博弈与离线强化学习等场景但 GRPO 所代表的“组内比较替代价值函数”思路在语言模型这类随机性采样任务里收益最明显。本文后续内容聚焦于 LLM 对齐场景不扩展到离散控制。2. GRPO 核心思想与设计思路2.1 基于同一个 Prompt 的组采样GRPO 的第一步是对每个 Prompt 采样多个回答。假设一个训练 batch 里有 B 个 Prompt对每个 Prompt q 从当前旧策略 ( \pi_{\theta_{old}} ) 中采样 G 个完成序列 ( o_1, o_2, ..., o_G )那么整个 batch 的偏好样本数是 ( B \times G )。这组回答之间天然共享同一个问题上下文因此它们之间的差异可以更纯粹地反映“模型某次生成得好不好”而不受 Prompt 本身难度不同的干扰。采样阶段使用的策略是旧策略。旧策略是当前正在被优化的策略的一个“冻结快照”通常在更新之前保存下来。采样后计算损失时需要对比当前策略与旧策略在同样 token 上的对数概率得到概率比 ratio。这个 ratio 正是近端更新能否生效的关键。2.2 组内相对优势替代绝对值优势PPO 中优势的定义是“当前状态动作相对平均水平的超出程度”。在 LLM 场景中如果我们直接用奖励绝对值来更新策略会遇到一个很实际的问题不同 Prompt 之间的奖励尺度差异可能很大。有些问题本来就简单模型随便生成也能得到高分有些问题很难所有采样都只能拿到很低的分数。如果直接比较绝对值模型会被简单问题牵走难问题上则更新不足。GRPO 的解决办法是把同一组内的 G 个奖励拿出来做标准化。计算方式为[ \hat{A}_i \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})} ]这里 ( r_i ) 是第 i 个回答的奖励( \text{mean}(\mathbf{r}) ) 和 ( \text{std}(\mathbf{r}) ) 是同一组内奖励的均值和标准差。这个标准化操作消除了 Prompt 难度带来的偏差让优势变成一个相对量只表达“这个回答是否明显好于或坏于当前策略在该 Prompt 下的平均水平”。在实现里为了防止标准差不小心为 0通常会给分母加一个极小值 epsilon。这个细节看起来不起眼但很多新手在复现时都会忽略导致出现除零或 NaN。2.3 近端更新在 GRPO 中的体现近端更新Proximal Update最早出现在 PPO 中目的是避免策略一步更新得过猛。直观理解是如果概率比 ( \frac{\pi_{\theta}}{\pi_{\theta_{old}}} ) 明显大于 1说明新策略在当前 token 上的概率比旧策略大了很多这一步更新很可能已经走出安全区域。PPO 的做法是对 ratio 做 clip不让它在梯度方向上提供过大的推动力。GRPO 保留了这一机制。每个 token 上的概率比定义为[ \rho_{t,i} \frac{\pi_{\theta}(o_{i,t} \mid q, o_{i,t})}{\pi_{\theta_{old}}(o_{i,t} \mid q, o_{i,t})} ]然后考虑 clip[ \text{clip}(\rho_{t,i}, 1-\epsilon, 1\epsilon) ]最后在策略损失项取未 clip 与 clip 后的较小值[ \min\left( \rho_{t,i} \hat{A}i, ; \text{clip}(\rho{t,i}, 1-\epsilon, 1\epsilon) \hat{A}_i \right) ]近端更新带来的直接好处是训练曲线更平稳不容易在某个 batch 中出现 logprob 突变。GRPO 在 clip 的幅度上通常沿用 PPO 的 0.2 作为默认值但实际项目中也可以根据任务的奖励噪声水平调小或调大。2.4 GRPO 与 PPO 的对比为了方便理解这里把 GRPO 和 PPO 的主要差异整理成一张表对比维度PPOGRPO价值网络需要训练 Critic不需要 Critic优势估计方式GAE 价值网络组内奖励标准化显存占用约两倍策略模型接近单倍策略模型超参数量gamma、lambda、vf coef 等少量主要在 KL 与采样更新稳定性依赖 Critic 收敛质量依赖组内采样多样性典型应用通用 RLHF、机器人控制大语言模型 RLHF需要说明的是这并不意味着 GRPO 全面优于 PPO。在价值网络容易训练的传统强化学习环境中PPO 仍然是很强的选择。GRPO 的适用条件是“能低成本地多次采样并计算奖励”本质上是把对 Critic 的依赖转移成了对采样数量的依赖。3. 数学原理与公式推导3.1 从策略梯度到 PPO 目标强化学习的经典策略梯度形式可以写成[ \nabla_\theta J(\theta) \mathbb{E}{\tau \sim \pi\theta} \left[ \sum_{t1}^T \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot A_t \right] ]其中 ( A_t ) 是优势函数。策略梯度最大的问题是方差大稍有不慎更新就会偏离。PPO 引入概率比[ \rho_t(\theta) \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{old}}(a_t \mid s_t)} ]并将目标函数设计为[ L^{CLIP}(\theta) \mathbb{E} \left[ \min\left( \rho_t(\theta) A_t, ; \text{clip}(\rho_t(\theta), 1-\epsilon, 1\epsilon) A_t \right) \right] ]当优势为正时模型被鼓励在该 token 上提高概率但提高幅度被 clip 限制当优势为负时模型被要求降低概率但同样会限制更新幅度。这个目标函数就是 GRPO 策略项的直接来源。3.2 组内相对优势公式GRPO 不再需要价值网络而是直接对一组采样做标准化。假设对 Prompt q 采样了 G 个完成序列 ( o_1, ..., o_G )奖励为 ( r_1, ..., r_G )则第 i 个序列的优势为[ \hat{A}i \frac{r_i - \frac{1}{G}\sum{j1}^G r_j}{\sqrt{\frac{1}{G}\sum_{j1}^G \left(r_j - \frac{1}{G}\sum_{k1}^G r_k\right)^2 c}} ]c 是防止除零的常数。注意这个优势是一个标量对一个序列中的所有 token 共享。这一点与 PPO 的逐时间步优势不同也是 GRPO 在符号表示上更简洁的原因。标准化本身不会改变梯度方向它改变的是梯度的尺度。如果奖励方差大则优势会被压缩如果奖励方差小优势会被放大。这种动态缩放类似于自适应学习率的思路能够减少跨 Prompt 的奖励尺度差异带来的训练抖动。3.3 GRPO 目标函数的完整形式GRPO 的完整目标函数可以写成[ \begin{aligned} J_{GRPO}(\theta) \mathbb{E}{q \sim P(Q), {o_i}{i1}^G \sim \pi_{\theta_{old}}(O \mid q)} \Bigg[ \frac{1}{G} \sum_{i1}^G \frac{1}{|o_i|} \sum_{t1}^{|o_i|} \ \min\left( \rho_{i,t}(\theta) \hat{A}i, \text{clip}\left(\rho{i,t}(\theta), 1-\epsilon, 1\epsilon\right) \hat{A}_i \right)\beta , \mathbb{D}{KL}\left[ \pi\theta | \pi_{ref} \right] \Bigg] \end{aligned} ]第一项是组内近端策略目标第二项是参考模型 KL 惩罚。( \pi_{ref} ) 是训练初期保存的参考模型通常是经过 SFT 的模型。KL 惩罚保证优化后的模型不会偏离初始能力太远避免奖励伪造Reward Hacking或语言质量退化。在实现时逐 token 的概率比计算需要与语言模型的对数概率输出对应。对于生成的完成序列按每个 token 计算 ( \log \pi_\theta ) 与 ( \log \pi_{\theta_{old}} )两者相减再取指数得到 ratio。对于 Prompt 部分一般不计算损失因为 RL 只关心生成动作而不是输入前缀。3.4 KL 散度项的工程化处理KL 散度在不同框架中的实现略有差异但核心思路一致。常见的一种无偏估计形式是[ \mathbb{D}{KL}\left[ \pi\theta | \pi_{ref} \right] \approx \exp\left( \log \pi_{ref} - \log \pi_\theta \right)\left( \log \pi_{ref} - \log \pi_\theta \right)1 ]这个估计量本身是在策略 ( \pi_\theta ) 的采样上计算的因此可以写成 PyTorch 代码log_ratio ref_log_probs - log_probs kl torch.exp(log_ratio) - log_ratio - 1需要说明的是KL 项通常只在生成 token 上累积并对序列长度做归一化。部分实现也会在 KL 上施加 stop gradient 操作不对参考模型求梯度因为参考模型是冻结的梯度只应回传到当前策略。4. 算法流程与核心代码4.1 训练主流程拆解GRPO 的单步训练可以拆成六个步骤从数据集中采样一批 Prompt。用当前旧策略对每个 Prompt 采样 G 个完成序列。用奖励模型或规则奖励函数对每个序列打分。按组对奖励做标准化得到每个序列的标量优势。计算当前策略在每个 token 上的概率比施加 clip 与优势乘法得到策略损失。加入参考模型 KL 惩罚做反向传播并更新策略参数。采样、奖励计算、损失计算的循环往复构成了 GRPO 训练过程。理解这个流程最重要的一点是旧策略是采样时使用的版本更新时使用的概率比必须与当前策略重新计算两者不能混淆。4.2 手写 GRPO Loss 核心代码下面给出一个简化版 GRPO Loss 实现目的是帮助理解核心计算逻辑。实际工程中推荐直接使用 TRL 等成熟库。import torch def grpo_loss( log_probs, # [group_size, seq_len] 当前策略的对数概率 old_log_probs, # [group_size, seq_len] 采样时旧策略的对数概率 ref_log_probs, # [group_size, seq_len] 参考模型的对数概率 rewards, # [group_size] 奖励模型打分 epsilon0.2, # clip 范围 beta0.04, # KL 惩罚系数 eps1e-6, # 防止除零 ): # 1. 组内相对优势 mean_reward rewards.mean(dim-1, keepdimTrue) std_reward rewards.std(dim-1, keepdimTrue) eps advantages (rewards - mean_reward) / std_reward # [group_size] # 2. 概率比 ratio torch.exp(log_probs - old_log_probs) # [group_size, seq_len] # 3. 近端更新 clipped_ratio torch.clamp(ratio, 1 - epsilon, 1 epsilon) # 4. 将标量优势扩展为每个 token 共享 adv advantages.unsqueeze(-1) # [group_size, 1] # 5. 策略损失最小化负目标 policy_loss -torch.min( ratio * adv, clipped_ratio * adv ) # 6. KL 散度估计 log_ratio ref_log_probs - log_probs kl torch.exp(log_ratio) - log_ratio - 1 # 7. 合并并平均 loss (policy_loss beta * kl).mean() return loss这段代码省略了 mask。实际训练中Prompt 部分的 token 不应参与损失计算因此需要按样本的 completion 长度构造 mask将 Prompt 位置置 0。另外advantages是逐序列共享的标量与 token 位置无关这也是 GRPO 的一大特征。4.3 计算图与梯度说明优化器更新时核心梯度来自两股信号。第一股是策略梯度它由优势 ( \hat{A}_i ) 加权决定模型让哪些 token 概率上升、哪些 token 概率下降。第二股是 KL 正则梯度它在策略开始偏离参考模型时产生一个“拉回”的力。由于 KL 项通常只影响当前策略不通过参考模型传播所以参考模型可以保持冻结。一个常见的误区是试图在损失中混合 value loss。GRPO 不需要 value loss因此 PyTorch 的计算图只包含 log_probs、old_log_probs、ref_log_probs 和 rewards 几个张量。如果想要验证计算图是否正确可以在训练脚本中打印loss.requires_grad并确认loss.grad_fn非空。5. 实战案例使用 TRL 库训练一个最小 GRPO 任务5.1 环境准备与依赖安装建议使用 Python 3.10 或更高版本PyTorch 2.1 以上。以下命令适合在 Linux 或 Windows 环境下安装核心依赖。pip install torch2.1 transformers4.40 datasets trl0.10 accelerate bitsandbytes如果当前 TRL 版本较旧需要先升级 TRL因为 GRPOTrainer 是在较新的版本中加入的。环境配置时如果 GPU 显存有限可以优先用 0.5B 左右的模型做代码验证训练步数只设几十步主要验证流程能跑通。5.2 构建 Prompt 数据集与奖励函数为了让 GRPO 的训练效果肉眼可见这里设计一个非常简单的数学数字任务Prompt 要求模型输出一个 0 到 9 的整数奖励函数检查输出是否合法。这个任务不需要外部奖励模型也不需要加载大规模数据集适合本地调试。from datasets import Dataset prompts [ 请直接输出一个 0 到 9 之间的整数不要解释。, 只输出一个 0 到 9 的整数。, 你的回答必须只有一个数字范围是 0 到 9。, 请输出一个数字数字必须在 0 到 9 之间。, 请用单个数字回答范围 0 到 9。, ] dataset Dataset.from_dict({prompt: prompts})奖励函数可以写成非常简单的规则def reward_func(completions, **kwargs): rewards [] for completion in completions: text completion.strip() if len(text) 1 and text.isdigit() and 0 int(text) 9: rewards.append(1.0) else: rewards.append(0.0) return rewards这里completions是模型生成的文本列表具体格式可能会因 TRL 版本不同而有细微差异。建议在训练脚本里先写一个调试函数打印completions的前几个结果确认格式后再写正式奖励逻辑。5.3 GRPOConfig 配置解读TRL 中的 GRPOConfig 负责训练超参与采样参数。下面对几个关键参数做说明learning_rate策略更新学习率通常比 SFT 更小推荐 5e-6 到 1e-5。betaKL 惩罚系数控制新策略与参考模型的距离默认 0.04 左右。num_generations每个 Prompt 的采样数量GRPO 的组大小一般取 4 到 16。max_prompt_lengthPrompt 部分最大 token 数。max_completion_length生成部分最大 token 数。temperature采样温度控制生成多样性语言模型场景常用 0.7 到 1.0。per_device_train_batch_size每次每卡处理的 Prompts 数不是总生成数量。需要注意的是真实的显存开销约等于num_generations与 batch 大小的乘积。如果显存不足优先减小num_generations再考虑减小max_completion_length。5.4 完整训练脚本下面给出一个可直接运行的 TRL 训练脚本。模型以 Qwen2.5-0.5B-Instruct 为例如果你的环境无法访问该模型可以替换成本地已有的任意小模型。# train_grpo.py from datasets import Dataset from transformers import AutoTokenizer from trl import GRPOTrainer, GRPOConfig # 1. 构造简单数据集 prompts [ 请直接输出一个 0 到 9 之间的整数不要解释。, 只输出一个 0 到 9 的整数。, 你的回答必须只有一个数字范围是 0 到 9。, 请输出一个数字数字必须在 0 到 9 之间。, 请用单个数字回答范围 0 到 9。, ] dataset Dataset.from_dict({prompt: prompts}) # 2. 奖励函数 def reward_func(completions, **kwargs): rewards [] for completion in completions: text completion.strip() if len(text) 1 and text.isdigit() and 0 int(text) 9: rewards.append(1.0) else: rewards.append(0.0) return rewards # 3. 加载 tokenizer model_name Qwen/Qwen2.5-0.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token # 4. 配置 GRPO config GRPOConfig( output_dir./grpo_output, learning_rate5e-6, beta0.04, max_prompt_length256, max_completion_length32, num_generations8, temperature0.8, per_device_train_batch_size2, gradient_accumulation_steps4, num_train_epochs1, logging_steps10, save_strategyno, bf16True, ) # 5. 创建 Trainer trainer GRPOTrainer( modelmodel_name, argsconfig, processing_classtokenizer, train_datasetdataset, reward_funcsreward_func, ) # 6. 开始训练 trainer.train()如果你的 TRL 版本初始化参数名不是processing_class而是tokenizer可以将上述代码中的processing_classtokenizer改为tokenizertokenizer。由于不同版本 API 变动建议先查看本机 TRL 的GRPOTrainer签名。5.5 运行与验证在终端中运行python train_grpo.py如果一切配置正确你会看到类似loss、reward、kl等日志每隔若干个 step 打印一次。训练结束后可以通过加载 checkpoint 或直接让训练后的模型生成文本来观察奖励变化。想要验证 GRPO 是否真的有效可以做一个简单的对比实验先让基线模型直接生成 Prompt 对应的回答统计合法输出比例再让 GRPO 微调后的模型生成同样 Prompt 的回答。一般情况下微调后的合法输出比例会明显上升。这个试验耗时很短适合作为 GRPO 入门验证。6. 常见问题与排查思路6.1 高频问题速查表问题现象常见原因解决思路训练刚开始 KL 就剧烈上升基座模型与参考模型差异过大或学习率太高降低学习率增大 beta检查策略初始化权重奖励不涨group size 太小奖励函数区分度低增大 num_generations设计更平滑的奖励模型输出重复文本采样温度太低多样性不足提高 temperature 到 0.8 以上显存不足 OOMnum_generations 与 batch 同时偏大减小 num_generations 或 max_completion_lengthloss 出现 NaN概率比极端值或 KL 估计溢出使用混合精度检查奖励是否存在异常值组内 std 为 0同一组所有回答奖励相同在标准化时给 std 加极小值并检查奖励函数训练后模型只会输出固定文本奖励被过度优化策略坍塌增大 KL 惩罚增加拒绝采样或多样性约束6.2 典型案例排查示例假设你看到 loss 刚开始是 0.5随后迅速涨到 3 以上且 KL 指标一直升高。这种问题通常发生在参考模型与策略初始化不一致时。解决办法是确认策略模型的 checkpoint 与参考模型是否来自同一个 SFT 结果。GRPO 假设策略初始化接近参考模型如果两者差距过大KL 项会给出非常大的惩罚。另一类高频问题是奖励函数返回了错误格式。TRL 中奖励函数返回的是一个 list长度需要和completions保持一致。如果列表长度不匹配训练会在非常早期就报错。遇到这种情况建议在奖励函数里打印completions的样本结构再逐步调整文本解析逻辑。7. 最佳实践与工程建议7.1 奖励函数设计GRPO 对奖励函数非常敏感。如果奖励区分度太低例如大多数样本都是 0 分或 1 分组内标准化的优势会变成正负号几乎随机训练效果会非常差。推荐的做法是设计平滑的连续奖励而不是只有 0/1 的稀疏奖励。以数学题为例可以根据答案向正确结果靠近的程度给分这样优势估计的方差更小。另外如果业务流程涉及用户内容或生产环境奖励函数本身必须经过合规审查不能包含带有偏见、歧视或不安全倾向的规则。7.2 采样组大小与温度组大小 G 是 GRPO 最核心的超参。G 太小组内均值和标准差不可靠优势估计噪声大G 太大采样成本高显存压力大。实际项目中G 取 8 到 16 是常见选择。温度参数直接影响组内多样性温度接近 0 时G 个回答高度相似优势没有区分度温度过高又会生成大量无意义文本。建议控制在 0.7 到 1.0 的范围内并根据任务类型调整。7.3 KL 正则与训练稳定性KL 惩罚系数 beta 的作用是限制策略与参考模型之间的距离。beta 过大模型可能不愿意尝试新行为奖励提升变慢beta 过小模型容易在几个 step 内完全偏离参考模型出现语言质量下降甚至复读。建议在训练过程中监控 KL 指标的中位数而非平均值因为平均值容易被个别异常样本拉高。如果发现 KL 持续上升即便奖励在涨也要及时停止训练并分析采样质量。7.4 训练资源与监控GRPO 虽然省去了 Critic但多路采样依然需要较大显存。工程上建议先用 0.5B 或 1B 模型跑通全流程再切换到更大模型。训练过程中要重点记录三类指标策略损失、平均奖励与 KL。三个指标同时观察才能定位问题。奖励正常但 KL 偏高说明策略正在冒险奖励不涨但 KL 稳定则可能需要调整奖励函数或增大采样组。生产环境推进时建议先在低资源沙箱环境验证整体流程保留每轮模型 checkpoint才能在训练失控时快速回滚。8. 总结与下一步学习路线本文从 RLHF 的经典流程出发讲解了 GRPO 为什么能替代 PPO组内采样、组内奖励标准化和近端更新三者结合让优势估计不再依赖价值网络。完整的数学目标函数包括两个核心部分带 clip 的组内近端策略损失以及朝向参考模型的 KL 惩罚。相比 PPOGRPO 的代码更简洁、显存占用更低、超参更少也因此成为当前大模型强化学习训练的重要方案。想继续深入学习的读者建议依次阅读三份材料DeepSeekMath 论文中 GRPO 章节TRL 源码中 GRPOTrainer 的实现细节以及经典 PPO 原文。读完代码后再回头看公式会发现许多工程实现上的选择例如标准化时对 std 加常数、KL 的估计形式、mask 处理方式都有明确的工程动机。动手层面可以先用 0.5B 模型跑一个随机初始化策略的权重初始化对比实验逐步调整组大小和 beta观察奖励与 KL 的变化趋势。如果后续训练中出现具体报错可以按 6.1 节的排查表逐项定位。