Skip to content

The RLHF Pipeline: Reward Models and PPO

So we have a supervised fine-tuned model from the previous post. It answers questions in the right format. It follows instructions, mostly. It knows what a chat turn looks like. What it does not know, and really cannot know from SFT alone, is which of its own valid-looking responses humans prefer.

I think this is the key unlock. SFT teaches the model to produce outputs that look like the training distribution. "Look like" is not the same as "is better than the alternative." If a prompt has ten reasonable responses and the model learned one demonstration of the format, the model has no signal about why that one was chosen and not the other nine.

RLHF is the field's answer to that gap. The pipeline is doing one specific thing: turning pairwise human judgments ("this response was better than that one") into a gradient signal that pushes the language model toward the preferred distribution. Everything about reward models and PPO and KL penalties is plumbing to make that work without blowing up the model you started with.

Prerequisites

This post assumes you've read:

  • Next-Token Prediction for the base-model setup and next-token loss.
  • Supervised Fine-Tuning for the SFT stage we build on top of.
  • Attention From Scratch for what "policy" and "logits" mean in transformer terms.
  • A cross-entropy / softmax comfort level. Policy gradients will make more sense if you've seen logπ\nabla \log \pi before, but I'll motivate it as we go.

The shape of the pipeline

Here's the pipeline in one sentence: take an SFT model, sample responses from it, ask humans which is better, fit a reward model to those preferences, then use the reward model to steer the language model with reinforcement learning.

Four moving pieces:

  1. SFT model (πSFT\pi_{\text{SFT}}). Already trained. The anchor. It's frozen during RLHF as the reference point and never touched again.
  2. Preference dataset. Pairs (x,yw,yl)(x, y_w, y_l) where ywy_w is the chosen response and yly_l is the rejected one, given the same prompt xx.
  3. Reward model (rϕr_\phi). Usually the same architecture as the LM, with the language-modeling head swapped out for a single scalar. Trained to rank the chosen response above the rejected one.
  4. Policy (πθ\pi_\theta). The thing we want. It starts as a copy of πSFT\pi_{\text{SFT}}, and PPO gradually updates it to produce responses the reward model likes. The KL penalty tethers it back to πSFT\pi_{\text{SFT}} so it does not drift off a cliff.

The hero loop above cycles through all four. Watch the PPO step: reward flows in from the right, the policy gets an update, and then a KL tether pulls it back toward the SFT box. That tether is doing more work than it looks like.


Reward model: turning "A > B" into a scalar

Humans are terrible at assigning absolute scores to language model outputs. Ask ten people to rate "how good is this answer on a 1-10 scale" and you'll get ten different calibrations. Ask them "which of these two answers is better?" and agreement jumps.

So we don't ask for absolute scores. We ask for comparisons. Given a prompt xx and two candidate responses yay_a and yby_b, a labeler picks one. That's the entire data format: (x,yw,yl)(x, y_w, y_l), where ww means "won" and ll means "lost."

The reward model learns to assign a scalar rϕ(x,y)r_\phi(x, y) to any (prompt, response) pair, such that the chosen response scores higher than the rejected one. The objective comes from the Bradley–Terry preference model, which has been the workhorse for pairwise ranking for over seventy years. The model says: the probability that ywy_w is preferred over yly_l is

P(ywylx)=σ(rϕ(x,yw)rϕ(x,yl))P(y_w \succ y_l \mid x) = \sigma(r_\phi(x, y_w) - r_\phi(x, y_l))

where σ\sigma is the logistic sigmoid. That should feel familiar: it's a binary-classification head on the reward gap.

The training loss is the negative log-likelihood of the observed preferences:

LRM(ϕ)=E(x,yw,yl)D[logσ(rϕ(x,yw)rϕ(x,yl))]\mathcal{L}_{\text{RM}}(\phi) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}\bigl[\log \sigma(r_\phi(x, y_w) - r_\phi(x, y_l))\bigr]

No elaborate multi-headed ranking objective, no listwise anything. Just a logistic loss on the score gap.

Preference pair → reward model → Bradley-Terry loss
scenario 1 / 3: RM is confident and correct
✓ CHOSEN (y_w)
I'd be happy to help. The capital of France is Paris...
× REJECTED (y_l)
pairs. what do you want
rewardmodelrφ(x, y) → ℝ(same arch as LM,scalar head)r_w =+0.00r_l =+0.00P(y_w > y_l) = σ(r_w − r_l) = 0.000loss = −log σ(r_w − r_l) = 0.000

Both completions go through the same reward model. The Bradley–Terry loss pushes the chosen score above the rejected score, with loss ≈ 0.69 when the scores tie and loss → 0 when the gap gets large. Scenario three shows what happens when the RM gets the preference wrong.

First, the reward model's architecture is usually initialized from the SFT model itself. Same transformer backbone, replace the next-token head with a scalar readout on the last-token hidden state. This is cheap (you reuse all the pretraining), and it turns out to matter a lot. A reward model built on top of a weak base ranks answers worse, which quietly caps the quality of everything downstream.

Second, the reward model scores a full response, not each token. It reads the whole thing, then outputs one number. In PPO we'll assign that terminal reward to the last token of the sequence and treat every token before it as having reward zero. More on that in the PPO section.

Third, the reward model is pretty bad at assigning absolute quality. It's calibrated for ranking, not for scoring. A reward of +1.3+1.3 doesn't mean anything by itself; only the gap to another response is meaningful. If you find yourself reading an RM output and thinking "oh that's a 1.3, that's pretty good," you're doing it wrong.


PPO for language models

Now we have a reward model. We have an SFT model. We need to use the first to update the second. This is where reinforcement learning shows up, and specifically where we use Proximal Policy Optimization.

Let me try to motivate PPO as narrowly as possible, just enough to show what it's protecting against. The naive move would be: sample a response, score it with the RM, and do a policy-gradient update θlogπθ(yx)r\nabla_\theta \log \pi_\theta(y \mid x) \cdot r. That works in toy environments. For a language model with billions of parameters, it explodes. A single lucky high-reward sample can push the policy into a region where it assigns near-zero probability to coherent English. Once that happens, every subsequent sample is gibberish, the RM score collapses, and training never recovers.

PPO's fix is a trust region. At each update, we compute the probability ratio

rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}

between the current policy and the policy we used to sample the data. If the ratio is one, nothing has changed. If it's 1.5, we've made the action 50% more likely. PPO says: don't let the update move that ratio too far from one. Specifically, the clipped surrogate objective is

LCLIP(θ)=Et[min(rt(θ)A^t, clip(rt(θ), 1ε, 1+ε)A^t)]\mathcal{L}^{\text{CLIP}}(\theta) = \mathbb{E}_t\Bigl[\min\bigl(r_t(\theta) \hat{A}_t,\ \text{clip}(r_t(\theta),\ 1-\varepsilon,\ 1+\varepsilon) \hat{A}_t\bigr)\Bigr]

where A^t\hat{A}_t is the advantage (how much better this action was than the baseline) and ε\varepsilon is typically 0.20.2.

The min-of-two-things is what makes this work. If the advantage is positive (the action was good) and the ratio is already at 1+ε1 + \varepsilon, increasing it further gives you no additional reward, because the clipped term caps the objective. If the advantage is negative (the action was bad) and the ratio has already dropped to 1ε1 - \varepsilon, pushing it lower gives you no additional penalty. Either way, the gradient through that sample is zero once you leave the trust region.

PPO clipped surrogate objective (ε = 0.2)
ratio r = π(a|s) / π_old(a|s) sweeping
L0advantage A > 0 (action was good) → reward going up, but capped at r = 1+ε1−ε = 0.811+ε = 1.2L−1advantage A < 0 (action was bad) → push prob down, but only until r = 1−ε1−ε11+εr = 0.500✗ r < 1−ε → gradient clipped, no further penalty

The clipped objective as a function of the probability ratio r. The green shaded band is the trust region [1−ε, 1+ε]. For a positive advantage, the gradient dies once r exceeds 1+ε. For a negative advantage, the gradient dies once r drops below 1−ε. The clip is the ceiling on how fast one update can move probability mass.

Watch the sweep. The marker traces LCLIPL^{\text{CLIP}} as a function of rr. The dashed line is the unclipped rAr \cdot A, which is what you'd get with vanilla policy gradient. The solid line bends at the clip boundaries and goes flat. That flat region is the "nothing more to gain" zone, and it's the reason PPO stays stable where naive policy gradient would diverge.


Reward, minus a baseline, minus KL

Before the PPO loss can fire, we need A^t\hat{A}_t. In a classic actor-critic setup, this would be "reward minus value function." For LLM RLHF, three specific things happen:

  1. Reward at the end. rϕ(x,y)r_\phi(x, y) scores the full response and gets credited to the last token. Every other token sees zero reward directly. This sounds wrong, but PPO (via GAE or equivalent) spreads the final-token signal backward through time.
  2. Baseline subtraction. We typically subtract the mean reward across the rollout batch (or a learned value function's prediction) so the advantages are mean-zero. This is just variance reduction; the trick you do in any REINFORCE-style setup.
  3. KL penalty rolled into the reward. This is the one that matters most for stability. At every token, we subtract βlog(πθ(ytx,y<t)/πSFT(ytx,y<t))\beta \cdot \log(\pi_\theta(y_t \mid x, y_{\lt t}) / \pi_{\text{SFT}}(y_t \mid x, y_{\lt t})) from the per-token reward. If the current policy is wildly more confident than SFT on this token, we pay for it. If it's less confident, we earn back a little. In expectation, this is exactly βKL(πθπSFT)\beta \cdot \mathrm{KL}(\pi_\theta \| \pi_{\text{SFT}}) per sequence.

So the effective per-token reward that PPO actually uses is

r~t=rtRMβlogπθ(yt)πSFT(yt)\tilde{r}_t = r^{\text{RM}}_t - \beta \cdot \log \frac{\pi_\theta(y_t \mid \cdot)}{\pi_{\text{SFT}}(y_t \mid \cdot)}

where rtRMr^{\text{RM}}_t is zero everywhere except the last token.

The KL term is not a regularizer in the "encourage parsimony" sense. It's an anchor. Without it, PPO will cheerfully drive the policy toward whatever collapsed distribution maximizes the RM score, because the RM is an imperfect model and it can be fooled.

KL penalty: policy drift from the SFT reference
healthy: KL small, reward rising
π_SFT (reference)helpful0.18sure0.16you0.14the0.12no0.10JACKPOT!!0.12sorry0.10I0.08π (current policy)helpful0.18sure0.16you0.14the0.12no0.10JACKPOT!!0.12sorry0.10I0.08RM reward r(x, y)0.92↑ as mass concentratesKL(π || π_SFT)0.00distance from SFTr − β·KL (β = 0.3)0.92the thing PPO actually maximizesdrift:

Two categorical distributions over a toy vocabulary: the SFT reference on the left, the current policy on the right. As PPO runs, the policy concentrates mass on a high-reward token. Watch KL grow and watch the net objective (reward minus β·KL) peak and then turn over. The KL term is what stops runaway collapse.

Watch what happens when the drift is small: reward goes up, KL is small, net objective rises. At moderate drift, KL has grown but reward is still rising faster. Then the policy starts to concentrate too much mass on one token, and the story inverts. KL explodes while reward barely moves. The net objective (the thing PPO is actually maximizing) falls off a cliff. This is what reward hacking looks like when it's caught by the KL tether. When it's not caught by the KL tether, you get the next viz.


Why RLHF is hard: the reward model is the bottleneck

Most of the engineering problems come down to one fact: the reward model is a lossy approximation to human preference, and PPO will find every imperfection.

If the RM rewards longer responses slightly more than shorter ones at equivalent quality, PPO will produce longer responses. If the RM slightly prefers confident hedging to honest uncertainty, PPO will produce confident hedges. If the RM has a blind spot where obviously-incorrect-but-plausible answers score fine, PPO will exploit that blind spot. These have all shown up in production RLHF runs.

The canonical picture is this: plot the RM score against human-judged quality over the course of training. For a while, they track each other. Then they diverge. The RM score keeps climbing, and the human-judged quality plateaus, then drops.

Reward hacking: training reward vs. human-judged quality
training step 0 / 120
0.000.250.500.751.000306090120PPO training stepRM reward (what PPO optimizes)human-judged quality (what we want)reward: 0.080quality: 0.226

Training reward (green) keeps climbing because that's what PPO optimizes. Human-judged quality (orange) tracks it for a while, then saturates, then falls off as the policy exploits RM quirks that don't correspond to real quality. The divergence is the essence of reward hacking.

The usual mitigations are all imperfect: regularize harder with KL (limits drift but also improvement), re-train the RM periodically on fresh policy samples (expensive), use multiple RMs and take a conservative aggregate, or cap training steps with humans watching for drift. The last one is the default in practice.

Anthropic's "Helpful and Harmless" paper spends a lot of pages on this tension, and Casper et al.'s 2023 review catalogs the full taxonomy. None of this makes RLHF useless. The pipeline is less "solve alignment" and more "compress human preferences into a gradient signal, knowing it's going to be imperfect, and stop before the imperfections eat the model."


Worked example

Let me trace one step of the PPO update for a single prompt. Made-up numbers, but the shape matches a real rollout.

Prompt: "Explain gradient descent briefly."

Step 1: rollout. We sample four completions from the current policy πθold\pi_{\theta_{\text{old}}}, each sampled with the standard temperature/top-p setup.

Step 2: score. We run each (x,yi)(x, y_i) through the reward model.

Step 3: compute advantage. We subtract the batch mean reward rˉ=0.4\bar{r} = 0.4 as a baseline, and subtract the per-token KL penalty with β=0.02\beta = 0.02 (a common default). In this toy, the KL term is small, and most of the advantage comes from rirˉr_i - \bar{r}.

Step 4: compute ratios. The update runs for a couple of inner epochs on this same rollout, so by the time we compute gradients, πθ\pi_\theta has drifted from πθold\pi_{\theta_{\text{old}}}. We compute the per-token ratio rt=πθ(yt)/πθold(yt)r_t = \pi_\theta(y_t \mid \cdot) / \pi_{\theta_{\text{old}}}(y_t \mid \cdot). The per-sequence product of ratios is what goes into the clipped objective.

Step 5: clip. For any token where rtr_t has moved outside [1ε,1+ε][1 - \varepsilon, 1 + \varepsilon] in the wrong direction, the gradient is killed (clipped contribution of zero). For tokens inside the trust region, we get the normal rtA^tr_t \cdot \hat{A}_t gradient.

One PPO step · prompt: "Explain gradient descent briefly." · mean reward baseline b = 0.40
#
completion y_i
RM r(x,y)
advantage A
ratios π_new / π_old (first 5 tokens)
1
"I'd be happy to. Gradient descent is an optimization…"
Σ log π_old = -9.2
+2.40
+1.85
1.051.091.021.071.11
2
"Sure — it's an iterative method that moves opposite…"
Σ log π_old = -11.5
+1.10
+0.55
1.020.981.011.030.99
3
"gradient descent goes downhill"
Σ log π_old = -14.2
-0.30
-0.85
0.910.860.790.820.88clipped
4
"JACKPOT!!! AMAZING QUESTION!!! GRADIENT…"
Σ log π_old = -21.0
-1.60
-2.15
0.550.420.360.580.71clipped
A = r(x, y) − b − β · log(π_new / π_old). The sign of A tells you which direction to push π, and the clipped ratios (red) stop the push once it crosses [1−ε, 1+ε] = [0.80, 1.20].

Four completions from one prompt. Completion 4 is reward-hacky (all-caps, excited), which the RM correctly rejects; its strong negative advantage pushes its token probabilities down. But notice the ratios — several are already below 1−ε = 0.80 — so PPO clips the gradient. The policy moves, but cautiously.

A couple of things to notice in the table:

  • Completion 1 has advantage +1.85+1.85 (positive). Its per-token ratios hover just above 1 (the policy now mildly prefers it). All ratios stay inside [0.80,1.20][0.80, 1.20], so the full gradient flows and the policy gets nudged further toward completion 1.
  • Completion 4 has advantage 2.15-2.15 (strongly negative). Its ratios have already dropped to [0.55,0.71][0.55, 0.71], meaning the policy has already moved substantially away. Every one of those ratios is outside 1ε=0.801 - \varepsilon = 0.80. PPO clips the gradient contribution to zero. The policy already moved; PPO stops further churn on this particular sample.
  • Completion 3 also has negative advantage, but its ratios are only at [0.79,0.91][0.79, 0.91], mostly inside the trust region. PPO still pushes here, just not hard.

Without the clip, completion 4's gigantic negative advantage would dominate the update and rip the policy apart on any token it touched. With the clip, the policy continues to move, but a single spicy sample cannot destroy everything.


Implementation sketch

The mechanics in twenty lines of PyTorch-ish pseudocode. Not runnable verbatim. You'd need the surrounding RL plumbing (GAE, value function, optimizer schedules, reference-model handling), and in practice you'd use a library like trl or trlX. But this captures the two losses.

import torch
import torch.nn.functional as F
 
# ------------------------------------------------------------------
# 1) Reward-model loss: Bradley–Terry on preference pairs.
#    rm(x, y) returns a scalar. We want rm(x, y_w) > rm(x, y_l).
# ------------------------------------------------------------------
def rm_loss(rm, x, y_w, y_l):
    r_w = rm(x, y_w)              # shape (batch,)
    r_l = rm(x, y_l)              # shape (batch,)
    # -log σ(r_w − r_l) == softplus(-(r_w − r_l))
    return F.softplus(-(r_w - r_l)).mean()
 
# ------------------------------------------------------------------
# 2) One PPO step on a rollout.
#    We have: prompts, sampled responses, per-token log-probs under
#    the *old* policy (logp_old), and the RM reward for each response.
# ------------------------------------------------------------------
def ppo_step(policy, ref_model, prompts, responses,
             logp_old, rm_rewards, beta=0.02, eps=0.2):
    # (a) forward pass — get new log-probs and the reference log-probs.
    logp_new = policy.logprobs(prompts, responses)   # (B, T)
    with torch.no_grad():
        logp_ref = ref_model.logprobs(prompts, responses)  # (B, T)
 
    # (b) per-token KL penalty — this is the anchor.
    kl_per_token = logp_new - logp_ref               # (B, T)
 
    # (c) effective reward per token: RM reward lives on the LAST token,
    #     minus beta * KL at every token.
    token_rewards = -beta * kl_per_token
    token_rewards[:, -1] += rm_rewards               # credit terminal reward
 
    # (d) advantage: here, a cheap batch-mean baseline. In practice you'd
    #     compute returns and use a learned value function (GAE).
    advantages = token_rewards - token_rewards.mean()
 
    # (e) clipped surrogate objective.
    ratio = torch.exp(logp_new - logp_old)           # (B, T)
    unclipped = ratio * advantages
    clipped = torch.clamp(ratio, 1 - eps, 1 + eps) * advantages
    loss = -torch.min(unclipped, clipped).mean()     # minus because we maximize
 
    return loss

A few comments on what's happening here.

The reward-model loss is one line of actual work. softplus(-(r_w − r_l)) is the numerically-stable form of logσ(rwrl)-\log \sigma(r_w - r_l). No magic.

The PPO step has five parts. Part (c) is the one I find confusing every time I reread it: the RM reward is a single scalar per sequence, but the loss is summed over tokens, so we fold the RM reward onto the terminal token and let the advantage propagate backward through time. Part (d) is a toy baseline; real systems use a learned value head. Part (e) is the clipped objective, exactly as written in Schulman's PPO paper.

The with torch.no_grad() on the reference model matters. ref_model is frozen πSFT\pi_{\text{SFT}}. We need its log-probs, but we absolutely do not want to update it. The KL anchor only makes sense if the anchor stays still.


Misconceptions

"The reward model learns what humans want." The reward model learns what humans prefer between pairs of samples from a specific distribution. That is not the same as "what humans want." If the RM only ever saw completions from a 13B SFT model in English on helpful-assistant prompts, it has no idea how to score completions from a 70B model doing creative writing in Spanish. Reward models generalize poorly outside the distribution they were trained on, and this is the root cause of most of RLHF's stability problems.

"PPO is the RL part." PPO is a stability trick layered on top of the actual RL part. The RL part (sample trajectories, get reward, compute policy gradient) would work with vanilla REINFORCE in principle. In practice, vanilla REINFORCE on a billion-parameter language model diverges within a handful of updates. PPO is the specific machinery (clipped ratio, multiple epochs on the same rollout) that keeps the updates inside a trust region so the policy does not collapse. There's a real sense in which PPO is less about RL and more about "how to do policy-gradient updates without catastrophically damaging a large pretrained network."

"The KL penalty is optional regularization." Skipping the KL penalty does not give you slightly-less-regularized RLHF. It gives you a policy that converges to a degenerate distribution maximizing the RM's flaws. This is the first thing that happens, not some subtle long-tail failure. The KL penalty is load-bearing, and the choice of β\beta is one of the two or three most important hyperparameters in the entire pipeline. A common framing is "steering between reward and KL," and that framing is closer to what's going on than calling KL a regularizer.

"RLHF makes the model honest / aligned / safe." RLHF makes the model's output distribution look more like the distribution humans prefer when ranking pairs. That is a narrower claim than "aligned." If the preference data systematically rewards confident-sounding answers over uncertain-but-correct ones, RLHF will make the model confidently wrong. The technique is agnostic about what humans actually value; it only reflects what labelers chose in the specific moment they chose it. Alignment properties come from the data curation, not from the optimizer.

What's next

The next post covers DPO and its variants, the supervised shortcut that replaces the entire RM-plus-PPO pipeline with a single closed-form loss on preference pairs, and why it has taken over most of the open-source post-training stack.


Additional reading (and watching)