RLHF, reward models, PPO & GRPO, direct alignment, reasoning, and the evaluation that gates it all.
Your last stage ended on a bridge: “the model follows instructions — making it prefer good answers is next.” Here is what that sentence actually costs, mechanically. Instruction tuning showed your Gemma what a good analyst output looks like. It never showed it which of two plausible outputs is better. That comparison — and everything built on top of it — is this stage.
Every technique in this stage is one of three verbs. Not three eras — three things you can teach a model to do. Keep this ladder in your head; every method below gets placed on a rung.
Three verbs, bottom to top. The rung you can climb to is decided by your data, not your ambition.
People argue about “DPO vs PPO” as if it were the interesting question. It usually isn't. The interesting question is which verb your problem needs — because that decides what data you have to go collect, which is the expensive part. Algorithms are a weekend; a preference-data pipeline is a quarter.
This is the diagram the whole stage hangs off. The top path is classical RLHF as InstructGPT defined it. The arc underneath is DPO: the discovery that you can solve the same objective, on the same data, without ever building the reward model or running the RL loop.
Hover or click any box. The dashed crimson arc is the route your single L4 can actually run end-to-end.
There is one failure mode underneath every technique here, and it has a name. Charles Goodhart's original phrasing was that any observed statistical regularity collapses once pressure is put on it for control purposes; the popular compression is “when a measure becomes a target, it ceases to be a good measure.”
In post-training the measure is your reward — a reward model, an LLM judge, or a verifier — and the target is your policy. Optimise hard enough and the policy will find whatever the measure rewards that the measure did not mean to reward.
Reward models reliably score longer answers higher, so policies get verbose for free points. Length-controlled win rates and length-normalised losses exist specifically because of this.
→ S2, S4Agreeing with the user's stated belief scores well with human and AI raters alike, even when it makes the answer less true. It is a property of the raters, not of the algorithm.
→ S2, S4Repeating rare tokens, formatting tics, structural quirks that happen to inflate a scalar head. Nothing about these is semantic; they are artefacts of RM training.
→ S3Every session in this stage ends up describing the same defence in different clothes: a KL penalty against a frozen reference model. In PPO it is subtracted from the per-token reward. In GRPO it is a separate loss term. In DPO it is baked into the β coefficient. Plus the unglamorous defences that matter just as much: early stopping, better data, and honest evaluation.
Most practitioners consume post-trained models. Deep literacy plus one real end-to-end run is a stronger position than shallow familiarity with all of it.
the source material is a leading alignment researcher's the RLHF literature (living edition). the section numbering below is the canonical map:
| Session | the source material section | Nuggets pulled in |
|---|---|---|
| S1 | 1 Overview · 3 Training Overview · 4 Instruction Fine-tuning (bridge-skim only) | — |
| S2 | 5 Reward Modeling | 10 Nature of Preferences |
| S3 | 6 Reinforcement Learning | 15 Regularization · 16 Over Optimization |
| S4 | 8 Direct Alignment Algorithms · 11 Preference Data | 12 Synthetic Data · 9 Rejection Sampling |
| S5 | the source material ch. 10 (Liu) as lab skeleton, modernised | the source material 8.3 implementation notes |
| S6 | 7 Reasoning & Inference-Time Scaling | 9.3 Best-of-N |
| S7 | 17 Evaluation | 16 Over Optimization |
the literature also maintains the material as a continuously-updated site at rlhfbook.com, and it has already moved past your PDF in two checkable ways:
Where a source claim has aged, the tab says so explicitly and gives the current replacement.
Place any technique on the imitate→prefer→achieve ladder and explain its mechanism with intuition-first math; defend the DPO-vs-RLHF-vs-variants choice for a given data budget; build a judge-generated preference set with known biases mitigated; run a real DPO pass and read its curves; price reasoning honestly; and gate a release on win rate.
Your SFT'd Gemma will answer. It will answer in JSON, with the right keys, because you taught it the shape. What it will not do is reliably pick the better of two answers it could equally well produce — because during training it never saw a worse one.
That gap is not academic. Consider a prompt your fintech pipeline actually sees:
Same schema, same shape, wildly different usefulness. SFT is blind to the difference.
Every term below is used constantly for the rest of the stage. Nothing later assumes you already know one of these.
The model you are training, written π_θ. “Policy” is borrowed from reinforcement learning: given a state, it outputs a distribution over actions. For a language model the state is the prompt (plus tokens so far) and the action is the next token.
A frozen copy of where you started, written π_ref. Almost always the SFT checkpoint. It exists for one reason: to measure how far the policy has drifted, so you can penalise drifting too far.
A learned model that maps (prompt, completion) to one scalar. It stands in for human judgement. Because it is learned from finite data, it is a proxy — the source of every over-optimisation problem in this stage.
A completion sampled from the current policy during training. Online methods (PPO, GRPO) generate rollouts every step; offline methods (DPO) never do, which is exactly why DPO is cheap.
A number measuring how different two probability distributions are. Zero when identical, growing as they diverge. Here it measures policy-vs-reference drift, and gets added to the loss as a leash.
On-policy data was generated by the model currently being trained (or a close relative). Off-policy data came from somewhere else. In preference tuning this distinction turns out to matter more than the algorithm choice.
the literature's framing, and the one worth internalising: post-training is a multi-stage processis the three-verb ladder from the stage map, stated in the material's own vocabulary.
| Family | What it teaches | Unit of feedback | Verb |
|---|---|---|---|
| IFT / SFT instruction / supervised fine-tuning | Formatting and the base of instruction-following. Largely about learning features in language. | per token | imitate |
| PreFT preference fine-tuning | Alignment to human preferences, plus a smaller capability bump. Largely about style and subtle preferences that are hard to quantify. | per response, contrastive | prefer |
| RLVR RL with verifiable rewards | Performance in domains where correctness can be checked by a program. The newest family, and the reasoning-model engine. | per response, verified | achieve |
SFT trains the model to predict a specific next token when the preceding text resembles something it has seen. RLHF tunes at the response level, and it says “this kind of answer is better” rather than “produce exactly this string.” It also supplies negative feedback — what to avoid — through a contrastive loss. That is the mechanical reason preference tuning transfers across domains where instruction tuning tends to memorise.
A useful mental model for why so much can be gained on top of a fixed base model: post-training is mostly extracting capabilities the base model already has, not installing new ones. The base model can already write a calibrated risk assessment somewhere in its distribution; post-training raises the probability that it does so reliably, on the first sample.
This is why the honest answer to “can post-training fix X?” is usually a question about whether X exists in the base distribution at all.
Imagine a junior analyst who has read every report your firm ever published (pretraining) and been coached on your house format (SFT). You now want them to get better, so you hire a reviewer who scores their drafts.
Two things immediately go wrong. First, the reviewer is not your actual customer — they are a stand-in whose taste you inferred from a few thousand comparisons. Second, the analyst is clever, and if you let them optimise the reviewer's score without limit they will start writing whatever the reviewer likes rather than whatever is true.
So you add a leash: “improve the score, but do not become unrecognisable compared to who you were on day one.” That leash is the KL penalty, and the person you were on day one is the reference policy. The entire optimisation is that sentence.
RLHF borrows RL's vocabulary but changes three things, and if you do not notice the changes the equations look stranger than they are.
| Reference text RL | RLHF for language models | Consequence |
|---|---|---|
| Reward is a fixed function baked into the environment | Reward is a learned model you train and control | Enormous flexibility — and the proxy problem |
| Actions change the state; dynamics carry you forward | No state transitions. Initial state = a prompt from a dataset; the action = the whole completion | The discount factor and horizon mostly vanish |
| Reward arrives per step | Response-level reward — one score for a whole sequence of tokens (a “bandit” problem) | Credit assignment across tokens is the hard part |
With those simplifications, the objective collapses to something you can hold in one line: sample a prompt, sample a completion, maximise the reward — minus a penalty for drifting from the reference.
Remember this equation. Session 3 solves it with reinforcement learning. Session 4 solves the same equation in closed form and skips the reinforcement learning entirely. Everything in between is bookkeeping.
the literature is explicit that effective RLHF requires a strong starting point — it cannot rescue a weak base or a bad SFT stage, and needs to be seen inside the broader post-training picture rather than as a fix-all.
You have already run QLoRA SFT with correctly-rendered Gemma chat templates and TRL's SFTTrainer. §4 is a skim. Here are the four things in it you probably have not internalised, because they only start to matter once a second training stage exists.
Loss is computed only on completion tokens; prompt tokens are masked out. You already do this. What matters now: the same rule holds for preference tuning and RL. When you compute a DPO log-probability, you are summing log-probs over completion tokens only — a bug here silently ruins the loss because prompt tokens are identical between chosen and rejected and would cancel unevenly with padding.
Two conventions. Final-turn only: loss on the last assistant turn, everything earlier masked; long conversations get unrolled into several training examples. Mask user turns only: every assistant turn contributes to the loss. The second trains directly on intermediate assistant replies. For preference data the same question resurfaces — preference is usually collected only on the final turn, and all earlier turns are masked.
OLMo 2 used batch sizes of 1024 (7B) and 2048 (13B) sequences for pretraining, but 256 sequences for post-training. Smaller batches mean these jobs cannot be sharded across as many devices — which is precisely why a single L4 is a plausible machine for a real preference run and not for pretraining. It also means running multiple seeds matters, because small-batch runs are noisier.
the literature's blunt version: if multiple training stages follow instruction tuning, the model can recover from some noise in the process — optimising the overall pipeline matters more than perfecting each individual stage. Practically: do not spend another month polishing your SFT set before starting DPO. The DPO stage will absorb some of that noise, and you will learn more from the round-trip.
Scale reference: roughly 1M prompts is enough to build a model capable of excellent post-training; beyond that, returns diminish quickly. The best prompts are the ones drawn from your downstream task distribution — which, for you, means your own pipeline traffic.
Step through the three recipes that define the eras. Notice what stays constant: SFT is always first, and evaluation always gates the end.
The recipe that produced ChatGPT and defined the field's mental model for two years.
Note the ratio: 10× more preference data than SFT data. Preference data was the expensive, decisive artefact from day one.
The fully-open recipe that showed what modern multi-stage post-training looks like.
Same recipe later applied to Llama 3.1, OLMo 2 and SmolLM. The 100× jump in SFT data and 10× in preference data between eras is the real story.
Reasoning models did not add a stage so much as change which stage is the centre of gravity.
Mirrored by the larger Qwen 3 models and Xiaomi's MiMo. Step 4 is the point worth remembering: even reasoning models come back to preference tuning at the end. RLVR does not replace RLHF; it sits on top of it.
Ai2's Olmo 3 (7B and 32B) is the most useful reference you have, because every stage is released — data, checkpoints, logs. Its post-training recipe is a three-stage flow: build an excellent SFT set, then use DPO as a highly iterable, cheap and stable preference method “despite its critics”, then finish with scaled-up RLVR.
Two details worth carrying into your own run:
This is the shape your own capstone will take, one order of magnitude smaller.
Before any GPU cost, the decision that dominates your budget is who labels the pairs. the literature's current figures (as of 2026): a single piece of human preference data costs on the order of $1 or more per comparison, and can exceed $10 per prompt for specialist domains; AI feedback from a frontier model costs under $0.01.
Your fintech pipeline should use E first, then D or C on what E cannot decide. Programmatic checks are free and unbiased — use them to settle every pair they can settle, and spend judge tokens only on the genuinely ambiguous ones. This also caps the damage from judge bias, because the judge never gets to overrule a hard rule. S4 builds the tree for this properly.
You will see kl logged in every RL run and implicitly controlled by β in every DPO run. Here is what the number physically means, on three tokens.
Hugging Face released TRL v1.0 on 31 March 2026, moving the library from a research repo to a stabilised post-training stack: a unified CLI, one config system, and a clear stable-vs-experimental split. It has shipped roughly monthly since — v1.4 (9 May), v1.5 (25 May), and v1.8 (9 July 2026), which graduated KTOTrainer out of experimental into the top-level package.
There is no longer one answer. TRL for single-node and PEFT work; verl (HybridFlow) and OpenRLHF for Ray-based multi-node RL; NVIDIA NeMo-RL for Megatron-native service-oriented training; AReaL and slime for asynchronous rollouts. NVIDIA open-sourced Molt on 22 July 2026 — a ~8,600-line PyTorch-native agentic RL framework that still scales to trillion-parameter MoE.
Career-relevant: NeMo-RL and Molt are the NVIDIA-stack entries.Both flagship open post-training efforts converged on the same shape. Olmo 3 runs SFT → DPO → RLVR and publishes every stage. NVIDIA Nemotron 3 Nano runs SFT → multi-environment RLVR → RLHF, using synchronous GRPO on NeMo-RL + NeMo Gym with vLLM rollouts, 128 prompts per step and 16 generations per prompt.
Your capstone is a scaled-down version of exactly this shape.No training code yet — this session's lab is an inventory. You already own more of this pipeline than you probably realise.
| Pipeline element | What you already have | Gap for this stage |
|---|---|---|
| Policy π_θ | QLoRA-SFT'd Gemma E4B fintech adapter — strict-JSON analyst outputs | none — this is the thing being improved |
| Reference π_ref | The same adapter, frozen. You do not need a second copy on disk. | none — S5 shows the adapter-swap trick |
| Prompt distribution 𝒟 | Real fintech pipeline traffic — the ideal distribution, matched to production by construction | dedupe + decontaminate against the golden set |
| Candidate generator | Your vLLM endpoint. Sample n>1 per prompt at temperature > 0. | a sampling script; prefix caching makes this cheap |
| Preference labeller | Programmatic format checks + calibrated LLM judge with position/verbosity/self-preference mitigations from your eval stage | wire it to emit chosen/rejected instead of scores |
| Trainer | TRL, known from SFTTrainer | DPOTrainer + DPOConfig — S5 |
| Eval gate | Golden dataset, statistical sizing, online eval in Langfuse | reframe accuracy metrics as a win rate — S7 |
| Job execution | K8s + Kueue on spot GPUs, or Modal (your gemma_modal.py pattern) | choose one — S5 argues it |
| Serving | vLLM multi-LoRA on L4, FP8, prefix caching, Prometheus + Langfuse | add the DPO adapter alongside the SFT one |
Look at that table again. The only genuinely missing artefact is the preference dataset. Everything else you built in earlier stages. This is the normal state of affairs in post-training and it is why S4 spends more time on data sourcing than on algorithms.
Pull 200 prompts from your pipeline's last week of traffic. For each, sample two completions from your SFT adapter at temperature=0.8. Do not label them yet. Just read twenty pairs yourself and write down, in plain English, the rule you were using to decide which was better.
If you can write that rule as code, you are on rung 3 and should be reading S6. If you cannot — and for a fintech analyst output you almost certainly cannot write it completely — you have just proven to yourself that you need rung 2. That is the whole point of the exercise.
The objective needs a reward r(x,y), and nobody hands you one — so the next session builds it, turning pairwise “A beat B” judgements into a single scalar score via the Bradley-Terry model.
That model is also the hinge of the whole stage: understand Bradley-Terry once, and you have simultaneously understood reward models, DPO's implicit reward, and how arena leaderboards rank models.
Write down, as a differentiable function of tokens, what makes a compliance flag useful. You can't. Nobody can. That impossibility is the founding fact of RLHF, and the reward model is the workaround.
The trick is a swap that sounds too easy to work: it is much harder for a person to write a good answer than to recognise one, and harder still to write a rule than to point at the better of two options. So we stop asking for rules and start collecting comparisons — then fit a model that turns comparisons into a number.
Three boxes, one idea: comparisons are cheap, rules are impossible, and Bradley-Terry converts one into the other.
Everything in sessions 2 through 5 is built on this record. It is worth staring at, because its shape constrains everything downstream.
one row of a preference dataset{"prompt": "Flag compliance risk in this excerpt: …",
"chosen": "{\"risk_score\":0.78,\"evidence\":\"we expect margins…\"}",
"rejected": "{\"risk_score\":0.5,\"evidence\":\"\"}"}
Three fields. Note what is not there: no score, no reason, no confidence. The magnitude of the preference has been thrown away — the standard practice is to binarise along the preference direction, reducing a rich rating into a bare chosen/rejected relation. Section 5.4 of the material covers attempts to keep that magnitude; none has become standard practice.
x = the prompt y = a completion y_c or y_w = the chosen (winning) completion y_r or y_l = the rejected (losing) one. The relation y_c ≻ y_r | x reads “y_c is preferred to y_r given x”.
Two chess players sit down. You cannot observe their skill directly; you only observe who wins. But if you watch enough games between enough players, you can infer a single number per player such that the differences between those numbers predict who wins. That is Elo, and Elo is Bradley-Terry.
Swap players for completions. Each completion has a hidden “strength” — how much a rater would like it. You never see the strength, only the outcome of comparisons. Bradley-Terry says: the probability that A beats B depends only on the gap between their strengths, squashed through a sigmoid.
Two consequences fall straight out, and both matter later:
Now make the scores come from a network. Give it the prompt and a completion, and read one number off the end: r_θ(x, y). The probability our model assigns to the observed preference is σ(r_θ(x,y_c) − r_θ(x,y_r)). Maximise the log-likelihood of every observed comparison — equivalently, minimise its negative:
In code the loss is one line — genuinely. This is the mechanism, so it belongs here rather than in the lab:
rewards_chosen = model(**inputs_chosen) # (batch,) rewards_rejected = model(**inputs_rejected) # (batch,) loss = -F.logsigmoid(rewards_chosen - rewards_rejected).mean
Architecture. The standard implementation is the AutoModelForSequenceClassification abstraction: take a causal LM, append a small linear head that maps the final hidden state to a single logit. The score is read at the EOS token — the position where the model has seen the entire completion. Everything else about the network is unchanged.
Score at EOS, not at truncation. A known RLHF failure is completions being cut off by a hard length cap; the reward model then scores an unfinished string, which is far out of its training distribution, and returns unpredictable numbers. The fix is to score only on the EOS token and separately penalise over-long generations.
Train for one epoch. The most common practice for reward models is a single pass over the data — they overfit fast, and an overfit RM is an RM that has learned annotator quirks rather than preferences.
Reward modelling is, by its own framing, a relatively under-explored area: the loss has been modified many times and the modifications have not solidified into a single best practice. Three attempts worth knowing:
If annotators give Likert scores (say 1–5), you know the chosen answer scored 5 and the rejected scored 2, so the margin is 3. Llama 2 added that margin inside the sigmoid: −log σ(r_c − r_r − m(r)), forcing a bigger gap for pairs the annotators felt strongly about.
What happened: Llama 3 removed it. The team observed diminishing improvements once the data scaled. A recurring pattern in this field — clever loss modifications that get outrun by more data.
If one prompt has 4 completions you get 6 pairs from it, and a prompt that generated many comparisons will dominate the gradient. InstructGPT weighted the loss per comparison per prompt; at the implementation level you get this almost free by putting all pairs from the same prompt in the same batch. Not doing so caused overfitting to prompts.
Directly relevant to you: if you sample n=4 candidates per fintech prompt and pair them exhaustively, batch by prompt.
Instead of pairs, ask for a full ranking of K completions and fit the Plackett-Luce model, which generalises Bradley-Terry (and reduces to it exactly when K=2). Used by the Starling 7B/34B models.
Once trained, these are used identically to any other reward model. Uncommon in open tooling.
This is the single most confusing terminology in post-training, partly because the term ORM is used inconsistently in the literature. Step through them.
Outputs: one scalar for the whole sequence, read at the EOS token — interpretable as the probability this text would be the chosen one.
Trained by: a contrastive loss over pairwise (or N-wise) comparisons.
Head: a regression/classification head on top of the LM features.
Use it when: quality is a matter of taste and you have comparisons. This is the default and the one PPO consumes.
Outputs: a probability that the answer is correct — emitted at every token, not just EOS.
Trained by: labelled outcome pairs from verifiable domains: one completion solves the problem, one does not. The label is binary correctness, and the loss is per-token binary cross-entropy with prompt tokens masked to −100.
Head: a language-modelling-style head predicting two classes per token.
Note the shift: no chosen/rejected structure is required. This is much closer to ordinary language-model training than to Bradley-Terry.
Terminology warning: the original definition (Cobbe et al. 2021, “training verifiers”) is per-token; later literature uses “ORM” loosely for any correctness scorer. Ask which one someone means.
Outputs: a score at the end of each reasoning step — typically three classes: −1 incorrect, 0 neutral, +1 correct.
Trained by: step-level annotations, with a per-step cross-entropy loss. In practice steps are delimited by a separator token (a double newline or a special token) and labels are placed only at those boundaries; everything else is masked.
The TRL packaging trick is worth seeing, because it shows exactly how sparse the supervision is:
separator_ids = tokenizer.encode(step_separator, add_special_tokens=False) completions_ids = [c + separator_ids for c in completions_ids] labels = [[-100] * (len(c) - 1) + [l] for c, l in zip(completions_ids, labels)]
Crucial nuance: a PRM label says whether the step is correct — not whether the model is on a path that will reach the right answer. Those are different questions, and conflating them is a common source of disappointment with PRMs.
Outputs: the expected future return given the current state — one number per token.
Trained by: regression to the realised return at each point in the sequence.
This is not a reward model at all, though it looks like one: it lives inside PPO as a learned baseline for variance reduction, and it predicts returns, not preferences. In language-model RLHF the discount factor is usually 1, which makes a value function look uncomfortably similar to an ORM — but the training loss differs.
Session 3 shows why PPO needs it and why GRPO deletes it.
Outputs: text. You prompt a capable language model with judging instructions, a prompt, and two completions, and it explains itself and then emits a verdict.
The seminal template (MT-Bench) is instructive because of what it has to explicitly ask for: avoid position bias, do not let length influence you, do not favour certain assistant names, be objective — then output a strict verdict token like [[A]], [[B]] or [[C]].
Every one of those instructions exists because the judge does the opposite by default. Your eval stage's bias mitigations are not optional extras; they are load-bearing.
A cheap robustness trick: sample the judge at temperature 0 to reduce rating variance.
Honest status: an entire field studies generative reward models, including models trained specifically to judge — but on reward-model benchmarks they still tend to sit behind purpose-trained reward models. LLM-as-judge is the pragmatic choice, not the accurate one.
This is the Goodhart section for reward models. Each of these is a real, documented way a reward model scores something other than quality.
RMs correlate higher scores with verbosity rather than actual quality. A policy optimising such an RM discovers that padding is free reward. Length-controlled AlpacaEval exists purely to debias evaluators against this.
mitigation → length-normalised losses, LC win rates, ODIN-style disentanglingOver-agreeing with the user's stated beliefs, or flattering them, at the cost of truthfulness. It reflects a property of humans that annotation guidelines rarely think to forbid — so it passes straight into the RM.
mitigation → adversarial pairs, separate prompt-writer from labellerThe beginning of a completion disproportionately drives the preference. A strong opening buys a weak body. Documented and measurable, and it maps directly onto your JSON case: the first key emitted colours everything after it.
mitigation → shuffle field order in candidates; score on full-sequence EOSLLM evaluators recognise and favour their own generations. If your judge is Gemma and your policy is Gemma, the judge is systematically biased towards the policy it is supposed to be correcting.
mitigation → cross-family judge, or accept it and measure the sizeMarkdown density, bullet counts, emoji. Easier to detect and mitigate than sycophancy, and worth a programmatic check because these are trivially measurable.
mitigation → strip/normalise formatting before judgingThe deepest one. An RM trained on one policy's outputs is only calibrated on that distribution. As RL pushes the policy away, the RM is increasingly asked about text it has never seen — and its errors get systematically amplified because it is the sole optimisation target.
mitigation → on-policy data, RM refresh, KL leash, early stoppingThe material's section on the nature of preferences makes an argument worth taking seriously: modelling human preferences accurately is not a problem that gets solved, because the object being modelled is not stable. Three specifics that change how you build data:
The same expression appears as the DPO loss in S4 (with Δ replaced by β times a log-ratio difference), as the win-rate model in S7, and as the ranking model behind arena leaderboards. Bradley-Terry is not one topic in this stage; it is the spine of it.
A rough but useful sanity check before committing budget. InstructGPT used ~100K pairwise prompts to train its RM on top of ~10K SFT examples; Tülu 3 used ~1M preference pairs.
An April 2026 survey frames reward hacking not as a collection of bugs but as a structural instability of proxy-based alignment under scale, arising from three interacting forces: objective compression (squeezing high-dimensional human goals into a scalar), optimisation amplification, and evaluator–policy co-adaptation. It explicitly unifies the RLHF, RLAIF and RLVR cases — meaning the verifier route is safer, not safe.
arXiv 2604.13602 · “Reward Hacking in the Era of Large Models”Documented reward hacking used to be semantic — verbosity, sycophancy, confident tone. Recent work demonstrates token-space attacks on reward models, exploiting the RM's numerical behaviour rather than its notion of quality. The relevance for you: a programmatic format check cannot be attacked this way, and a learned scalar head can.
arXiv 2604.02686 · one more reason to prefer verifiers where they existThe largest and most recent openly-released human preference data remains NVIDIA's HelpSteer line (HelpSteer2-Preference, HelpSteer3-Preference). the literature's blunt observation stands: at time of writing there are no open models with fully open human preference data released alongside the methods used to collect it. Academic work shows synthetic preference data performs comparably; frontier labs still behave as though human data is a competitive advantage.
If you want to study real human preference data, HelpSteer is where to look.The decision tree above already gave the recommendation. Here is the argument in your specific terms, and then the code for both routes so you can see what you are declining.
Do not train a Bradley-Terry reward model for the fintech adapter. You have ~4K prompts, no online RL loop to feed, and a large fraction of your quality signal is programmatically checkable. A trained RM would be a fourth model to host and evaluate, calibrated on 0.4% of the data that comparable RMs are trained on, in order to serve a training loop you are not going to run. Build the scorer instead — programmatic checks plus a judge — and feed its output straight to DPO as pairs.
import json, jsonschema ANALYST_SCHEMA = {...} # your existing production schema def hard_checks(text: str) -> dict: """Deterministic, unbiased, free. Returns a dict of booleans + a tier.""" out = {"parses": False, "schema_ok": False, "evidence_present": False, "score_in_range": False, "flag_has_citation": False} try: obj = json.loads(text) except json.JSONDecodeError: return out out["parses"] = True try: jsonschema.validate(obj, ANALYST_SCHEMA); out["schema_ok"] = True except jsonschema.ValidationError: pass out["evidence_present"] = bool(obj.get("evidence", "").strip) out["score_in_range"] = isinstance(obj.get("risk_score"), (int, float)) \ and 0.0 <= obj["risk_score"] <= 1.0 out["flag_has_citation"] = (not obj.get("compliance_flags")) or out["evidence_present"] return out def tier(checks: dict) -> int: # lexicographic quality tiers; higher is strictly better if not checks["parses"]: return 0 if not checks["schema_ok"]: return 1 if not checks["score_in_range"]: return 2 if not checks["flag_has_citation"]: return 3 return 4 def make_pair(prompt, cand_a, cand_b, judge): ta, tb = tier(hard_checks(cand_a)), tier(hard_checks(cand_b)) if ta != tb: # settled for free, zero bias hi, lo = (cand_a, cand_b) if ta > tb else (cand_b, cand_a) return {"prompt": prompt, "chosen": hi, "rejected": lo, "source": "programmatic", "margin": abs(ta - tb)} return judge.compare(prompt, cand_a, cand_b) # biased; measure it
Note the source and margin fields. Keep them. In S5 you will want to be able to slice your training curves by whether a pair was settled programmatically or by the judge — if the judge-labelled slice behaves differently, you have found a bias.
For completeness, so you recognise it in someone else's repo. TRL's RewardTrainer implements exactly the one-line loss above.
from trl import RewardTrainer, RewardConfig from peft import LoraConfig cfg = RewardConfig( output_dir="gemma-fintech-rm", num_train_epochs=1, # ONE. reward models overfit fast. per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=1e-5, max_length=1024, bf16=True, gradient_checkpointing=True, ) trainer = RewardTrainer( model="google/gemma-4-E4B-it", # + AutoModelForSequenceClassification head args=cfg, train_dataset=pairs, # prompt / chosen / rejected peft_config=LoraConfig(r=16, lora_alpha=32, task_type="SEQ_CLS"), ) trainer.train
If you ever do need this: group all pairs from the same prompt into one batch (the InstructGPT balancing point above), and evaluate the result on held-out pairs as a plain accuracy — “how often does the RM order the pair the way the judge did.” Anything under ~65% and the RM is not usable as an optimisation target.
Take 100 pairs from your scorer. For 50 of them, swap the presentation order of the two candidates and re-run the judge. Count how often the verdict flips. That flip rate is your judge's position bias, measured directly, in about ten minutes.
Anything above ~5% and you must either always run both orders and take the agreement (doubling judge cost), or drop pairs where the two orders disagree (cheaper, and it also removes the genuinely ambiguous pairs — which, per the saturation analysis above, were low-information anyway). You will use this number again in S4 and S7.
You now have a scalar. A scalar is not a gradient — something has to turn “this rollout scored 0.8” into a parameter update, without letting the policy sprint away from the model you started with.
Next session: the PPO four-model dance, GRPO's deletion of one of those models, and the KL leash that stops the whole thing eating your reward model alive.
Your reward model says this completion is worth 0.8. Now what? There is no target string to regress towards — the whole point was that you could not write one. You have a number attached to a sequence you sampled, and you need it to become a change in weights.
Policy-gradient reinforcement learning is the machinery for that. It is also the part of post-training that most people never run and most people misunderstand, so this session has an unusual job: give you complete mechanical understanding of something you will read about far more often than you execute.
Sample, score, baseline, update — then throw the rollouts away, because the policy has moved.
Suppose you are coaching someone and you can only give one instruction: “do more of what worked, less of what didn't.” That is the policy gradient. The subtlety is entirely in the phrase “what worked”.
Say a rollout scores 0.8. Is that good? Unanswerable without a reference point. If every rollout from this prompt scores around 0.9, then 0.8 was bad and you should make it less likely. If they average 0.3, 0.8 was excellent.
So the quantity that carries information is not the reward but the advantage: how much better this rollout was than what you'd have expected from this prompt. Reward tells you the score; advantage tells you the surprise. Gradients follow surprise.
The second subtlety: this is a noisy estimate from a handful of samples, so a single unlucky batch can shove the policy somewhere terrible and it never recovers. Hence a step-size limit — a trust region — that caps how far one update can move you.
In code, before any clipping, that is literally:
pg_loss = -advantages * ratio # ratio = π_θ(token) / π_old(token)
Three cases, and they are worth walking:
Every algorithm in this session — REINFORCE, RLOO, PPO, GRPO, GSPO, CISPO — is that one line plus a different answer to “where does A come from?” and “how do we stop the step being too big?”
| Algorithm | Where the baseline comes from | Step control | extra models |
|---|---|---|---|
| Vanilla PG / REINFORCE | Batch mean, or a moving average, or nothing | learning rate only | 0 |
| RLOO | Mean reward of the other samples for the same prompt (leave-one-out) | learning rate only | 0 |
| PPO | A learned value network, per token, usually via GAE | ratio clipping 1±ε | 1 (critic) |
| GRPO | Mean (and std) of rewards within a group of G completions to one prompt | ratio clipping, KL in the loss | 0 |
| GSPO | Same as GRPO | clipping on a sequence-level importance ratio | 0 |
| CISPO | Same as GRPO | clip the importance weight, not the objective | 0 |
Read that table twice. The entire algorithmic literature of RLHF fits in two columns.
clip_fraction metric; if it is zero, you are running REINFORCE with extra steps.This is the diagram people mean when they say PPO is heavy. Four copies of a language model, three of them just to make one of them learn safely.
Two frozen, two trainable, and a generation pass on top. This is why PPO farms are farms.
The min looks arbitrary until you enumerate the four interesting cases. Work through them:
Group Relative Policy Optimisation, from DeepSeekMath and used through DeepSeek-V3 and R1. The idea is almost embarrassingly simple: if you want to know whether a completion was better than expected, generate several completions to the same prompt and compare them to each other.
Dividing by the group standard deviation quietly rewards prompts where the answers barely differ: when nearly all completions are right or nearly all are wrong, std is small, so advantages get scaled up. Dr. GRPO removes the std term for this reason — but that trade has its own cost, because it down-weights exactly the high-variance prompts where only one or two samples found the answer, and those are often the most valuable learning signal you have.
A satisfying connection: Dr. GRPO's advantage is equivalent to RLOO's leave-one-out advantage up to a constant factor of G/(G−1). These algorithms are much closer relatives than their names suggest.
One full model copy in memory, plus its optimiser state, plus the unsolved problem of learning a value head on an LM backbone.
Many more rollouts per prompt. PPO can learn from one completion per prompt; GRPO needs a group — 8, 16, sometimes 64. You traded training memory for generation compute.
PPO subtracts a per-token KL from the per-token reward. Canonical GRPO adds KL as a separate loss term. Same intent, different placement, and it changes the gradient.
Two directions exist and they behave differently. The one used throughout RLHF is the reverse KL: samples come from the policy, probabilities are read from the reference. Intuitively it applies a large penalty when the policy puts substantial probability mass where the reference assigns low probability — that is, when the policy starts saying things the SFT model would basically never say. That is exactly the failure you want to catch.
The forward direction penalises the policy for not covering high-probability regions of the reference, which is a distillation-style objective and not what you want here.
Computing exact KL over the vocabulary at every position is expensive, so implementations use an approximation that falls out of sampling directly from the policy:
Which reduces to a handful of lines you have effectively already written in your SFT stage:
logits = model.forward(gen[:, :-1]).logits ref_logits = ref_model.forward(gen[:, :-1]).logits logprobs = F.log_softmax(logits, dim=-1) ref_logprobs = F.log_softmax(ref_logits, dim=-1) tok = logprobs.gather(-1, gen[:, 1:].unsqueeze(-1)).squeeze(-1) ref_tok = ref_logprobs.gather(-1, gen[:, 1:].unsqueeze(-1)).squeeze(-1) kl_approx = tok.sum(-1) - ref_tok.sum(-1)
Static vs dynamic. The original PPO implementations used adaptive KL controllers that targeted a specific KL and adjusted the penalty coefficient from recent measurements. Most modern RLHF uses a static penalty. And in the RLVR era, many reasoning recipes — RAGEN, Magistral, OpenReasonerZero, Skywork OR-1 — remove the KL penalty entirely, on the grounds that a verifier is far less hackable than a reward model, so the policy can be allowed to explore.
You now have two regularisers: PPO's internal step-size clipping and the external KL distance penalty. When language-model RL is run with one gradient step per batch — which is common — the clipping cannot fire, and the KL penalty is doing all the work alone. the literature's conclusion is worth quoting in spirit: for fine-tuning language models, the PPO-vs-REINFORCE distinction is far less meaningful than it is when training agents from scratch.
You have per-token losses. How do you reduce them to one scalar? Three answers, all in production use, and they do not agree.
Everything in S2 about biased reward models becomes actively dangerous here, because RL is a strong optimiser pointed directly at those biases.
In overfitting, training accuracy improves while held-out accuracy degrades — both measure the same task on different splits. In over-optimisation the model genuinely improves at the proxy, generalisably, but the proxy diverges from the true goal. The metric was never quite right. Different disease, different cure: more data does not fix it.
The early-chat-model symptom list is still diagnostic: canned phrases (“As an AI language model…”, “Certainly!…”), uninformative repetition and hedging, self-doubt and over-apologising, sycophancy, and over-refusal. If your DPO'd Gemma starts prefixing every JSON with an apology, you are looking at this.
The canonical example: a model asked “how do I kill a Linux process” refusing on the grounds that it cannot help harm any living being. Multiple 2023 releases shipped with this. the literature's careful framing matters — it is inaccurate to blame the algorithm. Training method plus the data curation guidelines the modelling team wrote produced the balance; and deployment settings such as the system prompt contribute too.
It is an open research question which error dominates: approximation error (the RM cannot fit preferences), estimation error (the RM overfit its training set), or optimisation error (the RL update itself). You cannot debug this from the loss curve — which is why the defence is empirical: hold out evals, stop early, refresh the data.
People say PPO is “too heavy for a single GPU” without ever showing the sum. Here it is, for your actual hardware and your actual model.
NVIDIA's Nemotron 3 Nano report gives unusually concrete numbers for a real RLVR run: synchronous GRPO with masked importance sampling to mitigate training–inference mismatch, 128 prompts per step, 16 generations per prompt, batch size 2048 (so updates are genuinely on-policy), MoE router weights frozen for stability, maximum generation length 49K, and overlong filtering — which they found boosts reasoning-intensive benchmarks. Infrastructure: NeMo-RL as the loop controller, Megatron-Core for training, rollouts routed through NeMo Gym and vLLM.
arXiv 2512.20848 — the closest thing to a production RLVR runbookSince v1.0 (Mar 2026), GRPOTrainer has picked up asynchronous rollouts (AsyncGRPOTrainer), a chunked LM-head path cutting peak memory up to 44× on 8K-token sequences, alternative loss types including VESPO and Dr.-GRPO-style variants, static and adaptive entropy regularisation to prevent policy collapse, and multi-environment agentic RL where each environment defines its own reward.
vllm_mode now defaults to "colocate".
Not a solved problem, and increasingly not a niche one. The 2026 literature includes work on monitoring emergent reward hacking via internal activations during generation, detecting it with gradient fingerprints, and testbeds studying its emergence in RLVR specifically — “LLMs Gaming Verifiers: RLVR can Lead to Reward Hacking” being the title that should keep you honest about rung 3 being safer rather than safe.
Verifiers reduce the attack surface. They do not close it.Do not run PPO or GRPO for the fintech adapter as your main path. The arithmetic above is the argument. But there is one genuinely valuable thing to run: a small GRPO job with a purely programmatic reward, on a tiny model, purely to see the mechanics with your own eyes. Two hours of your life, and afterwards the diagrams above stop being diagrams.
This is the most transferable artefact in this tab. Notice it is a verifier, not a reward model — which means if you ever do run RLVR (S6), this function is already most of the work.
rewards.py — composable verifier rewards for strict-JSON analyst outputimport json, jsonschema from trl import GRPOConfig, GRPOTrainer def json_parse_reward(completions, **kw): """1.0 if it parses at all. The floor.""" out = [] for c in completions: try: json.loads(c); out.append(1.0) except Exception: out.append(0.0) return out def schema_reward(completions, **kw): """1.0 if it validates against the production analyst schema.""" out = [] for c in completions: try: jsonschema.validate(json.loads(c), ANALYST_SCHEMA); out.append(1.0) except Exception: out.append(0.0) return out def evidence_grounding_reward(completions, source_text, **kw): """1.0 only if the evidence span is VERBATIM in the source. Anti-hallucination.""" out = [] for c, src in zip(completions, source_text): try: ev = json.loads(c).get("evidence", "").strip except Exception: out.append(0.0); continue out.append(1.0 if ev and ev in src else 0.0) return out def brevity_penalty(completions, **kw): """Explicit length control. Without this, RL discovers padding.""" return [-max(0.0, (len(c) - 900) / 900) for c in completions] cfg = GRPOConfig( output_dir="gemma-fintech-grpo-demo", num_generations=8, # the GROUP. this is what replaces the critic. reward_weights=[1.0, 2.0, 3.0, 0.5], beta=0.04, # KL coefficient. verifier reward → keep it small. per_device_train_batch_size=1, gradient_accumulation_steps=8, max_completion_length=1024, use_vllm=True, # colocate mode by default since TRL v1.0 log_completions=True, # you WILL need to read what it generated bf16=True, gradient_checkpointing=True, ) trainer = GRPOTrainer( model="google/gemma-4-E4B-it", reward_funcs=[json_parse_reward, schema_reward, evidence_grounding_reward, brevity_penalty], args=cfg, train_dataset=prompts, peft_config=lora_cfg, )
Two things to notice. Reward weights are a policy decision, not a hyperparameter — setting grounding to 3.0 and brevity to 0.5 is you stating that a hallucinated citation is six times worse than a verbose answer. Write that down somewhere a human can review it. And the brevity penalty is not optional: length bias is not a reward-model-only problem, and an unpenalised RL loop with a per-sequence-normalised loss will find padding.
You already have the pattern in gemma_modal.py: image-as-code, a persistent volume for the HF cache, secrets for the gated model. Spin up, run 200 GRPO steps, scale to zero. No cluster to babysit. For a two-hour learning run this is the right tool and it is not close.
Your fine-tuning stage already established this: a training job as a Kueue-managed workload on spot GPUs, checkpointing the adapter so a preemption costs minutes rather than the run. The advantage is queueing and preemption handling, which matter when the job is long enough that spot interruption is a certainty rather than a risk.
strength: survives spot preemption; shares a quota poolArgued choice for this session: Modal. The GRPO run here is a learning exercise with a known short duration and no queue contention, so the operational machinery of Kueue buys nothing. S5 makes the opposite argument for the real DPO run, where checkpoint-and-resume on spot capacity does pay for itself.
Run the config above for 100 steps on 300 of your prompts, with log_completions=True, and then read the completions table rather than the loss curve. Specifically: find a prompt where all 8 group members scored identically. Its advantages are all zero, so it contributed nothing to the gradient.
Count how many of your prompts are like that. If most are, your reward function is not discriminating and no amount of GRPO will help — this is the same difficulty-filtering insight that made Olmo 3 drop prompts its base model solved 8/8 times. The group-relative advantage is only informative when the group actually disagrees.
Everything in this session existed to solve one equation — maximise reward, minus β times the KL to the reference. It took four models, a generation server and a small library of clipping tricks.
Next session: that same equation has a closed-form solution, and if you substitute it back into Bradley-Terry the reward model cancels out entirely. No critic, no rollouts, one loss function — and a run that fits on your L4.
Session 3 built an elaborate machine to solve one equation. Direct Preference Optimization solves the same equation — same objective, same data, same β — by noticing that the optimal policy has a closed form, and that if you rearrange it to express the reward in terms of the policy, the reward model cancels out of the Bradley-Terry loss entirely.
The paper's subtitle is the whole idea: Your Language Model is Secretly a Reward Model. This is the most commonly misunderstood sentence in post-training. It does not mean DPO avoids reward modelling. DPO is still fitting a reward model — it just uses the policy's own log-ratio as that model, so there is nothing separate to train.
Hover the crimson box. The reward model does not disappear — it becomes implicit inside the policy.
Here is the trick in one paragraph, no symbols.
Your policy assigns a probability to any completion. So does your frozen reference. Take the ratio: how much more likely does the trained model find this text than the untrained one did? That ratio is a score. It is high for text the training pushed towards, low for text it pushed away from. It behaves exactly like a reward model, because it is one.
So instead of training a separate network to score text and then optimising against it, you write down the Bradley-Terry loss using this score, and do gradient descent on the policy directly. Making the policy assign higher relative probability to the chosen completion is raising its implicit reward.
The analogy: rather than hiring a critic, teaching them your taste, and then rewriting drafts until the critic is happy, you just read the two drafts side by side and rewrite until the better one feels more natural to write than the worse one. Same destination, no critic on payroll.
Because there is no sampling loop, DPO is offline: it only ever sees completions someone else generated. That is the source of both its cheapness and its ceiling, and it is why the “where do pairs come from” question later in this tab matters more than the loss function does.
−log σ(r_c − r_r). It is the same loss. Only the definition of r changed.And here is the entire implementation, from the original release — five lines, and it is genuinely all of it:
pi_logratios = policy_chosen_logps - policy_rejected_logps ref_logratios = reference_chosen_logps - reference_rejected_logps logits = pi_logratios - ref_logratios losses = -F.logsigmoid(beta * logits) chosen_rewards = beta * (policy_chosen_logps - reference_chosen_logps).detach rejected_rewards = beta * (policy_rejected_logps - reference_rejected_logps).detach
Those last two lines are not part of the loss — they are the implicit rewards, logged for you. In S5 they appear in your training curves as rewards/chosen and rewards/rejected, and reading them correctly is most of the skill of running a DPO job.
You do not need to reproduce this, but seeing the shape stops DPO feeling like magic.
Start from S1's objective: maximise expected reward minus β·KL to the reference. Expand the KL as an expectation, pull out the 1/β, and the whole thing rearranges into a KL divergence between the policy and some other distribution. Gibbs' inequality says a KL is minimised at exactly zero, and only when the two distributions are equal — so the optimal policy is that other distribution:
Read it as: the optimal policy is the reference model, re-weighted by an exponential of reward. High-reward completions get boosted; low-reward ones get suppressed; β controls how aggressively. Z(x) just makes it sum to 1.
Take logs of both sides and rearrange for r. That gives the implicit-reward equation above. Nothing clever — just algebra. The important structural point is that we have now written the reward as a function of the policy we are training, rather than as a separate network.
Bradley-Terry (S2) says P(y_c ≻ y_r) = σ(r_c − r_r). Substitute the expression from move 2 for both rewards. Both contain the term β log Z(x) — same prompt, same partition function — so the subtraction annihilates it. What survives is the DPO loss.
If Z(x) had not cancelled, DPO would be uncomputable. The entire method rests on the fact that a preference is a comparison, and comparisons only care about differences. Exactly the invariance you verified numerically in S2.
Three parts, each doing a distinct job:
In PPO the KL is measured on rollouts and the penalty responds to where the policy actually went. In DPO, β sets the KL: the method steps directly towards the exact optimal policy for that β, given the data. This makes β easier to reason about than an RL KL coefficient — but it also means the optimal value depends jointly on the model and the dataset, and cannot be transferred blindly from someone else's run.
The optimisation only cares about the margin. Nothing in the loss says “raise the chosen probability”; it says “make the gap bigger.” In practice the model usually satisfies this by reducing the probability of both completions, with the rejected reduced more.
rewards/chosen is not automatically a bug — but a steeply falling one is.Where does the freed probability mass go? Not to the chosen response — to unaddressed behaviours: tokens the model could generate but which appear nowhere in your preference data. Work on “unintentional unalignment” documents this, and Cal-DPO (adjusting the optimisation) and AlphaPO (modifying the reward shape) are direct mitigations. Honest status: the practical impact is not well characterised, but it is a leading hypothesis for why online methods beat vanilla DPO.
Multiple controlled studies — same data, different algorithms — conclude that policy-gradient methods outperform DPO and its variants. the literature's summary is that DPO algorithms are “a hair behind.” The posited reason is the offline one: the training signal comes from completions produced by previous or other models, not the model being trained.
And yet DAAs remain heavily used in leading models, because iteration speed on data beats a small algorithmic edge, and data is what actually determines outcomes. Ai2's phrasing for Olmo 3 is the practitioner's position: DPO as a highly iterable, cheap and stable preference method, “despite its critics.”
Start at 0.1. Move up if the model degrades; move down only if you trust your pairs and nothing is happening.
There are dozens. These are the ones you will meet, grouped by what problem they were built to solve rather than by name.
| Method | The one change | ref model? | pairs? | Choose it when |
|---|---|---|---|---|
| DPO | The baseline: Bradley-Terry on policy log-ratios | yes | yes | Default. Start here every time. |
| IPO | Softens the preference probability instead of optimising a hard label — moves away from the Bradley-Terry assumption | yes | yes | You suspect DPO is overfitting near-deterministic labels |
| cDPO | Label smoothing: assumes N% of your labels are simply wrong | yes | yes | You measured your judge's error rate and it is non-trivial |
| ODPO | Requires the chosen/rejected gap to exceed an offset, so pairs are not all equal | yes | yes | You kept the rating magnitudes — but labelling gets harder |
| KTO | Drops pairs entirely. Learns from unpaired binary signals — thumbs up / thumbs down — via a prospect-theory-inspired value function | yes | no | Your feedback is thumbs, not comparisons. Production telemetry, not annotation. |
| ORPO | Merges SFT and preference learning into one stage with an odds-ratio penalty. No reference model at all | no | yes | You want one training pass total, and memory is very tight |
| SimPO | Uses the average (length-normalised) log-probability as the implicit reward plus a target margin. No reference model | no | yes | Length bias is your main enemy and you want shorter, denser outputs |
| Online DPO / D2PO | Generates fresh completions during training; D2PO relabels with a reward model on the fly | yes | yes | You have rollout capacity and want to close the gap to RL |
| REBEL | Adds reward-model signal as a margin, rather than using bare pairwise labels | yes | yes | You already have an RM and want more than a binary label from it |
A 2025 study is titled, plainly, “The differences between direct alignment algorithms are a blur.” the literature's own conclusion is that the choice of algorithm is far less important than the initial model and the data used. RainbowPO reaches a similar place by unifying the improvements into one framework.
Practical reading: pick DPO, spend the time you saved on your preference pairs, and only reach for a variant when you can name the specific pathology it fixes.
This is the part that decides whether your run works. Source §11, plus the synthetic-data section, plus everything you already know about judge bias from your evaluation stage.
The original. A labeller sees a prompt and two completions and picks one, often with metadata: free-text notes, an overall conversation rating, sometimes a Likert scale.
Rankings vs ratings. The largest structural decision. Common practice is to train on rankings — relative ordering — while ratings get kept as metadata. A 5-point Likert scale records a single integer, just like a rating; the difference is in how the data is structured, not how it is recorded. Some early work used an 8-point scale specifically because an even scale removes the possibility of ties.
The multi-turn problem. Normally preference is collected only on the final turn, and the conversation continues with the chosen answer. If preferences are given on every turn you can unroll a conversation into many training examples — but carefully, or you bias the data toward whatever the long conversations were about. Earlier turns are masked from the loss, exactly as in SFT.
Open question the literature raises, credited to John Schulman: should the person who wrote the prompt be the one who labels the preference? Using the same person invites sycophancy in the data. Using different people makes real-time multi-turn collection impractical.
Once a model is deployed, the users become the annotators. Thumbs up/down on a response. Two candidate responses shown side by side (as ChatGPT has done). Regenerating an output, closing the tab, or writing an angry follow-up — all implicit feedback signals about quality.
Whether this data trains the next model or only evaluates it is an application-level decision. Note the shape mismatch: thumbs are unpaired, which is precisely the format KTO consumes and DPO cannot.
Warning from §16: making the reward function more specific by mining implicit feedback carries a real risk of losing stability. RL is a strong optimiser, and it is increasingly likely to exploit a reward function the smoother that function becomes.
The dominant open practice, and the one you will use. The recipe, essentially unchanged since UltraFeedback and refined by Tülu 3:
The on-policy requirement. Tülu 3 demonstrated that some completions must come from the model you are fine-tuning, mixed into a bigger model pool. The reason is mechanical: DPO's loss is contrastive and less direct than SFT's, so it needs to operate in the token space your model actually generates in.
This is the one most relevant to you, and it is under-used. In many domains preference data can be constructed automatically because the domain has innate structure that makes correctness checkable.
The clean example is precise instruction following (IFEval-style). A prompt has a constraint: “Write a short poem about a goldfish. Start each sentence with the letter g.” To build a pair, you prompt the same model twice — once with the constraint included, once without. The constrained generation becomes chosen; the unconstrained one becomes rejected. No judge, no human, no bias.
In maths, chosen = correct answer, rejected = incorrect answer. Same idea.
For a strict-JSON analyst adapter this is a goldmine: every schema rule, every required field, every numeric bound is a constraint you can ablate to manufacture a clean pair.
A newer theory that partly competes with the on-policy requirement. The Delta Learning Hypothesis argues that what matters is the difference between chosen and rejected, not which models produced them.
The striking evidence: both Olmo 3 and SmolLM 3, independently and concurrently, built preference data where chosen responses come from Qwen 3 32B and rejected responses come from Qwen 3 0.6B. Same family, wildly different capability. No judge in the loop at all.
This is cheap, unbiased in the judge sense, and it works. Its limitation is that it teaches “be more like a big model” rather than any preference specific to your task — so it is a strong general-quality signal and a weak domain signal.
Honest status: on-policy and delta learning are two theories with supporting evidence, not a settled question. Best practice for constructing these datasets is explicitly still evolving.
This is the most important connection in the session. In evaluation, a biased judge gives you a wrong number and you draw a wrong conclusion. In preference-data generation, a biased judge writes the wrong thing into your model's weights. The failure is no longer a measurement error; it is a behavioural change you then have to detect and undo.
Concretely: frontier models used as judges are known to have length bias and to prefer outputs that match their own style. So a “chosen” column labelled by an OpenAI model is slightly more likely to contain text from an OpenAI model or something stylistically similar. Every bias you catalogued in your eval stage — position, verbosity, self-preference — applies here with higher stakes.
rewards/margins is the number you watch, and it should rise steadily. (2) rewards/accuracies — the fraction of pairs ordered correctly — is a better health signal than loss, because it is β-invariant. (3) A run at β=0.5 will look calmer and do less. If nothing is happening, check your data before you touch β.TRL v1.4 (9 May 2026) added loss_type="sigmoid_norm" to DPOConfig — the per-token, length-normalised DPO loss used by Tülu 3 and OLMo specifically to mitigate length bias. Previously you either patched the loss yourself or switched to SimPO. Now it is one line.
Google's Vertex AI now offers preference tuning for Gemini as a first-class tuning job. The request body takes a preferenceOptimizationSpec with epochCount, adapterSize, learningRateMultiplier — and beta. The dataset format is exactly what you would expect: prompt plus a preferred and dispreferred response pair.
In June 2026 Amazon SageMaker AI added serverless model customisation for Gemma 4 E4B and 31B via SFT, DPO and reinforcement fine-tuning — joining Nova, Nemotron 3, Qwen, Llama, gpt-oss and DeepSeek families. A March 2026 launch had already extended serverless RFT (including RLVR and RLAIF) to twelve more open-weight models.
A genuine alternative to your L4 run — and a useful cost baseline to compare against.sigmoid_norm loss — TRL v1.4.0 release notes, 9 May 2026.No trainer code yet — S5 is the code-along. This lab designs the dataset, which is the decision that actually determines whether S5 works.
| layer | What it decides | Expected share | Bias |
|---|---|---|---|
| L0 | Structured pairs. Generate each prompt twice — once with the full schema + constraints in the system prompt, once with constraints ablated. Constrained = chosen. | manufacture ~1,000 pairs on demand | none |
| L1 | Hard checks. Parses / schema-valid / score in range / evidence non-empty / evidence verbatim in source. Different tier ⇒ pair settled. | ~55–60% of sampled pairs | none |
| L2 | Judge with order swap. Run the comparison twice with A and B exchanged. Agreement required. | ~30% of pairs | position bias neutralised |
| L3 | Drop. Judge contradicted itself across the swap ⇒ discard, do not guess. | ~10% | removes ambiguous, low-information pairs |
Recall the Bradley-Terry saturation arithmetic from S2. A pair where the judge flips under order swap is a pair with a tiny true margin — it carries almost no information and a coin-flip label. Training on it injects noise with no upside. Dropping it is the same move as Olmo 3 filtering out prompts its base model already solves 8/8 times: remove the samples that cannot teach anything.
enable_thinking=true as judgeFor: essentially free — you already serve it, and the FP8 + prefix-caching config in your gemma_modal.py makes bulk judging cheap. Prompts and rubric stay entirely inside your infrastructure, which matters for fintech data residency. Thinking mode measurably improves judgement quality on comparison tasks.
Against — and it is fatal here: your policy is Gemma. LLM evaluators are documented to recognise and favour their own generations. A Gemma judge scoring Gemma candidates has a self-preference bias pointing in exactly the direction that makes your DPO run a no-op — it will systematically prefer whatever the policy already does.
verdict: not as the primary judge for on-policy candidatesFor: different model family, so self-preference no longer aligns with your policy's habits. Higher agreement with expert labels on domain-taste questions. And the cost is trivially small at your scale — the S1 arithmetic put ~1,600 judged pairs at roughly $13.
Against: data leaves your infrastructure, which is a compliance conversation, not a technical one. It has its own length and style biases, which you must measure rather than assume away. And you have created a dependency on an external API in your training pipeline.
verdict: primary judge for L2, on redacted promptsArgued recommendation: external judge for L2 on redacted content, Gemma-as-judge as a second opinion for agreement measurement only. Where redaction is impossible, fall back to Gemma and widen L0 and L1 so that structured and programmatic pairs carry more of the dataset — trading coverage for bias, deliberately, and writing down that you did.
{"prompt": [{"role":"user", "content": "…excerpt + task…"}],
"chosen": [{"role":"assistant", "content": "{\"risk_score\":0.78,…}"}],
"rejected": [{"role":"assistant", "content": "{\"risk_score\":0.5,…}"}],
// metadata — not consumed by the trainer, essential for you
"layer": "L1",
"reason": "evidence_present",
"on_policy": true,
"judge_agreed_both_orders": null}
Conversational format, because your Gemma chat template must render identically to how it renders at serving time — the same discipline you already applied in SFT. Keep layer and on_policy: in S5 you will slice the curves by them, and in S7 you will need to prove your training data was decontaminated against the golden set.
Build 200 L0 structured pairs before anything else — same prompt, once with the full schema and grounding rules in the system prompt, once with them stripped. Then run your current SFT adapter on 50 held-out prompts and check what fraction already satisfies every rule you just used to build those pairs.
If it is above ~90%, L0 pairs are near-saturated and will teach almost nothing — go straight to L1 and L2 for taste. If it is below ~70%, you have found something better than a DPO opportunity: you have found an SFT bug, and rung 1 will fix it faster and more cheaply than rung 2. That check costs an hour and can save a week.
You have the loss, the leash, and a designed dataset. What is left is the part no paper explains: getting a four-forward-pass objective and two model copies onto a 24 GB card, and knowing what a healthy curve looks like.
Next session is the code-along — the reference-free LoRA trick, a working DPOConfig, the VRAM arithmetic that proves it fits, and the five failure smells worth recognising on sight.
S4 gave you five lines of loss. This session is everything those five lines don't tell you: how two model copies fit on one 24 GB card, which of TRL's three reference-model strategies to use and why, what a healthy margin curve looks like, and the five ways a DPO run fails while appearing to succeed.
This is the designated code-along session — but per the rule, concepts first. Read the recap below before running anything, because every code block afterwards implements one row of it.
| block | Concept it implements | Where it came from |
|---|---|---|
| 1 | Candidate generation. Sample n>1 completions per prompt from your SFT adapter, so at least one side of every pair is on-policy. | S4 · Tülu 3's on-policy requirement |
| 2 | The layered labeller. Structured pairs, then hard checks, then an order-swapped judge, then drop the contradictions. | S4 lab · S2 bias catalogue |
| 3 | Dataset schema. Conversational prompt/chosen/rejected, rendered through the same Gemma chat template you serve with. | S4 · your SFT stage |
| 4 | 4-bit base + trainable adapter. QLoRA, so the policy fits and only ~0.5% of parameters carry optimiser state. | your fine-tuning stage |
| 5 | The reference model, for free. πref obtained by disabling or swapping the adapter rather than loading a second model. | S3's four-model arithmetic · S4's β |
| 6 | DPOConfig. β as the KL leash, plus the memory knobs that make four forward passes per step survivable. | S4 · S1's regularised objective |
| 7 | Curve reading. rewards/chosen, rewards/rejected, rewards/margins, rewards/accuracies — the implicit reward model, logged. | S4's implicit reward |
| 8 | Failure smells. Likelihood displacement gone wrong, saturated pairs, format regression, judge bias leaking through. | S2 saturation · S4 displacement |
| 9 | Ship it. Adapter out, multi-LoRA alongside the SFT adapter, A/B on your vLLM endpoint. | your serving stage · S7's gate |
Every DPO step needs four numbers: log πθ(yc), log πθ(yr), log πref(yc), log πref(yr). The first two come from the model you are training. The second two come from a model that must be the SFT checkpoint, frozen. Naively that is a second full model in memory — and that is exactly the thing your 24 GB card cannot afford twice.
The insight that rescues you is one you already have from LoRA: a LoRA-adapted model is the base model plus a small delta. Turn the delta off and you are the base model again. So the reference model is not a second set of weights — it is the same weights with the adapter bypassed.
Same loss, three memory profiles. Option 3 is the one for a QLoRA continuation.
Because the reference model is frozen, its log-probabilities for your dataset never change. So you can compute them once, up front, cache them, and never run the reference forward pass again. TRL exposes this as precompute_ref_log_probs=True, and since v1.2 the trainer will not even load a separate ref_model when you set it. This cuts your per-step forward passes from four to two.
The catch: it commits you to a fixed dataset (no on-the-fly augmentation) and costs one full pass over the data before training starts. For a 4,000-pair set on an L4 that pass is minutes, and it is worth it.
Before writing a line of training code, prove the run fits. This is the arithmetic the brief asked for, at Gemma E4B scale (~4B effective params for the sum), sequence length 1024, batch size 1 with gradient accumulation.
TRL v1.x moves fast and trainer signatures shift across minor versions — pin trl, transformers, peft, accelerate, bitsandbytes and datasets as one set and revalidate on upgrade. Relevant recent changes: v1.2 stopped loading ref_model when precompute_ref_log_probs is set; v1.4 added loss_type="sigmoid_norm"; v1.4 also fails fast on unsupported PEFT + Liger-kernel combinations in DPO; and a quantization_config argument now sits alongside peft_config on DPOTrainer, so the trainer can load and quantise the model for you.
In June 2026 SageMaker AI added serverless customisation for Gemma 4 E4B via SFT, DPO and reinforcement fine-tuning, with no cluster setup or capacity planning. That is a real alternative to this lab, and a useful cost benchmark: if a managed DPO pass costs meaningfully more than $0.23 plus your time, the L4 run is the better deal — and unlike the managed path, it leaves you with the operational knowledge.
Also relevant to the NVIDIA/AWS/GCP career direction: know both paths.Liu's §10 walks a hand-rolled DPO: a dpo_loss function, a manual get_batch_logps, and two explicitly loaded full model copies with requires_grad=False on the reference. The maths is right and worth reading once. The engineering is the naive Setup A above, and on a 24 GB card it will not run at 4B scale. Its beta=0.1 default is still the correct default; its architecture is not.
Implements the on-policy requirement from S4. Run against your existing vLLM endpoint — prefix caching makes the shared system prompt nearly free, which is the same optimisation you already benchmarked.
gen_candidates.pyimport json, asyncio from openai import AsyncOpenAI client = AsyncOpenAI(base_url="https://…modal.run/v1", api_key="…") N_PER_PROMPT = 4 # UltraFeedback used 4; enough to find real disagreement async def sample(prompt, adapter="fintech-sft"): r = await client.chat.completions.create( model=adapter, # multi-LoRA: select the SFT adapter by name messages=[{"role": "user", "content": prompt}], temperature=0.9, top_p=0.95, # HIGH temp on purpose — see note max_tokens=512, n=N_PER_PROMPT, extra_body={"chat_template_kwargs": {"enable_thinking": False}}, ) return [c.message.content for c in r.choices]
At production temperature your four samples will be near-identical, every pair will be a tie, and you will have manufactured a dataset with no signal — the group-collapse problem from S3's exercise, in offline form. You need diversity to find disagreement. Sample hot, then let the layered labeller sort it out. This is one of the few places where a setting that would be wrong in production is right in data generation.
Implements S4's L0–L3 design. The hard_checks and tier functions come straight from S2's lab.
import itertools, random, json def judge_once(prompt, a, b, client, model): """Returns 'A', 'B' or 'TIE'. Temperature 0 to cut rating variance.""" msg = JUDGE_TEMPLATE.format(question=prompt, answer_a=a, answer_b=b) out = client.chat.completions.create( model=model, temperature=0, messages=[{"role": "user", "content": msg}]).choices[0].message.content if "[[A]]" in out: return "A" if "[[B]]" in out: return "B" return "TIE" def judge_symmetric(prompt, a, b, client, model): """L2. Ask both ways. Disagreement means the pair is genuinely ambiguous.""" fwd = judge_once(prompt, a, b, client, model) rev = judge_once(prompt, b, a, client, model) # A and B EXCHANGED if fwd == "A" and rev == "B": return a, b, True # both say the first text wins if fwd == "B" and rev == "A": return b, a, True return None, None, False # L3: drop, do not guess def build(prompt, cands, client, model): rows, seen = [], set for a, b in itertools.combinations(cands, 2): ta, tb = tier(hard_checks(a)), tier(hard_checks(b)) if ta != tb: # L1 — free, unbiased hi, lo = (a, b) if ta > tb else (b, a) rows.append(row(prompt, hi, lo, layer="L1", agreed=None)) else: # L2 — judged, order-swapped hi, lo, ok = judge_symmetric(prompt, a, b, client, model) if ok: rows.append(row(prompt, hi, lo, layer="L2", agreed=True)) # cap pairs per prompt so one prompt cannot dominate the gradient # (the InstructGPT balancing point from S2 — batch by prompt if you keep more) return random.sample(rows, min(2, len(rows)))
from datasets import Dataset ds = Dataset.from_list([{ "prompt": [{"role": "user", "content": r["prompt"]}], "chosen": [{"role": "assistant", "content": r["chosen"]}], "rejected": [{"role": "assistant", "content": r["rejected"]}], } for r in rows]) ds = ds.train_test_split(test_size=0.05, seed=0) # keep an eval split! # sanity: render one row exactly as the trainer will, and LOOK at it print(tok.apply_chat_template(ds["train"][0]["prompt"] + ds["train"][0]["chosen"], tokenize=False))
That last print is not optional. Chat-template mismatch between training and serving is the single most common silent failure in this whole pipeline, and you already know it from SFT. Confirm the rendered string is byte-identical in shape to what your vLLM endpoint produces.
import torch from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig from peft import PeftModel from trl import DPOTrainer, DPOConfig BASE = "google/gemma-4-E4B-it" SFT = "./adapters/fintech-sft" # your capstone adapter # --- block 4: 4-bit base -------------------------------------------------- bnb = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) tok = AutoTokenizer.from_pretrained(BASE) model = AutoModelForCausalLM.from_pretrained( BASE, quantization_config=bnb, dtype=torch.bfloat16, device_map={"": 0}) # --- block 5: the reference, for free (option 3) -------------------------- model = PeftModel.from_pretrained( model, SFT, is_trainable=True, # CONTINUE training the SFT adapter adapter_name="dpo_adapter", autocast_adapter_dtype=False) model.load_adapter( SFT, is_trainable=False, # <-- the frozen reference. IMPORTANT. adapter_name="reference_adapter", autocast_adapter_dtype=False) # --- block 6: config ------------------------------------------------------ cfg = DPOConfig( output_dir="gemma-fintech-dpo", beta=0.1, # the KL leash. S4's whole discussion. loss_type="sigmoid", # "sigmoid_norm" if length creeps — TRL ≥1.4 learning_rate=5e-6, # SURPRISINGLY LOW. see note below. lr_scheduler_type="cosine", warmup_ratio=0.1, num_train_epochs=1, # DPO overfits fast per_device_train_batch_size=1, gradient_accumulation_steps=8, # effective batch 8 max_length=1024, max_prompt_length=640, precompute_ref_log_probs=True, # 4 fwd passes → 2. huge win. bf16=True, gradient_checkpointing=True, eval_strategy="steps", eval_steps=50, logging_steps=5, save_strategy="steps", save_steps=100, # spot-preemption insurance report_to="none", # or "trackio" / your Langfuse sink ) trainer = DPOTrainer( model=model, ref_model=None, # the named adapters handle it args=cfg, train_dataset=ds["train"], eval_dataset=ds["test"], processing_class=tok, peft_config=None, # already a PeftModel — do NOT double-wrap ) trainer.train trainer.save_model("./adapters/fintech-dpo")
learning_rate=5e-6. Roughly 40× lower than a typical SFT LoRA rate. This is not a typo and it is not conservatism — when DPO was released the community took a while to figure out that it needs surprisingly low learning rates. Start at 5e-6; if margins do not move at all after 100 steps, try 1e-5 before touching β.
peft_config=None when you passed a PeftModel. If you pass both a PEFT model and a peft_config, you get a second adapter wrapped around the first and the reference logic silently stops meaning what you think it means. Pass one or the other, never both.
DPOTrainer logs the implicit reward model from S4 directly. These four numbers are your instrument panel.
Margins is the headline. Accuracies is the honest one, because it does not move when β does.
Diagnosis: your pairs are trivially separable. Recall the Bradley-Terry saturation table from S2 — at a margin of 4 the loss is already 0.018 and there is almost no gradient left. Your model is scoring easy wins on pairs that teach it nothing.
Usual cause: too much L1 (programmatic) data, where chosen parses and rejected does not. That is a formatting distinction your SFT model can already make.
Fix: rebalance towards L2 judged pairs; require that both sides of an L1 pair are at least schema-valid so the distinction is about quality rather than validity. Check your layer metadata field — this is what you kept it for.
Diagnosis: the model is widening the gap on the pairs it already gets right while never fixing the ones it gets wrong. Average margin goes up; the fraction ordered correctly does not.
Usual cause: label noise. A meaningful slice of your pairs are labelled backwards, so the model is being pulled in two directions and settles for amplifying the consistent subset.
Fix: measure it. Sample 100 pairs and hand-check. If more than ~10% are wrong, fix the labeller — and only then consider cDPO or IPO, which are built for noisy labels. As S4 put it, a variant that tolerates bad data is worse than good data.
Diagnosis: likelihood displacement, past the healthy range. Both log-probs are collapsing and the model is losing probability mass on both completions — meaning it is pushing that mass onto behaviours not present in your data at all.
Usual cause: β too low, learning rate too high, or too many epochs.
Fix: raise β (0.1 → 0.3), drop the learning rate, and stop at one epoch. Then generate samples and read them — this smell is the one that most often shows up as visible weirdness in outputs, and the loss curve alone will not tell you how bad it is.
Diagnosis: format regression. The DPO objective never mentioned your schema, and if the preference signal correlates with anything other than validity the model is free to trade format discipline for it.
Usual cause: judge-labelled pairs where the judge preferred a more discursive answer, plus a β low enough to let the policy leave the SFT format basin.
Fix: this is why you keep a programmatic eval running alongside training. Add a callback that generates 20 held-out completions every 100 steps and reports schema-validity as a metric. If it drops, you have your early-stopping signal — which is the S3 over-optimisation curve appearing in your own run, in a form you can actually measure.
Diagnosis: length bias, arriving through the judge. Your judge preferred longer answers; the model learned "longer".
Fix, in order of effort: (1) set loss_type="sigmoid_norm" — the length-normalised DPO loss added in TRL v1.4, used by Tülu 3 and OLMo for exactly this; (2) enforce a token-count band on candidates before they enter a pair, so length cannot be the distinguishing feature; (3) report length-controlled win rate in S7's gate rather than raw win rate.
Measure first: plot mean chosen-length minus mean rejected-length across your dataset. If that number is large and positive, the bias is in your data, not your loss.
Four of the five smells above are invisible in the DPO loss curve and obvious in generated outputs. Wire your programmatic checks into a TrainerCallback that fires every 100 steps. It costs you a minute of GPU time per check and it is the only thing standing between you and shipping a checkpoint that scored beautifully and broke your parser.
# vLLM engine args — the pattern you already run in gemma_modal.py
--enable-lora \
--max-loras 2 --max-lora-rank 16 \
--lora-modules fintech-sft=./adapters/fintech-sft \
fintech-dpo=./adapters/fintech-dpo \
--enable-prefix-caching --quantization fp8 --gpu-memory-utilization 0.92
Now both policies are addressable by name on one endpoint, on one GPU, and your Langfuse online-eval sampling can route a slice of live traffic to each. That is the A/B substrate S7's win-rate gate runs on — and it costs you no extra hardware, because a LoRA adapter is tens of megabytes.
A 31-minute, single-GPU job with a persistent HF-cache volume and a gated-model secret is precisely the shape your gemma_modal.py already handles. Image as code, scale to zero, no cluster. Spot preemption is irrelevant at this duration, and save_steps=100 covers you anyway.
The moment you are sweeping β across {0.05, 0.1, 0.3} × two loss types × three data mixes, you have 18 jobs and a queueing problem. That is what Kueue is for: a shared quota pool, spot GPUs, automatic requeue on preemption, and adapter checkpoints so a preemption costs minutes.
strength: sweeps, quotas, and surviving spotArgued choice: Modal for the first run, Kueue for the sweep. Note this is the opposite of the S3 recommendation's reasoning and lands in the same place for a different reason — there, Modal won because the job was a one-off demo; here it wins because the job is short enough that orchestration buys nothing until you have many of them.
Run the config twice, changing exactly one thing: beta=0.1 and beta=0.5. Plot rewards/accuracies for both on the same axes, then generate 50 completions from each adapter and compute schema-validity and mean token length.
You should see the β=0.5 run reach a lower accuracy but hold schema validity and length closer to the SFT baseline. That is the leash, measured on your own model — and it turns S4's β discussion from a paragraph you read into a trade-off you have personally observed. Keep both adapters; S7 will A/B all three against SFT-only.
You have climbed the second rung: a model that prefers. The third rung swaps the judge for a program that knows — and buys accuracy with decode tokens rather than with training compute.
Next session: RLVR and verifiable rewards, how reasoning models are trained, and what thinking actually costs on your own serving stack — priced in TTFT, ITL and KV pressure rather than in vibes.
Every failure mode in sessions 2 through 5 traces back to one fact: the reward was a learned opinion. Change the scorer from a model to a program, and Goodhart's grip loosens — not because the pressure disappears, but because a unit test cannot be flattered.
That is the third rung. RLVR — reinforcement learning with verifiable rewards — proceeds almost exactly like RLHF, but it makes the reward model optional, replacing it with a scoring function that returns a positive reward when the answer is correct and zero otherwise. It is the engine behind reasoning models, and it is also where post-training stops being a training-cost question and becomes a serving-cost question.
\boxed{} convention is borrowed from mathematical typesetting purely so a regex can pull the answer out regardless of how the model got there.Hover each. The right-hand box is why reasoning models could be trained at all.
Preference tuning is like being coached by someone with taste. RLVR is like doing past papers with the answer key. You attempt a problem, you check the answer, and you keep whatever approach got there.
Two things follow from that difference, and both are counter-intuitive.
First, you can iterate absurdly more. Standard instruction tuning does one or two epochs over its data. RLVR does hundreds or thousands of epochs over the same few data points, because there is no label to memorise — only a behaviour to discover and reinforce. The name is well chosen: you are reinforcing behaviours the base model would produce only occasionally into behaviours it produces reliably.
Second, the model discovers that thinking longer helps. Nobody instructs it to. Given a reward for correctness and freedom over how many tokens to spend, models trained heavily with RL generate more tokens per response, and that length increase correlates strongly with better downstream performance. Contrast this sharply with early RLHF's length bias, where responses got longer for marginal gains in preference rankings. Here the extra tokens are doing work.
That is the whole reasoning-model phenomenon in one sentence: RL training is a short path to buying accuracy with inference-time compute.
The verifier is a function, and for most domains it is short. Two canonical forms:
# maths — extract and compare def math_reward(completion, gold): m = re.search(r"\\boxed\{([^}]*)\}", completion) return 1.0 if m and normalise(m.group(1)) == normalise(gold) else 0.0 # code — run the tests def code_reward(completion, tests): passed = run_sandboxed(completion, tests) # all-or-nothing… return 1.0 if passed == len(tests) else 0.0 # …or partial credit: return passed / len(tests)
Answer extraction is a design choice with consequences: \boxed{}, “The answer is:”, an <answer> tag, or #### delimiters all work, and models trained on one format can lose substantial performance when evaluated in another. S7 returns to this.
Around that tiny function, real recipes accumulate a standard set of moves. These are the ones that recur across DeepSeek-R1, Kimi 1.5, Magistral, Llama-Nemotron, INTELLECT-2, MiMo-RL, Open-Reasoner-Zero and Skywork OR-1:
Sample N (often 16) completions per training prompt with the base model and record what fraction are correct. Drop the ones it always gets right and often the ones it never gets. What is left is the band where the gradient exists.
Filter within the batch, or schedule harder problems later. Addresses the second question after “which problems” — namely “in what order”.
As RL runs got longer and the reward function got less hackable, many recipes dropped the KL constraint entirely to let the model explore. A verifiable reward tolerates drift that a learned RM would not.
DAPO's “clip-higher” — asymmetric bounds on the two-sided clip — enables more exploration. Clipping has also been shown to produce spurious learning signals when rewards are imperfect.
Small extra rewards to keep <think>…</think> structure predictable, and language-consistency rewards so multilingual models do not switch language mid-reasoning.
Kimi 1.5 progressively extends the target length to combat overthinking; INTELLECT-2 runs a small penalty throughout. Progressive extension makes the model learn to reason efficiently in a tight budget first, then use those habits at longer lengths.
Completion lengths vary enormously and outliers are extreme, so synchronous loops leave GPUs idle. Partial-to-full async updates recover the throughput.
Recall S3's length bias in per-group normalisation. Magistral and MiMo normalise losses or advantages at the batch level rather than the group level to avoid it.
Note how many of these are data and bookkeeping decisions rather than algorithmic ones. That is the honest character of RLVR practice.
RL training and test-time compute are two ways of reaching the same place, and they predate each other's popularity.
Let the model generate a long reasoning trace before answering. This is what RL-trained reasoning models do by default, and the correlation between token count and downstream accuracy is the reason anyone tolerates the cost.
Cost shape: pure decode, strictly sequential, one request. It multiplies your inter-token-latency budget by however many thinking tokens you allow, and it holds a KV-cache slot for the whole duration.
the literature's caveat is the one to hold onto: what matters is the correlation between downstream performance and token count — otherwise it is just wasted energy.
Sample N completions, score them all, return the best. Note what it is not: Best-of-N does not modify the model at all. It is a sampling technique, closely related to rejection sampling — the difference being that rejection sampling fine-tunes on the selected completions, while BoN just returns them.
A satisfying detail: with simple BoN over one prompt, the argmax criterion and top-K with K=1 are provably the same thing. And you can still measure a KL distance for BoN relative to another policy, which is what makes BoN a fair baseline to compare against PPO.
Cost shape: N× the decode, but parallelisable — good news for your continuous batching, since N requests fill a batch rather than extending one. It also needs a scorer at serving time.
This is what “Pro” tiers of chat products often are: extra compute spent per query.
Sample several independent rollouts and return whatever answer most of them agree on. Used by DeepSeek-R1 and Phi-4 among others. Cruder than BoN but requires no reward model — you only need to be able to compare final answers, which for a verifiable domain is free.
The more advanced version — a scoring model trained to pick the best answer from parallel rollouts — was mentioned in the Claude 4 announcement and used in DeepSeek-GRM, but as of 2026 had not become common in open, documented reasoning recipes.
The one that matters most for you. Llama-Nemotron, Nemotron Nano, Qwen 3 and SmolLM 3 use system prompts, sometimes combined with length-controlled RL training, to give the user an on/off switch for thinking. Others (GPT-OSS, K2-V2) expose low / medium / high reasoning effort instead.
Your Gemma has exactly this: chat_template_kwargs={"enable_thinking": …}, already wired through your Modal endpoint. That single boolean is a per-request buy/skip lever on the entire cost curve below.
Training methods for graded effort levels are, by the material's own admission, not well documented. Binary toggles are the well-trodden path.
More thinking helps, then stops helping, then actively hurts. This is not a fringe result; it is the standard shape.
Look at that curve and then look back at Tab 0's over-optimisation figure. Same shape, different axis. In S3 the runaway variable was optimisation pressure against a proxy reward; here it is inference compute against a task. In both cases more of the thing that helped keeps looking like it should help, right up to the point where it doesn't.
The mechanism is documented: models reach a correct intermediate solution, continue reasoning, and overwrite it with an incorrect one. Work on oracle stopping — injecting </think> at every sentence boundary and picking the best stopping point in hindsight — found it improves average accuracy by 8% while cutting thinking tokens by 72%. Which is a startling amount of value being burned by not knowing when to stop.
You already benchmarked this hardware, so this is arithmetic on your own numbers rather than a vendor's. From your vLLM work: an L4 at batch 32 sustains 934 output tokens/second aggregate, and GCP L4 on-demand is $0.45/hour (≈$324/month for a 730-hour month).
Olmo 3's RLVR stage runs over five domains — maths, code, precise instruction following, general chat and more — with everything decontaminated against pretraining and midtraining data. For maths they dedupe DAPO Math, keep only English, semantically cluster the larger sets and keep one representative per cluster, then remove every prompt the final base model already solves 8 out of 8 times. What survives: 13.3K prompts. The filtering is not preprocessing; it is most of the method.
Ai2 Olmo 3, the most completely documented reasoning-model lifecycle available.NVIDIA's Nemotron 3 Nano was trained across many environments simultaneously — maths, code, QA, instruction following, multi-step tool use, multi-turn conversation, structured output — using synchronous GRPO with masked importance sampling, and finished with a separate RLHF stage using a large generative reward model to lift chat benchmarks. Its SFT stage explicitly instils reasoning budget control and reasoning on/off control as trained capabilities.
The toggle is a training outcome, not a serving flag bolted on afterwards.The 2025–26 literature measures this directly. OckBench finds same-size 7B reasoning models with similar accuracy differing 3.3× in tokens and 5× in latency — the “overthinking tax”. Work decomposing reasoning efficiency across 25 models finds accuracy and token-cost rankings only loosely aligned (Spearman ρ ≈ 0.63), with verbalisation overhead varying roughly 9× and only weakly tied to model scale.
Token efficiency is a selection criterion in its own right. Benchmark it.Frontier dollars-per-benchmark-point fall roughly 5–10× per year, but algorithmic efficiency improves only about 3× per year — and roughly half of recent frontier progress on GPQA-Diamond was bought with additional inference spend rather than better algorithms. Analyses of cost-of-pass — expected dollars per correct solution — further find that inference-time techniques like majority voting and self-refine rarely justify their cost.
Translation for your planning: when a vendor shows you a reasoning model beating a non-reasoning one, ask whether the comparison was token-controlled. Usually it was not. S7 makes this a gate condition.
enable_thinking a routing decisionYou already have an intelligent router that classifies request complexity and picks a model. Thinking is one more field in that same decision — and unlike model selection, it is free to change per request with no extra weights loaded.
thinking_router.py — extends your existing IntelligentRouterfrom dataclasses import dataclass @dataclass class Route: adapter: str # "fintech-dpo" | "fintech-sft" thinking: bool max_tokens: int reason: str # thresholds are MEASURED, not guessed — see the exercise below VERIFIABLE_TASKS = {"reconcile_totals", "extract_figures", "cross_foot"} def route(req) -> Route: # 1. latency-bound paths never think. silence is not free for a user. if req.sla_ms < 10_000: return Route("fintech-dpo", False, 512, "latency SLA") # 2. verifiable tasks think, because we can PROVE the benefit if req.task in VERIFIABLE_TASKS: return Route("fintech-dpo", True, 2048, "verifier-backed uplift") # 3. batch/backfill paths think — throughput-bound, silence costs nothing if req.channel == "batch": return Route("fintech-dpo", True, 2048, "offline batch") # 4. everything else: off. the default is the cheap default. return Route("fintech-dpo", False, 512, "default off")
Note max_tokens moves with the toggle. An unbounded thinking budget is how you end up on the right-hand side of the overthinking curve; a hard cap is the crudest possible version of the oracle-stopping result, and it costs nothing to implement.
import time, json, statistics as st def trial(prompts, thinking: bool, budget: int): correct, tokens, wall = 0, 0, [] for p in prompts: t0 = time.time r = client.chat.completions.create( model="fintech-dpo", max_tokens=budget, temperature=0, messages=[{"role": "user", "content": p.text}], extra_body={"chat_template_kwargs": {"enable_thinking": thinking}}) wall.append(time.time - t0) tokens += r.usage.completion_tokens correct += verify(r.choices[0].message.content, p.gold) # YOUR verifier n = len(prompts) gpu_s = tokens / 934.0 # your measured L4 throughput cost = gpu_s / 3600 * 0.45 return {"accuracy": correct / n, "tokens_per_req": tokens / n, "p50_latency_s": st.median(wall), "cost_per_1k": cost / n * 1000, "cost_per_correct": (cost / max(correct, 1))} for budget in (256, 512, 1024, 2048, 4096): print(budget, trial(golden_verifiable, True, budget)) print("off", trial(golden_verifiable, False, 512))
Read the cost_per_correct column, not the accuracy column. That is the whole discipline of this session: an accuracy improvement that costs 48× per correct answer is not an improvement, it is a purchase you would not have authorised if anyone had shown you the invoice.
thinking=true|false and the routing reason. Without the tag you cannot slice anything.Take 100 golden-set items where you have a programmatic verifier. Run the budget sweep above. Plot cost_per_correct against budget and find your peak — then set max_tokens for the thinking route to roughly 1.5× the peak budget.
Then do the more uncomfortable version: run the same sweep on 100 items where you have no verifier, only a judge. Compare the shape of the two curves. If the judge-scored curve keeps rising where the verifier-scored curve has already turned over, you have just caught your judge rewarding long reasoning traces for their own sake — length bias, in your evaluation, on your own data. That is exactly the failure S7 is built to prevent, and finding it yourself first is worth more than reading about it.
Every claim in this session — the DPO adapter is better, thinking is worth it, this checkpoint is ready — is a measurement claim, and measurement is where post-training quietly goes wrong.
Final session: win rates and pairwise evaluation, why cross-lab benchmark comparisons are close to meaningless, contamination and saturation, and the gate your own release has to pass. Then the capstone that ties all seven sessions into one plan.
Your fine-tuning stage taught you to measure accuracy against a golden set. That instrument was correct for rung 1 and is close to blind for rung 2 — because preference tuning changes which of several acceptable answers you get, and an exact-match accuracy metric cannot see a difference between two answers it both marks correct.
The instrument that can see it is the same one that generated your training data: a pairwise comparison. Which is a pleasing symmetry and a serious hazard, because it means your eval and your training data now share a bias surface. Get this wrong and you build a model that games your judge, then use that judge to certify it.
Hover each. One catches breakage, the other catches progress. You need both, and they answer different questions.
A win rate is the fraction of prompts on which a judge prefers model A's answer to model B's. That is it. Same prompt, two answers, pick one, count.
Now notice where you have seen this before. It is the same object as a preference pair from S2 — the only difference is what you do with the label. Feed it to a loss and it is training data; count it and it is an evaluation. And when you aggregate many such comparisons across many models and fit hidden strengths, you get Bradley-Terry again — which is exactly how arena leaderboards produce their ratings.
Three uses of one statistical object:
The hazard is equally symmetric. If the judge that labels your training pairs has a length bias, and the judge that computes your win rate has the same length bias, then your model will learn to be longer and your evaluation will congratulate it. The two instruments are correlated in exactly the direction that hides the failure.
Post-training evaluation went through three distinct phases, and the current best practice is a residue of all three.
| Era | What it measured | representative | What it got wrong |
|---|---|---|---|
| 1 · Early chat | Chat quality relative to a strong reference model, scored by LLM judges standing in for humans | MT-Bench · AlpacaEval · Arena-Hard | Narrow. Rewarded style; conflated verbosity with quality until length control was retrofitted. |
| 2 · Multi-skill | Knowledge, reasoning, maths, code, instruction-following, safety as separate axes | the Tülu suite: MMLU, GSM8K, HumanEval, IFEval … | Saturated. Frontier models now cluster above 90% on most of it. |
| 3 · Reasoning & tools | Hard knowledge, real software engineering, competition maths | GPQA Diamond · SWE-Bench · LiveCodeBench · AIME | Expensive, high-variance, and increasingly confounded by inference-time compute. |
These are your eval stage's mitigations, now load-bearing for a release decision. Position: swap and require agreement. Verbosity: length-control the metric, or normalise. Self-preference: the judge should not be from the same family as either checkpoint if you can manage it — and if both checkpoints are Gemma, at least the bias applies symmetrically. Variance: temperature 0.
This is the most useful cynicism in the material, and it is well-evidenced.
Evaluations inside model announcements can only be compared to other press releases with large error bars, because the internal process is neither controlled across models nor documented. Concretely, the Olmo 3 work found that most post-training evaluations in the reasoning era carry standard deviations between 0.25 and 1.5 points with the evaluation setup held constant — and larger swings come from merely changing prompts or sampling parameters.
So: a model “slightly better” than another on a press-release table should be treated as equivalent. There are also persistent rumours of custom per-benchmark prompts for headline evaluations like GSM8K or MATH.
The structural point: when results are shared you get the outputs of a function — the numbers — without the inputs, which are sensitive configurations that differ at every lab. Even fully open evaluation standards are hard to guarantee reproducibility on. The only repeatable evaluation is the one you run yourself, on your own models, with your own harness pinned.
As a benchmark approaches 100%, progress slows because only the hardest — and often the mislabelled — items remain, so it stops carrying signal. MMLU and HumanEval are the clean examples: frontier models cluster above 90%, so a two-point gap tells you essentially nothing about which model to pick. GPQA Diamond remains useful precisely because the frontier still spans a wide band on it.
What this means for you: your golden set will saturate too. If your DPO checkpoint scores 97% on your programmatic checks, that metric has stopped being a decision input and become a regression guard. Build the next set from the items your current model fails.
Different groups choose different evaluations to keep independent as true test sets, and nobody discloses which. MATH and GSM8K both ship training sets whose prompts can be used to lift scores directly — and if a lab is not tracking that benchmark internally, training on its high-quality training data is a perfectly rational decision.
The capability labs are actually buying by improving evaluations internally is statistical power: reducing noise on their prioritised signals so they can tell two training runs apart. That is precisely what you are doing in the sample-size arithmetic below, at a smaller scale.
Models improve by spending more tokens at inference. Controlling evaluation scores by total inference tokens is important — and not yet common practice. So a reasoning model beating a non-reasoning model on a leaderboard may be reporting a compute purchase rather than a capability difference.
Straight from S6: when you compare your thinking and non-thinking routes, report tokens alongside accuracy or the comparison is not a comparison.
Prompt formatting alone can collapse performance. And training-data formats conflict: NuminaMath puts answers in \boxed{XYZ} while MetaMath uses “The answer is: XYZ”, and training on both can be worse than training on either. Strong models handle multiple formats but still have a strongest one.
Directly relevant: your analyst adapter has one output format. Evaluate it in that format, and if you ever change the format, re-baseline everything.
Dataset contamination is evaluation data leaking into training data; decontamination is the search-and-remove process. The standard method is n-gram overlap between training prompts and evaluation prompts, or fixed-length character substring matching.
When the Tülu 3 team decontaminated their evaluation suite using 8-gram overlap from training prompt to evaluation prompt, they found popular open datasets were already contaminated with popular RLHF evaluations: UltraFeedback with TruthfulQA, Evol-CodeAlpaca with HumanEval, NuminaMath with MATH, and WildChat with safety evaluations.
These are the datasets everyone uses. If you pull any public preference set into your mix, assume it is contaminated with something and check.
Models can be trained on data near a benchmark — same problem wording, different numbers — producing behaviour that is hard to explain and hard to prove. The most unsettling symptom: benchmarks improving when models are trained with RL on random rewards, a contrived setup that should only help if certain contamination is present. This has been a substantial confounder on early RLVR work built on Qwen 2.5 and Qwen 3 base models.
Detection tool: perturbation benchmarks — regenerate the benchmark with slightly altered questions and see who drops. High variance is not proof, but it flags models trained to a specific format.
You control the entire data pipeline. Your prompts come from your own traffic, and your golden set is carved from the same pool. So decontamination is a single deterministic step you can actually complete: hash your golden-set prompts, run 8-gram overlap against every training prompt, drop the hits, and record the count in your run metadata.
The failure mode to actually fear is subtler and specific to this stage: your golden set and your preference set are drawn from the same traffic and labelled by the same judge. Different prompts is necessary but not sufficient — you also need a judge whose biases you have measured, or you have simply built a closed loop.
The single most useful piece of arithmetic in this session. You want to know whether your DPO adapter beats SFT-only by 5 points — 55% vs 50%. How many comparisons do you need before that claim means anything?
The arithmetic above already assumes a paired design — both checkpoints answer the same prompt — which is why it is a one-sample test against 0.5 rather than a two-sample comparison. That pairing is doing real work; an unpaired design at the same sample size would have visibly wider intervals. Two further, free improvements: drop the ties rather than splitting them (they carry no information, exactly as in S2's saturation analysis), and stratify your golden set by task type so that the win rate you compute is not dominated by whichever task happens to be most frequent in your traffic.
Chatbot Arena → LMArena → rebranded to Arena in January 2026, spun out as a for-profit (Arena Intelligence Inc.) with a reported nine-figure Series A. Top-of-leaderboard Elo rose from ~1,094 in May 2023 to over 1,500 by early 2026.
The methodology moved with it. The addition of Style Control and the rebrand shifted Elo distributions by 20–40 points for some models purely because formatting verbosity was penalised differently — no change in model quality involved. Procurement documents written across that window silently encoded methodology drift as performance signal.
Practical rule: anchor comparisons to a methodology version, not a date.Analyses of arena dynamics found providers could test many private variants in parallel — up to 27 in a single month in the period studied — and publish only the best, turning the leaderboard into a multiple-comparisons search where the winner is partly the luckiest draw. Proprietary models were sampled in more battles and de-listed less often than open-weight ones. And prompts repeat: 7.3% of December 2024 prompts reappeared verbatim in January 2025, rising to about 9% by semantic similarity.
The platform disputed parts of the framing, and that exchange is healthy. The structural point survives: any leaderboard with stakes attached is no longer a clean measurement.
Same Goodhart, applied to the measurement layer instead of the model.Scale's research team hand-wrote a brand-new grade-school maths test in the exact style of GSM8K, designed to be indistinguishable in difficulty. If a model had learned arithmetic, the two scores should match. For several model families they did not — some dropped by as much as 13 points, and the size of the drop correlated with how often a model would spontaneously regurgitate verbatim GSM8K problems.
Modern practice for models that do not disclose training data uses the same idea: create perturbed versions of a benchmark and see who falls.
You can run this trick yourself — rewrite 50 golden-set items and compare.Open evaluation frameworks worth knowing: Inspect AI (UK AI Safety Institute), LightEval (Hugging Face, powered the Open LLM Leaderboard), lm-evaluation-harness (EleutherAI, with a well-curated GPT-3-era setup), OLMES (Ai2), HELM (Stanford CRFM), and Databricks' Eval Gauntlet. For a narrow single-task adapter your own harness plus Langfuse is likely enough — reach for these when you need standardised public benchmarks alongside your golden set.
Everything from seven sessions, assembled into one runnable sequence against your actual infrastructure.
Hover any stage. Note how little of this is new infrastructure — the work is data and measurement.
Print this. It is the gate, in order, and the ordering is the point.
A short run record, because in six months you will not remember and neither will anyone else. Six lines:
trl, transformers, peft, bitsandbytes.rewards/margins and rewards/accuracies, and whether the eval callback showed any schema drift.That record is also your defence against the cross-lab-comparison problem from earlier in this tab, applied inward: the only reproducible evaluation is one whose inputs you wrote down.
Run the whole loop once, end to end, at the smallest scale that is still honest: 1,000 preference pairs, one DPO run at β=0.1, and a 1,000-prompt win-rate gate against SFT-only. Fill in the six-line run record. Then do the one thing that separates a completed exercise from a working pipeline: take the golden-set items where the DPO adapter lost, and feed them back into stage 1 as next round's prompts.
Iterating on the data with a fixed algorithm is the loop that Olmo 3's team described as making DPO worth using despite its critics — cheap, stable, and highly iterable. You now have that loop, and it runs for about a quarter of a dollar per turn.
The model now behaves — it follows instructions, it prefers the better answer, and you can prove the improvement with a win rate that has a confidence interval attached.
Giving it tools and autonomy is the next stage (agents). Note what carries forward: GRPO reappears as the optimiser for multi-turn tool use, verifiers become environments, and the reward-hacking section you just read gets considerably more interesting once the model can take actions in the world rather than only emit text about it.