Chapter 6 · Book pages 151–163

DPO — Direct Preference Optimization

Main read ≈ 20 minutes · dotted words have hover/tap definitions · ▸ boxes go deeper · finish with the test (100% to complete)

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.

DPO is the off-policy, no-generation counterweight to chapter 5's on-policy machinery: no rollouts, no reward model at training time, no 60–70% generation bottleneck — just preference pairs and a cross-entropy loss. The price shows up in stop 4 (distribution shift, no exploration). The RLHF-vs-DPO choice is chapter 4's two paradigms made concrete, and chapter 8's variants pick up where this chapter's zoo ends.

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)

Define the implicit reward r̂(x,y) = β log(πθ(y|x)/πref(y|x)). DPO minimizes cross-entropy where the "label" is: chosen should out-score rejected. β controls the required margin — large β demands a big margin and moves the policy aggressively (risk: forgetting); small β accepts a small margin and stays conservative. The reference model acts as a regularizer: the policy must "justify" any deviation from it by showing preference alignment.
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.

The book's worked example ("explain quantum entanglement to a 10-year-old"): log πθ(yw) = −15.3 vs log πref(yw) = −16.1 for the charming chosen answer, log πθ(yl) = −12.8 vs log πref(yl) = −12.5 for the textbook rejected one. With β = 0.1: r̂w = 0.1 × (−15.3 + 16.1) = 0.08, r̂l = 0.1 × (−12.8 + 12.5) = −0.03, so σ(0.11) = 0.527 and the loss is −log(0.527) = 0.64 — the model barely prefers chosen, so the gradient pushes hard. After training, the margin stabilizes around 1/(2β).

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.

Every response token gets a gradient. The token-level gradient (6.11) carries a scaling factor σ(−β·hθ) shared across all tokens in both sequences — an adaptive learning rate: when the model can't tell the pair apart (h ≈ 0) the scale is ≈ 0.5 and learning is aggressive; when the margin is already large the scale is ≈ 0 and the pair is left alone.

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:

βRegimeWhen to use
0.01Very aggressiveOnly if data is extremely clean and you need large distributional shift
0.05AggressiveGood data, want noticeable improvement over SFT
0.1StandardDefault starting point — good balance of quality vs stability
0.2ConservativeNoisy data, or model already close to desired behavior
0.5Very conservativeSafety 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.

Hold the symmetry with chapter 5: PPO's dangers were step-size dials (clip, KL coefficient); DPO's danger is a single margin dial. Both are the same trade-off — learn fast versus stay sane — wearing different math.
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).

The escape hatch: πref is completely static, so pre-compute its log-probs over the dataset once, cache the scalars, and evict the reference from GPU memory — available memory doubles, Bmicro rises from 1–2 to 4–8. In TRL: 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
    ...
)
Two details carry the load. 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:

VariantFixesOne-line mechanism
f-DPOReverse-KL rigiditySwap the divergence: any f-divergence generator derivative replaces the log-ratio
Robust DPONoisy labelsAnalytically debias under a known flip rate ϵ
TR-DPOStale referenceEMA-update the reference toward the policy every Tsync steps
EXOWrong KL directionSwap πθref roles to optimize true reverse KL
NCALikelihood collapseAdd absolute likelihood terms beside the relative margin
SLiC-HFSlow convergence at large marginHinge loss with margin δ — zero past the threshold
RPODiscriminates but can't generateAdd an NLL term on the winning response
SimPOReference model overheadReference-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.

Stop 8 — When to use which

The book's selection rules: f-DPO — JS for a diversity/quality balance, forward KL for creative tasks, reverse KL (standard DPO) for single-correct-answer tasks, alpha to interpolate · robust DPO — whenever labels are noisy (ϵ ∈ [0.05, 0.2]) · TR-DPO — long runs, early KL-domination plateaus, iterative pipelines · NCA — the trigger is watching the winning response probability decrease during DPO · RPO — reasoning tasks (math, code, logic) where the model must generate step-by-step solutions · SLiC-HF — when you want simpler, faster convergence · SimPO — when the reference model's memory and complexity are the problem.
Step back across three chapters: PPO (ch 5) is maximal machinery, maximal flexibility, online; DPO (ch 6) is minimal machinery, offline, and its variants each buy back one thing the simplification gave up. Next up, chapter 7's GRPO deletes the critic instead of the RL loop — the third point of the triangle.

Chapter test