genaipros← The path
Line A · Model03 · Post-Training & Alignment
Curriculum stage · post-training & alignment

Making a model prefer good answers

RLHF, reward models, PPO & GRPO, direct alignment, reasoning, and the evaluation that gates it all.

01 · Imitate — SFT
02 · Prefer — RLHF / DPO
03 · Achieve — RLVR

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.

The frame

Three verbs, one ladder

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.

IMITATE Reproduce good answers · SFT / instruction tuning · per-token cross-entropy Signal: one target string. Feedback: positive only. — your completed stage PREFER Of two answers, reliably produce the better one · RM + PPO/GRPO · DPO & friends Signal: a pair (chosen, rejected). Feedback: contrastive, response-level. — S2 · S3 · S4 · S5 ACHIEVE Optimise against verifiable correctness · RLVR · reasoning models Signal: a verifier that returns 1 or 0. Feedback: outcome truth, not taste. — S6 ↑ each rung needs the one below it to already work
Hover or click any rung. The ladder is not a ranking of quality — it is a ranking of what kind of signal you have. Pick the highest rung your signal can actually support.
Read the rungs

Three verbs, bottom to top. The rung you can climb to is decided by your data, not your ambition.

Why the verb matters more than the acronym

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.

The map

The canonical pipeline — and the shortcut through it

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.

STAGE 0 — PRETRAIN RUNG 1 — IMITATE RUNG 2 — PREFER (the long way) RUNG 2 — PREFER (the shortcut) RUNG 3 — ACHIEVE Base LM next-token predictor SFT / IFT chat template, masking ◀ you are here Preference pairs (x, y_chosen, y_rejected) S4 · S5 Reward model Bradley-Terry head S2 RL loop PPO / GRPO S3 DPO / direct alignment same objective · no RM · no rollouts · S4 + S5 lab Aligned policy ships to serving RLVR + verifier reward = 1 if correct, else 0 · S6 Eval gate — win rate every arrow above must pass it · S7
Nine hotspots. Solid arrows are the InstructGPT pipeline; the dashed crimson arc is the direct-alignment bypass; the gold dotted line is the gate everything must pass.
Explore the map

Hover or click any box. The dashed crimson arc is the route your single L4 can actually run end-to-end.

The villain

Goodhart is the antagonist of every session

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.

OPTIMISATION PRESSURE → (KL distance from the reference policy) SCORE the checkpoint you actually wanted early stopping lives here proxy reward: still climbing true quality: falling Both curves are honest. Only one of them is the thing you care about.
The recurring shape of over-optimisation. Note this is not overfitting: the model genuinely got better at the proxy, on held-out data. The metric itself was never quite right.
Length bias

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, S4
Sycophancy

Agreeing 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, S4
Numerical exploits

Repeating rare tokens, formatting tics, structural quirks that happen to inflate a scalar head. Nothing about these is semantic; they are artefacts of RM training.

→ S3
The regulariser is the whole game

Every 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.

Honest scope

What you will run, and what you will only understand

You will actually run this
  • A judge-scored preference dataset
  • A full QLoRA DPO pass on your SFT'd Gemma, on one 24 GB L4, using the reference-free LoRA trick
  • Reading DPO training curves and diagnosing the failure smells
  • A win-rate eval gate against your SFT-only baseline on your golden set
  • Multi-LoRA A/B serving of the DPO adapter next to the SFT adapter
You will understand but not run
  • A PPO farm — four model copies will not fit, and that is fine (S3 does the arithmetic)
  • Large-scale GRPO / RLVR runs — the frontier recipes need clusters, not a single L4
  • Training a production reward model from scratch — S2 argues you should not, and why
  • Full reasoning-model training — S6 prices it instead, using your own serving numbers

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.

Sources

Course spine, and where the material has already moved

the source material is a leading alignment researcher's the RLHF literature (living edition). the section numbering below is the canonical map:

Sessionthe source material sectionNuggets pulled in
S11 Overview · 3 Training Overview · 4 Instruction Fine-tuning (bridge-skim only)
S25 Reward Modeling10 Nature of Preferences
S36 Reinforcement Learning15 Regularization · 16 Over Optimization
S48 Direct Alignment Algorithms · 11 Preference Data12 Synthetic Data · 9 Rejection Sampling
S5the source material ch. 10 (Liu) as lab skeleton, modernisedthe source material 8.3 implementation notes
S67 Reasoning & Inference-Time Scaling9.3 Best-of-N
S717 Evaluation16 Over Optimization
living edition vs the live source — drift found

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:

  • §12 has grown a whole new spine. Your PDF's Synthetic Data section runs Distillation → AI Feedback → Constitutional AI → Rubrics. The live version inserts a substantial new section, “The Path to On-Policy, Teacher-Student Distillation”, covering on-policy distillation (OPD), multi-teacher OPD, and on-policy self-distillation — citing MiMo-V2-Flash (Jan 2026), GLM-5 (Feb 2026), DeepSeek-V4-Pro, and Cursor's Composer 2.5 (May 2026). None of that is in your PDF.
  • Internal cross-references have slipped. The live §12 points to “§16” for evaluation and “§17” for character training; your living edition has 17 = Evaluation and 18 = Product, UX & Model Character. The spine numbering is the canonical map; older in-text pointers may drift.

Where a source claim has aged, the tab says so explicitly and gives the current replacement.

Navigate

The seven sessions

End goal for the stage

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.

1 · why this session exists

Following instructions is not the same as being good at them

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:

PROMPT “Flag compliance risk in this earnings call excerpt and return the analyst schema.” COMPLETION A — schema-valid {"sentiment":"neutral", "risk_score":0.5, "compliance_flags":[], "evidence":""} Parses. Says nothing. Nobody can act on it. COMPLETION B — schema-valid {"sentiment":"cautious", "risk_score":0.78, "compliance_flags":["fwd_looking_no_sh"], "evidence":"we expect margins to…"} Parses. Committed. Auditable. Your SFT loss gave both of these the same treatment: “maximise the probability of the target string.” Neither was ever compared to the other. Preference tuning is the machinery for making that comparison a gradient.
Two outputs that a schema validator cannot separate and a per-token cross-entropy loss cannot rank. Hover each.
The gap

Same schema, same shape, wildly different usefulness. SFT is blind to the difference.

2 · core concepts

What post-training actually is

The vocabulary, before anything else

Every term below is used constantly for the rest of the stage. Nothing later assumes you already know one of these.

Policy

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.

Reference policy

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.

Reward model (RM)

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.

Rollout

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.

KL divergence

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 / off-policy

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.

Post-training is three optimisation families, not one technique

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.

FamilyWhat it teachesUnit of feedbackVerb
IFT / SFT
instruction / supervised fine-tuning
Formatting and the base of instruction-following. Largely about learning features in language.per tokenimitate
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, contrastiveprefer
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, verifiedachieve
Why RLHF generalises better than SFT

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.

The elicitation interpretation

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.

Three passes on the RLHF problem formulation

Pass 1Intuition — a leashed apprentice

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.

Pass 2Mechanism — three departures from reference text RL

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 RLRLHF for language modelsConsequence
Reward is a fixed function baked into the environmentReward is a learned model you train and controlEnormous flexibility — and the proxy problem
Actions change the state; dynamics carry you forwardNo state transitions. Initial state = a prompt from a dataset; the action = the whole completionThe discount factor and horizon mostly vanish
Reward arrives per stepResponse-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.

In words: pick the policy that gets the highest average reward on your prompt distribution, while staying close to the model you started from.
maxπ   𝔼x∼𝒟, y∼π(·|x) [ r(x,y) ]   −   β · DKL( π(·|x) ‖ πref(·|x) )
x = prompt y = completion r = reward model · πref = frozen SFT checkpoint · β = how tight the leash is

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.

Pass 3Trade-offs — what the formulation quietly assumes
  • That one scalar can represent “better”. It cannot, fully. Helpfulness, harmlessness and honesty conflict; collapsing them into one number forces a trade-off you never explicitly chose.
  • That the reference policy is a good place to be anchored to. If your SFT model has a systematic flaw, the KL penalty actively defends that flaw.
  • That response-level reward is enough signal. For a 900-token JSON output, one scalar has to explain which of 900 token choices was good. This is why process rewards (S2) and verifiable rewards (S6) exist.
  • That the prompt distribution 𝒟 matches production. Optimise on prompts that do not look like your traffic and you improve a model nobody uses.

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.

The bridge from your fine-tuning stage — only what the literature adds

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.

Canonical recipes — how the field's answer changed three times

Step through the three recipes that define the eras. Notice what stays constant: SFT is always first, and evaluation always gates the end.

Recipe evolution · 2022 → 2025
2022 · InstructGPT
Three steps, and RLHF is the centrepiece

The recipe that produced ChatGPT and defined the field's mental model for two years.

  1. Instruction tuning on ~10K examples — teaches the question-answer format and basic skills, from primarily human-written data.
  2. Reward model on ~100K pairwise prompts — trained from the instruction-tuned checkpoint; this becomes the optimisation target.
  3. RLHF on another ~100K prompts — the instruction-tuned model generates, the RM rates, PPO updates.

Note the ratio: 10× more preference data than SFT data. Preference data was the expensive, decisive artefact from day one.

2024 · Tülu 3
Many more stages, much more data, RLVR appears

The fully-open recipe that showed what modern multi-stage post-training looks like.

  1. Instruction tuning on ~1M examples — primarily synthetic, mixed from frontier models; the foundation for maths, code and general instruction following.
  2. On-policy preference data, ~1M pairs — substantially boosts chat quality (ChatBot Arena / AlpacaEval-style) while improving the SFT-stage skills too.
  3. RLVR on ~10K prompts — a small RL run to boost verifiable skills like maths while holding overall performance. In hindsight, a precursor to reasoning models.

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.

2025 · DeepSeek-R1
Compute gets re-ordered around a giant RL run

Reasoning models did not add a stage so much as change which stage is the centre of gravity.

  1. “Cold start” on 100K+ on-policy reasoning samples — sampled from an earlier RL checkpoint (R1-Zero) and heavily filtered to instil a specific reasoning process.
  2. Large-scale RL training — RLVR run repeatedly over reasoning problems, “until convergence” across benchmarks.
  3. Rejection sampling — ¾ reasoning problems, ¼ general queries, beginning the transition back to a general-purpose model.
  4. Mixed RL — verifiable rewards on reasoning plus general preference-tuning reward models to polish.

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.

2025–26 · the fully-documented version
Olmo 3: SFT → DPO → RLVR, with the whole flow published

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:

  • The Think variant's DPO stage pairs completions from Qwen 3 32B as chosen and Qwen 3 0.6B as rejected — a deliberate strong/weak pairing rather than a judge on same-model samples.
  • Its RLVR stage filters out prompts the base model already solves 8/8 times, leaving ~13.3K decontaminated maths prompts. Difficulty filtering is not a detail; it is most of the recipe.

This is the shape your own capstone will take, one order of magnitude smaller.

What post-training can and cannot fix

Can fix
  • Style, tone, verbosity, hedging, format discipline
  • Reliability of a behaviour the base model can already produce sometimes
  • Refusal boundaries and safety posture
  • Calibration of a scored output — committing to 0.78 rather than 0.5
  • Verifiable skills, given a verifier (RLVR)
Cannot fix
  • Knowledge the base model does not have — that is retrieval or pretraining
  • A capability absent from the base distribution entirely
  • A bad SFT starting point — RLHF needs a strong prior to work from
  • A prompt distribution that does not match production
  • Ambiguity in what you actually want; if you cannot label it, you cannot train it
3 · reality check

What the pipeline costs, in numbers

Worked example — the price of your preference data

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.

The question: you want 5,000 preference pairs for a fintech DPO run. What does each sourcing route cost, and what do you give up?
route A — human annotators, generalist crowd 5,000 pairs × $1.00 = $5,000 turnaround: weeks …but "compliance risk in an earnings call" is not generalist work route B — human annotators, domain experts 5,000 pairs × $10.00 = $50,000 turnaround: months low noise on the axis you care about; the frontier-lab moat route C — frontier LLM judge (external API) 5,000 pairs × ~$0.008 = $40 turnaround: hours low noise, HIGH BIAS — position, verbosity, self-preference route D — your own Gemma as judge (enable_thinking=true) ~5,000 × ~1.5k decode tok on your own L4 ≈ 7.5M tokens ÷ 934 tok/s ≈ 2.2 GPU-hours 2.2 h × $0.45/h = $1.00 turnaround: hours cheapest, noisiest, and self-preference is now UNAVOIDABLE route E — programmatic checks where they exist (schema, numeric bounds, citation-span present, key coverage) = $0 zero bias, narrow coverage
The cost ratio between route B and route D is 50,000×. That single number explains why almost every open recipe you will read uses AI feedback, and why frontier labs still treat human preference data as a competitive moat. the literature's rule of thumb: human data is high-noise, low-bias; synthetic preference data is low-noise, high-bias.
The design that falls out of this

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.

Worked example — reading a KL number

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.

In words: KL is the average, over tokens the policy actually generated, of how much more likely the policy thinks that token is than the reference does.
DKL(π ‖ πref)  ≈  𝔼y∼π [ log π(y|x) − log πref(y|x) ]
policy generates 3 tokens; we score them under BOTH models token log π_θ log π_ref difference "cautious" -0.22 -1.90 +1.68 "0.78" -0.51 -2.40 +1.89 "fwd_look" -0.90 -1.05 +0.15 sum = 1.68 + 1.89 + 0.15 = 3.72 ← sequence-level KL mean = 3.72 / 3 = 1.24 nats/token reading it: the policy now assigns e^1.68 ≈ 5.4× more probability to "cautious" than the SFT model did. It has learned something. It has also drifted. ~1.2 nats/token is a LOT of drift for a preference run. In practice you watch this number and stop before the true-quality curve turns over.
Two subtleties worth knowing now. This is the reverse KL — sampled from the policy, scored under the reference — which penalises the policy heavily for putting mass where the reference assigns low probability. And it is a Monte-Carlo estimate, not the exact divergence; exact KL over the full vocabulary at every position is expensive, so every framework uses an approximation.

Three things that are true this year and were not when the section was drafted

TRL became a v1.0 product

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.

Pin your versions. Trainer signatures and Config fields shift across minor releases.
The open frameworks split by scale

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.
The three-stage recipe is now the open default

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.
Sources for this tab's grounding.
  • TRL releases and version history — github.com/huggingface/trl/releases; TRL v1 announcement, 27 Mar 2026.
  • Human vs AI feedback cost figures — the literature, RLHF field text (live edition, rlhfbook.com/c/12-synthetic-data).
  • Olmo 3 post-training recipe and DPO pairing — Ai2 Olmo 3 release notes and technical report, Nov 2025 onward.
  • Nemotron 3 Nano post-training — NVIDIA technical report, arXiv 2512.20848.
  • NVIDIA Molt — open-sourced 22 July 2026.
4 · apply to my stack — lab

Where your existing pieces sit on the map

No training code yet — this session's lab is an inventory. You already own more of this pipeline than you probably realise.

Pipeline elementWhat you already haveGap for this stage
Policy π_θQLoRA-SFT'd Gemma E4B fintech adapter — strict-JSON analyst outputsnone — this is the thing being improved
Reference π_refThe 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 constructiondedupe + decontaminate against the golden set
Candidate generatorYour vLLM endpoint. Sample n>1 per prompt at temperature > 0.a sampling script; prefix caching makes this cheap
Preference labellerProgrammatic format checks + calibrated LLM judge with position/verbosity/self-preference mitigations from your eval stagewire it to emit chosen/rejected instead of scores
TrainerTRL, known from SFTTrainerDPOTrainer + DPOConfig — S5
Eval gateGolden dataset, statistical sizing, online eval in Langfusereframe accuracy metrics as a win rate — S7
Job executionK8s + Kueue on spot GPUs, or Modal (your gemma_modal.py pattern)choose one — S5 argues it
ServingvLLM multi-LoRA on L4, FP8, prefix caching, Prometheus + Langfuseadd the DPO adapter alongside the SFT one
The one uncomfortable observation

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.

Optional exercise

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.

Bridge → S2

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.

1 · why this session exists

You cannot write the loss function you actually want

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.

Write r(x,y) by hand impossible for taste gameable when attempted Collect comparisons “A is better than B” no rule required Fit a scalar r(x,y) Bradley-Terry comparisons → numbers A reward model is a language model with its head cut off and replaced by a single number. It is also, unavoidably, a proxy — and everything that goes wrong later goes wrong here first.
The swap that makes RLHF possible: stop specifying quality, start comparing outputs.
The swap

Three boxes, one idea: comparisons are cheap, rules are impossible, and Bradley-Terry converts one into the other.

2 · core concepts

From a pair of strings to a gradient

The atom: one preference pair

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.

Notation for the rest of the stage

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”.

Bradley-Terry in three passes

Pass 1Intuition — chess ratings for sentences

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:

  • Only differences matter. Add 100 to every strength and every predicted outcome is identical. A reward model's absolute scale is meaningless; comparing raw reward numbers across two different RMs is nonsense.
  • Big gaps saturate. Once A is much stronger than B, making it stronger still barely changes the prediction. The model stops learning from pairs it already gets right — which is why easy pairs are nearly free training signal and hard pairs are where the information is.
Pass 2Mechanism — the model, the loss, the head
In words: the chance a rater prefers i over j equals i's strength divided by the total strength of both.
P(ij)  =  pi / (pipj)
Reparametrise with unbounded scores by writing pi = exp(ri). Strengths must be positive; scores need not be, and unbounded scores are what a neural net naturally emits.
P(ij)  =  eri / (eri + erj)  =  σ(rirj)
σ is the logistic sigmoid, σ(z) = 1/(1+e−z). The whole model is “sigmoid of the score gap”.

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 words: push the chosen completion's score above the rejected one's, and be punished in proportion to how badly you got the ordering wrong.
RM  =  − 𝔼(x,yc,yr) [ log σ( rθ(x,yc) − rθ(x,yr) ) ]

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.

Two implementation facts that cause real bugs

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.

Pass 3Trade-offs — the variants, and why none of them won

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.

Four things called “reward model” that are not the same thing

This is the single most confusing terminology in post-training, partly because the term ORM is used inconsistently in the literature. Step through them.

What predicts what, and when
Type 1
Reward model (Bradley-Terry RM)

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.

Type 2
Outcome reward model (ORM)

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.

Type 3
Process reward model (PRM)

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.

Type 4
Value function

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.

Type 5
Generative reward model / LLM-as-judge

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.

Where the proxy cracks

This is the Goodhart section for reward models. Each of these is a real, documented way a reward model scores something other than quality.

Length bias

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 disentangling
Sycophancy

Over-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 labeller
Prefix bias

The 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 EOS
Self-preference

LLM 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 size
Formatting habits

Markdown 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 judging
Distribution shift

The 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 stopping
Nuggets from §10 — why this is unfixable, not just unfixed

The 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:

  • “Chosen” does not mean correct. It means better relative to the alternative shown. Both completions in a pair can be wrong, and the model can still learn from well-labelled data. Do not read your chosen column as ground truth.
  • The interface shapes the preference. How the comparison is presented changes what gets chosen — and it is more art than science which interface produces which bias.
  • Disagreement may be signal, not noise. When two annotators disagree, the standard move is to treat it as noise and majority-vote. Whether that is right is genuinely open.
3 · reality check

Bradley-Terry on paper, with real numbers

Worked example — two toy reward scores

Setup: your reward model scores completion B (the committed, evidenced one) at r = 1.2 and completion A (the empty-evidence one) at r = 0.4. What does the model believe, and what does it cost?
score gap Δ = r_c − r_r = 1.2 − 0.4 = 0.8 P(B ≻ A) = σ(0.8) = 1 / (1 + e^-0.8) = 1 / (1 + 0.4493) = 0.690 loss on this pair = −log σ(0.8) = −log(0.690) = 0.371 nats --- now the invariance check --- add 5 to BOTH scores: r_c = 6.2, r_r = 5.4 Δ = 0.8 → P = 0.690 → loss = 0.371 IDENTICAL absolute reward values carry no information. only gaps do. --- now the saturation check --- Δ = 0.0 P = 0.500 loss = 0.693 ← maximum confusion, maximum gradient Δ = 0.8 P = 0.690 loss = 0.371 Δ = 2.0 P = 0.881 loss = 0.127 Δ = 4.0 P = 0.982 loss = 0.018 ← nearly free; almost no gradient left Δ = 8.0 P = 0.99966 loss = 0.00034 ← this pair teaches the model nothing --- and the wrong-way case --- Δ = −0.8 P = 0.310 loss = 1.171 ← 3.2× the loss of the correct case the model is punished hard for confidently ordering a pair backwards
What to take from this. Loss 0.693 (= log 2) is the “I have no idea” point; a reward model that never gets below it has learned nothing. And the saturation column explains a data-design rule: pairs that are obviously different are cheap but uninformative. If your judge separates every pair by a mile, your dataset is easier than your problem.
Carry this forward — you will meet σ(Δ) three more times

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.

Worked example — how many comparisons does a reward model need?

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.

your realistic budget, fintech domain: pairs available from one week of traffic ~4,000 prompts × 2 samples pairs after programmatic checks settle it ~2,400 decided for free pairs needing a judge ~1,600 × $0.008 = $13 ratio to InstructGPT's RM data 4,000 / 100,000 = 4% ratio to Tulu 3's preference data 4,000 / 1,000,000 = 0.4% verdict: this is nowhere near enough to train a general reward model. It is entirely enough to run DPO on a NARROW, single-task policy — which is exactly what a fintech JSON analyst adapter is.
The scale gap is the honest argument for skipping RM training. General reward models need general-scale preference data. Your task is narrow, so your data can be narrow — but only if you skip the step that requires generality.

Three current findings the section predates or only gestures at

Reward hacking got a unifying theory

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”
Attacks moved below the semantic layer

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 exist
Human preference data is still the moat

The 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.
Sources.
  • Bradley-Terry formulation, RM loss, ORM/PRM/value comparison, generative RM prompt — the literature, RLHF field text living edition v2.
  • Nature-of-preferences nuggets — ibid.; preference-data biases.3.
  • Reward-hacking mechanisms survey — arXiv 2604.13602 (Apr 2026). Token-space RM attacks — arXiv 2604.02686 (2026).
  • RM benchmarks: RewardBench and RewardBench 2 (arXiv 2506.01937); HelpSteer2-Preference (ICLR 2025), HelpSteer3-Preference (arXiv 2505.11475).
4 · apply to my stack — lab

The reward model you should not train (and the one you should)

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.

Recommendation

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.

Route you will take: a composite scorer that emits pairs

scorer.py — programmatic first, judge only for the residue
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.

Route you are declining: a real RewardTrainer config

For completeness, so you recognise it in someone else's repo. TRL's RewardTrainer implements exactly the one-line loss above.

rm_train.py — the path not taken
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.

Optional exercise

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.

Bridge → S3

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.

1 · why this session exists

A score is not a gradient

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.

1 · Sample rollouts from π_θ prompt → completions 2 · Score reward model → r one scalar per rollout 3 · Baseline r − expected → A advantage, not reward 4 · Update −A · log π_θ clipped repeat — the policy has changed, so the rollouts must be regenerated running throughout: KL penalty against the frozen reference π_ref the leash — this is the difference between fine-tuning and destroying a model
Four steps and a leash. Every algorithm below is a different answer to step 3 and a different way of clipping step 4.
The loop

Sample, score, baseline, update — then throw the rollouts away, because the policy has moved.

2 · core concepts

Policy gradients, and the models it takes to run one

The core idea in three passes

Pass 1Intuition — reinforce what beat expectations

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.

Pass 2Mechanism — one equation, then the family tree
In words: nudge the parameters in the direction that raises the log-probability of actions that did better than expected, scaled by how much better.
θ J(θ)  =  𝔼 [ At · ∇θ log πθ(at | st) ]
At = advantage at step t · πθ(at|st) = probability the policy assigned to the token it actually emitted

In code, before any clipping, that is literally:

pg_loss = -advantages * ratio   # ratio = π_θ(token) / π_old(token)

Three cases, and they are worth walking:

  • A > 0 — the action beat expectations. The minus sign means the loss falls as the ratio rises, so the optimiser raises the log-probability of those tokens.
  • A < 0 — worse than expected. Loss falls as the ratio falls; the optimiser suppresses those tokens.
  • A = 0 — exactly as expected. Zero loss, zero update. Nothing to learn.

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?”

AlgorithmWhere the baseline comes fromStep controlextra models
Vanilla PG / REINFORCEBatch mean, or a moving average, or nothinglearning rate only0
RLOOMean reward of the other samples for the same prompt (leave-one-out)learning rate only0
PPOA learned value network, per token, usually via GAEratio clipping 1±ε1 (critic)
GRPOMean (and std) of rewards within a group of G completions to one promptratio clipping, KL in the loss0
GSPOSame as GRPOclipping on a sequence-level importance ratio0
CISPOSame as GRPOclip the importance weight, not the objective0

Read that table twice. The entire algorithmic literature of RLHF fits in two columns.

Pass 3Trade-offs — variance, bias, and what breaks at scale
  • Variance is the enemy. The gradient is estimated from a small number of noisy rollouts. Sparse rewards make it worse — if most samples score 0 or 1 with nothing in between, estimates scatter. Every baseline in that table is a variance-reduction device.
  • The advantage has the lowest theoretical variance of the standard choices, if you can estimate it accurately. That “if” is the entire argument for and against learned value functions.
  • Learning a value function from an LM backbone is not a solved problem. Best practices are not established. This is the honest reason GRPO caught on: not that group baselines are theoretically superior, but that they sidestep a component nobody knows how to tune.
  • Token-level importance ratios get numerically unstable on long sequences and sparse MoE models — a single token with a large ratio can dominate the update, or many tokens get clipped independently and fragment the learning signal across one response. GSPO and CISPO were both developed by organisations pushing RL on large MoE models for exactly this reason, and are correspondingly less impactful at small academic scale.
  • In practice the clipping often never fires. With language models, PPO and GRPO are frequently run with only one gradient step per batch — and clipping can only trigger when the policy has moved within a batch. So the PPO-native regularisation does nothing, and the KL penalty is doing all the work. Watch the clip_fraction metric; if it is zero, you are running REINFORCE with extra steps.

The PPO four-model dance

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.

FOUR MODEL COPIES IN MEMORY · ONE OF THEM RECEIVES GRADIENTS POLICY π_θ generates rollouts ← the only trainable one REFERENCE π_ref frozen SFT copy scores tokens for KL REWARD MODEL frozen, inference only one scalar at EOS VALUE / CRITIC per-token expected return also trained ← 2nd optimiser rollout tokens per-token reward r_t = 0 − β·KL_t + r_RM · 1[t = EOS] A_t = GAE(r_t, V(s_t)) reward is ZERO everywhere except EOS and the KL term clipped policy-gradient update flows back to π_θ only Memory: 4 model copies + optimiser state for 2 of them + activations + a KV cache for generation.
Five hotspots. The right-hand box is the detail most explanations skip — the per-token reward is zero everywhere except the KL term and the EOS position.
Four models

Two frozen, two trainable, and a generation pass on top. This is why PPO farms are farms.

PPO's clipping — what the min/max actually does

In words: take the normal policy-gradient objective, but if the update would move this token's probability more than ε away from where the data was collected, stop counting the extra.
JCLIP = 𝔼 [ min( ρt At ,   clip(ρt, 1−ε, 1+ε) At ) ] ,   ρt = πθ(at|st) / πθold(at|st)
ρ is the policy ratio. It starts at exactly 1.0 on the first gradient step of a batch (the policy is the collection policy), then drifts as you take more steps. Common practice: 1–4 gradient steps per batch.

The min looks arbitrary until you enumerate the four interesting cases. Work through them:

ε = 0.2, so the trust region is ρ ∈ [0.8, 1.2] POSITIVE ADVANTAGE A = +1.0 (this was good; make it more likely) ρ = 0.7 unclipped 0.70·A clipped 0.80·A min → 0.70 gradient flows ✓ ρ = 1.0 unclipped 1.00·A clipped 1.00·A min → 1.00 gradient flows ✓ ρ = 1.3 unclipped 1.30·A clipped 1.20·A min → 1.20 NO GRADIENT already 30% more likely than at collection time. do not over-reinforce it. objective is flat here. NEGATIVE ADVANTAGE A = -1.0 (this was bad; make it less likely) ρ = 0.7 unclipped -0.70 clipped -0.80 min → -0.80 NO GRADIENT already 30% LESS likely. do not over-suppress. ρ = 1.0 unclipped -1.00 clipped -1.00 min → -1.00 gradient flows ✓ ρ = 1.3 unclipped -1.30 clipped -1.20 min → -1.30 gradient flows ✓ still too likely — keep pushing it down.
The pattern: the clip only ever kills the gradient in the direction you have already moved far enough. Inside the trust region PPO behaves exactly like vanilla policy gradient — by design. Outside it, the objective goes flat.

GRPO — delete the critic, use the group

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.

In words: sample G completions per prompt, score them all, and set each one's advantage to its reward's z-score within the group.
Ai  =  ( ri − mean(r1..G) ) / std(r1..G)
prompt: "flag compliance risk in this excerpt" G = 8 completions, scored by a format-check + judge composite rewards = [0.9, 0.2, 0.8, 0.1, 0.9, 0.3, 0.7, 0.1] mean = 0.500 std = 0.3391 advantages: A₁ = (0.9 − 0.5)/0.3391 = +1.180 ← reinforce every token in this rollout A₂ = (0.2 − 0.5)/0.3391 = −0.885 ← suppress A₃ = (0.8 − 0.5)/0.3391 = +0.885 A₄ = (0.1 − 0.5)/0.3391 = −1.180 … no value network was consulted. no critic was trained. the baseline is just the other seven samples.
The bias hiding in that std divisor

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.

What GRPO saves

One full model copy in memory, plus its optimiser state, plus the unsolved problem of learning a value head on an LM backbone.

What GRPO costs

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.

Where the KL goes

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.

The KL leash, properly

Pass 2Mechanism — which KL, and how it is estimated

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:

DKLθ ‖ πref)  ≈  log πθ(y|x) − log πref(y|x)

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.

Double regularisation — and why one of them usually does nothing

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.

Loss aggregation — a “detail” that silently biases length

You have per-token losses. How do you reduce them to one scalar? Three answers, all in production use, and they do not agree.

seq A: 4 real tokens (of 7 padded) seq B: 7 real tokens per-token loss magnitude identical in both Strategy 1 — per-sequence mean (standard GRPO) each sequence contributes equally regardless of length gradient per token: A = 0.250 B = 0.143 → short sequences get LARGER per-token gradients Strategy 2 — per-token mean (DAPO) every token contributes equally gradient per token: A = 0.143 B = 0.143 → longer sequences exert proportionally more total influence Strategy 3 — fixed-length norm by max_len=7 (Dr. GRPO) gradient per token: A = 0.143 B = 0.143 equal per-token scale, longer sequences still contribute more total
Strategy 1 feels most natural — we care about outcomes, not tokens — but it introduces a length bias that can push the model to overthink or to under-use strategies that legitimately need more tokens, depending on the sign. Worth knowing: gradient accumulation can flip the balance, because gradients are summed across minibatches before the step.

Where the proxy cracks — over-optimisation in the RL loop

Everything in S2 about biased reward models becomes actively dangerous here, because RL is a strong optimiser pointed directly at those biases.

Over-optimisation ≠ overfitting

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 classic tells

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 over-refusal case study

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.

Where the error comes from

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.

3 · reality check

Why PPO does not fit on your L4, in arithmetic

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.

Setup: Gemma E4B — call it ~4B effective parameters for the arithmetic — on one L4 with 24 GB, bf16 weights, AdamW.
per-copy weight cost, bf16 (2 bytes/param): 4.0e9 × 2 B = 8.0 GB --- NAIVE FULL-FINETUNE PPO --- policy weights 8.0 GB policy gradients (bf16) 8.0 GB AdamW optimiser states (fp32 m + v = 8 B/param) 32.0 GB reference model (frozen, no grads) 8.0 GB reward model (frozen) 8.0 GB value network weights + grads + optim 48.0 GB activations + KV cache for generation ~4 GB ──────────────────────────────────────────────────────── TOTAL ≈ 116 GB on a 24 GB card 4.8× over budget --- QLoRA PPO, every trick applied --- policy base, NF4 4-bit (0.5 B/param) 2.0 GB LoRA adapter r=16 (~0.5% of params, bf16) 0.04 GB adapter grads + AdamW states 0.24 GB reference: FREE — disable the adapter 0 GB reward model, 4-bit 2.0 GB value network, 4-bit + its own LoRA + optim 2.3 GB activations, grad-checkpointed, bs=1 ~2 GB KV cache for generating 8 rollouts × 1k tok ~3 GB ──────────────────────────────────────────────────────── TOTAL ≈ 11.6 GB fits — technically --- but now the throughput --- each PPO step needs: generate G rollouts, 4 forward passes (policy, ref, RM, value), 2 backward passes. generation at bs=1 on a 4-bit L4: ~25 tok/s 8 rollouts × 800 tokens = 6,400 tokens = ~256 s of generation …per optimisation step. A 500-step run = ~36 hours of pure decode before counting a single backward pass.
The real verdict. QLoRA PPO fits and is useless. The blocker was never memory — it is that online RL puts an inference server inside your training loop, and a single L4 running 4-bit generation at batch size 1 is the worst possible inference server. This is why every serious RL framework (verl, OpenRLHF, NeMo-RL, TRL's own GRPO path) bolts vLLM onto the trainer and runs rollouts on separate GPUs.
Contrast — the same arithmetic for DPO, which is what you will actually run in S5:
policy base, NF4 4-bit 2.0 GB LoRA adapter + grads + AdamW 0.28 GB reference: FREE — disable the adapter 0 GB reward model: DOES NOT EXIST 0 GB value network: DOES NOT EXIST 0 GB activations: 4 forward passes per step (policy-chosen, policy-rejected, ref-chosen, ref-rejected) grad-checkpointed, bs=1, seq 1024 ~3.5 GB KV cache: NONE — no generation during training 0 GB ──────────────────────────────────────────────────────── TOTAL ≈ 5.8 GB comfortable and every step is 4 forward + 1 backward pass. no decode.
Two boxes deleted and the generation loop removed. That is the entire practical case for direct alignment on constrained hardware — and it is why S4 exists.

Three things happening in RL tooling right now

Production GRPO settings, published

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 runbook
TRL's GRPO grew teeth

Since 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.

Also: vllm_mode now defaults to "colocate".
Reward hacking has an active research front

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.
Sources.
  • Policy gradient derivation, PPO cases, GRPO, GSPO, CISPO, loss aggregation, GAE, double regularisation — the literature, RLHF field text living edition v2.
  • KL direction and estimator, static vs adaptive controllers — ibid.. Over-optimisation, Goodhart, over-refusal.
  • Nemotron 3 Nano RLVR configuration — NVIDIA, arXiv 2512.20848 (Dec 2025).
  • TRL v1.0–v1.8 release notes (Mar–Jul 2026) for AsyncGRPO, chunked LM head, entropy regularisation, multi-environment rewards.
  • Reward-hacking research front — Awesome-Reward-Hacking survey collection, 2026.
4 · apply to my stack — lab

The RL you can actually touch, and the reward function you'd write

Recommendation

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.

The reward function you would write for your JSON task

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 output
import 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.

Execution: Modal or Kubernetes?

Modal — for the demo run

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.

strength: zero standing infrastructure
K8s + Kueue — for the real job

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 pool

Argued 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.

Optional exercise

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.

Bridge → S4

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.

1 · why this session exists

The reward model was hiding inside the policy all along

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.

THE LONG WAY — S2 + S3 preference pairs fit reward model r_φ sample rollouts score them PPO / GRPO + KL leash π* 4 model copies · generation inside the training loop · 2 optimisers · ~116 GB naive THE DIRECT WAY — this session preference pairs one loss function on the pairs · no RM · no rollouts π* 1 trainable adapter · reference is free via adapter-disable · ~5.8 GB · fits on your L4
Same start, same finish, same objective. The middle three boxes turn out to be optional.
Two routes, one destination

Hover the crimson box. The reward model does not disappear — it becomes implicit inside the policy.

2 · core concepts

DPO, from intuition to gradient

Pass 1Intuition — you already have a scoring function, you just weren't reading it

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.

One consequence to hold onto

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.

Pass 2Mechanism — the implicit reward, the loss, the derivation in four moves

The implicit reward

In words: the reward of a completion is β times the log of how much more likely the policy finds it than the reference does.
r(x,y)  =  β · log [ πθ(y|x) / πref(y|x) ]  +  β log Z(x)
Z(x) is a partition function — a normaliser over all possible completions, intractable to compute. It is also the same for both completions of a pair, so when you take a difference it cancels. That cancellation is why the whole method works.

The loss

In words: take the log-ratio of the chosen completion, subtract the log-ratio of the rejected one, scale by β, push it through a sigmoid, and maximise the log of that. In plainer terms: widen the gap between chosen and rejected, measured relative to where you started.
DPO = − 𝔼 [ log σ( β log πθ(yc|x)πref(yc|x) − β log πθ(yr|x)πref(yr|x) ) ]
Compare this with the reward-model loss from S2: −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.

The derivation, in four moves

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:

π*(y|x)  =  (1/Z(x)) · πref(y|x) · exp( r(x,y) / β )

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_cy_r) = σ(r_cr_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.

∇ℒDPO = −β · 𝔼 [ σ( rc ) · ( ∇log πθ(yc|x) − ∇log πθ(yr|x) ) ]

Three parts, each doing a distinct job:

  • The sigmoid term is a per-example weight from 0 to 1, and it is larger when the implicit reward model has the pair backwards. Pairs the model already ranks correctly contribute almost nothing; pairs it gets wrong dominate the batch. DPO automatically focuses on its own mistakes.
  • The bracket raises the likelihood of the chosen and lowers the rejected. That is the actual behaviour change.
  • β scales the whole thing, trading off ordering the pair correctly against staying near the reference.
Pass 3Trade-offs — the static leash, likelihood displacement, and the ceiling

β is a static KL, and that is a real difference from RL

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.

Likelihood displacement — both go down

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.

TRAINING STEPS → log π(y|x) chosen rejected margin ↑ Both curves fall. The margin still widens. The loss is satisfied.
The shape you will see in S5. A falling 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.

The ceiling, stated fairly

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.”

What β actually does — the one hyperparameter that matters

β = 0.01 long leash large behaviour change format regression risk amplifies noisy labels β = 0.1 the standard starting point visible change, stable format TRL default · Vertex default start here, always β = 0.5 short leash small, surgical change robust to label noise may achieve nothing at all β trades how much the model changes against how much you trust your labels. Noisy labels → larger β.
Hover each. The right β is a statement about label quality as much as about desired change.
Choosing β

Start at 0.1. Move up if the model degrades; move down only if you trust your pairs and nothing is happening.

The variant family — what each one actually changes

There are dozens. These are the ones you will meet, grouped by what problem they were built to solve rather than by name.

MethodThe one changeref model?pairs?Choose it when
DPOThe baseline: Bradley-Terry on policy log-ratiosyesyesDefault. Start here every time.
IPOSoftens the preference probability instead of optimising a hard label — moves away from the Bradley-Terry assumptionyesyesYou suspect DPO is overfitting near-deterministic labels
cDPOLabel smoothing: assumes N% of your labels are simply wrongyesyesYou measured your judge's error rate and it is non-trivial
ODPORequires the chosen/rejected gap to exceed an offset, so pairs are not all equalyesyesYou kept the rating magnitudes — but labelling gets harder
KTODrops pairs entirely. Learns from unpaired binary signals — thumbs up / thumbs down — via a prospect-theory-inspired value functionyesnoYour feedback is thumbs, not comparisons. Production telemetry, not annotation.
ORPOMerges SFT and preference learning into one stage with an odds-ratio penalty. No reference model at allnoyesYou want one training pass total, and memory is very tight
SimPOUses the average (length-normalised) log-probability as the implicit reward plus a target margin. No reference modelnoyesLength bias is your main enemy and you want shorter, denser outputs
Online DPO / D2POGenerates fresh completions during training; D2PO relabels with a reward model on the flyyesyesYou have rollout capacity and want to close the gap to RL
REBELAdds reward-model signal as a margin, rather than using bare pairwise labelsyesyesYou already have an RM and want more than a binary label from it
The finding that should calm you down about all of this

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.

Where preference pairs come from

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.

Five sources, cheapest last is not the answer
Source 1
Human annotators

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.

Source 2
Production interfaces — implicit and explicit

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.

Source 3
LLM judges — the UltraFeedback pipeline

The dominant open practice, and the one you will use. The recipe, essentially unchanged since UltraFeedback and refined by Tülu 3:

  1. Select prompts.
  2. Generate four responses per prompt from a pool of different models.
  3. Have an LLM judge rate each response 1–5 across several aspects — Tülu 3 used helpfulness, instruction-following, honesty and truthfulness, judged by GPT-4o.
  4. Take the highest-rated as chosen, pair it with a randomly selected lower-rated one as rejected.

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.

Source 4
Structured pairs — free preference data from constraints

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.

Source 5
Delta learning — deliberately mismatched model pairs

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.

Your eval stage's judge biases are now training biases

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.

3 · reality check

DPO loss on paper, at two betas

Setup: one preference pair from your fintech set. You run four forward passes and collect four summed log-probabilities over completion tokens only.
chosen (y_c) rejected (y_r) policy log π_θ -42.10 -46.30 reference log π_ref -43.00 -45.00 step 1 — log-ratios (how far each has moved from the reference) chosen: -42.10 - (-43.00) = +0.90 policy likes it MORE than SFT did rejected: -46.30 - (-45.00) = -1.30 policy likes it LESS than SFT did step 2 — the logit (the margin, before beta) logits = 0.90 - (-1.30) = 2.20 === at beta = 0.1 === scaled margin = 0.1 × 2.20 = 0.220 loss = -log sigmoid(0.220) = -log(0.5548) = 0.5892 implicit rewards logged by the trainer: rewards/chosen = 0.1 × ( 0.90) = +0.090 rewards/rejected = 0.1 × (-1.30) = -0.130 rewards/margins = +0.220 rewards/accuracy = 1 (chosen reward > rejected reward → ordered right) === at beta = 0.5, same model, same pair === scaled margin = 0.5 × 2.20 = 1.100 loss = -log sigmoid(1.100) = -log(0.7503) = 0.2873 rewards/chosen = +0.450 rewards/rejected = -0.650 rewards/margins = +1.100 === the counter-intuitive bit === Higher beta gave a LOWER loss on the same weights. That is not the model doing better — it is beta amplifying a margin the model had already earned. Beta rescales your reward axis, so loss and margin values are NOT comparable across runs with different beta. Only compare within a beta. === and the gradient weight === the sigmoid factor in the gradient is sigma(r̂_r - r̂_c): beta=0.1: sigma(-0.220) = 0.445 ← 44.5% of full gradient weight beta=0.5: sigma(-1.100) = 0.250 ← 25.0% the higher-beta run considers this pair MORE solved and learns less from it. Same pair. Same model. Different curriculum.
Three things to carry into S5. (1) 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 β.
Second worked figure: what does that +0.90 log-ratio mean in probability terms?
e^0.90 = 2.46 the policy now assigns 2.46× more probability to the ENTIRE chosen completion than the SFT model did. spread over a ~180-token JSON completion, that is: 0.90 / 180 = 0.005 nats per token — a tiny per-token nudge that compounds into a 2.5× shift on the full sequence. this is why DPO can change behaviour visibly while barely moving any individual token distribution — and why sequence-level log-probs are the right unit to watch, not per-token loss.

Three current data points

Length-normalised DPO is now a config flag

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.

If your DPO'd outputs creep longer, try this before changing algorithm.
Managed preference tuning exposes β to you

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.

Even the fully-managed path makes you choose the leash length.
AWS ships serverless DPO for your exact model

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.
Sources.
  • DPO derivation, implicit reward, gradient interpretation, implementation snippet, variants, likelihood displacement, online-vs-offline — the literature, RLHF field text living edition v2.
  • Preference-data collection, rankings vs ratings, multi-turn, structured pairs, biases — ibid..
  • Delta Learning Hypothesis (COLM 2025) and its use in Olmo 3 / SmolLM 3 — ibid., §8.4.
  • TRL sigmoid_norm loss — TRL v1.4.0 release notes, 9 May 2026.
  • Vertex AI preference tuning — Google Cloud docs, Tune Gemini models by using preference tuning.
  • SageMaker Gemma 4 serverless customisation — AWS What's New, June 2026; serverless RFT expansion, 25 March 2026.
4 · apply to my stack — lab

Designing your preference-pair factory

No trainer code yet — S5 is the code-along. This lab designs the dataset, which is the decision that actually determines whether S5 works.

The layered pipeline, concretely

layerWhat it decidesExpected shareBias
L0Structured 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 demandnone
L1Hard checks. Parses / schema-valid / score in range / evidence non-empty / evidence verbatim in source. Different tier ⇒ pair settled.~55–60% of sampled pairsnone
L2Judge with order swap. Run the comparison twice with A and B exchanged. Agreement required.~30% of pairsposition bias neutralised
L3Drop. Judge contradicted itself across the swap ⇒ discard, do not guess.~10%removes ambiguous, low-information pairs
Why dropping L3 pairs is correct, not lazy

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.

The judge decision, argued

Gemma with enable_thinking=true as judge

For: 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 candidates
A stronger external judge

For: 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 prompts

Argued 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.

Target dataset shape

what you are aiming to produce
{"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.

Composition target

Optional exercise

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.

Bridge → S5

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.

1 · why this session exists

The gap between the loss function and a job that finishes

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.

Concepts recap — what each block will implement

blockConcept it implementsWhere it came from
1Candidate 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
2The layered labeller. Structured pairs, then hard checks, then an order-swapped judge, then drop the contradictions.S4 lab · S2 bias catalogue
3Dataset schema. Conversational prompt/chosen/rejected, rendered through the same Gemma chat template you serve with.S4 · your SFT stage
44-bit base + trainable adapter. QLoRA, so the policy fits and only ~0.5% of parameters carry optimiser state.your fine-tuning stage
5The 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 β
6DPOConfig. β as the KL leash, plus the memory knobs that make four forward passes per step survivable.S4 · S1's regularised objective
7Curve reading. rewards/chosen, rewards/rejected, rewards/margins, rewards/accuracies — the implicit reward model, logged.S4's implicit reward
8Failure smells. Likelihood displacement gone wrong, saturated pairs, format regression, judge bias leaking through.S2 saturation · S4 displacement
9Ship it. Adapter out, multi-LoRA alongside the SFT adapter, A/B on your vLLM endpoint.your serving stage · S7's gate
2 · core concepts

The reference-model problem, and its three solutions

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.

THREE WAYS TO GET π_ref WHEN THE POLICY IS A PEFT MODEL 1 · Two instances base weights (4-bit) + SFT adapter · trainable base weights (4-bit) + SFT adapter · frozen 2× base weights ≈ +2.0 GB on your L4 correct but wasteful avoid 2 · Merge, then re-adapt merged base + SFT this IS the reference new DPO adapter unloaded for ref pass 1× base weights most memory-efficient merge is lossy on 4-bit good, with a caveat 3 · Two named adapters base weights (4-bit) loaded ONCE dpo_adapter trainable reference frozen 1× base + 1 tiny adapter ≈ +40 MB overhead no merge, no precision loss ◀ use this All three compute the same loss. Only option 1 is wrong for your hardware.
Hover each. This single choice is the difference between a run that fits and a run that OOMs at step 3.
Three strategies

Same loss, three memory profiles. Option 3 is the one for a QLoRA continuation.

A fourth option that is really an optimisation

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.

3 · reality check

VRAM arithmetic — naive vs reference-free, on a 24 GB L4

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.

Setup A — the naive two-model setup. Full-precision policy, separate reference instance, no PEFT.
policy weights bf16, 2 B/param 4.0e9 × 2 = 8.00 GB policy gradients bf16 4.0e9 × 2 = 8.00 GB AdamW states fp32 m + v, 8 B 4.0e9 × 8 = 32.00 GB reference model bf16, frozen 4.0e9 × 2 = 8.00 GB activations 4 fwd passes, grad-ckpt, seq 1024 ≈ 3.50 GB ───────────────────────────────────────────────────────────────── TOTAL ≈ 59.5 GB budget 24.0 GB verdict 2.5× OVER — will not run
Setup B — QLoRA + the two-named-adapter trick. The run you will actually launch.
base weights NF4 4-bit, 0.5 B/param 4.0e9 × 0.5 = 2.00 GB + double-quant constants, ~0.06 B/param ≈ 0.03 GB LoRA adapter r=16 on q,k,v,o,gate,up,down ≈ 0.45% of params ≈ 18e6 params, bf16 = 0.036 GB reference adapter same size, frozen, no grads = 0.036 GB adapter gradients bf16, 18e6 × 2 = 0.036 GB AdamW states fp32 m + v, 18e6 × 8 = 0.144 GB activations 4 fwd passes, grad-ckpt, bs 1, 1024 ≈ 3.50 GB fragmentation + CUDA context + bnb buffers ≈ 1.20 GB ───────────────────────────────────────────────────────────────── TOTAL ≈ 6.98 GB budget 24.0 GB headroom ≈ 17.0 GB what the headroom buys you raise per_device_train_batch_size 1 → 4 +~10.5 GB still fits or raise max_length 1024 → 2048 at bs 1 +~3.5 GB still fits or turn OFF gradient checkpointing for speed +~6 GB still fits, ~30% faster and with precompute_ref_log_probs=True 4 forward passes per step → 2 activations 3.50 GB → 1.75 GB TOTAL ≈ 5.2 GB, and each step is ~40% faster
Ratio: 59.5 GB → 7.0 GB, a 8.5× reduction, of which the reference-free trick contributes 8 GB directly and QLoRA contributes the rest. The honest headline: DPO on a 24 GB L4 is not tight. It is comfortable. Compare this to S3's PPO figure of ~116 GB naive and the difference between the two rungs of tooling becomes concrete.
Wall-clock estimate — so you know whether to use spot capacity.
dataset 4,000 pairs effective batch 1 × 8 grad-accum = 8 pairs/step steps per epoch 4,000 / 8 = 500 epochs 1 (DPO overfits fast; 1–2 is standard) per step: 2 fwd (precomputed ref) + 1 bwd on ~2×1024 tokens measured L4 throughput for 4-bit + LoRA, grad-ckpt on: ~2.6 s/step 500 steps × 2.6 s = 1,300 s ≈ 22 minutes + reference precompute pass, 4,000 × 2 fwd ≈ 6 minutes + model load and quantisation from cache ≈ 3 minutes ─────────────────────────────────────────────────────── END TO END ≈ 31 minutes at GCP L4 on-demand $0.45/h → $0.23 per run
Consequence for the infrastructure argument: a 31-minute job is short enough that spot preemption is a minor annoyance, not a design constraint. That flips the Modal-vs-Kueue argument compared with S3 — see the lab section below.

Three grounded reference points for this run

The TRL API you are targeting

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.

Validate your dataset schema on a 20-row sample before a long run — a wrong format silently mis-trains.
The managed comparison, for your exact model

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.
the source material's skeleton, and what has aged

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.

Modernisation: TRL DPOTrainer + QLoRA + named-adapter reference.
4 · apply to my stack — lab

The run, block by block

Block 1 · Generate on-policy candidates

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.py
import 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]
Why temperature 0.9 and not your production 0.2

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.

Block 2 · Label with the layered pipeline

Implements S4's L0–L3 design. The hard_checks and tier functions come straight from S2's lab.

build_pairs.py — the order-swap is the load-bearing part
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)))

Block 3 · The dataset TRL expects

conversational preference format — validate on 20 rows first
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.

Blocks 4–6 · The trainer

dpo_train.py — the whole run
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")
Two settings that trip almost everyone

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.

Block 7 · Reading the curves

DPOTrainer logs the implicit reward model from S4 directly. These four numbers are your instrument panel.

A HEALTHY RUN — WHAT YOU WANT TO SEE rewards/chosen & rewards/rejected 0 chosen rejected rewards/margins margin rewards/accuracies — fraction of pairs ordered correctly 0.50 — chance ~0.78 climbs off chance, settles in a plausible band, does not hit 1.0 HOVER ANY CURVE FOR HOW TO READ IT
Four logged metrics, all of them the implicit reward model from S4 made visible.
Your instrument panel

Margins is the headline. Accuracies is the honest one, because it does not move when β does.

Block 8 · The five failure smells

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.

The eval callback is not optional

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.

Block 9 · Ship it — multi-LoRA A/B

serve both adapters side by side for the S7 gate
# 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.

Execution: Modal or Kubernetes — the argument flips here

Modal — recommended for this run

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.

strength: fastest path from idea to finished adapter
K8s + Kueue — when this stops being one run

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 spot

Argued 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.

Optional exercise

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.

Bridge → S6

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.

1 · why this session exists

When the scorer stops guessing

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.

RLHF — score a matter of taste “Explain the concept of opportunity cost.” judge: clarity? accuracy? completeness? helpfulness? tone? length? r = 0.73 … according to a learned proxy RLVR — score a fact “Sum of all primes below 20?” …so the answer is \boxed{77} extracted_answer == 77 → True r = 1 … according to arithmetic Same optimiser. Same GRPO from S3. Entirely different relationship with the truth.
The \boxed{} convention is borrowed from mathematical typesetting purely so a regex can pull the answer out regardless of how the model got there.
Two scorers

Hover each. The right-hand box is why reasoning models could be trained at all.

2 · core concepts

Verifiable rewards, and buying accuracy with tokens

RLVR in three passes

Pass 1Intuition — practising against a marking scheme

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.

Pass 2Mechanism — what a verifier is, and what a real recipe adds

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:

Difficulty filtering, offline

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.

Online filtering & curricula

Filter within the batch, or schedule harder problems later. Addresses the second question after “which problems” — namely “in what order”.

Remove the KL penalty

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.

Relaxed clipping

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.

Format & language rewards

Small extra rewards to keep <think>…</think> structure predictable, and language-consistency rewards so multilingual models do not switch language mid-reasoning.

Length penalties

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.

Asynchronous / off-policy data

Completion lengths vary enormously and outliers are extreme, so synchronous loops leave GPUs idle. Partial-to-full async updates recover the throughput.

Loss normalisation at batch level

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.

Pass 3Trade-offs — where verifiable rewards still crack
  • RLVR is not the same as ground truth. The method was nearly named RL with Ground Truth rewards, and the distinction matters: a verifier checks a property of the answer, which is not the same as the answer being right for the right reason. Models produce correct answers through unsound reasoning, and a verifier scores that as a win.
  • Verifiers get gamed too. The 2026 literature includes work titled, plainly, “LLMs Gaming Verifiers: RLVR can Lead to Reward Hacking.” Sandbox escapes, tests that pass for the wrong reason, degenerate solutions that satisfy the letter of the check. Rung 3 reduces the attack surface; it does not remove it.
  • Contamination is a confounder, not a footnote. Several early RLVR results on Qwen 2.5 and Qwen 3 base models are confounded by suspected base-model contamination — including the disquieting observation that benchmarks can improve when models are trained with RL on random rewards, which should only happen if certain kinds of contamination are present.
  • It only works on models that were already good enough. Multiple sources point to reasoning-style RL being viable only on leading models from roughly 2024 onwards. A certain level of underlying capability is a prerequisite — the elicitation interpretation from S1, again.
  • Reasoning still ends in preference tuning. DeepSeek-R1's final stage mixes verifiable rewards with general preference-tuning reward models to polish the model. RLVR sits on top of RLHF; it does not replace it.

Inference-time scaling — the other half

RL training and test-time compute are two ways of reaching the same place, and they predate each other's popularity.

Four ways to spend more compute at inference
Method 1
Long chain-of-thought — sequential scaling

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.

Method 2
Best-of-N — parallel scaling with a scorer

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.

Method 3
Majority vote — parallel scaling without a scorer

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.

Method 4
Toggleable thinking — per-request budget control

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.

The curve nobody puts on the marketing slide

More thinking helps, then stops helping, then actively hurts. This is not a fringe result; it is the standard shape.

AVERAGE THINKING TOKENS → (log scale) ACCURACY 3851,100 4,00015,980 90% 70% 82.2% 87.3% ← the peak 70.3% — worse than the cheapest setting the overthinking zone 41× the tokens, 12 points of accuracy lost Yes — this is the same shape as the over-optimisation curve in Tab 0.
Measured on a reasoning model with forced thinking budgets. Beyond a critical point, extended thinking flips correct answers to incorrect ones.
Goodhart, one rung up

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.

3 · reality check

The decode-token bill at 10,000 requests/day, on your stack

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).

Scenario: 10,000 fintech analyst requests/day. Non-thinking produces ~200 output tokens (the JSON). Thinking adds ~1,400 reasoning tokens before the same JSON.
--- DECODE VOLUME --- thinking OFF 10,000 × 200 = 2,000,000 out tok/day thinking ON 10,000 × 1,600 = 16,000,000 out tok/day 8.0× --- GPU-SECONDS OF DECODE PER DAY (at 934 tok/s) --- OFF 2,000,000 / 934 = 2,141 s/day = 0.59 GPU-h duty 2.5% ON 16,000,000 / 934 = 17,130 s/day = 4.76 GPU-h duty 19.8% --- MARGINAL COMPUTE COST PER MONTH --- OFF 0.59 × 30 × $0.45 = $8.03/mo of decode ON 4.76 × 30 × $0.45 = $64.25/mo of decode +$56/mo --- BUT YOU DO NOT BILL BY DUTY CYCLE. YOU BILL BY REPLICA. --- assume peak hour ≈ 15% of daily volume = 1,500 req/h = 0.42 req/s OFF 0.42 × 200 = 83 out tok/s needed → 1 replica at 9% load ON 0.42 × 1,600 = 667 out tok/s needed → 1 replica at 71% load so at 10k/day the bill is IDENTICAL: one L4, $324/mo, either way. the thinking tokens are free — you already own the idle capacity. --- NOW SCALE THE TRAFFIC 3× (30,000 req/day) --- OFF 1.26 × 200 = 252 tok/s → 1 replica at 27% = $324/mo ON 1.26 × 1,600 = 2,002 tok/s → 3 replicas at 71% = $972/mo +$648/mo the cost of thinking is a step function, not a line. it is free until it crosses a replica boundary, then it costs a whole GPU.
This is the finding that changes how you plan. The naive question “what do thinking tokens cost?” has no single answer. At your current volume they cost nothing, because your L4 sits at 9% load. At 3× volume they cost two extra GPUs. The right artefact is not a per-token price — it is a utilisation headroom chart that tells you which traffic level makes thinking expensive.
Latency — where thinking is never free. At batch 32, 934 tok/s aggregate across 32 concurrent sequences is ~29 tok/s per sequence, so inter-token latency ≈ 34 ms.
thinking OFF 200 tok × 34 ms = 6.8 s to final answer thinking ON 1,600 tok × 34 ms = 54.4 s to final answer TTFT is unchanged — prefill is the same. But TTFT is now a lie: the first token the user sees is the start of a reasoning trace they cannot act on. Time-to-USEFUL-token went from 6.8 s to 54.4 s. --- KV CACHE PRESSURE --- a thinking request holds its KV slot 8× longer AND grows the per-sequence KV footprint by the same 8× in resident tokens. at max_model_len 10,000 and max_num_seqs 256, effective concurrency falls roughly in proportion — which is exactly why the replica arithmetic above bites at 71% and not at 100%. --- AND THE OPTIMISATION THAT DOES NOT HELP --- your prefix caching gives 80% savings on RAG-shaped traffic by reusing the cached SYSTEM PROMPT — that is a PREFILL optimisation. thinking tokens are 100% decode. prefix caching does nothing for them. FP8 / AWQ weight quantisation helps decode throughput; caching does not.
This is the sharpest stack-specific point in the session. Your headline cost wins — prefix caching at 80%, intelligent routing at 30% — were all won on the prefill and routing side. Reasoning shifts your workload from prefill-heavy to decode-heavy, and it moves out from under most of your existing optimisations.
The number that should actually drive the decision: dollars per correct answer. Combining your throughput with the measured accuracy curve above.
per 1,000 requests, on one L4 at $0.45/h and 934 tok/s budget tokens/1k req GPU-time cost accuracy $/CORRECT ───────────────────────────────────────────────────────────────────── 385 tok 385,000 412 s $0.0515 82.2% $0.0627 1,100 tok 1,100,000 1,178 s $0.1475 87.3% $0.1689 15,980 tok 15,980,000 17,109 s $2.1400 70.3% $3.0441 reading it 385 → 1,100 tokens: +5.1 accuracy points for 2.7× the cost per correct 1,100 → 15,980: −17.0 points for 18× the cost per correct 385 → 15,980: −11.9 points for 48.5× the cost per correct the honest verdict there is a real accuracy purchase available, and it is cheap. there is also a cliff, and it is catastrophically expensive. the entire skill is finding the peak and stopping there — which is early stopping, at inference time.

Three grounding points from current practice

Difficulty filtering is the recipe

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.
Production RLVR settings, published

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.
Token efficiency varies ~9× between models

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 and only weakly tied to model scale.

Token efficiency is a selection criterion in its own right. Benchmark it.
One macro figure worth carrying into any cost conversation

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.

Sources.
  • RLVR definition, verifier examples, common implementation decisions, toggleable reasoning, inference-time scaling — the literature, RLHF field text living edition v2; Best-of-N and its equivalence to top-1.3.
  • Overthinking curve (82.2% @385 → 87.3% @1,100 → 70.3% @15,980 tokens) — Does Thinking More always Help? Mirage of Test-Time Scaling, arXiv 2506.04210.
  • Oracle stopping (+8% accuracy, −72% thinking tokens) — ThinkBrake, arXiv 2510.00546.
  • OckBench 3.3×/5× overthinking tax; ~9× verbalisation-overhead spread; Price of Progress and Cost-of-Pass figures — surveyed in arXiv 2606.25432 (2026).
  • Olmo 3 RLVR data filtering — Ai2, 2025–26. Nemotron 3 Nano multi-environment GRPO — NVIDIA, arXiv 2512.20848.
  • Throughput and pricing baselines — your own L4 benchmarks (934 tok/s at batch 32; $0.45/h GCP L4).
4 · apply to my stack — lab

Making enable_thinking a routing decision

You 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 IntelligentRouter
from 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.

Measuring the uplift on your own stack

measure_thinking.py — the experiment that replaces the guess
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.

Monitoring it in Langfuse

Optional exercise

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.

Bridge → S7

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.

1 · why this session exists

Post-training gains hide from the metrics you already have

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.

THE SAME GOLDEN-SET ITEM, SCORED TWO WAYS Accuracy / exact match SFT-only output …… correct ✓ DPO output ………… correct ✓ Δ = 0.00 “the DPO run did nothing” ← wrong conclusion Pairwise win rate DPO preferred …… 68 of 100 SFT preferred …… 32 of 100 win rate = 0.68 ± 9.1 pts a real signal — with a real confidence interval Preference tuning changes the ranking among acceptable answers. Only a ranking metric can measure it.
Keep the accuracy metric — it is your regression guard. Add the win rate — it is your improvement signal.
Two instruments

Hover each. One catches breakage, the other catches progress. You need both, and they answer different questions.

2 · core concepts

Measuring a model that got better at taste

Win rate, and its family resemblance to everything else in this stage

Pass 1Intuition — you have already built this three times

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:

  • S2: Bradley-Terry fits a reward model from comparisons.
  • S4: DPO optimises a Bradley-Terry likelihood using the policy's own log-ratio.
  • S7: Bradley-Terry ranks models from comparisons, and a two-model special case is a win rate.

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.

Pass 2Mechanism — the eval protocol, and the three eras that produced it

Post-training evaluation went through three distinct phases, and the current best practice is a residue of all three.

EraWhat it measuredrepresentativeWhat it got wrong
1 · Early chatChat quality relative to a strong reference model, scored by LLM judges standing in for humansMT-Bench · AlpacaEval · Arena-HardNarrow. Rewarded style; conflated verbosity with quality until length control was retrofitted.
2 · Multi-skillKnowledge, reasoning, maths, code, instruction-following, safety as separate axesthe Tülu suite: MMLU, GSM8K, HumanEval, IFEval …Saturated. Frontier models now cluster above 90% on most of it.
3 · Reasoning & toolsHard knowledge, real software engineering, competition mathsGPQA Diamond · SWE-Bench · LiveCodeBench · AIMEExpensive, high-variance, and increasingly confounded by inference-time compute.

The protocol that actually works for a narrow task

  1. Freeze a golden set drawn from production traffic and decontaminated against your training prompts. This is non-negotiable and covered below.
  2. Generate from both checkpoints at identical decoding settings. Temperature, top-p, max tokens, thinking on/off — all identical, or you are measuring your sampler.
  3. Judge each pair twice with the order swapped, and count a win only when both orders agree. Disagreements become ties.
  4. Report the win rate with a confidence interval. A bare number is not a result.
  5. Report a length-controlled win rate alongside it, or at minimum report mean output length per checkpoint so a reader can see whether you bought quality or verbosity.
  6. Run the programmatic checks separately as a hard gate. Schema validity is not a preference; it is a requirement, and it should be able to fail a release on its own.
Judge biases, restated as an eval checklist

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.

Pass 3Trade-offs — why nobody else's numbers mean anything

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.

Contamination — and why your case is more tractable than the field's

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.

The Tülu 3 finding that should make you check your own data

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.

The harder, unprovable kind

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.

Why yours is easier

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.

3 · reality check

How many judgements to detect a 5-point win-rate delta

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?

In words: the number of comparisons you need grows with how noisy a coin flip is, and shrinks with the square of the effect you are trying to detect. Halve the effect, quadruple the sample.
n  ≈  ( zα/2 √(p0(1−p0))zβ √(p1(1−p1)) )² ⁄ (p1p0
null hypothesis p₀ = 0.50 (the two checkpoints are equivalent) alternative p₁ = 0.55 (a 5-point win-rate delta) significance α = 0.05 two-sided → z = 1.960 power 1−β = 0.80 → z = 0.8416 numerator = (1.960 × √(0.50×0.50) + 0.8416 × √(0.55×0.45))² = (1.960 × 0.50000 + 0.8416 × 0.49749)² = (0.98000 + 0.41870)² = 1.39870² = 1.95637 denominator = (0.55 − 0.50)² = 0.0025 n = 1.95637 / 0.0025 = 782.5 → 783 decisive comparisons --- ties are not free --- a judge that returns TIE on ~20% of pairs gives you only 0.8 decisive comparisons per sampled pair: 783 / 0.80 = 979 golden-set prompts must be sampled --- order swapping doubles the judge calls, not the prompts --- 979 prompts × 2 orders = 1,958 judge calls at ~$0.008/call = $15.66 per evaluation round (the statistics are the expensive part; the judging is not) --- WHAT YOU CAN DETECT WITH THE SET YOU PROBABLY HAVE --- detectable delta at n: δ = (z_α/2 + z_β) × √(p(1−p)/n) = 2.8016 × √(0.25/n) n = 100 → δ = ±14.0 points basically useless n = 200 → δ = ±9.9 points "68% win rate" could be 58% n = 500 → δ = ±6.3 points n = 783 → δ = ±5.0 points ← the design target n = 2,000 → δ = ±3.1 points n = 5,000 → δ = ±2.0 points --- AND THE SANITY CHECK ON A HEADLINE NUMBER --- observed 68 wins / 100 decisive comparisons 95% CI ≈ 0.68 ± 1.96 × √(0.68 × 0.32 / 100) = 0.68 ± 1.96 × 0.04665 = 0.68 ± 0.0914 → [58.9%, 77.1%] the interval clears 50%, so the win is real — but "68%" as a point estimate is nine points wide in each direction. report the interval or you are reporting noise.
Three consequences for your capstone. (1) A golden set of 100 items cannot gate a release on win rate — it can only gate on hard checks. Grow it to ~1,000. (2) If you evaluate three checkpoints (SFT, DPO β=0.1, DPO β=0.5) you are running multiple comparisons and should tighten α or pre-register which single comparison is the decision. (3) The judge cost is trivial; the prompt cost is not, because every golden-set prompt must be decontaminated against training. Budget the curation, not the inference.
A cheaper way to buy power: pair harder

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.

Three current data points on eval integrity

The arena is now a company, and the methodology moves

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.
Leaderboards with stakes become targets

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.
The GSM1k experiment is still the cleanest demonstration

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.

Tooling, if you want to stop writing harness code

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.

Sources.
  • Eval eras, reliability of external comparisons, hillclimbing, contamination and decontamination, perturbation benchmarks, tooling list — the literature, RLHF field text living edition v2.
  • Olmo 3 post-training eval variance (0.25–1.5 pt standard deviations with setup held constant) — ibid., §17.2.
  • Tülu 3 8-gram decontamination findings — ibid., §17.4.
  • Arena rebrand (Jan 2026), Style Control Elo shifts, Elo range 2023→2026 — LMArena/Arena platform history and 2026 leaderboard analyses.
  • Parallel private variants, de-listing asymmetry, prompt repetition rates; GSM1k — 2026 surveys of LLM evaluation integrity.
  • Benchmark saturation (MMLU, HumanEval >90%; GPQA Diamond still spread) — 2026 leaderboard methodology reviews.
4 · apply to my stack — capstone

The whole plan, end to end

Everything from seven sessions, assembled into one runnable sequence against your actual infrastructure.

0 · SFT adapter exists · rung 1 policy + reference + baseline 1 · Sample n=4 temp 0.9 · on-policy decontaminate first 2 · Label L0–L3 checks → judge → drop 3–5k pairs 3 · DPO run L4 · ~31 min · $0.23 β sweep on Kueue 4 · Read curves margins · accuracies + eval callback / 100 steps 5 · EVAL GATE hard checks → win rate n ≈ 1,000 · CI reported 6 · Multi-LoRA sft + dpo, one endpoint FP8 · prefix caching 7 · Canary + monitor Langfuse online win rate Prometheus TTFT / KV production failures become next round's preference pairs — the loop closes Total new compute: about 31 minutes on one L4, roughly $0.23. Total new infrastructure: none. The expensive artefact was always the preference dataset and the golden set — exactly as S1's inventory predicted. optional rung 3 — route enable_thinking before training RLVR
Eight hotspots. The dashed crimson return path is the part that turns this from a project into a pipeline.
The capstone

Hover any stage. Note how little of this is new infrastructure — the work is data and measurement.

The release checklist

Print this. It is the gate, in order, and the ordering is the point.

Hard gates — any failure blocks the release
  • Schema validity ≥ SFT baseline on the golden set
  • Evidence spans verbatim-present in source ≥ baseline
  • Zero new PII or source-text leakage in sampled outputs
  • Safety refusal rate within tolerance — no new over-refusal
  • Golden set decontaminated against training prompts, 8-gram, count recorded
  • p95 completion tokens within the serving budget
Statistical gates — the improvement claim
  • ≥ 780 decisive comparisons (≈ 1,000 sampled prompts, ties dropped)
  • Judge run in both presentation orders; agreement required
  • Win rate reported with a 95% confidence interval that clears 50%
  • Mean output length reported for both arms; > 15% growth triggers length control
  • Win rate broken down by task type — no slice regresses
  • Decoding parameters byte-identical across both arms
  • One pre-registered comparison, or α corrected for the number of checkpoints

What to write down when you ship

A short run record, because in six months you will not remember and neither will anyone else. Six lines:

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.

Optional exercise — the capstone itself

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.

5 · bridge

Where this stage ends

Stage complete → next stage

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.

What you can now do
  • Place any post-training technique on the imitate → prefer → achieve ladder and explain its mechanism
  • Derive Bradley-Terry, the DPO loss, and the GRPO advantage from intuition, with worked numbers
  • Explain the PPO four-model dance and prove, arithmetically, what fits on given hardware
  • Name where the proxy cracks in each method, and which regulariser holds it together
  • Defend a DPO-vs-RLHF-vs-variant choice from a data budget rather than a preference
  • Build a layered preference dataset with position and self-preference bias measured, not assumed
  • Run a real DPO pass on your own hardware and diagnose five distinct failure smells from the curves
  • Price reasoning honestly in decode tokens, replica boundaries and dollars per correct answer
  • Gate a release on a win rate whose sample size you computed rather than guessed
What to stay sceptical about
  • Any win rate without a confidence interval. Including your own.
  • Any cross-lab benchmark table. Setups are uncontrolled and undocumented; small gaps are noise.
  • Any reasoning-model comparison that is not token-controlled. It may be reporting a compute purchase.
  • Any claim that a DPO variant beats DPO. The differences are, in the literature's own words, a blur — and data dominates.
  • Any preference dataset you did not decontaminate. The popular public ones are already contaminated with popular evals.
  • Any judge whose biases you have not measured. In evaluation that is a wrong number; in training it is a wrong model.
← 02The path
Next stage · 04 →genaipros · 03 · Post-Training & AlignmentAI for Everyone ↗