soup ship: the SHIP / DON'T-SHIP verdict (v0.71.25)
After a fine-tune there is exactly one question worth answering: did the model get better, or did I break it? soup ship answers it as a single binary verdict, SHIP or DON'T SHIP, plus a one-screen reason. It is not a dashboard to interpret, it is a decision you can gate CI on.
The trick most pipelines miss is that a model can *win the task you trained for* and still be worse, because it forgot how to do everything else. soup ship fuses both checks into one rule and refuses that model.
The decision rule
SHIP ⇔ (leg 1) task_tuned > task_base # strict improvement
AND (leg 2) every benchmark: base − tuned ≤ forgetting_threshold
else DON'T SHIP — even if the task metric looks great.- Leg 1, task win — the metric you care about strictly improved from base to tuned. A tie is not a win.
- Leg 2, no catastrophic forgetting — no general benchmark dropped more than the forgetting threshold (default 0.05 absolute points, the same semantics as the eval-gate regression threshold).
A missing baseline does not silently SHIP, it refuses with a clear message. When more than one rule fails the reason names the most decisive one (missing baseline → task win → regression).
Run it
# live: score a base model and a LoRA adapter on your task + the default suite
soup ship \
--base meta-llama/Llama-3.1-8B \
--adapter ./output/adapter \
--task-eval tasks.jsonl# judge the task win with an LLM, regress against named lm-eval suites
soup ship \
--base meta-llama/Llama-3.1-8B \
--tuned ./my-finetuned-model \
--task-eval tasks.jsonl \
--task-mode judge_score --judge-model ollama://llama3.1 \
--general-suite mmlu,gsm8k \
--baseline registry://abc123DON'T SHIP
Leg 1 task win (judge_score): 0.6200 -> 0.6100 [no win]
Leg 2 general suite (threshold 5.00%)
mmlu 0.7500 -> 0.6900 -0.0600 REGRESS
gsm8k 0.5200 -> 0.5300 +0.0100 ok
General benchmark(s) regressed past 5.00%: mmlu.
Catastrophic forgetting: DON'T SHIP even though the task metric was ok.Exit codes are CI-ready: 0 = SHIP, 2 = DON'T SHIP, 1 = runtime error.
Decide offline, with no model load
--evidence ev.json reaches a verdict from pre-computed scores, so CI can gate without a GPU:
soup ship --evidence ev.json --output verdict.json{
"task": { "mode": "metric", "base": 0.40, "tuned": 0.55 },
"benchmarks": {
"mini_mmlu": { "base": 0.80, "tuned": 0.79 },
"mini_common_sense": { "base": 0.60, "tuned": 0.62 },
"mini_instruction": { "base": 0.70, "tuned": 0.71 }
}
}--output verdict.json also persists the machine-readable verdict from a live run.
Pairwise judge win-rate (v0.71.31)
--task-mode pairwise decides leg 1 with a true head-to-head judge: for each prompt the judge picks base vs tuned, swap-debiased so a win only counts when both orders agree. The base is a 0.5 coin-flip, and the tuned model wins the leg only if its win-rate is above 0.5.
soup ship --base <m> --adapter ./out --task-eval tasks.jsonl \
--task-mode pairwise --judge-model ollama://llama3.1Offline, the --evidence task block takes { "mode": "pairwise", "base": 0.5, "tuned": <winrate> }.
Flags
| Flag | Default | Description |
|---|---|---|
--base | (required, live) | Base model id or path, the "before". |
--tuned / --adapter | (one required, live) | The "after": a separate tuned model, or a LoRA adapter on top of --base. Mutually exclusive. |
--task-eval | (required, live) | JSONL of leg-1 task-win eval items. cwd-contained, symlink-rejected. |
--task-mode | metric | Leg-1 mode: metric (eval accuracy), judge_score (pointwise LLM-as-a-judge), or pairwise (a swap-debiased judge win-rate, base vs tuned per prompt, base = 0.5). pairwise requires --judge-model (v0.71.31). |
--judge-model | (judge mode only) | Judge URL, validated by scheme and host (blocks SSRF prefix bypasses). |
--general-suite | mini suite | Leg-2 benchmarks. Default is the built-in mini suite; other names route through lm-eval. ≤ 50 names. |
--baseline | (none) | Recorded base leg-2 scores, registry://<id> or a JSON file, to skip the base run. |
--forgetting-threshold | 0.05 | Max allowed leg-2 drop in absolute points, in [0.0, 1.0]. |
--evidence | (offline mode) | Decide from a pre-computed scores JSON, no model load. O_NOFOLLOW, 16 MiB cap, cwd-contained. |
--output | (stdout) | Write the verdict JSON to a path (kept under cwd). |
--device | auto | cuda or cpu for the live run. |
Why no other tool ships this
Leg 2, the catastrophic-forgetting gate, is the moat. Unsloth, Axolotl, OpenPipe and Braintrust will all tell you whether your task metric went up; none of them refuse a model on the grounds that it *broke general knowledge*, as a first-class binary command. The regression math reuses Soup's existing eval-gate kernel, the new value is the single fused verdict, the one-screen reason, and a task-win leg that now includes a real pairwise judge win-rate.
v1 honesty
- Pairwise judge win-rate (
--task-mode pairwise) is live as of v0.71.31: the judge picks base vs tuned per prompt, swap-debiased (a win counts only when both orders agree), base is a 0.5 coin-flip and the tuned model wins only if its win-rate exceeds 0.5. Because CI resolves trl 1.x, a runtime adapter uses the pairwise judge on trl 0.19.x and the same judge pointwise on trl 1.x. - There is no `ShipConfig` schema yet,
soup shipis CLI-only in v1 (the verdict engine itself is pure-Python and fully tested). - Built-in mini-benchmarks are offline and instant, lm-eval suites block on a real model load.
See also
- Eval-gated training — halt a run mid-training on a quality regression;
soup shipis the post-training counterpart. - Model report card (soup diagnose) — the six-failure-mode x-ray that pairs with the verdict.
- Soup Loop — where the verdict slots into the traces → DPO → canary flywheel.
- Lean install + live wiring (v0.71) — the release line
soup shipcaps off.