GRPO 后训练系统工程实战:从 RLHF 偏好建模到可验证奖励的全链路架构

执行摘要:大多数人把后训练理解成"拿一堆偏好数据再微调一遍",这是 2023 年的认知。2026 年后训练真正的形态是:一个由采样引擎、奖励系统、策略训练器、权重同步通道四个子系统构成的分布式在线系统,它的瓶颈从来不在 FLOPS,而在采样吞吐、权重同步延迟与奖励信号的信噪比。本文拆开这套系统:为什么 PPO 的 Critic 在 LLM 场景下是负资产、GRPO 用组内基线替代价值网络意味着什么、可验证奖励(RLVR)如何把奖励黑客问题从"对抗"降级为"工程"、以及 rollout-colocate 架构下权重重分片为什么是最容易被低估的稳定性杀手。给出可运行的损失实现、奖励函数模板与生产坑位清单。

一、重新理解后训练:这不是训练问题,是系统问题

监督微调(SFT)是离线问题:数据固定、损失固定、吞吐由数据并行度线性决定。后训练(RLHF/RLVR)是在线问题:数据的分布由当前策略自己生成,策略每更新一步,下次采样的分布就变了。这个闭环带来三个 SFT 里不存在的工程约束:

维度SFT后训练(GRPO/PPO)
数据来源静态数据集当前策略在线采样
瓶颈训练算力采样吞吐(通常占 70~85% 步时间)
显存模型 + 优化器再加一份推理引擎 + KV Cache
失败模式欠拟合/过拟合奖励黑客、熵坍塌、长度爆炸
可复现性高低(采样非确定性 + 推理引擎版本敏感)

结论先行:后训练 80% 的工程精力应该花在采样与奖励上,而不是策略更新算法上。 算法层面 2026 年已经高度收敛——去掉 Critic、组内归一化、token 级损失、clip 约束,剩下的差异大多是工程细节。

二、PPO 的 Critic 为什么是负资产

经典 PPO 需要四个模型同时驻留显存:policy、reference、reward、critic(value)。在 LLM 场景下 critic 基本等于第二个 policy,显存和算力直接翻倍。更麻烦的是它的估计质量:

学一个 V(s) 去预测"从当前前缀出发的期望累计奖励",状态空间是整个 token 序列前缀。在 7B+ 模型、数千 token 的序列上,value network 的拟合误差往往和 advantage 本身同一量级。一个信噪比接近 1 的基线,减下去只会放大方差。 这就是为什么很多团队的 PPO 训出来不如 DPO 稳定——不是 PPO 理论有问题,是 Critic 在实践中没能用。

GRPO(Group Relative Policy Optimization)的做法极其朴素:同一个 prompt 采样 G 条轨迹,用组内的奖励均值当基线,用组内标准差归一化。

def grpo_advantages(rewards: Tensor, G: int) -> Tensor:
    """rewards: shape [B*G],同一 prompt 的 G 条轨迹连续排列"""
    r = rewards.view(-1, G)                    # [B, G]
    mean = r.mean(dim=1, keepdim=True)
    std  = r.std(dim=1, keepdim=True)

    # 关键:std 为 0 的组(全对或全错)必须置零,否则除零产生 inf
    adv = (r - mean) / (std + 1e-4)
    adv = torch.where(std < 1e-4, torch.zeros_like(adv), adv)
    return adv.view(-1)                        # 组内零和:sum(adv) == 0

注意 std < 1e-4 这个分支。这不是数值保护,而是语义决定:一组轨迹全对或全错时,组内没有可学习的对比信号,强行归一化只会把一个极小的浮点噪声放大成满幅梯度。生产里建议直接统计"有效组比例"(std 非零的组占多少),这个指标低于 30% 说明题目难度与当前策略不匹配——要么加难题,要么调采样温度。这是最实用的课程学习信号。

三、策略损失:token 级归一化才是正解

def grpo_loss(new_lp, old_lp, ref_lp, adv, mask, clip=0.2, beta=0.0):
    """各张量 shape: [B*G, T],mask 标记真实 token(排除 padding)"""
    ratio = torch.exp(new_lp - old_lp)                    # 重要性采样比
    unclipped = ratio * adv[:, None]
    clipped   = torch.clamp(ratio, 1 - clip, 1 + clip) * adv[:, None]
    pg = -torch.min(unclipped, clipped)                   # 悲观下界

    if beta > 0:                                          # KL 惩罚项
        kl = torch.exp(ref_lp - new_lp) - (ref_lp - new_lp) - 1.0
        pg = pg + beta * kl

    # 陷阱点:分母必须是本 batch 的总 token 数,而不是序列数
    return (pg * mask).sum() / mask.sum().clamp(min=1)

分母的选择是长度偏置的根源。如果按序列平均(mean(dim=-1) 再对 batch 平均),长序列里每个 token 的梯度权重被稀释,模型会学到"说短一点更稳"——表现为输出长度持续收缩、正确率反而下降。按 token 求和再除以总 token 数,每个 token 的梯度贡献才是均等的。这条在 DeepSeekMath 及后续大量复现工作中被反复验证。

关于 beta:GRPO 原论文把 KL 放进损失,但工程实践中很多团队设 beta=0,靠 clip 加单步内更新(on-policy,num_inner_epochs=1)约束漂移。理由是 ref 模型前向要多花一份算力,而 clip 已经足够。我的判断:做通用对话对齐时保留小 beta(0.001~0.01),做数学/代码 RLVR 时可以置零——后者的奖励本身有硬边界,策略跑偏会被奖励直接拉回。

四、奖励工程:把"对抗问题"降级为"工程问题"

RLHF 最难的部分从来不是优化器,是奖励模型会被策略找到漏洞钻。神经奖励模型(RM)本质上是一个会被分布外样本击穿的分类器,策略只要发现"写长一点、用更自信的语气、加几个礼貌用语"能稳定拿高分,就会迅速收敛到这些表面特征。

可验证奖励(RLVR)换了个思路:奖励不由模型给,由可执行的判定器给。

def math_reward(completion: str, answer: str) -> float:
    pred = extract_boxed(completion)          # 取 \boxed{} 内容
    if pred is None:
        return 0.0                            # 格式不合规:直接零分
    return 1.0 if symbolic_equal(pred, answer) else 0.0

def code_reward(completion: str, tests: list[str]) -> float:
    src = extract_code_block(completion)
    if src is None:
        return 0.0
    try:
        r = sandbox.run(src, tests, timeout_s=4)   # gVisor/seccomp 沙箱
    except Timeout:
        return 0.0                            # 超时不是 -1,是 0
    return r.passed / r.total                 # 连续奖励,梯度更平滑

几个实战观点:

  • 沙箱是奖励系统的一部分,不是外部依赖。 代码类 RLVR 的吞吐瓶颈经常在沙箱并发数上。建议独立部署、池化连接、按语言分池,超时统一返回 0 分而不是报错——超时的样本在组内是"最差样本",本身就有学习价值。
  • 稀疏奖励不是问题,判定器噪声才是。 0/1 奖励完全可用,前提是判定器准确。宁可让 20% 的样本因判定失败拿 0(随机噪声,可被组平均吸收),也不要让 5% 的样本拿到错误的 1(系统性偏差,会被策略精准利用)。
  • 二进制奖励加组内标准差归一化,等价于在学"排序"而非"打分"。 这解释了为什么 G 的选择很关键:G=8 时组内对比信号弱,G=64 时采样成本翻 8 倍。工程上常见配置是 G=16,配合动态采样(丢弃无效组后补足)。

五、Rollout:后训练真正的瓶颈

一次 GRPO 步的时间分布,实测大致是:采样 70~85%、奖励计算 5~10%、前向反向 10~15%、权重同步 3~8%。这意味着优化训练侧(比如换更快的算子)收益极小,优化采样才有杠杆。

# verl 风格的配置,重点是采样侧的四个开关
rollout:
  n: 16                        # 每个 prompt 采样 G 条
  temperature: 1.0             # 低于 0.8 组内多样性不足,std 频繁为 0
  max_tokens: 4096
  engine: vllm
  gpu_memory_utilization: 0.35 # 与训练侧共享显存时的上限
  enable_chunked_prefill: true # 长短混合 batch 下收益显著
actor:
  num_inner_epochs: 1          # on-policy:不要多轮复用同一批采样
  clip_ratio: 0.2
  loss_agg_mode: token-mean    # 见第三节

colocate 还是分离? 把推理引擎和训练进程放在同一批 GPU 上(colocate)能省掉跨节点权重传输,代价是显存要在两者间腾挪,且每步都要做一次"训练态权重到推理态权重"的重分片(TP/PP/EP 切分往往不同)。这是最容易被低估的稳定性杀手:

  • 权重同步必须是确定性的。 推理引擎侧若按不同 layout 加载,会引入静默的数值偏差,on-policy 假设被破坏,损失曲线看起来正常但实际在训错目标。建议每 N 步做一次"训练前向 vs 推理前向"的 logprob 对拍,差值超过 1e-3 就报警。
  • 显存碎片。 vLLM 的 KV Cache 池和训练侧的激活显存会互相挤压。生产上把 gpu_memory_utilization 压到 0.3~0.4,并预留 8~10GB 给峰值激活。
  • 长尾序列。 采样阶段 5% 的超长序列能吃掉 40% 的时间。设置分档 max_tokens 加部分 rollout(超长的留到下一步继续)比统一截断效果好得多——截断会污染奖励信号。

六、生产坑位清单

现象根因解法
奖励涨、人工评测降奖励黑客 / 判定器漏洞冻结策略做红队回放;增加格式与长度惩罚项
输出长度单调收缩损失按序列平均改 token-mean;检查 mask 是否误含 padding
熵迅速归零,输出模板化温度过低 / G 过小 / clip 过紧温度 1.0、G≥16、加最小熵约束
损失 NaNstd≈0 组除零 / ratio 溢出掩码零 std 组;对 old_lp 做 detach 与 clamp
吞吐上不去沙箱并发或长尾序列沙箱池化;分档 max_tokens
可复现性差推理引擎版本/算子差异锁定引擎版本;记录 logprob 对拍基线

七、结论

  1. GRPO 的价值不是"更好的算法",而是"去掉了一个训不好的 Critic"。 它把后训练从四模型降为三模型(policy/ref/reward),且在 RLVR 场景下 ref 也可省。
  2. 奖励的质量上限决定策略的上限。 把精力从调优化器转移到打磨判定器与难度分布,回报率高一个数量级。
  3. 盯住"有效组比例"这个指标。 它同时反映难度匹配、采样温度和奖励判别力,是后训练里最有信息量的单一健康度信号。
  4. 把后训练当在线系统而非训练脚本。 采样、判定、训练、同步四个子系统各自有 SLO,先测量再优化。

如果你的场景是数学、代码、SQL、工具调用这类有明确正确性判定的任务,GRPO 加 RLVR 目前是性价比最高的路径;如果是开放式对话质量,神经奖励模型依然不可替代,但 GRPO 的组内基线思路同样适用——把 RM 打分换成组内两两比较即可。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部