RLHF with PPO
Run the four-model RLHF training loop (policy, reference, reward, value), derive the PPO clipped surrogate objective, and see how the KL penalty prevents reward hacking.
The reward model gives a scalar signal for a complete response, but it is non-differentiable with respect to the token selection (argmax / sampling is not differentiable). Reinforcement learning from human feedback (RLHF) uses policy gradient methods to optimize the language model policy with this reward signal[1, 2].
The RLHF problem formulation
Frame language generation as a Markov Decision Process:
- State : the prompt plus tokens generated so far.
- Action : the next token sampled.
- Reward: at the final step; 0 otherwise. (Sparse terminal reward.)
- Policy : the language model.
We want to maximize:
where is the SFT model (frozen) and controls how far the policy is allowed to drift.
Why the KL penalty? Without it, the policy will maximize the reward model by finding adversarial outputs that score high but are low-quality (reward hacking). The KL term keeps the policy close to the sensible SFT distribution.
The four models in RLHF
At any point during RLHF training, four model instances are active:
- Policy : the LLM being trained. Generates responses; gradients flow through it.
- Reference : the frozen SFT model. Provides the KL baseline.
- Reward model : the frozen preference model. Scores complete responses.
- Value model : predicts the expected future reward from a given state (the critic). Initialized from the reward model backbone.
The value model is needed to compute the advantage for variance reduction in the policy gradient.
Policy gradient: REINFORCE
The basic policy gradient (REINFORCE):
where is the advantage: how much better than expected was the reward. The baseline reduces variance without changing the expectation.
Proximal Policy Optimization (PPO)
REINFORCE has high variance and large gradient steps can destabilize training. PPO (Schulman et al., 2017) adds two stabilization mechanisms[3]:
1. Importance sampling ratio. Reuse old rollouts for multiple gradient steps using importance weights:
2. Clipped surrogate objective. Clip to to prevent excessively large policy updates:
where is the advantage and .
The minimum takes the smaller (more conservative) of the clipped and unclipped objectives, ensuring the policy can improve but cannot make large moves that might overshoot.
The KL-modified reward
In practice, the reward signal per token is the terminal reward minus a KL penalty per token:
This transforms the sparse terminal reward into a dense per-token signal, making the credit assignment problem tractable.
The RLHF training loop
import torch
def rlhf_step(policy, ref_model, reward_model, value_model, prompts, optimizer, beta=0.1, eps=0.2):
# 1. Generate responses with the current policy
with torch.no_grad():
responses = policy.generate(prompts, max_new_tokens=128, do_sample=True)
# Log-probs under reference model
ref_logps = ref_model.log_probs(prompts, responses)
# Reward scores
rewards = reward_model(prompts, responses) # (B,)
# Value estimates for advantage computation
values = value_model(prompts, responses) # (B, T)
# Snapshot of policy log-probs for importance sampling (frozen)
old_logps = policy.log_probs(prompts, responses)
# 2. Compute per-token rewards with KL penalty
policy_logps = policy.log_probs(prompts, responses) # (B, T), with grad
kl_per_token = policy_logps - ref_logps # (B, T)
r_dense = -beta * kl_per_token # (B, T)
r_dense[:, -1] += rewards # terminal reward at last step
# 3. Compute advantages (simplified: use GAE in practice)
advantages = r_dense - values
# 4. PPO objective uses old_logps from the rollout snapshot
# 5. PPO objective
ratio = torch.exp(policy_logps - old_logps) # (B, T)
obj1 = ratio * advantages
obj2 = ratio.clamp(1 - eps, 1 + eps) * advantages
loss = -torch.min(obj1, obj2).mean()
# 6. Value loss (MSE against actual returns)
returns = advantages + values.detach()
value_loss = ((value_model(prompts, responses) - returns) ** 2).mean()
total_loss = loss + 0.5 * value_loss
optimizer.zero_grad()
total_loss.backward()
torch.nn.utils.clip_grad_norm_(policy.parameters(), 1.0)
optimizer.step()
return total_loss.item()Limitations of PPO-RLHF
- Complexity: requires four model instances simultaneously, substantial GPU memory.
- Instability: PPO is sensitive to learning rate, KL coefficient, and advantage normalization.
- Value model: requires training a separate value head, which adds a second optimization loop.
Exercises
Exercise 1. With clip parameter , evaluate the PPO clipped objective for these cases: (a) , ; (b) , ; (c) , . Explain in each case whether the clip is active and what behavior it enforces.
Solution
Step 1. (a) , . Unclipped: . Clipped: , giving . The min is . The clip is active: for a good action the objective stops rewarding pushing above , capping how far the policy moves.
Step 2. (b) , . Unclipped: . Clipped: . The min is , the unclipped term. Here the clip does not protect the policy: a bad action that became more likely () keeps the full negative objective, so the update strongly discourages it.
Step 3. (c) , . Unclipped: . Clipped: , giving . The min is , the unclipped term. For a good action that became less likely, the objective uses the smaller unclipped value, letting the gradient pull back up.
Exercise 2. The per-token KL-modified reward at a non-terminal step is . With , compute when the policy makes a token (a) more likely than the reference, and (b) equally likely. Explain the sign in each case.
Solution
Step 1. (a) The log-ratio is , so . The reward is negative: the policy is penalized for drifting above the reference on this token, which is the KL term pushing back toward the SFT distribution.
Step 2. (b) Equally likely means , log-ratio , so . No penalty where the policy agrees with the reference.
Step 3. The per-token reward is zero exactly when the policy matches the reference and turns negative as the policy diverges, so summed over the response it is the negative KL divergence (times ); only the terminal step adds the actual task reward .
Exercise 3 (code). Implement the PPO clipped surrogate (per token) in numpy and verify the three cases from Exercise 1. Then confirm that for the clipped objective never exceeds , so the per-sample improvement is bounded.
Solution
import numpy as np
def ppo_objective(ratio, adv, eps=0.2):
ratio = np.asarray(ratio, float)
adv = np.asarray(adv, float)
unclipped = ratio * adv
clipped = np.clip(ratio, 1 - eps, 1 + eps) * adv
return np.minimum(unclipped, clipped)
# Exercise 1 cases (a), (b), (c).
obj = ppo_objective([1.5, 1.5, 0.5], [3.0, -3.0, 3.0])
assert np.allclose(obj, [3.6, -4.5, 1.5])
# For A > 0 the objective is capped at (1 + eps) * A regardless of how large ratio gets.
rng = np.random.default_rng(0)
ratios = rng.uniform(0.1, 5.0, 1000)
adv_pos = 2.0
capped = ppo_objective(ratios, np.full_like(ratios, adv_pos))
assert np.all(capped <= (1 + 0.2) * adv_pos + 1e-9)
print("Ex.1 objectives:", np.round(obj, 4))The objectives match , and for positive advantage the clipped surrogate never rises above , the bound that keeps each PPO step from overshooting.
DPO (next lesson) eliminates the reward model and value model entirely by showing that the optimal policy under the KL-penalized RLHF objective can be expressed as a closed-form function of the preference data.
References
- [1]Ouyang, L., et al. (2022). "Training language models to follow instructions with human feedback (InstructGPT)". NeurIPS. arXiv:2203.02155. The canonical three-stage RLHF pipeline (SFT, reward model, PPO with a KL penalty).↩
- [2]Stiennon, N., et al. (2020). "Learning to summarize from human feedback". NeurIPS. arXiv:2009.01325. Applied PPO with a KL-to-reference penalty to optimize a learned reward.↩
- [3]Schulman, J., Wolski, F., Dhariwal, P., Radford, A., & Klimov, O. (2017). "Proximal policy optimization algorithms (PPO)". arXiv:1707.06347. The clipped surrogate objective used as the policy optimizer here.↩