Post-training and Alignment

The KL penalty and the reference model

Optimising a reward score too hard destroys the model, so training keeps a frozen copy of the starting model and penalises drifting away from it.

On this page 8
  1. The short answer
  2. The analogy you have already lived
  3. Why it exists
  4. How it works
  5. The shape you must know
  6. Where you have already seen this
  7. Remember this
  8. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

The short answer

Training keeps a frozen copy of the starting model. The new model is punished for drifting too far from that copy.

The analogy you have already lived

Picture a dog on a long leash in a park. The leash is not there to stop the dog enjoying itself. It is there to stop the dog running onto the road.

Give it more slack and the dog explores more, and gets closer to danger. Give it less and the dog stays safe but barely moves.

The frozen starting model is the peg in the ground. The penalty is the leash. Choosing its length is the whole job.

Why it exists

You have a reward model that scores answers. The plan is to train your chat model to get high scores. What could go wrong?

Everything, and quickly.

The reward model is an imitation of human taste, not human taste itself. It has blind spots. Push hard enough and the chat model finds them.

It will discover that longer answers score higher and pad every reply. Or that a certain phrase always scores well and start every reply with it. The reward score climbs beautifully. The model becomes useless.

This is called reward hacking. The model scores well on the measure. It gets worse at what the measure was meant to capture.

How it works

Before training starts, you make a frozen copy of the model. That copy never changes. It is the reference model.

At every step, you check two things.

   new model's answer  --> reward model --> score (want this HIGH)
   new model's answer  --> how different is this from what the
                           frozen copy would have said?      (want this LOW)

The training target is the first number minus a fraction of the second. A knob controls the fraction. It is usually called beta.

Small beta is a long leash. The model chases the reward score hard and eventually breaks. Large beta is a short leash. The model stays sensible and barely improves.

The shape you must know

Draw the true quality of the model against how far it has wandered.

   true quality
        |        ___
        |      _/   \___
        |    _/          \____
        |  _/                 \____
        | /                        \_____
        +---------------------------------- distance from start
          ^        ^                  ^
        start    the best        wandered too far,
                                 model is now worse
                                 than when it started

It goes up, then it comes down. Train longer and you cross the peak and keep going.

This is why "our reward score doubled" is not good news on its own. The reward score rises the entire time, including all the way down the far side of that hill.

Where you have already seen this

  • A dog on a leash in a park.
  • A goat tethered to a peg, grazing exactly one circle.
  • Teaching to the test — the exam score rises, the actual learning does not.

Remember this

  • A frozen copy of the starting model is kept, and drifting from it is penalised.
  • Reward score always rises. True quality rises, peaks, then falls.
  • The penalty strength is a dial with no correct setting, only trade-offs.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install torch

Runs on a CPU in a few seconds.

Watching over-optimisation happen

The setup is deliberately tiny: one prompt, six candidate answers, a PROXY reward that a reward model might plausibly have learned, and a TRUE reward representing what humans would actually say. They disagree about one answer.

kl_penalty.py
import torch

# Six candidate answers to one prompt. The reference (SFT) policy has its own
# preferences. The PROXY reward is what our reward model believes; the TRUE
# reward is what humans would actually say. They disagree about padding.
ANSWERS = ["short+right", "medium+right", "long+right",
           "short+wrong", "long+wrong", "long+empty-padding"]
REF_LOGITS = torch.tensor([1.2, 1.5, 0.9, 0.3, 0.1, -1.0])
TRUE = torch.tensor([0.90, 1.00, 0.85, 0.05, 0.00, -0.50])
PROXY = torch.tensor([0.60, 0.95, 1.05, 0.10, 0.35, 1.60])   # rewards length, blind to truth

ref = REF_LOGITS.softmax(0)
print("answer                 ref prob   TRUE reward   proxy reward")
for a, p, t, q in zip(ANSWERS, ref, TRUE, PROXY):
    print(f"{a:<22} {p:>8.3f} {t:>13.2f} {q:>14.2f}")
print(f"\nreference policy: TRUE {(ref * TRUE).sum():.4f}   proxy {(ref * PROXY).sum():.4f}")


def optimise(beta, steps, lr=0.02, log_every=None):
    logits = REF_LOGITS.clone().requires_grad_(True)
    opt = torch.optim.Adam([logits], lr=lr)
    rows = []
    for step in range(steps + 1):
        p = logits.softmax(0)
        kl = (p * (p.clamp_min(1e-12).log() - ref.log())).sum()
        if log_every and step % log_every == 0:
            rows.append((step, (p * PROXY).sum().item(), (p * TRUE).sum().item(), kl.item()))
        opt.zero_grad()
        (-((p * PROXY).sum() - beta * kl)).backward()
        opt.step()
    p = logits.softmax(0).detach()
    kl = (p * (p.clamp_min(1e-12).log() - ref.log())).sum().item()
    return rows, (p * PROXY).sum().item(), (p * TRUE).sum().item(), kl, p


print("\n=== no KL penalty: optimise the proxy as hard as you can ===")
rows, *_ = optimise(beta=0.0, steps=200, log_every=20)
print(f"{'step':>5} {'KL to ref':>10} {'proxy':>8} {'TRUE':>8}")
for step, pr, tr, kl in rows:
    bar = "#" * max(0, round(tr * 25))
    print(f"{step:>5} {kl:>10.3f} {pr:>8.4f} {tr:>8.4f}  {bar}")

print("\n=== the same optimisation, at several KL penalties ===")
print(f"{'beta':>6} {'final KL':>9} {'proxy':>8} {'TRUE':>8}   most likely answer")
for beta in (0.0, 0.1, 0.3, 0.5, 0.8, 1.2, 2.0):
    _, pr, tr, kl, p = optimise(beta, steps=600)
    print(f"{beta:>6.1f} {kl:>9.3f} {pr:>8.4f} {tr:>8.4f}   "
          f"{ANSWERS[p.argmax()]} ({p.max():.0%})")
Output
answer                 ref prob   TRUE reward   proxy reward
short+right               0.254          0.90           0.60
medium+right              0.343          1.00           0.95
long+right                0.188          0.85           1.05
short+wrong               0.103          0.05           0.10
long+wrong                0.084          0.00           0.35
long+empty-padding        0.028         -0.50           1.60

reference policy: TRUE 0.7218   proxy 0.7599

=== no KL penalty: optimise the proxy as hard as you can ===
 step  KL to ref    proxy     TRUE
    0      0.000   0.7599   0.7218  ##################
   20      0.067   0.8631   0.7817  ####################
   40      0.192   0.9315   0.8112  ####################
   60      0.297   0.9760   0.8018  ####################
   80      0.415   1.0206   0.7475  ###################
  100      0.619   1.0855   0.6254  ################
  120      1.050   1.2094   0.3472  #########
  140      1.981   1.3960  -0.0744  
  160      2.730   1.5069  -0.3143  
  180      3.054   1.5473  -0.3979  
  200      3.200   1.5640  -0.4315  

=== the same optimisation, at several KL penalties ===
  beta  final KL    proxy     TRUE   most likely answer
   0.0     3.522   1.5963  -0.4934   long+empty-padding (99%)
   0.1     3.263   1.5725  -0.4417   long+empty-padding (96%)
   0.3     0.567   1.1038   0.5429   medium+right (36%)
   0.5     0.202   0.9673   0.7101   medium+right (41%)
   0.8     0.081   0.8928   0.7470   medium+right (40%)
   1.2     0.037   0.8503   0.7501   medium+right (39%)
   2.0     0.014   0.8151   0.7441   medium+right (37%)

No randomness anywhere — this reproduces exactly. Written against PyTorch 2.5.1.

Read every row of that first table

Proxy reward rose monotonically. Every single step. 0.7599 to 1.5640, never once dipping. If proxy reward were your dashboard, this run looks like an unqualified success from beginning to end.

True reward peaked at step 40 and then collapsed. 0.7218 → 0.8112 → −0.4315. The bar chart makes it visible: the model got better, then worse, then far worse than it started.

The peak sat at a KL of about 0.19. That is a small number. The useful part of the optimisation was over almost immediately, and everything after it was damage.

By step 200 the model has found the exploit. 99% of its probability mass ends up on long+empty-padding, the answer with the highest proxy score and the worst true score.

And now read the beta sweep

There is a cliff between beta 0.1 and 0.3. At 0.1 the model still collapses onto the exploit. At 0.3 it does not. Beta is not a smooth dial; it has a threshold, and where that threshold sits depends on how large the proxy's error is.

The best true reward came from a large beta, and it barely beat doing nothing. 0.7501 at beta 1.2, against 0.7218 for the untouched reference. With a proxy this badly wrong, most of the achievable gain is unreachable. That is the honest situation, and it is why reward-model quality matters more than RL hyperparameters.

The actual objective in RLHF

$$ \max_{\pi_\theta}\; \mathbb{E}{x \sim \mathcal{D},\, y \sim \pi\theta(\cdot|x)}\left[r_\phi(x,y)\right] - \beta\, D_{\mathrm{KL}}!\left(\pi_\theta(\cdot|x) \,|\, \pi_{\text{ref}}(\cdot|x)\right) $$

In PPO-style implementations the KL term is folded into a per-token reward rather than computed exactly:

python
# per-token KL estimate used in practice, from sampled tokens
logratio = policy_logprob - ref_logprob         # both for the sampled token
kl_estimate = logratio                          # naive, unbiased, high variance
kl_k3 = (logratio.exp() - 1) - logratio         # low variance, always >= 0
shaped_reward = -beta * kl_k3                   # per token
shaped_reward[-1] += reward_model_score         # score arrives only at the end

The k3 estimator is Schulman's, and it is what most implementations use: unbiased, lower variance than the naive one, and non-negative, which matters because a negative KL estimate in a log makes debugging miserable.

Practical guidance

SettingTypical valueNotes
beta (PPO KL coefficient)0.01 – 0.1often adaptively controlled to hit a KL target
beta (DPO)0.1plays the same role, different mechanism
Target KL per token5 – 20 nats over a full responsemonitor it, do not only set it
Reference modelfrozen copy of the SFT modelnever updated in the standard recipe

Two operational points. First, the reference model costs a full extra forward pass per batch; some trainers precompute its log-probabilities instead, which works when the reference is frozen and the prompts are fixed. Second, log the KL every step and treat a sudden rise as an incident, exactly like a gradient-norm spike.

Common mistakes

Judging the run by reward score. The first table is the counter-example. Reward always rises. Hold out an evaluation the reward model never saw.

Forgetting to freeze the reference. If ref_model receives gradients, the KL term collapses to zero and the leash is gone. Load a second copy in eval() mode with requires_grad_(False), or use precompute_ref_log_probs.

Using the base model as the reference instead of the SFT model. The reference defines "normal". Anchoring to the base model penalises the instruction-following behaviour you spent the previous stage installing.

Setting beta once and never checking KL. The relationship between beta and achieved KL depends on the reward scale, which changes with every new reward model. Adaptive KL control — raise beta when KL exceeds a target, lower it when below — is the standard fix.

Assuming the sign of the KL estimate. The naive logratio estimator is frequently negative on individual samples. That is correct behaviour, not a bug.

Try it yourself

Change PROXY so that long+empty-padding scores 0.9 instead of 1.6 — a less badly broken reward model. Re-run the beta sweep. The cliff moves, and the achievable true reward rises. That is what "improve your reward model" buys you, measured.

What to learn next

Researcher — Mathematics and papers.

The constrained objective and its closed-form solution

RLHF optimises

$$ \max_{\pi}\;\mathbb{E}{x\sim\mathcal{D},\,y\sim\pi}[r(x,y)] - \beta\,D{\mathrm{KL}}(\pi \,|\, \pi_{\text{ref}}) $$

This is a KL-regularised bandit problem, and it has an exact solution:

$$ \pi^*(y \mid x) = \frac{1}{Z(x)}\,\pi_{\text{ref}}(y \mid x)\exp!\left(\tfrac{1}{\beta} r(x,y)\right), \qquad Z(x) = \sum_{y}\pi_{\text{ref}}(y\mid x)\exp!\left(\tfrac{1}{\beta}r(x,y)\right) $$

The optimal policy is the reference reweighted exponentially by reward. Two consequences follow.

$\beta$ is a temperature on the reward. As $\beta \to \infty$, $\pi^* \to \pi_{\text{ref}}$. As $\beta \to 0$, $\pi^*$ collapses onto $\arg\max_y r(x,y)$ — precisely the 99%-on-the-exploit behaviour in the code above.

$Z(x)$ is intractable, since it sums over all sequences. That intractability is the reason RLHF used RL at all — and rearranging this same equation to eliminate $Z$ is exactly how DPO is derived.

Mode-seeking, not mode-covering

The objective uses reverse KL, $D_{\mathrm{KL}}(\pi_\theta | \pi_{\text{ref}})$, which is zero-avoiding in $\pi_{\text{ref}}$ but mode-seeking in $\pi_\theta$: the trained policy is free to abandon regions the reference covers. Forward KL would instead force $\pi_\theta$ to cover everything $\pi_{\text{ref}}$ does.

This choice is why RLHF reliably reduces output diversity — mode collapse toward a narrower, safer distribution — and why base models remain more diverse generators than their aligned counterparts. Kirk et al., 2024 (Understanding the Effects of RLHF on LLM Generalisation and Diversity) measured this directly: RLHF improves out-of-distribution generalisation and measurably reduces output diversity relative to SFT, and the trade-off is not incidental to the method.

Wang et al., 2024 (Beyond Reverse KL) generalise the regulariser to any $f$-divergence, giving an explicit knob on the diversity–alignment trade-off. TRL exposes this as f_divergence_type on DPOConfig.

KL estimators

Only samples from $\pi_\theta$ are available, so KL must be estimated. Writing $\rho = \log\frac{\pi_\theta(y)}{\pi_{\text{ref}}(y)}$ for a sampled $y$:

EstimatorFormulaBiasVarianceSign
k1$\rho$unbiasedhighcan be negative
k2$\tfrac{1}{2}\rho^2$biasedlownon-negative
k3$(e^{\rho} - 1) - \rho$unbiasedlownon-negative

k3 is the standard choice (Schulman's blog on approximating KL divergence is the canonical reference). It is a control-variate correction of k1: since $\mathbb{E}{\pi\theta}[e^{\rho}] = 1$ under the change of measure, subtracting $e^\rho - 1$ removes variance without introducing bias.

Over-optimisation, quantified

Gao et al., 2023 (Scaling Laws for Reward Model Overoptimization) fitted the true reward as a function of $d = \sqrt{D_{\mathrm{KL}}(\pi | \pi_{\text{ref}})}$:

$$ R_{\text{bo}n}(d) = d\,(\alpha_{\text{bo}n} - \beta_{\text{bo}n} d), \qquad R_{\text{RL}}(d) = d\,(\alpha_{\text{RL}} - \beta_{\text{RL}}\log d) $$

Both are inverted-U shaped in $d$ — which is what the first table in the code reproduces, at a scale you can run in three seconds. Their empirical findings:

  • The peak's location moves further out with larger reward models and more preference data. Policy size barely affects it.
  • Best-of-$n$ sampling and RL trace different curves at the same KL, with best-of-$n$ more robust at small KL.
  • The KL penalty coefficient does not change the shape; it changes how fast you travel along it. Early stopping and a KL penalty are substitutes rather than complements.

That last point is the most practically useful and the least widely known.

Adaptive KL control

Fixing $\beta$ fixes the price of divergence, not the divergence. Since the reward scale changes with every reward model, the achieved KL varies wildly across runs at the same $\beta$.

The standard controller (Ziegler et al., 2019) targets a KL value $\mathrm{KL}_{\text{target}}$ and updates

$$ \beta \leftarrow \beta \left(1 + K_\beta \cdot \mathrm{clip}!\left(\frac{\mathrm{KL}{\text{actual}} - \mathrm{KL}{\text{target}}}{\mathrm{KL}_{\text{target}}}, -0.2, 0.2\right)\right) $$

This makes runs comparable across reward models, which is why it is worth the extra hyperparameter.

When the reference can be dropped

Two situations remove the need for a reference model entirely.

Verifiable rewards. When the reward is a program — a unit test, an exact-match check — there is no proxy to over-optimise, so the Goodhart argument does not apply in the same form. DeepSeek-R1's GRPO recipe and TRL's GRPOConfig default (beta=0.0) both reflect this; see RL with verifiable rewards. The model can still degrade in other ways — readability, language mixing — which is why R1 added separate format rewards rather than a KL term.

Reference-free preference objectives. SimPO (Meng et al., 2024) replaces the reference log-ratio with a length-normalised average log-probability and adds a target margin, removing the reference model's memory and compute cost entirely. It performs competitively; it also gives up the theoretical grounding above.

Papers

What to learn next