Preference Variety (v0.40.0)

Five preference losses live behind one config knob. Pick a loss without renaming your task, anneal β over training, periodically refresh the frozen reference, and blend simpo and orpo terms with a multi-objective surface (a blend naming dpo or ipo is refused at the first step; see below).

On a card that cannot hold the model: since v0.72.4, dpo, orpo, simpo and kto also run over a streamed base, where the frozen reference is that same stream with its adapters switched off rather than a second copy of the weights. Note the unified task: preference dispatcher below, and ipo and bco, are not in the streaming allowlist: name the loss in task directly. See Preference losses over streaming.

BCO (Binary Classifier Optimization)

Same input format as DPO; rows are split internally to TRL's BCO unpaired schema ({prompt, completion, label}).

yaml
task: bco
data:
  train: ./data/preferences.jsonl
  format: dpo
training:
  bco_beta: 0.1
  lora: { r: 64, alpha: 16 }
  quantization: 4bit

bco_beta defaults to 0.1 (gt=0). New soup init --template bco ships in the box.

Unified preference dispatcher

Use task: preference + training.preference_loss to swap losses without touching task. Hyperparameter sweeps over the loss type itself become trivial.

yaml
task: preference
data:
  train: ./data/preferences.jsonl
  format: dpo
training:
  preference_loss: dpo   # or simpo, orpo, ipo, bco

Legacy task: dpo / task: simpo / etc. remain first-class — the unified surface is additive.

KL-controlled DPO variants

Anneal β over training and periodically refresh the reference model.

yaml
task: dpo   # or task: preference + preference_loss: dpo, or task: ipo
training:
  dpo_beta: 0.1
  dpo_beta_schedule: linear   # linear | cosine | exponential
  dpo_beta_end: 0.01
  dpo_ref_regen_epochs: 2     # every 2 epochs, copy the current LoRA adapter into the frozen ref adapter
  lora: { r: 8, alpha: 16 }

Both controls are gated to DPO-family tasks (dpo, ipo, or preference with preference_loss in {dpo, ipo}). Transformers backend only.

The BetaScheduleCallback resolves total_steps lazily in on_train_begin so the schedule sees the real state.max_steps populated by HF Trainer. Epoch-0 regen is suppressed (avoids copying the untrained student).

Reference regeneration needs LoRA. Under LoRA, TRL keeps a frozen ref adapter beside the trainable one and no separate reference model. Since v0.76.0, dpo_ref_regen_epochs: N copies the active .default. adapter's weights into the .ref. adapter under torch.no_grad() every N epochs, in place (#1345). Full fine-tuning (lora.r: 0) with this key is refused when the config loads: dpo_ref_regen_epochs requires LoRA (lora.r >= 1): full fine-tuning (lora.r: 0) is not supported for DPO-family reference regeneration.

(Since closed in v0.76.0: through v0.75.2 a run that set dpo_ref_regen_epochs or dpo_beta_schedule on dpo, ipo or preference stopped when training started, because the two callbacks were plain classes rather than Hugging Face TrainerCallback subclasses (dpo_beta_schedule with AttributeError: 'BetaScheduleCallback' object has no attribute 'on_epoch_begin', #1229). Neither setting ever took effect, and no finished run exists to re-run. dpo_ref_regen_epochs is wired under LoRA now (#1345, #1636).)

task: dpo with modality: vision now loads a vision-language wrapper checkpoint (AutoModelForImageTextToText plus the model's processor) for text-only preference rows; task: preference with preference_loss: dpo follows it. backend: unsloth does not read modality for DPO and is refused at config load for that combination (#1393).

Multi-objective preference loss (v0.40.1, blend added in v0.53.11, narrowed in v0.76.0)

yaml
task: preference
training:
  preference_loss_weights: {simpo: 0.6, orpo: 0.4}

The schema validates 2–5 entries summing to 1.0 (±1e-6). Single-entry rejected with an actionable message pointing at scalar preference_loss. Mutually exclusive with scalar preference_loss. Rejected on MLX backend.

At v0.76.0 only a blend that names simpo and orpo alone trains (#1425). The CPO (SimPO) and ORPO trainers both return their per-sequence log-probs from concatenated_forward, so Soup captures them there. Blends that name dpo or ipo stop at the first training step and name the terms they could not compute: those trainers compute their log-probs inline and publish only means, and they need a frozen reference model this path does not build. The error starts preference_loss_weights: cannot compute <terms> on this step and ends Remove training.preference_loss_weights and set training.preference_loss to the single loss you want. The config itself loads, so soup doctor --config and soup train --dry-run do not catch it. To train one loss, remove the weights and set training.preference_loss.

  • What a term is. In a blend, simpo and orpo are their preference terms only (the margin or odds-ratio objective), without the likelihood term the standalone preference_loss: simpo and preference_loss: orpo runs carry. A simpo plus orpo blend is therefore not the same number as either standalone run, and training.cpo_alpha does not affect it. Each term reads its own config field (training.simpo_gamma for SimPO, training.orpo_beta for ORPO), whichever loss has the larger weight.
  • Evaluation. With val_split set, the eval loss is the primary trainer's own loss, not the blend's value, so training and evaluation report different numbers for the same batch.

bco cannot be combined with a paired loss, and the refusal arrives later than the others: the schema allowlist accepts the name, so the config loads clean and validate_weight_compat raises at trainer setup instead. BCO scores each completion on its own, the paired losses need a chosen-versus-rejected pair, and there is no shared batch shape. Use bco as a scalar preference_loss rather than as an ensemble member.

This surface shipped schema-only in v0.40.0, where training raised an actionable NotImplementedError (the same stub-then-live pattern as v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu / v0.39.0 ReLoRA). v0.40.1 replaced that stub with a primary-loss approximation: the highest-weighted loss becomes the trainer and its term is scaled by its weight. v0.53.11 added a per-batch weighted blend on top, but the blend reads the policy log-probs from the batch the trainer receives, and TRL 0.29's trainers do not put them there. (Since closed in v0.76.0: through v0.75.2 the blend then quietly fell back to the primary-loss scaling. {dpo: 0.7, simpo: 0.3} printed Multi-objective preference loss: 0.70·dpo + 0.30·simpo at setup and then trained DPO alone, scaled by 0.7, with nothing in the output saying so. v0.76.0 refuses that instead (#1513) and trains the simpo plus orpo blend described above (#1556). A task: preference run from an earlier version that set preference_loss_weights was a single-loss run.) The NotImplementedError stub is gone.

Re-run this

If you trainedWhat was wrongWhat to do
task: preference with preference_loss_weightsThe blend printed at setup was not the loss that trained. Only the highest-weighted loss trained, scaled by its weight, with nothing saying so (#1425)Re-run as a single loss with training.preference_loss, or as a simpo plus orpo blend
task: simpo on a pair where chosen is much longer than rejected and longer than data.max_lengthOn trl 0.29.x the shorter side could end with zero tokens, SimPO's length-normalised log-probability became 0/0, and the adapter went to NaN behind a logged loss of 0.0 (#1208)Check the adapter for NaN or zero tensors and re-run after shortening the data or raising data.max_length. v0.76.0 refuses such a dataset before the first step, naming how many rows, which ones and the data.max_length involved

Stats

  • Net +118 tests (4538 → 4656 across 136 files)
  • BCO trainer + dispatcher + β schedule math + ref-model regen TOCTOU + multi-objective schema bounds

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.