15. GRPO 详解
Group Relative Policy Optimization (GRPO) 由 DeepSeek 在 DeepSeekMath (Shao et al., 2024) 中首次提出,并在 DeepSeek-R1 (2025) 中大规模应用,成为推动推理大模型爆发的关键算法之一。它的核心创新只有一句话:用一组 (group) 采样的相对奖励替代 critic 的 baseline——一举省掉 PPO 的 value model,把 4 模型架构压成 3 个甚至 2 个,显存与稳定性同时改善。
本章串起 §13 的 PPO 数学和 §17 的 PRM/规则奖励,重点回答:为什么 group baseline 是无偏的?GRPO 与 RLOO/REINFORCE++ 的区别?以及 DeepSeek-R1 如何用 GRPO + 规则奖励训出"会思考"的模型。
15.1 起源与动机
15.1.1 PPO 的代价
回顾第 13 章,PPO 的训练流水线需要同时持有:
- Actor
; - Critic
; - Reference
; - Reward
。
其中 Critic 是最麻烦的:
- 需要专门训练(额外梯度);
- 与 Actor 同规模 → 显存翻倍;
- 在 LLM 场景下,reward 极度稀疏(一整个回答只在最后给一个分),critic 难以学好;
- Critic 不准时 GAE advantage 偏差大,训练不稳定。
15.1.2 DeepSeekMath 的洞察
Shao et al. (2024) 在做数学 RL 时观察到:
- 在数学这种结果可验证的任务上,reward 可以由规则给出(答对 = 1,答错 = 0),不需要训 RM;
- Critic 是为了估计 baseline,但 baseline 的本质只需要满足无偏——不一定要学一个 value 函数;
- 对每个 prompt 采多个样本,用它们的奖励均值作 baseline,已经无偏。
由此提出 GRPO:Group as Baseline。
15.2 GRPO 算法
15.2.1 流程
对每个 prompt
- 用旧策略
采 个回答 ; - 计算每个回答的奖励
(来自 RM 或规则); - 组内归一化 得到 advantage:
- 把
当作整条回答 所有 token 的 advantage: ; - 用 PPO-Clip 目标更新
。
15.2.2 GRPO 目标
完整目标(DeepSeekMath 论文公式):
其中
15.2.3 KL 惩罚的写法
GRPO 通常把 KL 作为显式 loss 项(与 PPO 把 KL 摊到 token reward 中不同)。这里 DeepSeek 使用的是 k3 估计器:
这个估计器满足:(i) 期望等于 KL;(ii) 始终
15.2.4 关键超参(DeepSeek-R1)
| 超参 | 值 | 备注 |
|---|---|---|
| Learning rate | 比 SFT 小 10× | |
| KL coefficient | 0.001 | 极小(依赖 ref refresh) |
| Clip | 10 (?) | 实际较松,因为 ratio 通常 ≈ 1 |
| Group size | 16 ~ 64 | R1 用 16 |
| Max sequence | 32K | 推理需要长 CoT |
| Batch | 512 prompts | × G = 8192 sequences |
| Reference refresh | 每 400 步 |
15.3 数学基石:Group Baseline 的无偏性
15.3.1 一般 baseline 性质
回到第 13 章 §13.1.4:策略梯度对 baseline 不变性:
所以任何 不依赖当前样本的 baseline 都不改变期望。
15.3.2 Group mean 是有效 baseline
考虑某个样本
因此用
15.3.3 Group mean (含自身) 也几乎无偏
GRPO 的 baseline 是
所以
这只是把 RLOO 的 advantage 缩了
结论:Group mean 与 RLOO 等价(差一个常数因子),都是合法的低方差 baseline。
15.3.4 std 归一化
GRPO 还除以 group std:
这并非"为了无偏"(除 std 会让
- 让 advantage 尺度跨 prompt 一致;
- 让 PPO 的 clip 阈值
与 reward 尺度解耦; - 减少 lr 调参敏感度。
代价:理论上引入小偏差,但实证收益远大于偏差。
15.3.5 与 GAE 的对比
| 维度 | GAE (PPO) | Group baseline (GRPO) |
|---|---|---|
| Baseline 来源 | 学习的 | 同 prompt 的多次采样 |
| 时间分辨率 | token 级 | 整条回答(共享) |
| 偏差 | 取决于 V 准度 | 几乎无偏 |
| 方差 | 取决于 group size | |
| 计算成本 | 训 critic | 多次 rollout |
| 显存 | +1 模型 | 无 |
简单结论:GRPO 用 rollout 计算换显存——把 critic 的"机器学的 baseline"换成"采样的 Monte Carlo baseline"。
15.4 GRPO vs PPO vs DPO
| 维度 | PPO | DPO | GRPO |
|---|---|---|---|
| 数据形式 | rollout (online) | pairwise (offline) | rollout (online) |
| 模型数 | 4 | 2 | 3 (R1-Zero: 2) |
| 是否需 RM | 是 | 否 | 是 / 规则 |
| 是否需 critic | 是 | 否 | 否 |
| 是否需 reference | 是 | 是 | 是 |
| Token-level signal | yes (GAE) | yes (logp 累加) | outcome only (token 共享 |
| Advantage 估计 | 隐式 | ||
| 显存 | 高 | 中 | 中(省 critic) |
| 调参复杂度 | 高 | 低 | 中 |
| 适合场景 | 通用 RLHF | 偏好对齐 | 推理任务、规则可验证 |
15.5 GRPO 在 DeepSeek-R1 中的应用
DeepSeek-R1 (2025) 把 GRPO 推到大规模推理训练,是 GRPO 最具代表性的应用案例。
15.5.1 R1-Zero:纯 RL 训出推理能力
关键设定:
- 不做 SFT cold-start,直接从 base model 开始;
- 不训 RM,奖励完全由规则给出;
- 不要中间监督,只看最终答案。
Rule-based reward:
详细:
- Accuracy reward:用 answer extraction + 数值/字符串匹配 验证(数学题)或 代码沙盒执行(编程题);
- Format reward:要求模型先输出
<think>...</think>推理过程再<answer>...</answer>给答案; - Language consistency:避免中英文混用 → 给中文/英文一致性奖励。
结果:经过几千步 GRPO 后,base 模型涌现出 chain-of-thought 推理、自我反思("wait, let me reconsider")、多路径探索等行为,AIME 准确率从 ~16% 提升到 71%。这是 RL 直接催生推理能力 的标志性证据。
15.5.2 R1:cold-start + 多阶段
R1 在 R1-Zero 基础上加 cold-start SFT + 多阶段对齐:
Stage 1: cold-start SFT
- 用人工编写的少量 CoT 数据微调 base
- 解决 R1-Zero 的可读性问题
Stage 2: GRPO 推理强化
- 在数学/代码上跑 GRPO + 规则奖励
- 类似 R1-Zero 但有 SFT 起点
Stage 3: SFT 拓展
- 用 R1 (Stage 2) 生成大量推理数据
- 加上写作、对话等通用任务数据
- 重新 SFT base 模型
Stage 4: GRPO 多任务对齐
- Hybrid reward:规则(数学/代码)+ RM(对话)
- 全任务对齐R1 的最终模型在 MATH、AIME、Codeforces、MMLU 上均达到顶尖水平,完全开源。
15.5.3 工程要点
DeepSeek-R1 的 GRPO 工程实现细节:
- vLLM rollout:每条 prompt 采 16 个回答,最长 32K token,单次 rollout 几秒;
- FSDP + ZeRO-3:actor 和 reference 都做参数分片;
- Reference refresh:每 400 步
,防止 KL 过紧; - Reward filtering:跳过所有 16 个回答都对或都错的 prompt(advantage 全 0,无信号);
- Mixed precision (BF16):rollout 与训练都用 BF16;
- Dynamic padding:按长度排序后分组打包。
15.6 GRPO 实现详解
15.6.1 完整伪代码
def grpo_iteration(prompts, π_θ, π_ref, reward_fn, β=0.001, ε=0.2,
G=16, ppo_epochs=1):
"""GRPO 一次迭代"""
# ---------- Stage 1: Group Rollout ----------
π_θ_old = copy_params(π_θ)
rollouts = []
for q in prompts:
# 采 G 个回答
outputs = π_θ_old.generate(q, num_return_sequences=G,
temperature=1.0, max_new_tokens=32768)
rewards = [reward_fn(q, o) for o in outputs] # rule-based or RM
# Group-relative advantage
r_arr = torch.tensor(rewards, dtype=torch.float)
if r_arr.std() < 1e-6:
continue # 全对或全错,跳过
r_norm = (r_arr - r_arr.mean()) / (r_arr.std() + 1e-8)
for i, o in enumerate(outputs):
# 计算 old logp(用 π_θ_old 重新前向,因为 generate 时可能用 sampling)
logp_old = compute_token_logprobs(π_θ_old, q, o) # [|o_i|]
with torch.no_grad():
logp_ref = compute_token_logprobs(π_ref, q, o)
rollouts.append({
"q": q, "o": o,
"advantage": r_norm[i].item(), # scalar,所有 token 共享
"logp_old": logp_old,
"logp_ref": logp_ref,
})
# ---------- Stage 2: GRPO Update ----------
for epoch in range(ppo_epochs):
for batch in make_minibatches(rollouts, batch_size=8):
for sample in batch:
logp_θ = compute_token_logprobs(π_θ, sample["q"], sample["o"])
# PPO-Clip
ratio = torch.exp(logp_θ - sample["logp_old"]) # [|o|]
 = sample["advantage"] # scalar,broadcast 到所有 token
surr1 = ratio * Â
surr2 = torch.clamp(ratio, 1-ε, 1+ε) * Â
L_clip = -torch.min(surr1, surr2) # [|o|]
# KL 惩罚(k3 估计器)
kl = torch.exp(sample["logp_ref"] - logp_θ) - (sample["logp_ref"] - logp_θ) - 1
# token 级 loss + 长度归一化
L = (L_clip + β * kl).mean()
L.backward()
optimizer.step()
optimizer.zero_grad()15.6.2 与 TRL 的 GRPOTrainer
Hugging Face TRL 在 v0.10+ 提供 GRPOTrainer:
from trl import GRPOTrainer, GRPOConfig
config = GRPOConfig(
output_dir="./grpo_output",
num_generations=8, # G
beta=0.01, # KL coef
learning_rate=1e-6,
per_device_train_batch_size=4,
max_prompt_length=512,
max_completion_length=2048,
gradient_accumulation_steps=4,
num_train_epochs=1,
bf16=True,
use_vllm=True, # 用 vLLM 加速 rollout
)
def reward_fn(prompts, completions, **kwargs):
"""返回每个 completion 的 reward (list of float)"""
rewards = []
for prompt, comp in zip(prompts, completions):
# 比如:检查数学答案是否正确
score = check_math_answer(prompt, comp)
rewards.append(score)
return rewards
trainer = GRPOTrainer(
model="Qwen/Qwen2-Math-7B",
reward_funcs=[reward_fn],
args=config,
train_dataset=dataset,
)
trainer.train()15.6.3 显存优化技巧
GRPO 虽然省了 critic,但 group rollout 仍带来新的显存压力:
- vLLM 解耦推理:rollout 用 vLLM(独立进程),训练用 PyTorch;
- Sequence packing:把多个回答打包成一个长序列,避免 padding 浪费;
- Gradient accumulation:减小 per-step batch;
- Weight sync:rollout 后把 actor 权重 push 到 vLLM;OpenRLHF/veRL 都有现成实现。
15.7 GRPO 的变体与改进
15.7.1 DAPO (字节, 2025)
Yu et al. "DAPO: an Open-Source LLM Reinforcement Learning System at Scale" (2025) 提出 4 个改进:
- 双 clip 阈值
: - 通常
(如 0.28 vs 0.2); - 让"有用的探索"(优势 > 0 的提升)有更大空间;
- 通常
- Dynamic Sampling:过滤掉 group reward 全平的 prompts(无梯度信号);
- Token-level loss aggregation:用 token 数加权平均,而非 sequence 平均(更公平地分配信号);
- 移除 KL 惩罚:直接让 ref refresh 替代 KL,简化损失。
15.7.2 Dr.GRPO (Liu et al., 2025)
发现 GRPO 中的两个偏差:
- 长度偏差:除以
让短回答 advantage 被放大; - std 归一化偏差:std 估计本身有偏,特别是
小时。
Dr.GRPO 的修正:
- 不除 sequence 长度:直接 token 级求和;
- 用无偏 std:bessel correction(除以
)。
15.7.3 REINFORCE++
把 GRPO 的 group baseline 推广为 global baseline:
- 用整个 batch 内所有 prompts 的奖励均值作 baseline;
- 实现极简,但方差略大;
- OpenRLHF 默认支持。
15.7.4 VinePPO / VinePPG
把 GRPO 的 outcome-level advantage 升级到 token-level:
- 在每个 token 处采
个 continuations; - 用 continuation 的 reward 均值估计 token-level value;
- 接近 critic 但不需要训练。
代价:rollout 量爆炸。
15.7.5 K3-GRPO / K1-GRPO 等 KL 估计器
不同 KL 估计器对训练稳定性影响:
- k1:
—— 简单但方差大; - k2:
—— 总是 ≥ 0; - k3 (推荐):
—— 总是 ≥ 0 且无偏。
15.8 GRPO + Process Reward Model (PRM)
15.8.1 ORM vs PRM 在 GRPO 中
GRPO 默认用 outcome-level reward(一整条回答一个分)。但在数学推理中,过程奖励 信号更密集。
PRM 给每一步打分:
GRPO + PRM 流程:
- 同样采
个回答; - 用 PRM 给每个 step 打分;
- token-level advantage = step-level reward 减 group baseline;
- 同样 PPO-Clip 更新。
DeepSeekMath 论文同时探索了 outcome / process / iterative 三种 GRPO 变体。
15.8.2 Process baseline
PRM-GRPO 中的 group baseline 也可以 step-level:
但需要不同回答在 step
15.9 GRPO 的成功要素
为什么 GRPO 在推理任务上特别成功?
- 规则可验证 reward:数学/代码任务有客观对错,避免 RM 的 imperfection;
- Group 让 reward 信号有对比性:即使 reward 是 0/1,组内也能区分"全对"vs"部分对";
- Outcome 监督足够:CoT 推理的"思考过程"由模型自主探索,无需 step-level 监督;
- 省 critic 让训练稳定:critic 在稀疏 reward 下尤其难训;
- Reference refresh 替代 KL 紧约束,给模型更大优化空间。
但 GRPO 也有局限:
- 在主观任务(创意写作、对话)上,规则奖励难以定义;
必须够大(≥ 8)才有有效 baseline; - 长 rollout 计算开销大;
- 仍需要好的 reward 设计(reward design 本身是难题)。
15.10 完整代码框架
简化版 GRPO 训练循环(详见 code/08_grpo_training.py):
import torch
import torch.nn.functional as F
import copy
from transformers import AutoModelForCausalLM, AutoTokenizer
def compute_token_logprobs(model, input_ids, attention_mask, response_mask):
"""返回每个 response token 的 log p(y_t | x, y_{<t})"""
out = model(input_ids=input_ids, attention_mask=attention_mask)
logits = out.logits[:, :-1, :]
targets = input_ids[:, 1:]
log_probs = F.log_softmax(logits, dim=-1).gather(2, targets.unsqueeze(-1)).squeeze(-1)
return log_probs * response_mask[:, 1:] # mask 掉 prompt 部分
def grpo_step(model, ref_model, batch, β=0.001, ε=0.2):
"""
batch:
input_ids: [B, T] prompt + response 拼接,每条样本来自 G 个回答
attention_mask: [B, T]
response_mask: [B, T] 只在 response token 上为 1
advantages: [B] group 归一化后的标量
old_logprobs: [B, T-1]
"""
response_mask = batch["response_mask"] # [B, T]
new_logp = compute_token_logprobs(model, batch["input_ids"],
batch["attention_mask"], response_mask)
with torch.no_grad():
ref_logp = compute_token_logprobs(ref_model, batch["input_ids"],
batch["attention_mask"], response_mask)
old_logp = batch["old_logprobs"]
 = batch["advantages"].unsqueeze(-1) # [B, 1] 广播到所有 token
# PPO-Clip on token level
log_ratio = new_logp - old_logp
ratio = torch.exp(log_ratio)
surr1 = ratio * Â
surr2 = torch.clamp(ratio, 1-ε, 1+ε) * Â
pg_loss = -torch.min(surr1, surr2) # [B, T-1]
# KL k3 estimator
kl = torch.exp(ref_logp - new_logp) - (ref_logp - new_logp) - 1.0
# 把 mask 应用到 token 级 loss 上
mask = response_mask[:, 1:].float()
n_tokens = mask.sum() + 1e-8
loss = ((pg_loss + β * kl) * mask).sum() / n_tokens
return loss, {
"pg_loss": (pg_loss * mask).sum().item() / n_tokens.item(),
"kl": (kl * mask).sum().item() / n_tokens.item(),
"ratio_mean": ratio.mean().item(),
"advantage_mean": Â.mean().item(),
}
def collect_rollouts(model, ref_model, prompts, reward_fn, G=16, gen_kwargs=None):
"""对每个 prompt 采 G 个样本,计算 group-normalized advantage"""
rollouts = []
for q in prompts:
outputs = model.generate(q, num_return_sequences=G, **gen_kwargs)
rewards = torch.tensor([reward_fn(q, o) for o in outputs], dtype=torch.float)
if rewards.std() < 1e-6:
continue # skip uninformative groups
adv = (rewards - rewards.mean()) / (rewards.std() + 1e-8)
for i, o in enumerate(outputs):
rollouts.append({
"q": q, "o": o, "advantage": adv[i].item(),
})
return rollouts
def train_grpo(model, ref_model, prompts, reward_fn, optimizer,
num_iters=1000, G=16, ppo_epochs=1):
for it in range(num_iters):
# ---------- Rollout ----------
rollouts = collect_rollouts(model, ref_model, prompts, reward_fn, G=G)
if not rollouts:
continue
# 计算 old_logprobs(用当前 model = π_θ_old)
for sample in rollouts:
ids, mask, resp_mask = build_inputs(sample["q"], sample["o"])
with torch.no_grad():
sample["old_logprobs"] = compute_token_logprobs(
model, ids, mask, resp_mask)
# ---------- Update ----------
for epoch in range(ppo_epochs):
for batch in make_minibatches(rollouts, batch_size=8):
loss, metrics = grpo_step(model, ref_model, batch)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
# ---------- Periodic reference refresh ----------
if (it + 1) % 400 == 0:
ref_model.load_state_dict(model.state_dict())
if it % 10 == 0:
print(f"iter {it}: {metrics}")完整版需要:
- vLLM 集成;
- FSDP/ZeRO-3;
- Weight sync between rollout/train workers;
- Logging + checkpointing。
15.11 调试清单
GRPO 训练常见问题:
| 现象 | 可能原因 | 排查 |
|---|---|---|
| Loss 不动,reward 不涨 | Group 内方差太小(reward 全 0 或全 1) | 加 dynamic sampling,过滤无信号 group |
| Reward 涨但生成质量退化 | 规则奖励被 hacked | 检查模型输出格式,加更多规则 |
| KL 飙升 | 调大 | |
| 输出截断(max_len 满) | reward 长度偏好 | 加长度惩罚或硬截断 reward |
| Rollout 极慢 | 没用 vLLM | 切换 vLLM/SGLang |
| Group 间 reward 极不平衡 | Prompt 难度差异大 | 按难度分桶,分别采 group |
本章小结
- 核心创新:用 group 内多采样的 reward 均值/标准差替代 critic 的 baseline,省一个模型;
- 数学基础:Group mean 与 RLOO 等价(差常数因子),都是无偏 baseline;std 归一化是方差缩放,引入小偏差但稳定性巨大;
- 算法:与 PPO 共享 clip 与 KL 框架,只换 advantage 估计;
- DeepSeek-R1 的成功要素:规则可验证 reward + GRPO + reference refresh,催生 CoT 推理涌现;
- 变体:DAPO(dynamic sampling、双 clip)、Dr.GRPO(无偏修正)、REINFORCE++(global baseline)、VinePPO(token-level);
- 适用场景:推理、代码、数学等结果可验证的任务最优;主观任务仍需 RM。
思考题
推导验证:证明 group baseline
在策略梯度中的偏差为 0。提示:考虑 与 之间的相关性;当对一个特定 求 时, 部分如何处理? 为什么 GRPO 适合推理但不适合对话? 从 reward 信号密度、group baseline 有效性、以及"生成多样性"角度分析。如果你要把 GRPO 用到对话任务上,会做哪些改造?
工程题:在 R1 训练中,DeepSeek 使用 reference refresh(每 400 步
)。这等价于"每 400 步把 trust region 重新锚定到当前点"。请分析:(a) 如果不做 ref refresh,单纯减小 行不行?(b) 如果做 refresh,KL 是否可能"周期性飙升"?(c) refresh 频率与 lr 之间应当如何匹配?