13. PPO 详解
Proximal Policy Optimization (PPO) 是 RLHF 在工业界落地的"标准件"。InstructGPT、ChatGPT、Claude、LLaMA-2、GPT-4 都用 PPO 完成 RL 阶段。本章从策略梯度定理开始一步步推导,覆盖 GAE、重要性采样、PPO-Clip、4 模型架构和工程稳定性技巧。这是整个对齐部分数学密度最高的一章——但只要走完一遍推导,后续所有 RL 类对齐算法(GRPO、RLOO、ReMax 等)都是它的变体。
13.1 策略梯度定理
13.1.1 RL 目标
强化学习的核心目标:找到使期望累积奖励最大的策略:
其中
我们想要:
最自然的想法是梯度上升:
但这里有一个根本困难:期望对
13.1.2 推导 ∇J
设轨迹概率:
其中
注意:环境动力学梯度被消掉了,这是 RL 的关键好处。
利用 log-derivative trick:
代入
这就是 策略梯度定理 (Policy Gradient Theorem) 的雏形(Williams, 1992 的 REINFORCE 形式)。
13.1.3 时间因果性:reward-to-go
注意到
定义状态-动作值函数
13.1.4 引入基线降低方差
REINFORCE 的方差非常高。可以减去任意只依赖
为什么减基线不改变期望
对任意
而:
所以减基线不改变期望,但减小方差。
最优基线
理论上最优基线是
这是策略梯度的实用形式。所有现代算法(A2C/A3C/TRPO/PPO/GRPO)都基于此式。
13.2 重要性采样:从 on-policy 到 off-policy
13.2.1 问题:数据浪费
REINFORCE 是严格 on-policy 的:用
- 每次 rollout 要生成上千 token,开销极大;
- 数据只用一次太浪费;
- 我们希望"采一次样,更新很多次"。
解决思路:重要性采样 (Importance Sampling)。
13.2.2 重要性比率
考虑要估计
应用到策略梯度,把"目标策略
利用
所以可以把目标写成:
重要性比率:
显然
13.2.3 重要性采样的危险
理论上 IS 无偏,但实际上方差可能爆炸。当
- 比率
可能极大(如 100)或极小(如 0.001); - 极少数高比率样本主导梯度,方差飙升;
- 等价于"训练不稳定"。
为此需要约束
13.3 TRPO 与 PPO
13.3.1 TRPO:硬 KL 约束
Trust Region Policy Optimization (Schulman et al., 2015) 在
求解需要二阶方法(计算 Fisher 信息矩阵 + 共轭梯度),实现复杂。
13.3.2 PPO-Penalty:软 KL 惩罚
把 KL 当作惩罚项加进目标:
并用 adaptive
13.3.3 PPO-Clip:裁剪比率(主流)
Schulman et al. (2017) 提出的 PPO-Clip 直接裁剪比率到
典型
直觉:为什么这个 min 能限制更新
考察单个
情况 1:
| 区域 | |||
|---|---|---|---|
| 小 | |||
| 大 |
情况 2:
| 区域 | |||
|---|---|---|---|
| 大( | |||
结论:
时,限制 不能涨过 ; 时,限制 不能跌过 ; - 但反方向不限:好动作的
可以无上限地跌(梯度向上推),坏动作的 可以无上限地涨(梯度向下压);这是"悲观下界 (pessimistic bound)"——只在偏离扩大时才生效。
图示
A > 0 时 J^CLIP 关于 r 的图:
J
│
│ ___________ ← (1+ε)A 之后被截
│ /
│ /
│ /
│_____/_______________ r
1-ε 1 1+ε
A < 0 时 J^CLIP 关于 r 的图:
J
│________________
│ \
│ \
│ \ ← 1-ε 之前被截
│___________________\___ r
1-ε 1 1+ε13.3.4 PPO-Clip 完整目标
实际 PPO 还包括值函数损失和(可选)熵奖励:
其中:
是 GAE 派生的目标回报; (典型 0.5 或 0.1):value loss 权重; (典型 0.01):熵正则权重,鼓励探索; 是策略熵。
13.4 Generalized Advantage Estimation (GAE)
13.4.1 优势估计的两个极端
回到 §13.1.4:我们需要估计
蒙特卡洛 (MC):
无偏,但方差极高(依赖整条轨迹)。
1-step TD:
方差小,但
更一般的
13.4.2 GAE:指数加权平均
Schulman et al. (2016) 的 GAE 用指数加权融合所有
其中
推导:为什么指数加权能融合所有 n-step
定义
代入
调换求和顺序(让
得证。
边界情况
: (1-step TD,高偏低方); : ,可以化简为 (蒙特卡洛减基线,无偏高方); :在两者之间平滑插值。
13.4.3 LLM 中的 GAE
LLM 场景下回答有限长度(典型 ≤ 4K token),常取:
(不折现,关心整段回答的总质量); (接近 1,偏 MC,因为 V 训练慢且稀疏 reward)。
实现:递推计算
GAE 可以反向递推 O(T) 计算:
def compute_gae(rewards, values, gamma=1.0, lam=0.95):
"""
rewards: [T] (per-token rewards)
values: [T+1] (V(s_t) for t=0..T,最后一项为 0 或 bootstrapping value)
返回 advantages [T],returns [T]
"""
T = len(rewards)
advantages = torch.zeros_like(rewards)
last_gae = 0.0
for t in reversed(range(T)):
delta = rewards[t] + gamma * values[t+1] - values[t]
advantages[t] = last_gae = delta + gamma * lam * last_gae
returns = advantages + values[:-1]
return advantages, returns13.5 LLM-PPO:4 模型架构
13.5.1 角色与显存
把上述 RL 框架搬到 LLM 上,需要同时持有 4 个大模型:
| 模型 | 符号 | 角色 | 是否更新 | 备注 |
|---|---|---|---|---|
| Actor / Policy | 生成回答(rollout)+ 训练 | ✅ | 主梯度 | |
| Reference | 计算 KL 惩罚 | ❌(冻结 SFT) | 仅前向 | |
| Reward | 给完整回答打分 | ❌(冻结) | 仅前向 | |
| Critic / Value | 估计 | ✅ | 第二梯度 |
GPU 显存预算(以 7B actor 为例,FP16/BF16):
| 模型 | 参数 | 梯度 | 优化器状态 (Adam) | 总显存 |
|---|---|---|---|---|
| Actor (7B, BF16, AdamW) | 14 GB | 14 GB | 56 GB (FP32 m,v + master) | ~84 GB |
| Critic (7B) | 14 GB | 14 GB | 56 GB | ~84 GB |
| Reference (7B, frozen) | 14 GB | - | - | 14 GB |
| Reward (7B, frozen) | 14 GB | - | - | 14 GB |
| 激活值 (rollout 时) | - | - | - | 20-40 GB |
| 合计 | ~220-240 GB |
需要 ZeRO-3 + offload 才能在多卡 H100 上跑。
13.5.2 训练流程
┌──────────────┐
│ Prompt set │
└──────┬───────┘
│
▼
┌──────────────────────────────┐
│ Stage 1: Rollout (生成阶段) │
│ │
│ π_θ ─── generate ───► y │
│ │
│ π_ref ─── forward ───► log p_ref(y|x)
│ │
│ r_φ ─── forward ───► r(x,y)
│ │
│ V_ψ ─── forward ───► V(s_t)
│ │
│ compute per-token rewards │
│ compute GAE → Â_t, R̂_t │
└──────────────┬─────────────────┘
│
▼
┌──────────────────────────────┐
│ Stage 2: Optimize (更新阶段) │
│ │
│ for ppo_epoch in K: │
│ for minibatch B: │
│ L = L_clip + cv·L_v - ce·H│
│ θ ← θ - α·∇L │
│ ψ ← ψ - α·∇L │
└──────────────┬─────────────────┘
│
▼
(loop back to next iter)13.5.3 Token-level reward 写法
把 RM 的稀疏标量奖励
每个 token 都有 KL 惩罚,只有最后一个 token 拿到 RM 奖励。
13.5.4 完整伪代码
def llm_ppo_iteration(prompts, π_θ, V_ψ, π_ref, r_φ, β, ε, γ, λ):
"""LLM-PPO 一次迭代"""
# ---------- Stage 1: Rollout ----------
π_θ_old = copy_params(π_θ) # 保存 old policy 用于比率
rollouts = []
for x in prompts:
# 用旧策略生成回答
y, logp_old = π_θ_old.generate(x, return_logprob=True)
# 三个冻结模型前向
with torch.no_grad():
logp_ref = π_ref.forward(x, y) # [|y|]
r_score = r_φ(x, y) # scalar
v_t = V_ψ(x, y) # [|y|]
# token 级 reward
kl_per_token = β * (logp_old - logp_ref) # 实现细节:用 logπ_θ vs logπ_ref
rewards = -kl_per_token.clone()
rewards[-1] += r_score # 最后 token 加 RM 分
# GAE
v_extended = torch.cat([v_t, torch.zeros(1)])
Â, R̂ = compute_gae(rewards, v_extended, γ, λ)
rollouts.append({
"x": x, "y": y, "logp_old": logp_old,
"Â": Â, "R̂": R̂
})
# advantage 归一化(whitening)
all_Â = torch.cat([r["Â"] for r in rollouts])
Â_mean, Â_std = all_Â.mean(), all_Â.std()
for r in rollouts:
r["Â"] = (r["Â"] - Â_mean) / (Â_std + 1e-8)
# ---------- Stage 2: Optimize ----------
for epoch in range(K):
for batch in make_minibatches(rollouts, batch_size):
logp_new = π_θ.forward(batch["x"], batch["y"])
v_new = V_ψ(batch["x"], batch["y"])
ratio = torch.exp(logp_new - batch["logp_old"])
# PPO-Clip
surr1 = ratio * batch["Â"]
surr2 = torch.clamp(ratio, 1-ε, 1+ε) * batch["Â"]
L_clip = -torch.min(surr1, surr2).mean()
# Value loss (with optional clipping)
v_clipped = batch["v_old"] + (v_new - batch["v_old"]).clamp(-ε, ε)
L_v1 = (v_new - batch["R̂"]) ** 2
L_v2 = (v_clipped - batch["R̂"]) ** 2
L_v = 0.5 * torch.max(L_v1, L_v2).mean()
# Entropy bonus
entropy = -(logp_new * logp_new.exp()).sum(-1).mean() # 简化
L = L_clip + 0.1 * L_v - 0.01 * entropy
optimizer.zero_grad()
L.backward()
torch.nn.utils.clip_grad_norm_(params, 1.0)
optimizer.step()13.6 训练稳定性技巧
LLM-PPO 是出了名的"调参炼金"——这里整理工业实现中最有效的稳定性技巧。
13.6.1 Reward 处理
Reward whitening(批内归一化):
降低 RM 分布漂移带来的影响。
Reward clipping:限制极端值
防止 outlier 拉爆梯度。InstructGPT 与 TRL 默认开启。
13.6.2 Advantage 处理
Advantage whitening(强烈推荐):
让 advantage 尺度与 clip 阈值
13.6.3 Value clipping
类似 policy clipping,对 value 也加裁剪:
避免 critic 大幅震荡。OpenAI baselines 与 TRL 默认开启。
13.6.4 Reference model 周期更新
DeepSeek-V3 等大模型采用:每
- 当
已经远离原 SFT 时,KL(π_θ||π_ref_old) 变得过大、过紧; - 重锚后可以继续优化;
- 类似 target network 在 DQN 中的作用。
13.6.5 Adaptive KL controller
InstructGPT 使用 PI 控制器锁定目标 KL:
class AdaptiveKLController:
def __init__(self, init_β=0.2, target_kl=6.0, K_p=0.1):
self.β = init_β
self.target = target_kl
self.K_p = K_p
def update(self, current_kl, n_steps=1):
proportional = (current_kl - self.target) / self.target
proportional = max(-0.2, min(0.2, proportional))
self.β *= 1 + self.K_p * proportional * n_steps13.6.6 Mini-batch + 多 epoch
PPO 的核心优势是数据复用:
- rollout batch:一次生成 256-1024 prompts × 各 N 个回答;
- ppo_epochs:在同一批 rollout 上做 2-4 次更新;
- mini_batch:每个 epoch 内分成更小的 batch(典型 32-64)多次更新。
经验:
- ppo_epochs > 4 容易 overfit 当前 rollout;
- mini_batch 太小则方差大;
- 总 update steps = ppo_epochs × (rollout_size / mini_batch)。
13.6.7 Gradient clipping
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)防止偶发大梯度毁掉模型。RLHF 必备。
13.6.8 Mixed precision 与 ZeRO
4 模型显存压力极大,需要:
- BF16/FP16 训练;
- ZeRO-3(参数、梯度、优化器状态分片);
- Gradient checkpointing(用计算换显存);
- CPU offload(把 reference / reward 模型放 CPU 或别的 GPU);
- vLLM / SGLang for rollout:用专门推理引擎做生成阶段。
13.7 工程参考:TRL 的 PPOTrainer
Hugging Face TRL 是最流行的开源 RLHF 实现。PPOTrainer 关键默认值:
| 超参 | 默认 | 说明 |
|---|---|---|
learning_rate | 1.41e-5 | actor 学习率 |
mini_batch_size | 1 | per-GPU |
batch_size | 256 | 全局 rollout |
ppo_epochs | 4 | 每批 rollout 的更新次数 |
cliprange | 0.2 | |
cliprange_value | 0.2 | value clip |
gamma | 1.0 | 折扣 |
lam | 0.95 | GAE |
vf_coef | 0.1 | value loss 权重 |
init_kl_coef | 0.2 | |
target_kl | 6.0 | adaptive controller 目标 |
whiten_rewards | True | reward 归一化 |
调用流程:
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
config = PPOConfig(
model_name="meta-llama/Llama-2-7b-hf",
learning_rate=1e-5,
batch_size=128,
ppo_epochs=4,
)
# Actor + Critic 共享 backbone(用 ValueHead 给 actor 加一个 value head)
model = AutoModelForCausalLMWithValueHead.from_pretrained(
"path/to/sft_model")
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(
"path/to/sft_model") # 参考模型
reward_model = ... # 自己加载 RM
ppo_trainer = PPOTrainer(config, model, ref_model, tokenizer)
for batch in dataloader:
# Stage 1: rollout
queries = batch["input_ids"]
responses = ppo_trainer.generate(queries, **gen_kwargs)
# Stage 2: 计算奖励
rewards = reward_model(queries + responses)
# Stage 3: PPO 更新
stats = ppo_trainer.step(queries, responses, rewards)
ppo_trainer.log_stats(stats, batch, rewards)更复杂的多机训练用 OpenRLHF / veRL:通过 Ray 调度,把 rollout 用 vLLM、训练用 PyTorch FSDP,实现高吞吐。
13.8 PPO 的常见问题与诊断
| 现象 | 可能原因 | 解决 |
|---|---|---|
| 训练初期 loss 爆炸 | RM 输出尺度太大、未归一化 | reward whitening、clipping |
| KL 飙升不可控 | adaptive KL、降 lr | |
| Value loss 不下降 | critic 学习率太低 / 初始化差 | warm-up critic、提 vf_coef |
| 输出退化(重复短语) | reward hacking | 早停、加 KL、RM ensemble |
| 训练显存 OOM | 4 模型 + 长序列 | ZeRO-3、offload、shorter rollout |
| Rollout 太慢 | 单卡 generate 慢 | vLLM 接入、多卡推理 |
| 学不动 | 调 |
经验法则:先看 KL,再看 value loss,再看 reward。KL 是 RLHF 的"温度计"。
13.9 PPO 的局限与替代
PPO 在 LLM 上虽然有效,但代价巨大:
| 问题 | 程度 | 替代方案 |
|---|---|---|
| 4 模型显存 | 严重 | DPO(无 RM、无 critic)、GRPO(无 critic) |
| 调参复杂(10+ 超参) | 严重 | DPO(仅 |
| Rollout 慢(生成阶段) | 显著 | DPO(无需 rollout) |
| 训练不稳定 | 显著 | DPO/IPO 的 closed-form |
| Reward hacking | 严重 | DPO + iterative、DAPO 等 |
后续章节会逐一介绍这些替代方案。但需要强调:PPO 仍是当前最强对齐能力的代表,OpenAI、Anthropic 至今主用。在数据足够、调参得当时,PPO + 大 RM 仍优于 DPO。
本章小结
- 策略梯度定理:
,是所有 RL 算法的起点; - 重要性采样:让 PPO 能用旧策略采样的数据多次更新,但需要约束策略变化;
- PPO-Clip:通过裁剪比率到
实现"软信任域",简单高效; - GAE:用指数加权融合所有
-step TD,平衡偏差-方差,实践 ; - 4 模型架构:actor / critic / reference / reward 同时驻留,显存压力是主要挑战;
- 稳定性技巧:reward/advantage whitening、value clipping、adaptive KL、reference refresh、grad clip 缺一不可;
- TRL/OpenRLHF/veRL 提供工业级实现,但调参依然是艺术。
思考题
为什么 PPO 用比率
而不是直接优化 (即 vanilla policy gradient)? 当 时 与 等价吗?请通过链式法则验证。 GAE 推导:完成
时 化简为蒙特卡洛减基线的过程:
提示:注意
- 工程题:某团队报告 PPO 训练中 KL 在前 100 步缓慢上升,第 150 步突然飙升 10×,loss 也炸了。请列举 3-5 种可能原因和对应的诊断/解决方法。如果你只能加一个监控量,你会选哪个?