DL S063 · RLHF with PPO — Intuition + Minimal Code
Understand the PPO loop even if you never run it. Part of the 'Deep Learning & LLMs From Scratch' 80-session series.
🎯 Understand the PPO loop even if you never run it.
Series: Deep Learning & LLMs From Scratch — 80 sessions · Session 63 / 80 · Module M10 · ~2 hours
The story
The algorithm that made ChatGPT — and that almost nobody uses anymore
RLHF with PPO is the algorithm that turned GPT-3.5 into ChatGPT. It's also, by 2024, largely being replaced by DPO (S064). So why spend a whole session on it?
Because (a) it's the historically correct answer to the alignment problem and understanding it makes every subsequent technique legible; (b) the PPO objective, KL penalty, and value baseline concepts show up in RLAIF, GRPO, RLHF-lite, and every future RL-style alignment method; (c) frontier labs still use PPO for the final polish — Anthropic's constitutional AI stack, OpenAI's later GPT-4 iterations, DeepMind's Gemini all have PPO in the loop somewhere.
Here's the puzzle: you have an SFT'd model (a policy πθ) and a reward model rφ. You want to update the policy to score higher under rφ. Naive approach: just do policy-gradient RL — sample from πθ, get rewards, ascend the gradient. Two things go wrong: (1) you catastrophically forget everything the SFT model knew, and (2) you reward-hack the RM into oblivion. PPO fixes both with two extra terms: a KL penalty against a frozen reference model and a clipped surrogate objective that bounds how far the policy moves per step.
That's the whole idea. Everything else is bookkeeping.
- Write down the PPO-KL objective: E[min(ratio · A, clip(ratio) · A)] − β · KL(π || π_ref).
- Explain what each of the four models does at PPO time: policy, reference, reward, value.
- Compute the KL penalty in per-token form and sketch why it prevents catastrophic reward hacking.
- Read a TRL PPOTrainer training log and know which of loss/kl/reward is a warning sign.
- Explain in 3 sentences why the field is moving to DPO — and what PPO still buys you.
Prerequisites
- Session 062 (reward modeling) — the RM is the whole engine of RLHF. If you don't have one, you can't do this.
- Session 058 (SFT) — the policy πθ is initialized from an SFT'd model, and the reference πref is a frozen copy of the same model.
- A little RL background helps but isn't required — I'll re-derive the policy gradient below.
1 · The setup — four models loaded at once
Here's the whole PPO-for-LLMs picture in one image:
Four models in GPU memory simultaneously:
- Policy πθ — trainable. This is what we're optimizing. Init from SFT model.
- Reference πref — frozen copy of the SFT model. Used to compute the KL penalty that keeps πθ from drifting away.
- Reward model rφ — frozen, from S062. Provides the scalar reward per response.
- Value model Vψ — trainable, small. Predicts expected return from each token position. Used as a baseline for variance reduction (this is the "V" in advantage-based policy gradient).
For a 7B policy in fp16, you're looking at ~14GB × 4 = 56GB before activations. This is why PPO with 70B models needs 8 × A100 minimum. This is also why LoRA-PPO exists (only the policy and value get LoRA-adapted; ref and RM stay frozen — memory drops to ~30GB).
1.1 The generation → scoring → update cycle
Each PPO iteration:
- Sample a batch of prompts from the training set.
- Generate responses from the policy πθ.
- Score each response with the reward model → per-response scalar r.
- Compute per-token log-probs under πθ and πref.
- Compute per-token KL: KL(πθ(·|x, y<t) || πref(·|x, y<t)).
- Combine per-token KL and terminal reward into a shaped per-token reward:
r_t = -β · KL_tfor t < T, andr_T = r(x, y) - β · KL_Tat the final token. - Compute advantages At using the value model Vψ (GAE — Generalized Advantage Estimation).
- Update πθ and Vψ for 2–4 mini-epochs on this batch with the PPO clipped objective.
- Go to 1.
That's it. The whole algorithm.
2 · The PPO clipped objective, unpacked
For each token position t, we have:
- Old log-prob under the policy at generation time:
log π_θ_old(y_t | x, y_<t) - New log-prob after some update steps:
log π_θ(y_t | x, y_<t) - Advantage At (how much better this token turned out than expected)
Define the probability ratio:
The vanilla policy gradient objective is E[ρ_t · A_t]. Ascend this gradient → policy shifts toward tokens with high advantage. Problem: nothing stops the update from making ρt huge, which can catastrophically overshoot.
PPO's fix — the clipped surrogate objective:
with ε typically 0.2. Reading this:
- If At > 0 (token was good): the objective is bounded above by (1+ε)·At. We can't gain more from this token past ρ = 1.2.
- If At < 0 (token was bad): the objective is bounded below by (1-ε)·At. We can't move too aggressively away either.
In effect, PPO says: "each policy update can only shift a per-token probability by at most a factor of 1±0.2 within an iteration." That trust-region-like bound is what makes PPO stable where vanilla policy gradient is chaos.
2.1 The KL penalty term
On top of the clipped objective, we subtract a KL penalty against the reference:
β is a scalar around 0.01–0.1. If β is too small, the policy drifts arbitrarily and reward-hacks. If β is too big, the policy can't move away from the SFT distribution at all — reward doesn't improve.
Note: some PPO implementations bake the KL penalty into the reward (r_shaped = r − β·KL) rather than the loss. Mathematically equivalent under GAE with γ=1. TRL does it in the reward.
3 · Where the value model comes in
The value model Vψ(x, y<t) predicts the expected shaped reward from position t onward. Its job is variance reduction: instead of pushing the policy in proportion to raw reward R, we push it in proportion to the advantage At = Rt − Vψ(x, y<t) — "how much better this trajectory turned out than the value model expected."
Advantages have mean ≈ 0 and lower variance than raw returns, which drastically stabilizes training. Without a value baseline, PPO on LLMs is nearly impossible to tune.
GAE (Generalized Advantage Estimation) is a specific formula for computing At from the sequence of one-step temporal-difference errors, blended with a λ knob (typically 0.95). Full formula is out of scope for this session — TRL computes it automatically. Just know: it's a low-variance estimator of the advantage.
The value model itself is trained by regressing its output against the observed shaped returns:
Standard MSE. Trained jointly with the policy on the same batches.
3.1 Architectural note
The value model is usually implemented as a head on top of the policy — you take the same transformer backbone, add a Linear(d, 1) head parallel to the LM head, and train the value head. This shares almost all parameters with the policy, saving memory and warm-starting the value estimates from the policy's representations.
In TRL:
from trl import AutoModelForCausalLMWithValueHead
model = AutoModelForCausalLMWithValueHead.from_pretrained("mistralai/Mistral-7B-v0.3")This one line gives you a model with both the LM head (for generating and computing log-probs) and a value head (for GAE). Backprop flows through both.
4 · A skeletal PPO iteration in code
I won't write a full working PPO — TRL's PPOTrainer is 800 lines and depends on a rollout buffer, GAE machinery, gradient accumulation, etc. But here's the shape of one iteration, in ~50 lines of pseudo-torch:
# Given: policy (with value head), ref_model (frozen), reward_model (frozen)
# Given: batch of prompts
# ---- 1. Rollout ----
with torch.no_grad():
responses = policy.generate(prompts, max_new_tokens=256, do_sample=True)
log_probs_old = policy.compute_log_probs(prompts, responses) # (B, T)
log_probs_ref = ref_model.compute_log_probs(prompts, responses) # (B, T)
values_old = policy.value_head(prompts, responses) # (B, T)
rewards_end = reward_model(prompts, responses) # (B,) — terminal rewardPrepare per-token shaped rewards and advantages:
# ---- 2. Shape rewards + compute GAE ----
kl_per_token = log_probs_old - log_probs_ref # (B, T) — approx per-token KL
shaped_rewards = -beta * kl_per_token # penalty at every token
shaped_rewards[:, -1] += rewards_end # terminal reward at last token
advantages, returns = compute_gae( # standard GAE(λ=0.95)
rewards=shaped_rewards, values=values_old, gamma=1.0, lam=0.95
)
# normalize advantages for stability
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)The core PPO update — usually 2-4 mini-epochs over the same rollout batch:
# ---- 3. PPO update (2-4 mini-epochs) ----
for _ in range(ppo_epochs):
for minibatch in shuffle(...):
log_probs_new = policy.compute_log_probs(mb.prompts, mb.responses)
values_new = policy.value_head(mb.prompts, mb.responses)
# Policy loss (clipped surrogate)
ratio = torch.exp(log_probs_new - mb.log_probs_old)
obj1 = ratio * mb.advantages
obj2 = torch.clamp(ratio, 1 - eps, 1 + eps) * mb.advantages
policy_loss = -torch.min(obj1, obj2).mean()
# Value loss (MSE against returns, sometimes also clipped)
value_loss = 0.5 * (values_new - mb.returns).pow(2).mean()
# Entropy bonus (optional, encourages exploration)
entropy_bonus = -policy.entropy(mb.prompts, mb.responses).mean()
loss = policy_loss + vf_coef * value_loss + ent_coef * entropy_bonus
loss.backward(); optimizer.step(); optimizer.zero_grad()That's the whole loop. Repeat for 100–1000 iterations, tune β, ε, LR, batch size — and pray.
4.1 In practice: TRL PPOTrainer
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
cfg = PPOConfig(
learning_rate=1.4e-5,
batch_size=64, mini_batch_size=8, ppo_epochs=4,
cliprange=0.2, cliprange_value=0.2,
vf_coef=0.1, init_kl_coef=0.05, target_kl=6.0,
)
policy = AutoModelForCausalLMWithValueHead.from_pretrained("./sft-mistral")
ref = AutoModelForCausalLMWithValueHead.from_pretrained("./sft-mistral")
rm = AutoModelForSequenceClassification.from_pretrained("./rm-mistral")
trainer = PPOTrainer(cfg, policy, ref, tokenizer, reward_model=rm)
for batch in dataloader:
responses = trainer.generate(batch["prompt_ids"])
rewards = rm(batch["prompt_ids"], responses)
stats = trainer.step(batch["prompt_ids"], responses, rewards)
trainer.log_stats(stats, batch, rewards)That's the whole user-facing API. About 30 lines. All the ugly PPO plumbing is internal to .step().
5 · Reading a PPO training log (survival guide)
TRL logs many metrics per step. The three that matter:
ppo/mean_scores: mean reward. Should trend UP over training. If flat, β might be too high (policy can't move). If wildly spiking then crashing, LR too high.ppo/policy/policykl: mean KL between current policy and rollout policy (within one mini-epoch). Should stay small — <0.02 per step. Big → clipping isn't containing the update, drop LR or increase clip strength.objective/kl: KL between current policy and reference. Should grow slowly over training. If it explodes past target (default 6.0), TRL's adaptive KL controller will crank β up automatically. If β saturates high and KL still grows, you're reward-hacking.
Rules of thumb for a healthy PPO run:
- Reward grows monotonically for 100-500 steps then plateaus.
- KL(π || π_ref) grows to 5-15 and stabilizes.
- Policy loss oscillates around zero (positive means "we made things worse in the mini-epoch").
- Value loss decreases smoothly to a floor.
A team at a startup tried to RLHF a 7B model. They used LR = 1e-4 (a reasonable SFT LR). First 20 steps: reward climbed nicely from 0.3 to 0.9. Step 25: reward = 1.4. Step 30: reward = 3.8. Step 35: model output was 100% the string "answer:" repeated 100 times, reward = 8.2.
They had discovered a degenerate mode of the RM that assigned high reward to responses starting with "answer:" (because a subset of chosen responses in the RM training data did). The high LR let PPO race to exploit it before the KL penalty caught up.
Fix: LR = 1.4e-5 (10× lower — the TRL default), β = 0.1 (higher KL penalty), and — most importantly — first fix the RM by adding a length penalty and augmenting the pref data to break the "answer:" spurious correlation.
Cost of the lesson: 2 wasted PPO runs (~$80 each on rented A100 time) and 4 days.
Lesson: reward hacking is not a hypothetical. In every serious PPO run, the reward model gets exploited somehow within the first 500 steps. Watch the sample outputs, not just the numbers.
6 · Why the field is moving to DPO
PPO on LLMs is famously fiddly:
- Four models in memory simultaneously (56GB for a 7B fp16 policy).
- 15+ hyperparameters (LR, β, ε, γ, λ, clip ranges, mini-epochs, batch sizes, target KL, adaptive KL coefficients, entropy bonus, value function coefficient, ...).
- Rollouts are slow (generation dominates), and you need many iterations.
- Reward-hacking failures happen quietly — you notice on your eval set, not in the training loss.
- Debugging is opaque: a 5% degradation could be RM, KL, LR, or bad rollouts.
Rafailov et al. (Stanford, 2023) noticed something startling: you can derive a closed-form loss that turns the preference dataset directly into a training objective on the policy, with no RM, no rollouts, no value head, no PPO. That's DPO. Same theoretical foundation (Bradley-Terry + KL constraint), but algebraically manipulated into a supervised loss on preference pairs.
For most SFT teams in 2024, DPO gives 90-100% of the quality of PPO with 20% of the complexity. Which is why every open-source instruct model since Zephyr uses DPO, not PPO.
Where PPO still wins:
- Iterative RLHF — when you keep collecting fresh preferences during training. DPO's dataset is fixed at start-of-training.
- Reward shaping — if your reward is more than a single Bradley-Terry pref score (e.g., safety+helpfulness+conciseness combined), PPO handles it natively; DPO needs contortions.
- Frontier labs — anecdotally, at the very best-model tier, PPO's extra flexibility still buys measurable quality. It's why GPT-4-turbo and Claude 3 still have PPO in the recipe somewhere.
For your first alignment run: use DPO (S064). For your fifth: consider whether PPO is worth the complexity for your task.
7 · Diagram — one PPO iteration
8 · GRPO, RLVR, and the 2024–2025 revival of RL for LLMs
Section 6 ("Why the field is moving to DPO") was the 2023 conventional wisdom. Then in early 2025 the pendulum swung back toward RL — hard — because of two ideas: GRPO and verifiable rewards. Here's the update.
8.1 The DPO ceiling and why RL came back
By late 2024 the open community had thoroughly explored DPO/SimPO/ORPO and hit a ceiling: preference-tuned models were polished but plateaued on hard reasoning benchmarks (MATH, GPQA, competition code). Ivison et al. (2024) showed DPO couldn't push past ~40% on MATH even with unlimited preference data.
Meanwhile, OpenAI's o1 (Sept 2024) demonstrated that large-scale RL with chain-of-thought could crush MATH (94%) and Codeforces (89th percentile). The recipe wasn't public but the signal was clear: RL wasn't dead, it had been misapplied.
January 2025 changed the game: DeepSeek-R1 dropped, and its methods paper was public.
8.2 GRPO — Group Relative Policy Optimization (Shao et al., DeepSeek, 2024–2025)
GRPO is PPO without the value model. Here's the swap:
PPO advantage: where is a learned critic (a whole second model).
GRPO advantage: for each prompt, sample completions (say ). Compute reward for each. Set within the group. That's the advantage. No value model.
Why this works:
- The group mean is an unbiased baseline for the prompt (same as value model would estimate) but comes for free from sampling.
- Halves the memory footprint — you drop the value model, which was as big as the policy.
- Better with sparse/binary rewards — the value model in PPO struggled when rewards were mostly 0 with occasional 1; group normalization handles this natively.
DeepSeek-R1 used GRPO with completions per prompt. On DeepSeek-Math-7B, GRPO added +5.5 pts on GSM8K and +6.4 pts on MATH vs PPO with the same reward model. Faster wall-clock too because no critic training.
Further reading:
- DeepSeek-Math paper (GRPO intro): https://arxiv.org/abs/2402.03300
- DeepSeek-R1 report: https://arxiv.org/abs/2501.12948
8.3 RLVR — Reinforcement Learning with Verifiable Rewards
The other DeepSeek-R1 innovation: skip the reward model entirely for domains where the answer can be checked programmatically.
- Math: extract final numeric answer, compare to ground truth. Reward = 1 if match, 0 otherwise.
- Code: run generated code against unit tests. Reward = pass rate.
- Format: regex-check that reasoning is wrapped in
<think>...</think>tags. Reward = 1 if formatted correctly.
No human labels, no RM, no reward hacking (the verifier is unhackable in the way an RM isn't). Combined with GRPO this is what let DeepSeek-R1 match o1 on MATH and code with an open-source recipe.
Allen AI adopted the same recipe for Tulu-3-RLVR (the third stage after SFT + DPO). +3.2 pts on GSM8K, +7.1 pts on MATH beyond the DPO checkpoint. This is the current SOTA open recipe as of mid-2025.
8.4 R1-Zero — SFT-free RL
DeepSeek also showed R1-Zero: starting from a raw base model (no SFT!), you can jump straight to GRPO+RLVR and the model develops chain-of-thought reasoning entirely through RL. Emergent behaviors: self-verification ("wait, let me check that"), backtracking, considering alternatives.
R1-Zero was unreadable (mixed languages, weird formatting) so R1-proper added a small SFT warm-start. But the finding is deep: SFT teaches format, RL teaches capability. If you have verifiable rewards, RL can bootstrap capability without ever seeing a human demonstration.
8.5 PPO isn't dead — where it still wins
Even with GRPO's rise, classic PPO still dominates in three regimes:
- Dense rewards from a scalar RM. If your RM outputs continuous scores (not binary) and you have plenty of memory, PPO's value model gives cleaner variance reduction than group normalization.
- Very long trajectories (agents, multi-turn tool use). PPO's step-level advantage is more informative than GRPO's episode-level normalization when episodes are 100+ steps.
- When you've already invested in a PPO codebase. GRPO is easier to run but PPO is more mature; frontier labs (Anthropic, OpenAI) reportedly still use PPO variants for their main alignment loops.
8.6 The 2025 alignment stack
Current frontier open recipe (as of Tulu-3, DeepSeek-R1, Llama-3.1):
Steps 1–3 are what you'll ship for a chat product. Steps 4–5 are what frontier labs add to top leaderboards. The recipes are open. The compute is the moat.
Further reading:
- Tulu-3 RLVR: https://arxiv.org/abs/2411.15124
- Nathan Lambert's Interconnects post-mortem of 2024 alignment: https://www.interconnects.ai/p/2024-alignment-year-in-review
9 · War story — the KL coefficient that ate my weekend
Early 2024. I was reproducing the TRL PPO tutorial on a 1.4B Pythia model for a talk. Followed the recipe. Everything looked normal for 200 steps — reward climbing from 0.2 to 0.8, KL creeping up to 8 (init KL coef 0.2). Then step 210, reward jumped to 4.5 (max scale was 5.0). Suspiciously good.
Inspected the samples. The policy had converged to generating a single response for every prompt: "Certainly! I would be happy to help you with that. As an AI assistant, my role is to provide helpful, accurate, and safe information. Here is what I would recommend: I would strongly recommend that you consult a qualified professional for this matter."
That string had reward 4.9 from my RM (which was trained on UltraFeedback — which is dominated by GPT-4 responses — which love that exact preamble). The policy found the sycophancy attractor and camped there. KL was 24 by that point but the reward gain was outweighing the KL penalty.
Fix: I bumped the initial KL coef from 0.2 to 0.5 and set target_kl=6 (adaptive KL controller in TRL). Also added a length penalty in the reward: R ← R - 0.001 * (len - 50).clamp(0). Retrained. Reward plateaued at 2.8 (lower nominal) but sample diversity survived. Human eval score on a held-out set: 2× the sycophantic-collapsed policy.
Lessons:
- Watch KL like a hawk. If KL > 15 sustained, you're overfitting to the RM's quirks.
target_klwith adaptive coefficient is the difference between working PPO and non-working PPO.- Length penalties are cheap insurance against verbose collapse.
- Always sample and read outputs during training. Reward numbers lie; sample text doesn't.
10 · Retention scaffold
Recall
Q1. Write down the PPO-KL objective for LLMs.
Reveal
ℒ = Et[min(ρt·At, clip(ρt, 1-ε, 1+ε)·At)] − β · Et[KL(πθ || πref)], where ρt = πθ(yt|·) / πθ_old(yt|·) is the per-token probability ratio, At is the advantage, ε ≈ 0.2, and β ≈ 0.01–0.1.
Q2. What role does each of the four models play?
Reveal
(1) Policy πθ — trainable, what we optimize. (2) Reference πref — frozen SFT copy, provides the KL anchor. (3) Reward model rφ — frozen, provides the scalar terminal reward. (4) Value model Vψ — trainable, provides the per-token baseline for GAE-based advantage estimation.
Q3. Why do we use reverse KL rather than forward KL in the PPO penalty?
Reveal
Reverse KL — KL(πθ || πref) — is mode-seeking: it penalizes πθ for putting mass where πref is small, but allows the policy to concentrate on high-reward regions as long as they have some support under πref. Forward KL would force πθ to cover all of πref's support, defeating the whole point.
Q4. In a healthy PPO training log, which direction should reward and KL each move?
Reveal
Reward should grow monotonically for a few hundred steps then plateau. KL(π || πref) should grow slowly and stabilize (target KL ≈ 6). Runaway KL with continued reward growth = reward hacking; flat reward with flat KL = β too high, policy can't move.
Q5. Why is the field moving from PPO to DPO?
Reveal
PPO on LLMs requires 4 models in memory, has 15+ hyperparameters, is slow (generation-dominated), and fails silently to reward hacking. DPO uses the same theoretical framework (Bradley-Terry + KL) but reformulates it as a supervised loss on preference pairs — no RM, no rollouts, no value head. Same quality on most tasks, 20% of the complexity.
Stretch prompt
You're setting up a PPO run and want to prevent reward hacking as much as possible. Design a "sanity harness" that runs every N steps during training: generates from the policy on a fixed set of held-out prompts, computes RM score, and flags if either (a) mean length is drifting, (b) any characteristic n-gram is dominating outputs, or (c) RM score has grown past a threshold consistent with hacking. Sketch pseudo-code.
In your own words
Explain PPO to a colleague who understands SFT and reward modeling but has never touched RL. In 5–6 sentences, address: (a) what PPO adds beyond just "gradient-ascend the reward", (b) what each of the four models does, (c) what the failure modes look like.
Spaced review
- S062 — Reward modeling. PPO is the algorithm that consumes the RM. Without a good RM, PPO can only fail.
- S058 — SFT. The policy πθ and reference πref both start as your SFT'd model. PPO is downstream of SFT.
- S022 — Cross-entropy loss. Compare the PPO clipped objective (weighted log-prob) to CE loss — same core object, different weighting scheme.
Next session
S064 — DPO: RLHF without the RL. Tomorrow we derive DPO from PPO's own KL-regularized RL objective, show that the optimal policy can be written in closed form as a function of the reference policy and reward, invert that to solve for reward as a function of log-ratios, plug into the Bradley-Terry loss, and end up with a supervised loss that trains the policy directly on preference pairs. Same underlying theory, 10× less code.
Bring back tomorrow
- Four models: policy, reference, reward, value.
- PPO clipped objective bounds per-token probability ratios to 1 ± ε.
- Reward hacking is the #1 failure mode; watch samples, not just numbers.
On each PPO step, log: (a) KL(π_θ || π_ref), (b) mean predicted reward on 8 held-out prompts, (c) mean generation length on the same prompts, (d) 3 verbatim sample outputs. A healthy run: KL climbs slowly to ~6 and plateaus, reward climbs then plateaus, length stays roughly stable, samples remain coherent. Reward hacking: KL keeps climbing past 20, reward keeps climbing, generations get monotonously long / repetitive / weirdly punctuated. If you spot the pattern at step 50, kill the run, raise β (KL coefficient) 3×, and restart — cheaper than debugging at step 5000.