Skip to content

18.2 GRPO 训练推理模型(RLVR 管线)

上一节介绍了 RLVR(Reinforcement Learning with Verifiable Rewards,可验证奖励强化学习)的整体范式——用规则验证器代替人类偏好打分,让模型在数学、代码等可自动判定对错的任务上"自我进化"。本节将动手实现一条完整的 RLVR 训练管线:从采样推出(rollout)、可验证奖励计算、组优势准备,到策略更新的完整循环。GRPO 的数学定义(目标函数、分组优势估计器)已在 §16.3 详细推导,本节不重复公式推导,而是聚焦于将这些公式落地为可运行的训练代码。


18.2.1 RLVR 管线的四个阶段

一个 RLVR + GRPO 的训练步骤可以分解为四个阶段:

阶段输入输出关键操作
1. Rolloutprompt qG 条回复模型 eval 模式,自回归采样
2. Reward回复文本 + 标准答案标量奖励 {r1,,rG}数学验证器比对答案
3. Advantage奖励向量优势向量 {A^1,,A^G}组内归一化
4. Update优势 + log 概率更新后的 θ策略梯度 + 梯度裁剪

表 18-1:RLVR + GRPO 单步训练的四阶段流水线。


18.2.2 阶段一:Rollout 采样

对每道数学题,模型在 eval 模式下用 temperature + top-p 采样 G 条独立回复。采样多样性至关重要——如果所有回复一样,组优势将退化为零(参见 §16.3 退化组讨论)。

python
import torch

@torch.no_grad()
def sample_one_response(model, input_ids, max_new_tokens=512,
                        temperature=0.8, top_p=0.9):
    """自回归采样一条回复,返回 (full_ids, prompt_len)。"""
    device = input_ids.device
    generated = []
    logits = model(input_ids.unsqueeze(0))[:, -1]

    for _ in range(max_new_tokens):
        if temperature != 1.0:
            logits = logits / temperature
        probs = torch.softmax(logits, dim=-1)
        # Top-p 过滤
        sorted_probs, sorted_idx = torch.sort(probs, descending=True)
        cumsum = sorted_probs.cumsum(dim=-1)
        mask = cumsum - sorted_probs > top_p
        sorted_probs[mask] = 0.0
        sorted_probs /= sorted_probs.sum(dim=-1, keepdim=True)
        probs.zero_().scatter_(1, sorted_idx, sorted_probs)

        next_token = torch.multinomial(probs, num_samples=1)
        token_id = next_token.item()
        generated.append(token_id)
        if token_id == model.config.eos_token_id:
            break
        logits = model(next_token)[:, -1]

    prompt_len = input_ids.numel()
    gen_ids = torch.tensor(generated, device=device, dtype=input_ids.dtype)
    return torch.cat([input_ids, gen_ids]), prompt_len

实现要点:采样期间必须关闭 dropout(eval 模式),否则同一 prompt 的多条回复会因 dropout 掩码引入额外噪声。生产级实现会维护 KV Cache 加速自回归推理,此处为了清晰省略了缓存细节。当 G 较大时,可将 G 条回复在 batch 维度并行生成以提高吞吐量——核心思路是将 prompt 复制 G 份后一次前向计算。


18.2.3 阶段二:可验证奖励计算

RLVR 与 RLHF 的核心区别在于奖励来源。在数学任务中,验证器从模型输出中提取 \boxed{} 内的答案,然后与标准答案比对。

RLVR 管线中的 GRPO 流程。对同一 prompt 采样 G 条回复,经奖励函数打分后计算组相对优势,最终更新策略。

图 18-2:GRPO 在 RLVR 管线中的工作流程。策略模型对同一 prompt 生成 G 条回复,每条回复经奖励函数评分后,通过 Group Computation 计算组相对优势。

python
import re

# --- 答案提取:从模型输出中定位最后一个 \boxed{} 并处理嵌套 ---
def extract_boxed_answer(text):
    """从模型输出中提取 \\boxed{} 内的答案,支持嵌套括号。"""
    idx = text.rfind("\\boxed{")
    if idx == -1:
        return None
    depth, start = 0, idx + len("\\boxed{")
    for i in range(start, len(text)):
        if text[i] == "{":
            depth += 1
        elif text[i] == "}":
            if depth == 0:
                return text[start:i].strip()
            depth -= 1
    return None

# --- 等价判断:先比字符串,不匹配再尝试数值比较 ---
def grade_answer(predicted, ground_truth):
    """判断预测答案与标准答案是否等价(字符串 + 数值比较)。"""
    if predicted is None:
        return False
    pred = predicted.strip().replace(" ", "")
    gt = ground_truth.strip().replace(" ", "")
    if pred == gt:
        return True
    try:
        return abs(float(pred) - float(gt)) < 1e-6
    except (ValueError, TypeError):
        return False

# --- 组合:提取答案 → 判断等价 → 返回二值奖励 ---
def reward_rlvr(response_text, ground_truth):
    """RLVR 二值奖励:正确 1.0,错误 0.0。"""
    extracted = extract_boxed_answer(response_text)
    if extracted is None:
        return 0.0
    return 1.0 if grade_answer(extracted, ground_truth) else 0.0

奖励设计考量:最简单的"对 = 1,错 = 0"二值奖励在实践中非常有效——组优势机制已在组内提供了足够的梯度信号。如果模型输出中没有 \boxed{},直接给 0 奖励,这种"缺省惩罚"会自然驱动模型学会输出格式化答案。DeepSeek-R1 还额外使用了格式奖励(检查推理标签是否齐全),但在纯 RLVR 管线中仅用正确性奖励已足够。


18.2.4 阶段三:组优势计算

拿到 G 条回复的奖励后,按 §16.3 的公式 A^i=(rir¯)/(σ+ϵ) 计算组相对优势:

python
# 教学示例:展示核心逻辑,省略了部分 import 和辅助函数定义
def compute_group_advantages(rewards, epsilon=1e-4):
    """计算 GRPO 组相对优势,返回与 rewards 同形状的张量。"""
    rewards_t = torch.tensor(rewards, dtype=torch.float32)
    return (rewards_t - rewards_t.mean()) / (rewards_t.std() + epsilon)

def is_degenerate_group(advantages):
    """检查优势是否全为零(退化组:全对或全错)。"""
    return torch.allclose(advantages, torch.zeros_like(advantages),
                          atol=1e-8, rtol=0.0)

退化组处理:当 G 条回复奖励完全相同时(全对或全错),优势全为零,模型学不到任何东西。跳过退化组不影响性能,还能节省计算。在数学数据集上约 30-50% 的步会遇到退化组(取决于 G 和模型初始能力)。


18.2.5 阶段四:策略更新

根据优势和 log 概率计算策略梯度损失。首先需要计算仅生成部分的 log 概率之和:

python
# 教学示例:展示核心逻辑,省略了部分 import 和辅助函数定义
def sequence_logprob(model, token_ids, prompt_len):
    """计算生成部分的总 log 概率(可反向传播)。"""
    logits = model(token_ids.unsqueeze(0)).squeeze(0).float()
    logprobs = torch.log_softmax(logits, dim=-1)
    targets = token_ids[1:]
    selected = logprobs[:-1].gather(1, targets.unsqueeze(-1)).squeeze(-1)
    return selected[prompt_len - 1:].sum()  # 只对生成部分求和

在无 KL 惩罚的简化版本中(DAPO、Dr.GRPO 等推荐移除 KL 项),损失化简为加权策略梯度:

L=1Gi=1GA^ilogπθ(oiq)

优势为正的回复增大概率,为负的回复减小概率。注意优势用 .detach() 停止梯度——它只作为权重,梯度仅通过 log 概率流回模型参数。


18.2.6 完整训练循环

将四个阶段串联起来,加上优化器、梯度裁剪和日志,就构成了完整的训练循环:

python
# 教学示例:展示核心逻辑,省略了部分 import 和辅助函数定义
def compute_grpo_loss(model, tokenizer, example, device,
                      num_rollouts=8, max_new_tokens=512,
                      temperature=0.8, top_p=0.9):
    """一个样本的完整 GRPO 流水线:rollout → reward → advantage → loss。"""
    prompt = (f"Solve the following math problem. "
              f"Put your final answer in \\boxed{{}}.\n\n{example['problem']}")
    input_ids = torch.tensor(tokenizer.encode(prompt), device=device)

    # 阶段 1-2:采样 + 奖励
    model.eval()
    rollout_data, all_rewards, samples = [], [], []
    for _ in range(num_rollouts):
        full_ids, plen = sample_one_response(
            model, input_ids, max_new_tokens, temperature, top_p)
        gen_text = tokenizer.decode(full_ids[plen:].tolist())
        reward = reward_rlvr(gen_text, example["answer"])
        rollout_data.append((full_ids, plen))
        all_rewards.append(reward)
        samples.append({"text": gen_text, "reward": reward,
                        "gen_len": full_ids.numel() - plen})
    model.train()

    # 阶段 3:优势
    advantages = compute_group_advantages(all_rewards)
    if is_degenerate_group(advantages):
        return {"loss": 0.0, "loss_tensor": None,
                "rewards": all_rewards, "samples": samples}

    # 阶段 4:损失
    logps = torch.stack([
        sequence_logprob(model, ids, plen)
        for ids, plen in rollout_data
    ])
    pg_loss = -(advantages.to(device).detach() * logps).mean()
    return {"loss": pg_loss.item(), "loss_tensor": pg_loss,
            "rewards": all_rewards, "samples": samples}


def train_rlvr_grpo(model, tokenizer, train_data, device,
                    steps=100, num_rollouts=8, max_new_tokens=512,
                    lr=1e-5, checkpoint_every=50):
    """RLVR + GRPO 完整训练循环。"""
    optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
    model.train()

    for step in range(1, steps + 1):
        example = train_data[(step - 1) % len(train_data)]
        stats = compute_grpo_loss(model, tokenizer, example, device,
                                  num_rollouts, max_new_tokens)

        if stats["loss_tensor"] is not None:
            optimizer.zero_grad()
            stats["loss_tensor"].backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()

        reward_avg = sum(stats["rewards"]) / len(stats["rewards"])
        print(f"[Step {step}/{steps}] loss={stats['loss']:.4f} "
              f"reward_avg={reward_avg:.3f}")

        if checkpoint_every and step % checkpoint_every == 0:
            torch.save(model.state_dict(), f"rlvr_grpo_step{step:05d}.pt")

关键设计决策

  • 单步更新(μ=1:TRL 默认设置,每份数据仅用一轮梯度更新,裁剪目标自动退化为普通策略梯度。
  • 梯度裁剪 1.0:防止长序列 log 概率累加导致梯度过大。
  • 学习率 1e-5:比 SFT 的 2e-5 略低,保证训练稳定。
  • 无 KL 惩罚:DAPO、Dr.GRPO、Open-Reasoner-Zero 等多项研究表明,RLVR 场景中移除 KL 惩罚反而有利于性能。TRL 框架默认 β=0

18.2.7 使用 TRL GRPOTrainer 快速启动

生产级训练通常使用 TRL 库的 GRPOTrainer,它封装了分布式训练、vLLM 加速推理、多种 loss 变体等工程细节:

python
from datasets import load_dataset
from trl import GRPOTrainer, GRPOConfig
import re

dataset = load_dataset("trl-lib/DeepMath-103K", split="train")

def accuracy_reward(completions, ground_truth, **kwargs):
    """可验证正确性奖励:提取 \\boxed{} 并与标准答案比对。"""
    rewards = []
    for comp, gt in zip(completions, ground_truth):
        match = re.search(r"\\boxed\{(.*?)\}", comp)
        extracted = match.group(1) if match else ""
        rewards.append(1.0 if extracted.strip() == gt.strip() else 0.0)
    return rewards

config = GRPOConfig(
    output_dir="grpo-math-output",
    per_device_train_batch_size=4,
    num_generations=8,         # 组大小 G
    max_completion_length=512,
    learning_rate=1e-5,
    beta=0.0,                  # 无 KL 惩罚
    loss_type="grpo",          # 可选 "dapo" / "dr_grpo"
)

trainer = GRPOTrainer(
    model="Qwen/Qwen2-0.5B-Instruct",
    args=config,
    reward_funcs=accuracy_reward,
    train_dataset=dataset,
)
trainer.train()
参数含义推荐值
num_generations组大小 G8-16
betaKL 惩罚系数0.0(RLVR 场景)
loss_type损失归一化方式"grpo" / "dapo" / "dr_grpo"
scale_rewards是否除以标准差True(原始)/ False(Dr.GRPO)
use_vllmvLLM 加速 rollout生产训练建议开启

表 18-2:GRPOTrainer 关键配置项。


18.2.8 训练过程观察与调参

以 Qwen3-0.6B + MATH 数据集 + 8 rollouts 的实验为例,典型的训练曲线表现为:

阶段典型表现
前 20 步reward 波动剧烈,模型还在探索,输出格式不稳定
20-50 步reward 稳步上升,模型学会使用 \boxed{} 格式
50-100 步reward 增速放缓,简单题基本掌握,难题仍失败
100+ 步需监控"奖励坍塌"(reward 骤降),这是常见不稳定现象

表 18-3:RLVR + GRPO 训练各阶段的典型表现。

关键超参数影响:增大组大小 G 能降低退化组比例但线性增加计算量,G=8 是性价比甜点;生成长度限制过短会导致推理链截断,过长则增加退化概率(超时截断被判错);当 num_rollouts 较小时可增加梯度累积步数来提升稳定性。


18.2.9 小结

本节从工程实现角度走通了 RLVR + GRPO 训练管线的完整流程:

  • 四阶段流水线:Rollout → Reward → Advantage → Update,每步都有清晰的输入输出接口。
  • 数学验证器:通过正则提取 \boxed{} 答案并比对,实现零成本可验证奖励。
  • 退化组处理:检测并跳过全对/全错的无效组,避免浪费计算。
  • 无 KL 简化:RLVR 场景中移除 KL 惩罚有利于性能,代码实现更简洁。
  • GRPOTrainer:TRL 框架提供开箱即用的生产级实现,支持 vLLM 加速和多种 loss 变体。

下一节将介绍在 GRPO 基础上的策略优化进阶技术,包括 DAPO、Dr.GRPO 等工程变体如何进一步提升训练稳定性。