1. 从PPO到GRPO:为什么KL惩罚项成了绕不开的坎
如果你最近在折腾大模型的对齐训练,大概率会碰到GRPO(Group Relative Policy Optimization)这个词。我第一次接触它是在复现一个数学推理任务的强化学习微调时,当时用PPO跑了一轮,发现critic模型(价值网络)的显存占用几乎和策略模型一样大,训练成本直接翻倍。后来换成GRPO,显存瞬间降下来了,但随之而来的问题是:KL散度惩罚项到底该怎么设?设大了模型学不动,设小了输出直接跑偏,满屏都是重复的废话。
这篇文章就是围绕这个核心矛盾展开的。我会把GRPO里KL散度惩罚项的作用机制、参数调节的实操经验、以及我在实际训练中踩过的坑,完整地拆开讲一遍。不管你是刚接触强化学习的新手,还是已经从PPO迁移过来的老手,只要你在做基于GRPO的对齐训练,这里面的细节应该都能直接用上。
先说清楚GRPO解决了什么问题。传统的PPO在训练语言模型时,需要同时维护四个模型:策略模型、参考模型、奖励模型、价值模型。其中价值模型(critic)的作用是估计每个状态的价值,用来计算优势函数。但在语言生成任务里,状态空间是词表大小的指数级,critic很难训得准,而且它本身也是个和策略模型差不多大的网络,显存开销非常夸张。
GRPO的做法很巧妙:它直接砍掉了critic,改用一组采样输出的平均奖励作为基线。具体来说,对同一个prompt,GRPO会采样G个输出(比如G=8或16),然后计算每个输出的奖励,用这组奖励的均值和标准差来归一化,得到每个输出的相对优势。这样一来,优势函数的估计不再依赖价值网络,显存和计算量都大幅下降。
但问题也随之而来。PPO里KL散度惩罚项是加在奖励里的,用来约束策略模型不要偏离参考模型太远。GRPO沿用了这个设计,但由于优势函数的计算方式变了,KL项的调节逻辑也跟着变了。很多人从PPO迁移过来时,直接照搬原来的KL系数,结果训练直接崩掉,这就是我一开始踩的坑。
2. KL散度惩罚项在GRPO里的真实作用位置
2.1 奖励重塑:KL项到底加在哪里
在GRPO的实现里,KL惩罚项通常不是直接加在最终奖励上的,而是以逐token的方式嵌入到奖励信号中。具体来说,对于每个采样输出,其每个token的奖励由三部分组成:任务奖励(比如答案是否正确)、格式奖励(比如是否遵循了特定模板)、以及KL惩罚项。
KL惩罚项的计算方式是:当前策略模型在某个token上的对数概率,减去参考模型在同一个token上的对数概率,再乘以一个系数β。这个值通常是负数,因为KL散度衡量的是两个分布的差异,策略偏离参考模型越远,惩罚越大。
但这里有个细节很多人会忽略:GRPO里的KL惩罚是逐token累加的,而不是只在序列末尾加一次。这意味着一个长序列如果中间有很多token都偏离了参考模型,累积的惩罚会非常大。我在训练一个代码生成任务时,输出长度平均在500个token左右,KL项累积起来直接把任务奖励淹没了,模型完全学不到东西。
2.2 为什么不能简单照搬PPO的KL系数
PPO里常用的KL系数在0.01到0.1之间,很多人直接拿这个范围去套GRPO,结果发现模型要么不动,要么直接崩。原因在于GRPO的优势函数计算方式不同。PPO里优势函数是GAE(广义优势估计),它的数值范围通常比较小,而GRPO用的是组内归一化的相对优势,数值范围可能大得多。
举个例子,假设一组8个输出的奖励分别是[1, 0, 0, 0, 0, 0, 0, 0],归一化后的优势大约是[2.6, -0.37, -0.37, ...]。这个数值范围比PPO里的优势函数大不少。如果KL系数还是用0.01,那KL惩罚项在总奖励里的占比就太小了,约束几乎不起作用。反过来,如果KL系数设成0.5,惩罚项又会直接压过任务奖励,模型只会输出和参考模型一模一样的内容,学不到任何新东西。
我实测下来,GRPO里KL系数的合理范围通常在0.001到0.05之间,具体取决于任务奖励的尺度和输出长度。这个范围比PPO窄很多,调节起来需要更精细。
2.3 参考模型的选择与更新策略
KL惩罚项依赖参考模型,而参考模型的选择直接影响训练效果。最常见的做法是用SFT(监督微调)后的模型作为参考模型,整个训练过程中保持冻结。这样做的好处是稳定,参考分布不会变,KL惩罚的意义明确。
但也有一种做法是动态更新参考模型,比如每隔一定步数把当前策略模型复制成参考模型。这样做可以让模型在训练后期有更大的探索空间,因为参考分布跟着策略走,KL惩罚不会一直把模型拉回初始状态。不过这种做法的风险是模型可能逐渐偏离原始SFT分布,导致输出质量下降。
我在做数学推理任务时试过动态更新,发现模型在训练后期确实能探索出一些新的解题路径,但同时也开始出现格式错误和逻辑跳跃。后来改成每500步更新一次参考模型,并且把KL系数调大了一点,才勉强稳住。所以我的建议是:除非你对任务非常熟悉,否则先用冻结参考模型的方案,等跑通了再考虑动态更新。
3. 逐token KL惩罚的累积效应与长度归一化
3.1 长序列训练中的KL爆炸问题
前面提到,GRPO的KL惩罚是逐token累加的。这意味着输出越长,KL项的总和越大。如果任务本身要求长输出(比如代码生成、长文写作),KL惩罚很容易主导整个奖励信号。
我遇到过一个极端案例:训练一个生成SQL查询的模型,输出长度平均在200个token左右。KL系数设的是0.02,参考模型是SFT模型。训练到第100步左右,模型开始输出极短的SQL,有时候甚至只输出一个SELECT就停了。排查后发现,KL惩罚在长序列上累积太大,模型为了降低惩罚,学会了“少说话”——输出越短,KL项越小。
这个问题在PPO里不太常见,因为PPO的优势函数是逐步计算的,KL项的影响被GAE平滑掉了。但GRPO的组内归一化优势是序列级别的,KL项直接加在每个token的奖励上,累积效应非常明显。
3.2 长度归一化的两种实现方式
解决这个问题的常见做法是对KL惩罚项做长度归一化。具体来说,有两种实现方式:
第一种是除以序列长度。把累积的KL惩罚除以输出token数,得到平均每个token的KL惩罚。这样做的好处是不同长度的序列在KL项上可比,模型不会因为输出短就占便宜。但缺点是短序列的KL惩罚被放大了,可能导致模型在短输出上过于保守。
第二种是设置KL惩罚的上限。比如当累积KL超过某个阈值时,直接截断,不再累加。这种做法更粗暴,但实际效果往往不错。我在代码生成任务里用的是上限截断,阈值设为任务奖励的0.5倍左右,训练稳定性明显提升。
两种方式各有优劣,我的经验是:如果任务输出长度差异很大(比如有的输出10个token,有的输出500个token),用长度归一化;如果输出长度相对集中,用上限截断更简单直接。
3.3 实测对比:不同归一化策略的训练曲线
为了更直观地说明问题,我跑了一组对比实验。任务是用GRPO训练一个模型做数学应用题,输出长度在50到300个token之间。三组实验分别用无归一化、长度归一化、上限截断三种策略,KL系数都设为0.01,训练步数1000步。
| 策略 | 最终准确率 | 训练稳定性 | 平均输出长度 |
|---|---|---|---|
| 无归一化 | 42% | 差,后期崩溃 | 80 token |
| 长度归一化 | 58% | 好 | 150 token |
| 上限截断 | 61% | 好 | 180 token |
从结果看,无归一化的方案在300步左右就开始崩,模型输出越来越短,准确率也上不去。长度归一化和上限截断都能稳住训练,但上限截断的最终准确率更高,因为模型没有被过度约束,能保持较长的推理链。
这个实验也说明了一个问题:KL惩罚项的设计不是孤立的,它和任务奖励的尺度、输出长度分布、参考模型的质量都有关系。没有一套参数能通吃所有任务,必须根据实际情况调。
4. 自适应KL系数:让惩罚项自己找平衡
4.1 固定KL系数的局限性
固定KL系数最大的问题是:训练初期模型偏离参考模型不多,KL惩罚很小,约束几乎不起作用;训练后期模型逐渐偏离,KL惩罚变大,可能突然把模型拉回来,导致训练震荡。这种“前松后紧”的现象在GRPO里特别明显,因为组内归一化优势会让模型在早期就产生较大的策略更新。
我试过在训练初期用较小的KL系数(0.001),后期逐渐增大到0.02,但手动调度的节奏很难把握。调快了模型学不动,调慢了模型已经跑偏了。
4.2 基于KL目标值的自适应调节
更优雅的做法是设定一个目标KL值,然后根据实际KL值动态调整系数。具体逻辑是:如果当前KL散度超过目标值,就增大KL系数;如果低于目标值,就减小系数。这样KL散度会被控制在一个合理范围内,既不会约束太紧,也不会放得太松。
目标KL值怎么设?我的经验是看参考模型和策略模型的初始KL。在训练开始前,先跑一批数据,计算策略模型和参考模型的平均KL散度,然后把这个值乘以一个系数(比如2到3倍)作为目标。这样目标KL既不会太苛刻,也能给模型一定的探索空间。
自适应调节的更新频率也很重要。更新太频繁会导致系数震荡,更新太慢又跟不上训练动态。我一般每10到20个训练步更新一次,用最近一批数据的平均KL作为依据。
4.3 自适应方案在数学推理任务上的表现
我在一个数学推理任务上对比了固定KL系数和自适应KL系数。固定系数设为0.01,自适应方案的目标KL设为初始KL的2.5倍。训练1000步后,自适应方案的准确率比固定系数高了约7个百分点,而且训练曲线更平滑,没有出现明显的震荡。
自适应方案还有一个好处:它对不同任务的泛化性更好。固定系数需要针对每个任务重新调,而自适应方案只需要设一个目标KL,剩下的交给算法自己调节。这对于需要快速迭代多个任务的场景非常实用。
不过自适应方案也有代价:它需要额外计算KL散度并维护一个调节器,实现复杂度比固定系数高。如果你的训练框架不支持,可能需要自己写一些回调逻辑。
5. 那些让我半夜爬起来改代码的坑
5.1 KL系数设为0的后果
有一次为了排查一个奖励异常的问题,我临时把KL系数设成了0,想看看纯任务奖励下模型会怎么学。结果训练到第50步左右,模型开始输出完全无关的内容,有时候甚至输出乱码。原因是没有任何约束,策略模型迅速偏离了参考分布,进入了奖励模型没有覆盖的区域,导致奖励信号完全失真。
这个坑让我意识到,KL惩罚项不仅仅是“防止过拟合”的正则化,它还是维持模型输出合理性的底线。即使任务奖励设计得很好,没有KL约束,模型也可能找到奖励模型的漏洞,输出人类看不懂但奖励很高的内容。
5.2 参考模型和策略模型初始化不一致
另一个坑是参考模型和策略模型的初始化不一致。有一次我在加载模型时,策略模型用了新的权重,但参考模型还是旧的SFT权重,导致初始KL散度就很大。训练开始后,KL惩罚项直接爆表,模型完全学不动。
排查这个问题花了我不少时间,因为表面上看训练loss在下降,但准确率一直不涨。后来打印了初始KL散度才发现问题。所以每次训练前,一定要确认参考模型和策略模型的初始权重是一致的,或者至少KL散度在合理范围内。
5.3 梯度累积与KL惩罚的交互
GRPO训练通常需要较大的batch size,但显存有限时只能用梯度累积。梯度累积和KL惩罚的交互有个细节:KL惩罚是在每个micro-batch上计算的,如果不同micro-batch的输出长度差异很大,累积后的KL惩罚可能会有偏差。
我遇到过一次训练不稳定,排查后发现是梯度累积的micro-batch之间输出长度差异太大,导致KL惩罚的方差很高。后来改成按长度排序后再分micro-batch,训练稳定性明显改善。这个细节在文档里很少提到,但实际训练中很容易碰到。
5.4 奖励尺度与KL系数的匹配
最后一个坑是奖励尺度和KL系数的匹配。如果任务奖励的范围是0到1,KL系数用0.01没问题;但如果任务奖励的范围是0到100,KL系数还是0.01,那KL惩罚就完全被淹没了。反过来,如果任务奖励范围是0到0.1,KL系数用0.01,惩罚项就会主导训练。
我的做法是先把任务奖励归一化到0到1之间,然后再调KL系数。这样不同任务之间的KL系数可以复用,不用每次重新调。归一化的方式可以是除以最大奖励,或者用running mean做标准化。
6. 一套可复用的KL惩罚项配置模板
6.1 参数配置清单
基于上面的经验,我整理了一套GRPO训练中KL惩罚项的配置模板。这套模板在数学推理、代码生成、文本摘要三个任务上都跑通过,可以直接作为起点。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| KL系数初始值 | 0.005 | 从较小值开始,避免早期约束过紧 |
| KL系数上限 | 0.05 | 防止自适应调节时系数过大 |
| KL系数下限 | 0.0005 | 防止系数过小导致约束失效 |
| 目标KL | 初始KL的2到3倍 | 训练前先跑一批数据计算 |
| 自适应更新频率 | 每15步 | 平衡响应速度和稳定性 |
| KL惩罚归一化 | 长度归一化或上限截断 | 根据输出长度分布选择 |
| 参考模型更新 | 冻结(默认) | 除非任务需要,否则不动态更新 |
6.2 训练前检查清单
在开始训练前,建议按这个清单过一遍:
- 确认参考模型和策略模型初始权重一致,初始KL散度在合理范围(通常小于1)。
- 确认任务奖励已归一化到0到1之间,或者至少尺度可控。
- 确认KL惩罚的归一化策略与输出长度分布匹配。
- 确认梯度累积的micro-batch之间输出长度差异不大,必要时按长度排序。
- 确认训练日志中记录了KL散度和KL系数的变化,方便排查问题。
6.3 训练中的监控指标
训练过程中,除了常规的loss和准确率,建议重点监控这几个指标:
- 平均KL散度:如果持续上升且不收敛,说明KL系数太小或目标KL设得太高。
- KL系数变化:自适应方案下,系数应该在合理范围内波动,不应该单调上升或下降。
- 平均输出长度:如果输出长度突然变短,可能是KL惩罚累积过大。
- 奖励分布:如果奖励方差突然变小,可能是模型输出趋同,KL约束过紧。
这些指标不需要每步都看,但每隔几十步检查一次,能提前发现大部分问题。
7. 关于KL惩罚项的一点个人体会
KL散度惩罚项在GRPO里看起来只是一个小参数,但它实际上是整个训练稳定性的调节阀。我见过太多人把注意力放在奖励模型和采样策略上,最后因为KL系数没调好导致训练失败。这个参数不值得花80%的时间去调,但至少值得花20%的时间去理解它的行为。
我现在的习惯是:每次换新任务,先用默认配置跑200步,观察KL散度和输出长度的变化,然后再决定要不要调KL系数。大部分情况下,默认配置加上长度归一化就能跑得不错。真正需要精细调节的场景,往往是任务奖励设计本身有问题,KL系数只是把问题暴露出来了。
另外,不要迷信某个“最优KL系数”。同一个任务,换一个参考模型或者换一批训练数据,最优系数可能就变了。理解KL惩罚项的作用机制,比记住某个具体数值重要得多。