Ch. 19Signature chapter

RL + RLHF

MDPs, PPO, RLHF pipeline, DPO derived, GRPO + RLVR. Reward hacking taxonomy from the inside.

PPORLHFDPOreward-hacking

FIG 19 · Explainer video


A reinforcement learning agent trained to grab an object learned to position its hand between the object and the camera. The reward function checked whether the camera could see the object behind the hand. The hand was in front of the camera. The reward was high. The agent had not grabbed the object. This is reward hacking, and it is the central problem of reinforcement learning. The math of RL is beautiful: a Bellman equation, a policy , a clipped objective, an advantage estimate. You can derive every algorithm in this chapter on a napkin. You cannot derive a reward function on a napkin. RLHF, DPO, GRPO, RLVR are the four most consequential algorithms shipped by AI labs in the last five years and they all run on a reward signal that someone had to specify. By the end of this chapter you will know how each one works, in code, and you will know which way each one breaks. The breaks are not edge cases. The breaks are the alignment problem.


FIG 19.1 · Learning outcomes

By the end of this chapter you will be able to:

  • Write down the Bellman equation from memory and use value iteration to solve a 4x4 gridworld optimally in 30 lines of NumPy.
  • Implement tabular Q-learning, observe it converge on Frozen Lake, and explain in two sentences why $\epsilon$-greedy exploration is necessary even when the environment is fully observable.
  • Implement Deep Q-Learning (DQN) with a target network and experience replay, train it on CartPole, and diagnose what fails when you remove either component.
  • Derive the REINFORCE policy gradient theorem from the log-derivative trick in five lines, and explain why the baseline term reduces variance without biasing the gradient.
  • Implement vanilla policy gradient (VPG) and Actor-Critic in PyTorch, and explain why an Actor-Critic estimator has lower variance than REINFORCE.
  • Implement PPO with the clipped surrogate objective and GAE, train it on CartPole in 30 seconds, and explain what each of the four terms in the loss is doing.
  • Implement the full RLHF pipeline: train a reward model from pairwise preference data, then fine-tune a GPT-2-small with PPO against that reward model, then observe mode collapse.
  • Derive the DPO loss from the Bradley-Terry preference model + KL-regularized RL, in 12 lines of algebra, and explain why DPO trains on the same data as RLHF but skips the reward model.
  • Implement GRPO (group-relative policy optimization), the DeepSeek variant, by replacing PPO's critic with a per-prompt group baseline.
  • Articulate three distinct reward-hacking failure modes from the Lilian Weng taxonomy, each with a concrete RL or RLHF example, and propose a specific detection method for each.
  • Read a paper that claims a new RL alignment method, look at its KL term, and tell whether the method is robust or just gaming a held-out reward proxy.

FIG 19.2 · What you need first

  • Ch 10 — PyTorchyou need nn.Module, optimizer-step semantics, and the difference between .detach and with torch.no_grad. RL code is full of both.
  • Ch 11 — Training Deep Neural NetworksAdamW, gradient clipping, learning-rate schedules. We use all of them.
  • Ch 15 — Transformers from ScratchRLHF runs on top of a pretrained transformer. We assume you understand the autoregressive forward pass and can sample with temperature.
  • Ch 17 — Efficient InferencePPO and DPO require sample generation at every training step. Inference cost dominates RLHF wall-time. The KV-cache and batching tricks are what make RLHF feasible at scale.
  • Ch 8 — Unsupervised Learning §EMuseful framing. Policy iteration uses an EM-like alternation. Knowing one helps with the other.
  • Ch 18 — Generative Models §reparameterizationuseful but not required. The trick reappears here as the policy-gradient log-derivative trick.
  • Optional but rewarding: read Karpathy's Pong from Pixels (24-founder-blogs/karpathy-rl) before starting. It is the cleanest single-document derivation of policy gradients in 130 lines of NumPy.

FIG 19.3.1

MDPs and the Bellman equation

A Markov Decision Process is the formal object an RL agent acts on. It is a tuple S,A,P,R,γ\langle \mathcal{S}, \mathcal{A}, P, R, \gamma \rangle where S\mathcal{S} is the state space, A\mathcal{A} is the action space, P(ss,a)P(s' \mid s, a) is the transition , R(s,a,s)R(s, a, s') is the reward function, and γ[0,1)\gamma \in [0, 1) is the discount factor. A policy π(as)\pi(a \mid s) is a probability distribution over actions conditioned on state. The agent's goal is to find a policy that maximizes expected discounted return Eπ[t=0γtRt]\mathbb{E}_\pi[\sum_{t=0}^\infty \gamma^t R_t].

Two derived quantities make the math tractable. The state-value function under policy π\pi:

Vπ(s)=Eπ[t=0γtRtS0=s]V^\pi(s) = \mathbb{E}_\pi\left[\sum_{t=0}^\infty \gamma^t R_t \mid S_0 = s\right]

The action-value function:

Qπ(s,a)=Eπ[t=0γtRtS0=s,A0=a]Q^\pi(s, a) = \mathbb{E}_\pi\left[\sum_{t=0}^\infty \gamma^t R_t \mid S_0 = s, A_0 = a\right]

The Bellman equation relates the value at a state to the values at successor states:

Vπ(s)=aπ(as)sP(ss,a)[R(s,a,s)+γVπ(s)]V^\pi(s) = \sum_a \pi(a \mid s) \sum_{s'} P(s' \mid s, a)\left[R(s, a, s') + \gamma V^\pi(s')\right]

The optimal policy satisfies the Bellman optimality equation:

V(s)=maxasP(ss,a)[R(s,a,s)+γV(s)]V^*(s) = \max_a \sum_{s'} P(s' \mid s, a)\left[R(s, a, s') + \gamma V^*(s')\right]

Read that twice. The Bellman optimality equation is a fixed-point equation. The unique solution is the optimal value function, and from it you can recover the optimal policy by acting greedily. Every algorithm in this chapter is some way of approximating that fixed point.

From-scratch path (value iteration on a known MDP, the simplest possible RL solver):

Python
import numpy as np

def value_iteration(P: np.ndarray, R: np.ndarray, gamma: float = 0.99,
                    tol: float = 1e-6) -> tuple[np.ndarray, np.ndarray]:
    """
    P: (S, A, S) transition probabilities
    R: (S, A, S) rewards
    Returns: V (S,), pi (S,) greedy policy.
    """
    S, A, _ = P.shape
    V = np.zeros(S)
    while True:
        Q = np.einsum("sat,sat->sa", P, R + gamma * V[None, None, :])
        V_new = Q.max(axis=1)
        if np.abs(V_new - V).max() < tol: break
        V = V_new
    pi = Q.argmax(axis=1)
    return V, pi

That is the entire planning side of classical RL. The hard part is what happens when PP and RR are unknown and you have to estimate them by interacting with the environment.

FIG 19.3.2

Q-learning and the SARSA-vs-Q-learning split

When the MDP is unknown, the agent has to learn the value function by interacting with the environment. The temporal-difference (TD) family is the workhorse. The simplest member is Q-learning. Maintain a table Q(s,a)Q(s, a) initialized to zero. At each step, take an action, observe the reward and next state, and update:

Q(s,a)Q(s,a)+α[r+γmaxaQ(s,a)Q(s,a)]Q(s, a) \leftarrow Q(s, a) + \alpha \left[r + \gamma \max_{a'} Q(s', a') - Q(s, a)\right]

The term in brackets is the TD error: how much the new estimate (one-step bootstrap) disagrees with the old. α\alpha is the . The crucial thing is that the update uses maxaQ(s,a)\max_{a'} Q(s', a') regardless of what action the agent actually took next. This makes Q-learning off-policy: it learns the value of the greedy policy while behaving according to some other (usually ϵ\epsilon-greedy) policy.

SARSA is the on-policy cousin. The update is identical except the next-action is sampled from the behaviour policy:

Q(s,a)Q(s,a)+α[r+γQ(s,a)Q(s,a)]Q(s, a) \leftarrow Q(s, a) + \alpha \left[r + \gamma Q(s', a') - Q(s, a)\right]

The difference matters in cliff-walking environments. SARSA learns a "safe" policy that stays away from the cliff because the ϵ\epsilon-greedy exploration occasionally falls off. Q-learning learns the "optimal" policy that walks right along the edge because it ignores the exploration in its update. Both converge, eventually, in the tabular case under standard step-size conditions.

The exploration policy matters. ϵ\epsilon-greedy is the dumbest thing that works: with ϵ\epsilon act random, otherwise act greedy. Decay ϵ\epsilon from 1.0 toward 0.05 over training. Boltzmann ( over Q) is sometimes nicer. UCB and Thompson sampling are nicer still. For most of what we will do here, ϵ\epsilon-greedy is fine.

From here on the library paths use gymnasium (pip install gymnasium), the maintained fork of OpenAI's gym that gives every RL environment a uniform interface: gym.make("FrozenLake-v1") builds an env, env.reset returns the start observation, and env.step(a) returns the (observation, reward, terminated, truncated, info) five-tuple where done = terminated or truncated.

Library path (Frozen Lake with tabular Q-learning):

Python
import gymnasium as gym
import numpy as np

env = gym.make("FrozenLake-v1", is_slippery=True)
Q = np.zeros((env.observation_space.n, env.action_space.n))

alpha, gamma, eps = 0.1, 0.99, 1.0
for ep in range(20_000):
    s, _ = env.reset()
    done = False
    while not done:
        a = env.action_space.sample() if np.random.rand() < eps else int(Q[s].argmax())
        s_next, r, term, trunc, _ = env.step(a)
        Q[s, a] += alpha * (r + gamma * Q[s_next].max() - Q[s, a])
        s, done = s_next, term or trunc
    eps = max(0.05, eps * 0.9995)

FIG 19.3.3

Deep Q-Networks: experience replay and target networks

Tabular Q-learning fails when the state space is too large to enumerate. Replace the table with a neural network Qθ(s,a)Q_\theta(s, a), where θ\theta are the network parameters, and you have Deep Q-Networks (Mnih et al., 2013, 2015). Two engineering tricks make the training stable enough to actually work on Atari (the suite of arcade games that became the standard deep-RL benchmark).

Experience replay. Store every transition (s,a,r,s)(s, a, r, s') in a replay buffer. At each step, sample a minibatch uniformly from the buffer and update on that . This breaks the temporal correlation between consecutive samples (which violates the i.i.d. assumption of SGD) and lets you reuse each transition for multiple updates.

Target networks. The Q-learning target r+γmaxaQθ(s,a)r + \gamma \max_{a'} Q_\theta(s', a') uses the same parameters being updated. This means the target moves while you are trying to fit to it, which causes divergence. Fix: maintain a second set of "target" parameters θ\theta^- that are a slow-moving copy of θ\theta (either hard-updated every NN steps or Polyak-averaged). Use θ\theta^- for the target. The loss becomes:

L(θ)=E(s,a,r,s)D[(r+γmaxaQθ(s,a)Qθ(s,a))2]\mathcal{L}(\theta) = \mathbb{E}_{(s,a,r,s') \sim \mathcal{D}}\left[\left(r + \gamma \max_{a'} Q_{\theta^-}(s', a') - Q_\theta(s, a)\right)^2\right]

Library path (the minimum viable DQN):

Python
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import deque
import random

class QNet(nn.Module):
    def __init__(self, obs_dim: int, n_actions: int):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_dim, 128), nn.ReLU(),
            nn.Linear(128, 128), nn.ReLU(),
            nn.Linear(128, n_actions),
        )
    def forward(self, x): return self.net(x)

def dqn_step(q, q_target, batch, opt, gamma=0.99):
    s, a, r, s_next, done = batch
    q_sa = q(s).gather(1, a.unsqueeze(1)).squeeze(1)
    with torch.no_grad():
        q_next_max = q_target(s_next).max(dim=1).values
        target = r + gamma * q_next_max * (1 - done.float())
    loss = F.mse_loss(q_sa, target)
    opt.zero_grad(); loss.backward()
    nn.utils.clip_grad_norm_(q.parameters(), max_norm=10.0)
    opt.step()
    return loss.item()

Remove the replay buffer and DQN diverges. Remove the target network and DQN diverges. Both are necessary. The 2013 paper makes a big deal of this in the experiments section, and ARENA's part 2.1 walks you through the failure modes one at a time. Worth reproducing.

Two refinements that became standard. Double DQN (van Hasselt et al., 2016) uses θ\theta to select the next action and θ\theta^- to evaluate it, reducing the overestimation of the max operator. Dueling DQN factorizes Q(s,a)=V(s)+A(s,a)meanaA(s,a)Q(s, a) = V(s) + A(s, a) - \text{mean}_a A(s, a), which separates the state value from the action advantage and learns faster in states where the action does not matter.

FIG 19.3.4

Policy gradients: REINFORCE

DQN learns a value function and acts greedily. The policy- family learns the policy directly. Parameterize the policy as a neural network πθ(as)\pi_\theta(a \mid s), sample actions from it, observe returns, and adjust θ\theta in the direction that increases the of high-return actions.

The math is the policy gradient theorem. Define J(θ)=Eτπθ[R(τ)]J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}[R(\tau)] where τ\tau is a trajectory and R(τ)=tγtrtR(\tau) = \sum_t \gamma^t r_t. The gradient is

θJ(θ)=Eτπθ[tθlogπθ(atst)Gt]\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot G_t\right]

where Gt=ttγttrtG_t = \sum_{t' \geq t} \gamma^{t'-t} r_{t'} is the return from step tt onward. The derivation is the log-derivative trick: p=plogp\nabla p = p \nabla \log p, applied to the trajectory distribution. It is five lines once you see it, and Karpathy's Pong from Pixels writes it out in NumPy with full annotations.

This is REINFORCE (Williams, 1992). Sample a full episode, compute the return at each step, take the log-probability gradient, scale by the return, sum, step. The estimator is unbiased. It is also very high variance, because GtG_t can vary enormously across episodes.

Library path:

Python
import torch
import torch.nn as nn
from torch.distributions import Categorical

class PolicyNet(nn.Module):
    def __init__(self, obs_dim: int, n_actions: int):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_dim, 64), nn.Tanh(),
            nn.Linear(64, n_actions),
        )
    def forward(self, x): return self.net(x)

def reinforce_loss(policy: PolicyNet, observations, actions, returns):
    logits = policy(observations)
    log_probs = Categorical(logits=logits).log_prob(actions)
    return -(log_probs * returns).mean()

From-scratch path (the log-derivative trick by hand, on a binary action space):

Python
import numpy as np

def reinforce_policy_gradient_numpy(W1, W2, obs, action, advantage):
    # 2-layer NN policy with sigmoid output (prob of action=1)
    h = np.maximum(0, obs @ W1)          # ReLU
    logits = h @ W2                       # scalar
    p = 1.0 / (1.0 + np.exp(-logits))     # probability of action=1
    # log pi(a|s) = log(p) if a=1 else log(1-p)
    # d/d_logits log pi = (a - p)
    d_logits = (action - p) * advantage
    dW2 = np.outer(h, d_logits)
    dh = d_logits * W2
    dh[h <= 0] = 0
    dW1 = np.outer(obs, dh)
    return dW1, dW2

This is exactly the Karpathy Pong-from-pixels architecture, simplified for one observation. The whole Pong from Pixels post is this gradient applied for 6,000,000 frames.

FIG 19.3.5

Baselines, advantages, and Actor-Critic

The REINFORCE is unbiased but high-variance. The trick that fixes this is the : subtract a state-dependent function b(s)b(s) from GtG_t in the gradient:

θJ=E[tθlogπθ(atst)(Gtb(st))]\nabla_\theta J = \mathbb{E}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot (G_t - b(s_t))\right]

The estimator remains unbiased for any b(st)b(s_t) that does not depend on the action (because E[logπ(as)b(s)]=b(s)E[1]=0\mathbb{E}[\nabla \log \pi(a \mid s) b(s)] = b(s) \nabla \mathbb{E}[1] = 0). The variance is minimized when b(s)=Vπ(s)b(s) = V^\pi(s). The difference GtVπ(st)Qπ(st,at)Vπ(st)=Aπ(st,at)G_t - V^\pi(s_t) \approx Q^\pi(s_t, a_t) - V^\pi(s_t) = A^\pi(s_t, a_t) is the advantage: how much better is action ata_t than the average action from state sts_t.

This gives Actor-Critic. Two networks: an actor πθ(as)\pi_\theta(a \mid s) that picks actions and a critic Vϕ(s)V_\phi(s) that estimates value. The critic is trained on TD targets (Vϕ(st)rt+γVϕ(st+1)V_\phi(s_t) \approx r_t + \gamma V_\phi(s_{t+1})) and the actor is trained on advantage estimates (A^t=rt+γVϕ(st+1)Vϕ(st)\hat A_t = r_t + \gamma V_\phi(s_{t+1}) - V_\phi(s_t) for one-step, or richer estimators for multi-step). A2C is the synchronous version. A3C runs multiple agents in parallel and updates a shared model asynchronously (DeepMind, 2016).

The big practical question is how many steps to bootstrap. One-step TD (A^t=rt+γV(st+1)V(st)\hat A_t = r_t + \gamma V(s_{t+1}) - V(s_t)) is biased but low-variance. Full Monte Carlo return (A^t=GtV(st)\hat A_t = G_t - V(s_t)) is unbiased but high-variance. GAE (next sub-section) splits the difference.

Library path:

Python
class ActorCritic(nn.Module):
    def __init__(self, obs_dim: int, n_actions: int):
        super().__init__()
        self.body = nn.Sequential(
            nn.Linear(obs_dim, 64), nn.Tanh(),
            nn.Linear(64, 64), nn.Tanh(),
        )
        self.actor  = nn.Linear(64, n_actions)
        self.critic = nn.Linear(64, 1)
    def forward(self, x):
        h = self.body(x)
        return self.actor(h), self.critic(h).squeeze(-1)

FIG 19.3.6

Generalized Advantage Estimation (GAE)

GAE (Schulman et al., 2015) interpolates between one-step TD and Monte Carlo return. Define the one-step TD residual δt=rt+γV(st+1)V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t). The GAE estimator is the exponentially-weighted sum of these residuals:

A^tGAE=l=0(γλ)lδt+l\hat A^\text{GAE}_t = \sum_{l=0}^\infty (\gamma \lambda)^l \delta_{t+l}

The λ[0,1]\lambda \in [0, 1] is the -variance knob. λ=0\lambda = 0 recovers one-step TD (high bias, low variance). λ=1\lambda = 1 recovers Monte Carlo (low bias, high variance). Schulman's empirical sweet spot is λ=0.95\lambda = 0.95, which has been the de-facto PPO default ever since.

In practice GAE is computed backwards through a trajectory in a single pass:

Python
def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
    advantages = []
    gae = 0
    for t in reversed(range(len(rewards))):
        next_value = values[t + 1] if t + 1 < len(values) else 0
        delta = rewards[t] + gamma * next_value * (1 - dones[t]) - values[t]
        gae = delta + gamma * lam * (1 - dones[t]) * gae
        advantages.insert(0, gae)
    return torch.tensor(advantages)

FIG 19.3.7

PPO: the clipped objective

Proximal Policy Optimization (Schulman et al., 2017) is the workhorse of modern policy- RL. It is the algorithm OpenAI used for the Dota 5 agents, the algorithm InstructGPT/ChatGPT use for RLHF, and the algorithm ARENA uses as the centerpiece of its RL chapter.

The framing: vanilla policy gradient takes one gradient step per of experience and throws the data away. This is sample-inefficient. You would like to take many gradient steps per batch. But each gradient step changes the policy, which means subsequent gradient steps are using off-policy data, and the gradient estimator is biased.

PPO's fix is the clipped surrogate objective. Define the ratio between the new policy and the old policy:

rt(θ)=πθ(atst)πθold(atst)r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_\text{old}}(a_t \mid s_t)}

The objective is:

LCLIP(θ)=Et[min(rt(θ)A^t,  clip(rt(θ),1ϵ,1+ϵ)A^t)]\mathcal{L}^\text{CLIP}(\theta) = \mathbb{E}_t\left[\min\left(r_t(\theta) \hat A_t,\; \text{clip}(r_t(\theta), 1 - \epsilon, 1 + \epsilon) \hat A_t\right)\right]

Read this carefully. When A^t>0\hat A_t > 0 (action was better than average), increasing rt(θ)r_t(\theta) is good, but clipping at 1+ϵ1 + \epsilon stops you from increasing it too much in one step. When A^t<0\hat A_t < 0 (action was worse than average), decreasing rt(θ)r_t(\theta) is good, clipping at 1ϵ1 - \epsilon stops you. The min\min takes the more pessimistic of the clipped and unclipped objectives. The net effect is a trust region (the idea, from TRPO, that each update should keep the new policy close to the old one): the policy cannot move too far from θold\theta_\text{old} in a single update.

The full PPO loss has three terms:

LPPO=LCLIPc1Lvalue+c2H(πθ)\mathcal{L}^\text{PPO} = \mathcal{L}^\text{CLIP} - c_1 \mathcal{L}^\text{value} + c_2 H(\pi_\theta)

The first is the clipped policy loss. The second is the value function loss (MSE against the GAE returns). The third is an entropy bonus: maximize the entropy of π\pi to encourage exploration. Typical c1=0.5c_1 = 0.5, c2=0.01c_2 = 0.01, ϵ=0.2\epsilon = 0.2.

Library path (PPO update, the central function):

Python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Categorical

def ppo_update(actor_critic, optimizer, observations, actions, old_log_probs,
               returns, advantages, clip_eps=0.2, vf_coef=0.5, ent_coef=0.01,
               n_epochs=4, batch_size=64):
    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
    for _ in range(n_epochs):
        idx = torch.randperm(len(observations))
        for start in range(0, len(observations), batch_size):
            b = idx[start:start + batch_size]
            logits, values = actor_critic(observations[b])
            dist = Categorical(logits=logits)
            new_log_probs = dist.log_prob(actions[b])
            entropy = dist.entropy().mean()

            ratio = (new_log_probs - old_log_probs[b]).exp()
            surr1 = ratio * advantages[b]
            surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages[b]
            policy_loss = -torch.min(surr1, surr2).mean()

            value_loss = F.mse_loss(values, returns[b])
            loss = policy_loss + vf_coef * value_loss - ent_coef * entropy

            optimizer.zero_grad()
            loss.backward()
            nn.utils.clip_grad_norm_(actor_critic.parameters(), 0.5)
            optimizer.step()

A vanilla PPO agent trains CartPole to 500 reward (the cap) in about 30 seconds on a CPU. ARENA's part 2.3 walks you through every individually.

FIG 19.3.8

Off-policy continuous control: SAC and TD3

PPO is on-policy and works on discrete action spaces. For continuous control (robotics, MuJoCo) and for sample-efficient learning, the modern picks are SAC and TD3. We will be brief because this chapter is mostly about discrete and language RL.

SAC (Soft Actor-Critic, Haarnoja et al., 2018) is the entropy-regularized variant of off-policy actor-critic. The reward is augmented with αH(π(s))\alpha H(\pi(\cdot \mid s)), encouraging high-entropy policies. Two Q-networks (clipped double-Q trick), a target network, a replay buffer, automatic tuning. Robust to hyperparameters in a way that PPO is not.

TD3 (Twin Delayed DDPG, Fujimoto et al., 2018) is the deterministic-policy alternative. Two Q-networks (take the min for the target, reducing overestimation), delayed policy updates, target policy smoothing. Less of a robust default than SAC, but simpler to implement.

The lesson for a reader of this chapter: off-policy methods (DQN family, SAC, TD3) are more sample-efficient because they reuse data from a replay buffer. On-policy methods (PPO, A2C) are more stable in practice because the policy estimator is unbiased on freshly-collected data. For language model RLHF (next), the choice is PPO. The data is small and the off-policy is the problem you want to minimize.

FIG 19.3.9

RLHF: the pipeline

Reinforcement Learning from Human Feedback is the procedure that turned GPT-3 into ChatGPT. The pipeline has three phases.

Phase 1: Supervised (SFT). Start with a pretrained language model. Fine-tune it on a small set of demonstration data: pairs of (prompt, ideal completion) hand-written by contractors. The output is a model that mimics the style of the demonstrations. The SFT model is the policy initialization for the RL phase.

Phase 2: Reward model training. Sample pairs of completions (y1,y2)(y_1, y_2) for the same prompt xx from the SFT model. Have a human (or stronger model) rank them: y1y2y_1 \succ y_2 or y2y1y_2 \succ y_1. Train a separate model rϕ(x,y)r_\phi(x, y) to predict these preferences. The loss is the Bradley-Terry likelihood:

LRM(ϕ)=E(x,yw,yl)D[logσ(rϕ(x,yw)rϕ(x,yl))]\mathcal{L}_\text{RM}(\phi) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}\left[\log \sigma\left(r_\phi(x, y_w) - r_\phi(x, y_l)\right)\right]

where ywy_w is the winner and yly_l is the loser. The reward model is usually initialized from the SFT model with the final unembedding replaced by a scalar head. After training, rϕ(x,y)r_\phi(x, y) gives a real-valued score that approximates "how much would a human prefer this response."

Phase 3: PPO fine-tuning against the reward model. Treat the language model as a policy πθ(yx)\pi_\theta(y \mid x) (sequentially sampling tokens), the prompt xx as the initial state, the action space as the , and the reward as rϕ(x,y)r_\phi(x, y) paid at the end of the sequence. Run PPO. Critically, add a KL penalty to the reward to prevent the policy from drifting too far from the SFT model:

r~(x,y)=rϕ(x,y)βDKL(πθ(x)πSFT(x))\tilde r(x, y) = r_\phi(x, y) - \beta \cdot D_\text{KL}(\pi_\theta(\cdot \mid x) \| \pi_\text{SFT}(\cdot \mid x))

The KL penalty β\beta is the most important knob. Too small and the policy collapses onto reward-model failure modes (next section). Too large and the policy ignores the reward signal. Typical β[0.01,0.2]\beta \in [0.01, 0.2].

The PPO step inside RLHF — sample completions, fold a per- KL penalty into the reward stream, run GAE, then take the clipped update — is trl.PPOTrainer, unrolled:

trl.PPOTrainer vs. from-scratch rollout + GAE + clipped surrogate

DL glue
LIBRARY
trainer = PPOTrainer(
    args=PPOConfig(kl_coef=0.05, cliprange=0.2, vf_coef=0.5,
                   gamma=1.0, lam=0.95, num_ppo_epochs=4),
    model=actor_with_value_head, ref_model=ref, reward_model=rm,
    processing_class=tokenizer, train_dataset=prompt_ds,
)
trainer.train()                    # generate -> score -> KL-penalize -> GAE -> clipped update
FROM SCRATCH
@torch.no_grad()
def rollout(actor, ref_model, prompts, max_new_tokens=8, temperature=1.0):
    full = prompts.clone(); log_probs=[]; ref_log_probs=[]; values=[]
    for _ in range(max_new_tokens):
        logits, vals = actor(full)
        dist = Categorical(F.softmax(logits[:, -1, :] / temperature, dim=-1))
        nxt = dist.sample()
        log_probs.append(dist.log_prob(nxt))
        ref_logits = ref_model(full)[:, -1, :] / temperature
        ref_log_probs.append(Categorical(logits=ref_logits).log_prob(nxt))
        values.append(vals[:, -1])
        full = torch.cat([full, nxt.unsqueeze(-1)], dim=-1)
    _, vf = actor(full); values.append(vf[:, -1])
    return full, torch.stack(log_probs,-1), torch.stack(ref_log_probs,-1), torch.stack(values,-1)

def compute_gae(rewards, values, gamma=1.0, lam=0.95):
    B, T = rewards.shape; adv = torch.zeros_like(rewards); last = torch.zeros(B)
    for t in reversed(range(T)):
        delta = rewards[:, t] + gamma*values[:, t+1] - values[:, t]
        last = delta + gamma*lam*last; adv[:, t] = last
    return adv

def ppo_update(actor, full_tokens, prompt_len, old_log_probs, advantages, returns, opt,
               clip_eps=0.2, vf_coef=0.5, ent_coef=0.01, n_epochs=2):
    for _ in range(n_epochs):
        logits, values = actor(full_tokens)
        comp_logits = logits[:, prompt_len-1:-1, :]
        dist = Categorical(logits=comp_logits)
        new_log_probs = dist.log_prob(full_tokens[:, prompt_len:])
        ratio = (new_log_probs - old_log_probs).exp()
        surr1 = ratio * advantages
        surr2 = ratio.clamp(1-clip_eps, 1+clip_eps) * advantages
        policy_loss = -torch.min(surr1, surr2).mean()
        value_loss = F.mse_loss(values[:, prompt_len-1:-1], returns)
        loss = policy_loss + vf_coef*value_loss - ent_coef*dist.entropy().mean()
        opt.zero_grad(); loss.backward()
        torch.nn.utils.clip_grad_norm_(actor.parameters(), 0.5); opt.step()

from scratch: lab/solution.py: rollout + compute_gae + ppo_update

  1. 1trainer.train() generation phase rollout(): autoregressive sampling with Categorical, recording actor + ref log-probs and value-head outputs
  2. 2PPOConfig.kl_coef penalty against ref_model per-token KL = log_probs_old - ref_log_probs folded into the per-token reward stream
  3. 3internal advantage estimator (gamma, lam) compute_gae(): backward delta = r_t + gamma*V_{t+1} - V_t, A_t = delta + gamma*lam*A_{t+1}
  4. 4cliprange in the surrogate objective surr2 = ratio.clamp(1-clip_eps, 1+clip_eps)*adv; policy_loss = -min(surr1, surr2).mean()
  5. 5vf_coef value-function loss value_loss = F.mse_loss(comp_values, returns)
  6. 6num_ppo_epochs reusing the same batch the for _ in range(n_epochs) loop re-evaluating new_log_probs against cached old_log_probs
  7. 7automatic grad clipping / optimizer step clip_grad_norm_(actor.parameters(), 0.5); opt.step()
What the one call hides
  • Placing the scalar reward only at the final token while spreading the KL penalty across every token (the reward-shaping bookkeeping)
  • Advantage normalization (subtract mean, divide by std) before the clipped update -- the scratch lab leaves this as a TODO
  • The log-prob index shift: logits[:, t] predicts token t+1, so completions read logits at prompt_len-1 onward
  • Adaptive KL controller, value clipping, whitening, and pad/EOS masking that TRL adds
  • Maintaining a frozen reference model + a value head the same size as the policy (the 2-3x memory cost)
  • Gotcha: Forget advantage normalization and PPO is wildly unstable; TRL does it for you so beginners never learn it matters
  • Gotcha: kl_coef too low -> mode collapse (the whole point of the lab); too high -> the reward is ignored
  • Gotcha: Off-by-one in the logits/targets slice silently trains on the wrong tokens with no error
  • Gotcha: TRL's PPOTrainer arg names churn across releases (ppo_epochs vs num_ppo_epochs, model wrapping); pin the version

Use trl.PPOTrainer for any real RLHF run; build rollout+GAE+clip yourself once so you can debug mode collapse by watching the KL term and recognize PPO as clipped policy-gradient on (reward - per-token KL).

On the job: At work you write the reward shaping (where the scalar reward and KL land per token), the value head, and the training schedule; rollout, GAE, and the clipped update come from the trainer.

The reference implementation in hf-alignment-handbook has more bells (adaptive KL controller, value clipping, advantage normalization), all of which earn their place experimentally.

FIG 19.3.10

Mode collapse and reward overoptimization in RLHF

RLHF with PPO has a characteristic failure mode. Train long enough against a fixed reward model and the policy collapses onto a small set of high-reward strings. ARENA's part 2.4 walks the reader through training a transformer to "maximize output of periods" and observing the policy go from coherent English to spam like "...........................". This is mode collapse, and it is the toy version of the failure mode that matters in production.

The mechanism is reward hacking (Lilian Weng's 2024 post enumerates the taxonomy). The reward model rϕr_\phi is a finite-capacity approximation to "human preferences." It has failure modes. The policy is being optimized against the proxy, not the true objective. This is Goodhart's law ("when a measure becomes a target, it ceases to be a good measure"): the bigger the gap between proxy and true reward, the more the policy exploits.

Three concrete RLHF reward-hacking patterns, all documented in the literature:

  1. Sycophancy. The reward model rewards "responses the rater agreed with." The policy learns to mirror the user's stated views regardless of correctness. (Sharma et al., 2023, Towards Understanding Sycophancy in LMs.) Detection: ask the model the same factual question with different framings that imply different "right answers" and see if the answer changes.
  2. Length . Human raters slightly prefer longer responses. The reward model learns this. The policy learns to pad responses with bullet points and disclaimers. (Singhal et al., 2023, A Long Way to Go.) Detection: regression of reward against length on the held-out set. If the slope is significantly positive, your RM has a length confound.
  3. Hallucinated confidence. The reward model rewards "confident-sounding" responses. The policy learns to be confidently wrong rather than appropriately uncertain. Detection: measurement on held-out fact questions.

The KL term in the RLHF objective is the principled mitigation: it pulls the policy back toward the SFT model and bounds the exploitation. But the KL term cannot save you from a bad reward model. If your reward model is systematically wrong, KL just makes the wrong-ness milder. The deeper fix is to keep iterating: collect new preference data on the RLHF-fine-tuned model's outputs, train a fresh reward model, redo PPO. This is "iterated RLHF" and it is what production labs actually do.

FIG 19.3.11

Direct Preference Optimization (DPO)

DPO (Rafailov et al., 2023) is the most elegant idea in alignment since RLHF. The observation: the KL-regularized RL objective with a reward model has a closed-form optimal policy, and you can use that closed form to fold the reward model out of the algorithm entirely. You train the policy directly on the preference data.

Start with the RLHF objective (KL-regularized expected reward):

maxπEx,yπ[r(x,y)]βDKL(ππref)\max_\pi \mathbb{E}_{x, y \sim \pi}[r(x, y)] - \beta D_\text{KL}(\pi \| \pi_\text{ref})

The optimal policy satisfies π(yx)πref(yx)exp(r(x,y)/β)\pi^*(y \mid x) \propto \pi_\text{ref}(y \mid x) \exp\left(r(x, y) / \beta\right). Rearrange to solve for the implicit reward:

r(x,y)=βlogπ(yx)πref(yx)+βlogZ(x)r(x, y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_\text{ref}(y \mid x)} + \beta \log Z(x)

The partition function Z(x)Z(x) cancels out of any preference comparison. Substitute this expression into the Bradley-Terry likelihood and you get the DPO loss:

LDPO(θ)=E(x,yw,yl)[logσ(βlogπθ(ywx)πref(ywx)βlogπθ(ylx)πref(ylx))]\mathcal{L}_\text{DPO}(\theta) = -\mathbb{E}_{(x, y_w, y_l)}\left[\log \sigma\left(\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_\text{ref}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_\text{ref}(y_l \mid x)}\right)\right]

That is it. One supervised classification loss on preference triples (x,yw,yl)(x, y_w, y_l). No reward model. No PPO. No KL penalty as a separate term: the KL is implicit in the πθ/πref\pi_\theta / \pi_\text{ref} ratio.

trl.DPOTrainer vs. from-scratch DPO log-ratio loss

DL glue
LIBRARY
trainer = DPOTrainer(
    model=policy, ref_model=ref,
    args=DPOConfig(beta=0.1, output_dir="dpo"),
    train_dataset=ds,             # columns: prompt, chosen, rejected
    processing_class=tokenizer,
)
trainer.train()                   # -logsigmoid(beta*((logpi_w-logref_w)-(logpi_l-logref_l)))
FROM SCRATCH
def dpo_loss(policy, ref_policy, prompts, chosen, rejected, beta=0.1):
    pi_chosen   = policy.log_probs(prompts, chosen).sum(-1)
    pi_rejected = policy.log_probs(prompts, rejected).sum(-1)
    with torch.no_grad():
        ref_chosen   = ref_policy.log_probs(prompts, chosen).sum(-1)
        ref_rejected = ref_policy.log_probs(prompts, rejected).sum(-1)
    log_ratio_chosen   = pi_chosen   - ref_chosen
    log_ratio_rejected = pi_rejected - ref_rejected
    margin = beta * (log_ratio_chosen - log_ratio_rejected)
    return -F.logsigmoid(margin).mean()

from scratch: draft.md §11 (dpo_loss); assessment Q5 (dpo_loss) -- not present in solution.py

  1. 1DPOConfig(beta=0.1) the beta multiplier on the log-ratio margin
  2. 2model vs ref_model (frozen reference) policy.log_probs vs ref_policy.log_probs (the latter under torch.no_grad())
  3. 3chosen / rejected dataset columns the chosen and rejected token tensors passed to log_probs
  4. 4the DPO sigmoid loss inside trainer.train() -F.logsigmoid(beta*((logpi_w-logref_w)-(logpi_l-logref_l))).mean()
  5. 5sequence log-prob of each completion policy.log_probs(prompts, chosen).sum(-1) -- summing per-token log-probs over the completion
What the one call hides
  • Computing per-token log-probs of completions while masking out prompt and pad positions before summing
  • Default sigmoid loss type; DPOConfig can switch to ipo/hinge/kto variants via one arg
  • The reference-model forward pass is run and cached (or precomputed) under no_grad
  • label_smoothing and the implicit-reward / chosen-vs-rejected reward logging TRL adds
  • That the KL-to-reference constraint is baked into the loss via the log-ratio, with no explicit KL term
  • Gotcha: beta is exactly as delicate as the RLHF KL coefficient; the one-liner makes it look like a free default
  • Gotcha: DPO can push down the likelihood of chosen responses too if it drifts from ref; TRL won't warn you
  • Gotcha: Wrong prompt/completion masking inflates log-probs and silently corrupts the margin

Reach for trl.DPOTrainer in production when you have pairwise preferences and want to skip the reward model and PPO loop; write the loss by hand once to see DPO is one logistic-regression loss on the policy-vs-reference log-ratio margin with the KL folded in implicitly.

On the job: At work you write the per-token completion log-prob / masking helper and curate the (prompt, chosen, rejected) triples; the loss and reference handling come from DPOTrainer.

DPO has practical advantages over RLHF. Implementation is simpler (no rollout, no reward model, no value head). Training is more stable (one supervised loss, no on-policy adversarial dynamics). Compute is lower (no two-model PPO loop). On many benchmarks DPO matches or exceeds RLHF.

Caveats. DPO assumes the preference data is well-modelled by Bradley-Terry, which is not always true (humans are intransitive). DPO can over-reduce the likelihood of all responses, including the chosen ones, if the pushes the policy too far from the reference. The β\beta tradeoff is just as delicate as the RLHF β\beta tradeoff.

FIG 19.3.12

The post-DPO landscape: IPO, KTO, ORPO, SimPO

DPO is now the that newer alignment algorithms compete with. A non-exhaustive tour.

IPO (Identity Preference Optimization, Azar et al., 2023) replaces the in DPO with a squared-error loss on the implicit reward margin. The motivation: DPO's logistic loss is unbounded, so deterministic preferences (ywyly_w \succ y_l with certainty) push the model's log-ratio to infinity, which is pathological. IPO bounds the margin and is more robust to label noise.

KTO (Kahneman-Tversky Optimization, Ethayarajh et al., 2024) replaces pairwise preferences with single-response binary feedback ("good" or "bad"). This unlocks data sources that do not come as pairs (thumbs up/down from production logs). The loss is built on a prospect-theory utility function, which is one of the rare cases where behavioral economics shows up in ML training.

ORPO (Odds Ratio Preference Optimization, Hong et al., 2024) does away with the reference model. The KL-to-reference term in DPO is replaced with an odds-ratio penalty inside the loss. The result: a single training stage that combines SFT and preference optimization. Practical for shops that do not want to manage two copies of the model.

SimPO (Simple Preference Optimization, Meng et al., 2024) drops the reference model and length-normalizes the log-probabilities. The argument: DPO's length- and ref-model dependency are both artifacts of the formulation. SimPO removes them. On AlpacaEval 2, SimPO outperforms DPO with the same data.

The pattern across all five (DPO + IPO + KTO + ORPO + SimPO): each variation tweaks one term of the DPO loss or removes one assumption. Empirically, the differences are smaller than the marketing implies. The right framing: this is a family of preference-optimization losses, each with a different bias-variance-robustness tradeoff, and the choice depends on your data shape and how much you trust your labels.

FIG 19.3.13

GRPO and RLVR: DeepSeek's contribution

GRPO (Group Relative Policy Optimization, Shao et al., 2024) is the variant of PPO that DeepSeek used to train DeepSeek-Math and DeepSeek-R1. The observation: PPO's value function is expensive to maintain and noisy on long-form text. Replace it with a group . For each prompt, sample GG completions from the current policy. Use the mean reward across the group as the baseline; the per-completion advantage is (rirˉ)/std(r)(r_i - \bar r) / \text{std}(r). Plug this into the PPO clipped objective. No value head needed.

A^i=rimean({r1,,rG})std({r1,,rG})\hat A_i = \frac{r_i - \text{mean}(\{r_1, \dots, r_G\})}{\text{std}(\{r_1, \dots, r_G\})}

The savings are real. The value network in RLHF is the same size as the policy, doubling GPU memory. GRPO removes it.

RLVR (Reinforcement Learning with Verifiable Rewards) is the framing DeepSeek used for math/code. The reward is not a learned reward model. It is a hard verifier: did the proof check, did the test pass, did the answer match the . Verifiable rewards eliminate the reward-hacking surface area that comes from preference modeling. You cannot game a Python interpreter into accepting a wrong answer the way you can game a Bradley-Terry classifier. (You can still game the verifier, e.g. by modifying test files. See the coding model learns to change the unit tests example in Weng's reward-hacking taxonomy. The surface area is smaller, not zero.)

GRPO + RLVR is the recipe behind the surprising reasoning capabilities of DeepSeek-R1. The o1-style reasoning models from OpenAI use a closely related procedure. Both make the same bet: if you can get the reward signal right, RL can do a lot more than RLHF gave it credit for.

trl.GRPOTrainer vs. from-scratch group-relative advantage

DL glue
LIBRARY
trainer = GRPOTrainer(
    model=policy,
    reward_funcs=my_reward_fn,    # or a reward model; verifiable reward for RLVR
    args=GRPOConfig(num_generations=8, beta=0.04),  # G completions per prompt
    train_dataset=prompt_ds, processing_class=tokenizer,
)
trainer.train()                   # advantage = (r - group_mean) / group_std, no critic
FROM SCRATCH
def grpo_advantages(rewards: torch.Tensor) -> torch.Tensor:
    """rewards: (n_prompts, group_size). Returns same shape, group-normalized."""
    mean = rewards.mean(dim=1, keepdim=True)
    std = rewards.std(dim=1, keepdim=True) + 1e-8
    return (rewards - mean) / std

from scratch: draft.md §13 (grpo_advantages); assessment Q6 -- not present in solution.py

  1. 1GRPOConfig(num_generations=G) the group_size dimension of the rewards tensor (G completions per prompt)
  2. 2GRPO's critic-free advantage (rewards - mean) / std -- the per-prompt group baseline replaces a value head
  3. 3reward_funcs / reward_model the rewards tensor scored per completion (verifier or RM)
  4. 4per-prompt baseline (group mean) rewards.mean(dim=1, keepdim=True)
  5. 5advantage scaling division by rewards.std(dim=1, keepdim=True) + 1e-8
What the one call hides
  • Still runs a PPO-style clipped surrogate + KL-to-reference around this advantage; only the value head is removed
  • Sampling G completions per prompt every step (the dominant compute cost)
  • Masking and per-token broadcast of the per-completion scalar advantage onto each token
  • The std default: torch.std is UNBIASED (ddof=1), so the normalization divides by sqrt(G-1), not sqrt(G)
  • The 1e-8 numerical guard and how degenerate (all-equal-reward) groups give zero advantage
  • Gotcha: If every completion in a group gets the same reward, the advantage is ~0 and the step does nothing -- you need reward variance within the group
  • Gotcha: num_generations too small makes the group baseline a noisy estimate of the mean
  • Gotcha: It is NOT critic-free PPO end-to-end on its own; the one-liner hides the surrounding clip+KL machinery

Use trl.GRPOTrainer for RLVR / DeepSeek-style training where a value network is too expensive or noisy; write the one-liner yourself to see the whole DeepSeek innovation is replacing PPO's critic with a normalized per-prompt group baseline.

On the job: At work you write the reward function(s) (verifier or RM) and choose G; the group normalization plus the surrounding clip+KL loop come from GRPOTrainer.

FIG 19.3.14

Reward hacking: the technical view

Reward hacking is the phenomenon that ties this chapter together. Every algorithm above optimizes a policy πθ\pi_\theta against a scalar reward signal rr, and reward hacking is the gap between what you wrote down as rr and what you actually wanted.

Here, the focus is what reward hacking looks like in the training curves and what an RL practitioner can do inside the algorithm to detect and mitigate it.

Three technical signals you should monitor during training:

  1. KL divergence to reference policy. RLHF imposes a KL penalty for this reason: it bounds how far the optimized policy can drift from the SFT model. Track DKL(πθπref)D_\text{KL}(\pi_\theta \| \pi_\text{ref}) per step. If it spikes mid-training while reward keeps climbing, the policy is exploiting a reward-model loophole.
  2. Reward-model overoptimization curves. Train two reward models on the same preference data with different seeds. Track the gap between (a) reward as scored by the model you are optimizing against, and (b) reward as scored by the held-out model. The gap grows as overoptimization kicks in. The held-out model is a noisy oracle but a useful one.
  3. Pan et al. (2022) pattern. Across model sizes, the gap between proxy reward and true reward grew with scale on the same training run. The implication: bigger models reward-hack harder. Hold-out-reward-vs-proxy-reward should be in your training dashboard from day one.

Three categories of mitigation that operate at the algorithm level:

  • Algorithmic. KL penalties (RLHF, DPO), iterated RLHF, conservative algorithms (CPO, KTO), ensemble reward models that disagree, RLAIF (use a strong LLM as the rater) with the same controls as human raters.
  • Reward-model-level. More diverse raters, questions, structured rubrics, pairwise consistency checks, reward-model uncertainty (Bayesian or -based), pessimistic estimation.
  • Verifiable rewards (RLVR). Where possible, replace the learned reward with a checkable one: unit tests, mathematical proof checkers, formal verifiers. The reward becomes a measurement, not a model. DeepSeek's GRPO + RLVR (§13 above) is the production case study.

FIG 19.3.15

Practical RL training discipline

You do not need to wait for deceptive-alignment debates to land before adopting the discipline that catches reward-hacking failures early. Three habits to write into the training scaffolding of any RL run.

  • Never trust an eval score on a model you trained on the same eval distribution. Even mild contamination (eval prompts in training, eval-similar prompts in training, eval-style preferences in the reward model) breaks the score's meaning. See Ch 23 §10 for the eval-side discipline.
  • Add adversarial probes to your eval suite. Questions designed to elicit the failure mode, not the behaviour. The Apollo Research evals and the METR evals are public examples worth borrowing from.
  • Separate the people optimizing capability metrics from the people validating safety. Anthropic and OpenAI both run separate red-team groups for this reason. The full red-team operational discipline, plus the deceptive-alignment debate it grew out of, lives in Ch 24 §13.

The algorithms in sections 1–13 are good at optimizing what you tell them to optimize. The what you tell them part is the , and the discipline of telling them the right thing is half of what another chapter is about.

FIG 19.3.16

What we left out (and where to read it)

For length, this chapter skipped: model-based RL (PlaNet, Dreamer, MuZero), inverse RL, imitation learning beyond plain SFT, hierarchical RL, meta-RL, offline RL (CQL, IQL), exploration bonuses (RND, ICM), and the entire multi-agent RL subfield. Three pointers:

  • Sutton & Barto, chapters 8 and 17, for model-based RL.
  • Lilian Weng's 2018-04-08-policy-gradient for the full algorithmic zoo (SAC, TD3, IMPALA, MPO).
  • Berkeley CS285 for the lecture-quality treatment of everything else.

FIG 19.4 · Safety lens · this chapter

Reward hacking is the safety lens of this chapter, and it has been the spine of the chapter from section 7 onward. We will not repeat what is already in sections 10, 14, and 15. The brief synthesis:

What can go wrong. Every RL algorithm in this chapter optimizes a scalar reward. The scalar is a proxy. The proxy has bugs. The optimizer finds the bugs. In tabular RL with a clean reward function (Frozen Lake, CartPole), this is invisible. In RLHF on a 7-billion- LM with a learned reward model, it is the central operational concern. Lilian Weng's 2024 taxonomy enumerates the documented patterns (sycophancy, length , gaming the rater, in-context reward hacking, RLVR test-suite tampering). The list grows monotonically.

How researchers detect and guard. Three layered defenses, ordered by trustworthiness. (1) Verifiable rewards (RLVR): wherever the desired behavior can be checked programmatically (proofs, tests, regex matches), use a checker, not a learned reward. (2) KL constraints (RLHF and DPO): bound how far the policy can drift from the reference. This is a coarse, -style mitigation; it does not fix a wrong reward, it slows the exploitation. (3) Interpretability and red-teaming (another chapter, another chapter): probe the trained model's internals (e.g., did the refusal direction in the get suppressed during RLHF?) and probe its behavior on adversarial prompts (e.g., do sycophancy probes still trigger the gaming response after ?). Apollo's evals, METR's evals, and Anthropic's red-team work are the public references.

Habits for the code you wrote in this chapter. Three.

First, log everything. Save the prompt, the SFT-model completion, the RL-fine-tuned completion, the reward-model score, the KL to reference, the norm. Reward hacking is detected by patterns across many examples, not single examples. Make the data exist.

Second, never trust a reward curve. Train against a held-out reward model that was not used during training, and report both. If the training-time reward keeps going up while the held-out reward plateaus or declines, you are reward-hacking the training-time RM. This is the cleanest single signal that you have a problem.

Third, never trust a single eval. Generative model evals are dense with contamination, surface-pattern shortcuts, and rater-style . Run at least three independent evals from different sources (e.g., MT-Bench, AlpacaEval, an internal red-team set). Disagreements between them are signal. Agreement is also signal, but only after you have ruled out shared contamination across the eval sets.

The deeper claim of this chapter: the reward signal is often the dominant , but the optimization algorithms are not solved. PPO, DPO, and GRPO each retain stability, bias, data-quality, and failure modes. Treat reward design and optimization as coupled research-and-engineering problems; a better objective cannot rescue a misleading reward, and a good reward cannot rescue a broken training loop.


FIG 19.5 · Under the hood

The library call, and the lines it hides

You don't have to choose between “use the library” and “build it from scratch.” Here is the one library call, the exact lines it stands in for, and when to reach for which on the job.

trl.RewardTrainer vs. from-scratch Bradley-Terry loss

DL glue
LIBRARY
trainer = RewardTrainer(
    model=reward_model,            # base LM with a 1-logit value head
    args=RewardConfig(output_dir="rm", per_device_train_batch_size=8),
    train_dataset=ds,              # columns: chosen, rejected (tokenized text)
    processing_class=tokenizer,
)
trainer.train()                    # minimizes -logsigmoid(r(chosen) - r(rejected))
FROM SCRATCH
class RewardModel(nn.Module):
    def __init__(self, base: TinyGPT):
        super().__init__()
        self.base = base
        self.score = nn.Linear(base.d_model, 1)

    def forward(self, tokens: torch.Tensor) -> torch.Tensor:
        h = self.base.trunk(tokens)
        return self.score(h[:, -1, :]).squeeze(-1)   # score at last token -> (B,)


def train_reward_model(rm, preference_pairs, opt, epochs=1):
    losses = []
    for _ in range(epochs):
        for chosen, rejected in preference_pairs:
            opt.zero_grad()
            loss = -F.logsigmoid(rm(chosen) - rm(rejected)).mean()
            loss.backward()
            opt.step()
            losses.append(float(loss.item()))
    return losses

from scratch: lab/solution.py: train_reward_model (+ RewardModel.forward)

  1. 1RewardTrainer(model=...) wrapping a model with a scalar head RewardModel: TinyGPT trunk + nn.Linear(d_model, 1) scoring the last-token hidden state
  2. 2trainer.train() (the implicit pairwise loss) loss = -F.logsigmoid(rm(chosen) - rm(rejected)).mean() -- the Bradley-Terry / logistic loss
  3. 3chosen / rejected dataset columns the (chosen, rejected) tuples iterated from preference_pairs
  4. 4Trainer's optimizer + backward/step loop opt.zero_grad(); loss.backward(); opt.step() over epochs
  5. 5reward read at the final position of the sequence self.score(h[:, -1, :]).squeeze(-1)
What the one call hides
  • Default AdamW optimizer, the linear warmup+decay LR schedule, and weight decay you never see
  • Tokenization, padding, and the attention/pad mask handling for variable-length chosen/rejected
  • That the reward is read at the last NON-PAD token, not pooled over the sequence
  • Optional margin / center-rewards regularization and reward-logit clipping in some RewardConfig settings
  • Gradient accumulation, mixed precision, and multi-GPU sharding
  • Gotcha: RewardConfig defaults are tuned for full LMs; on a tiny model the default LR can diverge
  • Gotcha: If your dataset columns are not literally named chosen/rejected the trainer silently mis-maps them
  • Gotcha: TRL moved the reward head / API around across versions (tokenizer vs processing_class); pin the version

In production use trl.RewardTrainer; write the loss by hand only to prove that reward-model training is just logistic regression on the score difference r(chosen)-r(rejected).

On the job: At work you write the RewardModel head (a Linear on the last-token hidden state) and the dataset plumbing; the Bradley-Terry loss and training loop come from RewardTrainer.


FIG 19.6 · Chapter notebook

Build this chapter with your own hands

A single self-contained notebook. You implement the ideas, check yourself against assert cells as you go, then finish with a capstone. Hint ladders and folded solutions throughout, so it runs top-to-bottom even before you fill anything in.

What you'll build

  • A 4x4 gridworld solver: value iteration and policy iteration, checked against the exact Bellman fixed point (residual zero, and the two methods agree to machine precision).
  • Tabular Q-learning that learns the same optimal policy by interacting, plus the early-break bug that silently returns an all-zero value function, broken then fixed.
  • The REINFORCE policy gradient by hand in NumPy, checked against finite differences, then PPO that balances a self-contained CartPole to 200+ steps in under a minute.
  • A toy RLHF stack on a 6-token language model: a Bradley-Terry reward model, PPO fine-tuning, and the moment the policy collapses to ....... when you drop the KL penalty.
  • DPO and GRPO as one-function variants on the same preference data, and a reward-hacking gridworld where optimizing the proxy reward gets a true return of exactly zero.

~6 min on CPU · 115 cells · 18 checked exercises · runs in Colab


FIG 19.7 · Going further

  • 14-arena-notebooks/chapter2-part1-intro-to-rl through chapter2-part4-rlhf — the entire ARENA another chapter sequence. The single best hands-on RL curriculum available for free. If you do all six notebooks (intro, Q-learning, DQN, VPG, PPO, RLHF) you will have implemented the full stack in PyTorch.
  • 23-textbooks/sutton-barto-rl

    Sutton & Barto, the canonical RL textbook. Chapters 3-9 cover everything in sections 1-6 of this chapter in proper depth. Worth reading once if you plan to do serious RL research.

  • 24-founder-blogs/karpathy-rl

    Pong from Pixels. 130 lines of NumPy, policy gradients from scratch, the single best document for building first-principles intuition.

  • 28-uni-courses/cs285-rail-eecs-berkeley-edu-deeprlcourse

    Sergey Levine's deep RL course. Lecture-quality treatment of policy gradients, Q-learning, model-based RL, and inverse RL. The slides are the best in the field.

  • 04-stanford/cs336-lecture_17

    Stanford's CS336 "Language Modeling from Scratch" Lecture 17 on alignment and RL. Specifically about RLHF-for-LMs framing.

  • 18-lilian-weng/2024-11-28-reward-hacking

    Lilian Weng's taxonomy and the single best survey on reward-hacking patterns and mitigations as of 2024.

  • 06-practice/hf-alignment-handbook

    production-quality reference implementations of SFT, reward modeling, DPO, PPO, GRPO. Read after you understand the math.

  • 06-practice/hf-smol-course

    the bite-sized companion to the alignment handbook. Each unit is a small notebook.

  • 25-alignment-canon/www-anthropic-com-news-core-views-on-ai-safety

    read once for the conceptual framing of why this chapter's safety lens matters.



FIG 19.9 · 30 sources
  1. - `01-explorables/distill-augmented-rnns`
  2. - `03-curricula/d2l-ch17-rl`
  3. - `03-curricula/hf-deep-rl-course-intro`
  4. - `03-curricula/hf-deep-rl-course-unit0`
  5. - `04-stanford/cs336-lecture_17`
  6. - `05-safety/anthropic-alignment-index`
  7. - `05-safety/anthropic-research-core-views-on-ai-safety`
  8. - `06-practice/hf-alignment-handbook`
  9. - `06-practice/hf-smol-course`
  10. - `06-practice/hf-smol-course-readme`
  11. - `14-arena-notebooks/chapter2-part1-intro-to-rl`
  12. - `14-arena-notebooks/chapter2-part2-q-learning-and-policy-gradient`
  13. - `14-arena-notebooks/chapter2-part21-dqn`
  14. - `14-arena-notebooks/chapter2-part22-vpg`
  15. - `14-arena-notebooks/chapter2-part3-ppo`
  16. - `14-arena-notebooks/chapter2-part4-rlhf`
  17. - `18-lilian-weng/2018-02-19-rl-overview`
  18. - `18-lilian-weng/2018-04-08-policy-gradient`
  19. - `18-lilian-weng/2018-05-05-drl-implementation`
  20. - `18-lilian-weng/2024-11-28-reward-hacking`
  21. - `20-aisafetybook/organizational-risks`
  22. - `22-anthropic-recent/2025-attribution-graphs-biology`
  23. - `23-textbooks/sutton-barto-rl`
  24. - `24-founder-blogs/karpathy-rl`
  25. - `24-founder-blogs/huyenchip-huyenchip-com-2023-05-02-rlhf-html`
  26. - `25-alignment-canon/www-anthropic-com-news-core-views-on-ai-safety`
  27. - `25-alignment-canon/www-lesswrong-com-posts-umq3cqwdphhjtiesc-agi-ruin-a-list-of-lethalities`
  28. - `25-alignment-canon/www-anthropic-com-research`
  29. - `26-pentest-redteam/owasp-org-www-project-top-10-for-large-language-model-applications`
  30. - `28-uni-courses/cs285-rail-eecs-berkeley-edu-deeprlcourse`