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
soup eval quant-check \
--before ./output/merged \
--after ./output/merged-quantized \
--tasks critical_eval.jsonlEach 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
| Code | Means |
|---|---|
| 0 | OK or MINOR on every task, which includes a run where both sides fell back to stubs under --allow-stub. |
| 2 | At least one task is MAJOR. |
| 1 | A 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. |
| 3 | A 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.05MAJOR—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:
soup eval quant-check --before X --after Y --tasks t.jsonl --format json > report.jsonTasks file
Reuse any custom eval 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 awqandgptqare 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.