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

bash
# 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
bash
# 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://abc123
text
DON'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:

bash
soup ship --evidence ev.json --output verdict.json
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.

bash
soup ship --base <m> --adapter ./out --task-eval tasks.jsonl \
  --task-mode pairwise --judge-model ollama://llama3.1

Offline, the --evidence task block takes { "mode": "pairwise", "base": 0.5, "tuned": <winrate> }.

Flags

FlagDefaultDescription
--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-modemetricLeg-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-suitemini suiteLeg-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-threshold0.05Max 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).
--deviceautocuda 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 ship is 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