Post-train X-rays (v0.66.0)
Mechanistic-interpretability on every fine-tune. Four probe families that surface what changed inside the model, not just what changed in outputs. Pure descriptive — no auto-mitigation, designed for CI logging and model cards.
soup probe sae-diff — Sparse Autoencoder feature movement
soup probe sae-diff ./gemma_scope.safetensors ./pre_acts.json ./post_acts.json \
--top-k 20 --output ./sae_diff.jsonEncode pre-FT and post-FT activation batches through a Sparse Autoencoder; report the top-K features whose mean activation moved the most. Closed SAE-repo allowlist — and as of v0.71.8 --auto-download fetches one from the HF Hub into ~/.soup/sae-cache/ (validated against the allowlist BEFORE any network call, via an SSRF-hardened snapshot_download):
- Gemma Scope (2B / 9B / 27B residual-stream)
- EleutherAI Pythia SAEs
- JBloomAus Llama SAEs
- OpenAI GPT-2 SAE
Bounds: top-k [1, 10K], up to 1M features, up to 1M tokens per batch, 16 MiB evidence cap. v0.71.8 also adds soup probe sleeper --weights, soup probe truth / harm, soup probe interference --measure (live PEFT multi-adapter N×N matrix), and soup train --capture-activations. That last one has a partner flag, soup train --capture-prompts <path>, which records the prompts the activations belong to; the capture is not much use without them, and the path is cwd-contained and symlink-rejecting like every other write Soup makes.
soup probe sleeper — defection-agent classifier
soup probe sleeper meta-llama/Llama-3-8B --evidence ./activations.json --output ./sleeper.jsonA linear defection probe applied to a 2D activation tensor ({"activations": [[...], ...]}, one row per token, each row as wide as the probe's hidden size, 4096 for Llama-3-8B; a mismatch exits 2). It reports the flagged-token rate and a verdict, and exits 2 on MAJOR:
| Flagged rate | Verdict |
|---|---|
| ≤1% | OK |
| ≤5% | MINOR |
| >5% | MAJOR |
The bundled probe weights are synthetic placeholders. With no --weights, the probe is a deterministic vector seeded from the base name: the math is real, but the six bundled bases carry no real calibrated vectors (the module itself lists this as a known limitation), so a verdict from the bundled probe only exercises the plumbing. It says nothing about your model. For a real reading pass --weights <probe.npz|.npy|.safetensors>, which also accepts any base id, or derive a probe from your own labelled activations with compute_contrast_probe (the contrast-pair method). The same holds for soup probe truth and soup probe harm, which take the same arguments (an honesty probe and a misuse probe, with 5% / 20% bands).
The base id is matched case-insensitively against the six bundled ids, and nothing shorter is accepted. The bundled bases are Qwen/Qwen2-7B, google/gemma-2-2b, google/gemma-2-9b, meta-llama/Llama-2-7B, meta-llama/Llama-3-8B and mistralai/Mistral-7B-v0.1; a short name such as llama-3-8b exits 2 (soup probe pack --list prints them). With no --evidence the command prints the probe's description and a "Pass --evidence" notice and exits 0 without scoring anything; if you also pass -o, the file it writes is a neutral report with 0 tokens and verdict OK, which is not a result. 16 MiB cap; symlink rejection.
soup probe interference — N×N adapter compatibility matrix
soup probe interference ./losses.json --output ./matrix.jsonInput is operator-measured per-pair losses; output is an N×N catastrophic-interference matrix:
score(A → B) = (loss(A_target | A+B) - loss(A_target | A alone)) / loss(A alone)The losses file is JSON, where the key "a|b" is the loss on A's domain with A and B loaded and "a|a" is the baseline with A alone:
{"adapters": ["a", "b"], "losses": {"a|a": 1.0, "a|b": 1.05, "b|a": 1.3, "b|b": 1.0}}Or let Soup measure them: soup probe interference --measure ./eval.jsonl --base-model <id> --adapter a=./a --adapter b=./b [--device cpu] (two or more adapters).
| Absolute score | Verdict |
|---|---|
| <5% | OK |
| <20% | MINOR |
| ≥20% | MAJOR (exit 2, gates CI) |
Bounds: 2..16 adapters (4..256 pairs). Adapter names ≤256 chars; markup-escaped before render against injection.
soup probe pack — bundled probes per base
soup probe pack meta-llama/Llama-3-8B --output ./pack.json
soup probe pack --listPer-base manifest listing the sleeper, truth and harm probes for that base (the same six base ids as above; any other id exits 2). Metadata only: no weights are embedded, and there is no probe-weights fetcher, so bring your own with --weights (no command fetches probe weights; soup probe sae-diff --auto-download downloads an SAE, which is not a probe weight). 1..32 probes per pack; per-field caps against operator-controlled-input bloat.
Influence-function blame (the CLI scores are synthetic)
Bundled in v0.66 alongside the probe family: a DataInf-style row attribution kernel that scores each training row by the cosine of its gradient against a probe gradient, times the row norm, and ranks the top rows. The kernel is real, but soup adapters blame does not feed it your adapter's gradients: the CLI calls it with no probe function, so it ranks deterministic synthetic vectors seeded from the adapter path, dataset path and layer label. Treat its table as a placeholder until a caller supplies real per-row gradients; see Adapters. The idea it is meant for pairs with v0.67 soup adapters bisect: bisect finds which checkpoint broke, blame is meant to say which rows caused it.
See also
- Diagnose — the 7-probe report card (6 at v0.56, citation recall added in v0.71.10); v0.66 adds 4 more probes on top.
- Adapter lifecycle — v0.67 bisect finds the broken checkpoint; blame is the intended follow-up for row-level attribution.
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.