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.
- 15 min read
- 3 reading levels
- Updated
Read these first
On this page 8
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 startedIt 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
- Direct preference optimisation — what happens when you solve this objective in closed form.
- Reward hacking and sycophancy — the failure this penalty is defending against.
- PPO — the algorithm that carries out the optimisation.
Developer — Code and libraries.
Setup
pip install torchRuns 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.
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%})")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:
# 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 endThe 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
| Setting | Typical value | Notes |
|---|---|---|
beta (PPO KL coefficient) | 0.01 – 0.1 | often adaptively controlled to hit a KL target |
beta (DPO) | 0.1 | plays the same role, different mechanism |
| Target KL per token | 5 – 20 nats over a full response | monitor it, do not only set it |
| Reference model | frozen copy of the SFT model | never 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
- Direct preference optimisation — what happens when you solve this objective in closed form.
- Reward hacking and sycophancy — the failure this penalty is defending against.
- PPO — the algorithm that carries out the optimisation.
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$:
| Estimator | Formula | Bias | Variance | Sign |
|---|---|---|---|---|
| k1 | $\rho$ | unbiased | high | can be negative |
| k2 | $\tfrac{1}{2}\rho^2$ | biased | low | non-negative |
| k3 | $(e^{\rho} - 1) - \rho$ | unbiased | low | non-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
- Ziegler et al., Fine-Tuning Language Models from Human Preferences, 2019 — arxiv.org/abs/1909.08593
- Schulman, Approximating KL Divergence, 2020 — joschu.net/blog/kl-approx.html
- Gao et al., Scaling Laws for Reward Model Overoptimization, ICML 2023 — arxiv.org/abs/2210.10760
- Rafailov et al., Direct Preference Optimization, NeurIPS 2023 — arxiv.org/abs/2305.18290
- Wang et al., Beyond Reverse KL, ICLR 2024 — arxiv.org/abs/2309.16240
- Kirk et al., Understanding the Effects of RLHF on LLM Generalisation and Diversity, ICLR 2024 — arxiv.org/abs/2310.06452
- Meng et al., SimPO: Simple Preference Optimization with a Reference-Free Reward, NeurIPS 2024 — arxiv.org/abs/2405.14734
What to learn next
- Direct preference optimisation — what happens when you solve this objective in closed form.
- Reward hacking and sycophancy — the failure this penalty is defending against.
- PPO — the algorithm that carries out the optimisation.