soup adapters — git for LoRA
After 6 weeks of running experiments you have 30 adapter folders and no idea which one to ship. soup adapters (v0.57.0, finished in v0.67.0) treats LoRA adapters as first-class versioned objects.
soup adapters has sixteen subcommands. The table covers the git-for-LoRA set from v0.57 and v0.67 plus soup lock; list, info and compare are the basic inspection commands, and arithmetic is v0.71.34.
| Command | What it does | Shipped |
|---|---|---|
soup adapters diff | Per-layer Frobenius + relative drift + SVD effective rank | v0.57 |
soup adapters merge | 5 strategies: linear / TIES / DARE / SVD / CMA-ES | v0.57 + v0.67 |
soup adapters blame | Dataset-row attribution: the plan is real, the scores the CLI prints are synthetic (see below) | v0.57 + v0.66 |
soup adapters branch | Name a LoRA config with SHA-pinned hashes | v0.57 |
soup adapters checkout | Copy the branch's config back out; refuses if that file has changed | v0.57 |
soup adapters branches | List everything | v0.57 |
soup adapters pr | GitHub-shaped PR markdown with eval-delta + sample diffs | v0.67 |
soup adapters bisect | Binary-search checkpoint history for regression boundary | v0.67 |
soup lock write / show / check | SHA256 closure over base, dataset and env; exit 3 on drift | v0.67 |
| (v0.60 supply chain) | scan / sign / verify / check-safetensors | v0.60 |
All adapter math is pure numpy, no torch. .bin adapters are rejected with an actionable "re-save as safetensors" message. See Adapter lifecycle (v0.67) for the v0.67 surfaces.
soup adapters diff
soup adapters diff adapter-a/ adapter-b/ --top-k 10 --format markdownFor each layer:
- Frobenius norm of the delta
- Relative drift (Frobenius / norm of a)
- SVD effective rank —
effective_rank(s) = exp(H(s²/Σs²))whereHis Shannon entropy. Tells you whether a high-rank adapter actually used its rank.
--top-k ranks layers by drift. --format accepts table / json / markdown and routes through render_report_*.
soup adapters merge — 5 strategies
soup adapters merge a/ b/ c/ -o merged/ \
--strategy ties --weights 0.4,0.3,0.3 --density 0.7 --seed 0
# v0.67 — evolutionary search over the simplex
soup adapters merge a/ b/ c/ -o merged/ \
--strategy cmaes --eval ./tasks.jsonl --budget 1hStrategies:
linear— weighted average. Baseline.ties(Yadav et al. 2023) — trim bydensity→ elect majority sign → disjoint average. Tied-sign defaults to+1.dare(Yu et al. 2024) — Bernoulli drop withdensity, rescale by1/density, deterministic via--seed.svd— linear-merge then low-rank SVD reconstruction (--rank).cmaes(v0.67, Sakana-style) — rank-mu CMA-ES (pure Python, nocmadep) over N-1 softmaxed logits on the simplex. Population [2..256], generations [1..10K], plateau-detects after 3 no-improvement gens. Eval-fn failures swallowed with sentinel score so one broken eval ≠ crashed run.
--eval must be a .jsonl file of eval tasks: one JSON object per line with a prompt and optionally expected, scoring and category, the same file soup eval custom --tasks reads. A .yaml suite is not read, even though the "requires --eval" error text says "YAML/JSONL". The sentinel is the catch: any failure while scoring a candidate (an unreadable suite, or a base model that fails to load) scores -1e9 instead of stopping the run, so a wrong suite still finishes with exit 0, a Best score of -1000000000.0000 in the final panel, and an arbitrary "best" merge. Check that line before you use the result.
Live as of v0.71.4 — --canary <canary.json> scores the merged adapter OK/MINOR/MAJOR via the Quant-Lobotomy taxonomy (--strict-verdict exits 2 on MAJOR), and --strategy cmaes runs the full evolutionary eval-suite loop (merge → materialise → score → write best). The canary file holds precomputed scores shaped {"baseline_scores": [...], "candidate_scores": [...]}, not an eval suite, and --canary and --strict-verdict are refused with --strategy cmaes (exit 2): the --eval suite already drives that search. The backdoor-scan + license-conflict gates run for every strategy.
soup adapters blame — dataset-row attribution (the plan is real, the CLI scores are not)
soup adapters blame adapter/ --dataset eval.jsonl --layer q_proj.7 --budget 5m --shards 4 --plan-onlyThe plan half is real. It splits the dataset into --shards hold-out ranges (2 to 100) and checks they fit a wall-clock budget. parse_budget accepts 60s / 5m / 2h (bounds [60s, 24h], default 4h). _MIN_PER_SHARD_SECONDS = 30 checks feasibility, so if the budget can't cover shards × 30s the plan is reported infeasible and nothing runs. It returns a frozen BlamePlan with one BlameShardWork per shard, and --plan-only prints it and stops.
Without --plan-only the command goes on to print a ranked influence table (--top-k rows, 1 to 10,000, default 50; -o also writes JSON). At v0.75.2 that table is not measured from your adapter. The v0.66.0 runner implements a DataInf-style influence score (cosine of a row gradient against a probe gradient, times the row norm) and takes a caller-supplied probe function that returns real per-row gradients, but the CLI never passes one. With no probe function the runner falls back to a deterministic synthetic probe: random per-row vectors seeded from the adapter path, dataset path and --layer string. The adapter is never opened and the dataset is only counted, so the run finishes in a fraction of a second, and an adapter directory that does not exist still prints a table and exits 0. Use the row_id and influence columns as placeholders, not as evidence about your data. --layer is a label recorded in the output (up to 256 characters), not a selector.
soup adapters branch — named, SHA-pinned configs
soup adapters branch chat-llama-v3 -c soup.yaml --base meta-llama/Llama-3.1-8B --dataset data.jsonlRecords a frozen Branch:
name chat-llama-v3
config_path soup.yaml
config_sha256 a3f1...
dataset_sha256 7c92...
base_model meta-llama/Llama-3.1-8B
created_at 2026-05-15T23:14:02Z
soup_version 0.58.0Branch-name regex: ^[A-Za-z0-9][A-Za-z0-9._\-]{0,127}$. Config files capped at 1 MiB. Pointer file capped at 1,024 entries. Atomic write + POSIX 0o600. SOUP_BRANCHES_DIR env override containment-checked to $HOME / $CWD / $TMPDIR.
soup adapters checkout — copy the branch's config back out
soup adapters checkout chat-llama-v3 -o restored-soup.yamlCopies the config file the branch points at to -o, but only if that file still hashes to the recorded config_sha256. A branch stores a pointer and hashes, not a copy of the config. Edit soup.yaml after making the branch and checkout exits 2 with branch config drifted from snapshot SHA, and because the original contents were never stored, the earlier version cannot be restored from the branch at all: keep a copy or a commit of any config you may want back. The dataset hash is recorded for provenance but is not re-checked, so editing the dataset does not make checkout fail.
See also
- Adapter algebra — v0.71.34 adds
soup adapters arithmetic: add, scale and negate LoRA deltas. - Registry — lineage DAG of full training runs
- Soup Cans — package an adapter + config + data ref into a portable bundle
- Adapter lifecycle (v0.67) —
soup adapters merge --strategy cmaesevolutionary search,soup adapters prGitHub-shaped reviews,soup adapters bisectregression history, andsoup lockshared SHA256 closure - Supply-chain security (v0.60) —
soup adapters scan / sign / verifyfor backdoor detection + Merkle-root tamper protection
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.