Is the adapter alive: soup adapters check and audit

Two commands added in v0.76.0 answer the question a healthy loss curve cannot: did this run produce an adapter that does anything, and did it train the way the config said? Both exist because each of the failures below looked like a normal run. v0.72.0 saved layer-streamed adapters under keys that loaded as the untuned base, a streamed run on peft 0.21 or newer saved an adapter holding zero tensors, and the MLX backend accepted training options it silently dropped (see what MLX honours).

CommandQuestionNeeds
soup adapters checkIs this LoRA adapter, saved by the transformers path, trained at all?the adapter directory only
soup adapters auditDid the run use the settings in soup.yaml?the adapter directory and the config

Neither loads a model, uses a GPU or touches the network. They are part of the soup adapters group described on the adapters page.

soup adapters check

bash
soup adapters check ./out/adapter
soup adapters check ./out/adapter --json

It reads adapter_model.safetensors and adapter_config.json and prints one verdict as its last line: alive, or inactive: <reason>.

text
Adapter: /home/you/project/out/adapter
Total ||ΔW||_F: 13.316776
Live fraction: 100.0% (4/4 layers)
All-zero lora_B layers: 0/4
Leaked .inner. keys: 0
alive

For every LoRA pair it computes ||ΔW||_F = scaling x ||B A||_F in float64, where scaling is lora_alpha / r (lora_alpha / sqrt(r) when the config sets use_rslora, and 1.0 when adapter_config.json has no r or lora_alpha). The total is the square root of the sum of the squares. A layer is live when its lora_B is not all zero and its delta is above zero.

The verdicts

The first match in this list wins, so a leaked-key adapter reports the leak even if its weights are also zero.

Last lineWhat it means
inactive: leaked .inner. keys detected (N tensors)Tensor names carry the extra .inner. segment that v0.72.0 layer streaming wrote. Loaded into an ordinary model they match nothing, and the result is the untuned base. The weights themselves can be healthy; the adapter is still reported inactive
inactive: incomplete LoRA pairs detected (N orphans)A lora_A without its lora_B, or the reverse
inactive: shape mismatch in LoRA projections (N layers)A pair whose shapes cannot multiply. Best effort: a pair that happens to fit the transposed layout is read as that layout and reported alive
inactive: non-finite weights detected (NaN/Inf)The printed total reads nan
inactive: no lora weights foundNo lora_A / lora_B tensors at all. This is the verdict for the zero-tensor adapter described on the v0.75.1 page, and for a non-LoRA PEFT adapter such as VeRA
inactive: all lora_B layers are zero (N/N)The adapter is still at its initial state: lora_B starts at zero, and nothing moved it
inactive: lora_B layers are zero (k/N)Any all-zero lora_B marks the whole adapter inactive, even when the other layers moved
inactive: total ||ΔW||_F is 0.0Every delta is zero by another route
aliveEvery pair is complete, finite and non-zero, and the total delta is above zero

Optional lines list the offending tensors for Zero lora_B projections:, Incomplete LoRA pairs:, Shape-mismatched LoRA pairs: and Leaked wrapper keys: (up to five, then ... and N more); Standalone non-LoRA tensors: N gives only a count. In the per-layer names an internal __LORA__ placeholder stands where lora_A or lora_B would be.

Exit codes

CodeMeaning
0alive
2inactive. A usage error (missing ADAPTER, unknown flag) is also 2, so read the last line as well as the code
1The command could not read the adapter (see below)

Exit 1 covers: a directory that is missing, outside the current directory or a symlink; no adapter_model.safetensors; an adapter_model.bin (re-save adapter as safetensors); a weights file over 2 GiB, which is refused from its size before it is opened; a corrupt weights file; an adapter_config.json that is not a JSON object; and a missing safetensors package.

What it cannot check

  • It needs safetensors and NumPy. The command is described upstream as running without PyTorch, and it does, but neither package is a dependency of the light core install. On a core install it exits 1 with safetensors package required; pip install safetensors. The training extras bring both.
  • MLX adapters exit 1. The MLX trainer writes its weights as adapters.safetensors, and the command only looks for adapter_model.safetensors, so an MLX adapter directory reports no adapter_model.safetensors found. Use the audit command below for those: it reads the MLX record.
  • A weights file holding bfloat16 tensors exits 1. The command reads the tensors through the NumPy interface of safetensors, and NumPy has no bfloat16 type. On a hand-built file with bf16 tensors, read by the v0.76.0 binary, the result was Corrupt or unreadable safetensors weights in adapter_model.safetensors: data type 'bfloat16' not understood and exit 1, while a float32 file read normally. Whether an installed ml_dtypes package changes that was not checked, and which of your adapters hold bf16 tensors depends on how they were trained and saved.
  • Only LoRA adapters saved by the transformers path are in scope. A VeRA adapter, or any other non-LoRA PEFT adapter, has no lora_A / lora_B pairs and reports inactive: no lora weights found, which is a statement about the format, not about whether it trained.
  • rank_pattern and alpha_pattern are ignored. An adapter with per-module ranks gets the single lora_alpha / r scaling, so the printed ||ΔW||_F is a health number, not an exact delta.
  • alive means the adapter moved away from its initial state. It says nothing about whether the model got better. For that, run soup ship.

--json

--json prints one object with sorted keys: adapter, verdict, verdict_line, reason, total_frobenius (null when not finite), live_fraction, live_layers, total_layers, all_zero_lora_b_layers, inner_keys, orphaned_layers, shape_mismatches, standalone_tensors and per_layer (per pair: name, frobenius, is_b_zero, has_non_finite, shape_a, shape_b). The exit code is the same as in text mode.

soup adapters audit

bash
soup adapters audit ./out/adapter --config soup.yaml
soup adapters audit ./out/adapter -c soup.yaml --json

check asks whether the adapter is alive. audit asks whether the run that produced it used what you asked for. It loads soup.yaml through the schema (an omitted key is compared at its schema default), reads the settings recorded in adapter_config.json next to the weights, and compares them row by row.

The rows are optimizer, learning_rate (against the recorded peak, not the value copied from the config), scheduler, warmup_ratio, weight_decay, max_grad_norm, gradient_accumulation_steps, gradient_checkpointing, data.train_on_responses_only, lora.r and lora.alpha, plus nan_skip_fraction for task: grpo. Each ends as ok, DIVERGED or unknown. A setting the record cannot speak to is unknown and never ok.

The case it was written for is a warmup that rounds away. warmup_ratio: 0.03 over 12 optimizer updates is zero warmup steps, so training started at the peak learning rate:

text
warmup_ratio: 0.03 x 12 optimizer updates rounds to 0, so training started at the peak learning rate; raise warmup_ratio or lower gradient_accumulation_steps to produce more updates

4 divergence(s)

What the record contains depends on the backend

Adapter fromWhat adapter_config.json recordsWhat audit can check
The MLX backendThe effective optimizer, peak learning rate, schedule, warmup and total updates, weight decay, gradient clipping, accumulation, checkpointing, the response-only mask and the LoRA shapeAll eleven rows
The transformers backendPEFT's own file, which holds LoRA shape and nothing elseOnly lora.r and lora.alpha. The other nine rows read unknown, and the footer says so
Neither (an empty or unrecognised record)Nothing Soup can readEvery row is unknown

An adapter that agrees on what it can check exits 0 with No divergences, 9 unchecked (or 11 unchecked for an empty record). That is a statement about what was checkable, not a clean bill for the run. A CI job should read checked_count in the --json output to tell "everything agreed" from "nothing could be compared".

Exit codes

CodeMeaning
0No row diverged. unknown rows do not fail the command
2At least one row is DIVERGED
1A path outside the working directory or a symlink, no adapter_config.json, a missing or invalid config, or a config the schema refuses (an unknown key exits 1 with Could not load <path>: unknown config key '<key>' - refused.)

A forgotten --config is exit 2 as well. --config is a required option, Click rejects the call before the command runs, and Click's code is 2, the same as a divergence. A script that treats 2 as "the run diverged" cannot tell the two apart from the code alone.

--json

The object holds record_kind (peft, mlx or unknown), diverged_count, unknown_count, checked_count, rows (per row: setting, asked, ran, status, detail) and unknown_reason. Warnings from loading the config go to stderr, so stdout parses.

Where to go next

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.