v0.73.0 "Borrowed Hardware"
Every number this project had ever published was measured on one machine: a 4 GB RTX 3050 Laptop running Windows. From 5 to 9 August 2026 Soup ran on a borrowed 8x H100 box for the first time, on a much newer torch, bitsandbytes, trl and peft stack.
Three days there found one silent correctness defect in layer streaming, four backends that had never actually run, and a documented multi-GPU entry point that had never launched, alongside the first evidence that the laptop result reproduces on hardware nothing like it. That is the release. It is a minor bump rather than another v0.72.x patch because it adds two capabilities that did not exist and repairs four backends.
What the measurements established has its own page. This one is the changelog, and it starts with the part that costs you something.
Re-run this
Six defects produced runs that completed successfully and were wrong anyway. If any of these describes something you already did, the output is not trustworthy.
| If you | What happened | What to do |
|---|---|---|
Trained supervised with data.max_length above 1024 | Every such run was silently truncated to 1024 tokens. Measured: a config asking for 4096 produced 1024 tokens per sample, with no warning | Re-run on v0.73.0. Long-context, document and multi-turn tunes are the ones that suffered |
Used use_fsdp2_compile: true | The adapter reloads as all zeros. The tensors trained, but every key was saved with a torch.compile prefix, so loading matched none of them and left the weights at their zero init. The run exited 0 with a healthy loss | Re-run. This is the third defect of this exact shape in the project's history |
| Streamed a 32B or larger model in NF4 | Gradients were wrong on every layer but the last few, while the forward stayed bit-exact and the loss matched a resident reference to every digit | Re-run. 8B and 14B are below the threshold and are fine |
Relied on a soup ship verdict | Two of the three behavioural suites scored 0.000 on a model that does both tasks correctly, and the refusal detector missed the typographic apostrophe Llama actually types, reporting 0.300 for a model whose true refusal rate is 1.000 | Re-run the gate. Stored evidence from before this release is not comparable |
| Exported a GGUF at f16 and then quantised | The quantised export deleted your f16 file, even with an unrelated --output | Re-export the f16 if you still need it |
Followed the printed --no-reexec hint | The hint dropped every flag you had typed, so following it literally trained without --fsdp, --gate or --wandb | Check what you actually ran |
None of these is a streaming-only problem except the third, and none of them announced itself.
The defect worth reading about
In NF4, above roughly 165 MB per decoder layer, the backward pass produced silently wrong gradients. Issue #331.
The threshold is a bracket rather than a number: exact at 163.8 MB per layer, broken at 171.5, monotone on both sides across seven measured points. In practice that is 32B (234 MB per layer) and 72B (432), and never 8B (105) or 14B (132), both of which survive a 50-consecutive-backward soak at exactly 0.0. bf16 was never affected at any size.
The cause is buffer aliasing, not a race, and that was settled by measurement: a full cuda.synchronize() does not fix it and de-aliasing does. bitsandbytes.MatMul4Bit stashes the packed 4-bit weight and its quantisation state on the autograd context as ordinary Python attributes instead of going through save_for_backward, so gradient checkpointing cannot discard and recompute them. The reference captured during the forward aliases a streaming buffer, and by the time the backward reads it that slot has been refilled with a different layer.
The obvious repair was measured and rejected. Giving every pooled tensor a private copy is correct and costs only about 6% of throughput, but it takes peak VRAM on a real 32B from 4,220 MB to 19,720 MB, which is approximately the whole model. A repair proportional to the model rather than to one layer deletes the reason the feature exists.
What shipped instead keeps streamed NF4 weights out of that code path entirely: dequantise inside the checkpointed region and use a native matmul, so the saved tensor goes through the ordinary mechanism. It is gated on two real models against a control that reproduced the defect in the same process: 256 of 256 gradient tensors exact at 32B against the control's 8 to 12, and 320 of 320 at 72B, the size where the defect was worst, against the control's 8. It costs 2.9% peak VRAM and 4.8% throughput at 32B, and 2.6% and 3.7% at 72B.
The library behaviour it works around is upstream and unchanged, filed as bitsandbytes-foundation/bitsandbytes#2034.
One consequence for the headline number. The published 119.6 tok/s on a 4 GB card was measured before this repair and has not been re-run on repaired code. The repair cost 4.8% at 32B, so treat it as a pre-repair figure until someone re-measures it on that card. A server card cannot stand in: the whole point of the H100 result is that this method is bound by host-to-device transfer, so its throughput does not carry across machines.
Two new capabilities
A seed you can set
training:
seed: 1234 # weight init of new params, data order, dropout
data_seed: 7 # optional: vary data order while holding init fixedBoth are config keys, not CLI flags, and both default to unset rather than to 42. That is deliberate: an unset seed has to reproduce two different historical defaults, Hugging Face's 42 for the trainer and 0 for the multipack sampler, and defaulting to 42 would have silently re-ordered every existing multipack run. A boolean is refused by name, because true would otherwise become seed 1.
Scope, stated rather than left as a footgun: the seed reaches the trainer arguments on supervised runs only. On a streamed run it also seeds the adapter initialisation for all five streaming tasks, which is why a streamed run is bit-reproducible from a seed and a resident 4-bit one still is not.
Full fine-tuning as lora.r: 0
training:
lora:
r: 0 # no adapter: train the model itselfThe trainer's full-fine-tune branch was dead code with no way to enter it, and the only shipped spelling was the Spectrum workaround unfrozen_parameters: ['.*']. Rank 0 was chosen on repo evidence rather than taste: three consumers already treat rank 0 as "no adapter", and r: 0 previously crashed inside PEFT, so no config that worked before changes meaning. lora.r also gained a lower bound, because r: -5 used to parse and die deep in the library.
It writes a dense checkpoint, not an adapter, and it is refused with a named message when combined with a non-transformers backend, a non-text modality, any quantisation, Spectrum, LISA, or any LoRA-shaped option. It also refuses rather than running a no-op if your freeze settings leave nothing trainable.
Four backends that had never run
This is the part borrowed hardware bought, and none of it is a new feature. All four were documented all along.
soup train --gpus Nnever launched. The launcher handsacceleratea script path, and Soup was passing the Python binary, so accelerate parsed a CPython executable as source and every rank died before the trainer existed. Invisible to single-GPU CI, which skips the launcher entirely.- DeepSpeed could not train a LoRA model on any stage. LoRA leaves one of Hugging Face's two optimizer parameter groups empty, DeepSpeed drops it, and the scheduler then hits a strict length check. Verified repaired on two H100s. Repaired in the supervised trainer only; the other wrappers still carry the same exposure.
- SGLang serving returned 500 on every request. Its runtime returns a JSON string where Soup subscripted a dict. Deterministic, not a race, and never caught because SGLang does not run on Windows.
- The Liger kernel crashed at step 0 across the whole supported dependency range, because Soup patched the model but never told the trainer the fused path returns no logits. Separately, Liger's architecture match was a substring of the model name, so a model loaded from a local directory silently trained without it.
The vLLM backend was repaired rather than resurrected: it ignored the model's chat template, hand-rolling a prompt while the transformers backend used the real one. On Llama-3.1-8B with identical sampling settings, the hand-rolled prompt produced a run-on loop that burned all 200 tokens where the correct one answered in 8. It also reported finish_reason as a clean stop even when it had truncated, and --dashboard silently did nothing. A new soup serve --max-model-len exposes an engine setting that already existed but could not be reached from the command line.
Layer streaming, beyond the defect
- Streaming is now refused when
nn.DataParallelwould engage. Hugging Face wraps the model whenever more than one CUDA device is visible and the run is not distributed, and DataParallel needs every parameter on the first card while streaming keeps the decoder on themetadevice. It refuses, namingCUDA_VISIBLE_DEVICES=0as the fix, rather than quietly using one card of eight. It accounted for eight of the nine streaming-suite failures on the borrowed box and is unreachable on a single-GPU machine, which is why it survived four releases. - The four preference losses never set gradient checkpointing. One omission with two opposite symptoms: on an older trl an explicit
gradient_checkpointing: truewas silently dropped, and on a newer one the run died outright with ametadevice error. device_map: autobroke every distributed launch, in fifteen places. A first pass had fixed six; nine more were found by replacing the hand-written test list with a scan of every trainer module, because the list is what hid them.- The pre-flight panel was titled after a flag that does not exist. It read
soup train --stream-layers; there is no such option, and it was the first thing every streaming run printed.
Packaging
- Python is now 3.10 to 3.12. The upper bound was missing, so on 3.13 and above pip resolved untested PyTorch wheels and the failure was not a Soup error message but a loader crash inside the native extension, before any Soup code ran. The bound stops at 3.13 because 3.13 is equally untested, and a test derives it from the CI matrix so the two cannot drift apart.
trlsupport widened to>=0.14.0,<0.29, behind a capability-probe layer rather than a version table, because a version table was wrong twice before. The trainers now ask each config class whether it accepts a field, and resolve the moved classes through trl's experimental namespace. All six preference trainers construct and train to identical losses on three different trl versions.pip install "soup-cli[all]"now also pulls the MCP server extra.
Known limitations
Stated because they are the reason to trust the rest.
- The seed reaches the supervised trainer's arguments only; other tasks parse it and ignore it.
- A resident 4-bit run is still not bit-reproducible from a seed, while a streamed one is, because the adapter is built before the seed is set. That has a real consequence for the ship gate: three runs of one unchanged resident config moved two suites by 0.375 and 0.269 against a 0.05 threshold, so five of seven suites can cross the regression line on a re-run that changed nothing.
- The streaming VRAM pre-flight still over-predicts, and its per-logit constant was deliberately left where it is rather than lowered. The asymmetry decides it: over-predicting refuses a run that would have worked, which is visible and annoying with the data intact, while under-predicting on Windows is not an exception at all but a silent spill into host memory.
- DeepSpeed with LoRA is repaired in the supervised trainer only.
- The SGLang backend still hand-rolls its own prompt and hardcodes its finish reason, the two defects the vLLM rewrite fixed. Named rather than quietly skipped.
- The reward-hacking controller's mechanism is confirmed and its efficacy is not: at 7B the between-mode difference sits inside the within-mode spread.
- Two ship suites still rank by something other than the capability they name.
- The RAM-versus-disk streaming throughput gap remains unmeasured, because the borrowed box had no NVMe.
- Layer streaming remains BETA.
See also
- Validation on hardware we do not own — what the three days measured, in full, including the numbers that were discarded.
- Layer streaming — the mechanism, the measured numbers and the refusal table.
- What's new across the v0.71 and v0.72 lines — everything before this release.
- The paper — the preprint, unaffected in its correctness claims and widened in scope by this work.
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.