v0.73.1: the free GPU tier

A patch release carrying everything that landed after v0.73.0. Its highest-value item is not the issue the slot was opened for.

The slot was opened to make the streaming VRAM pre-flight measure instead of predict. What sat beside it in the same pile was larger: bf16 was assumed on every CUDA card, in fourteen places, so every pre-Ampere card failed on every task, not only on layer streaming. That is a T4 on Colab's free tier, a T4 or a P100 on Kaggle, and every V100, GTX 16xx and RTX 20xx. The entire free notebook tier ran in a dtype its GPU has no units for, and said nothing about it.

Neither this release nor the one after it has a codename upstream. Borrowed Hardware is carried here as the name of the line.

Re-run this

If youWhat happenedWhat to do
Trained on a pre-Ampere card (T4, P100, V100, GTX 16xx, RTX 20xx)The run asked for bf16 on a card with no bf16 units. Every task, not only streamingRe-run on v0.73.1
Used backend: mlxIt never dispatched to the MLX trainer at all, silently falling through to the transformers path. And without a freeze first, the saved "adapter" was a full fine-tune: 172 tensors where 24 were expectedRe-run
Loaded an MLX adapter you had trainedadapter_config.json shipped target_modules unresolved, as {"keys": ["auto"]}, so loading dropped every LoRA tensor without a word. Generation with the adapter was bit-identical to the base model. auto is the schema default, so this was every MLX run that did not name its modules by handRe-train. The file cannot be repaired after the fact
Resumed from a periodic checkpoint-* under use_fsdp2_compilev0.73.0 repaired the final save only. Every intermediate checkpoint still loaded as a dead adapter, so --resume and load_best_model_at_end continued from a re-zeroed lora_B in total silence. Measured at 70B: 320 canonical keys in the output root against 320 prefixed ones in checkpoint-100Re-run from the start
Set training.seed on anything other than supervised fine-tuningIt was parsed and ignored. A seeded GRPO run trained at the framework default of 42, with no error and no warning, so replicates that differed only in their seed were the same runRe-run if you drew a conclusion from a seed sweep
Streamed at a sequence length above roughly 4,300 tokensThe VRAM pre-flight can under-predict, against a contract that says it never doesCheck the table below, or turn on the probe

That MLX adapter is the third time in this project's history that a healthy loss curve shipped a dead artifact, after v0.72.0's adapter keys and the full-fine-tune save above. The loss curve structurally cannot see it. Only the artifact can.

The headline: bf16 was assumed on every CUDA card

Fourteen places. Twelve trainer wrappers (bco, classifier, distill, dpo, embedding, ipo, kto, online_dpo, orpo, pretrain, reward_model, simpo) plus the streaming setup and the supervised wrapper's own precision resolver, each carrying some form of "if the device is CUDA, use bf16" with no capability check at all.

The trap recorded beside the fix is worth knowing, because the first attempt was a no-op on exactly the hardware it was written for. The capability check defaults to counting emulation, so a T4 answers yes when asked whether it supports bf16. The predicate now asks the question that distinguishes hardware support from emulation.

Two things make this the same shape as the four backends the borrowed H100 box found had never run: it cannot fail on an Ampere development box, and the knowledge already existed in the repository. One wrapper, the ASR trainer, already carried a comment naming pre-Ampere cards and had already fixed it, in that one file, never propagated. The repair replaced the hand-written list of wrappers with a scan of the trainer package, because a hand-written list is what hid them.

The correctness cross-check: streamed-versus-resident logits are bit-exact at 0.000000e+00 in float16 as well as bfloat16, in both quantisations, against resident references of matching numerics. That was measured using fp16 on an Ampere card, so it establishes the plumbing, not the Turing and Pascal kernels. What a genuinely pre-Ampere card does with bitsandbytes NF4 is not yet measured.

An 8B on a free Colab T4

With the repair in, the notebook that had been unrunnable on the published release runs.

Llama-3.1-8B in NF4, streamed, on a free-tier Tesla T4, batch 1, max_length: 256, LoRA r=8, fp16, with the process capped at 4.00 GB. The cap was shown to bite rather than assumed: a deliberate 4.29 GiB allocation was refused.

  • Measured peak 2.91 GB against a predicted 3.02, an over-prediction of 3.8%, which is the safe direction
  • 7 steps, exit 0, adapter written with 128 of 128 tensors non-zero

What that run does not establish is stated as plainly as what it does. No throughput figure is quoted from it, because a capped card is not a benchmark. Gradient exactness at 8B on a Turing card is not shown: non-zero tensors prove gradients flowed, not that they were right. The notebook's own streamed-versus-resident comparison produced no captured output on that run and is recorded as unrun, not as a pass. And one run, one seed, no repeats.

One thing it did surface: the pre-flight reported 15.10 GB free, the whole device, because the cap is enforced by the allocator and not by the driver. The fit decision was taken against a number 3.8 times larger than the budget actually in force. That is the open issue training.stream_vram_override exists to work around.

The notebook installs Soup from git, not from PyPI. Upstream has not yet switched its install cell back now that the fix has shipped, and its own text still says the fix is unreleased. If you run it, expect the git install and ignore that line.

The pre-flight was breaking its own contract

The streaming VRAM pre-flight's contract is that it never under-predicts, because on Windows an over-budget allocation does not raise. It spills silently into host memory, and the run slows by multiples with no error at all.

Measured through the real soup train on the 4 GB laptop, SmolLM2-135M streamed in bf16 at batch 1:

SequencePredictedReal peakRatio
43523.282 GB3.036 GB1.081x, over-predicts, safe
51203.844 GB4.118 GB0.934x, under-predicts
61444.590 GB5.830 GB0.787x, under by 21%

The existing guard could not have caught it. All ten rows of the grid that validates the estimator sit at sequence 256 or 512, so it varies batch and says nothing whatever about sequence length. A control only covers the variable it varies, and that is this project's own test applied to itself.

The mechanism is deliberately not claimed. The obvious candidate, the quadratic term from the attention score matrix, does not settle the numbers, and that is precisely the argument for measuring rather than adding another coefficient: a formula cannot model a term nobody has identified.

Two new config keys

Both are config keys, not CLI flags. Both are refused at config load if stream_layers is false, the same footgun gate the other streaming keys carry.

yaml
training:
  stream_layers: true
  stream_vram_probe: true          # decide the fit by MEASURING one real step
  # stream_vram_override: 4000000000   # or: assert what is free, in bytes

training.stream_vram_probe runs one real forward and backward at your configured shape and decides on that, instead of on the fitted formula.

  • Off by default, because it costs a step and it can refuse a run the formula would have accepted
  • task: sft only. A preference loss is not the step the probe runs, and its agreement with one is not established. The four streaming preference losses keep the fitted prediction, which is exactly where it was measured least accurate
  • Costs 1 to 5 seconds at ordinary shapes, and 52 seconds at a shape that does not fit, which is the honest worst case paid once instead of every step
  • Runs 12 to 14% conservative against the real training step, the correct direction for a gate
  • Cannot overrule a prediction more than four times over budget. The largest disagreement ever measured is 21%, so past a small multiple the config is simply too big and is refused by arithmetic without touching the GPU
  • Gates on allocated memory, not reserved. Reserved runs 1.08 to 1.41 times allocated, and gating on it would refuse this feature's own flagship configuration, which runs

training.stream_vram_override replaces the free-VRAM figure the pre-flight checks against, in either direction. It changes the yardstick, not the check: an override below real free VRAM still refuses a config that would otherwise fit, which is what makes it an escape hatch rather than a bypass flag. It is an assertion you are making, not a measurement.

The rest

  • training.seed now reaches every trainer wrapper, and is applied before the model loads, because the adapter's own initialisation is drawn before the trainer's seeding used to happen. An unset seed still resolves to 42 and data_seed still stays unset, so the values a run trains at are unchanged; what changed is when they arrive. Full detail on seeds and reproducibility.
  • The MLX backend reads neither seed field, and now says so in its existing "MLX backend ignores" line. A warning rather than a rejection, because a config valid on transformers should not become unloadable by switching backend.
  • training.batch_size gained a lower bound. batch_size: -4 used to load, and then meant whatever each trainer's arithmetic did with it, including the streaming pre-flight, which multiplies by it.
  • The dead calibration hook was wired up. The estimator's calibration entry point had no caller anywhere in the source; it now feeds both the budget and the printed panel. Every streamed run pays one transient 96 MiB allocation and two device synchronisations for it. On today's stacks that is a no-op in effect, which is the point of a guard against a stack that has not shipped yet.
  • mlx-lm floor raised to the version the MLX path is built against.

Known limitations

  • The probe is supervised-fine-tuning only, which is where the formula was measured least accurate.
  • The mechanism behind the long-sequence divergence is unidentified, so the formula is not fixed, only overrulable.
  • The probe measures a plain causal-language-model step, not the run's own step.
  • Windows still does not raise on an over-budget allocation, so the refusal direction cannot be verified by OOM on that platform.
  • fp16 streaming is not yet measured on a genuinely pre-Ampere card.
  • Layer streaming remains BETA.

The working record carries three readings that were withdrawn during this work, two of which looked like the headline result. They are left standing in the benchmarks directory with their refutations beside them, which is why that directory is worth reading and why you should link a file's top rather than deep-link a passage the same page later corrects.

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.