Skip to content

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 Vψ
  • Reference πref
  • Reward rϕ

其中 Critic 是最麻烦的:

  1. 需要专门训练(额外梯度);
  2. 与 Actor 同规模 → 显存翻倍;
  3. 在 LLM 场景下,reward 极度稀疏(一整个回答只在最后给一个分),critic 难以学好;
  4. 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 q

  1. 用旧策略 πθoldG 个回答 {o1,o2,,oG}
  2. 计算每个回答的奖励 {r1,r2,,rG}(来自 RM 或规则);
  3. 组内归一化 得到 advantage:
r~i=rimean({r1,,rG})std({r1,,rG})+ϵ
  1. r~i 当作整条回答 oi 所有 token 的 advantage:A^i,t=r~i
  2. 用 PPO-Clip 目标更新 πθ

15.2.2 GRPO 目标

完整目标(DeepSeekMath 论文公式):

JGRPO(θ)=EqD,{oi}i=1Gπθold(|q)1Gi=1G1|oi|t=1|oi|{min[πθ(oi,t|q,oi,<t)πθold(oi,t|q,oi,<t)A^i,t,clip(πθ(oi,t|q,oi,<t)πθold(oi,t|q,oi,<t),1ε,1+ε)A^i,t]βKL(πθ(|q,oi,<t)πref(|q,oi,<t))}

其中 A^i,t=r~i(组归一化后的奖励)。

15.2.3 KL 惩罚的写法

GRPO 通常把 KL 作为显式 loss 项(与 PPO 把 KL 摊到 token reward 中不同)。这里 DeepSeek 使用的是 k3 估计器

D^KL(πθπref)πref(ot|st)πθ(ot|st)logπref(ot|st)πθ(ot|st)1

这个估计器满足:(i) 期望等于 KL;(ii) 始终 0(普通 log 比率估计可能负值)。

15.2.4 关键超参(DeepSeek-R1)

超参备注
Learning rate3×106比 SFT 小 10×
KL coefficient β0.001极小(依赖 ref refresh)
Clip ε10 (?)实际较松,因为 ratio 通常 ≈ 1
Group size G16 ~ 64R1 用 16
Max sequence32K推理需要长 CoT
Batch512 prompts× G = 8192 sequences
Reference refresh每 400 步πrefπθ

15.3 数学基石:Group Baseline 的无偏性

15.3.1 一般 baseline 性质

回到第 13 章 §13.1.4:策略梯度对 baseline 不变性:

Eaπ[logπ(a)b]=bEa[logπ(a)]=0(若 b 与 a 无关)

所以任何 不依赖当前样本的 baseline 都不改变期望。

15.3.2 Group mean 是有效 baseline

考虑某个样本 i,它的"对照"是同 prompt 下的其他 G1 个样本:

r¯i=1G1jirj

r¯ioi 独立(因为是不同样本),所以代入策略梯度:

E[(r¯i)logπ(oi)]=E[r¯i]E[logπ(oi)]=0

因此用 rir¯i 代替 ri 不改变期望,但减小方差。这正是 RLOO (Leave-One-Out) baseline。

15.3.3 Group mean (含自身) 也几乎无偏

GRPO 的 baseline 是 r¯=1Gjrj包含 ri 自己

r¯=ri+(G1)r¯iG

所以 rir¯=ririGG1Gr¯i=G1G(rir¯i)

这只是把 RLOO 的 advantage 缩了 G1G 倍,不改变方向,且当 G 大时缩放可忽略。

结论:Group mean 与 RLOO 等价(差一个常数因子),都是合法的低方差 baseline。

15.3.4 std 归一化

GRPO 还除以 group std:

r~i=rir¯σr+ϵ

这并非"为了无偏"(除 std 会让 J 不再严格等于原期望),而是 方差缩放。好处:

  • 让 advantage 尺度跨 prompt 一致;
  • 让 PPO 的 clip 阈值 ε 与 reward 尺度解耦;
  • 减少 lr 调参敏感度。

代价:理论上引入小偏差,但实证收益远大于偏差。

15.3.5 与 GAE 的对比

维度GAE (PPO)Group baseline (GRPO)
Baseline 来源学习的 Vψ(s)同 prompt 的多次采样
时间分辨率token 级整条回答(共享)
偏差取决于 V 准度几乎无偏
方差λ 调节取决于 group size G
计算成本训 critic多次 rollout
显存+1 模型

简单结论:GRPO 用 rollout 计算换显存——把 critic 的"机器学的 baseline"换成"采样的 Monte Carlo baseline"。


15.4 GRPO vs PPO vs DPO

维度PPODPOGRPO
数据形式rollout (online)pairwise (offline)rollout (online)
模型数423 (R1-Zero: 2)
是否需 RM是 / 规则
是否需 critic
是否需 reference
Token-level signalyes (GAE)yes (logp 累加)outcome only (token 共享 A^)
Advantage 估计A^GAE隐式r~i (group 归一化)
显存中(省 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

r(q,o)=raccuracy答案对 = 1+rformat格式对 = 0.1+rlanguage语言一致性

详细:

  • 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 工程实现细节:

  1. vLLM rollout:每条 prompt 采 16 个回答,最长 32K token,单次 rollout 几秒;
  2. FSDP + ZeRO-3:actor 和 reference 都做参数分片;
  3. Reference refresh:每 400 步 πrefπθ,防止 KL 过紧;
  4. Reward filtering:跳过所有 16 个回答都对或都错的 prompt(advantage 全 0,无信号);
  5. Mixed precision (BF16):rollout 与训练都用 BF16;
  6. Dynamic padding:按长度排序后分组打包。

15.6 GRPO 实现详解

15.6.1 完整伪代码

python
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

python
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 仍带来新的显存压力:

  1. vLLM 解耦推理:rollout 用 vLLM(独立进程),训练用 PyTorch;
  2. Sequence packing:把多个回答打包成一个长序列,避免 padding 浪费;
  3. Gradient accumulation:减小 per-step batch;
  4. 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 个改进:

  1. 双 clip 阈值 εlowεhigh
    • 通常 εhigh>εlow(如 0.28 vs 0.2);
    • 让"有用的探索"(优势 > 0 的提升)有更大空间;
  2. Dynamic Sampling:过滤掉 group reward 全平的 prompts(无梯度信号);
  3. Token-level loss aggregation:用 token 数加权平均,而非 sequence 平均(更公平地分配信号);
  4. 移除 KL 惩罚:直接让 ref refresh 替代 KL,简化损失。

15.7.2 Dr.GRPO (Liu et al., 2025)

发现 GRPO 中的两个偏差:

  1. 长度偏差:除以 |oi| 让短回答 advantage 被放大;
  2. std 归一化偏差:std 估计本身有偏,特别是 G 小时。

Dr.GRPO 的修正:

  • 不除 sequence 长度:直接 token 级求和;
  • 用无偏 std:bessel correction(除以 G1)。

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 处采 G 个 continuations;
  • 用 continuation 的 reward 均值估计 token-level value;
  • 接近 critic 但不需要训练。

代价:rollout 量爆炸。

15.7.5 K3-GRPO / K1-GRPO 等 KL 估计器

不同 KL 估计器对训练稳定性影响:

  • k1log(πθ/πref) —— 简单但方差大;
  • k212(log)2 —— 总是 ≥ 0;
  • k3 (推荐)πrefπθlogπrefπθ1 —— 总是 ≥ 0 且无偏。

15.8 GRPO + Process Reward Model (PRM)

15.8.1 ORM vs PRM 在 GRPO 中

GRPO 默认用 outcome-level reward(一整条回答一个分)。但在数学推理中,过程奖励 信号更密集。

PRM 给每一步打分:

rtPRM=PRM(st,at)

GRPO + PRM 流程:

  1. 同样采 G 个回答;
  2. 用 PRM 给每个 step 打分;
  3. token-level advantage = step-level reward 减 group baseline;
  4. 同样 PPO-Clip 更新。

DeepSeekMath 论文同时探索了 outcome / process / iterative 三种 GRPO 变体。

15.8.2 Process baseline

PRM-GRPO 中的 group baseline 也可以 step-level:

A^i,t=ri,tPRMmeanj(rj,tPRM)stdj(rj,tPRM)

但需要不同回答在 step t 上"对齐",工程复杂。实际多用 outcome-only。


15.9 GRPO 的成功要素

为什么 GRPO 在推理任务上特别成功?

  1. 规则可验证 reward:数学/代码任务有客观对错,避免 RM 的 imperfection;
  2. Group 让 reward 信号有对比性:即使 reward 是 0/1,组内也能区分"全对"vs"部分对";
  3. Outcome 监督足够:CoT 推理的"思考过程"由模型自主探索,无需 step-level 监督;
  4. 省 critic 让训练稳定:critic 在稀疏 reward 下尤其难训;
  5. Reference refresh 替代 KL 紧约束,给模型更大优化空间。

但 GRPO 也有局限:

  • 主观任务(创意写作、对话)上,规则奖励难以定义;
  • G 必须够大(≥ 8)才有有效 baseline;
  • 长 rollout 计算开销大;
  • 仍需要好的 reward 设计(reward design 本身是难题)。

15.10 完整代码框架

简化版 GRPO 训练循环(详见 code/08_grpo_training.py):

python
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 飙升β 太小 + 没 ref refresh调大 β 或开 ref refresh
输出截断(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。

思考题

  1. 推导验证:证明 group baseline r¯=1Gjrj 在策略梯度中的偏差为 0。提示:考虑 jrjri 之间的相关性;当对一个特定 iE[r¯logπ(oi)] 时,ri 部分如何处理?

  2. 为什么 GRPO 适合推理但不适合对话? 从 reward 信号密度、group baseline 有效性、以及"生成多样性"角度分析。如果你要把 GRPO 用到对话任务上,会做哪些改造?

  3. 工程题:在 R1 训练中,DeepSeek 使用 reference refresh(每 400 步 πrefπθ)。这等价于"每 400 步把 trust region 重新锚定到当前点"。请分析:(a) 如果不做 ref refresh,单纯减小 β 行不行?(b) 如果做 refresh,KL 是否可能"周期性飙升"?(c) refresh 频率与 lr 之间应当如何匹配?

基于 MIT 协议发布