Adapter Lifecycle Finish (v0.67.0)
Six surfaces that complete what v0.57 soup adapters started. Adapters are now first-class versioned, collaborative, multi-tenant, evolvable, lockfile-tracked, bisect-able artifacts. We are not aware of a hosted vendor offering this set: evolutionary merge is mostly research demo, VeRA-style storage cuts against per-GPU-hour pricing, MoLE routing needs both a training and a serving stack, and adapter PRs need weights, eval and history together.
CMA-ES merge — evolutionary search over LoRA weights
soup adapters merge ./lora_a ./lora_b ./lora_c --strategy cmaes \
--eval ./tasks.jsonl --budget 1h --output ./mergedSakana-style evolutionary merge. Pure-Python rank-mu CMA-ES (no cma dependency). Softmaxes N-1 logits onto the simplex, samples a population, keeps the elite half, plateau-detects after 3 generations without improvement (converged=True).
- 2..16 adapters; population
[2, 256]; generations[1, 10K] - Budget
[60s, 24h]— reuses v0.57blame.parse_budget - Eval-fn failures swallowed with sentinel -1e9 score (one broken eval ≠ crashed run)
- Live as of v0.71.4 — the full loop merges, materialises, and scores each candidate against the eval suite and writes the best-weighted merge.
--canaryand--strict-verdictare refused with--strategy cmaes(exit 2); they belong to the other strategies --evalis a.jsonlfile of eval tasks (prompt, plus optionalexpected,scoringandcategory), the same filesoup eval custom --tasksreads. A.yamlsuite is not read, even though the "requires --eval" error says "YAML/JSONL". Because every scoring failure becomes the sentinel, a wrong suite does not stop the search: it finishes with exit 0, aBest scoreof -1000000000.0000 in the final panel and an arbitrary "best" merge, so check that line
VeRA / VB-LoRA vector bank — multi-tenant adapter economics
from soup_cli.utils.vector_bank import BankEntry, VectorBank, write_bank, estimate_bank_size
bank = VectorBank(
name="customer-personalisation",
base_model="meta-llama/Llama-3-8B",
projection_seed=0,
vector_dim=128,
entries=(
BankEntry(user_id="user_1", scaling=(0.31, -0.07, 0.0, ...)), # 128 floats
BankEntry(user_id="user_2", scaling=(0.0, 0.0, 0.0, ...)),
),
)
write_bank(bank, "./bank.json")
# 128-D scaling vector at fp32 ≈ 512 bytes / user
# vs. ~30 MB per rank-16 LoRA on Llama-3-8Bentries is a tuple of BankEntry(user_id, scaling), each scaling a tuple of exactly vector_dim floats; the ... above stands for the rest of them, so the snippet needs real vectors to run. projection_seed and vector_dim are required.
Shared random projection P (vector_dim × vector_dim) + per-user scaling vector v_u. Thousands of per-user adapters at MB-each instead of hundreds-of-MB per LoRA. Atomic JSON I/O + cwd containment + symlink rejection + 16 MiB cap.
estimate_bank_size(num_users=..., vector_dim=...) (keyword-only) for sizing. Live as of v0.71.12 — soup serve --bank <bank.json> [--bank-strength S] reconstructs the projection + per-user vectors and installs a decode-time hook; the active user is chosen per request via the X-User-Id header (unknown/absent id is a zero-delta no-op, so no cross-request leak).
MoLE — per-token gating over task LoRAs
# soup.yaml
task: moe_lora_routing
training:
mole_task_adapters: # the live form: paths to the frozen task LoRAs
- ./adapters/coder
- ./adapters/math
mole_temperature: 1.0 # softmax sharpness
mole_top_k: 2Mixture of LoRA Experts. Gating network routes per-token activations to top-K task adapters via softmax over hidden state. Backend-cross-validator rejects mlx. Live as of v0.71.12 — mole_task_adapters: [...] trains a real per-token gating network that blends N frozen task LoRAs (only the router trains); the gate is saved as mole_gate.pt.
Changed in v0.76.0, and one of these means a finished run may be wrong. The gate is the only tensor MoLE trains. On a CUDA device named plainly cuda it was cast to bf16 with no fp32 master copy, so its gradient and AdamW moments were bf16 as well and most AdamW steps rounded away; a gate trained that way on CUDA kept most of its initial weights (#1266). The gate is now fp32 on every device string and is created on the base model's device. mole_gate.pt is saved in fp32, and soup serve --mole loads it and a bf16 file from an earlier run alike. If you trained a router gate on CUDA before v0.76.0, retrain it. Also new: soup train --resume now takes effect for task: moe_lora_routing. Before, the trainer's train() call received no checkpoint, so a resumed run started again from step 0 and overwrote the checkpoints; now each checkpoint-N carries its own mole_gate.pt and a resume restores it with the adapters frozen again (#1220).
soup adapters pr — GitHub-shaped adapter pull requests
soup adapters pr "Better politeness on EU support tickets" \
--base-sha <64-hex> --adapter ./candidate \
--eval ./eval_delta.json --samples ./sample_diffs.json \
--output ./PR.mdPR = {base SHA, dataset diff, adapter weights, eval-delta report} rendered as review-friendly Markdown with eval-delta table + per-sample baseline/candidate diffs:
| Metric | Baseline | Candidate | Δ |
|---|---|---|---|
| judge_score | 7.4 | 8.2 | +0.8 |
| retry_rate | 12.1% | 4.6% | -7.5% |
_md_table_escape neutralises the backtick, |, \n, \r and \t in operator-controlled cells. JSON output also available for v0.68 GitHub Action. Bounds: ≤64 deltas, ≤256 samples, ≤32 KiB per sample.
soup lock — shared run lockfile
soup lock write --base-model meta-llama/Llama-3-8B \
--base-sha <64hex> --dataset-sha <64hex> --env-hash <64hex> \
--output soup.lock
soup lock show soup.lock
soup lock check --base-model ... --base-sha ... --dataset-sha ... --env-hash ...soup lock check exits 2 on drift and 3 when the lock file is missing or unreadable, the SHA is not 64 hex characters, or the call is mistyped. That is a move in v0.76.0: through v0.75.2 drift was 3 and a missing lock was 2, so a CI step written for the old codes reads them the wrong way round. soup lock show on a missing file is now 3 as well. soup lock write did not move: it still exits 2 for a missing environment lock or a malformed SHA.
Closure of (base_model_sha, dataset_sha, env_hash):
closure_sha = SHA256(base_sha || dataset_sha || env_hash)--env-hash is optional on soup lock write. Left out, it is derived from soup-env.lock in the current directory (the file soup env lock writes), or from the file named by --env-lock <path>; the command exits 2 if that file is missing. An explicit --env-hash wins and --env-lock is then not read. soup lock check has no such fallback and still requires --env-hash.
Commit soup.lock to git so the whole team coordinates on the same reproducible run. soup_version + created_at are advisory only — legitimate operator upgrades don't trigger drift. Composes with v0.64 soup env lock (provides env_hash) and v0.64 soup plan (provides base/dataset hashes from config).
soup adapters bisect — binary search over training history
soup adapters bisect ./ckpt-0500 ./ckpt-1000 ./ckpt-1500 ./ckpt-2000 \
--eval-command "soup eval gate --suite evals/gate.yaml --baseline baseline.json --model {ckpt}"Binary search over ordered checkpoint history. Operator supplies a shell template with {ckpt} placeholder — Soup uses shlex.split after shlex.quote(ckpt) (argv-list mode, no shell=True). Probes both endpoints first (short-circuits all-OK / all-broken), then ~log₂(n) midpoint probes. Exit 2 on BROKEN_AT (2 is also the usage-error code).
The eval command is the pass/fail test, and it must exit non-zero on a bad checkpoint. Bisect treats exit 0 as PASS and anything else as FAIL, including a command whose executable is not found or that times out after an hour. soup eval gate does exit non-zero when the gate fails (2 since v0.76.0, 1 before), which includes a score that drops past its regression threshold against the baseline, and that is why the example uses it. A usage error from the gate command (exit 3) is non-zero too, and bisect reads it as a failed checkpoint, so run the command once by hand before you let it search. soup eval custom --model {ckpt} --tasks ./regression.jsonl does not work as the test: it exits 0 whenever the eval completes, whatever the accuracy, so every checkpoint passes and the verdict is ALL_OK even if the model collapsed.
After bisect finds the broken checkpoint, soup adapters blame is the intended follow-up for row-level attribution, but its CLI scores are synthetic at v0.76.0, so do not rely on them.
Numbers
+165 tests in v0.67.0 (10,836 → 11,021), 7 new test files. CMA-ES eval-wiring went live in v0.71.4, VeRA multi-tenant serve in v0.71.17, and the MoLE gating kernel in v0.71.12.
See also
- Adapter algebra (v0.71.34) — task arithmetic over LoRA deltas, the newest member of this command family.
- Adapters (v0.57) — diff / merge / blame / branch / checkout, the foundation v0.67 builds on.
- Is the adapter alive — v0.76
soup adapters checkandsoup adapters audit, for the adapter a merge or a bisect hands you. - Post-train x-rays — v0.66 influence-function blame, the intended follow-up to
bisect(its CLI scores are synthetic at v0.76.0). - Pre-flight & tooling — v0.64
soup env lock+soup planare the inputs tosoup lock.
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.