Quant-Lobotomy Checker

Quantization — 16-bit → 4-bit, say — often destroys niche capability you care about while the aggregate score looks fine. soup eval quant-check runs one JSONL task file through a before model and an after model and renders a single OK / MINOR / MAJOR verdict for the pair, so you can catch a stealth regression before you ship it. Since v0.76.0 the verdict is also the exit code (MAJOR exits 2), so it can gate a CI step. It scores one average over the whole file, not one verdict per task or category; see "What can make an OK meaningless" below.

Run it

bash
soup eval quant-check \
  --before ./output/merged \
  --after ./output/merged-quantized \
  --tasks critical_eval.jsonl

Each side is loaded with AutoModelForCausalLM.from_pretrained, so each must be a Hugging Face-format model directory that transformers can load, such as a merged model or a quantized checkpoint directory, and it must sit under the current working directory. A standalone .gguf file cannot be loaded this way: it is refused up front with exit 3. Either side can instead be registry://<id> pointing at a registry entry; that resolves to the first artifact recorded on the entry, whatever its kind.

Output

One row, named default, whatever the category values in the tasks file:

Quant check
┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓
┃ Task    ┃ Before ┃ After ┃  Delta ┃ Verdict ┃
┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩
│ default │  0.540 │ 0.310 │ -0.230 │ MAJOR   │
└─────────┴────────┴───────┴────────┴─────────┘

What can make an OK meaningless

The score is one average over the whole tasks file: category is not split out, so run the command once per task file if you want a verdict per area.

If either side fails to load, the command stops (v0.76.0). It prints Failed to load --before model (<path>): <reason> (pass --allow-stub to score with deterministic stubs instead), naming the side that failed (--before or --after), and exits 1. A JSON file, an empty directory or a missing dependency all land here.

Before v0.76.0 that failure was silent. The command printed Live model load failed (...); using deterministic stub., replaced both models with a stub that returned the same canned text for every prompt, showed default | 0.000 | 0.000 | +0.000 | OK and exited 0: a pass that measured nothing. Any CI step that ran soup eval quant-check and looked only at the exit code passed on a broken load. Re-run those.

--allow-stub is the opt-in to the old behaviour, for a smoke run of the plumbing in CI without weights. The table is then titled Quant check (deterministic stub - not a live measurement), --format json carries "stub": true, and in the other formats a line (Failed to load --before model (<reason>); using deterministic stub.) names each side that fell back. A run where both sides fell back still measures nothing and exits 0. The verdict is computed from the scores as usual, so a run where only one side fell back compares a stub against a real model and can exit 2.

Exit codes

CodeMeans
0OK or MINOR on every task, which includes a run where both sides fell back to stubs under --allow-stub.
2At least one task is MAJOR.
1A model failed to load (without --allow-stub) or an unexpected error. A --tasks file that is not valid JSONL is one of them: it exits 1 at v0.76.0 (Invalid JSON on line 1), although upstream's changelog says 3.
3A usage or input error: an unknown --format, a --before, --after or --tasks path that is missing, outside the working directory or an unresolvable registry:// reference, a standalone .gguf, or a mistyped flag.

Before v0.76.0 the command exited 0 for every verdict and 1 for a bad path. A script that tested for exit 1 as "a bad regression" never fired; test for 2. See Exit codes.

Verdict thresholds

The classifier uses absolute drops in score:

  • OK — drop < 0.02 (or the score actually improved)
  • MINOR — 0.02 ≤ drop < 0.05
  • MAJOR — drop ≥ 0.05

Output formats

--format accepts table (default, Rich-rendered), json, or markdown. Pipe the output to a file if you want to persist the report:

bash
soup eval quant-check --before X --after Y --tasks t.jsonl --format json > report.json

Tasks file

Reuse any custom eval JSONL:

jsonl
{"prompt": "Solve: ...", "expected": "42", "category": "math.competition_hard", "scoring": "exact"}
{"prompt": "Capital of France?", "expected": "Paris", "scoring": "contains"}

The file name must end in .jsonl and hold at most 10,000 rows. prompt is required; expected defaults to empty, category to default and scoring to exact. scoring is one of exact, contains, answer, regex, semantic, tool_call_match, tool_call_name_match or tool_call_args_subset; any other value is refused with the line number. Two changes in v0.76.0 move scores: contains now requires the expected string to appear as a whole token (an ASCII alphanumeric run, so 14 no longer contains 4, though 3.4 still does), and answer is new: it compares the final answer the model states, and refuses at load a row whose expected states no single answer. A --before or --after score you recorded with the old substring contains can be higher than the same outputs score now. soup eval custom and soup ship --task-eval read the same format.

See also

  • Export — GGUF and the other formats (soup export --format awq and gptq are deprecated at v0.76.0, with removal tracked for v0.77)
  • Evaluation

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.