Online DPO (v0.71.31)

soup train with task: online_dpo puts an LLM judge (or a reward model) in the training loop. Each step samples two on-policy completions per prompt, a pairwise judge names the winner chosen and the loser rejected, and the model takes a DPO step on that fresh, self-generated pair. It wraps TRL's OnlineDPOTrainer, and it is transformers + text only (prompt-only data, like GRPO).

This is the training face of the v0.71.31 judge-in-the-loop suite: the same swap-debiased judge also mines data with best-of-N, grows it with Evol-Instruct, and decides SHIP with a pairwise win-rate in soup ship.

Config

yaml
base: HuggingFaceTB/SmolLM2-135M-Instruct
task: online_dpo
data:
  train: ./data/prompts.jsonl      # prompt-only rows; see the note on row shape below
training:
  online_dpo_judge: "ollama://llama3.1"   # a pairwise judge (ollama:// | https:// | http://localhost, 127.0.0.1 or [::1])
  # OR set a reward model instead of a judge (exactly one of the two):
  # reward_model: ./my-reward-model
  online_dpo_loss_type: sigmoid           # sigmoid | ipo
  online_dpo_max_new_tokens: 64
  dpo_beta: 0.1
  lora: { r: 8, alpha: 16, target_modules: auto }
bash
soup train --config online_dpo.yaml
# or start from the ready-made recipe:
soup recipes use online-dpo-smollm2-135m

Row shape. The data goes through the normal format detection first, so a row has to be one the loader recognises. A chat row with no assistant turn, {"messages": [{"role": "user", "content": "Say hi"}]}, passes soup train --dry-run as chatml; a bare {"prompt": "Say hi"} row does not (Cannot detect format. Keys found: {'prompt'}, exit 1) unless you wrap it in a recognised shape.

Config fields

FieldDefaultMeaning
online_dpo_judge(none)Judge URL (ollama://model, https://..., or http://localhost / http://127.0.0.1 / http://[::1]; the loopback spellings are one shared set since v0.76.0). Mutually exclusive with reward_model. Since v0.75.2 a URL naming a private, link-local or reserved IP address is refused when the config loads, not when the trainer starts, so soup train --dry-run catches it; use a hostname (private addresses refused). A numeric-only host that is not a valid IPv4 address (10.0.0.256, 4294967296) is refused at load as well (online_dpo_judge: an all-numeric host that is not a valid IPv4 address is not allowed (SSRF protection); use a valid IPv4 address or a hostname).
reward_model(none)A reward model to score completions instead of a judge. Exactly one of judge / reward_model must be set.
online_dpo_loss_typesigmoidsigmoid (DPO) or ipo.
online_dpo_max_new_tokens64Tokens to sample per completion (1..4096).
betavia dpo_betaThe DPO temperature; reuses the existing dpo_beta.

Two behaviours that follow from the generation loop (v0.76.0, #1223): task: online_dpo neither evaluates nor holds out data.val_split, because evaluating means generating. The split is ignored, every row trains, and the run prints Note: data.val_split is ignored for task=online_dpo because evaluation means generation, so every row trains. training.eval_steps is refused at config load (training.eval_steps is not supported for task='online_dpo' because online DPO's evaluate() crashes on prompt-only rows; ...). --fsdp and --deepspeed now reach this trainer (#1204); through v0.75.2 the wrapper stored them and never passed them to its trainer arguments.

The cross-validator requires backend: transformers and modality: text, and rejects both-set or neither-set judge / reward_model, plus any online_dpo_* field left non-default when the task is not online_dpo.

How the judge is used (and a per-version note)

The judge is Soup's JudgeEvaluator adapted to TRL, swap-debiased: it compares the two completions in both orders and only records a winner when (A,B) and (B,A) agree, so position bias cannot leak a preference.

The judge is chosen by capability, not by version: Soup asks the installed trl OnlineDPOTrainer whether it accepts a pairwise judge=.

  • On the supported range (trl>=0.29.0,<1.0.0, which is what [train] installs) the swap-debiased pairwise judge is used (judge=).
  • On a trl that has dropped judge= (trl 1.x removed pairwise judges, and is outside the declared range) the same JudgeEvaluator is adapted to a pointwise reward_funcs= scorer.

Same judge model either way; the comparison shape differs with what the installed trl accepts. The upper bound sits below trl 1 because that migration is separate work, so the pointwise branch is there for a mismatched install, not the normal path.

When the judge cannot rank a pair (v0.76.0)

The judge answers "no winner" for a tie, a failed or unreadable call, or a verdict that changes when the two completions are swapped. On the online_dpo_judge path such a pair now adds exactly zero gradient, and the loss is the mean over the pairs the judge did rank in each batch (each micro-batch, under gradient accumulation) (#1225). The run logs judge/invalid_rate, the share it could not rank, with a WARNING the first time and on every step that ranked nothing, and the logged loss and train_loss average only the batches that ranked a pair. A step that ranked nothing still takes an optimizer step, so AdamW momentum and weight decay keep moving the weights. Once 32 pairs in a row go unranked, training stops with an error naming the judge. A run that ranked no pair at all fails the same way at the end instead of saving an adapter. The end-of-run line reads the judge could not rank N of M pairs (x%); they were left out of the loss. The reward_model path is unchanged.

An unreachable judge is reported as unavailable (it is not scored as a tie) by the other judge-backed commands since v0.76.0 (#1447); in Online DPO a failed call leaves its pair unranked, as above.

(Since closed in v0.76.0: through v0.75.2, trl 0.29's OnlineDPOTrainer built mask = rank == 0, so every unranked pair trained as a win for the second completion and the loss was averaged over all pairs. With the judge server down, a whole run learned arbitrary labels and finished normally.)

Re-run this

If you trainedWhat was wrongWhat to do
task: online_dpo with training.online_dpo_judgeA pair the judge could not rank (a tie, a failed or unreadable call, or an order-dependent verdict) trained as a win for the second completion, with nothing saying so (#1225)Re-run, especially if the judge was remote or flaky

Honesty

Proof-of-mechanism only. Online DPO was validated on SmolLM2-135M with a synthetic length-preferring judge on CPU (rewards/* and objective/kl logged), not a production RLHF claim. The 4-bit path is code-correct but hardware-gated (validated at quantization: none on CPU; real QLoRA online-DPO needs a bigger GPU). Scale validation is an open community ask (issue #286).

See also

Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.