Chapter 6 · Book pages 151–163
DPO — Direct Preference Optimization
The big picture
Chapter 5's PPO works, but it is heavy: 4 models in memory (policy, reference, reward model, value head), complex RL infrastructure, and notorious instability. DPO asks a disarming question: can we skip the RL and learn directly from preferences? The key insight — the optimal policy under the RLHF objective (reward maximization + KL penalty) has a closed-form solution, so a supervised loss can implicitly optimize the same objective.
Stop 1 — The derivation: how RL disappears
Four steps. (1) Start from the RLHF objective: maxπ E[r(x,y)] − β DKL[π ‖ πref]. (2) Its optimal solution is known in closed form: π*(y|x) = (1/Z(x)) πref(y|x) exp(r(x,y)/β), with Z(x) the partition function. (3) Rearrange to express the reward in terms of the policy: r(x,y) = β log(π*(y|x)/πref(y|x)) + β log Z(x). (4) Substitute into the Bradley-Terry preference model P(yw ≻ yl) = σ(r(yw) − r(yl)) — and Z(x) cancels, leaving a loss over preference pairs alone:
LDPO(θ) = −E(x,yw,yl) log σ( β log [πθ(yw|x)/πref(yw|x)] − β log [πθ(yl|x)/πref(yl|x)] ) (6.1)
Go deeper: why the cancellation is the whole trick
Z(x) is intractable (it sums over all possible responses), which is exactly why RLHF needed RL in the first place. Because Bradley-Terry only sees reward differences, the β log Z(x) term appears once for yw and once for yl and cancels — the unreachable constant drops out, and the reward model never needs to be trained: the policy's own ratio against the reference is the reward. Book §6.1–6.2, p. 151.
Stop 2 — Gradient analysis and the worked example
Differentiating eq. 6.1 decomposes the gradient into a direction and a weight:
∇θL = −β · σ(−r̂w + r̂l) · [ ∇θ log πθ(yw|x) − ∇θ log πθ(yl|x) ] (6.2)
The bracket raises the chosen response's probability and lowers the rejected one's. The weight σ(−r̂w + r̂l) is largest when the model currently prefers the wrong answer — DPO automatically focuses learning on the confusing pairs.
Interactive · The worked example as a calculator
Slide the four log-probabilities and β — implicit rewards, margin, sigmoid weight, and loss update live. Defaults reproduce the book's numbers.
Go deeper: reading the example's dynamics
Notice the model assigns higher absolute probability to the rejected response (−12.8 beats −15.3) — but DPO doesn't compare absolutes, it compares deviations from the reference. Chosen gained +0.8 nats over the reference while rejected lost 0.3, so the pair is already mildly correctly ranked, the sigmoid sits at 0.527, and learning continues until the margin approaches 1/(2β) = 5 at β = 0.1. Book §6.3, p. 151–152.
Stop 3 — Full token-level mechanics
The key quantity is the log-probability of an entire response — computed as a sum, not an average, over response tokens:
log πθ(y|x) = Σt=1T log πθ(yt | x, y<t) (6.3)
One training example flows through a 7-step forward pass: (1) concatenate [x; yw] and [x; yl], pad within the batch; (2) run both through the policy, collecting logits at every response position; (3) take log-softmax of the actual token at each position; (4) sum over response tokens → logp_chosen, logp_rejected (6.7–6.8); (5) subtract the reference log-probs (pre-computed or a second forward pass) → ratio_w, ratio_l (6.9–6.10); (6) loss = −log σ(β·(ratio_w − ratio_l)); (7) backprop through steps 5→2.
Interactive · The adaptive learning-rate curve
Slide the margin h from negative (model prefers the rejected response!) to large positive and watch the shared scaling factor σ(−β·h) throttle the gradient.
Label masking decides what gets gradient: prompt tokens — no (context only, their logits don't enter the sum); padding — no (masked out); all chosen and rejected response tokens — yes, pushed up and down respectively. And the sum-not-average choice has a subtle consequence: longer sequences naturally score lower (more non-positive terms), so raw-sum DPO implicitly penalizes verbosity — length normalization (used by SimPO, stop 7) fixes the bias but can hurt instruction-following quality; standard unnormalized DPO is more common in production.
Go deeper: pseudocode and the five pitfalls
def dpo_loss(model, ref_model, batch, beta=0.1):
logps_c = get_sequence_logprob(model, batch["chosen"])
logps_r = get_sequence_logprob(model, batch["rejected"])
with torch.no_grad():
ref_c = get_sequence_logprob(ref_model, batch["chosen"])
ref_r = get_sequence_logprob(ref_model, batch["rejected"])
rewards_c = beta * (logps_c - ref_c)
rewards_r = beta * (logps_r - ref_r)
return -F.logsigmoid(rewards_c - rewards_r).mean()
The pitfalls: forgetting to mask the prompt (you optimize prompt likelihood — useless — and corrupt the effective β); using mean instead of sum (different length penalty — must be consistent between πθ and πref); a stale reference model (base instead of SFT checkpoint → KL term dominates, gradients vanish — use the SFT checkpoint); β too large (log-prob differences magnify, the sigmoid saturates, gradients die — start at 0.1, tune in [0.05, 0.5]); β too small (the gradient ∝ β·σ(−βh) vanishes — flat landscape, extremely slow convergence: "permission to move far, but no signal where to move"). Book §6.5.1–6.5.8, p. 153–156.
Stop 4 — When DPO fails, and choosing β
Five failure modes, all consequences of skipping the RL loop: distribution shift (preference data from an old model — the loss optimizes irrelevant examples), no exploration (can't discover behaviors absent from the dataset — stuck in a local optimum), reference collapse (too strong a reference: the policy can't move; too weak: no regularization), data quality (PPO averages over many samples; DPO memorizes individual pairs, so noisy labels poison training), and pair diversity (pairs must cover the full quality spectrum — pairs differing in approach, not just quality, teach richer distinctions).
The β selection guide:
| β | Regime | When to use |
|---|---|---|
| 0.01 | Very aggressive | Only if data is extremely clean and you need large distributional shift |
| 0.05 | Aggressive | Good data, want noticeable improvement over SFT |
| 0.1 | Standard | Default starting point — good balance of quality vs stability |
| 0.2 | Conservative | Noisy data, or model already close to desired behavior |
| 0.5 | Very conservative | Safety fine-tuning where you must not break capabilities |
Interactive · β regime board
Slide β across the five regimes and see the guidance — plus the two ways β itself breaks training.
Go deeper: the two β failure modes, side by side
β too large: differences get magnified, σ saturates, gradients vanish (start 0.1, tune [0.05, 0.5]). β too small: the KL leash loosens in theory, but the gradient scale β·σ(−βh) collapses — flat landscape, extremely slow convergence. The dial has a dead zone on both ends, which is why 0.1 is the default and 0.01 is reserved for extremely clean data. Book §6.5.8, 6.7, p. 156.
Stop 5 — Batch size and scaling
DPO's pairwise loss changes the memory math. The empirical target for the global batch is:
Bglobal ∈ [32, 128] (6.12) · Bglobal = Bmicro × NGPUs × Kaccum (6.13) · Tsequences = 2 × Bmicro (6.14)
Below 32: severe gradient noise in the implicit reward — the policy oscillates between alignment goals (helpfulness vs safety). Above 128: diminishing returns and communication overhead. The pairing multiplier is the gotcha: each instance carries chosen and rejected, so a micro-batch of Bmicro pairs is really 2×Bmicro sequences — and with two model copies resident (policy + reference), models >7B on 80GB GPUs at 4–8K context are pinned to Bmicro ∈ [1, 2]. The book's profiles for Bglobal = 64: single GPU — Bmicro 2, Kaccum 32 (slow, sequential); 8-GPU node — Bmicro 2, Kaccum 4 (high parallel throughput).
precompute_ref_log_probs=True; for 70B models this saves ~140GB across the cluster.Interactive · Batch-size decomposer
Dial Bmicro, GPUs, and accumulation to hit the Bglobal ∈ [32,128] band — then flip the precompute toggle and watch Bmicro headroom open up.
Go deeper: why the multiplier and the eviction work
Memory per micro-batch is dominated by activations over sequences, and DPO doubles both factors: two sequences per pair (6.14), two models in memory. Gradient accumulation (Kaccum) trades wall-clock for memory by summing gradients over K forward passes before stepping. Eviction removes an entire model replica — legal only because the reference never updates (contrast TR-DPO in stop 7, where the reference does move and pre-computation must be redone each sync). Book §6.8.1–6.8.4, p. 156–157.
Stop 6 — TRL implementation
Minimal working DPO with HuggingFace TRL — dataset format {"prompt", "chosen", "rejected"}, config values verbatim from the book:
dpo_config = DPOConfig(
beta=0.1, # KL regularization strength
learning_rate=5e-7, # very low LR for stability
loss_type="sigmoid", # standard DPO loss
max_length=2048,
max_prompt_length=1024, # truncation for prompts
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # effective batch = 16
num_train_epochs=1, # DPO overfits fast — 1 epoch!
warmup_ratio=0.1,
)
trainer = DPOTrainer(
model=model,
ref_model=None, # with LoRA, ref = base model (no copy!)
args=dpo_config,
peft_config=lora_config, # r=64, alpha=16, dropout=0.05, all projections
...
)
num_train_epochs=1: DPO overfits fast — it memorizes pairs (stop 4), so one pass is the book's default. ref_model=None with LoRA: the reference is just the base model with adapters disabled — no second copy needed. Metrics to monitor: train/rewards/chosen, train/rewards/rejected, train/rewards/margins.Go deeper: reading the config against the chapter
beta=0.1 is the stop-4 standard regime; lr 5e-7 is an order below typical SFT rates because the loss landscape is a single sigmoid; batch 2 × accumulation 8 = 16 sits under the stop-5 band's floor of 32 — the book's own minimal example, meant for a single-GPU start rather than the production profiles. The LoRA config (r=64, α=16, dropout 0.05, all seven projection matrices) mirrors chapter 5's PPO setup. Book §6.4, p. 152–153.
Stop 7 — The variant zoo
Eight extensions, each fixing one specific weakness of vanilla DPO. The map first, details in the deep-dives:
| Variant | Fixes | One-line mechanism |
|---|---|---|
| f-DPO | Reverse-KL rigidity | Swap the divergence: any f-divergence generator derivative replaces the log-ratio |
| Robust DPO | Noisy labels | Analytically debias under a known flip rate ϵ |
| TR-DPO | Stale reference | EMA-update the reference toward the policy every Tsync steps |
| EXO | Wrong KL direction | Swap πθ/πref roles to optimize true reverse KL |
| NCA | Likelihood collapse | Add absolute likelihood terms beside the relative margin |
| SLiC-HF | Slow convergence at large margin | Hinge loss with margin δ — zero past the threshold |
| RPO | Discriminates but can't generate | Add an NLL term on the winning response |
| SimPO | Reference model overhead | Reference-free: length-normalized log-prob reward + margin γ |
Go deeper: f-DPO, robust DPO, TR-DPO, EXO
f-DPO: reverse KL is mode-seeking (concentrate on a few high-reward responses — standard DPO, f′(u) = log u); forward KL is mode-covering (spread mass over all plausible responses, f′(u) = −1/u, better diversity); js_divergence (f′(u) = log(2u/(u+1))) is balanced; alpha_divergence (f′(u) = uα−1) interpolates — α = 0 is forward KL, α = 1 reverse KL. Robust DPO: with flip rate ϵ, Lrobust = [(1−ϵ)LDPO(yw,yl) − ϵLDPO(yl,yw)] / (1−2ϵ); ϵ = 0 recovers DPO, ϵ = 0.5 makes the denominator zero (pure noise, no learning); ϵ ∈ [0.05, 0.2] covers most real annotation noise; TRL: loss_type="robust", label_smoothing=0.1. TR-DPO: πref ← α·πθ + (1−α)·πref every Tsync steps (TRL: sync_ref_model=True, ref_model_mixup_alpha=0.6, ref_model_sync_steps=512) — for long runs where KL domination plateaus the loss. EXO: standard DPO optimizes forward KL in reward space though derived from reverse KL; EXO swaps the roles to β log(πref/πθ) — the theoretically correct direction (TRL: loss_type="exo_pair"). Book §6.9.1–6.9.4, p. 158–160.
Go deeper: NCA, SLiC-HF, RPO, SimPO
NCA: LNCA = −log σ(rw) − ½ log σ(−rw) − ½ log σ(−rl) — the absolute terms stop the winning probability from sinking along with the losing one; use small β = 0.01 so the absolute term dominates (TRL: loss_type="nca_pair"). SLiC-HF: L = max(0, δ − β log(πθ(yw)/πref(yw)) + β log(πθ(yl)/πref(yl))) — hinge loss, zero once the margin exceeds δ (TRL: loss_type="hinge"). RPO: L = λ₁LDPO + λ₂LNLL(yw) — the NLL term keeps generation ability for reasoning tasks (TRL: rpo_alpha=1.0). SimPO: implicit reward (β/|y|)·log πθ(y|q), loss adds margin γ — no reference model at all; the book's settings: beta=2.5, simpo_gamma=0.5; length normalization is critical (without it the model prefers long responses); the book calls SimPO "simpler than DPO and more principled than ORPO". Book §6.9.5–6.9.8, p. 161–163.