<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Preference-Optimization on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/preference-optimization/</link><description>Recent content in Preference-Optimization on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Wed, 27 May 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/preference-optimization/index.xml" rel="self" type="application/rss+xml"/><item><title>Modern alignment: DPO, KTO, ORPO and SimPO — the sommelier who learns without a reward model</title><link>https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/</link><pubDate>Wed, 27 May 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/</guid><description>&lt;blockquote>
&lt;p>This post is the natural continuation of &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning in production&lt;/a>, which covers the operational cycle (Postgres + SQL queries + hot-swap) that feeds these methods with real data. Here we go inside each one: what each loss optimises, what hypothesis it makes, and why they choose one or the other.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Classic RLHF, the kind in the InstructGPT papers, is practically extinct in production. The reason is not ideological: it is that in 2023 Rafailov and others showed that the reward model &lt;strong>does not need to exist&lt;/strong> as a separate object. With an elegant change of variable, the optimal policy can be trained directly from preference pairs, without going through a reward and without RL. That is &lt;strong>DPO&lt;/strong>. From there the family has branched into four methods that coexist in 2026: &lt;strong>DPO&lt;/strong> when you have &lt;code>(chosen, rejected)&lt;/code> pairs, &lt;strong>KTO&lt;/strong> when you only have a binary 👍/👎 signal, &lt;strong>ORPO&lt;/strong> when you want SFT and preferences in the same pass to save memory, and &lt;strong>SimPO&lt;/strong> when you also want to get rid of the reference model and normalise by length. This post explains exactly what each loss does, proves it with an end-to-end numerical example, gives the real decision table between the four and takes apart the three biases that kill the method in production when nobody watches them.&lt;/p>
&lt;h2 id="you-are-here-tune">You are here: TUNE&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Tune">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#alm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#alm)}&lt;/style>
&lt;defs>&lt;marker id="alm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: TUNE · preference optimization without a reward model&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box active"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> places Tune between Data and Eval. Inside Tune three modalities coexist: SFT (supervised fine-tuning), preference optimization (what this post covers) and agent training / RFT. What follows is the &lt;strong>inside&lt;/strong> of the second modality.&lt;/p>
&lt;h2 id="the-analogy-the-sommelier-who-trains-the-palate-without-a-theory-book">The analogy: the sommelier who trains the palate without a theory book&lt;/h2>
&lt;p>Imagine you want to train a sommelier. You have two routes. The first is teaching them oenological theory: grape varieties, terroirs, vinification methods, barrel types. You give them cards with canonical wine descriptions and ask them to memorise them. That is &lt;strong>SFT&lt;/strong>: &lt;code>(prompt, ideal answer)&lt;/code> pairs. It works as general education, but the sommelier who comes out of it cannot tell two excellent Riojas apart.&lt;/p>
&lt;p>The second route is the blind comparator. You put two identical opaque glasses in front of them. You say &amp;ldquo;this one is better than this one&amp;rdquo;. You do not explain why. You repeat the exercise a thousand times, with a thousand different pairs. After a while, the sommelier does not need you to say it: they have &lt;strong>a trained palate&lt;/strong>. They have not learned oenological theory, they have learned to &lt;strong>discriminate&lt;/strong>.&lt;/p>
&lt;p>DPO, KTO, ORPO and SimPO are four variants of the second route. All four train the model to discriminate, not to memorise. The differences between them are:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DPO&lt;/strong>: two glasses at each tasting, the sommelier always knows which one is the &amp;ldquo;good&amp;rdquo; one and which is the &amp;ldquo;bad&amp;rdquo; one.&lt;/li>
&lt;li>&lt;strong>KTO&lt;/strong>: a single glass each time, they are only told &amp;ldquo;you would like this&amp;rdquo; or &amp;ldquo;you would not like this&amp;rdquo;. No pairs.&lt;/li>
&lt;li>&lt;strong>ORPO&lt;/strong>: the tasting also includes a small embedded theory class (SFT in parallel) so the catalogue is not forgotten.&lt;/li>
&lt;li>&lt;strong>SimPO&lt;/strong>: like DPO, but the sommelier does not compare against &amp;ldquo;what your master would have said&amp;rdquo; (reference model); they compare the two glasses directly, normalising by the amount of liquid in each.&lt;/li>
&lt;/ul>
&lt;p>The analogy is not decorative: the rest of the post is that same idea expressed mathematically.&lt;/p>
&lt;h2 id="why-dpo-exists-rafailovs-trick">Why DPO exists: Rafailov&amp;rsquo;s trick&lt;/h2>
&lt;p>To understand DPO it helps to walk through in thirty seconds what it replaces. Classic RLHF, the InstructGPT kind, has three phases:&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="classic RLHF vs DPO">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.bxa{fill:#ffe6d6;stroke:#444;stroke-width:1.4;rx:8}
.bxb{fill:#d6eaff;stroke:#444;stroke-width:1.4;rx:8}
.bxc{fill:#d9f5d6;stroke:#444;stroke-width:1.4;rx:8}
.bxx{fill:#fff5b0;stroke:#444;stroke-width:1.4;rx:8}
.lt{font:600 13px sans-serif;fill:#222}
.ls{font:400 11px sans-serif;fill:#555}
.h{font:700 13px sans-serif;fill:#222}
.ar{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mrl)}
.aw{stroke:#c0392b;stroke-width:1.8;fill:none;marker-end:url(#mrlx)}
&lt;/style>
&lt;defs>
&lt;marker id="mrl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>
&lt;marker id="mrlx" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c0392b"/>&lt;/marker>
&lt;/defs>
&lt;text x="200" y="20" text-anchor="middle" class="h">Classic RLHF (3 phases, 3 models)&lt;/text>
&lt;rect x="40" y="35" width="160" height="55" class="bxa"/>
&lt;text x="120" y="56" text-anchor="middle" class="lt">SFT&lt;/text>
&lt;text x="120" y="74" text-anchor="middle" class="ls">π_ref ← (prompt, answer)&lt;/text>
&lt;rect x="40" y="110" width="160" height="55" class="bxb"/>
&lt;text x="120" y="131" text-anchor="middle" class="lt">Reward Model&lt;/text>
&lt;text x="120" y="149" text-anchor="middle" class="ls">r_φ ← (prompt, chosen, rejected)&lt;/text>
&lt;rect x="40" y="185" width="160" height="55" class="bxc"/>
&lt;text x="120" y="206" text-anchor="middle" class="lt">PPO (RL)&lt;/text>
&lt;text x="120" y="224" text-anchor="middle" class="ls">π_θ maximises r_φ with KL to π_ref&lt;/text>
&lt;path class="ar" d="M120,90 L120,110"/>
&lt;path class="ar" d="M120,165 L120,185"/>
&lt;text x="220" y="135" class="ls">3 models in memory&lt;/text>
&lt;text x="220" y="150" class="ls">+ on-policy rollouts&lt;/text>
&lt;text x="220" y="165" class="ls">+ unstable, hard to debug&lt;/text>
&lt;text x="560" y="20" text-anchor="middle" class="h">DPO (1 phase, 2 models)&lt;/text>
&lt;rect x="400" y="35" width="160" height="55" class="bxa"/>
&lt;text x="480" y="56" text-anchor="middle" class="lt">SFT (same)&lt;/text>
&lt;text x="480" y="74" text-anchor="middle" class="ls">π_ref ← (prompt, answer)&lt;/text>
&lt;rect x="400" y="125" width="160" height="80" class="bxx"/>
&lt;text x="480" y="146" text-anchor="middle" class="lt">DPO&lt;/text>
&lt;text x="480" y="164" text-anchor="middle" class="ls">π_θ ← (chosen, rejected)&lt;/text>
&lt;text x="480" y="180" text-anchor="middle" class="ls">closed-form loss on log-probs&lt;/text>
&lt;text x="480" y="196" text-anchor="middle" class="ls">no RL, no explicit reward&lt;/text>
&lt;path class="ar" d="M480,90 L480,125"/>
&lt;text x="580" y="220" class="ls" font-style="italic">"the reward is implicit&lt;/text>
&lt;text x="580" y="236" class="ls" font-style="italic">in log π_θ - log π_ref"&lt;/text>
&lt;path class="aw" d="M260,140 L390,140"/>
&lt;text x="325" y="130" text-anchor="middle" class="ls" fill="#c0392b">Rafailov 2023:&lt;/text>
&lt;text x="325" y="158" text-anchor="middle" class="ls" fill="#c0392b">"you only need π_θ and π_ref"&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>Phase 1 – SFT.&lt;/strong> You train the model on &lt;code>(prompt, ideal answer)&lt;/code>. Out comes &lt;code>π_ref&lt;/code>: the reference policy. It is the &amp;ldquo;educated&amp;rdquo; model.&lt;/p>
&lt;p>&lt;strong>Phase 2 – Reward model.&lt;/strong> On the same model (a different head), you train a regressor: given &lt;code>(prompt, chosen, rejected)&lt;/code>, it learns to give a higher score to the &lt;code>chosen&lt;/code>. Out comes &lt;code>r_φ(x, y)&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Phase 3 – PPO.&lt;/strong> You take &lt;code>π_ref&lt;/code> as the starting point and train another copy &lt;code>π_θ&lt;/code> so that it &lt;strong>maximises &lt;code>r_φ(x, π_θ(x))&lt;/code>&lt;/strong> with a KL penalty to keep it from drifting too far from &lt;code>π_ref&lt;/code>. That requires generating rollouts (on-policy decode at every step), keeping the three models in memory (&lt;code>π_θ&lt;/code>, &lt;code>π_ref&lt;/code>, &lt;code>r_φ&lt;/code>), and a classic RL setup: unstable, sensitive to hyperparameters and famous for its reproducibility problems.&lt;/p>
&lt;p>Rafailov&amp;rsquo;s observation was: &lt;strong>phase 3 has a closed-form solution&lt;/strong>. If you write out the exact optimisation problem PPO solves&lt;/p>
$$\max_{\pi_\theta} \; \mathbb{E}_{x \sim D, \, y \sim \pi_\theta(\cdot|x)} \big[ r_\phi(x,y) \big] - \beta \, \mathrm{KL}\big(\pi_\theta(\cdot|x) \,\|\, \pi_\mathrm{ref}(\cdot|x)\big)$$
&lt;p>it turns out the optimal policy has the form&lt;/p>
$$\pi^{*}(y|x) = \frac{1}{Z(x)} \, \pi_\mathrm{ref}(y|x) \, \exp\!\left( \tfrac{1}{\beta} r_\phi(x,y) \right)$$
&lt;p>and from there you can solve for the implicit reward:&lt;/p>
$$r_\phi(x,y) = \beta \log \frac{\pi^{*}(y|x)}{\pi_\mathrm{ref}(y|x)} + \beta \log Z(x).$$
&lt;p>The second term is a function of &lt;code>x&lt;/code> only and cancels out when you compare two answers to the same prompt. &lt;strong>The reward does not need to be learned&lt;/strong>: it is implicit in the log-prob ratio between the trained model and the reference one. If you plug that into the Bradley-Terry model for preferences (the formula that says &amp;ldquo;the probability that &lt;code>yw&lt;/code> is preferred to &lt;code>yl&lt;/code> is &lt;code>σ(r(x,yw) - r(x,yl))&lt;/code>&amp;rdquo;), out comes the &lt;strong>DPO loss&lt;/strong>:&lt;/p>
$$\mathcal{L}_\mathrm{DPO}(\pi_\theta;\pi_\mathrm{ref}) = -\,\mathbb{E}_{(x, y_w, y_l) \sim D} \log \sigma\!\Big( \beta \big[ \log \tfrac{\pi_\theta(y_w|x)}{\pi_\mathrm{ref}(y_w|x)} - \log \tfrac{\pi_\theta(y_l|x)}{\pi_\mathrm{ref}(y_l|x)} \big] \Big).$$
&lt;p>That is the whole of DPO. &lt;strong>There is no reward model, no RL, no rollouts.&lt;/strong> Only log-probs over static data.&lt;/p>
&lt;h2 id="dpo-with-real-numbers">DPO with real numbers&lt;/h2>
&lt;p>The formula is less intimidating when you evaluate it with an example. Imagine a pair from the dataset:&lt;/p>
&lt;ul>
&lt;li>&lt;code>x&lt;/code> = &amp;ldquo;Explain what a KVM switch is&amp;rdquo;.&lt;/li>
&lt;li>&lt;code>y_w&lt;/code> = correct answer (chosen).&lt;/li>
&lt;li>&lt;code>y_l&lt;/code> = confusing answer (rejected).&lt;/li>
&lt;/ul>
&lt;p>After a forward pass we have four numbers (sums of per-token log-probs, negative sign because each &lt;code>log p_token ≤ 0&lt;/code>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quantity&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>log π_θ(y_w | x)&lt;/code>&lt;/td>
&lt;td>−45.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_θ(y_l | x)&lt;/code>&lt;/td>
&lt;td>−52.1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_ref(y_w | x)&lt;/code>&lt;/td>
&lt;td>−47.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_ref(y_l | x)&lt;/code>&lt;/td>
&lt;td>−50.8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &amp;ldquo;ratios&amp;rdquo; are the improvement of the trained model over the reference one for each answer:&lt;/p>
&lt;ul>
&lt;li>For &lt;code>y_w&lt;/code>: &lt;code>−45.2 − (−47.3) = +2.1&lt;/code> → the trained model gives it more probability than the reference one. Good.&lt;/li>
&lt;li>For &lt;code>y_l&lt;/code>: &lt;code>−52.1 − (−50.8) = −1.3&lt;/code> → the trained model gives it less probability. Also good.&lt;/li>
&lt;/ul>
&lt;p>With &lt;code>β = 0.1&lt;/code> (a typical value) the &amp;ldquo;margin&amp;rdquo; inside the logarithm is:&lt;/p>
$$m = \beta \cdot (2.1 - (-1.3)) = 0.1 \cdot 3.4 = 0.34$$
&lt;p>And the loss:&lt;/p>
$$\mathcal{L}_\mathrm{DPO} = -\log \sigma(0.34) = -\log(0.584) \approx 0.538.$$
&lt;p>The intuition is visible straight away: the more the chosen log-prob rises and the more the rejected one falls (relative to &lt;code>π_ref&lt;/code>), the more positive &lt;code>m&lt;/code> becomes, the higher the sigmoid and the lower the loss. If the margin is negative (the model gets it wrong), &lt;code>σ(m) &amp;lt; 0.5&lt;/code> and the loss blows up. The gradient pushes the model to increase &lt;code>π_θ(y_w|x)&lt;/code> and lower &lt;code>π_θ(y_l|x)&lt;/code>.&lt;/p>
&lt;p>The role of &lt;code>β&lt;/code>: it is the &lt;strong>temperature of the alignment&lt;/strong>. If &lt;code>β&lt;/code> is small, the model is allowed to drift a long way from &lt;code>π_ref&lt;/code>; if it is large, the KL weighs heavily and the model barely moves. Typical value in 2026: &lt;strong>0.05–0.3&lt;/strong>, with &lt;code>0.1&lt;/code> as a starting point.&lt;/p>
&lt;h2 id="kto-when-you-only-have-">KTO: when you only have 👍/👎&lt;/h2>
&lt;p>DPO needs &lt;strong>pairs&lt;/strong>. In practice, that is almost never what the product gives you: what the product gives you is binary feedback (a thumbs up or a thumbs down, a conversion or an abandonment). KTO — Kahneman-Tversky Optimization, Ethayarajh et al. 2024 — solves exactly that case.&lt;/p>
&lt;p>The intuition comes from Kahneman and Tversky&amp;rsquo;s &lt;strong>prospect theory&lt;/strong>: humans are &lt;strong>more sensitive to losses than to gains&lt;/strong> of the same magnitude (loss aversion: losing 100 € hurts more than gaining 100 €). KTO carries that over into the loss:&lt;/p>
$$\mathcal{L}_\mathrm{KTO}(x, y) =
\begin{cases}
\lambda_d \big[ 1 - \sigma\!\big(\beta \cdot ( h_\theta(x,y) - z_0 ) \big) \big] &amp; \text{if } y \text{ is desirable},\\
\lambda_u \big[ 1 - \sigma\!\big(\beta \cdot ( z_0 - h_\theta(x,y) ) \big) \big] &amp; \text{if } y \text{ is undesirable},
\end{cases}$$
&lt;p>where &lt;code>h_θ(x,y) = log(π_θ(y|x) / π_ref(y|x))&lt;/code> is exactly the same ratio that appeared in DPO, &lt;code>z_0&lt;/code> is an estimate of the batch KL divergence (it acts as a &amp;ldquo;neutral point&amp;rdquo;) and &lt;code>λ_d&lt;/code>, &lt;code>λ_u&lt;/code> are the desirable / undesirable weights.&lt;/p>
&lt;p>The critical point: &lt;strong>KTO does not need pairs&lt;/strong>. Each example is &lt;code>(prompt, answer, binary label)&lt;/code>. This fits real product telemetry. The usual practical rule is &lt;code>λ_u &amp;gt; λ_d&lt;/code> (e.g. 1.0 vs 0.33) when the database has more 👍 than 👎, so that the negative signal is not diluted.&lt;/p>
&lt;p>KTO works particularly well in two scenarios:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Products with explicit feedback UX&lt;/strong> (chatbots with 👍/👎): each thumbs is a direct KTO example, no need to synthesise pairs.&lt;/li>
&lt;li>&lt;strong>Imbalanced datasets&lt;/strong> (far more 👍 than 👎, or the other way round): the weights &lt;code>λ_d&lt;/code>, &lt;code>λ_u&lt;/code> handle it explicitly.&lt;/li>
&lt;/ol>
&lt;h2 id="orpo-sft-and-preferences-in-a-single-pass">ORPO: SFT and preferences in a single pass&lt;/h2>
&lt;p>DPO assumes you have already done SFT. You train two phases: first SFT to get &lt;code>π_ref&lt;/code>, then DPO on top of &lt;code>π_ref&lt;/code>. Two passes over the data, two optimisations, two models in memory.&lt;/p>
&lt;p>ORPO — Odds Ratio Preference Optimization, Hong et al. 2024 — &lt;strong>merges both phases&lt;/strong>. The loss combines two terms:&lt;/p>
$$\mathcal{L}_\mathrm{ORPO} = \mathcal{L}_\mathrm{SFT}(y_w) + \lambda \cdot \mathcal{L}_\mathrm{OR}(y_w, y_l)$$
&lt;p>The first term is classic SFT on the chosen answer (negative cross-entropy). The second is the &lt;strong>odds ratio&lt;/strong> between chosen and rejected:&lt;/p>
$$\mathcal{L}_\mathrm{OR} = -\log \sigma\!\Big( \log \tfrac{\mathrm{odds}_\theta(y_w|x)}{\mathrm{odds}_\theta(y_l|x)} \Big), \quad \text{with } \mathrm{odds}_\theta(y|x) = \tfrac{P_\theta(y|x)}{1 - P_\theta(y|x)}.$$
&lt;p>What matters: &lt;strong>there is no &lt;code>π_ref&lt;/code>&lt;/strong>. ORPO trains a single model, in a single pass, without loading the reference policy into memory. On paper it sounds good and in practice it works: a Llama 3 8B aligned with ORPO over 5k pairs takes about 3 hours on 4×H100 and fits in VRAM with aggressive QLoRA on a single RTX 4090.&lt;/p>
&lt;p>The &lt;code>λ&lt;/code> parameter is the weight of the preference term. Typical: &lt;code>0.1–0.3&lt;/code>. If &lt;code>λ&lt;/code> is too high, the model learns to discriminate but forgets the SFT (catastrophic forgetting); if it is too low, the alignment barely shows.&lt;/p>
&lt;h2 id="simpo-do-you-really-need-a-reference-model">SimPO: do you really need a reference model?&lt;/h2>
&lt;p>SimPO — Simple Preference Optimization, Meng et al. 2024 — takes ORPO&amp;rsquo;s question one step further: if ORPO frees itself from &lt;code>π_ref&lt;/code> for the combined SFT+preference case, &lt;strong>why not free yourself from &lt;code>π_ref&lt;/code> in the pure DPO case too?&lt;/strong>&lt;/p>
&lt;p>The SimPO loss:&lt;/p>
$$\mathcal{L}_\mathrm{SimPO} = -\log \sigma\!\Big( \tfrac{\beta}{|y_w|} \log \pi_\theta(y_w|x) - \tfrac{\beta}{|y_l|} \log \pi_\theta(y_l|x) - \gamma \Big).$$
&lt;p>Two changes with respect to DPO:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>There is no &lt;code>π_ref&lt;/code>&lt;/strong>: absolute log-probs of the trained model are compared directly.&lt;/li>
&lt;li>&lt;strong>Length-normalization&lt;/strong>: each log-prob is divided by the length of its answer &lt;code>|y|&lt;/code>. This is key because without normalising, long answers tend to have a lower total log-prob (each token contributes its &lt;code>log p &amp;lt; 0&lt;/code>), creating an artificial bias.&lt;/li>
&lt;li>&lt;strong>Explicit margin &lt;code>γ&lt;/code>&lt;/strong>: the loss is low if the difference of normalised log-probs exceeds &lt;code>γ&lt;/code>. Typical: &lt;code>γ = 0.5–1.5&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>With &lt;code>β = 2.0&lt;/code>, &lt;code>γ = 1.0&lt;/code>, &lt;code>|y_w| = 120&lt;/code> tokens, &lt;code>|y_l| = 100&lt;/code> tokens and the earlier log-probs:&lt;/p>
$$m = \tfrac{2.0}{120} \cdot (-45.2) - \tfrac{2.0}{100} \cdot (-52.1) - 1.0 = -0.753 + 1.042 - 1.0 = -0.711.$$
$$\mathcal{L}_\mathrm{SimPO} = -\log \sigma(-0.711) \approx 1.11.$$
&lt;p>A higher loss than DPO at the same point: SimPO takes longer to converge, but uses half the memory (a single model in VRAM) and removes the dependency on &lt;code>π_ref&lt;/code>. It is the dominant option when memory is the bottleneck.&lt;/p>
&lt;h2 id="decision-table-which-one-to-use-and-when">Decision table: which one to use and when&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Signal available&lt;/th>
&lt;th>Memory available&lt;/th>
&lt;th>Prior SFT&lt;/th>
&lt;th>Recommended method&lt;/th>
&lt;th>Rationale&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>(chosen, rejected)&lt;/code> pairs&lt;/td>
&lt;td>High (≥ 80 GB GPU)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>DPO&lt;/strong>&lt;/td>
&lt;td>Most established baseline, better reproducibility&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>(chosen, rejected)&lt;/code> pairs&lt;/td>
&lt;td>Low (24–48 GB GPU)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>SimPO&lt;/strong>&lt;/td>
&lt;td>Removes &lt;code>π_ref&lt;/code> → ~50 % less VRAM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>(chosen, rejected)&lt;/code> pairs&lt;/td>
&lt;td>Any&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;strong>ORPO&lt;/strong>&lt;/td>
&lt;td>SFT and preferences in one pass&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Binary 👍/👎 signal without pairs&lt;/td>
&lt;td>High&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong>&lt;/td>
&lt;td>The only method native to unpaired data&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Binary signal + few pairs&lt;/td>
&lt;td>High&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong> with a DPO sub-batch&lt;/td>
&lt;td>Combination documented in TRL 0.13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multistep trajectories (tool use)&lt;/td>
&lt;td>Very high&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>&lt;strong>Pure RLHF/RFT&lt;/strong>&lt;/td>
&lt;td>Preference-pair methods do not capture the dynamics&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Typical dataset magnitudes:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Minimum viable&lt;/th>
&lt;th>Sweet spot&lt;/th>
&lt;th>Plateau&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>1,000 pairs&lt;/td>
&lt;td>5,000–20,000&lt;/td>
&lt;td>&amp;gt; 50,000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>2,000 pairs&lt;/td>
&lt;td>5,000–20,000&lt;/td>
&lt;td>&amp;gt; 50,000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>3,000 pairs (includes SFT)&lt;/td>
&lt;td>10,000–30,000&lt;/td>
&lt;td>&amp;gt; 80,000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>5,000 binary examples&lt;/td>
&lt;td>20,000–80,000&lt;/td>
&lt;td>&amp;gt; 200,000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>KTO typically needs &lt;strong>3-4× more volume&lt;/strong> than DPO because the binary signal is weaker than the comparative one. The trade-off is that the binary signal is the one a production product naturally produces.&lt;/p>
&lt;h2 id="the-three-biases-that-break-the-method">The three biases that break the method&lt;/h2>
&lt;p>All four methods share one problem: they are training on &lt;strong>proxies&lt;/strong> for quality, not on quality. Those proxies have systematic biases the model can exploit trivially.&lt;/p>
&lt;h3 id="length-bias">Length bias&lt;/h3>
&lt;p>Documented in the original DPO paper and in the literature that followed. Long answers tend to be preferred by humans, probably because they look &amp;ldquo;more complete&amp;rdquo;. If the pair dataset inherits that bias, the model learns that &lt;strong>lengthening the answer is what gets rewarded&lt;/strong>, not that better content is what gets rewarded. Result: after 2–3 epochs the model spews waffle.&lt;/p>
&lt;p>Mitigations:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DPO&lt;/strong>: filter the dataset, removing pairs where &lt;code>|y_w| &amp;gt; 1.3 · |y_l|&lt;/code> (the 30 % rule).&lt;/li>
&lt;li>&lt;strong>SimPO&lt;/strong>: the length-normalization in the loss fixes it by construction.&lt;/li>
&lt;li>&lt;strong>ORPO / KTO&lt;/strong>: dataset filtering or an auxiliary length regularisation (DPOP, R-DPO).&lt;/li>
&lt;/ul>
&lt;div class="diagram" style="max-width:740px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 740 230" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Length bias during training">
&lt;style>
.ax{stroke:#444;stroke-width:1.4;fill:none}
.gr{stroke:#888;stroke-width:0.5;fill:none;stroke-dasharray:2 2}
.cv1{stroke:#c0392b;stroke-width:2.5;fill:none}
.cv2{stroke:#2980b9;stroke-width:2.5;fill:none}
.lb{font:600 11px sans-serif;fill:#222}
.sm{font:400 10px sans-serif;fill:#555}
.h{font:700 13px sans-serif;fill:#222}
&lt;/style>
&lt;text x="370" y="20" text-anchor="middle" class="h">Length bias: mean answer length during training&lt;/text>
&lt;line x1="60" y1="180" x2="700" y2="180" class="ax"/>
&lt;line x1="60" y1="180" x2="60" y2="50" class="ax"/>
&lt;line x1="60" y1="150" x2="700" y2="150" class="gr"/>
&lt;line x1="60" y1="120" x2="700" y2="120" class="gr"/>
&lt;line x1="60" y1="90" x2="700" y2="90" class="gr"/>
&lt;line x1="60" y1="60" x2="700" y2="60" class="gr"/>
&lt;text x="55" y="184" text-anchor="end" class="sm">100&lt;/text>
&lt;text x="55" y="154" text-anchor="end" class="sm">150&lt;/text>
&lt;text x="55" y="124" text-anchor="end" class="sm">200&lt;/text>
&lt;text x="55" y="94" text-anchor="end" class="sm">250&lt;/text>
&lt;text x="55" y="64" text-anchor="end" class="sm">300&lt;/text>
&lt;text x="20" y="115" class="sm" transform="rotate(-90,20,115)">tokens / answer&lt;/text>
&lt;text x="370" y="210" text-anchor="middle" class="sm">epoch&lt;/text>
&lt;text x="100" y="200" text-anchor="middle" class="sm">0&lt;/text>
&lt;text x="220" y="200" text-anchor="middle" class="sm">1&lt;/text>
&lt;text x="340" y="200" text-anchor="middle" class="sm">2&lt;/text>
&lt;text x="460" y="200" text-anchor="middle" class="sm">3&lt;/text>
&lt;text x="580" y="200" text-anchor="middle" class="sm">4&lt;/text>
&lt;text x="680" y="200" text-anchor="middle" class="sm">5&lt;/text>
&lt;path class="cv1" d="M100,150 C160,140 200,120 220,110 C280,80 340,65 460,55 C540,52 620,51 680,50"/>
&lt;path class="cv2" d="M100,150 C160,148 220,146 340,144 C460,143 580,142 680,142"/>
&lt;rect x="430" y="60" width="170" height="40" fill="white" stroke="#bbb"/>
&lt;line x1="438" y1="72" x2="468" y2="72" class="cv1"/>
&lt;text x="475" y="76" class="lb">DPO without filter&lt;/text>
&lt;line x1="438" y1="90" x2="468" y2="90" class="cv2"/>
&lt;text x="475" y="94" class="lb">SimPO (length-norm)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The chart shows the usual pattern: with no mitigation, in 3 epochs a Llama 3 8B with DPO can go from answers of about 150 tokens to answers of about 280 tokens, &lt;strong>without any improvement in human-evaluated quality&lt;/strong>. SimPO keeps the length roughly stable.&lt;/p>
&lt;h3 id="position-bias-in-dataset-curation">Position bias (in dataset curation)&lt;/h3>
&lt;p>If the pairs are generated automatically with an LLM judge (covered in detail in the next post of this series), there is a known bias: &lt;strong>judges prefer the first answer&lt;/strong> they see when the two are comparable. If every pair in the dataset always has the chosen in position A and the rejected in position B, the model does not learn preference: it learns an artefact of the curation process.&lt;/p>
&lt;p>Mitigation: random shuffling of the order in the query to the judge &lt;strong>and averaging two passes with the order reversed&lt;/strong> (seen in tools such as Promptfoo and Inspect AI by default).&lt;/p>
&lt;h3 id="distribution-shift-between-the-data-and-π_ref">Distribution shift between the data and &lt;code>π_ref&lt;/code>&lt;/h3>
&lt;p>DPO, KTO and SimPO implicitly compare the trained model against a distribution. If the preference data comes from a model (another LLM generating candidates) very different from &lt;code>π_ref&lt;/code>, the trained model can explore regions where &lt;code>π_ref&lt;/code> has almost zero probability, giving numerically unstable ratios (the log of very small quantities). In practice this shows up as loss explosions, NaN gradients or silent regression.&lt;/p>
&lt;p>Mitigation: &lt;strong>generate the dataset candidates with &lt;code>π_ref&lt;/code> itself&lt;/strong> whenever possible (rejection sampling over &lt;code>π_ref&lt;/code>, with an external judge picking the chosen). That is the canonical on-policy RLHF prescription applied to the offline setting.&lt;/p>
&lt;h2 id="implications-on-on-premise-hardware">Implications on on-premise hardware&lt;/h2>
&lt;p>The figures below are indicative for a typical May 2026 scenario: Llama 3.1 8B Instruct as &lt;code>π_ref&lt;/code>, a dataset of 5,000–20,000 pairs, QLoRA (NF4) with LoRA rank 16 over all the transformer block projectors (&lt;code>q,k,v,o,gate,up,down&lt;/code>), effective batch size 16, 1–3 epochs.&lt;/p>
&lt;h3 id="on-an-rtx-4090-24-gb">On an RTX 4090 (24 GB)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Peak VRAM&lt;/th>
&lt;th>Time / epoch (5k pairs)&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>~22 GB&lt;/td>
&lt;td>50–80 min&lt;/td>
&lt;td>Needs &lt;code>π_ref&lt;/code> in VRAM even if quantised FP8/INT8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>~14 GB&lt;/td>
&lt;td>45–70 min&lt;/td>
&lt;td>No &lt;code>π_ref&lt;/code>, the natural option on a 4090&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>~16 GB&lt;/td>
&lt;td>60–90 min&lt;/td>
&lt;td>No &lt;code>π_ref&lt;/code>, comfortably viable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>~22 GB&lt;/td>
&lt;td>90–150 min (10k binary)&lt;/td>
&lt;td>Same VRAM as DPO, more data per epoch&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The 4090 (24 GB, Ada Lovelace, no NVLink) is &lt;strong>perfectly viable&lt;/strong> for Llama 8B with QLoRA if you pick the method sensibly. For 13B the choice between SimPO/ORPO is no longer a preference, it is a requirement.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink">On a generic 4×H100 SXM cluster (320 GB, NVLink)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Method&lt;/th>
&lt;th>Viable model&lt;/th>
&lt;th>Time / epoch (10k pairs)&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>Llama 3 70B (4-bit)&lt;/td>
&lt;td>60–90 min&lt;/td>
&lt;td>Tensor parallel = 4, still comfortable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>Llama 3 70B (BF16)&lt;/td>
&lt;td>50–75 min&lt;/td>
&lt;td>Full BF16 fits thanks to having no &lt;code>π_ref&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>Llama 3 70B (BF16)&lt;/td>
&lt;td>70–100 min&lt;/td>
&lt;td>Similar to SimPO in consumption&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>Llama 3 70B (4-bit)&lt;/td>
&lt;td>100–140 min&lt;/td>
&lt;td>Larger datasets offset by parallelism&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On an NVLink cluster the operational difference between methods blurs: they all fit. The choice goes back to &lt;strong>what kind of signal you have&lt;/strong>, not to budget.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>LoRA and QLoRA fundamentals&lt;/strong>: the maths of the low-rank adapter that underpins everything above, and why a 70B fits in 24 GB.&lt;/li>
&lt;li>&lt;strong>LLM-as-judge fundamentals&lt;/strong>: how to build the judge that generates the chosen/rejected pairs without position or verbosity bias. The next post in this batch covers it.&lt;/li>
&lt;li>&lt;strong>Online DPO and iterative on-policy&lt;/strong>: the 2026 research state of the art (Fast-Slow Chasing, RLOO, iterative preference learning) and why it is not production yet.&lt;/li>
&lt;li>&lt;strong>Distillation and synthetic preference data&lt;/strong>: when it is worth generating pairs with a large model to train a small one.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — the operational cycle (Postgres, SQL queries, multi-LoRA hot-swap) that feeds pairs to the methods in this post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> — how production signals become the preference dataset.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs: the layer after tracing&lt;/a> — the battery of evaluators that decides whether the aligned adapter is promoted to production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge: the exam marker&lt;/a> — the mechanism that generates the &lt;code>(chosen, rejected)&lt;/code> pairs consumed here. Judge calibration with Cohen&amp;rsquo;s kappa and the four biases that invalidate the pairs if nobody watches them.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving: the single translator with a thousand glossaries&lt;/a> — each alignment policy (DPO with dataset A, KTO with dataset B, ORPO with dataset C) can live as a separate adapter and be served in parallel: real production A/B without deploying three copies of the base.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Rafailov, R., Sharma, A., Mitchell, E., Ermon, S., Manning, C. D., Finn, C. &lt;em>Direct Preference Optimization: Your Language Model is Secretly a Reward Model&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Ethayarajh, K., Xu, W., Muennighoff, N., Jurafsky, D., Kiela, D. &lt;em>KTO: Model Alignment as Prospect Theoretic Optimization&lt;/em> (ICML 2024).&lt;/li>
&lt;li>Hong, J., Lee, N., Thorne, J. &lt;em>ORPO: Monolithic Preference Optimization without Reference Model&lt;/em> (EMNLP 2024).&lt;/li>
&lt;li>Meng, Y., Xia, M., Chen, D. &lt;em>SimPO: Simple Preference Optimization with a Reference-Free Reward&lt;/em> (NeurIPS 2024).&lt;/li>
&lt;li>HuggingFace TRL 0.13 — reference implementations: &lt;a href="https://huggingface.co/docs/trl">https://huggingface.co/docs/trl&lt;/a>.&lt;/li>
&lt;li>Tunstall, L. et al. &lt;em>The Alignment Handbook&lt;/em> — reproducible recipes: &lt;a href="https://github.com/huggingface/alignment-handbook">https://github.com/huggingface/alignment-handbook&lt;/a>.&lt;/li>
&lt;li>Park, R. et al. &lt;em>Disentangling Length from Quality in Direct Preference Optimization&lt;/em> (R-DPO, ACL 2024).&lt;/li>
&lt;/ul></description></item></channel></rss>