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,simpoandktoalso 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 unifiedtask: preferencedispatcher below, andipoandbco, are not in the streaming allowlist: name the loss intaskdirectly. 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}).
task: bco
data:
train: ./data/preferences.jsonl
format: dpo
training:
bco_beta: 0.1
lora: { r: 64, alpha: 16 }
quantization: 4bitbco_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.
task: preference
data:
train: ./data/preferences.jsonl
format: dpo
training:
preference_loss: dpo # or simpo, orpo, ipo, bcoLegacy 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.
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)
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,
simpoandorpoare their preference terms only (the margin or odds-ratio objective), without the likelihood term the standalonepreference_loss: simpoandpreference_loss: orporuns carry. Asimpoplusorpoblend is therefore not the same number as either standalone run, andtraining.cpo_alphadoes not affect it. Each term reads its own config field (training.simpo_gammafor SimPO,training.orpo_betafor ORPO), whichever loss has the larger weight. - Evaluation. With
val_splitset, 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 trained | What was wrong | What to do |
|---|---|---|
task: preference with preference_loss_weights | The 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_length | On 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
- DPO training guide — preference dataset format
- Trace-to-preference — harvest pairs from production logs
- Registry — track preference variants in the lineage DAG
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.