Preference losses over layer streaming (v0.72.4)
Until v0.72.4, a model too big for your card could only be supervised fine-tuned on it. task had to be sft, and dpo, orpo, simpo and kto were all refused when the config was read. All four run against a streamed base now, with the same keys SFT already used, so preference alignment stops being the step that forces you to rent a bigger card.
Nothing else about streaming changed. If you have not read it yet, start with Layer streaming for the mechanism and Scaling a streaming run for architectures, batches and the disk tier. This page is only about what the preference losses add on top.
The five accepted tasks
sft dpo orpo simpo ktoThat list is exact. ipo, bco and the unified task: preference dispatcher are not in the streaming allowlist, so "all preference losses stream" would be false. grpo and ppo are excluded permanently, and their refusal deliberately names no release: generation rollouts re-read every layer once per generated token, which destroys the amortisation streaming exists to buy.
DPO's reference model is free
DPO scores the policy against a frozen reference. The obvious implementation builds a second copy of the model, which doubles the weights and defeats the entire premise of streaming: you would be back to needing the model twice over on a card that could not hold it once.
Soup takes the reference from the same streamed base with its LoRA adapters switched off. Policy and reference are one set of weights, one RAM store and one buffer pool, and the adapter is the only difference between the two forward passes.
Measured on the reference box, a 4 GB RTX 3050 Laptop, in bf16 with a 365.2M-parameter model (730.4 MB of weights, 24 decoder layers, vocabulary 260, sequence length 64, batch 1):
| Arm | Peak VRAM | vs streamed SFT |
|---|---|---|
| Streamed SFT | 89.53 MB | 1.000x |
| Streamed DPO | 81.87 MB | 0.914x |
| Control: DPO forced to build a real second model | 812.32 MB | 9.92x |
The RAM store was byte-identical between the SFT and DPO arms (729.91 MB each), and so was the VRAM buffer pool (60.83 MB each).
The control arm is what makes the headline mean anything. Forcing a real second instance moved the peak by 730.44 MB against 730.44 MB of weights, which is exactly one copy. As the gate record puts it: "DPO is not 2x SFT" means nothing unless the same harness can show what 2x looks like.
Read 0.914x as "no second copy", not as a saving
DPO is not cheaper than SFT in general. That row sits below 1.0 because of the fixture, not because of the feature: the SFT arm computes its loss over all 64 positions, while the DPO arm splits the same 64 into a 32-token prompt and a 32-token completion, so its logits tensor is smaller. A negative delta is simply the strongest available form of "there is no second copy of the weights".
These numbers also come from a synthetic 365.2M-parameter model, not from a real checkpoint and not from Llama-3.1-8B. They are a memory-accounting result, not a throughput result.
Why this was measured rather than asserted
A passing loss curve cannot detect the failure this feature is exposed to. The streamed layer substitutes base weights through functional_call rather than through the module's own forward, so it is entirely plausible that disabling the adapter is a silent no-op through that path. If it were, the reference would be the policy, every log-ratio would be zero, and the DPO loss would sit at 0.6931 forever, which reads as slow training rather than as a bug.
So three things were checked that a loss curve cannot check:
- The memory table above, with its forced-second-instance control.
- A policy-versus-reference gap of
8.219452e-01on chosen completions, proving the two passes really differ. - A zero-adapter control measuring exactly
0.000e+00, proving that gap comes from the adapter and not from some other difference between the two forwards.
KTO is not reference-free
However it is usually described, KTO picks its reference exactly the way DPO does, so it gets exactly the same treatment: the same streamed base with adapters disabled, no second copy. ORPO and SimPO genuinely are reference-free and never construct one at all.
KTO carries one extra requirement, and Soup enforces it when the config is read rather than minutes into a run after the checkpoint has already been sharded:
task='kto' requires training.batch_size >= 2 (TRL's KL term is
degenerate at batch 1).Note that KTO is streamable only because v0.72.3 lifted streaming's own batch-1 restriction. Under v0.72.0 through v0.72.2 it could not have shipped at all.
Free in memory is not free in time
DPO walks the layer stack more often than supervised fine-tuning does, because it adds a reference forward on top of the policy forward and the gradient-checkpoint recompute. Measured on the same 24-layer model: 46 layer loads per step for SFT against 70 for DPO, which is 1.52x.
That is the honest cost, and it is the one to plan around. Streaming makes the reference free in memory; it does not make it free.
Correctness
Every one of the four losses is bit-exact against a resident run of the same loss, difference exactly 0.0. DPO's arm ran in float32 on CPU precisely so that "bit-exact" means literally zero rather than bf16 noise: streamed 0.67242944 against resident 0.67242944 over 16 synchronised adapter tensors. Layer-0 adapter gradients are non-zero, and two runs at the same seed differ by 0.000e+00.
End to end through the released CLI, streaming SmolLM2-135M under NF4 (30 layers, a 0.05 GB pinned RAM store, two 2 MB VRAM buffers):
| Task | Loss |
|---|---|
dpo | 0.6931 to 0.6695 |
orpo | 5.3816 to 5.0559 |
simpo | 5.2033 to 4.8749 |
kto | 0.5000 to 0.4904 (6 epochs) |
All four saved adapters are ordinary LoRA files: 120 tensors, zero keys carrying the streaming wrapper segment, and 60 of 60 lora_B tensors non-zero. They load into a non-streaming model like any other adapter.
A config that runs
DPO over a streamed, NF4-quantized base:
base: Qwen/Qwen2.5-3B
task: dpo
backend: transformers # streaming requires it
data:
train: ./data/preferences.jsonl
format: dpo
max_length: 512 # see the pre-flight note below
training:
epochs: 3
dpo_beta: 0.1
batch_size: 1 # a concrete value; "auto" is refused
gradient_accumulation_steps: 1
quantization: 4bit # or none
gradient_checkpointing: true
stream_layers: true
stream_source: auto # RAM when it fits, NVMe when it does not
stream_buffers: 2
lora:
r: 64
alpha: 16
output: ./outputThe other three are the same file with one or two lines changed:
- ORPO:
task: orpo, andorpo_beta: 0.1in place ofdpo_beta. - SimPO:
task: simpo, withsimpo_gamma: 0.5andcpo_alpha: 1.0. - KTO:
task: kto,format: kto,kto_beta: 0.1, andbatch_size: 2or more.
Before you configure it: the pre-flight is a loose upper bound
The VRAM pre-flight predicts peak VRAM and refuses a run that will not fit. It is sound but conservative, and a preference run feels that most because it sends twice the rows through the same charge of 14 bytes per logit element, while TRL reduces preference logits to per-token log-probabilities with selective_log_softmax instead of holding a full-vocabulary fp32 upcast. The gap is large: DPO's entire above-resident cost measured 51.76 MB where the charge for the same shape is roughly 458 MB.
One correction worth carrying, from a later run of 72 configurations with three repeats and no spread: the over-prediction is not a preference-loss problem. A supervised control at the same effective row count misses by the same margin, so doubling the rows for a paired loss is the right thing to do and one shared coefficient is simply too high. Preference losses are where you notice it, not where it comes from.
In practice, on a 4 GB card with a 128k-vocabulary 1B model, DPO is allowed at max_length: 512 and refused from 768 up, even though it would probably fit.
That shipped deliberately, because under-predicting is the strictly worse failure. On Windows an overcommit is not an exception you can catch: WDDM silently spills into shared host memory, so the run completes an order of magnitude slower with no error at all. A run completing is not evidence that its configuration fits. Refusing is.
If the pre-flight refuses a preference run you believe fits, lower data.max_length first. Tightening the estimate for these losses is tracked upstream.
A packaging bug fixed along the way
Six preference trainers pass a field that TRL removed across several of its own releases, so on a fresh install soup train with task: orpo could fail at import before training ever started. It was invisible to CI because the TRL imports live inside the trainer's setup(), and no test had ever called setup() on those wrappers. The dependency bound that pins it was then settled by constructing all six configs against each candidate version rather than by reading source, which is the general lesson worth keeping: a version bound derived by reading source is a hypothesis, and the experiment that tests it is constructing the object.
This was a pre-existing defect, not one v0.72.4 introduced. A contract test now drives the real setup() for all six on the ordinary non-streaming path, and derives its covered set from the trainer sources so a seventh trainer joins automatically.
What is still out of scope
Layer streaming remains BETA.
taskmust be one ofsft,dpo,orpo,simpo,kto.ipo,bcoandtask: preferenceare refused.- GRPO and PPO are permanently excluded, not pending a release.
backend: transformers,modality: text, plain LoRA, andquantizationofnoneor4bit.- No tokens-per-second figure is claimed for any preference loss, at any model size, because none has been measured. A later run on borrowed hardware timed all four in samples per second, which is a different unit and a directional one; see the section below.
- The sizes above 8B that have been measured for streaming were measured on server hardware, not on the card this feature targets. What those runs establish.
Since measured on a real 8B
The numbers above come from a 365.2M-parameter synthetic fixture, which is the right size to make a second copy of the weights impossible to miss but the wrong size to be convincing on its own. The same claim was later checked on Llama-3.1-8B in NF4, streamed, through the shipped CLI, on an 8x H100 box:
| Streamed task | Reference model | Peak VRAM | Samples/s, median of 3 |
|---|---|---|---|
sft | none | 3,689 MB | 6.082 |
orpo | none, genuinely reference-free | 3,719 MB | 5.973 |
simpo | none, genuinely reference-free | 3,719 MB | 5.946 |
dpo | the same base, adapters disabled | 3,733 MB | 4.665 |
kto | the same base plus a separate KL forward | 3,669 MB | 3.595 |
Peak VRAM is flat across all five, within 64 MB or 1.7%. DPO costs 44 MB more than supervised fine-tuning, against roughly 5.6 GB for a second resident copy of an NF4 8B. That is the whole claim of this page, measured at a size where a second copy could not hide.
The throughput column needs three cautions. These are samples per second, not tokens per second, so they do not translate into the tok/s figures quoted elsewhere on this site. They come from 64-row single-epoch runs where setup is a large share of an 11 to 14 second step, so the ordering is reproducible and the absolute ratios are directional. And kto ran a different dataset, unpaired, so only its VRAM column compares directly.
Note that DPO's 0.77x here and the 1.52x layer reads quoted above are not the same metric and neither corrects the other: one counts samples per second end to end, the other counts weight reads per optimizer step.
One scoping note on a rule stated elsewhere: a preference loss on Llama-3.1-8B is refused by the pre-flight on a 4 GB card, and that is a property of the card, not of the model. On an 80 GB card the same configuration passes the pre-flight and trains, which is exactly what the table above is.
Measurement records and citation
The gate record behind this release is published in full, including the checks that failed, the diagnoses that turned out wrong, and the numbers that were measured and then discarded: the benchmarks directory. The layer-streaming work also has a preprint, 10.5281/zenodo.21771064, summarised on the paper page. Note the scope: v1 of the preprint measured the mechanism as it shipped in v0.72.3, v2 adds the 8x H100 session and the correctness repair that shipped in v0.73.0, and v3 retracts one explanation v2 gave without changing a measured number. The preference losses on this page came after v1 and are still outside what the paper measures.
See also
- Layer streaming — the mechanism, the measured numbers and the full refusal table.
- Scaling a streaming run — architectures, batch versus gradient accumulation, the pre-flight, resume and the disk tier.
- Preference variety — the preference losses themselves, and how they differ.
- Online DPO and the judge in the loop — preference training with a judge or reward model on-policy.
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.