Compliance pack (v0.71.35)
Ship a regulated fine-tune with the paperwork it needs: start from a regulation-shaped config, publish a provenance-carrying model card, and gate every future change in CI.
1. Start from a compliance template
soup init --template hipaa # Protected Health Information
soup init --template soc2 # SOC 2 Trust Services Criteria
soup init --template eu-ai-act # EU AI Act Annex XI/XII
soup init --template sr-11-7 # SR 11-7 Model Risk ManagementBe clear about what a template is, because the honest constraint shaped the design: Soup's compliance controls are CLI flags and commands, not config keys. There is no audit_log: or bom: key in the schema, so a template cannot "pre-wire audit-log on" as YAML, and pretending otherwise would be a lie in a document meant to prove things.
Each template is therefore a valid, immediately trainable config on a license-clean Apache-2.0 base, plus header comments naming the exact commands for that regime: PII flagging and air-gap for HIPAA, BOM / attest / sign for SOC 2, Annex XI plus energy tracking for the EU AI Act, repro-receipt plus diagnose and ship for SR 11-7. Built-in templates went from 17 to 21.
The header comments in a generated file are a starting sketch, and some of their command lines omit flags the command requires: soup data pii and soup data decontaminate need --input (and decontaminate needs a benchmark source, see below), and soup bom emit and soup lock write need --base-model. Use the forms on this page rather than pasting the header lines.
2. The provenance path
soup data pii --input ./data/train.jsonl --output ./pii_flagged.jsonl
soup data decontaminate --input ./data/train.jsonl \
--benchmark-file ./benchmarks/eval_items.jsonl --output ./clean.jsonl
soup train --config soup.yaml --repro-receipt receipt.json
# EU AI Act: Annex XI/XII docs + measured energy
soup train --config soup.yaml --annex-xi annex_xi.md \
--track-energy --energy-country DEU --energy-out energy.json
soup registry push --run-id <run-id> --name my-model --tag v1
soup bom emit --name my-model --base-model <hf-repo-id> \
--base-sha <hex> --config-sha <hex> \
--energy energy.json --format both -o my-model.bom \
--attach-to-registry my-model:v1
soup adapters scan ./output
soup adapters sign ./output --backend ed25519 --generate-key key.pem
soup attest emit --stage train --subject my-model --sha <hex> \
--sign ed25519 --key key.pem -o my-model.attest.json \
--attach-to-registry my-model:v1
soup adapters verify ./output --strict
soup airgap-bundle --model ./output --output my-model.tar --repro-receipt receipt.jsonRead what the two data commands actually do before you rely on them.
soup data piiwrites the rows it flagged, not a cleaned file. Each flagged row carries a_pii_hitslist; nothing is removed or masked. It matches email, phone-number, SSN and card-number patterns only, so there is no medical-record-number detector (an MRN is caught only when it happens to look like a phone number). The optional[data-pro]extra swaps in Presidio.- It reads only
text,contentandmessages[].content. Alpaca (instruction/input/output), ShareGPT (conversations) and preference-pair rows are not scanned at all and report zero. Convert first (soup data convert <file> --to chatml) or check the dataset's format before trusting a clean result. The same row reader feedstoxicity,langdetectandscore. soup data decontaminatebundles no benchmark corpora.--benchmarks mmlu(the names aremmlu,gsm8k,humaneval,truthfulqa,arc,hellaswag) only records a label in the audit log and removes nothing. Real filtering needs--benchmark-file, your own JSONL of benchmark items (rows withtext,contentormessages[].content; apromptfield is not read, and a file with none of those loads zero texts and removes nothing), and a row is dropped when it contains at least--threshold(default 0.8) of an item's 8-grams (--n, default 8). With neither flag the command exits 1. The compositesoup data scoreworks the same way since v0.76.0: its Decontaminated row readsnot run (no benchmark texts)unless you pass--benchmark-file <jsonl>(your own comparison texts under the working directory;--thresholddefaults to 0.8),-b mmlualone exits 1 withscore --benchmarks labels need --benchmark-file; benchmark corpora are not bundled, and the labels are validated only: they do not select rows of the file. Through v0.75.2 that row printed0, which read like a measurement. A count with a file describes only the texts you supplied, not the absence of contamination against public benchmarks. The row that was labelledToxic flaggedis nowAbuse-keyword flagged, which is what the check is: a keyword heuristic, not a toxicity classifier. These outputs were run on the v0.76.0 CLI.
Annex XI/XII records not measured unless something measured it, and FLOPs are never measured. The training-compute, energy and CO2 rows of the document are written as not measured unless the run used --track-energy (energy and CO2), and the compute (FLOPs) row always reads not measured, because Soup does not measure it. If you filed an Annex XI or XII document written by an earlier release, regenerate it: through v0.75.2 a run without --track-energy printed 0 and 0.000 kWh / 0.000 kg for those rows as if they were measurements (#1446). The BOM, attestation and repro receipt changed in the same fix; the details are on Governance.
On the BOM and signing commands: soup bom emit --format both requires -o, which it treats as a prefix (it writes my-model.bom.cdx.json and my-model.bom.spdx.json), and --attach-to-registry needs -o too. soup card can only list what was attached. --generate-key refuses a path that already exists, so run it once; later commands reuse the file with --key key.pem. It writes the private key only; the matching public key is embedded in .soup-signature.json, so a plain verify proves the files still match what was signed, not who signed them. Pass --public-key <pem> with a key you obtained from the signer out of band to require that signer. scan exits 0 (OK), 1 (WARN) or 3 (FAIL); verify exits 0, or 1 on a missing or mismatched signature (3 with --strict). See supply-chain security.
Keyless signing instead of a key file (v0.76.0). With pip install "soup-cli[sigstore]", soup adapters sign ./output --backend sigstore and soup attest emit ... --sign sigstore -o my-model.attest.json sign with an ambient OIDC identity (for example a GitHub Actions job) and write a public Rekor entry that carries that identity; verify then needs --cert-identity and --cert-oidc-issuer supplied by you. Without the extra both signing commands exit 2 and write nothing, which is a change from v0.75.2, where --sign sigstore on attest emit silently produced an unsigned statement and exited 0. It is shipped, not validated: upstream's tests mock the signer and no end-to-end record exists. The flags, exit codes and one verify-side difference are on supply-chain security and Governance.
The audit log records every command automatically (soup audit-log tail, soup audit-log rotate).
3. Model card autogen
soup card my-model:v1 -o MODELCARD.md
# or override the auto-generated card when uploading:
soup push --model ./output --repo you/my-model --card my-model:v1soup card turns a registry entry into a publishable Hugging Face card carrying base model, the real training config, an eval scorecard, config and data sha256 hashes, lineage (ancestors), and a table of every registered artifact. Since v0.76.0 the scorecard shows the newest scored row for each benchmark, and a torchao export counts as a dense artifact like gguf or onnx when the card decides between adapter and full model.
Adapter versus full model is inferred from the registered artifacts and falls back to the training config's LoRA rank, with Spectrum and LISA full fine-tunes correctly treated as dense. This is not a detail: the live smoke caught a real LoRA run with no attached artifacts rendering Type | Full model and library_name: transformers, a false claim in a provenance document that 90 green tests had missed.
soup push --card is Hugging Face only (it warns and ignores on other hubs), and a bad ref fails fast before any network call.
4. Gate future changes in CI
soup ci init --data data/train.jsonl --suite expectations.yaml --evidence ship_evidence.json
# writes .github/workflows/soup-gate.ymlThe generated job runs, in order:
soup data validate <data> # dataset format compliance
soup expect <data> <suite> # PII / token-length / refusal expectations
soup ship --evidence <ev.json> # SHIP / DON'T-SHIP (exit 2 blocks the merge)Since v0.76.0 each step is wrapped so that exit 2 (a gate verdict) and any other non-zero exit (a usage or configuration error, exit 3 for these three commands) are labelled differently in the job log, and both fail the job: [soup] data validation failed: unusable data, [soup] expectations suite failed: quality regression and [soup] gate rejected: model failed ship checks for 2, and usage or configuration error (exit N) for the rest. An existing workflow file is not rewritten unless you pass --force. The three commands share one exit-code scheme, listed on Exit codes; soup ci init also takes --config soup.yaml to bind the ship step to a committed config, and --output to choose where the workflow is written. The workflow above was generated and read on the v0.76.0 CLI; it was not run on a CI runner.
The suite file (expectations.yaml above) is a list of mappings, each with a name and optional args:
expectations:
- name: expect_no_pii
- name: expect_token_length_between
args: {min_tokens: 32, max_tokens: 2048}
- name: expect_no_refusal_patternsoup expect exit codes (changed in v0.76.0). 0 means every expectation passed, 2 means an expectation failed, 3 means the suite or the data was refused (a usage or input error, and a mistyped flag is also 3), and 1 means the judge could not be reached. Through v0.75.2 a failed suite exited 3 and a rejected input exited 2, the reverse of the codes above, where 2 blocks the merge. A script that treats exit 3 as "the suite failed" now misses a real failure (exit 2) and reads a refused input as one. All of this was run on the v0.76.0 CLI against a two-row DPO file.
Arguments go under args:, and a mistake is refused when the suite loads. A key beside name, an argument the expectation does not take, or a top-level key other than expectations is refused before the data file is opened, with exit 3 and the nearest valid name: expect_token_length_between takes no argument 'max_token' (did you mean 'max_tokens'?); accepted: max_tokens, min_tokens. A bare name in the list (- expect_no_pii) is refused too (expectations[0] must be a dict). Through v0.75.2 these were dropped and the expectation ran on its defaults, so a misspelled bound passed a 5,000-token row. expect_token_length_between still counts whitespace-separated words, not tokenizer tokens, and its bounds default to 1 and 1,048,576.
expect_chosen_preferred_over_rejected_by_judge needs a judge. Pass --judge <url> (for example ollama://llama3.1 or an https:// URL), or put judge: under args: (--judge wins when both are set). With no judge, every row is reported as a violation (no judge configured: set args.judge (e.g. ollama://llama3.1) or pass --judge, or remove this expectation) and the command exits 2. To accept the existing labels on purpose, write args: {advisory: true}: the run passes and prints advisory: no judge ran; 2 rows not scored. A judge that cannot be reached after bounded retries exits 1. Before v0.76.0 the CLI ran this expectation with no judge and passed any preference dataset, so a green soup expect on a DPO file proved nothing about preference quality; re-run those gates. The judge call itself was not run here (it needs a model server).
What is scanned. The file is read as UTF-8 with a BOM allowed. A malformed line, or a line that is valid JSON but not an object, is refused with its line number (exit 3) instead of being skipped, and a file with no checkable rows is refused instead of passing. Upstream states that the PII, length and refusal checks now read the text of every format Soup trains on (Alpaca, ShareGPT, ChatML, DPO chosen and rejected, KTO completion, multimodal parts, tool calls, embedding pairs); a row with no extractable text fails the check instead of being skipped. On the v0.76.0 CLI a PII pattern in a DPO chosen field failed expect_no_pii with exit 2, where v0.75.2 never read that field. The soup data pii command did not change: it still reads only text, content and messages[].content, as described above.
Flags: --branch main, --python 3.11, --force. Every interpolated path is validated to stay under the repo root and shell-quoted, the branch and Python version are regex-gated so they cannot break out of their YAML context, and the write is atomic, symlink-rejecting, and refuses to clobber an existing workflow without --force.
GGUF export now works on Windows
The same release validated soup export --format gguf end to end on Windows for the first time (llama.cpp built CPU-only with VS2022 BuildTools, SmolLM2-135M exported to q4_0, q4_k_m, q8_0 and f16, then soup deploy ollama, then live inference). Doing so surfaced four real bugs, each independently fatal:
| Bug | Effect |
|---|---|
SOUP_DIR used relatively, not anchored to ~ | ~/.soup/llama.cpp was never found, so a fresh ~200 MB checkout landed in whatever directory you ran from |
The auto-clone pip-installed llama.cpp's requirements.txt | It pins torch~=2.2.1 against the CPU wheel index: torch 2.5.1+cu became 2.2.2+cpu, so CUDA was gone, and transformers 4.57 dropped to 4.46. Your first GGUF export silently destroyed your training setup. Soup now installs only the convert script's extra dependencies, unpinned, never touching torch |
| MSVC is a multi-config generator | It emits build/bin/Release/llama-quantize.exe, but only the flat layout was searched, so a correctly built llama.cpp was "not found" |
soup deploy ollama on a relative GGUF path | Ollama resolves FROM against the Modelfile's directory and Soup writes it to a temp dir, so it failed with "pull model manifest: file does not exist". The Modelfile now emits an absolute path |
That is the argument for validating a feature on real hardware rather than trusting a green suite: 361 export and ollama tests were passing throughout.
Security
Model-card injection was hardened, and the hole was pre-existing in soup push's own auto-card rather than new: the Training section interpolated base, task, scheduler and recipe unescaped into a card destined for the public Hub, and since base and scheduler have no charset validator, a crafted-but-schema-valid config could smuggle raw HTML, or a backtick that breaks out of the surrounding code span. All values now go through the markdown escaper, which also neutralises backticks and strips C0/ESC control bytes.
See also
- Governance & provenance — BOM, attestations, audit log, air-gap.
- Supply-chain security — ed25519 signing, scan gates, license conflicts.
- soup ship — the verdict the CI gate blocks a merge on.
- Model export — GGUF, ONNX, TensorRT; AWQ and GPTQ export are deprecated, with removal tracked for v0.77.
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.