v0.75.1: an adapter with nothing in it, and eight hardening fixes
Every change in this release is the maintainer's own work, so there is no contributor list this time. That is a departure worth naming, because the release it patches carried 60 pull requests from 22 people outside the maintainer, and the two before it were the same shape.
It is also a backport rather than a cut of the development branch. Its tree is the v0.75.0 tag plus 25 cherry-picks, one test adaptation and the release commits. The branch was 186 commits past v0.75.0 when this was cut, so releasing from it would have shipped an entire minor's worth of unreleased work under a patch number.
Upstream gave it no codename, for the third release running. The two things in it that cost you something are an adapter that saved with nothing in it, and a security advisory with three credentials to rotate.
Re-run this
Four of this release's fixes mean something you already ran was lost, deleted, or left running and billing. None of them raised.
| If you | What happened | What to do |
|---|---|---|
Trained with stream_layers: true on peft 0.21 or newer | The saved adapter is a 40-byte file holding zero tensors. trainer.save_model(), every save_steps checkpoint and get_peft_model_state_dict() all returned nothing, and nothing raised | Re-train. It cannot be recovered: there is no key to rename and no association to restore. Check a file with python -c "from safetensors.torch import load_file; print(len(load_file('adapter_model.safetensors')))" — 0 means the run produced nothing |
Typed soup runs clean --keep-weights on Click 8.1 | It did the opposite of its name and deleted whole non-best checkpoints. The option was a bare flag with a True default, which Click 8.1 treats as a toggle, so passing it explicitly parsed as False | Nothing to undo, but check what you still have. The flag is now the paired --keep-weights/--no-keep-weights, so it means the same thing on every Click version |
Ran soup train --cloud modal | The container was ephemeral, so the checkpoints went with it, while the entrypoint printed a line telling you to download them from a directory that had none | Re-run. Outputs now land on a soup-outputs volume and are downloaded at the end, including after a failed run |
Pressed Ctrl+C during soup train --cloud lambda --cloud-submit | The controller was killed a quarter of a second later, while it was still inside the block that terminates the paid instance | Check your Lambda console for an instance still running. soup now waits for the controller across the interrupt |
Rotate these credentials
Three of the eight security issues disclosed here sent a credential somewhere it should not have gone. These are upstream's own conditions, reproduced rather than reworded, because narrowing them would be the wrong kind of helpful:
- Ran
soup push --hub modelscopeor--hub modelerson 0.53.10 to 0.75.0 while logged in to Hugging Face or withHF_TOKENset? Rotate your Hugging Face token at huggingface.co/settings/tokens. - Had a judge URL on 0.33.0 to 0.75.0 pointing anywhere other than
api.openai.comwhileOPENAI_API_KEYwas set? Rotate that key. - Passed
--tool-auth-token,--auth-tokenor--api-keyon 0.71.3 to 0.75.0 and collect your audit log anywhere? Rotate those tokens and purge the log.
Two clarifications that widen who should treat themselves as affected by the first bullet, neither of which changes upstream's condition. The token chain also reads HUGGINGFACE_HUB_TOKEN and the legacy ~/.huggingface/token, which the shorthand "logged in to Hugging Face or with HF_TOKEN set" does not name. And if the ModelScope or openMind SDK was not installed, the command failed on the import before the token was used, so nothing left the machine.
The adapter that saved with nothing in it
This is the third defect of one shape, and the shape is worth naming because it has now produced three different symptoms.
The streaming wrapper holds the real decoder layer as a child named inner. Its module and parameter names carried .inner. while its state_dict() keys were canonical. That asymmetry was documented on this site as a known limitation for three releases, and it looked cosmetic.
peft 0.21 made it fatal. Where 0.20 selected an adapter's tensors by the lora_ substring, 0.21 reads prefixes off model.named_modules() and keeps the matching state_dict() entries. The names and the keys no longer met in the middle, so the selection returned an empty set, and an empty set is not an error.
adapter_model.safetensors 40 bytes, 0 tensorsThe fix is a rename, not a pin: the wrapper now enumerates the inner layer's tree at its own prefix, so names and keys are one spelling. Ten peft-version-independent tests pin them together, and the four tests that documented the old asymmetry were inverted rather than deleted.
Soup does not pin peft below 0.21. The declared bound is peft>=0.20.0,<1.0.0, so a fresh install resolves 0.21 by default, and the remedy for anyone not yet on v0.75.1 is pip install "peft<0.21" on their side.
One thing the rename also prevents, stated carefully because it is a trap in the other direction: transformers builds its weight-decay grouping from named_children(), so the same asymmetry would have dropped every LoRA parameter into the no-decay group. It did not, because before this fix both halves were spelled .inner. and agreed with each other. The rename keeps them moving together.
Nothing on the streamed forward path changed, so no measured number moves. The 119.6 tok/s laptop row, the 3.32 GB peak, the H100 cross-check and the bit-exactness ledger are all untouched, and all their existing caveats still apply.
Security
This release is the fix for advisory GHSA-63h4-gvp4-r26g, rated high, affecting 0.7.0 through 0.75.0. It was found by a full-repository review with OpenCodeReview and verified by the maintainer. The advisory itself carries the complete detail; this is the summary, with the affected range on each.
1. Credentials sent to the wrong service. soup push resolved the Hugging Face token for every hub and handed it to the ModelScope or openMind SDK, so pushing to either disclosed a token that is often write-scoped to a third party (0.53.10 to 0.75.0). Separately, a judge URL of the form https://<host>/<model> was classified as the OpenAI provider whatever the host was, so OPENAI_API_KEY went to it as a Bearer token (0.33.0 to 0.75.0). Each hub now authenticates only with its own credential, MODELSCOPE_API_TOKEN or MODELERS_TOKEN or a --token typed on the command line, and only api.openai.com receives OPENAI_API_KEY. Both are behaviour changes on upgrade: HF_TOKEN no longer works for a non-HF push, and a self-hosted judge that relied on the key being forwarded must now be given one explicitly.
2. soup adapters verify --public-key accepted unsigned adapters (0.71.2 to 0.75.0). The trusted key was consulted only inside the ed25519 branch, so a record with any other backend verified with exit 0, even under --strict. That is the one failure mode a signature check exists to prevent.
3. The Web UI rendered dataset and run content as HTML (0.7.0 to 0.75.0) and sent no Content-Security-Policy. Inspecting a third-party dataset could therefore run script in the origin that holds your bearer token and can start training. Every rendered value now goes through one escaping template, inline handlers are gone, and the UI sends a CSP with no unsafe-inline for scripts and no eval, plus X-Content-Type-Options, Referrer-Policy and X-Frame-Options. Chart.js is pinned with Subresource Integrity.
4. soup serve tool and adapter routes checked neither Host nor Origin (tool routes 0.53.7 to 0.75.0, adapter routes 0.30.0 to 0.75.0), so a page the operator merely visited could reach them through DNS rebinding, and adapter activate could be driven cross-site. The tool token was also compared with != rather than in constant time. See the serving page for exactly which routes now answer 421 and which answer 403, because the two halves are deliberately different.
5. soup mcp serve --allow-execute did not pin every approved input (0.73.3 to 0.75.0). A train plan digested only some config paths, and two of the names in that list are not schema fields at all, so a reward model, PRM, teacher model, reward-function file or unlearning set could be swapped between plan approval and execution. Inputs outside the working directory were silently not pinned rather than refused, the one-active-execution check read only the 50 newest runs, and a bookkeeping failure after the child started left it running unsupervised with the slot held.
6. Config regexes could hang loading or training (lr_groups 0.41.0 to 0.75.0, unfrozen_parameters 0.71.23 to 0.75.0), and the detail worth keeping is that the old guard was itself the problem: it ran the untrusted pattern against a 128-character probe to decide whether the pattern was safe. It is replaced by a structural check that parses the pattern and never matches with it.
7. Credential option values were written to the audit log (0.71.3 to 0.75.0) whenever they did not begin hf_, sk- or Bearer. Masking was by value shape, and is now by option name, which is why --tool-auth-token, --auth-token and --api-key were the three that leaked: their values usually do not look like the patterns.
8. Reading an untrusted .can archive could exhaust memory, through YAML alias expansion in an attestation that was counted after its size cap rather than before, or through manifest and config members read whole regardless of size. Manifest fields were escaped for Rich markup only, so terminal control sequences could rewrite the soup can run confirmation panel (0.26.0 to 0.75.0).
Also in this release
soup runs cleangained--no-keep-weights, and a repo-wide check now fails any true-default flag that has no negative form, so the Click-toggle shape cannot come back.- Modal runs keep their outputs, on a named volume, committed in a
finallyso a crashed run keeps what it produced. - The Lambda interrupt path waits for its controller, and two narrower windows found after the first fix were closed with it: a signal arriving between the process being spawned and the guarded region being entered, and one landing on the "still waiting" notice, which is written from the interrupt handler itself.
GET /v1/adapters, activate and deactivate now require the tool token too, with the check running before the 404 branches so an unauthenticated probe cannot learn which adapter names are loaded from an "unknown adapter" reply. One disclosed consequence:soup loop's deploy step posts with no authorization header, so against a token-protected endpoint it now fails closed.- Every local input a train plan reads is pinned, from twenty fields rather than five, with a test that fails when a new path-shaped field is added without being classified either way.
What was measured, and what was not
No gate record, and upstream states it rather than leaving it implicit: nothing in this release was decided by a new measurement, every item is a correctness or hardening fix with its own test, and no file was added to the benchmarks directory.
The preprint is unchanged. No measured number in it moves and its scope does not change, because nothing here touches layer streaming, its mechanism or its architecture list.
Test files go from 475 to 500. The release body also prints a suite total and a collected count; neither appears here, for the same reason no total has appeared on this site since v0.73.3.
Known limitations
- 26 tests fail on the maintainer's development box, and none is a regression. All are the NF4, bitsandbytes and streaming files, and they fail with identical identifiers on a clean v0.75.0 copy, which is the control that makes "not a regression" checkable rather than asserted. The underlying divergence is the Blackwell one: two bitsandbytes 4-bit compute paths disagreeing on
sm_120. CI cannot see it, because those modules skip without a GPU and the hosted runners have none. - The POSIX interrupt test for the Lambda controller runs on CI only, because it needs a real process group and a real signal, so it is skipped on Windows. The platform-independent half runs everywhere.
- The Web UI token still lives in
sessionStorage. The escaping work removed the sinks that could read it and the policy forbids inline script andeval, but the storage choice itself is unchanged. - Three regex sinks still have no complexity check, in the custom eval path, the diagnose formatter and the recipe runner. The new check shipped where config-supplied patterns reach peft and the trainers; these three take patterns from other inputs and were deliberately left out of a security release rather than swept in.
soup can run's extracted config still goes through the generic loader, bounded now by the pre-run size check, but the loader itself is unchanged.
See also
- Layer streaming — the empty-adapter notice, beside the two earlier defects of the same shape.
- Scaling a streaming run — the limitation this release closed.
- Serving — which routes now check
Host, which check onlyOrigin, and why they differ. - The MCP server — what a plan pins now.
- Experiments —
soup runs cleanand the Click 8.1 trap. - Backends — where Modal outputs go, and the Lambda interrupt.
- v0.75.0: MLX honours its config — the release before this one.
- Everything new across v0.71 to v0.75.
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.