v0.73.3: four flags that did nothing
Every one of the 24 pull requests in this release came from someone other than the maintainer. Eight people, five of them appearing here for the first time. The maintainer's own work in that window arrived as four direct commits: license headers, a test repair, a CI guard.
That is worth stating plainly because of what outside eyes found. Not a missing feature, and not a crash. Four separate flags that the schema validated, the documentation described, and nothing in the codebase ever read.
Re-run this
| If you | What happened | What to do |
|---|---|---|
| Trained with assistant-only loss masking | The tokenizer returns a mapping that is not a dict, so a type guard missed it and the label mask was built from the mapping's key strings. No exception, no warning, a normal-looking loss curve, and zero trained tokens | Re-run. An adapter from such a run learned nothing |
Trained on Apple Silicon with quantization: 4bit | Device detection did not know MLX, so the run reported "CPU (no GPU detected)" and silently rewrote 4bit to none | Re-run if you needed 4-bit. The run did train, just not at the precision you asked for |
Followed the command soup train --no-reexec printed | The printed hint dropped the flags you actually typed, so following it literally trained without --fsdp — and the run succeeded, so nothing pointed back at the hint | Re-run under the flags you meant |
Set training.bnb_4bit_use_double_quant: false | Nothing read it. Every 4-bit path hardcoded double-quantization on, so the setting changed your config fingerprint and nothing else | Nothing to re-run: you got the default. But the key is live now, so the same config will train differently from here |
One more consequence of that last row, and it is cosmetic rather than a correctness problem: because the resolved config now carries the field, a config that never set it will show a one-time fingerprint drift in soup ship --evidence provenance and soup lock check, with the model numerics unchanged.
Four flags that did nothing
training.bnb_4bit_use_double_quant
Every place Soup builds a BitsAndBytesConfig hardcoded use_double_quant=True. The flag is now threaded through the three call sites Soup owns: the resident loader, the layer-streaming path (which reads it once and passes the same value to both the sharder and the meta skeleton, so streamed-versus-resident bit-exactness cannot drift), and the 4-bit save path.
The design choice is the more interesting half. The field is Optional[bool] defaulting to unset, not a plain true, because a true default emits the key into every dumped config and trips the "requires quantization: 4bit" guard on re-validation. That was measured, not guessed: 21 of 173 shipped configs stopped round-tripping, which would have broken train --replay and soup sweep. So unset resolves to the shipped default, an explicit false now genuinely disables, and only an explicit true fires the footgun check.
One path cannot honour it. The Unsloth loader builds its own BitsAndBytesConfig internally with double-quant hardcoded on and exposes no override, so it is out of scope by construction rather than by omission.
A matching CLI flag lands with it:
soup merge --adapter ./output --save-format 4bit --no-double-quantIt applies to the 4bit and 4bit_forced save formats and is ignored for fp16.
Apple Silicon was read as a CPU
detect_device() only probed PyTorch MPS and fell back to 'cpu', which fired a false "4bit quantization is not supported on CPU" warning and took the quantization down with it. The label was never the harm.
Both detect_device() and get_gpu_info() now take an optional backend. With backend: mlx they resolve to mlx with the chip name, report Apple unified memory in telemetry, and preserve quantization: 4bit for pre-quantized mlx-community checkpoints, which is a different thing from bitsandbytes NF4 and was never the CPU case the warning was written for.
The decision itself moved out of a branch buried in a long function and into an explicit, testable resolve_quantization(). The CUDA-shaped analytical VRAM pre-flight is also skipped on the MLX path, because Apple unified memory is managed by Metal rather than by a fixed VRAM pool.
The launch hint that dropped your own flags
soup train --no-reexec prints the accelerate launch command you should run yourself. It was built from a second, hand-maintained copy of "what the user typed", and that copy was short.
Rather than patch the printed copy, the contributor deleted it and derived the hint from the argv that actually launches the run, so the two cannot drift again. Four flags that used to fall off now survive: --name, --replay, --replay-ratio and --replay-seed.
data.interleave, which is still open
The fourth one is not fixed, and saying so is the point. data.interleave validates, documents and parses, and nothing reads it at training time. It was found during review of a different fix and filed rather than papered over.
What did get fixed is the command that made it visible. soup data mix --optimize is the one command whose entire output is a config file, and it wrote one that would not load: data.train came out as a YAML list against a field typed str. It now renders the single highest-weighted dataset, keeps the full ranked weight and path breakdown as a comment above it so no information is lost, and says in that comment that the mixture is not yet consumed by training.
The other failure that a loss curve cannot see
On Windows, GetExitCodeProcess returns 259 for a running process, but 259 is also a legal exit code, so a child that exited *with* 259 was indistinguishable from one still running and read as alive forever. That defeated reconcile-on-read for the experiment tracker and could wedge the MCP one-active-execution cap shut, refusing every later execution with no error an operator could act on.
The check now waits on the process handle, which is signalled the instant the process exits whatever its code, and falls back to reading the exit code only if the wait itself fails. The two byte-identical copies of that primitive became one shared module, with a test that fails if a third appears.
Alongside it, a run whose watcher died is no longer reported running forever. The tracker reconciles on read: a running row whose recorded process is gone is rewritten to terminated with an unknown exit code, deliberately, so a lost outcome is never recorded as a success. The richer completed and failed statuses are left alone.
The MCP server can now execute
This is the release's one genuinely new capability, and it is a big enough change that it has its own page.
soup mcp serve --allow-execute used to reserve a gate it never opened. It now runs a planned training or export behind a short-lived, single-use, server-generated confirmation token bound both to the plan and to the execution kind. Two new tools, train_execute and export_execute, accept only that token: no command, no argv, no shell string, no client-supplied environment. The server count goes from 16 tools to 18.
A note on auditing this one. The
--helptext for--allow-executewas not updated by the feature commit and still says execution is reserved for the future. The changelog, the command reference and the running code all agree that it executes. On this flag, at this release, the binary's own help string is the stale party.
Also in this release
soup ship --noise-floormeasures leg 1 in every task mode. At v0.73.2 it was--task-mode metriconly.judge_scorenow scores the base side N times through the judge, andpairwisejudges the base model against itself, where the expected win rate is 0.5 by construction, so the spread is measured rather than inferred. Because those repeats fold in the judge's own sampling noise, that floor is labelled decode + judge on the panel and stampedjudge_inclusivein the evidence, so it can never be read as a decode-only number. It costs N extra judge API calls.eval.ship.noise_flooris committable tosoup.yaml, bounded to 2 through 10 from the same constant the CLI validates against, with the usual CLI over config over default precedence. It was the one gate-policy flag a team could not put in a committed config.- A boxed answer with a space no longer scores as no answer. v0.73.2 shipped a pattern for the exact spelling quoted in its ticket and missed
\boxed {A}, which the typesetting language permits and models emit. Same defect, one space to the left. It is closed here. soup draft distill --steps Ndelivers about N optimiser steps instead of roughly a fifth of them. The old arithmetic ignored that the validation split removes rows and that gradient accumulation makes several micro-batches into one step. The resolved budget is now printed up front, and the emitted config pins the run shape so the arithmetic and the trainer cannot disagree.soup env checkflags an installed package that violates Soup's own declared bounds, exit code 3, read from package metadata rather than a second hardcoded copy, and independent of whether you ever wrote a lock file. It exists because installing a fast serving stack into a training environment silently pushestransformerspast the cap Soup itself declares.- Layer streaming sees through a paravirtual disk. A virtio device reports itself as rotational with no media hint, so a genuinely NVMe-backed cloud disk measured at 1.5 GB/s was refused the disk-overflow tier, which is the audience that tier exists for. When the flag is unreliable, media type is now decided by a bounded direct sequential read; a genuinely slow disk is still refused.
training.stream_disk_kindis the override, and it prints what it overrode beside what was detected. bomandattestationare first-class registry artifact kinds, sosoup bom emitandsoup attest emittake--attach-to-registryandsoup cardlinks them for free. Signed attestations attach their detached signature sidecar too.- Two new recipes,
qwen3.5-4b-pretrainanddeepseek-v4-flash-grpo, taking the catalog to 144.
What this release deliberately did not do
No new measurement was made, and no benchmark record was added. Upstream states that rather than leaving it implicit: every item here is a contributor patch, reviewed and green in CI at merge, and none of it was gated by a new number. The published measurement records are unchanged, and so is the preprint, whose measured figures and scope this release does not touch.
Two of the four flags are also not fully closed. data.interleave has a filed issue and no training-time reader, and the same list-shaped defect exists in a second renderer. The release is named for a pattern it found, not for one it finished.
Known limitations
data.interleavestill has no reader at training time. Filed, not fixed.- The
--allow-executehelp text is stale in the shipped binary. It describes the pre-v0.73.3 behaviour. - The Unsloth loader cannot honour
bnb_4bit_use_double_quant. It builds its own quantization config with double-quant hardcoded on. - A
stream_disk_kindoverride carries no measured rate, deliberately, so a later refusal can never cite a reading you overrode. - The noise floor is still n=3, one model, one dataset. Widening it to the judge modes changed its scope, not its statistical weight: it sizes an effect, it does not calibrate a threshold.
- The MLX repair was not accompanied by a resolution of the reported MLX hang, which remains open and unreproduced.
See also
- The MCP server — the execution gate, the token, and all 18 tools.
- The ship gate — where the noise floor lives.
- v0.73.2: the release gate — the release before this one, and the two items it left for this one.
- Everything new across v0.71, v0.72 and v0.73.
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.