soup diagnose — seven failure-mode probes, one verdict

After you finish training, the question is no longer "did the loss go down" — it's "did I quietly break something." soup diagnose (v0.56.0) is a post-training model report card that runs 7 pure-function failure-mode probes:

ProbeWhat it catches
forgettingPer-task Δ accuracy with tolerance band — extends v0.25 eval/forgetting. Live, it is scored on --holdout prompts or a built-in general-knowledge set, never on your training rows (v0.76.0)
refusaladvbench / xstest delta over caller-supplied generators (_MAX_REFUSAL_SCAN = 8192)
formatJSON / regex / tool-call validity over the RLVR verifier set, with an explicit ReDoS probe (compiled.search("a" * 128))
mode_collapsePairwise n-gram-Jaccard distance over K completions (k ∈ [2, 32], ngram_n ∈ [1, 8])
memorizationTraining-prefix echo via partial-prompt continuation (_MAX_SCAN_ROWS = 1000). Live, it scores clipped completion-bigram precision against the held-out suffix (v0.76.0)
contaminationn-gram overlap with public benchmarks (combined-complexity cap rejects when training rows × benchmark corpus > 1e9)
citationcitation-recall regression on RAFT-shaped rows: does the answer still cite the supporting [doc-id]? (v0.71.10)

Every probe is a pure function over a generator callable, which is why the same code path serves both modes. Since v0.71.7 soup diagnose can supply that generator itself: pass --base-model (with --dataset, and optionally --holdout, --tokenizer, --device, --citation-style, --shuffle-seed and --allow-not-run) and it loads the model and runs the probes for you. Without --base-model it stays a pure scorer over evidence you supply, and you wire in your own GeneratorFn around a serve backend.

Usage

bash
soup diagnose <run-id> \
  --evidence ./evidence.json \
  --output ./diagnose.json \
  --badge ./badge.svg \
  --attach-to-registry registry-id

--attach-to-registry <id> links the --output file to that registry entry as a diagnose_report artifact; <id> is an entry id, a unique id prefix, name:tag or registry://<id>. A requested attach is part of the command's success: an unknown or ambiguous entry, or a registry that cannot be read, prints an error and exits 1 with the report left on disk, and the flag given without --output is refused before anything runs (exit 3). Before v0.76.0 it attached nothing. The command passed three positional arguments to a registry helper that takes only the entry id positionally, printed a yellow Warning: could not attach to registry: TypeError: attach_artifact() takes 1 positional argument but 3 were given and carried on with exit 0, so a report you believed was on a registry entry never was.

Verdict thresholds (composed in compose_report):

  • ≥ 0.85 → OK
  • ≥ 0.60 → MINOR
  • < 0.60 → MAJOR
  • NOT_RUN — see below; it is not a score

NOT_RUN: a probe that did not run is not a pass (v0.76.0)

A live probe that was requested but produced no measurement, such as a --dataset or --holdout with no usable prompt and answer rows or a probe that raised, is reported as NOT_RUN, never as OK 1.00. The evidence column keeps the reason, including the exception text. NOT_RUN ranks above MINOR and below MAJOR in the overall verdict, and the table and the badge show it grey as not run instead of a score (the stored 0.0 in the JSON report is a placeholder, not a measurement).

soup diagnose exits 3 when the overall verdict is NOT_RUN. Pass --allow-not-run to keep exit 0 for runs that knowingly skip probes. MAJOR still exits 2 and wins over NOT_RUN. Probes that do not apply stay a neutral OK (format when the dataset targets are not JSON, contamination without a benchmark corpus, citation on non-RAFT rows, the dataset probes when no --dataset is given). On the --evidence path a mode left out of the file is still a neutral OK and is not gated, while a mode listed with "verdict": "NOT_RUN" (score omitted or 0.0) is NOT_RUN. A --tokenizer that cannot be resolved is an input error, exit 3, raised before any model loads. Run with no --evidence and no --base-model, it still prints every mode as probe not run (no evidence), scores them OK and exits 0: that is the scorer with nothing to score, not a request that failed.

bash
soup diagnose my-run --evidence ./evidence.json
json
{"scores": {"forgetting": {"score": 0.0, "verdict": "NOT_RUN"}, "refusal": {"score": 0.95}}}

That evidence file makes the command above print the overall verdict NOT_RUN and exit 3; add --allow-not-run and it exits 0.

What the live probes read (v0.76.0)

  • --holdout <jsonl> feeds the forgetting probe: the first 12 usable prompt and answer rows, or built-in general-knowledge prompts with reference answers when the flag is unset. --dataset keeps feeding the memorization, format and citation probes. If --holdout has no usable rows the forgetting probe reports NOT_RUN.
  • Rows are read the way training reads them: ShareGPT conversations, Alpaca instruction plus input (and system when present), ChatML messages (the turns before the first assistant reply are the prompt), tool-calling rows, and DPO or KTO rows with string prompt and answer fields. Memorization needs text, not pairs, so it also runs on plaintext rows; the probes that need a pair report NOT_RUN when plaintext is all there is. LLaVA, ShareGPT4V and audio rows are skipped because the probe generators are text-only, and a row that carries both an audio field and messages, which used to be probed as text, is now NOT_RUN. A prompt over 8,192 characters or containing a NUL byte is skipped by the prompt and answer probes, and Rows skipped: ... reports the count per cause (for example 2 unreadable, 1 over-long).
  • Memorization compares adjacent normalised words, ignoring stopwords and words of two letters or fewer, or adjacent sub-word tokens with --tokenizer. It replaced the earlier set-Jaccard threshold, so memorization results saved by an older release are not directly comparable.

Re-run this. A live soup diagnose from before v0.76.0 can mislead in four ways. The forgetting probe scored the first 12 rows of --dataset, the adapter's own training data, which a fine-tuned adapter answers at least as well as its base, so it could read OK 1.000 while general knowledge had been lost. A probe that never ran scored OK 1.00. A model answering in Cyrillic, Greek, Arabic, Hebrew, Hindi or CJK could read as MAJOR mode collapse, and token_f1 forgetting probes scored zero retention, because the tokenizer discarded non-ASCII characters and standalone digits. And Alpaca prompts now include input and system, so scores on Alpaca data move. Re-run the report.

Output: a frozen FailureReport (run_id / base / adapter / scores / overall / soup_version / extras) plus an embeddable SVG badge from render_badge_svg, one cell per probe (html-escaped — safe for HF model cards). Missing modes fill via neutral_score.

soup train --diagnose-gate

bash
soup train --diagnose-gate ./evidence.json

Runs after training has finished: the "Training Complete!" panel has printed, the adapter is already saved and the tracker has already recorded the run as finished. It reads the evidence JSON you pass (a scores object keyed by mode, each entry with a score and optionally a verdict and evidence string), runs no probe itself, and scores any mode you leave out as a neutral OK. If any supplied mode is MAJOR it prints the offending modes and exits 2, so a CI step fails. Since v0.76.0 a mode listed as NOT_RUN is refused too, with exit 3, and an evidence file that cannot be read exits 1. It does not withhold or delete the checkpoint, and the --help text for the flag ("refuses to mark the run successful") overstates it. The diagnose_report artifact kind is registered in the v0.26 registry alongside eval_suite and canaries from v0.55.

Exit codes

CodeMeans
0Overall OK or MINOR, or NOT_RUN with --allow-not-run. With no evidence and no --base-model every mode is a neutral OK.
2Overall MAJOR. Also an --evidence file that does not exist, an invalid --citation-style and a mistyped flag.
3Overall NOT_RUN without --allow-not-run, --attach-to-registry without --output, or a bad --tokenizer.
1An --evidence file that is not valid JSON, a live run that failed, an unwritable --output, or an attach that failed after the report was written.

soup diagnose is not one of the eleven commands that moved a mistyped flag from 2 to 3, so one code still means two things here: 2 is a MAJOR verdict and also a mistyped flag or a missing evidence file, and 3 is NOT_RUN and also an input error. See Exit codes.

See also

  • Eval-gated training — pre-training gate at epoch boundaries
  • Quant-check — quant-specific regression check
  • Eval design — design the evidence your gate scores
  • Eval depth (v0.65) — 4 deeper failure-mode probes (behavior, capability, checklist, irt-subset) that stack on top of the 7 diagnose probes
  • Post-train x-rays (v0.66) — 4 mechanistic-interpretability probes (sae-diff, sleeper, interference, pack) — failure-mode coverage 7 → 11

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.