Post-training · Learning from comparisons
Offline preference optimisation
Six ways to teach a model with comparisons instead of examples. SFT only ever says “write this”. These methods also say “not that”: from a chosen and a rejected answer, or from single answers labelled good or bad, they raise the better answer and push the worse one down. They train on a fixed dataset collected in advance, with no reward model and no sampling during training: that is what offline and RL-free mean.
Fact 1
SFT only pushes up
From the SFT workbook: training raises the target token, and every other token loses a little, in proportion to its probability. A specific bad answer is never pushed down on purpose.
Fact 2
A preference is a comparison
People and judges find it much easier to say which of two answers is better than to write the perfect one or to score one from 1 to 10. A preference pair is a prompt \(x\) with a chosen answer \(y_w\) (w for win) and a rejected one \(y_l\) (l for lose).
Fact 3
The reference model is a leash
Most methods keep a frozen copy of the starting model, \(\pi_{\text{ref}}\). An answer is scored by how much more likely the trained model makes it than the copy does, so the model improves without forgetting how to write.
One loss, four levers · click a technique
The loss all six build on
Give the chosen answer a higher score than the rejected one. An answer's score \(\hat r\) is how much more likely the trained model \(\pi_\theta\) makes it than the frozen reference \(\pi_{\text{ref}}\) does.
What each symbol means
| \(\mathcal{L}(\theta)\)“L of theta” | resultThe loss for one comparison: one positive number, lower is better. It depends on the weights \(\theta\). |
| \(\theta\)“theta” | trainedAll the weights of the model being trained. Training changes only these. |
| \(x\) | dataThe prompt. |
| \(y_w\)“y win” | dataThe chosen answer, the better one of the pair. |
| \(y_l\)“y lose” | dataThe rejected answer, the worse one. |
| \(y\) | dataAny one answer: in the second line it stands for \(y_w\) or \(y_l\) in turn. |
| \(\pi_\theta(y\mid x)\)“pi theta of y given x” | trainedThe probability that the trained model writes the whole answer \(y\) after the prompt \(x\). \(\pi\) (pi) is the usual letter for a model that chooses what to write, called a policy; it has nothing to do with 3.14 here. |
| \(\pi_{\text{ref}}(y\mid x)\)“pi ref” | frozenThe same probability under a frozen copy of the starting model, the reference. It never changes during training. |
| \(\hat r_\theta(x,y)\)“r hat”, the score | trainedThe answer's score: the log of how many times more likely the trained model makes \(y\) than the reference does. 0 means “no change”, positive means “more likely than before”, negative means “less likely”. The hat \(\hat{\ }\) marks something estimated rather than given. |
| \(\log\frac{a}{b}\) | mathA log-ratio. It equals \(\log a - \log b\), so the score is simply the trained model's log-probability minus the reference's. |
| \(\beta\)“beta” | settingA number you choose, usually 0.1: how strongly the gap counts, and so how far the model may move from the reference. |
| \(\big[\,\hat r_\theta(x,y_w) - \hat r_\theta(x,y_l)\,\big]\) | trainedThe gap, or margin: the chosen answer's score minus the rejected answer's. Positive when the model already favours the chosen one. |
| \(\sigma\)“sigma”, the sigmoid | math\(\sigma(u) = 1/(1+e^{-u})\): squashes any number into a probability between 0 and 1. \(\sigma(0) = 0.5\), large positive gaps give almost 1, large negative ones almost 0. |
| \(-\log\) | mathTurns that probability into a penalty: 0.69 at a coin flip (\(\sigma = 0.5\)), near 0 when the model clearly prefers the chosen answer, large when it prefers the rejected one. |
Three questions every method answers
What does one example hold?
A pair costs two answers and a comparison for every prompt. Single good or bad labels are cheaper and far more plentiful.
What stops the model drifting?
A model can win every comparison by turning strange. DPO ties it to a frozen reference; others use an SFT term, a target gap, or nothing at all.
Which part gets the credit?
A comparison of whole answers does not say which words or steps made the difference. The unit can be one step, one answer or a whole session.
The base · every other technique changes one part of it
DPO Direct preference optimisation
Show the model a prompt with two answers, one marked better. Raise the better one and lower the worse one, both measured against a frozen copy of the starting model, so the model learns the preference without wandering far from what it already does well.
A chosen and a rejected answer, a frozen reference, and the one scalar that pushes them apart.
- Changes vs SFT
- Adds a rejected answer and pushes it down; keeps a frozen reference model
- Its answers
- Q1 pairs · Q2 reference + β · Q3 whole answers
- Cost
- ●●●●● pairs + two models' passes
- Library
- Ready: TRL
DPOTrainer, multi-turn and tool calls supported (tool turns are scored too: see #15)
The idea in plain words
SFT is told only what to write. DPO is also told what not to write. Each training example is a prompt \(x\) with two answers: the chosen one \(y_w\) and the rejected one \(y_l\). Someone decided the first is better: a person, an LLM judge or an automatic check.
Training raises the chosen answer and lowers the rejected one, but not in absolute terms. Each answer is scored by how much more likely the trained model makes it than a frozen copy of the starting model, the reference \(\pi_{\text{ref}}\). The loss wants the chosen answer's score above the rejected one's, and it pushes less and less once the gap is comfortable.
The strength \(\beta\) sets how tightly the model is tied to the reference. The name says what DPO skips: classic RLHF first trains a separate reward model and then runs reinforcement learning against it. DPO reaches the same goal directly, with one loss on the preference pairs.
See it work
Push the chosen up, the rejected down
A tiny model chooses among four whole answers to one prompt. Pick a pair, press Train, and watch the chosen answer gain on the frozen reference while the rejected one loses. Then lower β to see how far the model will drift to win, or switch to SFT to see what plain imitation does to the rejected answer.
Where the training signal comes from
Three decisions: who writes the two answers, who decides which is better, and which pairs you keep.
A small example
The prompt and its two answers
The judge's verdict
A or B, then one sentence.Same winner in both orders, so the pair is kept.
What DPO computes for this row
Each log-probability is the sum over the answer's tokens, as in SFT.
The model already leans the right way: the loss is below its starting value of \(\log 2 = 0.69\). Training keeps widening the gap.
What a training batch looks like
| Prompt | Chosen | Rejected | Decided by |
|---|---|---|---|
| Explain why the sea is salty, for a 10-year-old. | Rain slowly dissolves tiny bits of salt from rocks… | Ocean salinity results from the weathering of crustal minerals… | judge, order swapped |
| Is 91 a prime number? | No. 91 = 7 × 13. | Yes, 91 is prime: it is odd and not divisible by 3 or 5. | checker |
| Write a one-line reminder to water the plants. | Don't forget to water the plants today! | This is a friendly reminder that it would be a good idea to water the plants at some point today, if you have the time. | human rater |
| Translate to French: “The shop opens at nine.” | Le magasin ouvre à neuf heures. | La boutique ouvre neuf. | judge |
# dpo_train.jsonl: one pair per line. In conversational rows, "prompt" holds the messages # so far and "chosen" / "rejected" hold the competing assistant turns. {"prompt": [{"role": "user", "content": "Is 91 a prime number?"}], "chosen": [{"role": "assistant", "content": "No. 91 = 7 × 13."}], "rejected": [{"role": "assistant", "content": "Yes, 91 is prime: it is odd and not divisible by 3 or 5."}]} {"prompt": [{"role": "user", "content": "Translate to French: \"The shop opens at nine.\""}], "chosen": [{"role": "assistant", "content": "Le magasin ouvre à neuf heures."}], "rejected": [{"role": "assistant", "content": "La boutique ouvre neuf."}]}
The judge's verdicts, the swap check and the source are kept in a separate file while you build the set. The trainer reads only these three fields.
Every row needs four log-probabilities: the chosen and the rejected answer, each under the trained model and under the frozen reference.
| The 91 row | trained \(\log\pi_\theta\) · gradients on | reference \(\log\pi_{\text{ref}}\) · frozen | score \(\hat r\) |
|---|---|---|---|
| chosen: “No. 91 = 7 × 13.” | −9.8 | −10.6 | +0.8 |
| rejected: “Yes, 91 is prime…” | −24.1 | −23.3 | −0.8 |
The reference never changes, so its two numbers can be computed once before training starts (precompute_ref_log_probs=True in TRL). With a new LoRA adapter the reference is the same network with the adapter switched off; if you continue training an existing adapter, TRL keeps a frozen copy of that adapter as the reference. Either way no second copy of the model is loaded.
The loss
Read it aloud: “For every pair, score each answer by how much more likely the trained model makes it than the reference does. Subtract the rejected answer's score from the chosen one's, scale by β, and squash it through a sigmoid: that is the probability that the model prefers the chosen answer. Push the log of that probability up.” Point at a row of the table to light its term up in the formula.
one pair Inside \(\mathbb{E}\) is the loss of one row, and here a row is a pair: a prompt \(x\) with a chosen and a rejected answer. \(\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\) means “averaged over pairs”, in practice over the pairs of a batch. Station 7 follows one full training step, from the batch to the update.
Every symbol in the formula
| Symbol | What it means |
|---|---|
| The data | |
| \(x\) | dataThe prompt, for example “Explain why the sea is salty, for a 10-year-old.” |
| \(y_w\)“y win” | dataThe chosen answer: the one the judge, checker or person preferred (A in the example above). |
| \(y_l\)“y lose” | dataThe rejected answer, the worse one of the pair (B in the example). |
| \(\mathcal{D}\)“calligraphic D” | dataThe preference dataset: all your \((x, y_w, y_l)\) triples, one per row of the JSONL file. |
| \((x,y_w,y_l)\sim\mathcal{D}\)“a triple drawn from D” | mathThe tilde \(\sim\) means “is picked from”: one row of the dataset at a time. |
| \(\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\big[\;\cdot\;\big]\)“expected value over the dataset” | mathThe average of the bracket over every row. In practice: compute the bracket for each pair in a batch and average. |
| The two models | |
| \(\theta\)“theta” | trainedThe weights being trained, the only thing that changes. |
| \(\pi_\theta(y\mid x)\)“pi theta of y given x” | trainedThe probability that the trained model writes the whole answer \(y\) after prompt \(x\): the product of its token probabilities, as in SFT. \(\pi\) is the usual letter for a model that chooses what to write (a policy). The bar \(\mid\) reads “given”. |
| \(\pi_{\text{ref}}(y\mid x)\)“pi ref” | frozenThe same probability under the reference: a frozen copy of the model as it was before this training. It is computed once and never changes. |
| The scores and the gap | |
| \(\log\frac{\pi_\theta(y_w\mid x)}{\pi_{\text{ref}}(y_w\mid x)}\)“log-ratio of the chosen answer” | trainedThe chosen answer's score \(\hat r_w\) (“r hat w”). It equals \(\log\pi_\theta(y_w\mid x) - \log\pi_{\text{ref}}(y_w\mid x)\): positive when the trained model makes it more likely than the reference did, 0 when nothing has changed. In the example, \(-41.2 - (-41.6) = +0.4\). |
| \(\log\frac{\pi_\theta(y_l\mid x)}{\pi_{\text{ref}}(y_l\mid x)}\)“log-ratio of the rejected answer” | trainedThe rejected answer's score \(\hat r_l\). Making the rejected answer less likely than the reference did lowers it, and that widens the gap. In the example, \(-58.9 - (-58.2) = -0.7\). |
| \(\big[\;\hat r_w - \hat r_l\;\big]\)“the gap”, or margin | trainedThe inner square bracket: chosen score minus rejected score. Positive means the model already prefers the chosen answer more than the reference did. Example: \(0.4 - (-0.7) = 1.1\). |
| \(\beta\)“beta” | settingThe leash, usually 0.1. It multiplies the gap before the sigmoid. A small β lets the model move far from the reference to win a pair; a large β keeps it close (see “Why the reference matters” below). |
| From gap to loss | |
| \(\log\sigma\)“log sigmoid” | math\(\sigma(u) = 1/(1+e^{-u})\) turns the scaled gap into a probability between 0 and 1: the probability that the model prefers the chosen answer. \(\sigma(0) = 0.5\), \(\sigma(3) = 0.95\), \(\sigma(-3) = 0.05\). The log makes a confident wrong ranking very costly and a confident right one almost free. |
| \(-\) | mathThe log of a probability is negative; the minus sign makes the loss positive, so training can push it down. |
| \(\mathcal{L}_{\text{DPO}}(\theta)\)“L DPO of theta” | resultThe loss averaged over the preference dataset. It starts at \(\log 2 = 0.69\), while the model is still identical to the reference, and falls toward 0 as the gap grows. It never reaches 0, so it keeps pushing. |
What you must supply: rows of prompt \(x\), chosen answer \(y_w\) and rejected answer \(y_l\), plus the value of \(\beta\). No scores and no reward model: “which one is better” is the whole label. The reference \(\pi_{\text{ref}}\) is simply your starting model, frozen.
How the loss works, step by step
- Four log-probabilities. Run the chosen and the rejected answer through the trained model and through the reference. Each log-probability is the sum of the answer's token log-probabilities, as in SFT.
- Two scores. For each answer, subtract the reference's number from the trained model's: \(\hat r_w\) and \(\hat r_l\). They say how much each answer has moved since training began.
- One gap. \(\hat r_w - \hat r_l\), times \(\beta\). Only the difference counts: raising both answers by the same amount changes nothing.
- Squash and penalise. \(\sigma\) of the scaled gap is the model's probability of preferring the chosen answer; \(-\log\) of it is the loss.
- Step. The gradient raises the chosen answer's log-probability and lowers the rejected one's, with a weight \(\sigma(-\beta[\hat r_w - \hat r_l])\) that is large while the pair is ranked badly and fades once it is ranked well.
Worked example: the sea-salt pair, β = 0.1
The loss goes down when the chosen answer gains probability relative to the reference, or the rejected one loses it, or both. It goes up when the model starts preferring the rejected answer. Because only the gap counts, the loss can fall even while both answers lose probability, as long as the rejected one falls faster: a catch listed below.
One training step on the GPU
A DPO step is #1's step run on pairs, with two changes: a frozen reference model also reads every answer, and the slope at each answer token is no longer always “raise the target”. It raises the chosen answer and lowers the rejected one, scaled by one number per pair. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Exactly what one training step does with one pair: how many slopes, what each one needs, and when the weights move. Take a prompt with a chosen answer of 10 tokens and a rejected answer of 8.
- A slope cannot be computed from its own token alone. c needs every token of both answers under both models first. That is the one order inside DPO's backward pass: c first, then all 18 slopes at once. In SFT, each slope needs only its own position.
- No division by length. Every token gets the full c, so the 10-token answer gets 10 pushes and the 8-token one 8. In a batch of B pairs, c is divided by B.
- Read, not graded: the prompt, in both sequences.
- Not one after the other. The 18 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
Each pair is a prompt with two answers. The trainer writes both out in full, prompt + chosen and prompt + rejected, and stacks them: B pairs give one grid of 2B sequences. The prompt is masked in both.
2Forward pass
The trained model reads all 2B sequences at once, as in #1, and keeps its activations. For each sequence it adds up log p over the answer tokens: a sum, not an average. The reference reads the same 2B sequences, frozen and with no gradient; since it never changes, its 2B numbers can also be computed once before training and stored.
3Loss: from each pair to one number
Each pair gives one gap \(h = \hat r_w - \hat r_l\), the chosen score minus the rejected score of station 6, and one loss \(-\log\sigma(\beta h)\); the batch averages the B pair losses.
4Backward pass: the slope at every answer token
(\(y_t\) is 1 at the target token and 0 at every other word.) The whole pair collapses into one number, c, computed from its gap. Every token of the chosen answer gets #1's direction, raise the target, scaled by c; every token of the rejected answer gets the opposite, lower its own token. A badly ranked pair has a large c and pushes hard; a well-separated pair has c near 0 and stops pushing. Unlike SFT there is no division by the answer length: a score is a sum over tokens, so every token gets the full c, and a longer answer gets more pushes in total. That is the length bias #13 addresses. Below the logits nothing is new: #1's backward pass runs on all 2B sequences, and every weight adds up the pushes of both answers of every pair.
5Update
Every weight of the trained model moves once. The reference stays frozen for the whole run.
On a real GPU
- Four sequence reads per pair (two answers × two models), done as two forward calls: the trained model on all 2B sequences at once, and the reference on the same 2B. Only the trained model's call is backpropagated.
- Memory holds two models: the trained one with gradients and optimiser state, and the reference's weights. Computing the reference's numbers once, before training, takes it off the GPU.
- The prompt is processed twice, once with each answer.
The maths behind it
Where it comes from: the RLHF goal
Classic RLHF trains a reward model \(r(x,y)\) from the comparisons, then asks the language model to collect as much reward as possible while staying close to the reference:
$$\max_{\pi}\;\mathbb{E}_{x,\;y\sim\pi(\cdot\mid x)}\big[r(x,y)\big] \;-\; \beta\,\mathrm{KL}\big(\pi(\cdot\mid x)\,\big\|\,\pi_{\text{ref}}(\cdot\mid x)\big)$$\(\max_{\pi}\)“maximise over pi” mathFind the model \(\pi\) that makes everything after it as large as possible. \(\pi\)“pi” trainedThe language model being trained, seen as a policy: for each prompt, a probability for every possible answer. \(x\) dataA prompt from the dataset. \(y\sim\pi(\cdot\mid x)\)“y drawn from pi given x” generatedAn answer the model itself writes for \(x\). The dot \(\cdot\) stands for “every possible answer”: \(\pi(\cdot\mid x)\) is the whole list of answer probabilities. \(\mathbb{E}_{x,\,y\sim\pi}\big[\;\cdot\;\big]\) mathThe average over prompts and over the answers the model writes for them. \(r(x,y)\)“r”, the reward frozenA reward model: a separate network, trained on the comparisons, that gives each answer a number, higher for better answers. \(\beta\) settingThe price of moving away from the reference. The same β that appears in the DPO loss. \(\mathrm{KL}\big(\pi \,\|\, \pi_{\text{ref}}\big)\)“KL divergence of pi from pi ref” mathHow different the trained model's answer probabilities are from the reference's: \(\sum_y \pi(y\mid x)\log\frac{\pi(y\mid x)}{\pi_{\text{ref}}(y\mid x)}\). It is 0 when they are identical and grows as the model drifts. The double bar \(\|\) separates the two lists. \(\pi_{\text{ref}}\) frozenThe frozen copy of the starting model. The reward pulls toward good answers; the KL term charges for every move away from the reference. \(\beta\) is the price of moving.
The best model for that goal has a closed form
$$\pi^{*}(y\mid x) = \frac{1}{Z(x)}\;\pi_{\text{ref}}(y\mid x)\;\exp\!\Big(\frac{r(x,y)}{\beta}\Big)$$\(\pi^{*}(y\mid x)\)“pi star” resultThe best possible model for the goal of step 1: the probability it gives to answer \(y\). The star means “the best one”. \(\pi_{\text{ref}}(y\mid x)\) frozenThe reference model's probability for the same answer: the starting point. \(r(x,y)\) frozenThe answer's reward. \(\beta\) settingThe price of moving. Dividing by a small β makes the rewards count more, so the best model moves further. \(\exp(u)\)“exp”, or \(e^u\) math\(e \approx 2.718\) raised to the power \(u\). It is always positive, equals 1 at \(u = 0\), and grows fast: the multiplier for each answer. \(Z(x)\)“Z of x”, the normaliser mathThe sum of \(\pi_{\text{ref}}(y\mid x)\,e^{r(x,y)/\beta}\) over every possible answer. Dividing by it makes the new probabilities add up to 1. It depends only on the prompt, and in practice it cannot be computed, because there are too many possible answers. Take the reference and multiply each answer's probability by \(e^{r/\beta}\): high-reward answers grow, low-reward ones shrink. \(Z(x)\) rescales everything so the probabilities add up to 1 again.
two answers, both at π_ref = 0.50; rewards: A = +1.0, B = 0.0 β = 1: A: 0.50 × e¹ = 1.36 B: 0.50 × e⁰ = 0.50 Z = 1.86 → π*: A 0.73, B 0.27 β = 0.5: A: 0.50 × e² = 3.69 B: 0.50 × e⁰ = 0.50 Z = 4.19 → π*: A 0.88, B 0.12 the smaller β, the further the best model moves from the referenceTurn it round: the reward hides inside the model
Solve the last formula for \(r\):
$$r(x,y) = \beta\log\frac{\pi^{*}(y\mid x)}{\pi_{\text{ref}}(y\mid x)} + \beta\log Z(x)$$\(r(x,y)\) resultThe reward that this model implies for answer \(y\). \(\log\frac{\pi^{*}(y\mid x)}{\pi_{\text{ref}}(y\mid x)}\) mathThe log-ratio: how many times more likely the model makes the answer than the reference, on a log scale. With \(\pi_\theta\) in place of \(\pi^*\) this is exactly the score \(\hat r\) of the DPO loss. \(\beta\) settingThe same leash; here it converts the log-ratio into reward units. \(\beta\log Z(x)\) mathA constant for each prompt: the same number for every answer to \(x\). That is why it vanishes when two answers to the same prompt are compared. Any model, compared with the reference, implies a reward. Apart from the \(\beta\log Z(x)\) term, which the next step cancels, that implied reward is the score \(\hat r\) in the DPO loss, times \(\beta\). It is why the DPO paper is subtitled “your language model is secretly a reward model”.
Compare two answers and \(Z\) cancels
The Bradley–Terry model, the standard model of comparisons, says the chance that \(y_w\) beats \(y_l\) depends only on the difference of their rewards:
$$P(y_w \succ y_l \mid x) = \sigma\big(r(x,y_w) - r(x,y_l)\big)$$\(P(\,\cdots\mid x)\) resultA probability: here, the chance that a person or judge, shown both answers to \(x\), picks \(y_w\). \(y_w \succ y_l\)“y w is preferred to y l” mathThe curly \(\succ\) means “beats” or “is preferred to”. \(r(x,y_w)\), \(r(x,y_l)\) frozenThe rewards of the two answers. Only their difference matters. \(\sigma\) mathThe sigmoid, \(\sigma(u) = 1/(1+e^{-u})\): turns the reward difference into a probability. Equal rewards give 0.5. rewards 2.0 against 0.5 → P(first wins) = σ(+1.5) = 0.82 rewards 0.5 against 0.5 → σ(0) = 0.50 (a coin flip) rewards 0.5 against 2.0 → σ(−1.5) = 0.18Substitute the implied reward. The awkward \(\beta\log Z(x)\) is the same for both answers to the same prompt, so it disappears from the difference. Maximum likelihood on the observed preferences, exactly as SFT does on tokens, then gives the DPO loss: no reward model to train, no reinforcement learning to run.
What one training step does
$$\nabla_\theta\mathcal{L}_{\text{DPO}} = -\beta\;\underbrace{\sigma\big(\beta\,[\hat r_\theta(y_l) - \hat r_\theta(y_w)]\big)}_{\text{how wrong the ranking still is}}\;\Big[\underbrace{\nabla_\theta\log\pi_\theta(y_w\mid x)}_{\text{raise the chosen}} - \underbrace{\nabla_\theta\log\pi_\theta(y_l\mid x)}_{\text{lower the rejected}}\Big]$$\(\nabla_\theta\)“nabla theta”, the gradient mathFor every weight in \(\theta\): how much the quantity after it changes when that weight is nudged up. It is a direction in which to move the weights. \(\nabla_\theta\mathcal{L}_{\text{DPO}}\) resultThe direction that increases the loss. Training steps the opposite way, so every minus sign flips: the chosen answer goes up, the rejected one down. \(\hat r_\theta(y_w)\), \(\hat r_\theta(y_l)\) trainedThe two scores, log-ratios against the reference, as in the loss. \(\sigma\big(\beta\,[\hat r_\theta(y_l) - \hat r_\theta(y_w)]\big)\) trainedThe weight of this pair: the probability the model currently gives to the wrong ranking. Note the order: rejected minus chosen. \(\nabla_\theta\log\pi_\theta(y_w\mid x)\) trainedThe SFT direction for the chosen answer: the change to the weights that makes it more likely. \(\nabla_\theta\log\pi_\theta(y_l\mid x)\) trainedThe same for the rejected answer; subtracting it makes the rejected answer less likely. \(\beta\) settingScales the whole step, and inside the sigmoid sets how fast the weight fades. \(\underbrace{\;\cdots\;}_{\text{note}}\) mathAn underbrace is a label: it names the part above it. The update is SFT on the chosen answer minus SFT on the rejected one, with a weight in front. The weight is large while the model ranks the pair the wrong way and fades once the chosen answer is comfortably ahead: DPO learns most from the pairs it still gets wrong.
scaled gap β(r̂_w − r̂_l) weight σ(−scaled gap) −3.0 0.95 ← ranked the wrong way: full push 0.0 0.50 ← where training starts, model = reference +3.0 0.05 ← already right: almost no pushWhy the reference matters
Without \(\pi_{\text{ref}}\), the easiest way to win a pair would be to make the rejected answer impossible, along with everything that resembles it, and to inflate the chosen one: the model would forget how to write. The reference turns the target into “more likely than you used to be”, and \(\beta\) sets the price. In the lab, train 50 steps at \(\beta = 0.1\) and then at \(\beta = 1\): the model puts about 99% of its probability on the chosen answer at the small value, against 72% at the large one, and at the small value the push is still strong.
Watch out for
Length wins pairs
Judges and people often prefer longer answers, so chosen answers are longer on average. DPO then learns that length pays. Compare the lengths of chosen and rejected answers; consider length-matched pairs or a length-normalised loss (#13).
Both answers can lose probability
DPO only looks at the gap. In practice the chosen answer's log-probability often falls during training too, just less than the rejected one's. Watch both, not only the margin. Mixing in a small SFT loss on the chosen answers helps, as does DPO-Positive, which penalises any fall in the chosen answer's probability (Pal et al., 2024).
Pairs from another model learn slowly
Pairs written by a different model sit where your model puts little probability, so the gradients are weak. Pairs sampled from the model itself, or SFT on the chosen answers first, work better.
The labeller is the ceiling
DPO learns the preferences it is given, mistakes included. A judge that favours a confident style trains a confidently styled model. Check the labels against people on a sample.
The gap keeps growing
With clean, certain preferences the loss keeps pushing the gap wider long after the ranking is right, and the leash stops holding. One or two epochs are usually enough; IPO (#13) addresses this directly.
Silent truncation
DPOConfigkeepsmax_length = 1024tokens by default. A long prompt pushes the answers out of the window and the pair compares cut-off text, and rows whose prompt alone fills the window are dropped without notice. Raise the limit or shorten the prompts.
No pairs needed · one answer, one label
KTO Kahneman–Tversky optimisation
Label single answers as good or bad, with no partner answer. KTO raises good answers above a reference point and pushes bad ones below it, each as a separate example. It borrows its shape from prospect theory, the economics of how people value gains and losses.
Two unpaired rows, one good and one bad, scored against a reference point.
- Changes vs DPO
- One answer per row with a good or bad label, instead of a pair
- Its answers
- Q1 changed: single labelled answers. Q2 and Q3 as in DPO
- Cost
- ●●●●● one label per answer
- Library
- Ready: TRL
KTOTrainer
The idea in plain words
A pair needs two answers to the same prompt and a comparison between them. Often you have something simpler: a thumbs-up or thumbs-down on one answer, a judge that labels each step good or bad, a test that passes or fails. KTO trains on exactly that.
Each answer is scored as in DPO, by how much more likely the trained model makes it than the reference does. With no partner to beat, the score is compared with a reference point \(z_0\) instead: an estimate of how far the model has drifted from the reference overall. A good answer should score above the reference point and a bad one below it. Once an answer is clearly on the right side, the loss barely pushes it.
The name comes from Kahneman and Tversky's prospect theory: people judge outcomes as gains or losses against a reference point, and both feel less intense the further they go. KTO gives good and bad answers separate weights, \(\lambda_D\) for desirable (good) answers and \(\lambda_U\) for undesirable (bad) ones, which also balance a dataset with many more bad answers than good ones.
See it work
Six answers, each labelled on its own
Every answer carries a judge's label, good or bad, and no partner. Train and watch the good answers rise above the reference point and the bad ones fall below it, each along its own curve. Then flip some labels so the data is unbalanced, and see why KTO has two weights.
Where the training signal comes from
Two decisions: who labels each answer, and how you balance the good rows against the bad ones.
A small example
Labelled good, from a thumbs-up
Labelled bad, by the judge
it is wet
the end
No partner needed
The two rows have different prompts, and neither has a second answer. DPO could not use either one; KTO uses both.
Going the other way is easy: any DPO pair splits into two KTO rows, its chosen answer as good and its rejected answer as bad.
What a training batch looks like
| Prompt | Answer | Label | Source |
|---|---|---|---|
| Give me a tip for remembering names. | Repeat the name right away, “Nice to meet you, Sam”, then use it once more before you say goodbye. | good | thumbs-up |
| Write a haiku about rain. | Rain is falling down / it is wet / the end | bad | judge |
| Is 91 a prime number? | No. 91 = 7 × 13. | good | checker |
| Is 91 a prime number? | Yes, 91 is prime. | bad | checker |
| Translate “thank you” to Spanish. | Gracias. | good | judge |
| Summarise the email in one line. | The email is about some things. | bad | thumbs-down |
# kto_train.jsonl: one answer per line, with a true / false label {"prompt": [{"role": "user", "content": "Give me a tip for remembering names."}], "completion": [{"role": "assistant", "content": "Repeat the name right away, \"Nice to meet you, Sam\", then use it once more before you say goodbye."}], "label": true} {"prompt": [{"role": "user", "content": "Write a haiku about rain."}], "completion": [{"role": "assistant", "content": "Rain is falling down\nit is wet\nthe end"}], "label": false} # KTOConfig(beta=0.1, desirable_weight=λ_D, undesirable_weight=λ_U)
The reference point is measured from the batch itself. The trainer pairs each prompt with another row's answer, which has nothing to do with it, and measures how much more likely the trained model makes those mismatched answers than the reference does. That average, clamped at 0, is \(z_0\).
\(z_0\) only sets the bar: no gradient flows through it. A batch of one row would pair the row with itself, which is why TRL asks for a per-device batch size above 1.
The loss
Read it aloud: “Score each answer against the reference, as in DPO. A good answer earns value when its score is above the reference point, a bad one when its score is below it. The value goes through a sigmoid, so it can never exceed the row's weight. The loss is the value still missing.” The first line is the loss; the second defines the value \(v\) it uses, with one case for good rows and one for bad rows. Point at a row of the table to light its term up.
one row + batch Inside \(\mathbb{E}\) is the loss of one row: one answer with its good/bad label. One piece is not per row: the reference point \(z_0\) is measured on the whole batch, so a row's loss also depends on the rows it was batched with. Station 7 follows one full training step, from the batch to the update.
Every symbol · both lines of the formula
| Symbol | What it means |
|---|---|
| The data | |
| \(x\) | dataThe prompt, for example “Write a haiku about rain.” |
| \(y\) | dataOne answer to that prompt. It has no partner: each row is a single answer with a label. |
| “good” / “bad” | dataThe row's label: true or false in the JSONL file. It decides which of the two cases of \(v\) applies. |
| \(\mathcal{D}\) | dataThe dataset of labelled rows \((x, y, \text{label})\). |
| \(\mathbb{E}_{(x,y)\sim\mathcal{D}}\big[\;\cdot\;\big]\)“expected value over the dataset” | mathThe average of the bracket over the rows: in practice, over the rows of a batch. \(\sim\) reads “drawn from”. |
| The score and the bar | |
| \(\hat r_\theta(x,y)\)“r hat”, the score | trainedThe answer's score, \(\log\pi_\theta(y\mid x) - \log\pi_{\text{ref}}(y\mid x)\), as in DPO: how much more likely the trained model \(\pi_\theta\) makes this answer than the frozen reference \(\pi_{\text{ref}}\) does. 0 at the start. |
| \(z_0\)“z nought”, the reference point | trainedThe reference point: an estimate of how far the model has drifted from the reference overall, measured on mismatched pairs inside the batch (the third tab above), clamped at 0 and not trained on. It is the bar an answer's score is compared with. No gradient flows through it. |
| \(\hat r_\theta - z_0\) | trainedFor a good answer: how far its score is above the bar. Positive is good. |
| \(z_0 - \hat r_\theta\) | trainedFor a bad answer, the order flips: how far its score is below the bar. Positive is good here too. |
| \(\beta\)“beta” | settingHow steep the sigmoid is: the same role as in DPO, usually 0.1 (KTOConfig(beta=0.1)). |
| \(\sigma\)“sigma”, the sigmoid | math\(\sigma(u) = 1/(1+e^{-u})\): squashes any number into the range 0 to 1, with \(\sigma(0) = 0.5\). It is the S shape that makes far-away answers count less and less. |
| Value, weights and loss | |
| \(v(x,y)\)“v”, the value | trainedThe value of the answer, as in prospect theory: how far it sits on the right side of the reference point, flattened by a sigmoid so that distant answers matter less and less. It lies between 0 and the row's weight. |
| \(\begin{cases}\cdots & \text{if } \dots\\ \cdots & \text{if } \dots\end{cases}\)a curly brace with cases | math“Use the top line when the condition on its right is true, otherwise the bottom line.” Here: top line for good answers, bottom line for bad ones. |
| \(\lambda_y\)“lambda y” | settingThe weight of the row's class: \(\lambda_D\) for good (desirable) answers, \(\lambda_U\) for bad (undesirable) ones. It is also the most value a row can earn, so each row's loss lies between 0 and \(\lambda_y\). |
| \(\lambda_D\), \(\lambda_U\)“lambda D”, “lambda U” | settingdesirable_weight and undesirable_weight in KTOConfig. Both default to 1; raise the one for the rarer label (step 3 of the maths). |
| \(\mathcal{L}_{\text{KTO}}(\theta)\)“L KTO of theta” | resultThe loss over single labelled answers. Rows have no partner; each one counts on its own. \(\lambda_y - v\) is “the value still missing”: 0 when the answer sits far on the right side of the bar. |
What you must supply: rows of prompt \(x\), one answer \(y\) and a good/bad label, plus \(\beta\), \(\lambda_D\) and \(\lambda_U\). No second answer and no score. The reference model is your frozen starting model, and \(z_0\) is measured by the trainer from each batch.
How the loss works, step by step
- Score the answer. As in DPO: trained log-probability minus reference log-probability, \(\hat r_\theta\).
- Measure the bar. The trainer pairs prompts with other rows' answers, scores those mismatched pairs the same way, averages and clamps at 0: \(z_0\). It is how much the model has drifted on answers that have nothing to do with quality.
- Distance on the right side. Good answer: \(\hat r_\theta - z_0\). Bad answer: \(z_0 - \hat r_\theta\). Either way, positive means “where it should be”.
- Value. Multiply by \(\beta\), pass through \(\sigma\), multiply by the class weight: \(v\), between 0 and \(\lambda_y\).
- Loss. \(\lambda_y - v\): the value still missing. The gradient raises a good answer's log-probability and lowers a bad one's, most strongly when the answer sits near the bar, since that is where the sigmoid is steepest.
Worked example: β = 0.1, z₀ = 0.05 (from the batch tab), λ_D = λ_U = 1
A good answer lowers the loss by rising faster than the general drift; a bad one by falling, or at least rising less than the drift. With \(\lambda_D = 4\) the names-tip row would count four times as much: loss \(4 \times 0.451 = 1.81\) and a push four times stronger. That is how the weights rebalance a dataset that is mostly complaints.
One training step on the GPU
A KTO step is #1's step on single answers, each with a good/bad label, plus an extra pass without gradient that measures the reference point \(z_0\) on the batch itself. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Take a batch of 4 single rows with 10 answer tokens each: 2 labelled good, 2 bad. No row has a partner.
- A row's k needs its whole row and z₀, but no partner. Its score is a sum over its own 10 tokens; z₀ comes from the batch and is treated as a constant. One row's k never depends on another row's probabilities, except through z₀.
- No division by length: every token of a row gets that row's full k.
- Not one after the other. The 40 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
Each row is a prompt with one answer and its label. From the same batch the trainer builds B mismatched pairs, each prompt with another row's answer; they are only used to measure \(z_0\).
2Forward pass
As in DPO, the trained model and the frozen reference both read every row, and each answer gets its score \(\hat r = \log\pi_\theta - \log\pi_{\text{ref}}\), summed over its tokens. Then both models read the mismatched pairs, with no gradient: their scores, averaged and clamped at 0, give \(z_0\).
3Loss: from each row to one number
Each row's loss is \(\lambda_y - v\), with the \(z_0\) shared by the whole batch; the batch averages them.
4Backward pass: the slope at every answer token
(\(y_t\) is 1 at the target token and 0 at every other word.) \(\sigma_i\) is the sigmoid inside the row's value. \(\sigma(1-\sigma)\) is largest, ¼, when the row sits right at the bar and fades on both sides, so rows near \(z_0\) push hardest. No partner is needed: each row pushes on its own, up if good, down if bad. \(z_0\) is a constant for the gradient. Below the logits, #1's backward pass, on the B real rows only.
5Update
Once. The reference stays frozen; the next batch measures its own \(z_0\).
On a real GPU
- Four sequence reads per row: the row and its mismatched pair, each through both models, batched into a few forward calls. Only the trained model's read of the real rows is backpropagated.
- z₀ comes from the batch, so a bigger batch, or averaging \(z_0\) over the rows of all GPUs, gives a steadier bar.
- Memory holds the trained model and the reference, as in DPO.
The maths behind it
Prospect theory in one table
Kahneman and Tversky found that people value outcomes as gains or losses relative to a reference point, not in absolute terms, and that both flatten out: a second £1,000 feels smaller than the first. KTO's value has the same S shape. With \(\beta = 1\) for readable numbers:
score above the reference point r̂ − z₀ −4 −2 0 +2 +4 value of a good answer σ(r̂ − z₀) 0.02 0.12 0.50 0.88 0.98 push on it σ·(1 − σ) 0.02 0.10 0.25 0.10 0.02The push is largest at the reference point and fades on both sides. A good answer far above the bar barely moves. So does one hopelessly far below it, which makes KTO somewhat robust to wrong labels: an answer labelled good whose score is already far below the reference point gets little push.
Why a reference point, not a partner
In DPO the rejected answer is the bar the chosen one has to clear. KTO has no partner, so the bar is the model's general drift from the reference:
$$z_0 \;\approx\; \max\Big(0,\ \frac{1}{m}\sum_{i=1}^{m}\log\frac{\pi_\theta(y'_i\mid x_i)}{\pi_{\text{ref}}(y'_i\mid x_i)}\Big)$$\(z_0\) resultThe reference point, the bar each answer's score is compared with. \(\approx\)“is approximately” mathThe batch gives an estimate, not the exact value. \(\max(0,\ \cdot\,)\) mathTake the larger of 0 and the average: a negative estimate is clamped to 0. \(m\) mathThe number of mismatched pairs in the batch. \(\frac{1}{m}\sum_{i=1}^{m}\) mathThe average over those \(m\) pairs, \(i = 1, 2, \dots, m\). \(x_i\) dataThe prompt of row \(i\). \(y'_i\)“y prime i” dataThe answer of another row, glued to prompt \(x_i\). The prime mark \('\) means “a different one”. \(\log\frac{\pi_\theta(y'_i\mid x_i)}{\pi_{\text{ref}}(y'_i\mid x_i)}\) trainedThe score of that mismatched pair: trained log-probability minus the frozen reference's. In the batch tab: +0.12, +0.05 and −0.02, average +0.05. Here \(y'_i\) is another row's answer, so the pair \((x_i, y'_i)\) is unrelated: the average measures drift that has nothing to do with quality, an estimate of \(\mathrm{KL}(\pi_\theta\,\|\,\pi_{\text{ref}})\). A good answer has to rise by more than the general drift to count as a gain; a bad one earns its value by staying below the drift, rising by less than it or falling.
Balancing good and bad rows
The KTO paper recommends weights that put the good side's total weight level with the bad side's, or up to one and a half times it:
$$1 \;\le\; \frac{\lambda_D\, n_D}{\lambda_U\, n_U} \;\le\; \frac{3}{2}$$\(n_D\), \(n_U\) dataHow many good (desirable) and bad (undesirable) rows the dataset has. \(\lambda_D\), \(\lambda_U\) settingThe weights you give each good row and each bad row. \(\lambda_D\, n_D\), \(\lambda_U\, n_U\) mathThe total weight of each side: number of rows times weight per row. \(1 \le \cdots \le \frac{3}{2}\) math\(\le\) reads “at most”. The ratio of the two totals should be between 1 (level) and 1.5 (the good side up to half as heavy again). Earlier versions of the paper, and the warning
KTOTrainerprints, use 4/3 as the upper bound.n_D = 200 good rows, n_U = 800 bad rows, λ_U = 1 λ_D × 200 / 800 ≥ 1 → λ_D ≥ 4 λ_D × 200 / 800 ≤ 3/2 → λ_D ≤ 6Less information per row, more rows
“B is worse than A” says which way to move between two specific answers. “B is bad” only says that B should fall. A label carries less information than a comparison, but labels are far easier to collect. The KTO paper reports matching or beating DPO at model sizes from 1B to 30B parameters, and, for a 7B model, still beating DPO after discarding up to 90% of the good answers.
Watch out for
Unbalanced labels
Real feedback is mostly complaints. Without adjusting \(\lambda_D\) and \(\lambda_U\), the model mostly learns what not to do. Check the ratio first.
The bar needs a real batch
\(z_0\) is measured on mismatched pairs inside each batch. Tiny batches give a noisy bar, and a batch of one gives none: TRL requires a per-device batch size above 1.
Labels without context
A step labelled bad because the whole session failed may have been a fine step. Label steps, not sessions, when you can.
Not a free upgrade
With plenty of clean pairs, try DPO as well. KTO's strength is the data pairs cannot use, not a better use of pairs.
One stage, no reference model
ORPO Odds ratio preference optimisation
Do SFT and preference learning in a single run. ORPO trains on the chosen answer exactly like SFT and adds a penalty that grows as the rejected answer's odds come close to the chosen one's, or pass them. The SFT part keeps the model writing well, so no frozen reference model is needed.
SFT on the chosen answer plus an odds-ratio penalty, with no reference model.
- Changes vs DPO
- No reference model and no separate SFT stage: one loss, SFT plus an odds-ratio penalty
- Its answers
- Q2 changed: the SFT term takes the reference model's job
- Cost
- ●●●●● one run, one model in memory
- Library
- Ready (exp.): TRL
ORPOTrainer. Pin the TRL version
The idea in plain words
The usual recipe has two stages: SFT on good answers, then DPO on pairs, with the SFT model frozen as the reference. That is two training runs, and a second model in memory during the second one.
ORPO merges them. Its loss is the SFT loss on the chosen answer, plus \(\lambda\) times a penalty that compares the odds of the chosen answer with the odds of the rejected one. The SFT part teaches the model to write the good answer; the penalty adds the “not that” which SFT lacks. Because the model keeps learning to produce good text, it cannot win comparisons by turning into nonsense. In DPO, preventing that was the reference model's job.
Odds are another way of writing a probability: \(p/(1-p)\). A probability of 0.5 is odds of 1 (“one to one”); 0.8 is odds of 4. ORPO uses each answer's average per-token probability, so that long answers do not always look unlikely.
See it work
SFT alone against SFT plus the odds penalty
The four answers from the DPO lab. Two models start equal: one trains with SFT on the chosen answer only, the other with ORPO. Train both and compare where the rejected answer ends up. Raise λ to strengthen the penalty.
Where the training signal comes from
The same pairs as DPO, with one difference: the chosen answer is now also the SFT target, so it has to be good enough to imitate.
A small example
One row, two jobs
What ORPO computes
What is not computed
DPO: the trained model and the frozen reference, four log-probabilities per row, after a separate SFT run.
ORPO: the trained model only, two log-probabilities per row, in one run that starts from the base model.
What a training batch looks like
| Prompt | Chosen · SFT target and preferred | Rejected · pushed down |
|---|---|---|
| Explain why the sea is salty, for a 10-year-old. | Rain slowly dissolves tiny bits of salt from rocks… | Ocean salinity results from the weathering of crustal minerals… |
| Is 91 a prime number? | No. 91 = 7 × 13. | Yes, 91 is prime: it is odd and not divisible by 3 or 5. |
| Write a one-line reminder to water the plants. | Don't forget to water the plants today! | This is a friendly reminder that it would be a good idea to water the plants… |
# orpo_train.jsonl: exactly the DPO format {"prompt": [{"role": "user", "content": "Is 91 a prime number?"}], "chosen": [{"role": "assistant", "content": "No. 91 = 7 × 13."}], "rejected": [{"role": "assistant", "content": "Yes, 91 is prime: it is odd and not divisible by 3 or 5."}]} # ORPOConfig(beta=0.1) ← "beta" here is the paper's λ, the weight of the odds penalty
The loss
Read it aloud: “Do ordinary SFT on the chosen answer. Then compare the two answers' odds, and add a penalty that grows as the rejected answer's odds come close to the chosen one's, or pass them. λ sets how much the penalty counts.” The second line defines the two ingredients: an answer's odds, and the average per-token probability \(P_\theta\) they are built from. Point at a row of the table to light its term up.
one pair Inside \(\mathbb{E}\) is the loss of one pair: the SFT term on its chosen answer plus \(\lambda\) times its odds penalty. \(\mathbb{E}\) means “averaged over pairs”, in practice over a batch. Station 7 follows one full training step, from the batch to the update.
Every symbol · both lines of the formula
| Symbol | What it means |
|---|---|
| The data | |
| \(x\) | dataThe prompt. |
| \(y_w\)“y win” | dataThe chosen answer. In ORPO it is both imitated (the SFT term) and preferred (the penalty). |
| \(y_l\)“y lose” | dataThe rejected answer. It is only pushed down, never imitated. |
| \(y\) | dataIn the second line: either answer, \(y_w\) or \(y_l\), in turn. |
| \(\mathbb{E}_{(x,y_w,y_l)}\big[\;\cdot\;\big]\) | mathThe average of the bracket over the pairs in the dataset (in practice, in a batch). The same rows as DPO. |
| From tokens to odds | |
| \(\theta\) | trainedThe weights being trained. There is no reference model: \(\theta\) is the only model in the formula. |
| \(p_\theta(y_t\mid x, y_{<t})\) | trainedThe probability the model gives to token \(t\) of the answer, having read the prompt and the answer's earlier tokens \(y_{<t}\). |
| \(|y|\), \(\frac{1}{|y|}\sum_{t=1}^{|y|}\) | math\(|y|\) is the number of tokens in the answer; \(\frac{1}{|y|}\sum_t\) averages over them. So the inside of \(\exp\) is the average token log-probability. |
| \(\exp(\cdot)\) | math\(e\) to the power of the bracket: it undoes the log, turning the average log-probability back into a probability. |
| \(P_\theta(y\mid x)\)capital P, “average probability” | trainedThe answer's average per-token probability (the geometric mean). Using the average, not the product, stops long answers from always having tiny odds. Between 0 and 1. |
| \(\text{odds}_\theta(y\mid x)\) | trainedOdds: the probability divided by its complement, \(P / (1-P)\). Odds of 1 mean fifty-fifty; \(P = 0.60\) gives odds 1.5 (“three to two”), \(P = 0.40\) gives 0.67. |
| The two terms of the loss | |
| \(\mathcal{L}_{\text{SFT}}(\theta;\,x,y_w)\)“SFT loss on the chosen answer” | trainedPlain SFT loss (#1 in the SFT workbook) on the chosen answer only: the average of \(-\log p_\theta\) over its tokens. This is the anchor that replaces the reference model. The semicolon separates the weights from the data they are evaluated on. |
| \(\frac{\text{odds}_\theta(y_w\mid x)}{\text{odds}_\theta(y_l\mid x)}\)“the odds ratio”, OR | trainedHow many times larger the chosen answer's odds are than the rejected one's. Above 1 when the model already favours the chosen answer. |
| \(\log\text{OR}\)“log odds ratio” | trainedThe log of that ratio: positive when the chosen answer is ahead, 0 when they are level, negative when the rejected answer is ahead. It plays the part of DPO's gap. |
| \(\sigma\) | mathThe sigmoid, \(\sigma(u) = 1/(1+e^{-u})\): turns the log odds ratio into a probability that the chosen answer wins. |
| \(-\log\sigma(\log\text{OR})\) | trainedThe odds-ratio penalty: DPO's log-sigmoid shape applied to the log of the two answers' odds ratio. 0.69 when the answers are level, near 0 when the chosen answer's odds are far higher, large when the rejected answer is ahead. |
| \(\lambda\)“lambda” | settingHow much the odds penalty counts, often 0.1. TRL calls it beta. Note that there is no β inside the sigmoid here. |
| \(\mathcal{L}_{\text{ORPO}}(\theta)\) | resultThe ORPO loss: one number per pair, with nothing frozen beside the model. SFT term plus λ times the penalty. |
What you must supply: the same rows as DPO, prompt \(x\), chosen \(y_w\) and rejected \(y_l\), plus \(\lambda\). No reference model and no separate SFT run first: ORPO starts from the base model. Because the chosen answers are imitated, they must be good enough to copy.
How the loss works, step by step
- One pass per answer. Run the chosen and the rejected answer through the model. Each gives a log-probability per token.
- SFT term. On the chosen answer only: the average of \(-\log p_\theta\) over its tokens, exactly as in plain SFT.
- Average probability. For each answer, average its token log-probabilities and take \(\exp\): \(P_w\) and \(P_l\).
- Odds and their ratio. \(P/(1-P)\) for each, then the log of chosen odds over rejected odds.
- Penalty. \(-\log\sigma\) of that log ratio: small when the chosen answer is well ahead.
- Add. SFT term \(+\ \lambda \times\) penalty. The gradient of the first term raises every chosen token; the second also lowers the rejected answer, pushing hardest when the rejected answer is already likely.
Worked example: λ = 0.1, numbers per token
The loss goes down as the chosen answer becomes more likely (both terms fall) and as the rejected one becomes less likely (the penalty falls). Unlike DPO, the chosen answer cannot quietly lose probability while the gap grows: the SFT term would rise and stop it.
One training step on the GPU
An ORPO step runs on pairs like DPO, but with no reference model: one model, two answers per pair, and a slope that combines SFT on the chosen answer with a push that separates the two. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Take one pair: a chosen answer of 10 tokens and a rejected answer of 8. There is no reference model.
- The two k's need the whole pair first: both answers' average probabilities and the log odds ratio.
- Each answer's push is shared over its own length (÷ 10, ÷ 8), because P is an average per token. In a batch of B pairs, also ÷ B.
- Not one after the other. The 18 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
As in DPO: prompt + chosen and prompt + rejected, stacked into one grid of 2B sequences.
2Forward pass
Only the trained model runs, with gradient, on all 2B sequences. For each answer it averages log p over the tokens, which gives \(P_w\) and \(P_l\).
3Loss: from each pair to one number
Each pair's loss is the SFT term on its chosen answer plus \(\lambda\) times its odds penalty; the batch averages the pairs. (TRL pools the SFT term over all chosen-answer tokens of the batch, as in #1, and averages the penalty over the pairs.)
4Backward pass: the slope at every answer token
(\(y_t\) is 1 at the target token and 0 at every other word.) \(\ell\) is the log odds ratio. The chosen answer always gets the full SFT push (the 1) plus a little more from the penalty; the rejected answer gets only the penalty, downward. The penalty's push grows when the rejected answer is likely (\(1-P_l\) small) and fades once the chosen answer is well ahead (\(\sigma(-\ell)\) small). Below the logits, #1's backward pass on all 2B sequences.
5Update
Once. There is no reference to keep.
On a real GPU
- Two answers per pair, one model. Both answers are backpropagated and there is no reference: that saves the reference's forward passes and its weights (2 bytes per weight, 16 GB for an 8B model, next to about 130 GB for the trained model). With LoRA, DPO's reference is the same network with the adapter switched off, so the memory saving disappears; the time saving stays.
- No separate SFT run. The SFT term replaces the SFT stage that usually comes before DPO.
The maths behind it
Probability, odds and log odds
p odds p/(1−p) log odds 0.10 0.11 −2.20 0.50 1.00 0.00 0.60 1.50 +0.41 0.80 4.00 +1.39 0.95 19.00 +2.94Log odds run from \(-\infty\) to \(+\infty\) and stretch out near 0 and 1, where a probability looks almost stuck.
The penalty compares the two answers' log odds
$$\log\frac{\text{odds}_\theta(y_w)}{\text{odds}_\theta(y_l)} = \log\text{odds}_\theta(y_w) - \log\text{odds}_\theta(y_l)$$\(\text{odds}_\theta(y_w)\), \(\text{odds}_\theta(y_l)\) trainedThe odds of the chosen and of the rejected answer, \(P/(1-P)\), from the model's average per-token probability. The prompt \(x\) is left out of the notation to keep it short. \(\log\frac{a}{b} = \log a - \log b\) mathThe log of a ratio is a difference of logs. So the log odds ratio is just “chosen log odds minus rejected log odds”, a gap like DPO's. chosen 0.60, rejected 0.40: +0.405 − (−0.405) = 0.81 → penalty −log σ(0.81) = 0.37 chosen 0.80, rejected 0.20: +1.386 − (−1.386) = 2.77 → penalty −log σ(2.77) = 0.06What the penalty's gradient does
Writing \(P_w\) and \(P_l\) for the two average probabilities, and using \(\nabla\log\text{odds}(P) = \nabla\log P\,/\,(1-P)\):
$$\nabla_\theta\Big(-\log\sigma(\log\text{OR})\Big) = -\,\sigma\big(-\log\text{OR}\big)\left[\frac{\nabla_\theta\log P_w}{1-P_w} - \frac{\nabla_\theta\log P_l}{1-P_l}\right]$$\(\nabla_\theta\)“nabla theta”, the gradient mathFor every weight: how much the quantity after it changes when that weight is nudged up. Training steps the opposite way to the loss's gradient. \(\log\text{OR}\) trainedThe log odds ratio, chosen log odds minus rejected log odds. \(\sigma(-\log\text{OR})\) trainedThe weight of this pair: the probability the model currently gives to the wrong ranking. Near 1 while the rejected answer is ahead, near 0 once the chosen one is well ahead. \(P_w\), \(P_l\) trainedThe average per-token probabilities of the chosen and the rejected answer. \(\nabla_\theta\log P_w\), \(\nabla_\theta\log P_l\) trainedThe SFT directions that make each answer more likely. The minus sign in front of the rejected one turns it into “less likely”. \(\frac{1}{1-P}\) mathAn amplifier that comes from the odds: it is 1.25 at \(P = 0.2\) and 5 at \(P = 0.8\), so a rejected answer the model already finds likely gets the strongest push. As in DPO: raise the chosen answer, lower the rejected one, harder while the ranking is poor. The \(1/(1-P)\) factors add something new: the more likely the rejected answer already is, the harder it is pushed down.
rejected answer's average probability 0.20 0.50 0.80 factor 1 / (1 − P) 1.25 2.00 5.00 ← a likely rejected answer gets the strongest pushWhy no reference model is needed
DPO needs \(\pi_{\text{ref}}\) because its loss on its own can be satisfied by wrecking the model. In ORPO the SFT term keeps pulling toward the chosen answers, token by token, so the model must stay a good writer of them; the penalty only reshapes how much probability the rejected answers keep. The price is that the chosen answers must be good enough to imitate, because now they are imitated, not just preferred.
Watch out for
Chosen answers are copied
In DPO a chosen answer only has to beat the rejected one. In ORPO it is also the SFT target, so a mediocre chosen answer is learned word for word.
An experimental API
ORPOTrainerlives undertrl.experimentaland can change between releases. Pin the TRL version.λ trades fluency for contrast
Too small and ORPO is just SFT; too large and the penalty swamps the SFT anchor. Sweep a few values and evaluate both answer quality and preference win-rate.
Style comes with the chosen answers
The SFT term pulls the model's style toward the chosen answers, whatever model you start from. If they were written by another model, expect its voice too.
Same pairs, a different loss
SimPO, IPO and other DPO variants Changing pieces of the DPO loss
DPO's loss has three parts you can change: the score of an answer, the reference it is measured against, and the curve that turns the gap into a loss. Each variant changes one or more of them to fix a specific problem: answers that keep growing longer, a gap that never stops growing, or the cost of the reference model.
The same pair trained three ways: SimPO, IPO and DPO, and what length does to each.
- Changes vs DPO
- The loss only: the pairs stay the same, and most variants keep
DPOTrainer; SimPO moves toCPOTrainer - Its answers
- Q2 changed: a target gap (IPO), or no reference at all (SimPO)
- Cost
- ●●●●● as DPO; SimPO saves the reference
- Library
- Ready:
DPOTrainerwithloss_type="ipo","sigmoid_norm"(length-normalised) and about a dozen more. SimPO without a reference:CPOTrainer(exp.)
The idea in plain words
These are not new families; they are settings. The data is still (prompt, chosen, rejected) pairs, and most variants run in the same trainer with a different loss_type. What changes is how the gap between the two answers becomes a loss.
- IPO (identity preference optimisation) replaces DPO's log-sigmoid with a squared loss around a target gap. DPO keeps pushing the gap wider for as long as you train; IPO stops at the target, and pulls back if the gap overshoots, so it overfits less when the preferences are clean.
- Length-normalised DPO divides each answer's score by its number of tokens. A summed score rewards long answers just for being long.
- SimPO (simple preference optimisation) drops the reference model, scores each answer by its average log-probability, and asks the chosen answer to lead by a fixed margin \(\gamma\). No reference model, and much less length bias.
- Hinge, robust and the rest. A hinge loss stops pushing once a margin is met, as in SLiC. The robust loss assumes a known share of the labels are flipped and corrects for it. TRL lists about a dozen more.
See it work
How hard each loss pushes, and whether padding pays
Part one plots how strongly each loss pushes the gap wider at every gap size: move the gap and find the loss that never stops. Part two pads an answer with extra tokens and compares a summed score with an averaged one.
Which variant for which problem
The signal is the same pairs as DPO, so the choice here is about the symptom. Pick the problem you see.
A small example: one pair, three scores
The pair
Three ways to score it
Per token, the chosen answer improved twice as much. The summed score hides that, because the rejected answer has three times as many tokens to add up.
What a training batch looks like
The rows are exactly the DPO rows of #10: prompt, chosen, rejected. Switching variant is a configuration change, so you can compare variants on the same data in a few runs.
# DPOTrainer: same data as DPO, only the config changes loss_type = "ipo" # IPO: target gap 1 / (2β), measured per token in TRL: sweep β, do not reuse 0.1 loss_type = "sigmoid_norm" beta = 0.1 # DPO on length-normalised scores loss_type = "robust" label_smoothing = 0.1 # assume 10% of the pairs are flipped loss_type = "hinge" beta = 0.1 # stop pushing once β·gap reaches 1 # CPOTrainer (experimental: pin the TRL version): SimPO, no reference model loss_type = "simpo" cpo_alpha = 0.0 beta = 2.0 simpo_gamma = 0.5 # SimPO uses a much larger β than DPO (the paper: β 2 to 2.5, γ 0.5 to 1.5). # cpo_alpha defaults to 1.0, which adds an SFT term: set it to 0 for pure SimPO.
The loss
DPO, for comparison: summed scores, a reference, a log-sigmoid that never stops
$$-\log\sigma\big(\beta\,h\big), \qquad h = \log\frac{\pi_\theta(y_w\mid x)}{\pi_{\text{ref}}(y_w\mid x)} - \log\frac{\pi_\theta(y_l\mid x)}{\pi_{\text{ref}}(y_l\mid x)}$$Read it aloud (DPO): “Call the gap between the chosen and the rejected answer's log-ratios \(h\). The loss is minus the log of the sigmoid of β times \(h\).” This is the loss of #10, rewritten with a short name for the gap so that the variants below can reuse it.
IPO: a squared loss around a target gap
$$\htmlClass{term term-1}{\mathcal{L}_{\text{IPO}}} = \Big(\htmlClass{term term-2}{h} - \htmlClass{term term-3}{\frac{1}{2\beta}}\Big)^{2}$$In TRL (loss_type="ipo") each answer's log-ratio is first divided by its number of tokens, so \(h\) and the target are per-token gaps.
Read it aloud (IPO): “Square the distance between the gap and a target of \(1/(2\beta)\), so the loss is zero exactly at the target.” Too small a gap and too large a gap are both penalised.
SimPO: average log-probabilities, no reference, a fixed margin
$$\htmlClass{term term-4}{\mathcal{L}_{\text{SimPO}}} = -\log\sigma\Big(\htmlClass{term term-5}{\frac{\beta}{|y_w|}\log\pi_\theta(y_w\mid x)} - \frac{\beta}{|y_l|}\log\pi_\theta(y_l\mid x) - \htmlClass{term term-6}{\gamma}\Big)$$Read it aloud (SimPO): “Score each answer by its average log-probability, scaled by β, with no reference model, and ask the chosen answer to lead by at least γ.” Point at a row of the table to light its term up in the formulas.
one pair Each formula here is the loss of one pair. Training averages it over the pairs of a batch, exactly as DPO does. Station 7 follows one full training step, from the batch to the update.
Every symbol · the three formulas share most of them
| Symbol | What it means |
|---|---|
| Shared by all three | |
| \(x\) | dataThe prompt, for example “Summarise this news story in one sentence.” |
| \(y_w\), \(y_l\)“y win”, “y lose” | dataThe chosen and the rejected answer: exactly the DPO rows of #10. |
| \(\pi_\theta(y\mid x)\)“pi theta of y given x” | trainedThe probability that the model being trained writes the whole answer \(y\), the product of its token probabilities. \(\log\pi_\theta(y\mid x)\) is the sum of its token log-probabilities. |
| \(\beta\)“beta” | settingA number you choose, but its job differs per variant: DPO's leash (around 0.1); IPO's target \(1/(2\beta)\); SimPO's scale on the average log-probabilities (around 2 to 2.5). Do not reuse a value across variants. |
| \(\sigma\)“sigma”, the sigmoid | math\(\sigma(u) = 1/(1+e^{-u})\): squashes any number into a probability between 0 and 1. \(-\log\sigma(u)\) is 0.69 at \(u = 0\) and falls toward 0, never reaching it, as \(u\) grows. |
| DPO and IPO: the gap against a reference | |
| \(\pi_{\text{ref}}(y\mid x)\)“pi ref” | frozenThe same probability under the frozen reference, the model as it was before training. |
| \(\log\frac{\pi_\theta(y\mid x)}{\pi_{\text{ref}}(y\mid x)}\) | trainedAn answer's score, or log-ratio: how much more likely the trained model makes it than the reference does. A sum over the answer's tokens, so longer answers add up more. |
| \(h\)“h”, the gap | trainedThe gap between the two answers' log-ratios, as in DPO but without β in front: chosen score minus rejected score. In the example above, \(+2.0 - 3.0 = -1.0\). |
| \(\frac{1}{2\beta}\)“one over two beta” | settingThe target gap: β = 0.1 gives 5. A larger β means a smaller target, a tighter leash. In TRL the gap is per token, where 5 is very large: tune β again. |
| \(\big(\cdots\big)^{2}\)“squared” | mathMultiply the bracket by itself. A square is never negative and is 0 only when the bracket is 0, so missing the target in either direction costs the same. |
| \(\mathcal{L}_{\text{IPO}}\) | resultThe IPO loss for one pair. Its minimum is 0, reached exactly at the target gap. |
| SimPO: no reference | |
| \(|y_w|\), \(|y_l|\)“length of y” | dataThe number of tokens in each answer: 40 and 120 in the example. |
| \(\frac{\beta}{|y|}\log\pi_\theta(y\mid x)\) | trainedSimPO's score: the average log-probability per token, times β. No reference model appears anywhere. Dividing by the length means a long answer gains nothing just by being long. |
| \(\gamma\)“gamma” | settingThe target margin, around 0.5 to 1.5: the curve is shifted so that the push only falls below half its strength once the chosen answer leads by more than γ. Unlike a hinge, it never reaches zero. A larger γ asks for a clearer separation. |
| \(\mathcal{L}_{\text{SimPO}}\) | resultThe SimPO loss: DPO's shape with a different score inside. |
What you must supply: the same pairs as DPO, \(x\), \(y_w\), \(y_l\). Switching variant only changes settings: \(\beta\) for each, plus \(\gamma\) for SimPO. IPO and length-normalised DPO still need the frozen reference; SimPO does not.
How each loss works, step by step
- Score both answers. DPO and IPO: summed log-ratio against the reference. SimPO: average log-probability of the trained model alone.
- Take the gap. Chosen score minus rejected score; SimPO also subtracts the margin \(\gamma\).
- Apply the curve. DPO and SimPO: \(-\log\sigma\), which keeps rewarding a wider gap forever, more and more weakly. IPO: a square around the target, which pushes the gap up when it is too small and pulls it back when it is too large.
- Step. As in DPO, the gradient raises the chosen answer and lowers the rejected one. Only how hard, and when it stops, changes from variant to variant.
Worked example: the news-summary pair above, chosen 40 tokens, rejected 120
| Score of an answer | Needs the reference | Where the loss reaches 0 | Settings | |
|---|---|---|---|---|
| DPO | summed log-ratio | yes | never: only as the gap goes to infinity | β |
| IPO | summed log-ratio (per token in TRL) | yes | exactly at the target gap \(1/(2\beta)\) | β |
| SimPO | average log-probability | no | never, but the push drops below half once the lead passes γ | β, γ |
One training step on the GPU
All three variants run the DPO step on pairs. They differ in how an answer is scored (summed, per token or averaged), in whether the reference runs, and in the curve that turns the gap into the size of the push. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Take one pair: a chosen answer of 10 tokens and a rejected answer of 8. The variants differ only in the numbers computed before the slopes.
- Only the first numbers change. The forward pass, the form of the 18 slopes, the sum and the one move are the same for all three.
- IPO's k changes sign past the target gap: then the chosen tokens are lowered and the rejected ones raised, pulling the gap back.
- SimPO divides by each answer's length, so each of the 8 rejected tokens carries 1/8 of its answer's push. In the summed form, DPO and IPO give every token the full k. In a batch of B pairs, every k is also ÷ B.
- Not one after the other. The 18 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
As in DPO: 2B sequences, prompt masked.
2Forward pass
The trained model reads all 2B sequences with gradient. DPO and IPO also run the frozen reference, or read its stored numbers. SimPO does not: it scores each answer by its average log p under the trained model alone.
3Loss: from each pair to one number
Each pair gives one loss with its variant's formula; the batch averages them.
4Backward pass: the slope at every answer token
Each variant hands every token of the chosen answer \(+k_w\,(p_t - y_t)\) and every token of the rejected answer \(-k_l\,(p_t - y_t)\), with \(y_t\) 1 at the target token and 0 elsewhere. For DPO and IPO \(k_w = k_l = k\); SimPO divides by each answer's own length. Below the logits, #1's backward pass on all 2B sequences.
| variant | k for the pair, before dividing by B | how it behaves |
|---|---|---|
| DPO | \(\beta\,\sigma(-\beta h)\) | always positive, fades as the gap grows |
| IPO | \(2\,\big(\tfrac{1}{2\beta} - h\big)\) | changes sign: pushes the pair apart below the target gap, pulls it back above (TRL's per-token version also divides by each answer's length) |
| SimPO | \(\beta\,\sigma(-m)/|y|\), with \(m = \tfrac{\beta}{|y_w|}\log\pi_\theta(y_w) - \tfrac{\beta}{|y_l|}\log\pi_\theta(y_l) - \gamma\) | divided by each answer's own length; fades once the lead passes γ |
5Update
Once.
On a real GPU
- SimPO is the cheapest: two forward passes per pair and one model in memory.
- DPO and IPO need the reference: two more sequence reads per pair, without gradient, or its numbers computed once before training.
The maths behind it
Why DPO's gap grows forever
\(-\log\sigma(\beta h)\) only reaches 0 as \(h \to \infty\). When a pair's preference is certain, as it is for every pair in a clean dataset, the best answer to the loss is to make the rejected answer infinitely unlikely, whatever \(\beta\) is. The leash only holds where the data itself is uncertain.
β·h loss −log σ(βh) push σ(−βh) 0 0.69 0.50 2 0.13 0.12 5 0.0067 0.0067 10 0.000045 0.000045 ← tiny, but never zero: with enough steps the gap keeps growingIPO stops at a target
$$\frac{\partial\mathcal{L}_{\text{IPO}}}{\partial h} = 2\Big(h - \frac{1}{2\beta}\Big)$$\(\frac{\partial\mathcal{L}_{\text{IPO}}}{\partial h}\)“partial L-IPO over partial h” resultThe slope of the IPO loss as the gap \(h\) changes. Training moves \(h\) against the slope: a negative slope means “widen the gap”, a positive one “narrow it”. \(h\) trainedThe gap: chosen log-ratio minus rejected log-ratio. \(\frac{1}{2\beta}\) settingThe target gap, set by your choice of β. \(2(\cdots)\) mathThe slope of a square \(u^2\) is \(2u\): twice the distance from the target, so the pull is proportional to how far off the gap is. Negative below the target, so the gap is pushed up; zero at the target; positive above it, so the gap is pulled back. The leash now holds even when every label is certain.
β = 0.1 → target gap 1 / (2 × 0.1) = 5 h = 0: slope 2 × (0 − 5) = −10 pushed up hard h = 4: slope 2 × (4 − 5) = −2 pushed up gently h = 5: slope 0 stops h = 7: slope 2 × (7 − 5) = +4 pulled backSummed scores pay for length
DPO's score is a sum over tokens, \(\sum_t \log\frac{\pi_\theta(y_t\mid\cdot)}{\pi_{\text{ref}}(y_t\mid\cdot)}\). Once training has nudged every token of the preferred style up a little, each extra token of that style adds to the score: doubling the answer doubles the score.
per-token log-ratio after some training: +0.05 40-token answer: summed 40 × 0.05 = 2.0 average 0.05 80-token answer: summed 80 × 0.05 = 4.0 average 0.05 ← twice the score for the same qualitySimPO: score answers the way generation does
SimPO's authors point out that generation, especially with beam search, ranks text by its average log-probability per token. So they use exactly that as the score, \(\frac{1}{|y|}\log\pi_\theta(y\mid x)\). No reference is needed, training runs one model, and the margin \(\gamma\) replaces DPO's zero line.
A map of the variants
Variant Score of an answer Reference Loss curve Fixes DPO summed log-ratio yes log-sigmoid the baseline Length-normalised DPO average log-ratio yes log-sigmoid length bias IPO summed log-ratio; per token in TRL yes squared, target \(1/(2\beta)\) overfitting clean pairs Hinge summed log-ratio, as in RSO and TRL; SLiC-HF's original uses plain log-probs yes stops at a margin overfitting Robust DPO summed log-ratio yes log-sigmoid, corrected for flips noisy labels SimPO average log-prob no log-sigmoid with margin \(\gamma\) length bias, memory CPO log-prob no log-sigmoid + SFT term memory
Watch out for
No reference, no leash
SimPO and CPO keep one model in memory. SimPO has no anchor at all; CPO's SFT term ties the model to the chosen answers, as in ORPO, but not to where it started. Watch fluency and general skills, not just the win-rate.
Each variant brings its own knob
IPO's β sets a target gap; SimPO adds γ and needs a much larger β than DPO. Values tuned for DPO do not carry over.
Experimental trainers
CPOTrainer, where SimPO lives, is undertrl.experimental. Pin the version.Fix the data first
If the chosen answers are systematically longer, a length-normalised loss treats the symptom; length-matched pairs fix the cause.
Cut the pair at the first wrong step
Step-level DPO Step-DPO · step-wise preference optimisation
In a multi-step answer, find the first step that went wrong. Keep everything before it as a shared history, and build the pair right there: the correct next step as chosen, the wrong one as rejected. The preference lands on the exact step that made the difference.
A recorded solution, the first wrong step, and a pair where only one word moves.
- Changes vs DPO
- What the pair compares: one step after a shared history, not two whole answers
- Its answers
- Q3 changed: the credit lands on one step
- Cost
- ●●●●● find the first error, write and check a fix
- Library
- Build it: pairs cut at the first wrong step →
DPOTrainer
The idea in plain words
Two whole answers to a maths problem usually differ everywhere: different wording, a different order, and somewhere a mistake. A whole-answer pair tells the model “this whole text is better than that whole text”, so the push lands on every token, including all the correct steps of the losing answer.
Step-level DPO cuts the pair where it matters. A judge or a checker finds the first wrong step. The history up to that point goes into the prompt, shared by both sides and never graded. The rejected completion is the wrong step; the chosen completion is a correct step written for the same history. Nothing else differs, so all of the signal falls on that one decision.
It is the preference version of correction SFT (#6 in the SFT workbook). Correction SFT trains only on the right step; step-level DPO also pushes the wrong step down, from the same situation.
See it work
Whole answers against one step
Two solutions to the same problem: A is right, B goes wrong at step 3. Compare what a whole-answer pair and a step-level pair push up and down. Then move the cut and see why the first wrong step is the place to cut.
Where the training signal comes from
Three jobs: find the first wrong step, write a correct one, and check it.
A small example
The recorded solution
The step-level pair
Step 1: Taking a third off £12 removes £4.
Step 2: Each ticket is then £8.
What is left out of the row
Step 4 of the recorded solution (“Total: £45”), which only follows from the mistake, and any other complete solution. Neither one is about the decision at step 3.
The two completions differ by a single number. Everything the model learns from this row is about that number.
What a training batch looks like
| Shared history · the prompt | Chosen step | Rejected step | Error found by |
|---|---|---|---|
| Train tickets + steps 1–2 | For five tickets: 5 × £8 = £40. | For five tickets: 5 × £8 = £45. | checker |
| “Is 2025 a leap year?” + “2025 ÷ 4 = 506.25.” | 506.25 is not a whole number, so 2025 is not a leap year. | So 2025 is a leap year. | judge |
| “Sort [3, 1, 2] in Python.” + “I'll use the built-in sort.” | sorted([3, 1, 2]) returns [1, 2, 3]. | [3, 1, 2].sorted() returns [1, 2, 3]. | interpreter |
| “Rice for 4 is 300 g. How much for 6?” + “Per person: 300 ÷ 4 = 75 g.” | For 6 people: 6 × 75 = 450 g. | For 6 people: 300 + 6 = 306 g. | checker |
# step-level pairs: "prompt" holds the problem and the shared steps, already put through the # chat template; "chosen" and "rejected" are the two versions of the next step. # For agents, where every step is its own assistant turn, conversational rows work as well. {"prompt": "<|user|>A train ticket costs £12. A railcard takes a third off. How much do 5 tickets cost with the railcard?<|end|><|assistant|>Step 1: Taking a third off £12 removes £4.\nStep 2: Each ticket is then £8.\nStep 3:", "chosen": " For five tickets: 5 × £8 = £40.", "rejected": " For five tickets: 5 × £8 = £45.", "first_error_step": 3, "found_by": "checker"}
The last two fields are provenance for you; the trainer ignores them. With plain-text rows TRL appends an end-of-sequence token to chosen and rejected, so each pair also teaches the model to stop after this step. That is natural when each step is its own turn, as for agents; for one long answer, remove that final token from both completions in a custom collator.
The loss
Read it aloud: “Exactly the DPO loss. The prompt is the problem plus the first \(k-1\) steps, and the two answers are two versions of step \(k\): the correct one and the wrong one.” Point at a row of the table to light its term up in the formula.
one pair Inside \(\mathbb{E}\) is the loss of one pair, and a pair here is two versions of one step after one shared history. \(\mathbb{E}\) means “averaged over step pairs”, in practice over a batch. Station 7 follows one full training step, from the batch to the update.
Every symbol in the formula
| Symbol | What it means |
|---|---|
| The data: one step-level pair | |
| \(x\) | dataThe problem, for example the train-ticket question. |
| \(k\) | dataThe number of the first wrong step in the recorded solution: 3 in the example. It is found by a judge, a checker or roll-outs. |
| \(s_{1:k-1}\)“s one to k minus one” | dataThe steps before it, step 1 up to step \(k-1\): here steps 1 and 2. The colon means “from … to …”. |
| \(x,\,s_{1:k-1}\) | dataThe shared history: the problem and the steps before the first error. It is the prompt, read and never graded. |
| \(s^{+}_k\)“s plus k” | dataThe chosen step: a correct step \(k\), written for this exact history and checked (“5 × £8 = £40.”). The plus marks the good version. |
| \(s^{-}_k\)“s minus k” | dataThe rejected step: the step \(k\) that was actually written, the first wrong one (“5 × £8 = £45.”). \(k\) is found by a judge, a checker or roll-outs. |
| \(\mathbb{E}\big[\;\cdot\;\big]\) | mathThe average over all step-level pairs in the dataset; the subscript is left out to keep the formula short. |
| Models, scores and loss: as in DPO | |
| \(\pi_\theta(s_k\mid x, s_{1:k-1})\) | trainedThe probability that the model being trained writes step \(s_k\) after reading the problem and the earlier steps. It is the product of the step's token probabilities. |
| \(\pi_{\text{ref}}(s_k\mid x, s_{1:k-1})\) | frozenThe same probability under the frozen reference model. |
| \(\log\frac{\pi_\theta(\cdot)}{\pi_{\text{ref}}(\cdot)}\) | trainedThe step's score: how much more likely the trained model makes it than the reference does. One for the chosen step, one for the rejected step. |
| \(\beta\) | settingThe leash, as in DPO, usually 0.1. |
| \(\log\sigma\) | mathThe log of the sigmoid \(\sigma(u) = 1/(1+e^{-u})\), which turns the scaled gap into the probability that the model prefers the correct step. |
| \(\mathcal{L}_{\text{step}}(\theta)\) | resultThe DPO loss of #10, with steps as the answers. It starts at 0.69 and falls as the correct step pulls ahead. |
What you must supply: for each pair, the problem \(x\), the shared steps \(s_{1:k-1}\) (put into the prompt), a checked correct step \(s^+_k\) and the first wrong step \(s^-_k\), plus \(\beta\). Finding \(k\), the first error, is the expensive part; the training itself is ordinary DPO.
How the loss works, step by step
- Find the first error. A checker, a judge or roll-outs mark step \(k\) as the first wrong step of a recorded solution.
- Build the row. Prompt: the problem plus steps 1 to \(k-1\). Rejected: the wrong step. Chosen: a correct step \(k\) written for the same history. Everything after step \(k\) is thrown away.
- Run DPO. Four log-probabilities, of the two steps only, under the trained model and the reference; two scores; the gap; \(-\log\sigma(\beta \cdot \text{gap})\).
- Step. The gradient raises the correct step and lowers the wrong one. Because the history is in the prompt, the earlier correct steps are never pushed down.
Worked example: step 3 of the train-ticket solution, β = 0.1
The loss goes down only by making “£40” more likely than “£45” after this exact history, relative to the reference. With a whole-answer pair, step 4 and any other differences between the answers would also enter the gap and blur that lesson.
One training step on the GPU
A Step-DPO step is a DPO step whose rows are step pairs: the prompt holds the problem and the steps so far, and each answer is a single step. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Take one pair built at the first wrong step: the shared history, then a correct step of 6 tokens or a wrong step of 6 tokens.
- The history makes no slope. The problem and the earlier correct steps are read, never graded, so they are never pushed down. They get gradient through attention, because the step's tokens read them.
- c needs both steps first, as in #10. The steps after the error are not in the row at all. In a batch of B pairs, c is ÷ B.
- Not one after the other. The 12 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
Each row: the problem and steps 1 to k − 1 as the prompt, masked; then the correct step k in one sequence and the wrong step k in the other. Pairs from different problems, with different k, sit in the same batch.
2Forward pass
As in DPO, both models read all 2B sequences; each answer's log p is summed over the step's tokens only.
3Loss: from each pair to one number
Each step pair gives one gap and one loss \(-\log\sigma(\beta\cdot\text{gap})\); the batch averages them.
4Backward pass: the slope at every answer token
(\(y_t\) is 1 at the target token and 0 at every other word.) #10's slope on the step's tokens: \(c = \beta\sigma(-\beta\cdot\text{gap})/B\), then \(+c\,(p_t - y_t)\) on the correct step and \(-c\,(p_t - y_t)\) on the wrong one. The history has no slope of its own, so the earlier correct steps are never pushed down; they still receive gradient through attention, because the step's tokens read them.
5Update
Once.
On a real GPU
- Costs like DPO. The history is long and the step short, so most of the compute goes into the shared history, read once with each step.
- Building the data is the expensive part: finding the first wrong step can take many roll-outs, each written token by token.
The maths behind it
Credit: where the push lands
The DPO gradient raises every token of the chosen answer and lowers every token of the rejected one. With two whole answers that share nothing, the correct steps of the losing answer are pushed down too.
whole-answer pair step-level pair pushed up A1 A2 A3 A4 pushed up the fixed step 3 pushed down B1 B2 B3 B4 pushed down B3 correct steps pushed down: 2 (B1, B2) correct steps pushed down: 0 graded steps: 8 graded steps: 2A shared beginning cancels
By the chain rule, an answer's log-probability is the sum of its pieces' log-probabilities, each given what came before. If both answers start with the same steps, those steps contribute the same amount to both scores and drop out of the gap (\(x\) is left out to save space):
$$\hat r(s_{1:k-1}\,s^{+}_k) - \hat r(s_{1:k-1}\,s^{-}_k) = \underbrace{\hat r(s_{1:k-1}) - \hat r(s_{1:k-1})}_{=\,0} + \hat r(s^{+}_k\mid s_{1:k-1}) - \hat r(s^{-}_k\mid s_{1:k-1})$$\(\hat r(\cdot)\)“r hat”, the score trainedA piece of text's log-ratio, \(\log\pi_\theta - \log\pi_{\text{ref}}\): how much more likely the trained model makes it than the reference does. \(s_{1:k-1}\,s^{+}_k\) dataA whole answer: the shared steps 1 to \(k-1\) followed by the correct step \(k\). Writing pieces side by side means “this, then that”. \(s_{1:k-1}\,s^{-}_k\) dataThe same shared steps followed by the wrong step \(k\). \(\hat r(s_{1:k-1})\) trainedThe score of the shared steps alone. It appears once on each side, so the difference is 0. \(\hat r(s^{\pm}_k\mid s_{1:k-1})\) trainedThe score of step \(k\) alone, given the shared steps before it. These are the only terms left. \(\underbrace{\;\cdots\;}_{=\,0}\) mathA label under the brace: the part above it equals 0. Step-level DPO makes the shared beginning exact by construction and puts it in the prompt, so only the step after it counts.
Why the first wrong step
Cut before it and both sides would be the same correct step: no pair. Cut after it and the history already contains the mistake, so the pair can only teach a repair; the first error itself is never pushed down. The first wrong step is the only cut that targets the mistake directly. The lab's cut slider shows all three cases.
Fewer pairs, cleaner ones
A solution with an error gives one pair, at its first error; a solution with no error gives none. Step-level pairs are scarcer and costlier than whole-answer pairs, but each one carries a much cleaner signal. The Step-DPO paper reports gains on maths benchmarks from about ten thousand such pairs.
Watch out for
A late first error
If the judge names a step after the real first error, the history holds an uncaught mistake and the pair teaches the wrong lesson. Check the judge's first-error labels against people.
The chosen step must be right
An unverified fix that is itself wrong pushes the model toward a new mistake, exactly where it is most fragile.
Fixes in someone else's voice
A fix written by another model sits where the model puts little probability and teaches slowly. Resampling the step from the model itself and keeping a verified one works better.
Near-identical pairs can lower both sides
Two completions that differ by a single number are exactly the case where DPO most often lowers the chosen side's probability too (Pal et al., 2024). Watch the chosen step's log-probability; an SFT term on the chosen step helps.
Only some tasks have steps
Maths, code and agent actions split cleanly into steps. Free prose does not, and a cut in the middle of a paragraph has no clear meaning.
Whole sessions as the unit
Trajectory-level preference ETO-style · exploration-based trajectory optimisation
For an agent that works through many turns, compare whole sessions: a successful one as chosen, a failed one on the same task as rejected. Only the outcome needs judging. Every action in the winning session is pushed up and every action in the losing one down, while the tool and environment outputs in between are read and never graded.
Two agent sessions: every action pushed, every observation left ungraded.
- Changes vs DPO
- The answer is a whole session; only the agent's own turns are graded
- Its answers
- Q3 changed: the credit is spread over the whole session
- Cost
- ●●●●● many sessions to run, long sequences to train on
- Library
- Build it: whole sessions as chosen / rejected →
DPOTrainer
The idea in plain words
An agent's session is a trajectory: action, observation, action, observation, until the task succeeds or fails. Often the only reliable signal is at the end: the item was bought or not, the tests pass or not. Judging every step is expensive and uncertain.
Trajectory-level preference uses that end signal directly. For the same task, take a session that succeeded and one that failed, and use them as the chosen and the rejected answer in the DPO loss. A session's log-probability is the sum over the agent's turns. The observations, written by the environment, are part of the context: read, never graded.
ETO (Song et al., 2024) builds this into a loop: start from an SFT agent, let it explore the training tasks, pair its failures with the expert's sessions on the same tasks, train with DPO, and explore again with the improved agent. Each round finds new failures to learn from. That loop is a step toward the online methods of the later families.
See it work
Credit over a whole session
Two sessions of a shopping agent on the same task: one bought the right item, one did not. See which turns the trajectory pair pushes up or down and which ones are only read, then compare with a step-level pair. Below, see how often a task gives a pair at all.
Where the training signal comes from
Three decisions: where the successful session comes from, who decides success, and which sessions you pair.
A small example
Chosen: the session that succeeded
search[blue water bottle]click[Blue steel bottle £12]buyRejected: the session that failed
search[water bottle]click[Blue glass bottle £18]buyWhat a training batch looks like
| Task | Chosen session · actions | Rejected session · actions | Rewards |
|---|---|---|---|
| Buy a blue water bottle under £15. | search → click the £12 blue bottle → buy | search → click the £18 blue bottle → buy | 1 vs 0 |
| Book the earliest train to Leeds on Friday. | search Friday → sort by time → pick 06:10 → book | search Friday → pick the first listed, 08:40 → book | 1 vs 0 |
| Find the opening hours of the city library. | search → open the library's own page → read the hours | search → open a 2019 news article → answer from it | 1 vs 0 |
# trajectory pairs: the task is the prompt; chosen and rejected are whole sessions. # Only the assistant turns may be graded: the tool turns were written by the environment. {"prompt": [{"role": "user", "content": "Buy a blue water bottle under £15."}], "chosen": [{"role": "assistant", "content": "search[blue water bottle]"}, {"role": "tool", "content": "Blue steel bottle £12 · Red bottle £9 · Blue glass bottle £18"}, {"role": "assistant", "content": "click[Blue steel bottle £12]"}, {"role": "tool", "content": "Blue · £12 · in stock"}, {"role": "assistant", "content": "buy"}], "rejected": [{"role": "assistant", "content": "search[water bottle]"}, {"role": "tool", "content": "Red bottle £9 · Blue glass bottle £18 · Green bottle £11"}, {"role": "assistant", "content": "click[Blue glass bottle £18]"}, {"role": "tool", "content": "Blue · £18 · in stock"}, {"role": "assistant", "content": "buy"}], "task_id": "shop-0412", "rewards": [1, 0]}
TRL's DPOTrainer (checked in v1.15 and v1.16) scores every token after the prompt, tool messages included, and has no option to skip them. To grade only the agent's turns you need a custom collator that zeroes completion_mask on the tool turns. ETO itself masks the observations.
The loss
Read it aloud: “The DPO loss, with each answer replaced by a whole session. A session's score is the sum of its actions' scores, each action taken given everything before it. The observations sit in the history and have no term of their own.” The second line spells out that history. Point at a row of the table to light its term up.
one pair Inside \(\mathbb{E}\) is the loss of one pair of whole sessions. Each session's score adds up its actions; the observations are masked. \(\mathbb{E}\) means “averaged over session pairs”, in practice over a batch. Station 7 follows one full training step, from the batch to the update.
Every symbol · both lines of the formula
| Symbol | What it means |
|---|---|
| A session, piece by piece | |
| \(u\) | dataThe task, the user's request that opens the session: “Buy a blue water bottle under £15.” It plays the part of the prompt \(x\). |
| \(t\) | mathThe turn number: \(t = 1\) is the agent's first action. |
| \(a_t\)“a sub t”, an action | dataThe agent's action at turn \(t\): a whole assistant turn, such as search[blue water bottle]. Its probability is the product of its tokens' probabilities. |
| \(o_t\)“o sub t”, an observation | dataWhat the environment answered after action \(t\), such as the search results. Written by the environment, not by the model. |
| \(h_t\)“h sub t”, the history | dataThe history before action \(t\): the task \(u\), then every earlier action \(a\) and observation \(o\). Observations come from the environment, so they appear only here, as context. |
| \(\big(u,\; a_1, o_1,\; \dots,\; a_{t-1}, o_{t-1}\big)\) | mathA list in order; the dots \(\dots\) stand for the turns in between. \(h_1\) is just \((u)\); \(h_2 = (u, a_1, o_1)\); and so on. |
| \(a^{+}_t,\ h^{+}_t\)“a plus”, “h plus” | dataThe actions and histories of the successful session \(\tau^{+}\) (“tau plus”), the chosen side. |
| \(a^{-}_t,\ h^{-}_t\)“a minus”, “h minus” | dataThe same for the failed session \(\tau^{-}\) on the same task, the rejected side. The two sessions can have different lengths. |
| \(\mathbb{E}\big[\;\cdot\;\big]\) | mathThe average over all session pairs in the dataset. |
| Scores and loss: as in DPO | |
| \(\pi_\theta(a_t\mid h_t)\) | trainedThe probability that the agent being trained takes action \(a_t\) after reading history \(h_t\). |
| \(\pi_{\text{ref}}(a_t\mid h_t)\) | frozenThe same probability under the frozen reference (in ETO, the agent from the previous round). |
| \(\sum_t\) | mathAdd up over every action of the session. |
| \(\sum_t \log\frac{\pi_\theta(a^{+}_t\mid h^{+}_t)}{\pi_{\text{ref}}(a^{+}_t\mid h^{+}_t)}\) | trainedThe successful session's score: the sum over its actions \(a^{+}_t\). Each action is a whole assistant turn, itself a sum over tokens. |
| \(\sum_t \log\frac{\pi_\theta(a^{-}_t\mid h^{-}_t)}{\pi_{\text{ref}}(a^{-}_t\mid h^{-}_t)}\) | trainedThe failed session's score, summed the same way. |
| \(\beta\) | settingThe leash you choose, as in DPO, usually 0.1. |
| \(\log\sigma\) | mathThe log of the sigmoid \(\sigma(u) = 1/(1+e^{-u})\), which turns the scaled gap into the probability of preferring the successful session. |
| \(\mathcal{L}_{\text{traj}}(\theta)\) | resultDPO over sessions. Each pair is a success \(\tau^{+}\) against a failure \(\tau^{-}\) on the same task. |
What you must supply: pairs of whole sessions on the same task, one that succeeded and one that failed (the environment's reward decides which), plus \(\beta\). Each session is a list of messages in which the agent's turns are graded and the tool or environment turns are masked out.
How the loss works, step by step
- Collect two sessions on one task. Run the agent (or take an expert session) until one session succeeds and another fails.
- Score every action. For each agent turn, compare the trained model's log-probability with the reference's, given the history up to that turn.
- Skip the observations. Environment turns are context only. They get no term, because neither model wrote them.
- Add up per session. The successful session's score minus the failed one's is the gap; then \(-\log\sigma(\beta \cdot \text{gap})\), as in DPO.
- Step. Every action of the successful session is pushed up and every action of the failed one down, all with the same pair-level weight, whether or not that action decided the outcome.
Worked example: the two bottle sessions above, β = 0.1
The loss goes down when the agent's successful actions become more likely and its failing actions less likely, relative to the reference. Note that buy appears in both sessions and is pushed up in one and down in the other; across many pairs such shared, innocent actions roughly cancel, while the decisive ones (searching for “blue”, picking the £12 bottle) add up.
One training step on the GPU
A trajectory step is a DPO step on whole sessions: very long sequences in which only the agent's own turns are graded. Plain SFT's station 7 walks through the full step with every number; this one shows what changes.
One step, slope by slope
Take one pair of whole sessions on the same task. Say each has 4 agent turns of 10 tokens, so 40 action tokens, with the environment's observations in between.
- Every action of the failed session is lowered, decisive or not: c is one number for the whole session.
- Observations make no slope. The environment wrote them; they are read as history and get gradient through attention.
- Long sessions, still one move. Often only one or two pairs fit on a GPU, so trainers use gradient accumulation: the micro-batches run one after the other, but they only add into each weight's gradient, and the weights move once per batch.
- Not one after the other. The 80 slopes are measured on the same old weights and added into each weight; the weights move once, as in #1.
1The batch
Each session is written out as one sequence: the task, then the agent's turns and the observations in order. The mask is 1 on the agent's tokens and 0 on the task and the observations.
2Forward pass
Both models read every session; each session's score is the sum of log p over the agent's tokens only.
3Loss: from each pair to one number
Each session pair gives one gap and one loss; the batch averages them.
4Backward pass: the slope at every answer token
(\(y_t\) is 1 at the target token and 0 at every other word.) #10's slope: \(c = \beta\sigma(-\beta\cdot\text{gap})/B\) per pair; \(+c\,(p_t - y_t)\) at every agent token of the successful session and \(-c\,(p_t - y_t)\) at every agent token of the failed one, decisive or not. Observations have no slope of their own, but receive gradient through attention.
5Update
Once.
On a real GPU
- Activations dominate memory. Sessions run to thousands of tokens, so trainers fit one or two pairs per GPU and use gradient accumulation and activation checkpointing.
- Like DPO: four sequence reads per pair in two forward calls, only the trained model's backpropagated; the reference can be computed once before training.
- Collecting the sessions, by running the agent in its environment, happens before training and is usually the slowest part.
The maths behind it
Why the observations drop out
The probability of a whole session multiplies the agent's choices and the environment's responses:
$$P(\tau) = \prod_{t}\pi(a_t\mid h_t)\;P_{\text{env}}(o_t\mid h_t, a_t)$$\(\tau\)“tau”, a trajectory dataA whole session: the task, then every action and observation in order. \(P(\tau)\) resultThe probability of that exact session happening, agent and environment together. \(\prod_t\) mathMultiply over every turn \(t\) of the session. \(\pi(a_t\mid h_t)\) trainedThe agent's part: the probability of choosing action \(a_t\) given the history \(h_t\). \(P_{\text{env}}(o_t\mid h_t, a_t)\)“P env” frozenThe environment's part: the probability that it answers \(o_t\) after that action. The website, the shop or the tool, never the model. In the score, the trained model and the reference face the same environment, so its factors are identical on the top and the bottom of the ratio and cancel:
$$\log\frac{P_\theta(\tau)}{P_{\text{ref}}(\tau)} = \sum_t \log\frac{\pi_\theta(a_t\mid h_t)}{\pi_{\text{ref}}(a_t\mid h_t)} + \underbrace{\sum_t \log\frac{P_{\text{env}}(o_t\mid h_t,a_t)}{P_{\text{env}}(o_t\mid h_t,a_t)}}_{=\,0}$$\(P_\theta(\tau)\), \(P_{\text{ref}}(\tau)\) trainedThe probability of the session with the trained agent, and with the frozen reference agent, in the same environment. \(\log\frac{P_\theta(\tau)}{P_{\text{ref}}(\tau)}\) trainedThe session's score. The log of a product is a sum of logs, so it splits into one sum for the actions and one for the observations. \(\log\frac{\pi_\theta(a_t\mid h_t)}{\pi_{\text{ref}}(a_t\mid h_t)}\) trainedOne action's score: the only terms that survive. \(\log\frac{P_{\text{env}}}{P_{\text{env}}}\) mathThe same number on top and bottom: a ratio of 1, whose log is 0. \(\underbrace{\;\cdots\;}_{=\,0}\) mathA label under the brace: the whole sum above it is 0. So the right score has no observation terms at all. A trainer that also runs the observation tokens through the language model adds terms \(\log\frac{\pi_\theta(o_t\mid\cdot)}{\pi_{\text{ref}}(o_t\mid\cdot)}\), the models' guesses of text the environment wrote. Those do not cancel and do not belong in the score, which is why the observations are masked.
Credit is spread thin
Every action in a session gets the same pair-level weight. A long losing session with one decisive mistake spends most of its push on actions that decided nothing.
losing session: 20 actions, 1 decisive mistake actions pushed down: 20 of which the decisive one: 1 the other 19 are punished for being in a failed session; across many pairs this averages outWhich tasks give a pair
With \(N\) attempts per task at success rate \(s\), a task gives a self-made pair only if it has at least one success and at least one failure:
$$P(\text{the task gives a pair}) = 1 - s^{N} - (1-s)^{N}$$\(s\) trainedThe agent's success rate on this task: the chance that one attempt succeeds. \(N\) settingHow many attempts you run per task. \(s^{N}\) mathThe chance that all \(N\) attempts succeed: no failure to pair with. \((1-s)^{N}\) mathThe chance that all \(N\) attempts fail: no success to pair with. \(1 - \cdots - \cdots\) resultWhatever is left: the chance of at least one success and at least one failure, that is, of getting a pair. s = 0.10, N = 4: 1 − 0.0001 − 0.6561 = 0.34 ← most tasks are never solved: no pair s = 0.50, N = 4: 1 − 0.0625 − 0.0625 = 0.88 s = 0.95, N = 4: 1 − 0.8145 − 0.0000 = 0.19 ← most tasks never fail with an expert session for every task: 1 − sᴺ (any one failure is enough)The ETO loop
round 0 SFT on the expert sessions → agent π₀ round k explore: πₖ runs every training task pair: each failure with the expert session on that task (pairs with equal rewards are dropped) train: DPO on the new pairs, reference = πₖ, no SFT term → agent πₖ₊₁Each round's agent fails in new ways, so each round produces new pairs about its own current mistakes. Training on fresh failures of the current model is the idea that the iterative and online families take further.
Watch out for
Good actions punished
The losing session's sensible turns are pushed down along with its mistake. With many pairs this averages out; with few it can teach odd avoidances. When you can find the decisive step, step-level pairs (#14) are sharper.
Mask the environment
The observations must not be graded, and TRL's
DPOTrainergrades them: it scores every token after the prompt,toolmessages included. Zero theircompletion_maskin a custom collator.Long sessions, short windows
Sessions run to many thousands of tokens. With
DPOConfig's defaultmax_lengthof 1,024, only the first turns of each session would be compared, the decisive later turns would be cut away, and a row whose task prompt alone fills the window would be dropped. Raise the limit, and budget the memory: every pair is two long sequences, each run through two models.Length shortcuts
Failures often run long, as the agent wanders, and successes short. The model may learn to stop early. Pair sessions of similar length, or check what the pairs reward.
Same task only
Pairing a success on an easy task with a failure on a hard one teaches task difficulty, not behaviour.
Putting the family together
How they stack
In practice you pick one preference method, not six, and run it after SFT; ORPO is the exception, because it replaces both stages. Step-level and session pairs (#14, #15) are extra data for that same DPO stage. The choice follows from the data you have.
Choose by the data you have
- Pairs of whole answers: a chosen and a rejected answer per prompt#10 DPO
- …and the answers drift longer, the gap overfits, or there is no memory for a reference#13 SimPO, IPO
- Single answers labelled good or bad, or a judge's per-step labels#11 KTO
- Pairs, a base model, and one training run instead of two#12 ORPO
- Answers made of steps, and a way to find the first wrong one#14 Step-level
- Agent sessions where only the outcome is known#15 Trajectory-level
A typical pipeline
- SFT on good demonstrations, from the SFT familyA SFT family
- Sample several answers per prompt from the SFT model, then judge them: pairs for DPO, single labels for KTO
- One preference stage, with the loss that fits the data#10 DPO#11 KTO#13 Variants
- Optional, for multi-step tasks: add pairs cut at the first wrong step#14 Step-level
- Optional, for agents: session pairs, and the loop of explore, pair and train#15 Trajectory
- Evaluate: the win-rate against the SFT model, answer length, and regression checks on general skills
Instead of steps 1 to 3, ORPO (#12) does SFT and the preference stage in one run from the base model. It needs ready-made pairs, such as an existing preference set or a strong model against a weak one, because there is no SFT model to sample from yet.
The family at a glance
| # | Technique | One example holds | Reference model | What changes vs DPO | Cost |
|---|---|---|---|---|---|
| 10 | DPO | A pair of whole answers | Yes | The baseline | ●● |
| 11 | KTO | One answer and a good or bad label | Yes | No pairs needed | ●● |
| 12 | ORPO | A pair | No | SFT and an odds penalty in one stage | ●● |
| 13 | SimPO, IPO, variants | A pair | IPO yes, SimPO no | The loss curve, the length, the margin, or no reference | ●● |
| 14 | Step-level DPO | Two next steps after a shared history | Yes | Where the pair is cut | ●●● |
| 15 | Trajectory-level | Two whole sessions on the same task | Yes | The unit is a session | ●●● |
The ceiling, and what comes next
Everything on this page learns from a fixed set of comparisons made in advance, and two limits follow. The labeller is the ceiling: the model learns the judge's or the raters' taste, blind spots included. And the data goes stale: the pairs were drawn from an earlier model, and as training moves the model away from it, they describe its old mistakes rather than its new ones.
The next families address both. Reward and value models turn the labels into a scorer that can grade answers nobody has labelled yet. Iterative methods sample, judge and train again in rounds, as ETO does. Online reinforcement learning scores the model's own answers during training.