v0.77.0: what v0.76 warned about is now refused

v0.77.0 was tagged on 2026-10-26. Upstream gave it no codename and titled it with a sentence again: what v0.76 warned about is now refused. It is a cut of the development branch, not a backport like v0.76.1, and it carries v0.76.1's seven changes. In the release body's words, it "ends the one-release notice v0.76 gave: the config fields and values that v0.76 loaded with a "Soup v0.77 will refuse it" warning now stop the load, and so do fifteen settings that v0.76 accepted without a word and never applied." Fourteen of those fifteen stop the load; the fifteenth, a malformed KTO label, drops the row at data load instead (see below).

151 of the 226 pull requests in this release came from 44 people outside the maintainer. That is the release body's own count; the names are under Credits.

Four things in it cost you something if you skip them:

  • A config that loaded on v0.76 can stop loading on v0.77.0. Run soup doctor --config soup.yaml before you upgrade a working setup. See Before you upgrade.
  • Python 3.10, AWQ export and GPTQ export are gone. pip on Python 3.10 keeps installing v0.76.x, and you can pin it with soup-cli[train]<0.77. AWQ and GPTQ export need v0.76.x and its [awq] or [gptq] extra. See Install and upgrade.
  • The two soup serve routes that run code need a token on every bind, loopback included. See The soup serve tool routes.
  • Some finished runs trained something other than what their config said, among them a dpo, ipo or grpo run resumed from a checkpoint. See Re-run this.

Before you upgrade

bash
soup doctor --config soup.yaml

Run it with v0.77.0 installed, on each config you train from. A refused config prints Config validation error:, one indented line per refused setting, then Config could not be loaded (see above)., and the command exits 2. soup train exits 1 on the same config, soup train --dry-run included. Since v0.77.0 soup doctor no longer prints All checks passed! above a refusal (#2100, #2106); its system, GPU and dependency tables still print first. A config that leaves the refused settings unset, or writes them at their defaults, loads as before. The exception is training.quantization on the eight tasks that never quantise: there an explicit 4bit, the schema's default, is refused, and only leaving it unset or writing none loads.

This is what v0.77.0 prints for a config that sets four staged fields and one deprecated value (lines joined where the terminal wrapped them):

text
Config validation error:

  training.lr_groups is accepted but read by nothing; refused as of v0.77. Delete the line.
  training.early_stop_patience is accepted but read by nothing; refused as of v0.77. Delete the line.
  training.rl_checkpoint_include_ref_model is accepted but read by nothing; refused as of v0.77. Delete the line.
  data.video_fps is accepted but read by nothing; refused as of v0.77. Delete the line.
  data.remove_unused_columns: true never had an effect (no trainer reads it; the trainers that set it pass false). Delete the line. Refused as of Soup v0.77.

Config could not be loaded (see above).

The terminal now prints a value refusal without the Value error, prefix that v0.76 put in front of it. Three rules for what to do with the answer:

  • Delete the line, do not rename it. In upstream's words, none of the staged fields "changed what a run did, so deleting them changes no result." For regression gating and early stopping, which several of them promised, use soup train --gate <suite.yaml>.
  • Fix it and run it again until it prints no error. The staged-field lines and the deprecated-value lines arrive together in one report, staged lines first. A cross-field refusal, such as training.relora_steps on task: dpo, is reported one at a time: the next one shows only after you fix the first.
  • soup plan and soup apply are not this check. At v0.77.0 soup plan --config writes a plan, and soup apply --dry-run reports no drift, both with exit 0, for a config that soup train refuses. Use soup doctor --config or soup train --dry-run.

Re-run this

Every row is something upstream's changelog or release body says ran wrong in a release before v0.77.0. Most of them did not raise, and most exited 0. Text in quotation marks is upstream's own wording, the rest a close paraphrase of the same entry, and this page adds no explanation of why. Pull request numbers are upstream's, issue first where both are given. "Verified at v0.76.1" means this site's audit read or ran the v0.76.1 code; elsewhere the scope is upstream's "before this release". The remedy is to re-train, re-judge or regenerate the artifact unless the row says otherwise.

Two rows, lora.use_rslora and freeze_layers with LoRA, are settings that now take effect: the same config trains something different on v0.77.0, and re-training matters only if you wanted the setting.

Training artifacts

What was wrongWho is affectedPR
A resumed run "restored the optimizer and the step counter and trained a freshly initialised adapter: on a 4-step run resumed at step 2, the result was two steps from zero." Upstream: "Re-run any dpo, ipo or grpo training that was resumed from a checkpoint."soup train --resume on task: dpo, ipo or grpo, and task: preference with dpo or ipo. Upstream says grpo with grpo_beta: 0, orpo, simpo, bco, kto, sft and reward_model were not affected. Verified at v0.76.1#1768, #1797
training.relora_steps on a loss with a frozen reference: "every ReLoRA restart merges the trained adapter into those weights, so from the first restart on the reference was the policy as of that step, without an error." Refused at load since v0.77.0relora_steps with task: dpo, ipo, kto or bco, grpo with grpo_beta above 0, or task: preference whose loss or blend names dpo, ipo or bco, on v0.76.0 and v0.76.1 (the merging restart arrived in v0.76.0; through v0.75.2 a restart pruned the adapter instead). To train again, remove relora_steps#1835, #1891
training.freeze_layers / freeze_ratio "did nothing on a LoRA run": every targeted module got an adapter, the frozen layers included. Since v0.77.0 a frozen layer gets no adapter, so such a config "now trains fewer layers than it did before, which is what the option always documented"sft and tts LoRA runs that set either key. Verified at v0.76.1. On v0.77.0 the freeze needs a list lora.target_modules; a regex, all-linear, lora.target_parameters, lora.use_vera and a plan that freezes every layer are refused when the model is built#1432, #2145
lora.use_rslora: true "loaded on both backends and was never read, so an rsLoRA run trained plain-LoRA scaling (alpha / r, an 8x smaller update at the default r: 64, alpha: 16)". Such configs now train at the rank-stabilised scale and their results change; upstream: "drop the flag to keep the old alpha / r behaviour"backend: mlx or backend: unsloth with use_rslora: true. Verified at v0.76.1#1853, #1899
LLaMA Pro with a LoRA rank: "The block expansion runs before get_peft_model, which froze the appended blocks again, so only adapters on the original layers trained while the appended blocks stayed the identity." Refused at load since v0.77.0: on task: sft the error points to lora.r: 0, and on task: pretrain, whose trainer always attaches an adapter, LLaMA Pro is refused and the error points to task: sfttraining.expand_layers with freeze_trainable_layers and any lora.r above 0 (the default is 64), on task: sft or pretrain. Verified at v0.76.1#1326, #1783, #1875
An explicit training.unlearn_alpha: 0.0 ran as 1.0. Upstream now respects it "for pure forget loss in unlearn training (NPO, SimNPO, and RMU) instead of silently falling back to 1.0"task: unlearn with unlearn_alpha: 0.0. Verified at v0.76.1#2108, #2114
Under gradient accumulation, PRM and RAFT "logged and clipped losses G times too large". Now the logged loss, train_loss and grad_norm "match a gradient_accumulation_steps: 1 run", and max_grad_norm clips the gradient for the stated losstask: prm, and task: sft with data.format: raft, at gradient_accumulation_steps above 1 (the default is 4). Verified at v0.76.1#1444, #1771
KTO labels: upstream now refuses "malformed KTO labels instead of silently training nulls, floats or containers as desirable or undesirable rows"task: kto on data whose label is not a boolean, a true / false-style string, or exactly -1, 0 or 1. On v0.77.0 the loader drops such a row with its reason, and soup data validate data.jsonl --format kto counts them and prints the reasons for the first three; fix the labels and re-train. Verified at v0.76.1#2119, #2126
target_modules: auto on a DeepSeek-V3 base whose config sets q_lora_rank: null, such as Moonlight-16B-A3B: "the key/value and output projections took the adapter and the query path trained none, without a warning."v0.76.0 and v0.76.1, where the deepseek_v3 row of the auto table arrived. A base with an integer q_lora_rank is unaffected#2071
moe_lora: true dropped LoRA targets. A string target_modules was replaced by the MoE list: all-linear, which target_modules: auto resolves to on qwen4_exp, so "a qwen4_exp base with moe_lora lost the Gated DeltaNet" projections among others, and any other string, a regex, "was dropped outright". Under stream_layers, every name the MoE list does not repeat lost its adapter ("on Qwen3.5-MoE the linear-attention in_proj_qkv / out_proj were dropped"), and "an explicit lora.target_modules list was dropped entirely"moe_lora: true with a string lora.target_modules, resident or streamed, and moe_lora: true with stream_layers: true. Verified at v0.76.0 and v0.76.1#2072, #2095
On a Mistral Hub repository that ships consolidated.safetensors beside the shards, soup spectrum scan scored both name sets and "emitted unfrozen_parameters matching no parameter"Scans of such a repository, and Spectrum runs configured from them: run the scan again on v0.77.0. Verified at v0.76.1#1806, #1906
Removing the line changes nothing. Refused at load since v0.77.0, and trained without the setting before: training.use_mod: true on a route that installs no router ("Full fine-tuning, LISA and Spectrum on SFT, vision, audio, layer streaming, unsloth and MLX SFT all trained without one"); a non-default moe_aux_loss_coeff on backend: unsloth or with stream_layers ("loading and being ignored"); neftune_alpha on the 14 tasks that never apply it ("silently dropped"); citation_faithful on pretrain; reasoning_effort and train_on_eot on pretrain, classifier, reranker and cross_encoder ("loaded and changed nothing"); vision or audio task: distill ("accepted and ignored by the text-only trainer"); vision or audio SFT on backend: unsloth ("silently ran on transformers"); and lora.rank_pattern / alpha_pattern on MLX or Unsloth ("trained a uniform adapter")A config that carried one of them. Deleting the key trains what the run trained before. Upstream adds that "a pretrain cache built with the flag has to be rebuilt without it" for train_on_eot; if per-module ranks mattered, train them on backend: transformers#1752, #1757, #1412, #1749, #1820, #1883, #1406, #1843, #806, #1587, #1186, #2014, #2015, #1996, #2153

Served answers

What was wrongWho is affectedPR
soup serve --backend sglang with a LoRA adapter: "The backend registered the adapter at startup but never named it, so every answer came from the base model while /v1/models and each response reported the adapter."Anything answered, judged, benchmarked or gated through soup serve --backend sglang with an adapter as --model: run it again on v0.77.0, whose [sglang] extra needs sglang>=0.5.1. A full model is unaffected. Verified at v0.76.1#1724, #1732

Verdicts and gates

What was wrongWho is affectedPR
tool_call_match and tool_call_args_subset "compared parsed arguments with Python ==", so "a model that answered {"enabled": 1} where the task expected {"enabled": true} scored 1.0". "Scores recorded before this change can be higher than the same outputs score now."Custom eval task files whose rows use scoring: tool_call_match or tool_call_args_subset, read by soup eval custom, the custom tasks of soup eval gate, the metric leg of soup ship and soup eval quant-check#1755, #1756
The bundled-suite scale moved twice: "scorer revision 6 prevents generation failures from earning credit", and mini_mmlu "no longer sits at a 1.000 ceiling": it "grows from 26 to 40 rows", and "a model that always answers "B" drops from 0.462 to 0.300". Separately, the MCQ extractor "now accepts a lone option letter wrapped in markdown emphasis, quotes, backticks or square brackets"soup ship and soup eval gate scores and --baseline files from the bundled suites. Upstream: a baseline written by v0.76 "is not compared silently on the new scale: write it again"#1759, #1958, #1192, #2113, #1442, #1924
soup eval gate passed or recorded what it never measured: it "passed a suite with no tasks", "duplicate gate task names overwrote baseline evidence", "an empty judge prompt file scored 0.0 without judging anything", and "a NaN or infinite baseline let soup eval gate pass"Gate verdicts and baseline files from suites of those shapes. On v0.77.0 an empty or repeated task list and a non-finite baseline exit 3, and an empty judge prompt file fails the gate with exit 2#1798, #1821, #1824, #1893, #1910, #1928, #1773, #1779
soup drift-alarm could print an OK panel and exit 0 when it read no live or reference row. Upstream: it "exits 1 and shows no OK panel when it could read no live or reference row, naming the file and the keys it reads (output, response, text)"Drift verdicts whose panel showed n_reference: 0 or n_live: 0: nothing was compared#1808, #1905
A CMA-ES merge in which every evaluation failed wrote "a "converged" merge of the first sample" and exited 0soup adapters merge --strategy cmaes runs whose evaluations all failed. On v0.77.0 the merge exits non-zero and writes no adapter#1808, #1905

Data, configs and records

What was wrongWho is affectedPR
Six data quality commands "scanned alpaca, preference and ShareGPT rows as empty and reported them clean"Scans of alpaca, DPO, KTO or ShareGPT data with soup data pii, toxicity, score, langdetect, educational or decontaminate: scan again on v0.77.0#1427, #2037
soup data validate "did not count rows the loader skipped" (not a JSON object, or not valid JSON). Since v0.77.0 they count as invalid in the N/M rows valid line, in --min-valid-fraction and in the exit codeValidation results for files with unparseable lines, and the CI steps that relied on them#1887, #1908
"The gate soup ci init writes passed a file with nine broken rows out of ten." v0.77.0 writes soup data validate <data> --min-valid-fraction 1.0; upstream: "existing workflows are unchanged"A .github/workflows/soup-gate.yml written by soup ci init before v0.77.0: regenerate it with soup ci init --force and the same flags, or add --min-valid-fraction 1.0 to its validate step#2055, #2079
Trace and ingest parsers could pair answers from different conversations. Upstream now keeps "context through the last user turn so unrelated answers do not form preference pairs"Preference files built by soup data from-traces from multi-turn traces, including files written by soup ingest: build them again, and re-train a model trained on them. Verified at v0.76.1#1809, #1848
"Four commands could overwrite an input with their output." v0.77.0 refuses an output path that is also an input of soup ingest, soup prune-prompt, soup ra-dit or soup data best-of-nAnyone who gave one of those an output equal to an input: check that input against its source#1422, #2018
soup migrate "dropped the validation split and re-defaulted LoRA settings"; it "now writes what the source trained instead of Soup's defaults", where lora_target: all had become auto, "two of seven projections on Llama"Configs generated by soup migrate from axolotl, LLaMA-Factory or an Unsloth notebook: generate them again and compare. v0.77.0 also lists every source key it does not carry in one warning#1807, #1901
soup compile "never handed the eval signal to DSPy, TextGrad and GEPA"Programs from soup compile and tool descriptions from soup compile-tools: compile again#1810, #1956
"The RL checkpoint manifest now reports has_ref_model: false and has_rollout_buffer: false, because no save writes reference-model or rollout-buffer state." Before, it copied both flags from the config, "so a checkpoint holding only the adapter and optimizer claimed both"Mid-epoch RL checkpoints whose manifest says either is true: they hold the adapter and the optimizer only. Setting either flag to true is refused at load on v0.77.0#808, #1980

If a row names a verdict, a scan or a config, the artifact is that verdict, scan or config, not the weights: a model trained from it changes only where the row says so.

Not rows, and why. Runs that crashed have no finished artifact to replace: task: pretrain with lora.r: 0 (#1875; LLaMA Pro on pretrain with a LoRA rank finished and is in the LLaMA Pro row above), llava and sharegpt4v rows on the preference tasks (#2064), a preference blend that named dpo or ipo on v0.76.0 or v0.76.1 (it stopped at the first step; since v0.77.0 such a term trains, with the exceptions listed under Behaviour), and MLX on a split smaller than one batch (#1857). Changes to memory or speed only are not rows either: training.gradient_checkpointing (see Behaviour that changes results, memory or exit codes), stream_layers with use_cut_ce and quantization: 8bit on Unsloth (both refused now, below), and use_liger: true reported as applied on model types it has no kernels for (#1947). MLX SFT runs were never seeded before this release, so a run started before it cannot be reproduced; it was not wrong, and there is nothing to re-train.

What is refused at load now

The staged fields and the deprecated values

v0.76.0 warned about these, all but the two named below, and named v0.77. v0.77.0 refuses them.

  • 31 staged fields (validated, read by nothing), each refused when set away from its default, on its own line ending refused as of v0.77. Delete the line.: training.long_context_grpo, vision_grpo, quantize_ref_model, grace_codebook, grace_codebook_size, grace_codebook_dim, lr_groups, early_stop_patience, citation_recall_threshold, forgetting_eval_steps, forgetting_benchmark, forgetting_stop, checkpoint_eval_steps, checkpoint_eval_metric, checkpoint_eval_tasks, checkpoint_keep_top, convergence_window, convergence_rel_tol, rl_checkpoint_include_ref_model, rl_checkpoint_include_rollout_buffer, and data.video_dir, video_fps, video_maxlen, image_min_pixels, image_max_pixels, image_resize_algorithm, split_thinking, eval_on_each_dataset, resize_vocab, extend_conversation, skip_prepare_dataset.
  • Two of them were never warned about. training.rl_checkpoint_include_ref_model and rl_checkpoint_include_rollout_buffer were staged after v0.76.0 and were in no release, so v0.77.0 refuses true with no warning release in between. An explicit false loads.
  • data.remove_unused_columns: true. No trainer ever read it. Configs written by soup autopilot and run-config dumps from v0.75 and earlier carry it. Delete the line.
  • training.quantization: 4bit or 8bit, or training.load_in_8bit: true, on a task that never quantises the base: distill, classifier, reranker, cross_encoder, prm, moe_lora_routing, unlearn and asr. Delete the line or set quantization: none.
  • soup ab --beta exits 2 and ends Refused as of Soup v0.77. Remove it; for a stricter accept_h0, lower --alpha.

A soup sweep --param that sets one of the 31 staged fields away from its default in any arm is refused the same way: the command exits 1 before the first arm starts. A deprecated value such as data.remove_unused_columns=true is not part of that pre-check (soup sweep --dry-run exits 0), and it is refused inside each arm that sets it instead. When a staged field's own check fails as well (for example grace_codebook: true without a size and a dimension), only the staged line is shown, so no message asks for a line that the next load refuses.

Fifteen settings v0.76 accepted without a word

This is upstream's list. Each message names the setting and what to change:

  • training.use_mod: true on a route that never installs a Mixture-of-Depths router (#1752)
  • a non-default training.moe_aux_loss_coeff on backend: unsloth or with stream_layers (#1749)
  • training.expand_layers (LLaMA Pro) with a LoRA rank (#1783), and on task: pretrain (#1875)
  • lora.r: 0 on task: pretrain (#1875)
  • training.citation_faithful on task: pretrain (#1843)
  • training.neftune_alpha on the 14 tasks that never apply it (#1883)
  • training.relora_steps where the loss has a frozen reference (dpo, ipo, kto, bco, grpo with grpo_beta above 0, and blends naming them) (#1891)
  • quantization: 8bit with backend: unsloth (#1946)
  • vision or audio task: distill (#2014)
  • vision or audio SFT on backend: unsloth (#2015)
  • training.stream_layers with training.use_cut_ce (#2023)
  • llava or sharegpt4v rows on dpo, ipo, simpo, orpo, bco and preference; the pixtral-dpo recipe is retired (#2064)
  • lora.rank_pattern or lora.alpha_pattern with backend: mlx (#1996)
  • lora.rank_pattern or lora.alpha_pattern with backend: unsloth (#2153)
  • a KTO label that is not a boolean, one of the documented strings or exactly -1, 0 or 1 (#2126). This one is a data check, not a config refusal: the row is dropped at data load with its reason, which soup data validate and soup train --dry-run report before any model loads, and soup data validate still exits 0 while some row is valid unless you pass --min-valid-fraction

The release body names a few more refusals of the same kind, each with the fix in its message: training.reasoning_effort and train_on_eot on tasks whose formatter never reads them (they now load on sft, tts and distill); data.chat_template with backend: mlx; the known Kimi K2 Hub bases loaded resident on the transformers backend, refused at load with Resident Kimi K2 is unsupported on Transformers 5.x (the two resident kimi-k2-* recipes are marked unavailable, and soup recipes use refuses them with exit 3); training.freeze_layers or freeze_ratio with LoRA on a shape peft cannot restrict by layer, refused when the model is built; an eval-gate suite with no tasks or with a task name used twice; an ambiguous registry name:tag; and an output path that is also an input of soup ingest, soup prune-prompt, soup ra-dit or soup data best-of-n.

The refusals grouped by what they touch, with the start of each message, are on Settings Soup refuses at load.

A LoRA target on a streamed embedding or head

Under training.stream_layers, v0.76 checked only the literal names lm_head and embed_tokens at load, so a regex or dotted spelling loaded, and a regex then failed at its first save, or in peft under task: dpo. Since v0.77.0 the check runs when the model is built: on an untied checkpoint any spelling is refused before a streaming buffer is allocated, and on a tied checkpoint, where v0.76 refused the literal names too, the target now trains and saves (#2082). Because the check moved, soup train --dry-run now passes such a config.

Python 3.11 to 3.13

requires-python is >=3.11,<3.14 (#1805). Python 3.10 reached end of life on 2026-10-01, and pip on 3.10 keeps installing v0.76.x. Python 3.13 is now supported; upstream's CI runs it in the nightly and release runs, not in pull-request runs. The Typer bound is typer>=0.15.4,<0.28.

AWQ and GPTQ export are removed

soup export --format awq and --format gptq, soup quantize --to awq and --to gptq, and the [awq] and [gptq] extras are gone (#1951). AWQ and GPTQ export need v0.76.x and its [awq] or [gptq] extra, for example pip install "soup-cli[awq]<0.77"; upstream notes that both projects are archived and incompatible with [train], and names a separate environment with the upstream tools as the other route. What v0.77.0 prints:

text
Unsupported format: awq
--format awq was removed in v0.77.0.
Supported: gguf, onnx, tensorrt, bitnet, tq1_0, torchao, gguf-ud

That is soup export --model ./out --format awq, and it exits 1. soup quantize ./out --to gptq exits 2 with --to must be one of ['gguf', 'onnx', 'tensorrt']. soup export also lost --bits, --group-size and --calibration-samples, so a script that passes them exits 2 with No such option, and --calibration-data is now read only by --format gguf-ud.

Training on a base that is already quantised with AWQ or GPTQ (training.quantization: awq or gptq) is a different thing, and it is not part of this removal. soup deploy autopilot --target rtx-4090-24gb now plans 4-bit QLoRA with vLLM instead of writing quantization: awq, and its --measure-candidates examples read 4bit,8bit,fp8. See Model export.

The soup serve tool routes

POST /v1/tools/python now answers HTTP 501 without running anything unless the host provides OS-level isolation, as POST /v1/tools/bash already did (#2166). It runs where os.unshare can create the namespaces (Linux, Python 3.12 and newer) or where sandbox-exec is available (macOS), and answers 501 on Windows and on Linux with Python 3.11. Where it runs, the code still goes through the RLVR code sandbox, with its timeout and memory limit, as before; what v0.77.0 adds is the isolation requirement and the token.

Both routes now require --tool-auth-token on every bind, loopback included, and answer 401 without it (#2174). --tool-auth-token also reads SOUP_TOOL_AUTH_TOKEN, which keeps the token off the command line. To keep calling the two routes, start the server with the token and send Authorization: Bearer <token> with each request. The isolation cuts the network; the code still runs as the server's user, so use a long random token, and read upstream's docs/serving-and-export.md before you run the server for callers you do not fully trust.

These routes exist on the transformers backend only; the vLLM, SGLang and MII apps have none. soup serve still starts without a token on loopback, and the generation, adapter, web_search and thumbs routes are unchanged: generation, /v1/models, /health and /metrics still take no token on any backend. soup loop watch gained --tool-auth-token (or SOUP_TOOL_AUTH_TOKEN) for its deploy stage, which a token-protected server used to refuse (#1184). See Inference server.

Behaviour that changes results, memory or exit codes

  • training.gradient_checkpointing now reaches the trainer (#1778, #1981, #2140). With the default false, grpo, ipo, bco, reward_model and online_dpo train with checkpointing off, where TRL's own default used to apply, and on backend: unsloth false now disables Unsloth's checkpointing. Set training.gradient_checkpointing: true to keep the memory you had; the shipped recipes, templates and examples for those tasks already set it. distill, classifier, reranker, cross_encoder, asr and unlearn now honour true too; their default is unchanged.
  • lora.use_rslora and training.freeze_layers / freeze_ratio with LoRA are applied (they were ignored). rsLoRA runs on MLX and Unsloth train at the rsLoRA scale, and a frozen layer gets no adapter, so such a run trains fewer layers than before, as the option always documented. Their results change.
  • MLX SFT runs are seeded from training.seed and training.data_seed. A run started before this release cannot be reproduced.
  • A dpo, ipo or grpo run resumed from a checkpoint trained a freshly initialised adapter before this release; re-run any such resumed training.
  • prm and RAFT sft runs under gradient accumulation logged and clipped losses that were gradient_accumulation_steps times too large; they now match the other trainers.
  • training.unlearn_alpha: 0.0 is pure forget loss, as documented. It ran as 1.0.
  • task: simpo on the transformers backend caps the prompt to data.max_length // 2 tokens, as DPO and ORPO do.
  • A dpo or ipo term in a preference_loss_weights blend now trains under any primary (#1785, #1828). With a dpo or ipo primary the reference runs twice per step and the eval loss is the blend's value; under a simpo or orpo primary the blend makes the reference itself by switching the LoRA adapter off, with one more forward per step, so there init_strategy: pissa, olora or loftq is refused before the model loads and a run without an adapter (lora.r: 0) still stops at the first step. relora_steps beside a blend that names dpo or ipo is refused at config load (#1891). A bco blend is still refused at trainer setup.
  • The bundled eval scorers moved to provenance revision 7, and mini_mmlu grew to 40 rows, so a baseline written by v0.76 is not compared silently on the new scale: write it again.
  • soup train with stdin piped or redirected and no --yes exits 3 before it starts; piping y no longer answers the prompt. Pass --yes in scripts.
  • Commands that used to exit 0 when nothing succeeded (#1905): soup sweep exits 1 when every arm failed (one completed arm still exits 0); a CMA-ES merge in which every evaluation failed exits non-zero and writes no adapter; soup drift-alarm exits 1 when it could read no live or reference row; soup eval auto exits 1 when no leg evaluated anything; soup loop watch --detach checks its numbers in the parent and starts no child when they are invalid.
  • More exit-code changes. soup eval gate exits 3 on an empty, missing or misspelled task list, a repeated task name and a non-finite baseline, and an empty judge prompt file fails the gate with exit 2. soup recipes use on an unavailable recipe exits 3. soup data validate counts rows the loader skipped as invalid, and still exits 0 while any row is valid unless you pass --min-valid-fraction; a workflow that soup ci init writes on v0.77.0 passes --min-valid-fraction 1.0, and an existing workflow keeps its old step until you regenerate it with soup ci init --force. The table is on Exit codes.
  • A streamed soup train on Linux, WSL2 included, sets PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True before CUDA starts, unless you exported a value of your own (#2156).
  • The first layer-streamed run on a Hub base after upgrading re-shards into a new <slug>@<commit> cache slot, which costs disk space and time; the old slot is left in place.
  • The layer-streaming disk pre-flight sizes the cache from the checkpoint's headers (#2039). A dense base under quantization: 4bit is charged more, because embeddings, head and norms stay at the stream dtype (Llama-3.1-8B 4.14 GB to 5.70 GB), so a run that passed by less than that margin is now refused before sharding; a fused MoE base is charged what it really writes.
  • Audit-log lines written by older versions get the same masking as new lines, applied once when the log is next written (#2065).

What is new

DPO, ORPO, SimPO and KTO on Apple Silicon

Through v0.76.1 backend: mlx ran supervised fine-tuning and refused every other task at config load. v0.77.0 trains task: dpo, orpo, simpo and kto there through Soup's own MLX loop, on TRL's objectives: DPO's sigmoid loss with dpo_beta, ORPO and SimPO with no reference model, and KTO with kto_beta against a reference precomputed once from the base (#1846, #2073, #2097, #2111). task: preference stays refused on MLX (tracked as #2202), and so does grpo. Upstream qualified ORPO, SimPO and KTO on Qwen2.5-0.5B-Instruct in bf16 and 4-bit on an M4 Pro, and says for SimPO and KTO that this shows the objective moving, not alignment quality.

MLX text SFT gains native DoRA (lora.use_dora: true, #1920), training.eval_steps counted in optimizer updates (#2074), the loss watchdog (#2115) and seeded runs (#1855): training.seed and data_seed are applied, so two runs with the same seed reproduce each other's losses. A split smaller than one batch no longer crashes the run (#1857). lora.use_rslora is now applied on MLX and Unsloth, and lora.rank_pattern / alpha_pattern and data.chat_template are refused at load on MLX. See Apple Silicon MLX backend.

Layer streaming reaches DeepSeek-V3-family MoE models

Layer streaming is Soup's implementation of layer-to-layer streaming of a frozen base (L2L, Pudipeddi et al. 2020). It now admits model_type: deepseek_v3, gated on resident-versus-streamed parity forward and backward on a tiny from-config model on CPU (#1914); no real DeepSeek-V3 checkpoint was compared against a resident run, and there is no NF4 parity check for it. That takes the allowlist to eleven architectures with the same five streaming tasks; no parity was measured for the preference losses on DeepSeek-V3. DeepSeek-V3's FP8 block-quantised checkpoints are dequantised at shard time (#2061). Kimi-K2-Base and Kimi-K2-Instruct configs are admitted behind a field-by-field check, verified on CPU with a tiny K2-shaped checkpoint only; nothing has been trained on K2 itself (#2107). Still opt-in (stream_layers: true) and still BETA. New streaming keys:

  • training.stream_expert_layout: per_expert streams one routed expert at a time, DeepSeek-V3 family only (#2029). On one RTX 5070 Laptop 8 GB, the same Moonlight-16B LoRA config peaked at 1.0 GB of GPU memory against 3.1 GB fused (NF4 experts against bf16, different cache tiers), at about 2.7x the step time; that is one run each, and the record's controlled pair reads 1.9 GB against 3.9 GB at about 2x (record).
  • training.stream_recompute: off | auto keeps streamed activations instead of recomputing them in the backward, with bit-identical gradients, on task: sft only (#2152). On the same card's RAM tier a Mistral-7B NF4 step took 22% / 17% / 8.5% off the step time at 512 / 1024 / 2048 tokens, with no gain on a read-bound disk tier and slower at a high VRAM fill (record). The default on keeps the shipped behaviour.
  • training.stream_loss_chunk runs the output head and the loss in token chunks, task: sft only (#1880). It matches the unchunked step within 1e-5 of the largest value, not bit for bit, and it has not been measured on a GPU.
  • training.stream_nf4_scale_cache keeps the expanded NF4 block scales on the device, bit-identical and off by default (#1892). It costs 4 bytes of VRAM per 64-element block.

See Streaming a Mixture-of-Experts base, Layer streaming and Scaling a streaming run.

Provenance you can check

soup registry push --data <file> records the file's SHA-256 as data_hash: the file registered as the training input, not proof of what the run trained on. --idempotency-key makes a push safe to retry, and an ambiguous name:tag is refused (#1907, #1934, #1964). soup train records the SHA-256 of a local training file at startup (#2024). On the transformers backend, resident, layer-streamed and multi-GPU runs record the Hub commit of the base they loaded (#1972, #2094, #2127); Unsloth and MLX loads and local or cached-path hints are not bound (#1902). soup ship --require-provenance checks evidence against the adapter, task and source bytes, suite version and verdict policy it was stamped with (#2025); its help says it requires --evidence, --config, --adapter and task eval, and calls --data-sha256 "caller-asserted". In upstream's words, this "is not a model run, attestation, signature or forgery protection"; the unkeyed hashes detect drift. See Model registry and soup ship.

GRPO can score with an LLM judge

training.reward_fn: judge_rank sends all of a prompt's completions to one judge call and scores each against the others (#2117), on task: grpo only and not with modality: vision. The judge is set with training.grpo_judge, checked at load, and the run must state training.grpo_judge_max_calls: a run that could exceed it is refused after the data loads and before the model does. Prompts and completions are sent to the judge host you configure, and a request the judge answers with 429 or a 5xx, or that times out, is retried up to twice inside each counted call, so the host can see up to three requests per counted call. No quality result is claimed. See RLVR.

Data, speech, recipes and the CLI

  • pip install "soup-cli[documents]" provides the PDF and DOCX readers for soup data ingest; Markdown and TXT need no extra (#2096).
  • soup merge --low-memory merges a plain LoRA adapter into a safetensors base one tensor at a time, with values identical to the regular merge in upstream's words; it needs the fp16 save format and refuses adapters it cannot reproduce exactly (#2006).
  • User-supplied files with a UTF-8 BOM load: JSON, CSV and TXT datasets (#1954), and the agent, tokenizer, bench, draft, edit, shrink, diagnose, can, generate and autopilot readers (#2155). The six data quality commands read alpaca, preference, KTO and ShareGPT rows (#2037).
  • Sesame CSM fine-tunes from raw transcript and audio pairs through a native trainer (#1769), with data.format: audio and quantization: none. Raw-audio Spark and Oute stay refused.
  • training.fast_lora switches the Fast-LoRA kernels on for SFT on the transformers backend, text only, with quantization none or 4bit and lora.dropout: 0 (#1973). It is verified on CPU only, and no speedup is claimed.
  • Two recipes, qwen3.5-4b-dpo and qwen3.5-2b-dpo (#2129, #2160), with no result claimed; pixtral-dpo is retired, and kimi-k2-sft and kimi-k2-thinking-grpo are marked unavailable.
  • A RunPod controller behind soup train --cloud runpod (#2081). It is plan-only by default: it writes a stub and contacts nothing. --cloud-submit needs RUNPOD_API_KEY and RUNPOD_SSH_PRIVATE_KEY and otherwise exits 1. It has not been run against a real RunPod account yet (tracked as #2098).
  • soup --version (#2105).
  • Two reference pages generated from the code, docs/config-reference.md and docs/cli-reference.md, which upstream's test suite keeps equal to what the code does (#1799).

Counts at the tag: 171 recipes, 1070 test files, 36 pip extras (documents is new; awq and gptq are gone), 31 staged fields, each refused when set away from its default, and 11 streaming architectures with the same five streaming tasks. The 78 top-level commands and 29 command groups are unchanged.

Security

No security advisory is published with this release. In upstream's words it "tightens six behaviours": two on the soup serve tool routes (above), one in the RunPod controller, and three that the changelog lists under Security. Upgrading is enough for the last four; the tool routes need the token.

  • POST /v1/tools/python runs only under OS-level isolation (#2166), and both code routes need --tool-auth-token on every bind (#2174): see The soup serve tool routes.
  • The RunPod controller runs ssh and scp by the absolute path found on PATH, never from the current directory, without RUNPOD_API_KEY in their environment, and fetches results into a new directory, moving only the expected output directory into place (#2081).
  • The documents extra requires pypdf>=6.21.2.
  • Replies to Soup's judge client, and to soup data generate's providers and Magpie, are size-bounded (#1929, which closes #1819). A judge reply over 1 MiB, and an HTTP generation or Magpie reply over 8 MiB, is refused while it arrives, gzip expansion included, before it is parsed, and the response body is not logged. Magpie drops the affected sample and continues within its attempt limit. The judge cap covers soup eval judge, the eval-gate judge_model, soup ship's judge, online DPO, GRPO judge_rank, soup data best-of-n, soup data evolve and soup expect; the generation cap covers soup data generate and Magpie.
  • Printed text drops bidi controls and invisible separators (#1953). Soup's terminal sanitiser now also removes U+202A to U+202E, U+2066 to U+2069, U+061C, U+200E, U+200F, U+200B, U+2028, U+2029 and U+FEFF from dataset, adapter and evidence text before it is printed, so a crafted row cannot reorder or hide what a preview shows. ZWJ and ZWNJ are kept for Arabic, Persian, Indic and emoji text.

Also in this release, outside upstream's six: soup ui checks the Host header on a loopback bind (#1839), and soup ui --public prints and opens a loopback URL instead of 0.0.0.0, which Windows cannot reach (#2110); --public keeps the bearer gate. Audit-log lines written by older versions get the same masking as new lines, applied once when the log is next written (#2065). training.grpo_judge is checked at load: plain http only to loopback, and no private, link-local or reserved IP literal (training.online_dpo_judge refuses such an IP literal at load too, and checks plain http later, at trainer setup).

Supported versions. 0.77.x is supported. 0.76.x, the last line that runs on Python 3.10 and exports AWQ and GPTQ, receives security fixes only, until 0.78.0 is released. Anything below 0.76 is not supported. See SECURITY.md at v0.77.0.

What was measured, and what was not

The preprint (DOI 10.5281/zenodo.21771064) is unchanged: no number it states was re-measured or corrected, and its measurements stay scoped to the Soup versions it names. This release's DeepSeek-V3-family, FP8-source, per-expert, chunked-loss and no-recompute streaming are outside what it measured; upstream says the next preprint version covers them and cites L2L. See The paper.

Twelve records were added under benchmarks/ at this tag, the failed ones included. Both measured streaming rows on this page (per_expert and stream_recompute) come from one RTX 5070 Laptop 8 GB. Two of the records are worth naming here: gate-kimi-k2-admission.md is CPU only, with no K2 weights, and one of its checks fails as registered; and probe-rtx5070-moonlight-fp8-source.md, the GPU loss check of an FP8-block Moonlight source against its bf16 copy, failed two of its limits and is published as written. The FP8 bit-exactness that does hold is a CPU gate on a tiny DeepSeek-V3: streamed from its FP8 copy, it matches a resident model holding the same dequantised weights bit for bit. The records and their scopes are listed on Measurement records.

No benchmarks/ record was added for MLX, soup merge --low-memory or fast_lora.

Known limitations

These are upstream's, in plain terms.

  • training.rl_checkpoint_include_ref_model and rl_checkpoint_include_rollout_buffer went from accepted to refused with no warning release in between; they were staged after v0.76.0.
  • AWQ and GPTQ export are gone. They need v0.76.x and its [awq] or [gptq] extra, for example pip install "soup-cli[awq]<0.77"; upstream notes that both projects are archived and incompatible with [train].
  • task: simpo on the transformers backend cuts a prompt longer than data.max_length // 2 tokens even when the whole row would fit, until #1196 lands (#1185).
  • training.fast_lora is verified on CPU only (float32 and float64 against peft). The bf16 and fp16, NF4 and streamed-CUDA checks have not been run, and no speedup is claimed (#792).
  • training.stream_loss_chunk has not been measured on a GPU; the peak-VRAM table is still open (#1693).
  • training.stream_recompute is SFT only, and auto works on the RAM tier only. On a read-bound disk tier it gains nothing, and at a high VRAM fill it is slower (measured on one RTX 5070 Laptop).
  • Kimi K2 admission is verified on CPU with a tiny K2-shaped checkpoint only; nothing has been trained on K2. Its tokenizer needs --trust-remote-code and pip install tiktoken. On a full-size K2, setup briefly allocates about 22.5 GB of CPU commit per MoE layer before moving it to meta (#2154). Resident K2 loads are refused at load, and Kimi-K2-Thinking and Kimi-K2-Instruct-0905 have no supported route yet (#2104).
  • stream_expert_layout: per_expert covers the DeepSeek-V3 family only and costs step time, about 2x to 2.7x on Moonlight-16B (see the record). A cache volume that is not NVMe is warned about (#2170).
  • After upgrading, the first pinned layer-streamed run re-shards into a new <slug>@<commit> cache slot; the old slot stays on disk and nothing names or removes it (#2196).
  • The RunPod controller has not been run against a real RunPod account yet (#2098). It makes one attempt, copies the results once with a 15-minute limit into a new directory next to the output path, and deletes the Pod afterwards; a failed or timed-out copy removes that directory when it is empty and names it in the error otherwise. Local input files are not uploaded, so the base and the data must be reachable from the Pod.
  • soup export --format gguf still clones llama.cpp b5270, which cannot convert Qwen3.5. Since this release it refuses before cloning and names --llama-cpp <dir>; moving the pin is open (#1911).
  • Base-revision binding covers the transformers backend. Unsloth and MLX loads, and local or cached-path hints, are not bound yet (#1902).
  • lora.rank_pattern and alpha_pattern are refused on MLX and Unsloth until those trainers apply them (MLX: #1996).
  • freeze_layers and freeze_ratio with LoRA need a list of lora.target_modules. A regex, all-linear, lora.target_parameters and VeRA are refused when the model is built (#2201).
  • soup merge --low-memory refuses adapters it cannot reproduce exactly (DoRA, modules_to_save, rank or alpha patterns, embedding adapters) and names the regular merge instead.
  • /v1/tools/python is unavailable (501) on Windows and on Linux with Python 3.11, as /v1/tools/bash already was. Where it runs, a failure inside the submitted code is reported as timed_out: true with an empty stderr, as in v0.76 (#2194).
  • On MLX, task: preference stays refused (#2202).
  • Still open from v0.76.0: fields that reach no consumer outside the staged table (#1814), soup doctor --config coverage (#1815), config-error wording (#1816) and vague messages for rows of an unknown shape (#1818).
  • Layer streaming remains BETA and opt-in, and --cuda-graphs remains experimental.

One more, read from the v0.77.0 binary rather than from upstream's list: soup plan and soup apply --dry-run accept a config that soup train refuses, both with exit 0 (see Before you upgrade).

Install and upgrade

bash
pip install -U soup-cli
soup --version
soup doctor --config soup.yaml

With pipx, pipx upgrade soup-cli replaces the first line. soup --version prints soup v0.77.0. Run soup doctor --config before the first run of each existing config.

  • Python 3.10: pip and pipx stay on v0.76.x there; to pin it, pip install "soup-cli[train]<0.77" (v0.76.x supports Python 3.10 to 3.12). If pipx built Soup's environment with Python 3.10, reinstall it on a newer interpreter, for example pipx install --force --python python3.12 "soup-cli[train]".
  • AWQ or GPTQ export: AWQ and GPTQ export need v0.76.x and its [awq] or [gptq] extra, for example pip install "soup-cli[awq]<0.77"; upstream notes that both projects are archived and incompatible with [train], and names a separate environment with the upstream tools as the other route. v0.76.x receives security fixes only, until 0.78.0 is released.
  • PDF and DOCX ingest: pip install "soup-cli[documents]". Markdown and TXT need no extra.
  • soup serve tool routes: start the server with --tool-auth-token <token> (or set SOUP_TOOL_AUTH_TOKEN) and send Authorization: Bearer <token> to /v1/tools/python and /v1/tools/bash.

Docker

The release body publishes an image: ghcr.io/makazhanalpamys/soup:0.77.0, also tagged latest. It is Ubuntu 24.04 with Python 3.12 on a CUDA 12.8 runtime, installs exactly this release with the train, serve, data and eval extras, and starts soup with /workspace as its working directory. The host needs a driver that supports CUDA 12.8 (570 or newer on GeForce) (#2092).

bash
docker pull ghcr.io/makazhanalpamys/soup:0.77.0
docker run --gpus all -v "$(pwd)":/workspace ghcr.io/makazhanalpamys/soup:0.77.0 train --config soup.yaml --yes

Keep the --yes. docker run without -it gives soup train no terminal on stdin, and since v0.77.0 soup train exits 3 in that case unless --yes is passed, so the same command without it stops before training.

Credits

151 of the 226 pull requests in this release came from 44 people outside the maintainer, in the release body's count. In the release body's order, by merged pull requests:

WhoPRs
Nurkhan Esenbek (@kok-o)34
Nariman Abdikarim (@Nari-Ab)14
Yang Bai (@ybai08)12
@dryn85-max8
@ndablizov8
AmixDigital (@Amix29)6
苏俊杰 (@Momoyeyu)5
Abdulwaarith Zakariyya (@abdulwaarith0)5
Samarth Mukhija (@Samearth17)5
@vaputa4
Srinivasan R (@Srinivasan8888)4
Pernav Jain (@here-2007)3
Tal (@taljeon)3

With two pull requests each: Dipak Chaudhari (@dchaudhari7177), Younes Beriane (@drakeo338), Faisal Fayaz (@Faisal-Fayaz), Areen Agrawal (@Areen-09), Aditya Pratap Singh (@akkupratap323), Vincent Le (@VincentTLe), Shivansh Shukla (@ShivanshShukla), Abhishek Jha (@SpinachDigital) and Rahul Seervi (@Rahul-cs50).

With one each: Sohaib Kamash (@Nick-800), Batyrkhan (@Batyrkhantzy111), Matt Van Horn (@mvanhorn), Tanmay Garg (@Tanmay9223), Vladimir Babin (@chiliec), Nehal K (@sloemo01), Aykut Güney (@KeuqS), Emre K. (@kocaemre), Abhinay Kumar (@abhinyaay), Alexis (@bernalalexis-try), @DBVamsi, @dipesh-singh07, George Tonmoy Roy (@george07-t), Aaditya Desai (@aadityad12), Kuanysh (@kuanysh228), Mohammad Hijjawi (@MohammadHijjawi97), martin (@Martini147), @Shutaru, Vishal Kumar Singh (@Vishalkumarsingh6428), Aaron Probha (@AaronProbha18), Vansh Sharma (@VanshSharmaPES) and Daivik Awasthi (@daivikawasthi01).

Reports that led to fixes in this release, as the release body thanks them: @feloau, @bobbonauta, @maximilianfeix, @Ercaner1988, @VincentTLe, @abdulwaarith0, @taljeon, @yunaremaia, @Amix29, @akkupratap323, @abdulbasit-ogunrinu and @Yi-111-a.

What each person found or built, with every issue and pull request, is in the release notes and the changelog at the tag.

See also

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.