Trust regions: from TRPO to PPO
I now have a policy gradient (Part V chapter 4), a learned baseline, and a low-variance advantage estimate from GAE (chapter 6). In principle I could just take gradient steps and be done. In practice, if I do that naively, the policy collapses, and it collapses in a way that is not a bug in my code but a fundamental feature of on-policy RL. This chapter is about why, and about the two fixes that define modern policy optimization: TRPO, which solves the problem correctly but expensively with a hard trust-region constraint, and PPO, which approximates that constraint with a clipped objective so simple you can implement it in five lines and so robust it became the default for essentially all of RLHF. I will derive the surrogate objective from the performance-difference lemma, sketch the TRPO constrained problem, and then do the PPO clip in full, including the piecewise case analysis that is the whole reason the clip works. The geometry of that clip is the payoff: once you see which way each piece pushes, PPO stops being a magic formula and becomes obvious.
Theory
Why a naive policy-gradient step destroys the policy
The policy gradient tells me a direction in parameter space, not a distance. The gradient is a statement about an infinitesimal step; it says nothing about how far I can move before the linear approximation it is built on stops being true. And in policy optimization the approximation breaks down viciously fast, for a reason specific to RL that supervised learning does not share: the data distribution depends on the parameters I am changing. In supervised learning, a too-large step gives a worse model on a fixed dataset, and the next batch corrects it. In RL, a too-large step gives a worse policy, that worse policy generates the next batch of data, and if the policy has moved somewhere terrible it collects terrible data and the estimate of the next gradient is computed on that garbage. There is no fixed dataset to fall back to. A single overlarge step can push the policy into a region where it emits degenerate outputs, those outputs get near-zero or misleading reward, the gradient signal collapses, and the run never recovers. This is the "falling off a cliff" failure, and it is why you cannot just crank the learning rate.
The deeper issue is that the natural distance in parameter space (Euclidean distance on ) has nothing to do with the distance in policy space (how different the resulting action distributions are). A tiny change in can enormously change if you are in a sensitive region, and a large change in can barely move elsewhere. So bounding is the wrong constraint. What I actually want to bound is how much the policy distribution moves, and the natural currency for that is the KL divergence . Keep the new policy inside a small KL ball around the old one and the data distribution cannot lurch, the advantage estimates I computed under stay approximately valid, and each step is a safe, monotonic-ish improvement. That KL ball is the "trust region," the region where I trust my local model of the objective. Everything below is machinery for optimizing inside it.
The surrogate objective and the performance-difference lemma
To optimize offline (using a batch of data from to take a step that produces ), I need to express the new policy's performance in terms of old-policy samples. The exact bridge is the performance-difference lemma (Kakade and Langford, 2002):
where is the discounted state-visitation distribution under the new policy. The lemma is exact and beautiful, but it has a problem that is the crux of everything: the expectation is over states visited by , the very policy I am trying to find, so I cannot sample from it yet.
Proof sketch of (7.1) first, because it is short and it shows where the advantage comes from. Write the discounted return as a telescoping sum of advantages along a trajectory drawn from the new policy , using :
The terms telescope: in expectation. So the right side is , which is (7.1).
Now the move that makes it usable. I cannot sample states from , so I approximate it with the old policy's visitation , which is valid as long as is close to (close policies visit similar states). That gives the surrogate objective:
The inner expectation is still over , which I also do not have samples from (my actions came from ). Fix that with importance sampling: reweight old-policy actions by the ratio of new to old probability,
which is exact (it is just multiplying and dividing by inside the sum over actions). Defining the probability ratio
the surrogate becomes something I can estimate entirely from old-policy rollouts:
This is the objective every trust-region method maximizes. Note and , so at the starting point the surrogate's gradient is exactly the policy gradient. The surrogate is a local model of the true objective that agrees with it to first order, and is trustworthy only while stays near , which is precisely why it must be paired with a trust region.
The two approximations I just made (swapping for , and trusting importance sampling with a ratio that could be anything) are both only valid near . Push far and the surrogate stops predicting the true , the ratio can blow up, and maximizing happily walks you off the cliff. So the surrogate is necessary but not sufficient; it must be constrained.
TRPO: the hard constraint
TRPO makes the trust region explicit. Maximize the surrogate subject to a hard KL constraint:
Approximate the two pieces near . The surrogate is linear to first order, with the policy gradient. The KL constraint is quadratic to second order, because its first-order term vanishes (KL is minimized at zero when the policies match, so its gradient there is zero):
where is the Fisher information matrix (the Hessian of the KL at the origin). So (7.5) becomes: maximize subject to . The Lagrangian solution is the natural gradient:
This is the correct, geometry-aware step: it moves along the gradient preconditioned by the inverse Fisher, so a fixed KL budget translates into an appropriately-sized parameter step no matter how sensitive the local policy is. TRPO computes without forming (which for a large network is astronomically big) using conjugate gradient on Fisher-vector products, then backtracks along the step to enforce the exact constraint and the actual surrogate improvement.
TRPO works and comes with a monotonic-improvement guarantee, but equation (7.6) is a lot of machinery: conjugate-gradient inner loops, Fisher-vector products, a line search, all per update. For a transformer with billions of parameters and a training loop I want to run on one GPU, this is both a memory and an engineering burden. PPO's entire pitch is: get 90% of the trust-region benefit with a first-order method that needs none of that.
PPO: clipping as a soft trust region
PPO throws away the explicit KL constraint and instead bakes the trust region into the objective itself, so that plain SGD/Adam cannot want to move too far. The clipped surrogate is
where and is a small constant, typically to . Two things are happening: the ratio is clipped to the interval , and then the objective takes the minimum of the clipped and unclipped terms. The minimum is what makes it a lower bound (a pessimistic surrogate), and the direction of the clip flips depending on the sign of the advantage. That sign-dependence is the whole design, so I will take it apart case by case.
Fix a single timestep and drop the subscript: ratio (which is at and moves as changes), advantage , clip width . The per-sample objective is . Split on the sign of .
Case 1: (this action was good, I want to increase its probability, i.e. push up). The unclipped term rises without bound as increases. The clipped term is , capped at . Taking the min of the two:
- For : clip does nothing, , ordinary gradient pushing up.
- For : clipped term is the smaller one, so , a constant. Its gradient in is zero.
So once the new policy has made this good action more than times as likely as the old policy did, the objective flatlines and stops rewarding further increases. The incentive to keep climbing is switched off exactly at the trust-region boundary.
Case 2: (this action was bad, I want to decrease its probability, i.e. push down). Now becomes more positive as decreases (a negative advantage times a shrinking ratio), so the objective wants small. The clipped term is (the clip's lower arm binds now), floored at . Because , the min of the two picks the more negative, i.e. the smaller, value:
- For : clip inactive, , gradient pushing down.
- For : clipped term is now the smaller (more negative) term, and since we take the min, , again a constant with zero gradient.
So once the new policy has driven this bad action below times its old probability, the objective flatlines and stops rewarding further suppression. Symmetric to case 1.
The unified statement. The clip removes the gradient incentive to move the ratio further past the boundary in the direction the advantage wants. It does not clip when the ratio moves in the "wrong" direction (a good action becoming less likely, or a bad action becoming more likely); there the full unclipped gradient applies, so PPO can always correct an overshoot, it just refuses to chase one. Taking the min is what guarantees this asymmetry: is a pessimistic lower bound on the unclipped surrogate, tight at and only ever pulling the objective down, never inflating it. That pessimism is the soft trust region.
The geometry is worth stating in plain words because it is the mental model I carry into every PPO and GRPO run. Picture as a function of the ratio , with the old policy sitting at . For a good action, the objective is a ramp going up and to the right that hits a flat ceiling at . For a bad action, it is a ramp going up and to the left (toward smaller ) that hits a flat floor at . In both cases there is a flat plateau beyond the boundary in the "desired" direction and a live slope on the near side and in the "undo" direction. Gradient ascent slides up the ramp until it reaches the plateau, then stops, per token, per sample. No Fisher matrix, no line search, no explicit KL, just a clamp and a min. The clip width is the radius of the trust region measured in ratio space, and meaning "don't let any single token's probability move by more than about 20% per update round" is a genuinely useful one-line summary.
The clip alone does not bound the KL divergence, a subtlety that trips people who read "PPO replaces the KL constraint." Clipping only kills the gradient once a ratio is already outside ; a single large update, or many small correlated ones, can still carry the ratio far past the boundary before the gradient dies, and the min does not pull it back, it only stops pushing. That is why practical PPO for LLMs keeps a separate explicit KL penalty against a reference policy in the reward (the per-token KL I mentioned in the GAE chapter), and why implementations often add early-stopping on measured KL per epoch. The clip is a soft, per-token trust region; it is necessary but, on its own, not a hard guarantee. Hold this thought, because GRPO's KL term is exactly this separate penalty, and DAPO's "clip-higher" is exactly a modification of .
Tooling
The tool is again TRL's PPOTrainer, now read through the lens of equation (7.7). One PPO iteration does four things: generate responses from the current policy (this snapshots , whose log-probs are cached), score them with a reward model or verifier and compute per-token rewards including the KL-to-reference penalty, compute advantages with GAE from the previous chapter, then take several gradient epochs over the batch optimizing plus the clipped value loss plus an entropy bonus. The ratio is computed as exp(new_logprob - old_logprob) per token, where old_logprob is the cached value from generation time and new_logprob comes from a fresh forward pass under the current ; this is why PPO does multiple gradient steps per rollout batch (the " epochs"), because after the first step and the ratio genuinely starts to move away from 1, which is the entire point of the clip.
"On-policy" is a spectrum in practice, and the ratio is what buys the slack. Strictly on-policy would mean one gradient step per rollout, wasting the expensive generation. Importance sampling (equation 7.3) lets PPO reuse each batch for several epochs by correcting for the growing mismatch between and the that generated the data, and the clip is what keeps that reuse from going off the rails when the ratio drifts too far. So the clip is not just a safety rail on step size, it is what makes PPO sample-efficient enough to afford at all: without it you could not safely take more than one step per generation, and generation is the costliest part of the loop. On the baseline machine (RTX 5080 16GB), where a generation pass with vLLM is the throughput bottleneck, squeezing 2-4 gradient epochs out of each rollout batch is the difference between a training run that finishes overnight and one that does not.
PPO's memory footprint on 16GB is dominated by carrying four model roles at once: the policy being trained (weights + grads + optimizer state), the value model (chapter 6, cheap if it is a shared head, expensive if separate), a frozen reference policy for the KL penalty (weights only, no grads, but still a full forward pass), and the reward model or verifier (weights only, or free if the verifier is a Python function, which is exactly the RLVR advantage the thesis leans on). For a 1.5-3B policy in BF16 this is tight but feasible on the RTX 5080 if the value head is shared and the reference/reward passes run in no_grad; push to a separate value network or a large reward model and you overflow. Record the peak with torch.cuda.max_memory_allocated() per configuration (measured on the baseline machine — record value, date, driver). This four-model burden is precisely what GRPO attacks: drop the value model (chapter 6's expensive optional thing) and, in the RLVR setting, drop the reward model too by using a verifier, and suddenly you are carrying policy plus a frozen reference, which is what fits comfortably on one card.
Lab
The goal of this lab is to make the clip's geometry undeniable by plotting itself, for both advantage signs, and overlaying the unclipped surrogate so you can see exactly where and why the objective flattens. This is the picture from the derivation, rendered from the same arithmetic PPO runs on every token. It is pure math, no GPU, no model, and it runs instantly. Seeing the two ramps-into-plateaus once is worth more than re-reading the min-of-clip formula ten times.
This is a uv project. From the repo root:
uv init labs/ppo-clip-geometry
cd labs/ppo-clip-geometry
uv add numpy matplotlib
"""Plot the PPO-clip per-sample objective as a function of the ratio r.
Reproduces the piecewise picture from the derivation: for A>0 a rising ramp
that ceilings at r=1+eps, for A<0 a rising-to-the-left ramp that floors at
r=1-eps. Overlays the unclipped surrogate r*A to show where the clip bites.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
OUT = Path("artifacts"); OUT.mkdir(exist_ok=True)
EPS = 0.2
def clipped_obj(r, A, eps=EPS):
unclipped = r * A
clipped = np.clip(r, 1 - eps, 1 + eps) * A
return np.minimum(unclipped, clipped)
def main():
r = np.linspace(0.0, 2.0, 400)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharey=False)
for ax, A, title in [(axes[0], +1.0, r"$\hat{A} > 0$ (good action)"),
(axes[1], -1.0, r"$\hat{A} < 0$ (bad action)")]:
ax.plot(r, r * A, "--", color="0.6", label=r"unclipped $r\hat{A}$")
ax.plot(r, clipped_obj(r, A), color="C0", lw=2.4, label=r"$L^{CLIP}$")
for x in (1 - EPS, 1.0, 1 + EPS):
ax.axvline(x, color="0.85", lw=1, zorder=0)
ax.set_title(title); ax.set_xlabel(r"ratio $r_t(\theta)$")
ax.set_ylabel("per-sample objective"); ax.legend(loc="best")
ax.annotate(r"$1-\epsilon$", (1 - EPS, ax.get_ylim()[0]),
ha="center", va="bottom", fontsize=8, color="0.4")
ax.annotate(r"$1+\epsilon$", (1 + EPS, ax.get_ylim()[0]),
ha="center", va="bottom", fontsize=8, color="0.4")
fig.suptitle(r"PPO-clip geometry ($\epsilon = 0.2$): the plateau is the trust region")
fig.tight_layout()
png = OUT / "clip_geometry.png"
fig.savefig(png, dpi=130)
# Print the four regimes as a table so the plateaus are checkable numerically.
print("A>0: r=0.9 -> L=%.2f r=1.3 -> L=%.2f (ceiling at 1+eps)"
% (clipped_obj(0.9, 1.0), clipped_obj(1.3, 1.0)))
print("A<0: r=1.1 -> L=%.2f r=0.7 -> L=%.2f (floor at 1-eps)"
% (clipped_obj(1.1, -1.0), clipped_obj(0.7, -1.0)))
print(f"Artifact: {png.resolve()}")
if __name__ == "__main__":
main()
Run it:
uv run python clip_geometry.py
Read the plateaus carefully against the derivation, because the intuition most people carry is half wrong. On the panel the objective flatlines for (stop chasing a good action once it is already 20% more likely), but it keeps a live downward slope for where a good action has become less likely, so PPO will still fight to bring it back. On the panel it is mirrored: flat for , but a live slope for where a bad action got more likely. The clip is one-sided per case; it disables the incentive to overshoot in the desired direction while leaving the correction always available. If your mental model was "PPO clips whenever the ratio leaves the band," this plot is the correction: it clips only on the far side of the direction the advantage is pushing.
What you should see. Two panels. Left (): a straight line rising with that abruptly goes flat at , with the dashed unclipped line continuing up past it, so the gap between them is the reward PPO is deliberately declining. Right (): a line rising as decreases, going flat at , again with the unclipped dashed line diverging below. The four printed numbers pin the plateaus: for , gives exactly (clamped) while gives (unclamped); for , gives (clamped) while gives (unclamped, still being corrected). That asymmetry, live on the correction side and flat on the overshoot side, is the entire trust-region behavior of PPO in one figure, and it is the picture I want in your head when GRPO reuses this exact clip in the next chapter.
Read this against [RLHF] ch. 6 for the RLHF-specific PPO recipe (the per-token KL reward, the value head, the practical loss composition) and [BRM] ch. 6, which walks the clipped objective and its implementation in the reasoning-model context this thesis targets. My derivation of the surrogate from the performance-difference lemma (equations 7.1-7.4) is the theoretical backing both treatments assume; the primary sources are Schulman et al. (2015) "Trust Region Policy Optimization" for equations (7.5)-(7.6) and Schulman et al. (2017) "Proximal Policy Optimization Algorithms" for the clip (7.7).
"Why you can't just turn up the learning rate on a policy." The one-paragraph version of the whole cliff problem: in supervised learning a bad step is corrected by the next batch, but in RL the policy generates the next batch, so a bad step poisons its own future data and there is no fixed ground truth to fall back to. TRPO's answer is a careful KL-constrained natural-gradient step (correct, expensive); PPO's answer is a clip so simple it fits on one line, and the post's payoff is the geometry: two ramps into two plateaus, live on the side that corrects mistakes and flat on the side that would chase them. The hook is that the most-deployed RL algorithm on earth is, geometrically, just "stop pushing once you've pushed enough," and you can plot the whole thing in twenty lines with no GPU.