做过大模型对齐训练的朋友应该都清楚,强化学习微调(RL Finetuning)和传统的 SFT 完全不是一回事。SFT 只要算 loss、回传梯度就行,但 RL 要把生成、奖励计算、优势估计、策略更新整个闭环串起来,任何一个环节出问题,训练都可能在几小时内炸掉。最近我在用 VeRL 框架跑大模型的 PPO 微调,绕来绕去发现最关键的其实不是网络结构、不是 reward model 怎么训,而是一堆看起来不起眼的配置参数。今天想重点聊其中一个:algorithm.use_pf_ppo。
这个参数一开始我根本没当回事,以为是框架内部自动处理的开关,结果有一次实验把它从默认值改掉之后,整个训练曲线的形态都不一样了。后来翻了源码、做了几组对照实验,才彻底搞清楚它背后的设计逻辑。这篇文章适合两类人:一类是刚接触 VeRL、用 PPO 微调模型但被各种参数搞得头晕的新手,另一类是已经在跑 RL 训练、但总觉得 KL 控制或者训练稳定性和预期不符、想排查配置问题的同学。我会从 PPO 的基本原理讲起,再把这个参数的开与关、怎么配、踩过的坑全部展开,尽量做到你读完就能照着调。
1. 先搞清楚 VeRL 和 RL 微调这件事
1.1 VeRL 框架到底解决了什么问题
VeRL 是一个面向大语言模型的强化学习训练框架,核心解决的是“大规模 PPO/GRPO 训练跑得动、跑得稳”这个问题。你如果在小模型上手动写过 PPO 就会发现,一个最简单的策略梯度更新,需要同时维护 actor 模型、reference 模型、reward model,甚至还有 critic 模型。模型一多,显存立刻就不够用了。VeRL 这类框架做的事就是把 rollout(采样生成)、reward 计算、训练更新这些阶段拆开,用混合并行、分片、重计算等手段把大模型 RL 训练变成一个可配置的流水线。
我个人的体感是,VeRL 在设计上非常有“工程化”味道。它不像某些实验框架那样把算法和模型绑得很死,而是把策略模型、奖励模型、参考模型都抽象成可插拔的组件,训练算法则通过一个配置文件里的 algorithm 字段来切换。正是这种灵活性,导致配置项特别多,而且每个参数背后几乎都藏着一个 RL 训练里的经典问题。
1.2 为什么 PPO 微调比 SFT 难那么多
SFT 的本质是最大似然估计:给我一批 prompt 和期望输出,让模型学会模仿。但 RL 微调的目标是“在给定 reward signal 的情况下,让模型的输出策略不断优化”。这里有几个绕不开的麻烦:
第一,生成是离散采样,梯度传不回去。所以要用策略梯度类方法,通过 log_prob 的比值来估计梯度方向。第二,强化学习的奖励信号通常很稀疏,而且容易被模型钻空子,所以几乎所有实用的 RLHF 实现都会加 KL 惩罚,让模型不要偏离 SFT 模型太远。第三,训练过程中模型自己在更新,而数据是模型自己采样的,分布飘来飘去,非常容易陷入奖励 hacking 或者策略崩溃。
第三点特别有意思,也是use_pf_ppo这类参数存在的根源。PPO 在做重要性采样时,需要拿到当前策略的 log_prob 和旧策略的 log_prob,才能算出 ratio。这个“旧策略的 log_prob”到底怎么来、存哪里、用什么模型算,就成了不同实现框架的分水岭。有的框架在 rollout 阶段就把 log_prob 存下来,训练时直接读取;有的框架则在训练阶段重新做一次前向计算。两者各有代价,也各有适合的场景。use_pf_ppo正是在这个岔路口上做一个选择。
2. use_pf_ppo 参数到底在控制什么
2.1 从 PPO 的目标函数说起
先快速回顾一下 PPO 的更新目标,这个公式看起来简单,但它是理解后面所有参数的基础:
L = E[ min( ratio * A, clip(ratio, 1-eps, 1+eps) * A ) - beta * KL ]其中ratio = π_new(a|s) / π_old(a|s),也就是新旧策略在同一个动作上的概率比值。计算 ratio 需要两个值:当前策略对新旧样本的 log_prob(π_new),以及采样时旧策略对样本的 log_prob(π_old)。
在实际工程里,π_old的处理方式非常影响训练架构。可以把它看成一对“快照”和“缓存”的问题。Rollout 时模型还是旧版本,采完样顺便算一下 log_prob,存下来,等训练时直接拿来当分母。这相当于在做题时提前把已知数填好,后面只用算分子。另一种做法是训练时把模型权重恢复到某个旧状态,再重新前向一次,这相当于每次做题都从头推导一遍,虽然更精确,但代价高。
VeRL 的use_pf_ppo参数,本质上就是在控制这种“旧策略 log_prob 的获取方式”以及“KL 惩罚的计算方式”。这是我基于配置结构和训练行为的理解,实际源码在不同版本里实现细节有差异,但思路是一脉相承的。
2.2 pf 的含义:policy function 的显式计算
参数名里的pf我理解为 policy function 的缩写。use_pf_ppo = true时,框架会走一条“显式策略函数”的路径:在 PPO 更新阶段,额外维护一份策略函数的前向计算逻辑,用它来算当前样本的 log_prob、KL 散度等。这个路径更接近“教科书版 PPO”,因为它的更新过程严格依赖策略函数的当前状态,对于策略变化比较敏感的场景会更稳定。
而use_pf_ppo = false时,相当于走更激进的“缓存复用”路线。Rollout 阶段保存的 log_prob 直接作为旧策略的依据,KL 惩罚也会以更轻量的方式合并进 reward,而不是显式建模。这个模式训练速度更快,但对 rollout 数据和训练数据的一致性要求极高,一旦模型更新后分布偏了,容易出现 ratio 异常大的情况,进而导致更新步长失控。
我用一个生活化的类比:显式策略函数模式像你每次做饭都自己尝咸淡,依据当前口味调整;缓存复用模式像提前记好配方,到点直接下料,速度快但配方一旦和实际不符就容易翻车。实际训练中,前者需要更多显存和计算,但 KL 控制更稳;后者吞吐更高,但更考验超参配合。
2.3 两种模式的差异对照
为了直观,我把两种模式的核心差异整理成一张表,实践时可以直接参考:
| 对比维度 | use_pf_ppo=true | use_pf_ppo=false |
|---|---|---|
| 旧策略 log_prob 来源 | 训练阶段显式前向计算 | 复用 rollout 阶段缓存 |
| KL 惩罚处理 | 偏向显式建模,配合 kl_ctrl 使用 | 偏向并入 reward,简化计算 |
| 计算开销 | 更高,多一次策略前向 | 更低,吞吐量更好 |
| 训练稳定性 | 通常更稳,策略漂移可感知 | 对超参更敏感,易出现 ratio 异常 |
| 显存占用 | 更高,需要预留额外激活内存 | 相对更低,适合资源受限环境 |
| 典型场景 | 长序列、大规模模型、要求稳定的场景 | 快速验证、算力紧张、短序列场景 |
需要注意,这个表描述的是两个模式在常见配置下的相对行为,不是绝对的性能差距。不同版本、不同模型规模下,差异幅度会有变化。我建议以你自己跑一版的曲线为准,不要盲信任何默认值。
3. 配置实操:参数组合与调参建议
3.1 一份可以抄的 YAML 配置示例
VeRL 的配置是按照actor_rollout_ref、algorithm、reward_model这类模块来组织的。use_pf_ppo位于algorithm下。下面是我实际跑过的一份 PPO 微调配置片段,模型是 7B 级别,训练数据是数学推理类任务:
actor_rollout_ref: model_path: /path/to/sft_model rollout_provider: vllm rollout_batch_size: 1024 temperature: 0.7 max_tokens: 2048 use_dynamic_bsz: true algorithm: use_pf_ppo: true adv_estimator: gae use_kl_in_reward: false kl_ctrl: type: adaptive horizon: 10000 target_kl: 0.02 init_kl_coef: 0.05 min_kl_coef: 0.001 max_kl_coef: 5.0 reward_model: model_path: /path/to/reward_model reward_batch_size: 512 training: trainer: fsdp micro_batch_size: 4 gradient_accumulation_steps: 8 lr: 1.0e-6 lr_scheduler: cosine total_episodes: 20000 save_freq: 2000这段配置里有几个关键配合关系值得说。use_pf_ppo: true让我启用了显式策略函数路径,同时我把use_kl_in_reward关掉,让 KL 惩罚独立于奖励计算,配合kl_ctrl里的自适应系数来控制偏离程度。adv_estimator: gae则是用广义优势估计来算 advantage,适合 rewards 密度较高、需要兼顾长远回报的场景。
如果切换成use_pf_ppo: false,我一般会把use_kl_in_reward设为 true,把 KL 系数设置成一个小值,比如0.01到0.05,并且关闭或放宽自适应 KL 控制。这样做的好处是训练速度快,缺点是一旦 reward 模型的分数波动剧烈,模型很容易跑偏。新手我建议先用use_pf_ppo: true跑通整个流程,再尝试关闭路径优化吞吐。
3.2 两个配置之间如何平滑切换
改配置不是改一个字段就完事。我做过几次对比实验,发现从默认开启切到关闭时,训练状态几乎不能用,因为 optimizer 的状态、rollout 缓存里的 log_prob 字段、甚至 KL 控制器的内部统计量都是按不同逻辑维护的。所以我的经验是:不要把两个模式混在一个训练 run 里,要么全开、要么全关。如果非要对比,就分开两个实验,保持其他超参完全一致。
另外,切换模式之后,学习率最好也重新调。显式策略函数模式因为多了一次前向,等效更新步数更扎实,可以用稍大一点的 lr;缓存复用模式建议把 lr 调低 30% 到 50%,防止 ratio 突变导致更新步长过大。我自己在 7B 模型上的经验是:true 模式 lr 用 1e-6,false 模式降到 5e-7 左右,KL 曲线才比较平稳。
3.3 和其他参数的配合关系
use_pf_ppo不是独立起作用的,它至少和三个参数强相关:
第一个是kl_ctrl.type。如果是 adaptive 模式,框架会根据 target_kl 自动调整 KL 系数,这在显式策略函数模式下效果很好,因为 KL 算得准。但如果用缓存复用模式,KL 本身近似度高,自适应控制容易反应过激,建议改成 fixed 模式,固定一个当前经验下安全的小系数。
第二个是use_kl_in_reward。这个参数决定 KL 惩罚是加在 reward 里,还是单独算 loss。和use_pf_ppo搭配时,我推荐 true + no-extract-KL 的组合,意思是开显式策略函数,让 KL 单独作为 loss 项;或者 false + in-reward 的组合,让一切从简。交叉搭配不是不行,但调试成本高。
第三个是adv_estimator。GAE 的计算依赖 advantage 的平滑度,如果use_pf_ppo开启后策略更新更精细,GAE 的优势估计会更稳定;如果关闭,ratio 容易出现尖刺,GAE 的 lambda 建议调低一点,比如从 0.95 降到 0.9,削弱远步收益的贡献。
4. 训练中的典型问题与排查实录
4.1 显存 OOM,是不是 use_pf_ppo 的锅
有朋友问过我,为什么开了use_pf_ppo之后显存爆了。这个锅一半要参数背,另一半其实是自己的配置问题。开启显式策略函数模式会额外引入一次完整的前向计算,激活值显存占用自然更高。尤其是在长序列、大 batch 场景下,OOM 很容易出现。
我排查 OOM 的顺序是:先看 rollout 阶段的显存峰值,再看训练阶段。如果是训练阶段 OOM,优先开 activation checkpointing,把激活值重计算打开;其次减小micro_batch_size,把梯度累积步数调大;实在不行再考虑关闭use_pf_ppo。但注意,关掉参数属于“降级方案”,别一遇到 OOM 就这么干,否则模型的最终对齐效果可能变差。
4.2 KL 曲线爆炸或直接归零
KL 曲线爆炸是我在关闭use_pf_ppo后遇到的第一个坑。现象是训练刚开始几百步,KL 从 0.02 直接飙到 2 以上,接着 reward 也开始乱跳。这个问题的根源在于缓存复用的 log_prob 分布和当前策略已经出现较大偏差,导致 ratio 异常大,策略更新一步迈了平时好几步的距离。
解决方法有三个方向,按优先级排序:第一,把学习率降下来,至少降一半;第二,把 KL 系数提高,或者打开kl_ctrl的自适应模式兜底;第三,检查 rollout 的采样参数,把 temperature 降低一些,减少策略分布的极端值。还有一个小技巧,就是看同一份 rollout 数据在 PPO 更新中被 reuse 的次数。如果框架支持 epoch 数配置,优先把它降下来,比如从 1 个 epoch 改成 1 次更新就换新数据。
相反的情况是 KL 直接归零,这通常不是好事,意味着策略已经完全停止更新。我会检查 reward 的方差,如果 reward 全部趋同,advantage 几乎没有区分度,模型梯度接近于零。这时要回头检查 reward model 的输入输出,看看是不是 reward 被错误地 scale 到零附近了。
4.3 奖励在涨但评测指标不涨
这是 RL 训练里最经典也最头疼的问题。训练 reward 曲线一路上升,但跑评测集时模型表现原地踏步,甚至变差。出现这种情况,先怀疑 reward hacking,也就是模型找到了奖励函数里的漏洞,而不是真正学会了推理。
use_pf_ppo在这里的关联是:如果我开启显式策略函数模式,KL 约束更强,模型不容易偏离 SFT 太远,奖励 hacking 的概率会低一些;如果关闭,模型自由度更高,更可能在奖励曲线上制造“虚假繁荣”。我的建议是在训练中期定期冻结 checkpoint 跑一次评测,不要只看训练 reward。一旦发现 reward 和评测指标背离,优先加强 KL 约束系数,并把 reward model 的输出做 clip,限制单步奖励的极端影响。
4.4 两种模式下的最佳实践清单
根据我跑过的对照实验,最后整理一个实践清单,算是给新手的“避坑指南”:
- 第一次跑通流程,直接用默认的
use_pf_ppo: true,不要一上来就优化速度。 - 开
use_pf_ppo时,KL 控制用 adaptive 模式,target_kl 从 0.01 到 0.05 之间起步。 - 关
use_pf_ppo时,学习率打对折,KL 用固定小系数,reward 做 clip。 - 无论开关,都要在 rollout batch 和训练 batch 之间保持稳定的比例,一般 rollout batch 是训练 batch 的 4 到 8 倍。
- 保存 checkpoint 时记录 KL 系数、advantage 均值和 reward 均值,这些是判断训练状态最直接的特征。
- 遇到任何诡异现象,先做“控制变量”,把模型规模、序列长度、数据分布这些因素固定住,再单独切换参数。
5. 用一组快速实验验证参数行为
5.1 最小化对照实验设计
知道了理论,不如亲手跑一组对照实验。我在本地用一个 1.5B 模型做过快速验证,数据集选了一个比较简单的指令遵从数据集,训练步数只跑 300 步,每 50 步记录一次 KL 和 reward。对比组就是use_pf_ppo开和关,其他配置全部一致。
实验记录如下:
| 训练步数 | true 模式 KL | false 模式 KL | true 模式 reward | false 模式 reward |
|---|---|---|---|---|
| 50 | 0.018 | 0.025 | 0.42 | 0.38 |
| 100 | 0.021 | 0.041 | 0.51 | 0.45 |
| 150 | 0.019 | 0.067 | 0.58 | 0.55 |
| 200 | 0.023 | 0.089 | 0.62 | 0.61 |
| 250 | 0.020 | 0.115 | 0.66 | 0.64 |
| 300 | 0.024 | 0.142 | 0.69 | 0.68 |
从数据里能看到非常明显的规律:关闭use_pf_ppo后,reward 也在涨,但 KL 的增速明显快于开启模式,这说明策略已经开始偏离参考模型,但 reward 信号还没暴露问题。如果训练拉长到几千步,这种隐性偏离大概率会演化成 KL 爆炸或者评测指标下滑。
5.2 实验结论和扩展到更大模型的注意事项
这组实验得出的结论是:use_pf_ppo = true更适合需要长期稳定训练、对最终对齐质量有要求的场景;而 false 模式在短任务、小模型、快速验证场景下完全够用,甚至因为吞吐高更有效率。
但这组实验在小模型上的结论,不能直接外推到 7B 甚至 70B。模型越大,显式策略函数前向的额外开销越明显,需要更谨慎地搭配 offload 和重计算。还有一点,长序列场景下,缓存复用模式对内存的节省非常显著,如果你做的是 8K 以上超长上下文微调,false 模式可能是唯一跑得动的选项。这时要在稳定性上做出妥协,用更强的 KL 设置来兜底。
6. 写在最后的一点个人体会
参数调来调去,我的体会是:use_pf_ppo本质不是一个“越开越好”或者“越关越好”的开关,而是框架在不同资源约束下给出的两种 PPO 落地姿势。它背后反映了模型并行训练中的一个永恒矛盾——计算精度和资源效率之间的取舍。
如果你正处于刚接触 RL 微调的阶段,别急着关掉它来提速。先开着跑几轮,把 reward 曲线、KL 曲线、checkpoint 评测结果这些基准数据攒起来,再去体验关闭模式。手头有了一组对照数据,你才能真正理解这个参数在训练流程中的分量。最后再说一个容易被忽略的细节:每次升级框架版本之后,最好重新看一眼默认配置,很多参数名看着没变,但背后的默认行为和实现路径可能已经换了,光靠默认值跑到底,容易踩到版本的暗坑。