Skip to content

DPO and Its Variants: Skipping the Reward Model

RLHF works. It has also accumulated a lot of scar tissue. You train a reward model on human preferences, then fine-tune a policy against that reward using PPO, while clipping against a reference with a KL penalty so the policy doesn't run off into nonsense. Three models in memory. Three training loops. Two sets of hyperparameters that interact in unintuitive ways. And a reward model that drifts off whenever the policy drifts too far from the distribution it was trained on.

It is a lot of machinery for "take some pairs of responses that humans ranked and shape the model toward the preferred ones." Does it have to be this complicated?

Rafailov et al.'s Direct Preference Optimization from 2023 says no. The move is algebraically clean. They show that the optimal RLHF policy, the one that maximizes expected reward while staying close to the reference in KL, can be written as a closed-form function of the reference policy and the reward. That relationship is invertible: you can express the reward as a function of the policy. Plug that into the preference likelihood and the reward model disappears. What's left is a supervised loss over log-probabilities. One model, one loop, no PPO.

In practice it turned out to be trickier than the paper makes it sound, and a bunch of "almost-DPO" variants have grown up around it. I want to build the intuition for the original loss from first principles, then walk through the main variants (IPO, KTO, ORPO), and then be honest about when DPO-style methods match RLHF and when they don't.

Prerequisites

This post assumes you've read:

Very little math in this post is actually new. What's new is the way a few familiar pieces snap together.


The magic trick, in five lines

The whole derivation is short enough to fit on one page if you don't dwell on the motivation. Let me lay it out flat, then spend the rest of the section pointing at the parts that matter.

RLHF → DPO in five lines
1. RLHF objective
max  E[ r(x, y) ]  −  β · KL(π_θ || π_ref)

Standard RLHF: maximize a reward subject to a KL penalty toward the reference model. That KL is what keeps the policy from drifting into gibberish.

2. Closed-form optimal policy
π*(y | x)  =  (1 / Z(x)) · π_ref(y | x) · exp( r(x, y) / β )

Solve for the policy that maximizes the objective. It's the reference distribution reweighted by exp(reward / β), normalized by a partition function Z(x).

3. Invert to express reward
r(x, y)  =  β · log ( π*(y | x) / π_ref(y | x) )  +  β · log Z(x)

Solve the previous line for r(x, y). The reward is just a log-ratio plus a log-partition term that only depends on x.

4. Plug into Bradley–Terry
P(y_w ≻ y_l | x)  =  σ( r(x, y_w) − r(x, y_l) )

Bradley–Terry says the probability of preferring y_w over y_l is a sigmoid of the reward difference. The log Z(x) terms cancel because they're identical for both responses.

5. The DPO loss
L_DPO  =  − log σ(  β · log π_θ(y_w|x)/π_ref(y_w|x)  −  β · log π_θ(y_l|x)/π_ref(y_l|x)  )

Cross-entropy on a sigmoid of log-prob ratios. No reward model, no RL loop. Just four log-probs and a sigmoid.

Five lines from the RLHF objective to the DPO loss. The punch is in line 4, where the partition function log Z(x) cancels because it's the same for both responses in a preference pair.

Here's the one sentence version. RLHF's optimal policy is the reference distribution tilted by exp(reward/β). That tilt can be inverted: the reward is β times the log-ratio between policy and reference, plus a term that only depends on the prompt. Bradley–Terry turns a reward difference into a preference probability; the prompt-only term cancels in the subtraction; and you're left with a sigmoid of a difference of log-ratios. Training that sigmoid as a binary classifier with cross-entropy is the DPO loss.

Concretely, for a single preference pair (x,yw,yl)(x, y_w, y_l) where ywy_w is the chosen response and yly_l is the rejected one:

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

Read that slowly. Four log-probabilities: the policy on the chosen response, the reference on the chosen response, the policy on the rejected response, the reference on the rejected response. Two log-ratios. One subtraction times β. Sigmoid, negative log.

The things that vanished are the reward model and the PPO loop. What replaced them is a clever reparameterization. The reward isn't gone; it's sitting inside the log-ratio βlogπθ(yx)/πref(yx)\beta \log \pi_\theta(y | x) / \pi_{\text{ref}}(y | x), which is the implicit reward DPO is optimizing. If you want to know what reward your DPO-trained model has learned, you compute exactly that quantity and treat it like a scalar reward on any (x, y) pair.

There's a subtle thing I want to flag. The step where the partition function cancels is only valid if you can write the reward as a function of (x, y) that doesn't depend on any other response in the batch. That's true in Bradley–Terry because you're comparing two responses at a time. It wouldn't be true for a Plackett–Luce ranking over five responses where the normalization mixes all of them. That's one of the reasons DPO variants for richer feedback modalities look different from the plain binary case.


What a DPO training step actually does

Let me switch from algebra to what happens on the GPU.

You have a batch of preference pairs. For each one, you run four forward passes: the policy on ywy_w, the policy on yly_l, the reference on ywy_w, and the reference on yly_l. The reference passes are with no_grad and often cached. The reference doesn't change during training, so once you've computed those log-probs for a given dataset, you can store them and skip those forward passes entirely in subsequent epochs.

You get four log-probabilities per pair. You compute two ratios, subtract, scale by β, pass through log-sigmoid, take the negative mean over the batch, and backprop. Exactly the computational shape of a supervised fine-tuning step, just with a different loss function.

policy vs reference — log π_θ(y) / π_ref(y)
π_ref (reference)-4-2+2+4choseny_w+0.00rejectedy_l+0.00
step 0 — initialized at π_ref
margin = +0.00
β · margin = +0.000 (implicit reward)

Policy and reference are the same model. Both log-ratios are zero; the implicit reward margin is zero.

A DPO update pushes the policy's log-prob on the chosen response up relative to the reference, and pushes the log-prob on the rejected response down. The 'margin' between them is what the loss optimizes. Notice the late step: once the margin is wide, the log-sigmoid keeps pushing — that's the overfitting pressure IPO was designed to fix.

First, β in the loss controls how much weight the loss puts on divergence from the reference. Small β (like 0.01–0.1) means small moves per update and strong anchoring to π_ref. Large β (like 1.0) means the loss becomes more willing to move the policy aggressively. In practice DPO papers almost always use β around 0.1.

Second, notice what happens to a response that the reference already likes and the policy now likes even more. The log-ratio is large and positive. For the chosen side that's great; the loss is happy. But look at the loss landscape for that case: the log-sigmoid has a vanishingly small gradient. Far out along the margin, the loss stops pushing. That sounds like a feature (stop overfitting), but the derivative is still nonzero and SGD will happily keep nudging. On long generations with many pairs clearly ranked, this nudging accumulates and the policy drifts.

Third: the reference is frozen. It sits in memory, but it never updates. This is a structural thing that matters for a later misconception. DPO's "KL regularization" is not the same KL regularization as PPO's. It's implicit, baked into the loss, and it only exists as a pressure toward the reference's distribution on responses in your preference dataset. The policy is completely free to do whatever it wants on prompts and responses that never show up in the pairs.


Worked example

Let me put numbers on this with one concrete pair.

Say the prompt is "Explain transformers in one sentence." The chosen response ywy_w is a reasonable one-sentence explanation. The rejected response yly_l is a joke about robots from Michael Bay movies. We pass each through both the current policy and the frozen reference, and we get four log-probabilities.

one preference pair → one DPO loss value
prompt x
“Explain transformers in one sentence.”
chosen y_w
“Neural nets that use attention to weight which parts of the input matter for each output.”
rejected y_l
“Transformers are a type of robot.”
chosen
log π_θ(y_w | x)-12.4
log π_ref(y_w | x)-13.8
rejected
log π_θ(y_l | x)-18.2
log π_ref(y_l | x)-16.9
log π_θ(y_w)/π_ref(y_w)
-12.4 − (-13.8) = +1.4
log π_θ(y_l)/π_ref(y_l)
-18.2 − (-16.9) = -1.3
margin (implicit reward gap)
m = β · (1.4 − (-1.3)) = 0.1 · 2.7 = 0.270
DPO loss for this pair
L = −log σ(0.270) = 0.567
P(y_w ≻ y_l | x) = σ(0.270) = 56.7%

A preference pair: the human preferred y_w over y_l, given prompt x.

One pair, start to finish. Four log-probs, two ratios, one margin, one loss. Watch how the sign flips: the chosen ratio is positive because the policy has learned to prefer this response over what the reference would have said; the rejected ratio is negative because the policy has learned to suppress it.

The numbers won't look impressive in isolation. A loss of 0.57 and a preference probability of 57% don't sound like a strong signal. But remember, DPO runs this over millions of pairs. Each gradient step moves the four relevant log-probs by a tiny amount. The magic is that those tiny moves are targeted: they push up log π_θ(y_w) and push down log π_θ(y_l) proportional to how wrong the current ordering is. Across a whole dataset, those targeted nudges add up to a policy that consistently prefers the kinds of responses humans ranked higher.


The minimal implementation

If you've ever written an SFT training loop, the DPO loop is surprisingly close. The loss function is the one part that needs thought. Here's the whole thing, stripped of batching tricks and reference-caching machinery.

import torch
import torch.nn.functional as F
 
def dpo_loss(
    policy_chosen_logps: torch.Tensor,   # log π_θ(y_w | x)   shape: (batch,)
    policy_rejected_logps: torch.Tensor, # log π_θ(y_l | x)   shape: (batch,)
    ref_chosen_logps: torch.Tensor,      # log π_ref(y_w | x) shape: (batch,)
    ref_rejected_logps: torch.Tensor,    # log π_ref(y_l | x) shape: (batch,)
    beta: float = 0.1,
) -> torch.Tensor:
    """
    DPO loss for a batch of preference pairs.
 
    Each input is a 1-D tensor of per-sequence log-probabilities. You get those
    by summing per-token log-probs over the response tokens (not the prompt).
    """
    # Log-ratio of policy vs reference on the chosen response
    chosen_logratio = policy_chosen_logps - ref_chosen_logps
    # Log-ratio of policy vs reference on the rejected response
    rejected_logratio = policy_rejected_logps - ref_rejected_logps
 
    # The implicit-reward margin
    margin = beta * (chosen_logratio - rejected_logratio)
 
    # DPO loss: -log σ(margin) = softplus(-margin)
    loss = -F.logsigmoid(margin).mean()
 
    # Handy to log during training: what fraction of pairs the policy ranks correctly
    with torch.no_grad():
        accuracy = (margin > 0).float().mean()
 
    return loss, accuracy
 
# --- minimal training step ---
def dpo_step(policy, ref, batch, optimizer, beta=0.1):
    """
    batch is a dict with:
      - prompt_ids, chosen_ids, rejected_ids (all token tensors)
      - chosen_logp_mask, rejected_logp_mask (0 for prompt, 1 for response)
    """
    # Forward pass — policy gets gradients, reference does not.
    p_chosen = sequence_logp(policy, batch["chosen_ids"], batch["chosen_mask"])
    p_rejected = sequence_logp(policy, batch["rejected_ids"], batch["rejected_mask"])
 
    with torch.no_grad():
        r_chosen = sequence_logp(ref, batch["chosen_ids"], batch["chosen_mask"])
        r_rejected = sequence_logp(ref, batch["rejected_ids"], batch["rejected_mask"])
 
    loss, acc = dpo_loss(p_chosen, p_rejected, r_chosen, r_rejected, beta=beta)
 
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    return loss.item(), acc.item()

The whole DPO loss function is seven lines. The training loop is three more. Compare that to a working PPO-RLHF setup with a reward model, an old-policy buffer, value estimation, advantage computation, clipping, and KL tracking. The appeal of the simpler loop is obvious.


Where DPO falls short, and the variants that patch it

DPO works well, but it doesn't work perfectly. Two clear failure modes have driven most of the variant literature.

The first is overfitting to preference margins. The log-sigmoid in DPO's loss never saturates. On pairs where the policy already prefers ywy_w strongly, the loss is small but nonzero, and its gradient keeps pushing the margin wider. In practice this looks like the policy becoming over-confident on the training pairs (margins going to 5, 6, 8+) while generalization degrades. Azar et al. diagnose this explicitly in their IPO paper and propose replacing the log-sigmoid with a squared distance from a target margin τ:

LIPO=(logπθ(ywx)πref(ywx)logπθ(ylx)πref(ylx)τ)2\mathcal{L}_{\text{IPO}} = \left(\log \frac{\pi_\theta(y_w | x)}{\pi_{\text{ref}}(y_w | x)} - \log \frac{\pi_\theta(y_l | x)}{\pi_{\text{ref}}(y_l | x)} - \tau\right)^2

When the margin reaches τ, the loss bottoms out and stops pushing. The policy is allowed to learn "prefer y_w over y_l by some amount," not "push the margin to infinity."

The second is data format. DPO needs preference pairs. A lot of real feedback is thumbs-up / thumbs-down on single responses, not A-vs-B comparisons. Ethayarajh et al.'s KTO adapts the loss for this setting by drawing on prospect theory from behavioral economics: gains from chosen samples and losses from rejected samples are weighted asymmetrically (usually with losses weighing more, matching the empirical finding that humans are loss-averse). You can run KTO on a dataset that's just (response, thumbs-up-or-down), no pairing required. For a lot of production setups where you're collecting user feedback naturally, this format-friendliness matters more than the exact loss shape.

A third direction, taken by Hong et al.'s ORPO, is to ask whether you even need a reference model at all. ORPO combines the SFT loss and a preference signal into a single objective that uses an odds-ratio penalty. No separate reference forward pass. No two-stage pipeline. You do SFT and preference alignment in the same training run. The trade-off is that ORPO gives up the principled RLHF derivation; it's more of a pragmatic regularizer than a closed-form solution to an objective. But on small models with modest compute, it frequently matches or beats staged DPO with half the moving parts.

preference loss vs margin
-3-1.501.53401234margin m = β · (log π(y_w)/π_ref(y_w) − log π(y_l)/π_ref(y_l))lossDPO
DPO
IPO
KTO
ORPO

DPO: Log-sigmoid. Never saturates on the right side — once the pair is clearly ranked, the loss keeps nudging the margin wider. That's DPO's overfitting pressure.

Each loss plotted as a function of the margin m = β·(chosen log-ratio − rejected log-ratio). DPO never saturates on the right. IPO bottoms out at its target. KTO is asymmetric and bounded. ORPO slopes in the same direction but doesn't reference a frozen model.

There's also SimPO from Meng et al., which drops the reference entirely and uses a length-normalized log-prob margin. It trades some of the theoretical grounding for lower memory footprint (no reference model in memory) and often comparable empirical results. If I had to pick one "useful for practitioners" variant, it would be either SimPO or ORPO, depending on whether you want a reference-free DPO-style loss or a reference-free combined SFT+alignment loss.

These are tools with different trade-offs, not competitive benchmarks waiting to resolve. IPO is useful for overfitting-prone setups, KTO for unpaired feedback, and ORPO or SimPO for smaller models where you're paying attention to wall-clock and memory.


When DPO matches RLHF, and when it doesn't

The cleanest statement I've seen about this comes from Tajwar et al.: preference fine-tuning benefits most from on-policy data, especially in the regime where the policy has enough capacity to be making mistakes the reference model wouldn't. DPO uses offline preference data. The pairs were collected before training started, maybe by ranking outputs from a different model entirely. PPO-RLHF samples from the current policy, ranks those samples with the reward model, and uses the rewards to update. On any prompt where the current policy generates something the offline pairs don't cover, DPO has no gradient signal. PPO does, via the reward model extrapolating from the pairs it was trained on.

This matters most on long generations. The longer the generation, the larger the space of possible outputs, and the more quickly the policy can drift into regions not represented in the offline preference set.

response space — where samples actually live
labeled preference pairs(near π_ref)π_θ samplesgenerated tokens20policy samples near a labeled pair98%

Early generation. The policy's samples overlap the labeled region — DPO sees the pairs it was trained on.

As generations get longer, the policy samples more from regions outside the labeled preference pairs. DPO's gradient signal thins out in exactly the regions the policy is learning to produce. PPO-style online sampling doesn't have this problem because it rewards wherever the policy goes.

This doesn't mean DPO is broken. For short responses, standard helpfulness/harmlessness fine-tuning, and preference datasets that were collected on something close to the model you're training, DPO matches PPO-RLHF closely on most benchmarks at a fraction of the engineering cost. That's why the open community, starting with Zephyr-7B in late 2023 and continuing through Tulu 2, OpenHermes, and a lot of the Nous/Argilla recipes, has standardized on DPO as the default alignment method.

For frontier labs with lots of compute and reasoning-heavy tasks where the policy can wander far from the reference during generation, the picture is more nuanced. OpenAI, Anthropic, and Google have all kept RLHF-style online components in their post-training stacks, often mixed with DPO-style offline passes. The frontier recipe as of 2026 is typically: SFT → one or more rounds of DPO/IPO for broad alignment → one or more rounds of PPO-style online RL for the domains where online matters most (code, math, agentic tasks). The industry didn't "switch to DPO"; it absorbed DPO into a larger toolkit.


Misconceptions

"DPO doesn't need a reward model, so it's reward-model-free." Not really. DPO trains an implicit reward. The quantity βlogπθ(yx)/πref(yx)\beta \log \pi_\theta(y | x) / \pi_{\text{ref}}(y | x) is the reward your model has learned; that's not metaphor, it's what the derivation gives you. Rafailov's paper shows this explicitly: you can take a DPO-trained model and extract a reward function from it that ranks responses coherently. The reward model didn't disappear. It became the policy.

"DPO is just supervised fine-tuning on preferences." The forward pass looks like SFT, but the loss function is not an SFT loss. SFT maximizes log-probability of the target. DPO maximizes the log-odds ratio of the chosen vs rejected response's policy-to-reference log-prob ratios. Those are very different objectives. An SFT loss on ywy_w alone would happily push the probability of all reasonable completions up, with no push-down on rejected responses. DPO's combined push-up-and-push-down is what makes it a preference method.

"You can run DPO without an SFT stage." Usually a worse outcome. The reference model needs to be a reasonable starting point. Without that anchor, the "push up on chosen, push down on rejected" dynamic can send the policy into degeneracy. Almost every DPO recipe in production is SFT → DPO, not DPO-from-base-model. ORPO is the one that deliberately collapses these two stages, and it does so by adding a term that behaves like the SFT loss anyway.

"DPO's β is the same as PPO's KL penalty coefficient." They play analogous roles, in that both control how far the policy can drift from the reference. But they do different things. PPO's KL term penalizes divergence from the reference on the current policy's samples at each step. DPO's β scales the implicit reward margin and only regularizes on the preference pairs in the dataset. A DPO policy is free to be wildly different from π_ref on prompts no one labeled. This is closely related to why DPO struggles with distribution shift on long generations.

What's next

The next post covers Constitutional AI and RLAIF, where we replace human preference labelers with model-generated critiques guided by explicit principles, and see what it buys us at scale.


Additional reading (and watching)