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:
- TRPO[317] (2015): Constrain KL divergence between old and new policy. Works perfectly but requires expensive second-order optimization (Fisher information matrix, conjugate gradients).
- 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.
where is the probability ratio.
Full PPO Loss
- Value loss (): Trains the critic to predict returns. Also clipped for stability.
- Entropy bonus (): 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:
Step 2: Policy Gradient Theorem
The gradient of with respect to policy parameters:
where is the advantage function (how much better action was compared to the average action in state ). This replaces the full return with the advantage to reduce variance.
Step 3: Importance Sampling for Off-Policy Data
PPO collects data using but updates . To correct for this distribution mismatch, apply importance sampling:
Define the probability ratio . Using the identity , we get:
This means maximizing the surrogate objective:
Step 4: The Problem with Unconstrained Surrogates
is a valid objective, but without constraints, a single gradient step can push 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 . 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:
Derivation of the gradient:
Let where .
Expanding the conditions:
- When and : Gradient flows normally — policy is encouraged to increase .
- When and : Gradient is zero — policy has already increased enough, stop pushing.
- When and : Gradient flows normally — policy is encouraged to decrease .
- When and : 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:
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:
- : State, action taken, and reward at step .
- : Log-probability of taking that action under the exact policy that generated it (needed for ratio computation).
- : Value function’s baseline prediction (needed for GAE advantage computation).
The Rollout Buffer Lifecycle
The buffer operates in a strict three-phase clockwork cycle:
- 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).
- Train: Compute GAE advantages across trajectories. Run epochs (typically 3–10) of mini-batch gradient descent to update policy weights using the clipped objective.
- 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 would become stale and the clipping guarantees would break.
PPO for RLHF: The Full Loop

Detailed Mechanics: Logits and Policy Updates

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 steps. At each time-step :
- The environment yields the current state/observation (for LLMs: prompt + tokens generated so far).
- State is passed through the current network snapshot ().
- The network outputs raw unnormalized values — logits — a vector of size (vocabulary size 32K–128K).
- Probabilities are computed via Softmax:(5.14)
- An action (next token) is sampled from , and the transition tuple along with is stored in the rollout buffer.
Phase 2: Optimization Loop (Mini-Batch Updates)
Once the rollout buffer is full, PPO runs epochs (typically 3–10) over mini-batches. For every gradient step, logits are generated for both policies using the stored state :
Old Policy Evaluation (frozen):
Implementation shortcut: reuse the stored scalar from rollout instead of re-computing.
Live Policy Evaluation (updating):
Because updates after every mini-batch gradient step, changes continuously throughout the optimization loop, whereas 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:
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:
This ratio is injected into the PPO clipping objective:
The PPO Weight Lifecycle
| Phase | Live | Old | Ratio |
|---|---|---|---|
| 1. Rollout Start | Active copy | Same active copy | Always (by identity) |
| 2. Batch Step 1 | Computes gradients | Frozen | (initial step) |
| 3. Batch Step | Modifying () | Frozen | Deviates from (e.g., , ) |
| 4. Clipping Active | Bounded by | Frozen | Trapped at bound () |
| 5. Optimization End | Highly optimized | Discarded | N/A |
| 6. Next Cycle | Receives fresh | Resets back to |
Table 5.1:Evolution of and 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:
The ratio 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_rateToo high: catastrophic forgetting.batch_size64–256Larger = smoother gradients, more gen compute.temperature0.7–1.0Lower: less exploration. Higher: noisier advantages.