Motivation and History

Problem: Vanilla policy gradient updates have no constraint on step size. A single unlucky batch can push the policy into a region where it generates garbage → garbage gets low rewards → next gradient makes things worse → unrecoverable collapse.

Solution history:

  1. TRPO[317] (2015): Constrain KL divergence between old and new policy. Works perfectly but requires expensive second-order optimization (Fisher information matrix, conjugate gradients).
  2. PPO (2017) [319]: Achieve similar stability with a simple first-order clipped objective. 10× simpler to implement, works almost as well, scales to distributed training trivially.

The Clipped Objective

The core innovation of PPO is a clipped surrogate objective that prevents destructively large policy updates while remaining simple to implement.

LCLIP​(θ)=𝔼t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ),1−ϵ,1+ϵ)​A^t)](5.1)

where rt​(θ)=πθ​(at|st)πθold​(at|st) is the probability ratio.

Full PPO Loss

L=LCLIP−c1​(Vθ​(st)−Vttarget)2⏟value loss+c2​H[πθ(⋅|st)]⏟entropy bonus(5.2)
  • Value loss (c1=0.1): Trains the critic to predict returns. Also clipped for stability.
  • Entropy bonus (c2=0.01): Prevents premature convergence to deterministic policy. Critical for exploration.

Derivation of the PPO Gradient and Update Rule

This section traces the mathematical path from the RL objective to the PPO update rule, showing why the clipped surrogate works.

Step 1: The RL Objective

The goal is to maximize expected cumulative reward under the policy:

J⁡(θ)=𝔼τ∼πθ​[∑t=0Trt](5.3)

Step 2: Policy Gradient Theorem

The gradient of J⁡(θ) with respect to policy parameters:

∇θJ​(θ)=𝔼πθ​[∑t=0T∇θ​log​πθ​(at|st)⋅A^t](5.4)

where A^t is the advantage function (how much better action at was compared to the average action in state st). This replaces the full return with the advantage to reduce variance.

Step 3: Importance Sampling for Off-Policy Data

PPO collects data using πθold but updates πθ. To correct for this distribution mismatch, apply importance sampling:

∇θJ​(θ)=𝔼πθold​[πθ​(at|st)πθold​(at|st)​∇θ​log⁡πθ​(at|st)⋅A^t](5.5)

Define the probability ratio rt​(θ)=πθ​(at|st)πθold​(at|st). Using the identity ∇θ​log​f=∇θff, we get:

∇θJ​(θ)=𝔼πθold​[∇θrt​(θ)⋅A^t](5.6)

This means maximizing the surrogate objective:

LCPI​(θ)=𝔼t​[rt​(θ)⋅A^t](5.7)

Step 4: The Problem with Unconstrained Surrogates

LCPI is a valid objective, but without constraints, a single gradient step can push rt​(θ) far from 1.0, causing:

  • Importance weights become extreme → high variance
  • Policy enters untested regions → reward model gives unreliable scores
  • Catastrophic collapse: policy generates garbage, can’t recover

TRPO solution: Constrain DKL(πθold∥πθ)≤δ. Requires second-order methods (expensive).

Step 5: PPO’s Clipped Surrogate (First-Order Approximation)

PPO replaces the hard KL constraint with a clipped objective that achieves similar behavior using only first-order gradients:

LCLIP​(θ)=𝔼t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ),1−ϵ,1+ϵ)​A^t)](5.8)

Derivation of the gradient:

Let Lt=min⁡(rt​A^t,r¯t​A^t) where r¯t=clip​(rt,1−ϵ,1+ϵ).

∇θLt={∇θrt​(θ)⋅A^tif ​rt​A^t<r¯t​A^t​ (unclipped term is smaller)0if ​rt​A^t≥r¯t​A^t​ (clipped term is smaller, gradient = 0)(5.9)

Expanding the conditions:

  • When A^t>0 and rt<1+ϵ: Gradient flows normally — policy is encouraged to increase πθ​(at|st).
  • When A^t>0 and rt≥1+ϵ: Gradient is zero — policy has already increased enough, stop pushing.
  • When A^t<0 and rt>1−ϵ: Gradient flows normally — policy is encouraged to decrease πθ​(at|st).
  • When A^t<0 and rt≤1−ϵ: Gradient is zero — policy has already decreased enough, stop pushing.

Step 6: The Complete PPO Update Rule

Combining the clipped policy loss, value loss, and entropy bonus:

θk+1=θk+α⋅∇θ[LCLIP​(θ)−c1​LVF​(θ)+c2​H​[πθ]](5.10)

where:

Rollout Buffer and Rollouts

In PPO, data management relies on a specialized, short-term storage system known as a Rollout Buffer. Unlike off-policy algorithms (DQN) that store experiences indefinitely in a replay buffer, PPO requires an ephemeral structure to satisfy its on-policy mathematical constraints.

What is a Rollout?

A rollout (trajectory) is a sequence of interactions generated by the agent running its current policy in the environment:

  • The process: The agent observes a state, selects an action, receives a reward, and moves to the next state. It repeats for a fixed number of steps or until the episode ends.
  • In LLMs/RLHF: A rollout consists of taking a prompt from a dataset and letting the language model generate a complete sequence of tokens token-by-token until an end-of-text marker is hit. Each token is one “step.”

The Rollout Buffer

The rollout buffer temporarily stores all data collected during the rollout phase. For every generated token/step, it records:

ℬ={(st,at,log⁡πθold​(at|st),rt,V⁡(st))}t=1T(5.13)
  • st,at,rt: State, action taken, and reward at step t.
  • log⁡πθold​(at|st): Log-probability of taking that action under the exact policy that generated it (needed for ratio computation).
  • V⁡(st): Value function’s baseline prediction (needed for GAE advantage computation).

The Rollout Buffer Lifecycle

The buffer operates in a strict three-phase clockwork cycle:

  1. Collect: The active policy interacts with the environment to fill the buffer with fresh trajectories (for a 70B model with batch=128, max_tokens=512: up to 65K token-level transitions per rollout).
  2. Train: Compute GAE advantages across trajectories. Run K epochs (typically 3–10) of mini-batch gradient descent to update policy weights using the clipped objective.
  3. Purge: The entire buffer is completely wiped clean. Because PPO is on-policy, data generated by the old policy cannot be safely reused for the next update cycle — the ratio rt​(θ) would become stale and the clipping guarantees would break.

PPO for RLHF: The Full Loop

Detailed Mechanics: Logits and Policy Updates

Figure 5.1: PPO end-to-end: from prompt batch through generation, reward scoring, KL computation, advantage estimation,
Figure 5.1:PPO end-to-end: from prompt batch through generation, reward scoring, KL computation, advantage estimation, to clipped policy update. The feedback loop shows the updated policy being used for the next generation step.

PPO manages two distinct parameter states in memory, which share the same neural network topology but hold different weight values during optimization:

Phase 1: Rollout (Data Collection)

During data collection, the agent interacts with the environment for T steps. At each time-step t:

  1. The environment yields the current state/observation st (for LLMs: prompt + tokens generated so far).
  2. State st is passed through the current network snapshot (θold).
  3. The network outputs raw unnormalized values — logitszold — a vector of size |V| (vocabulary size 32K–128K).
  4. Probabilities are computed via Softmax:P⁡(a∣st)=Softmax​(zold)=exp⁡(zold,a)∑j=1|V|exp⁡(zold,j)(5.14)
  5. An action at (next token) is sampled from P⁡(a∣st), and the transition tuple ⟨st,at,rt,st+1⟩ along with log⁡πθold​(at∣st) is stored in the rollout buffer.

Phase 2: Optimization Loop (Mini-Batch Updates)

Once the rollout buffer is full, PPO runs K epochs (typically 3–10) over mini-batches. For every gradient step, logits are generated for both policies using the stored state st:

Old Policy Evaluation (frozen):

zold=f⁡(st,θold)⟶log⁡πθold​(at∣st)=LogSoftmax​(zold)​[at](5.15)

Implementation shortcut: reuse the stored scalar from rollout instead of re-computing.

Live Policy Evaluation (updating):

znew=f⁡(st,θ)⟶log⁡πθ​(at∣st)=LogSoftmax​(znew)​[at](5.16)

Because θ updates after every mini-batch gradient step, znew changes continuously throughout the optimization loop, whereas zold remains perfectly static.

From Logits to Probability Ratio

The core PPO ratio measures how much more or less likely an action is under the new policy vs the old:

rt​(θ)=πθ​(at∣st)πθold​(at∣st)(5.17)

To avoid catastrophic numerical underflow/overflow from dividing raw probabilities, the calculation is performed in log-space:

The ratio is recovered via exponentiation of the difference:

rt​(θ)=exp⁡(log⁡πθ​(at∣st)−log⁡πθold​(at∣st))(5.20)

This ratio is injected into the PPO clipping objective:

ℒCLIP​(θ)=𝔼^t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ), 1−ϵ, 1+ϵ)​A^t)](5.21)

The PPO Weight Lifecycle

PhaseLive θOld θoldRatio rt​(θ)
1. Rollout StartActive copySame active copyAlways 1.0 (by identity)
2. Batch Step 1Computes gradientsFrozen1.0 (initial step)
3. Batch Step NModifying (θ≠θold)FrozenDeviates from 1.0 (e.g., 1.06, 0.94)
4. Clipping ActiveBounded by ϵFrozenTrapped at bound (1±ϵ)
5. Optimization EndHighly optimizedDiscardedN/A
6. Next Cycleθ→θoldReceives fresh θResets back to 1.0

Table 5.1:Evolution of θ and θold across PPO training phases.

Continuous Action Spaces Extension

For continuous action spaces (not typical for LLMs, but important for robotics RL), the network outputs distribution parameters instead of discrete logits:

  • Predicted mean vector μ
  • Predicted standard deviation vector σ

Log-probabilities are computed via the Gaussian log-PDF:

log⁡π⁡(at∣st)=−12​(at−μσ)2−log⁡(σ)−12​log⁡(2​π)(5.22)

The ratio rt​(θ)=exp⁡(log⁡πθ−log⁡πθold) is then computed identically and fed into the same clipping objective.

TRL Implementation

The HuggingFace TRL library [376] provides production-ready implementations of all major RL methods for LLMs.

from trl import PPOConfig, PPOTrainer, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
from peft import LoraConfig
 
# Model setup
model = AutoModelForCausalLMWithValueHead.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    torch_dtype=torch.bfloat16, device_map="auto",
    peft_config=LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj","v_proj","k_proj","o_proj"])
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
 
# PPO config with all critical hyperparameters
ppo_config = PPOConfig(
    learning_rate=1.5e-6,        # Low LR for stability
    batch_size=128,              # Prompts per step
    mini_batch_size=16,          # Gradient accumulation unit
    ppo_epochs=4,                # Epochs per batch (reuse data)
    gamma=1.0,                   # No discounting (single turn)
    lam=0.95,                    # GAE lambda
    cliprange=0.2,               # PPO epsilon
    cliprange_value=0.2,         # Value function clipping
    vf_coef=0.1,                 # Value loss coefficient
    init_kl_coef=0.05,           # Initial KL penalty
    target_kl=6.0,               # Adaptive KL target
    whiten_rewards=True,         # Normalize advantages
    gradient_accumulation_steps=4,
    max_grad_norm=1.0,
)
 
ppo_trainer = PPOTrainer(config=ppo_config, model=model, tokenizer=tokenizer,
    dataset=prompt_dataset, data_collator=collator)
 
# Training loop
for batch in ppo_trainer.dataloader:
    # 1. Generate responses
    query_tensors = batch["input_ids"]
    response_tensors = ppo_trainer.generate(
        query_tensors, max_new_tokens=512, temperature=0.7, top_p=0.9, do_sample=True
    )
    # 2. Score with reward model
    texts = [tokenizer.decode(r, skip_special_tokens=True) for r in response_tensors]
    rewards = [torch.tensor(reward_model.score(q, r)) for q, r in zip(batch["query"], texts)]
    # 3. PPO update (handles KL, GAE, clipping internally)
    stats = ppo_trainer.step(query_tensors, response_tensors, rewards)
    # Monitor: stats["ppo/mean_scores"], stats["ppo/policy/approx_kl"]

Critical Hyperparameters

ParameterTypicalEffect of Getting It Wrongcliprange0.2Too low: no learning. Too high: instability.init_kl_coef0.01–0.1Too low: reward hacking. Too high: stuck at SFT.target_kl4–8Adaptive controller target. Lower = conservative.ppo_epochs4Too many: overfits to batch. Too few: wastes gen compute.learning_rate1−5×10−6Too high: catastrophic forgetting.batch_size64–256Larger = smoother gradients, more gen compute.temperature0.7–1.0Lower: less exploration. Higher: noisier advantages.