第十七周有两大块:强化学习(RL 基础 → RLHF → DPO → GRPO → DeepSeek-R1)和分布式训练(DP / TP / PP、ZeRO)。
作业是用强化学习提升小模型做数学题的能力。我用 Qwen2-0.5B-Instruct + GRPO,在自己的 4060 Ti 上跑了 200 步、3.1 分钟、峰值显存 5.58 GB。
结果是总正确率从 50.8% 涨到 83.8%。但这周真正学到的东西,是怎么把这个涨幅拆开归因——因为不拆开的话,我会把「学会了答题格式」包装成「学会了算术」。
一、RL 的核心概念
优势函数:为什么要减掉基线
A(s,a) = Q(s,a) − V(s) 这一步比局面的平均水平好多少
没有基线时,赢了棋这一局所有走法(包括臭棋)都会被强化。 有了基线,只有超出平均水平的走法才被强化——梯度方差大幅降低。
这一条是后面 GRPO 的直接铺垫。
探索与利用
只「利用」 总走已知最优定式,容易陷入局部最优
只「探索」 随机尝试,永远学不到有效棋感
后面在我自己的实验里,熵坍缩 12 倍就是「探索」这一侧塌掉的实测样子。
二、从 PPO 到 GRPO
RLHF 三阶段与 PPO 的工程痛点
PPO 要同时加载 4 份模型:Actor(更新)、Critic(更新)、Reward Model(冻结)、Reference(冻结)。
三个具体问题:
- Critic 一样大 —— 70B 模型训练需要 280B+ 参数的显存
- Critic 训练不稳定 —— 价值估计偏差会累积并传播到策略梯度
- 工程复杂度高 —— 4 个模型的学习率、更新频率都要单独调
核心问题:能不能去掉 Critic,同时保持训练稳定?
DPO:绕过 RL
把 RM + PPO 合成一个监督学习目标,只需 2 个模型。简单稳定,但只能离线学习。
GRPO:用组内平均代替 Critic
同一个 Prompt 采样 G 条回答,每条拿到奖励 r₁…r_G
advantage_i = (r_i − mean(r)) / std(r)
基线不是学出来的,是同组另外 G−1 条样本给的。
代价是一个很硬的约束——组内 G 条如果奖励全一样,std = 0,advantage 全是 0,这一步完全没有梯度。这条约束后面在我的实验里实实在在地生效了。
DeepSeek-R1-Zero 的 Aha Moment
基础模型是未经 SFT 的 V3-Base,纯 GRPO + 可验证奖励,完全不用人工标注的推理示范数据。
结果模型自发在 think 块里写出:
让我数一数…等等,让我重新数一遍。1×21=21,对,是 21
没有人教模型怎么反思。 在纯 RL 训练过程中,模型自发产生了「等等,让我重新想想」的行为,推理链长度也随训练自然增长。
为什么选纯 RL 而不是 SFT 热启动,课上用了 AlphaZero 类比:
AlphaZero 不依赖人类棋谱,发现了人类从未使用过的走法。 SFT-first 会被人类示范「锚定」,难以发现更短更优的解法。
但 R1 正式版的工作流又说明 RL 与 SFT 相辅相成:R1-Zero 纯 RL → 产出 CoT 数据 → SFT 冷启动 → 再 GRPO。SFT 提供可读性,GRPO 提供推理深度。
三、分布式训练:显存账
先把账算清楚。GPT-3 175B + Adam + BF16:
模型参数 350 GB 175B × 2 字节
梯度 350 GB 与参数等大
优化器状态 2100 GB FP32 参数副本 + 动量 + 方差,每项 4 字节
─────────────────────
合计 约 2800 GB(还不含激活)
占比:优化器状态 75%、参数 13%、梯度 13%——所以优化的第一刀就砍向优化器状态。
ZeRO 三个 Stage
ZeRO-1 分片优化器状态 节省约 75%
ZeRO-2 再分片梯度 节省约 87%
ZeRO-3 参数也切,用时 AllGather 节省约 97%
64 卡、175B 的每卡占用:2800 GB → 700 → 350 → 29 GB。
ZeRO 的本质:把「每卡都存一份」变成「每卡存 1/N,需要时再聚」——用通信换显存。
三种并行的分工
| DP 数据并行 | TP 张量并行 | PP 流水线并行 | |
|---|---|---|---|
| 切什么 | mini-batch | 权重矩阵维度 | 模型层数 |
| 通信频率 | 每步 1 次 | 每层 2 次 | 仅在 stage 边界 |
| 显存效果 | 不减单卡显存 | 减到 1/TP | 减到 1/PP |
| 适用带宽 | 任意 | 需 NVLink | 低带宽可用 |
FFN 的张量并行有个很漂亮的设计:Linear₁ 列并行 → GeLU 本地做(零通信)→ Linear₂ 行并行 AllReduce,整个 FFN 前向只通信 1 次。
TP 的局限很实在:GPT-3 96 层算下来总通信量约 175TB,所以 TP 限制在单节点内(NVLink 域),跨节点扩展交给 PP 或 DP。
PP 的核心挑战是 Bubble:p 个 stage 时朴素流水线的 bubble 占比 = (p−1)/p,4 卡时利用率只有 25%。GPipe 用 micro-batch 把它压到 (p−1)/(m+p−1)。
四、作业:先把问题拆成两个
题目是「用 RL 提升模型做数学题的能力」。我拆成两个可以分别测量的问题:
- RL 能不能让模型算得更对
- RL 学到的是「算术能力」还是「答题格式」
这两件事很容易混在一起,如果不拆开,正确率涨了也说不清是因为什么。
所以奖励函数从一开始就设计成两个独立的函数:
| 函数 | 满分 | 判据 |
|---|---|---|
reward_correct | 1.0 | 答案对(宽松口径:取输出里最后一个数字) |
reward_format | 0.2 | 输出里有 <answer>数字</answer> |
指标也记三套口径:
fmt_rate 有没有按格式答 只看格式
strict_rate 标签里的数字对不对 格式 + 正确
loose_rate 最后一个数字对不对 只看正确,不管格式
后面会看到,这个拆分是这次实验里最有用的一个决定。
题目分级:训练集配比不是随手定的
六个难度全部程序生成,训练只用 L2/L3/L5,L1/L4/L6 一道都不训,留出来测泛化。
配比依据就是 GRPO 那条约束——训练集要放模型「会一点但不稳」的档位:
训练前摸底的 informative_group_rate(组内 8 条有对有错的比例):
L1 47.5% ← 太简单,组内常常全对
L6 7.5% ← 太难,组内几乎全错
L2/L3/L5 85% / 82.5% / 77.5% ← 梯度最密
L1 和 L6 放进训练集,大半步数都是白跑的。
五、主结果
难度 fmt_rate strict_rate loose_rate
L1_add_1digit 0.0% → 100.0% 0.0% → 100.0% 100.0% → 100.0%
L2_addsub_2digit 7.5% → 100.0% 7.5% → 97.5% 55.0% → 97.5%
L3_addsub_3digit 7.5% → 100.0% 7.5% → 90.0% 47.5% → 90.0%
L4_mul_1digit 0.0% → 100.0% 0.0% → 97.5% 65.0% → 97.5%
L5_mul_2x1digit 0.0% → 100.0% 0.0% → 97.5% 35.0% → 97.5%
L6_mul_2x2digit 2.5% → 100.0% 0.0% → 20.0% 2.5% → 20.0%
────────────────────────────────────────────────────────────────────
总计(240 题) 2.9% → 100.0% 2.5% → 83.8% 50.8% → 83.8%
+97.1pt +81.2pt +32.9pt
三个口径涨幅差这么多,说明的是不同的事:
fmt_rate+97.1pt —— 格式几乎是从零学到满分loose_rate+32.9pt —— 这才是「算术能力」的真实提升strict_rate+81.2pt —— 数字最好看,但它是前两者的乘积效应
训练前那句「答对了但没按格式写」
9 + 2 = 11 训练前输出:11
训练后输出:<answer>11</answer>
基线 loose_rate 50.8% 而 strict_rate 只有 2.5%。
如果这次只统计
strict_rate,会得出「基座模型基本不会算术」的结论,这是错的——它会算,只是不按要求的格式写。
L6 是个反例,而且很有用
L6(两位数×两位数)训练后 fmt_rate = 100%,strict_rate 只有 20%。
格式学满了,算术没学会。 这一栏单独证明了两个奖励确实解耦:格式分能独立收敛到满分,不需要算对。
RL 能矫正行为,不能凭空创造能力。
六、训练动态:正确分先收敛,格式分后收敛
区间 正确分 格式分 补全长度 熵 零方差组比例
(1.0) (0.2)
1–45 步 0.756 0.028 4.96 0.253 0.29
50–100 步 0.850 0.114 7.39 0.153 0.21
105–150 步 0.877 0.190 9.58 0.064 0.52
155–200 步 0.886 0.196 9.80 0.049 0.61
顺序和我预期的相反
我写代码时在注释里预期的是「格式先收敛、正确率后爬坡」——格式简单,应该先学会。实测正好反过来。
正确分在前 20 步就从 0.425 冲到 0.89,之后 180 步只再涨 0.03格式分前 45 步几乎不动,50 步之后才开始爬,是一条明显的 S 形曲线
原因是奖励尺度:正确分满分 1.0,格式分只有 0.2。advantage 是在两项之和上算的,早期组内答案对错分明,正确分的差异就有 1.0 的量级,格式分那 0.2 完全被盖住。等到正确率涨到 0.85 以上、正确分不再有区分度,格式分才成为唯一还能拉开差距的信号。
复合奖励里,各项的收敛顺序由权重决定,不由难度决定。
补全长度是格式收敛的独立佐证
completions/mean_length 从 4.5 涨到 9.9,涨的区间(50–120 步)和格式分的 S 形上升段完全重合。多出来的 5.4 个 token 就是 <answer> </answer> 这对标签。
这条曲线的价值在于:它不是奖励函数直接优化的对象,却精确复现了格式分的轨迹。 两个来源不同的信号指向同一件事,比单看奖励曲线可信得多。
训练信号在后半程枯竭
零方差组比例 0.29 → 0.61 组内 8 条奖励全一样,advantage 恒为 0
熵 0.253 → 0.049 采样几乎退化成贪心,12 倍的坍缩
loss −0.069 → +0.0017 梯度贡献趋近于 0
到最后 45 步,六成的组已经产生不了任何梯度。第 160 步还出现过一次 grad_norm = 0——整批四个组全部退化。
这一轮的有效训练在 200 步之前就基本结束了。 继续堆步数不会有收益,要接着提升只能换供给(提难度 / 提温度)。这不是「训练不充分」,是「这批题目已经榨干了」。
epsilon 全程没起过作用
clip_ratio 的五个字段在 41 条日志里全部恒为 0。
因为每批采样只做一次梯度更新,是严格 on-policy 的,重要性采样比恒等于 1,永远落在 [1−ε, 1+ε] 里。
PPO 的裁剪机制在这个配置下是挂着但没生效的。
七、泛化:格式完全迁移,算术能力部分迁移
strict_rate loose_rate
训练前 训练后 变化 训练前 训练后 变化
训练过 L2/L3/L5 5.0% 95.0% +90.0pt 45.8% 95.0% +49.2pt
没训过 L1/L4/L6 0.0% 72.5% +72.5pt 55.8% 72.5% +16.7pt
strict_rate上没训过的涨了 72.5pt——这主要是格式迁移。格式是题目无关的行为,一旦学会就无差别作用在所有难度上。loose_rate上没训过的只涨了 16.7pt,训练过的涨了 49.2pt——去掉格式因素后,算术能力的迁移明显弱于训练过的难度。
只看
strict_rate会把格式的功劳记到算术能力头上。
八、几个必须显式覆盖的配置
| 问题 | 后果 |
|---|---|
Qwen2-0.5B 的 config 写着 torch_dtype=float16 | AdamW 的 eps=1e-8 在 fp16 下下溢成 0,除零得 NaN,一步训废 |
| transformers 5.x 下开 gradient checkpointing | generate 前向输出损坏,采样全是乱码、奖励恒 0 |
| trl 0.21 用了 transformers 的私有函数 | 5.x 改成返回元组,非空元组恒为真 → import 直接崩 |
这三个都属于「不显式覆盖就静默出错」的类型,光看报错信息定位不到。
一个输出「很合理的错话」的 bug
冒烟测试时模型完全不写标签,strict_rate 前后都是 0。汇总脚本默认拿 strict_rate 算泛化,于是打出:
→ 训练过的都没怎么涨,先看训练曲线是不是根本没学起来
结论是反的——同一次跑里 loose_rate 涨了 16.7pt,训练明明学起来了。四个数全是 0 说明的是「这个指标失效」,不是「模型没学到东西」。
这类 bug 最麻烦的地方在于它输出的是一句读起来很合理的话。如果当时直接跑全量、没做冒烟,这句话很可能就被抄进报告了。
采样只采到了 L1
原来的写法是「前 12 条各存一份」。评测集按难度顺序生成,前 12 条全落在 L1 里——想看 L6 到底错成什么样,一条样本都没有,而最难那一级的失败长相恰恰是最想看的。
这周的收获
1. 涨幅必须拆开归因。
strict_rate 从 2.5% 涨到 83.8% 很好看,可以直接写简历。但拆开看:格式贡献 +97.1pt,算术能力贡献 +32.9pt。不拆开就是把「学会了答题格式」包装成「学会了算术」。
2. 复合奖励的收敛顺序由权重决定。
格式(0.2 分)比正确(1.0 分)简单得多,却晚了 50 步才开始学——因为早期它的信号被完全盖住了。这跟我的直觉完全相反。
3. 有效训练会自己结束。
零方差组比例 0.29 → 0.61、熵坍缩 12 倍——GRPO 那条「std=0 就没梯度」的约束,从纸面变成了曲线。学一个算法的公式,和看到它在自己的训练日志里生效,是两种不同的理解。
4. 找一个不受优化的独立信号来交叉验证。
补全长度不是奖励函数优化的对象,却精确复现了格式分的轨迹。两个来源不同的信号指向同一件事,比单看奖励曲线可信得多。
下周是最后一周——复习与求职。