Skip to content

v0.77.1: data commands keep their input

v0.77.1 was tagged on 28 October 2026. Upstream gave it no codename and titled it with a sentence again: data commands keep their input; plan and apply refuse what train refuses. It is a cut of the development branch after v0.77.0, not a backport: it carries every pull request merged since that tag. In the release body's words, "the fixes that matter most are three. soup data commands no longer write their output over their own input. soup plan and soup apply now refuse the configs soup train refuses. And the README's Docker example works again. Nothing in v0.77.0 is taken back."

22 of the 28 pull requests in this release came from 11 people outside the maintainer. That is the release body's own count; the names are under Credits. The six maintainer pull requests are #2205, #2213, #2242, #2244, #2245 and #2261.

This is not a security release. No security advisory is published with it. Its Security section says only that it "tightens how external programs are started" (see External programs). The supported versions are unchanged: 0.77.x is supported, 0.76.x receives security fixes only until 0.78.0 is released, and anything below 0.76 is not supported.

Coming from v0.76.x or older? Read v0.77.0 first. A config that loaded on v0.76 can stop loading on v0.77, and v0.77.1 changes none of that. It adds one thing to the same family: soup plan and soup apply now stop on the configs soup train stops on.

What to do

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.1. Run soup doctor --config before the first run of each existing config.

From v0.77.0, a valid soup.yaml needs no change. Four kinds of setup do:

  • A script that wrote a soup data result over its own input now fails. It exits non-zero before the data is read, and the input is left alone. Write to a new file and rename it. See Data commands keep their input.
  • A pipeline that planned a config soup train refuses now stops at soup plan. Exit 1, no plan written. Delete the line the message names. See soup plan and soup apply.
  • A tool that Soup found only in the current directory, or through a relative PATH entry, is no longer found. On Windows the current directory is also left out of the search for the programs Soup starts. Put the tool's directory on PATH as an absolute path, or give the tool's own path. See External programs.
  • A caller of /v1/tools/python that treated every non-success as a timeout now sees timed_out: false for an ordinary failure. See Also fixed.

Two more things are worth a look, neither a re-train:

  • Re-run one ingest, if it matters to you: soup ingest --source langfuse --logs on an export that carries scores of 0, 0.0 or false. See Re-run this.
  • The VRAM pre-flight prices runs differently. Some long-sequence, large-vocabulary runs, and some runs without gradient checkpointing, that it let through are now refused, inside the range its estimate was fitted on; --allow-oom-attempt still launches them. See One peak-VRAM estimate.

Coming from v0.76.x or older, Python 3.10 stays on v0.76.x (pip install "soup-cli[train]<0.77"), and AWQ and GPTQ export were removed in v0.77.0. See Docker and install for the image.

Re-run this

What was wrongWho is affectedPR
Upstream: "soup ingest --source langfuse --logs dropped scores of 0, 0.0 and false; they now reach the thumbs-down signal and preference-pair construction. Only a missing or null score falls back to rating."soup ingest --source langfuse --logs on a Langfuse export that carries a score of 0, 0.0 or false, run through v0.77.0. Re-run the ingest on such an export; anything built from the earlier output, preference pairs included, was built without those scores.#1856, #1871 by @akkupratap323

This is the one re-run row on this page: upstream frames no other change in this release as a wrong finished run. The refusals below apply from v0.77.1 on. They do not restore an input that an earlier version already replaced, which in upstream's words could not be recovered, so check any dataset you passed through one of the commands below with an --output that named it.

Data commands keep their input

In the release body's count, eighteen soup data commands "used to write their result over one of their own inputs and exit 0; the original could not be recovered." Each now compares its outputs with its inputs before the data is read, and refuses the same file under any spelling: the same string, ./x, sub/../x, the absolute path, a case variant on a case-insensitive file system, a hard link or a symlink. The message names both paths and says what to change. An output that is a different file works as before, and default output names are unchanged.

bash
soup data sample d.jsonl -o d.jsonl --n 2
text
--output 'd.jsonl' is the same file as the input 'd.jsonl'; choose a different --output

That run exits 1 and d.jsonl is untouched. Write the result to another path, and rename it afterwards if you wanted an in-place edit:

bash
soup data sample d.jsonl -o d.sampled.jsonl --n 2

The eighteen, and what each one's output used to replace (#2245, closes #2225; #2261, refs #2226):

CommandExit codeWhat used to be replaced, in the release body's words
decontaminate, toxicity, langdetect, pii, educational1An input that had the command's default output name: clean.jsonl, toxicity.jsonl, langdetect.jsonl, pii.jsonl or educational.jsonl. decontaminate also compares --output with --benchmark-file.
lint, doctor, topicslint 3, the others 1The JSON report replaced the dataset.
from-traces1The preference pairs, often an empty file, replaced the trace file.
canary insert1The result replaced the input. It also refuses a --manifest that is the dataset or the --output.
sample, dedup, filter, convert, merge, active-sample1The result replaced the input.
augment, evolve2The result replaced the input.

Details that decide whether a script still works:

  • A default output name that the input already has is refused too: merged.jsonl, prefs.jsonl, active_samples.jsonl, augmented.jsonl, and the five default names in the first row. The advice the v0.77.0 notes gave, to pass --output a different path, is now what the error message says.
  • The exit codes follow each command's other path errors. 1 for most, lint 3 and augment and evolve 2. Which message comes first for a missing input depends on the command: dedup and lint report the missing file, merge and pii the refusal.
  • On Windows a trailing space or dot, ::$DATA and a junction are refused as the same file too. Upstream lists these spellings under the first five commands; they were also refused when checked on sample, dedup and pii.
  • canary insert --manifest: before, the dataset write replaced the manifest that had just been written, and in upstream's words the only record of the inserted secrets was lost.
  • soup data clean already refused this and is unchanged.

Where it stops. soup data persona-mix still writes over its --prompts input when --output names it; that was reproduced on v0.77.1 for this page. mix, forge, generate, gen-magpie, recipe and canary check have not been checked yet. Both are tracked in #2226, so "every soup data command" is not a safe reading. See Data tools, Data Forge and Data Moat II for the commands.

soup plan and soup apply refuse what soup train refuses

Since v0.77.1 (#2242) soup plan and soup apply run the validation soup train runs and print the same Config validation error: lines: a staged field, a refused deprecated value or any other schema error exits 1, writes no plan and leaves an existing state file unchanged. Before, they checked only for unknown keys, so a config with a staged field set, a refused deprecated value such as data.remove_unused_columns: true or any other schema error got a plan with exit 0, and soup apply and soup apply --dry-run accepted it. Upstream's release body calls this "the gap the v0.77.0 notes said was open."

This config is refused on purpose, because training.early_stop_patience is a staged field:

yaml
base: org/model
task: sft
data:
  train: ./data.jsonl
training:
  early_stop_patience: 5
bash
soup plan --config soup.yaml
text
Config validation error:

  training.early_stop_patience is accepted but read by nothing; refused as of v0.77. Delete the line.

soup apply --dry-run and soup apply print the same lines, exit 1 and do not mark an existing state file applied. A valid config plans and applies as before. With several unknown keys, every line of the message is now indented, as soup train prints it. The YAML read limits are unchanged.

Which commands refuse a staged field or a refused deprecated value, at v0.77.1: soup train exits 1, soup doctor --config exits 2, soup recipes verify --config and soup ship --config exit 3, and soup plan and soup apply exit 1.

One config still loads under soup train and not under soup plan or soup apply. A distill soup.yaml with training.minillm_enabled: true, training.minillm_teacher_mix_ratio: 0 and no training.minillm_on_policy loads only under soup train --minillm-on-policy, which sets the field before validation. soup plan and soup apply have no such flag, so they refuse that config with minillm_enabled with minillm_on_policy=false requires minillm_teacher_mix_ratio > 0. Write training.minillm_on_policy: true in the YAML.

Other changes to the same two commands:

  • soup apply no longer reads the model. It compares hashes only, so it makes no request to the Hub.
  • soup plan prints a dim line before its first Hub request: Reading the model's config.json from the Hub to size the run (HF_HUB_OFFLINE=1 skips this)...
  • peak_vram_gb is the shared estimate, or null. The plan records peak_vram_gb: null with a peak_vram_reason when it cannot price the run, instead of the fixed 8.0 GB it recorded for every run through v0.77.0. A config that leaves batch_size at its default auto is one of those. A state file written before this still loads. The reverse does not hold: a v0.77.1 state file with peak_vram_gb: null makes the v0.77.0 soup apply exit 2 with a bare float() error (checked on both binaries for this page).

See Pre-flight and tooling and Settings Soup refuses at load.

GRPO trains on Apple Silicon

Since v0.77.1, task: grpo with backend: mlx trains natively through Soup's own MLX loop (#2182 by @dryn85-max, the dense baseline, step 1 of #1976): it generates num_generations completions per prompt from the current policy, scores them with the built-in accuracy or format reward and takes one policy-gradient step per update, on the objective Soup's transformers path trains on TRL 0.29.1; held-out prompts are scored once after training as val_reward. It was refused at config load through v0.77.0. MLX now trains six tasks: sft, dpo, orpo, simpo, kto and grpo. task: preference stays refused (#2202).

This config loads on v0.77.1: soup doctor --config prints no error and soup train --dry-run --yes ends with Config valid. Ready to train! (the dry run was checked on six chatml rows, {"messages": [{"role": "user", ...}, {"role": "assistant", "content": "#### 5"}]}, whose final assistant turn is the gold answer).

yaml
base: Qwen/Qwen2.5-0.5B-Instruct
task: grpo
backend: mlx
data:
  train: ./train.jsonl
  format: chatml
  max_length: 256
training:
  reward_fn: accuracy
  num_generations: 4
  batch_size: 4
  gradient_accumulation_steps: 2
  grpo_beta: 0.1
  lr: 1e-5
  quantization: none
  lora:
    r: 16
    alpha: 32

What to know before you run it:

  • The base must be unquantized. Use quantization: none and a bf16, fp16 or fp32 checkpoint. A quantized base is refused only after the model loads, so soup doctor --config and --dry-run pass it. A 4-bit mlx-community/... base, which the MLX recipes in the catalog use for SFT, is refused for GRPO.
  • Refused at config load: quantization: 8bit, lora.r: 0, custom or ensemble reward_fn settings (only the built-in accuracy and format rewards score), non-text modalities, and grpo_variant. Among others, the reward-hack, echo-trap, mid-run RL checkpoint and curriculum settings, rollout_backend and the other GRPO stability settings are refused too, each by name. The dapo in this loop's objective is TRL's default loss_type, not a Soup grpo_variant, so this does not add DAPO or the GRPO variants to MLX.
  • --resume and --hf-resume are refused at launch (exit 1), because only the adapter is saved.
  • Validation: on backend: mlx the loader holds data.val_split out (default 0.1) and scores it once after training as val_reward. Set data.val_split: 0 to train on every row. training.eval_steps is refused for this task, because there is no mid-run evaluation.
  • soup doctor --config has nine combinations now. grpo on mlx has four settings (training.use_galore, use_ring_attention, moe_lora, use_fsdp2_compile); with none switched on it prints None of the 4 setting(s) known to be unread on task=grpo backend=mlx is switched on in this config. The other eight are unchanged.
  • No recipe exists for it. The catalog is still 171 recipes.

Upstream qualified the loop on Qwen2.5-0.5B-Instruct on an M4 Pro with a synthetic arithmetic task that it describes as easy and fully verifiable, and says the run "shows the loop training, not reasoning quality in general." No figure from it is reproduced here: upstream's docs print one without a record under benchmarks/, and Soup publishes no MLX speed, memory or timing figure. The settings, refusals and the loop's details are on Apple Silicon MLX backend.

soup infer --backend mlx

soup infer takes --backend transformers|mlx (default transformers). With --backend mlx, native text LoRA adapters and MLX base models load and generate without Torch or PEFT, through a shared strict loader that checks every adapter name, shape and finite value and refuses unsupported tasks, controls, targets and variants. Dense and genuine 4-bit bases are covered, and base-only generation works. It is part 1 of #1986 (#2121 by @Amix29).

bash
soup infer --model ./out --input prompts.jsonl --output answers.jsonl --backend mlx
  • It needs Apple Silicon and the [mlx] extra. The loader's message when they are missing is Native MLX generation requires soup-cli[mlx]; install it on Apple Silicon. (read in the code).
  • It refuses, one named option at a time, with exit 1: --task other than text (MLX generation requires --task text), --batch-size above 1 (MLX generation requires --batch-size 1) and --cuda-graphs (MLX generation does not support --cuda-graphs). A --device value is refused too (MLX selects its native device; omit --device), and so is a --hub other than hf. Any other --backend value exits 2.
  • A native Soup adapter given to the default backend is refused with Native MLX adapter detected; select --backend mlx, and a malformed native adapter_config.json is refused with a message naming the file.
  • soup chat on MLX is still open (#1986, #2123).

No speed, memory or timing figure exists for it. The option refusals above were run on a machine without MLX; the loading and generation path was read in the code and not run. See Experiment tracking for soup infer and Apple Silicon MLX backend.

One peak-VRAM estimate

soup train's pre-flight and soup profile priced the same run with two formulas in two units, and neither charged the logits and loss chain, which decides long, large-vocabulary runs. Since v0.77.1 soup train's pre-flight, soup profile, the MCP profile tool and soup plan price a run with one estimate (#2146 by @abdulwaarith0). It reads the model's config.json and safetensors headers, never the weights, and sums weights, LoRA adapters, gradients, optimizer state, activations, the logits and loss chain over the vocabulary, the 4-bit dequantization copy and overhead.

Upstream calibrated it on 39 measured runs: resident two-step sft runs on one RTX 3060 Laptop 6 GB under WSL2, across seven dense models from 135M to 3B parameters (the record). The record states that the coefficients are fitted on those same rows, so its bands show how well they fit them, not the error on a run they were not fitted to.

Where it refuses and where it warns.

  • Inside the range it was fitted on, which upstream prints as "dense models of 3B and below on one 6 GB card", a run the estimate says does not fit is refused, and --allow-oom-attempt launches it anyway. The pre-flight now refuses some long-sequence, large-vocabulary runs, and some runs without gradient checkpointing, that it let through.
  • Outside it (a larger model, a mixture-of-experts model, medium or selective checkpointing, use_liger or use_cut_ce, backend: unsloth; none of these is among the 39 measured runs) a run that does not fit gets the estimate as a warning, with a footnote that it may overstate the peak, and starts.
  • One case is refused outside the range too: a run whose weights, the CUDA context, its adapters and the gradients of the parameters it trains alone are more than the card holds, plus their optimizer state for the AdamW optimizers, unless GaLore replaces the optimizer on sft, tts or pretrain. --allow-oom-attempt launches that as well. Optimizer state counts only for adamw_torch, adamw_torch_fused, schedule_free_adamw and adamw_bnb_8bit.
  • The shipped longcontext template (an 8B model at 131,072 tokens) is outside the range, and its header says how to turn on use_liger.
  • A config that leaves batch_size at its default auto is not priced by the soup train pre-flight, which returns without pricing it (checked at v0.77.1). Set a number to get the check.

How it prices the pieces. gradient_checkpointing is priced as the trainer applies it: auto resolves on the card as sft does, selective is priced as no checkpointing (an upper bound), and tasks other than sft and tts checkpoint fully. Tasks without an LM head (classifier, reranker, cross_encoder, embedding, reward_model) are charged no logits.

Reading a Hub model's shape can reach the Hub. The pre-flight, soup profile and soup plan read a Hub model's config.json and write it into the Hugging Face cache, unless HF_HUB_OFFLINE or TRANSFORMERS_OFFLINE is set. soup plan prints a dim line before it does, soup apply compares hashes only and does not read the model, and the MCP profile tool reads only a local directory or the local cache. A Hub model with no readable config.json is priced from a shape guessed from its name.

What soup profile prints. Its numbers change, and its JSON gains the exact bytes per term, the fit verdict, where the model's shape came from and gradient_checkpointing_tier (off, full, medium or selective). gradient_checkpointing in that JSON, and in the MCP profile result, stays the config's own value. See Experiment tracking and Pre-flight and tooling.

External programs are started by an absolute path

Two maintainer changes (#2205, #2213) with one visible effect. In the changelog's list, nvidia-smi, sysctl, the disk-probe tools, git, ollama, the llama.cpp binaries, trtllm-build, docker, the Modal CLI, gh, ssh and scp (the RunPod and Lambda controllers), the code-sandbox tools, accelerate for soup train --gpus and the soup adapters bisect --eval-command program now run by the absolute path found in an absolute PATH directory.

  • Not searched: empty and relative PATH entries (., bin, and on Windows \bin or C:bin), and the current directory. If a tool is only in the current directory or in a relative PATH entry, Soup no longer finds it. Put its directory on PATH as an absolute path, or give the tool's path.
  • On Windows the current directory is left out of the program search (#2213), for soup and for the programs it starts, eval scripts and the scripts behind a custom reward_fn included. soup sets NoDefaultCurrentDirectoryInExePath=1 for itself when it is not set. A value you already set is kept, and per upstream any non-empty value, 0 included, already leaves the current directory out, so no setting brings the old search back. To run a program from the current directory, give its path (./tool.exe, or an absolute path in quotes) or put the directory on PATH as an absolute path.
  • A set but empty PATHEXT on Windows falls back to .COM;.EXE;.BAT;.CMD.
  • soup adapters bisect exits 2 before the first probe when its --eval-command program is not on PATH (--eval-command program 'x' was not found on PATH; install it, put it on PATH, or give its path). When the checkpoint id fills in the program name, each probe resolves it the same way, and a name not on PATH still counts as a failed probe. Through v0.77.0 a program that could not be found counted as a failed probe and bisect printed BROKEN_AT on the first checkpoint (checked on both binaries for this page), so exit 2 no longer tells a verdict from a rejected command by itself: read the first line.
  • --eval-command is split with POSIX shell rules, in which a backslash is an escape. A Windows path there takes forward slashes or quotes; .\tool.exe would reach the program as .tool.exe.
  • The macOS code sandbox runs sandbox-exec from /usr/bin/sandbox-exec only, and a llama.cpp checkout given as --llama-cpp . runs its binaries by absolute path.
  • Cloud controllers. soup train --cloud lambda refuses an output with a part that starts with -, as --cloud runpod already did (Cloud plan failed: Lambda output_dir parts must not start with '-', exit 2), and both controllers stop before launching with "OpenSSH ssh and scp must be installed and on PATH" when they are missing. The RunPod controller still has not been run against a real RunPod account (#2098).

The changes are in the Security section of the release body, which says only that the release "tightens how external programs are started" and that upgrading is enough. See Environment variables and Adapter lifecycle.

soup ship --progress jsonl

soup ship takes an opt-in --progress jsonl (#2171 by @kok-o). It writes versioned start, progress, end and error events to stderr, one JSON object per line, so a CI job can follow a verdict without parsing console text. Ordinary output, stdout and exit codes are unchanged.

bash
soup ship --evidence evidence.json --progress jsonl 2> progress.jsonl

With offline --evidence that file holds exactly two lines (the other phases appear on live runs):

text
{"schema_version":1,"event":"start","phase":"verdict","side":null,"suite":null,"repeat":null,"done":null,"total":null}
{"schema_version":1,"event":"end","phase":"verdict","side":null,"suite":null,"repeat":null,"done":null,"total":null}
  • Fields: schema_version (1), event, phase (load, task_eval, suite_eval or verdict), side (base, tuned or null), suite, repeat (the one-based noise-floor repeat, or null) and done and total (counts, or null); an error event also carries error, always execution_aborted. Noise-floor repeats get explicit identities, and a group that aborts carries the fixed code execution_aborted.
  • Exit codes: a completed DON'T SHIP (exit 2) ends with end. Evidence that cannot be read ends with start then error ("error":"execution_aborted", never the exception text) and exit 3. --progress with any value but jsonl exits 3 with Error: --progress must be jsonl. A Click usage error keeps the ordinary format and emits no events.
  • In JSONL mode, stderr is exclusive to the events. Per the code, warnings, logging and tracebacks do not appear on it, and an unexpected exception becomes exit 1 with start and error events and no traceback.
  • No clock or throughput field exists, and no task text, response, path or credential is an event field. It is not a progress bar.

The offline --evidence paths and the option check were run on v0.77.1; the events of live groups were read in the code. See soup ship.

Unknown config keys name the Soup field

Unknown config keys still refuse the load; what changed is the hint (#2099, #2138 by @Nari-Ab). A key written the way another library names it now names the Soup field: learning_rate suggests lr, num_train_epochs suggests epochs, and training.lerning_rate suggests training.lr instead of an unrelated field. "Use X" is for those names and one-edit typos of them; a name that is only similar gets the "did you mean" ranking, with a cutoff of 0.65 in every section. per_device_eval_batch_size, per_gpu_eval_batch_size, eval_batch_size, warmup_steps and warmup_step have no Soup counterpart and get a sentence saying so, at the top level and under training: alike. learning_rate_decay is no longer sent to a different setting. The hints are diagnostics: the schema still decides what loads.

This config is refused on purpose, and soup doctor --config soup.yaml prints:

yaml
base: org/model
task: sft
data:
  train: ./data.jsonl
training:
  learning_rate: 2e-5
  per_device_eval_batch_size: 4
  warmup_steps: 10
  bacth_size: 4
text
Config validation error:

  unknown config key 'training.learning_rate' - use training.lr. Refused.
  unknown config key 'training.per_device_eval_batch_size' - Soup has no separate evaluation batch size; evaluation runs at the training batch size. Refused.
  unknown config key 'training.warmup_steps' - Soup has no step-count warmup setting; warmup is a fraction of the training steps, set with training.warmup_ratio (0 to 0.5). Refused.
  unknown config key 'training.bacth_size' - did you mean 'batch_size'? Refused.

Config could not be loaded (see above).

The command exits 2. See Settings Soup refuses at load.

Layer streaming

Three changes, none of them with a hardware record.

  • A LoRA target on a streamed embed_tokens or lm_head of an untied checkpoint is accepted, and it saves and resumes (#2159 by @kok-o). Through v0.77.0 it was refused when the streamed model was built. Since v0.77.1 the adapter saves with PEFT's defaults and resumes through a Trainer checkpoint or load_adapter with every tensor kept, reloads keep the runtime's device map, and DPO's reference adapter can attach through the streaming wrappers. This completes item 1 of #1048, following the build-time guard from #2082. Upstream's checks are CPU tests with PEFT 0.20.0 and 0.21.0: they establish correctness, not CUDA speed or memory, and no figure is published for it. Saving needs CPU memory for each adapted boundary matrix for a moment, the run's matching shard cache has to stay available through saving and resume, and loading weights into an arbitrary new named adapter is still unsupported. A tied checkpoint keeps both weights resident and follows the normal PEFT path; this change does not touch it.
  • Building a streamed fused-expert MoE no longer asks the CPU for each full expert tensor (#2157 by @ybai08, closes #2154). The skeleton is built on meta, but each MoE layer first created its expert tensor on the CPU (15 GB for DeepSeek-V3, 22.5 GB for Kimi K2 in bf16) before moving it. On Linux with strict overcommit, a small machine, or Windows with too little page file this failed with DefaultCPUAllocator: can't allocate memory or not enough memory. Every CPU request of 1 MiB or more is now made on meta while the skeleton is built, and a model that registers a buffer that large is built the old way with the buffer named in a warning. A refused CPU allocation now gets a short message and a fix hint. This is a setup fix: nothing has been trained on K2.
  • Two reads of a streamed routed expert per step instead of three (experimental, API only; #2158 by @Batyrkhantzy111). build_streamed_model(..., two_read_experts=True) keeps the decoder checkpoint but no longer fetches routed experts in its replay, so the forward and the expert's own backward read each expert once, and the router reuses the forward's top-k choice while its scores stay differentiable. It needs the per_expert layout and reaches DeepSeek-V3-family bases only. There is no config field, flag or default change, so soup train still reads each routed expert about three times per step. A forward hook on a two-read layer, its MoE block or its routed experts is refused with an error. On CPU the losses and gradients equal the resident model's bit for bit. No hardware speed record is part of this release (its scope is discussed in #2170).

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

Also fixed

  • /v1/tools/python reports what failed (#2204 by @Nari-Ab, closes #2194). A timeout is timed_out: true with exit_code: 124 and no output. Other process failures carry their stdout, stderr and exit code with timed_out: false, and output overflow is exit_code: 1. Every HTTP 401 from the tool routes carries WWW-Authenticate: Bearer. Through v0.77.0 stderr was always empty and timed_out was true for any failed run, so a caller that treated every non-success as a timeout now sees timed_out: false for an ordinary failure. A line number in a Python traceback counts a six-line network-guard prefix, so the first line of your code is line 7. The route is still unavailable (501) on Windows and on Linux with Python 3.11. This was verified on the function the route calls, not over HTTP. See Inference server.
  • soup eval gate handles a malformed suite (#1895 by @Nari-Ab, reported by @taljeon in #1878). Malformed YAML and non-mapping roots ([1], 5, hello, and the empty [], false, 0, "") exit 3 with Cannot load suite: and a message that names the file. Before, malformed YAML and a list or scalar root exited 1 with a bare Error: TypeError or ParserError line, and an empty root such as [] exited 3 with a generic validation message. soup train --gate with such a suite exits 1, and the training callbacks keep their fatal-failure policy. See Eval-gated training.
  • Adapter commands name the missing dependency (#2141 by @Nari-Ab, closes #2056). They reported corrupt weights when NumPy or safetensors was missing. Read-only operations (check, diff, scan) now ask for NumPy, and merge and arithmetic ask for NumPy and safetensors: numpy package required; pip install "soup-cli[train]" (or pip install numpy) and numpy and safetensors packages required; pip install "soup-cli[train]" (or pip install safetensors numpy), both exit 1 (run with NumPy blocked). See Adapter check.
  • soup export cleans up after a failed step (#1464 by @ShivanshShukla in #2164). The temporary .soup_merge_tmp_<adapter>/ directory is removed when llama.cpp is not found, a clone or install fails, or the merge dies while saving, for the gguf, onnx, tensorrt, bitnet and gguf-ud paths; before, a multi-gigabyte directory was left next to the adapter. It removes only a directory that export created: one that was already there is left alone, and the output says so. This was read in the code and not run.
  • Spike recovery hints are written safely (#2169 by @Nari-Ab). They refuse symbolic-link targets and are replaced atomically, which keeps other hard links and the previous hint when the write fails, without consuming an attempt.
  • soup profile --gpu and the MCP profile tool know the RTX 30-series laptop GPUs (#2144 by @abdulwaarith0). --gpu "rtx3060 laptop" was "Unknown GPU", and the laptop 3060 has 6 GB, not the desktop card's 12. Each part now has its own entry with NVIDIA's memory, the names the driver reports resolve to it, and a size written as a word ("A100 40GB") reaches the matching entry.
  • A malformed model file no longer stops the estimate. A model's config.json that is malformed, or states a figure no model has, is treated as an unreadable config.json by the VRAM pre-flight, soup plan, soup profile and the MCP profile tool (soup profile prices a guessed shape and says so; soup plan records that it could not price the run). A malformed .safetensors header is skipped and the parameter count comes from config.json, approximately.
  • A mixture-of-experts model known only by its cached config.json is priced with all its experts, not as a dense model of a few billion parameters; the estimate's note says the count is approximate.
  • Names from a config or a command line reach the terminal cleaned. soup profile prints the LoRA target modules, and soup adapters bisect the checkpoint ids, with control characters and bidi marks removed.
  • training.curriculum_dynamic is documented as telemetry (#2142 by @Nari-Ab). Its weights are logged to curriculum_history.jsonl for soup runs curriculum-curve and do not change which samples a batch contains. The schema descriptions, reference entries and training guide now say so; behaviour is unchanged.

Five v0.77.0 statements corrected

Upstream corrected five statements about v0.77.0 in its own text (the v0.77.0 CHANGELOG, SECURITY.md, docs/backends-and-ops.md and the README What's New). No behaviour changed; the only source edits are two comments (#2244).

  1. soup doctor --config. The System, GPU, System Resources and Dependencies tables still print first. What moved is the All checks passed! line: the config is checked before it, and a refused, missing or unreadable config exits 2 without it. The earlier text had said the config is checked "before the environment summary".
  2. A malformed KTO label is dropped, not refused. A KTO row whose label is not a boolean, a true/false-style string or exactly -1, 0 or 1 is dropped with a warning (soup train names the first such row and stops only when none is left; soup data validate --min-valid-fraction 1.0 exits 2). It is not refused at load, as the earlier text listed it: fourteen of the settings on upstream's list of fifteen stop the load, not fifteen.
  3. A dpo or ipo term under a simpo or orpo primary trains (since #1828), against an adapter-off reference. Only with full fine-tuning, which has no adapter to switch off, does it still stop at the first step, with a message that names the fix. The #1785 entry had said it always stopped. This is not a statement about every blend of preference losses.
  4. Telemetry: as released, nothing is sent. The bundled analytics key is a placeholder, so an opted-in command prints Notice: telemetry is not yet live. and makes no network request. Only an operator's own SOUP_POSTHOG_KEY, and optionally an HTTPS SOUP_POSTHOG_ENDPOINT, gives one HTTPS POST at command exit; the payload list now names every field it carries. The earlier text that said to set both is wrong: a key alone is enough, and without an endpoint the bundled default is used. Upstream's SECURITY.md adds that an endpoint you set must be HTTPS and that loopback, private and internal hosts are refused.
  5. Which commands refuse a staged field or a refused deprecated value: soup train exits 1, soup doctor --config exits 2, soup recipes verify --config and soup ship --config exit 3. soup plan and soup apply did not at v0.77.0; since v0.77.1 they exit 1 (see above).

See the v0.77.0 page for the release these statements describe.

Measurement records

Three files were added under benchmarks/ at this tag, which makes 53 records. No existing record changed, and no record covers MLX GRPO, soup infer --backend mlx, two_read_experts or the Windows PATH change.

  • gate-2049-resident-vram.md (#2146): the 39 resident runs behind the shared estimate. One RTX 3060 Laptop 6 GB under WSL2 on Windows 11, seven dense models from 135M to 3B parameters, vocabularies from 32,000 to 151,936. The record says its coefficients are fitted on those rows, so the figures it prints show the fit and not an error on unseen runs. Seven of the rows allocated more than the free memory and spilled into shared memory under WSL2; in the record's words, on a native Linux driver those runs would have run out of memory. No model of 7B or more, no mixture-of-experts model, no medium or selective row, no fused loss and no Unsloth run was measured.
  • gate-2050-selective-checkpoint.md and its pre-registered protocol (#2050; #2161 by @Shutaru): selective attention checkpointing of streamed NF4, on one RTX 3090 on Windows with Mistral-7B, five alternating pairs per comparison. Only saving Q/K/V/O at sequence length 2048 met the rule fixed in advance: 3.1% less measured time (interval 3.0% to 3.4%), for +1.342 GB retained. Q/K/V at 2048 and Q/K/V at 512 were inconclusive, and Q/K/V/O at 512 was slower in all five pairs. The record did not isolate the cause of the short-sequence variation. Every logit, loss, LoRA gradient tensor and updated parameter matched exactly over two updates. The installer is benchmark-only and nothing in soup train uses it, and default flash attention and other GPUs were not measured, so this is not a claim that streaming got faster.
  • The stream_loss_chunk_vram harness runs each arm in its own process (#2151 by @ybai08), because only the first arm per process got the RAM tier. It is step 1 of #1693's order, and it still has no GPU record.

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. Two-read routed experts, saving a streamed LoRA on an untied embedding or head, and the selective-checkpointing record are outside what it measured. The records and their scopes are listed on Measurement records.

Docker and install

bash
pip install -U soup-cli
soup --version

With pipx, pipx upgrade soup-cli. The Python range is unchanged at >=3.11,<3.14; there is no dependency change in this release.

The image is ghcr.io/makazhanalpamys/soup:0.77.1, also tagged latest. The Dockerfile is unchanged since v0.77.0: Ubuntu 24.04 with Python 3.12 on a CUDA 12.8 runtime, installing exactly this release with the train, serve, data and eval extras. The host driver must support CUDA 12.8.

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

Keep the --yes. docker run without -t gives soup train no terminal on stdin, and since v0.77.0 soup train exits 3 in that case unless --yes is passed. The README's Docker example left it out and exited 3 on v0.77.0; v0.77.1's README passes it, and so do its four translations (#2244, closes #2229). The image was not pulled for this page.

Credits

22 of the 28 pull requests in this release came from 11 people outside the maintainer, in the release body's count. Grouped by person, in the release body's order, with what each found or built:

WhoPRsWhat
Nariman Abdikarim (@Nari-Ab)7Unknown-key hints that know other libraries' parameter names (#2138); soup eval gate exits 3 on a malformed suite (#1895, reported by @taljeon); adapter commands name the missing dependency (#2141); /v1/tools/python failure diagnostics and WWW-Authenticate (#2204); safe spike recovery hints (#2169); the curriculum_dynamic telemetry contract (#2142); the GGUF progress test isolated from a developer's own converter (#2207); the Windows-path test change that keeps the suite green on pytest 9.2.0 (from #2284)
Yang Bai (@ybai08)3The streamed fused-expert MoE build without a CPU allocation first (#2157); the stream_loss_chunk_vram harness with one process per arm (#2151); the fresh-process streaming test on this tree (#2208)
Nurkhan Esenbek (@kok-o)2Save and resume of streamed LoRA on an untied embedding or head (#2159); soup ship --progress jsonl (#2171)
Abdulwaarith Zakariyya (@abdulwaarith0)2The single peak-VRAM estimate and its calibration on 39 runs (#2146); the RTX 30-series laptop GPUs (#2144); the pytest 9.2.0 diagnosis and fix (#2286)
Saurav Mukherjee (@SauravMukherjee44)2Vision and audio checkpointing coverage (#2180); the atomic-writer destination-directory pin (#2179)
dryn85-max (@dryn85-max)1Native GRPO on Apple Silicon (#2182)
AmixDigital (@Amix29)1Native MLX inference, soup infer --backend mlx (#2121)
Batyrkhan (@Batyrkhantzy111)1Two reads of a streamed routed expert per step (#2158)
Aditya Pratap Singh (@akkupratap323)1Langfuse zero scores kept as negative feedback (#1871)
Shivansh Shukla (@ShivanshShukla)1soup export removes its temporary merge directory on failure (#2164)
Shutaru (@Shutaru)1The selective attention checkpointing measurement for streamed NF4 (#2161)

Reports that led to fixes in this release, as the release body thanks them: @taljeon (#1878) and @abdulwaarith0 (#2286). The six maintainer pull requests (#2205, #2213, #2242, #2244, #2245 and #2261) carry no credit line here.

Known limitations

New in this release, in upstream's terms:

  • The pre-flight's estimate was fitted on dense models of 3B and below on one 6 GB card (benchmarks/gate-2049-resident-vram.md). No model of 7B or more, no mixture-of-experts model, no medium or selective tier, no fused loss and no Unsloth run was measured, so for those it warns and starts instead of refusing, unless the run's weights, CUDA context and trained-parameter state alone are more than the card holds (see above). A model known only by its cached config.json is counted from that file (approximate for a mixture-of-experts one).
  • A distill config with minillm_enabled: true and minillm_teacher_mix_ratio: 0 needs training.minillm_on_policy: true written in the YAML for soup plan and soup apply, which have no --minillm-on-policy flag.
  • soup data persona-mix still writes over its --prompts input when --output names it. mix, forge, generate, gen-magpie, recipe and canary check have not been checked yet. Both are tracked in #2226.
  • MLX GRPO is the dense baseline. quantization: 8bit, lora.r: 0, custom or ensemble reward_fn settings and non-text modalities are refused at load, and --resume is refused because only the adapter is saved (#1976). Native MLX inference covers text LoRA adapters only, and soup chat on MLX is open (#1986, #2123).
  • two_read_experts has no hardware speed record yet; its scope is discussed in #2170.

Upstream repeats the limitations of v0.77.0, except two that it marks as fixed since v0.77.0 and no longer limiting, and it does not repeat the first two items of the v0.77.0 list (the rl_checkpoint_include_* refusal and the AWQ and GPTQ removal; see that page): the 22.5 GB CPU commit while a full-size Kimi K2 is set up (#2154) and the empty stderr of a failing /v1/tools/python call (#2194). The soup plan and soup apply gap and the refusal of a LoRA target on an untied streamed embedding or head, both listed in v0.77.0, are closed above. The v0.77.0 list is on its page.

What was checked, and what was not

Checked for this page, on the v0.77.1 binary (a light install with no Torch, MLX or GPU, on tiny local inputs, with the network off): the output refusals of the eighteen soup data commands, with each exit code and several spellings, and persona-mix still overwriting its prompts; soup plan, soup apply and soup apply --dry-run on a staged field (exit 1, no plan, state file unchanged), on a valid config, and the peak_vram_gb: null state file on both v0.77.0 and v0.77.1; soup doctor --config and soup train --dry-run --yes on an MLX GRPO config, the --resume and quantization: 8bit refusals, and the nine-combination table; soup ship --evidence --progress jsonl on SHIP, DON'T SHIP and unreadable evidence; the soup infer --backend mlx option refusals; the unknown-key messages; soup adapters bisect on both binaries; the adapter dependency messages with NumPy blocked; the soup train --cloud lambda output refusal; and the VRAM pre-flight against synthetic card sizes. Test files go from 1070 to 1106, and the catalog stays at 171 recipes.

Read in the code and not run: the MLX GRPO training loop and MLX generation (no Apple Silicon here); streaming, including the untied embedding and head adapter, the MoE skeleton build and two-read experts (no Torch or CUDA here); /v1/tools/python, verified at the function level because the HTTP route answers 501 on Windows; soup export cleanup; the spike recovery hint write; the Windows PATH rules, with the symlink and junction spellings as upstream lists them; the cloud controllers' behaviour with ssh or scp missing; and any Hub request. The Docker image was not pulled.

Not measured, and not claimed: an MLX speed, memory, time or accuracy figure, a speed or memory effect of the streamed boundary adapter or of two-read experts, and the estimate's error on any run outside the 39 it was fitted on. The selective-checkpointing record is a benchmark of an installer nothing in soup train uses.

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.