Correctness First (v0.36.0)
Four silent-failure modes Soup had → loud failures.
Assistant-only loss masking
By default, Soup masks every non-assistant token with -100 so the SFT loss reflects only what the model should generate. Toggle via data.train_on_responses_only (default true):
data:
train: data.jsonl
train_on_responses_only: true # default
# OR per-message control:
# train_on_messages_with_train_field: trueWhen the tokenizer ships a chat template with {% generation %} markers, the mask is exact. Without those markers, Soup falls back to an incremental tokenize-delta walk and documents the looseness.
It does not apply to vision or audio SFT. The multimodal collators supervise every token. soup doctor --config reports the key as ignored for task: sft with modality: vision (multimodal vision collator supervises all tokens; assistant-only masking is not implemented for vision datasets (#1156)) and for modality: audio (audio training supervises all tokens ...); both lines were produced on the v0.76.0 CLI.
If you trained SFT with
training.packing: truewhile this key was on (the default), re-run it if the model should only learn the replies. With TRL 0.29.1, upstream reports, dataset packing drops the unpreservedlabelscolumn and falls back to training all input tokens except the first, so the prompt and system tokens were trained on as well, and the loss curve looked normal. Since v0.76.0 SFT datasets carry anassistant_maskscolumn alongsidelabels(also underuse_liger: true), and pre-tokenized caches that carrylabelsare augmented with it automatically (#1236, #1304).
If you trained with this before v0.73.3, re-run it. A tokenizer returns a mapping object that is not a
dict, and a type guard missed that, so the label mask was built from the mapping's key strings rather than from its token ids. There was no exception and no warning: the loss curve looked entirely normal, and the run trained on zero tokens. Token ids are now read throughinput_idsand normalised to plain integers before they reach the collator, and a missing or non-integer id fails loudly instead of producing a garbage mask. Separately, a template that reports an all-zero assistant mask while assistant messages plainly exist now falls back to the incremental walk, rather than shipping an all--100no-op. The same assumption was removed fromsoup data doctor, which had inherited it.
--trust-remote-code opt-in
Thirteen command modules require --trust-remote-code to load any HF model that ships custom Python (auto_map in config.json): train, chat, serve, infer, diff, merge, export, shrink, draft, generate, and the data, data doctor and eval groups. The default is deny, everywhere.
soup train --config soup.yaml --trust-remote-codeFirst-party orgs (Meta, Mistral, Qwen, Google, etc. — 15 in the allowlist) suppress the warning panel; everything else prints a REMOTE CODE WARNING panel before loading.
The count is still thirteen at v0.76.0: the same modules declare the option (counted by searching src/soup_cli/commands/ at both tags), and v0.76.0 made it reach commands that dropped it (#1449, #1478):
soup eval autoforwards the flag to both of its steps.soup eval benchmarkandsoup eval customtake their own--trust-remote-code, off by default, and the flag is appended to the lm-eval model arguments from the flag alone, never from an adapter'sadapter_config.json. Through v0.75.2soup eval autoaccepted the flag and nothing read the value (see Model Evaluation).soup train --find-lrloads the model with the resolved flag (it used a hardcodedFalse), andsoup draft distilland the heal step ofsoup shrinkpass--trust-remote-codeon to the childsoup traininstead of telling you to re-run with a flag you had already given.soup data download --trust-remote-codeis refused (exit 1) when the installeddatasetspackage is version 4 or newer, because those versions dropped remote dataset code and the flag would be silently ignored. Installdatasets<4if a dataset needs its loading script. Without the flag nothing changes.
Chat-template hardening
Tokenizers without a chat template now raise a ValueError with a fix suggestion instead of silently building garbage f"{role}: {content}" strings.
data:
train: data.jsonl
chat_template: chatml # or: llama3, qwen2.5, mistral, gemma3, phi4, deepseek-r1, or a raw Jinja stringRaw Jinja strings are validated: null bytes, >64 KB, and filesystem-touching directives ({% include %}, {% import %}, {% from %}, {% macro %}, {% extends %}) are rejected at config-load.
Which trainers honour data.chat_template. Through v0.75.2 only the SFT path applied it: a DPO, ORPO, SimPO, KTO, BCO, IPO, PPO, reward-model, GRPO or online-DPO run with data.chat_template: llama3 silently rendered its prompts with the tokenizer's own template (#804, #919). Since v0.76.0 those ten trainers install the override before conversational data is rendered, as SFT does. If you set data.chat_template on one of them, the override was ignored; re-run if the rendering mattered. Tasks that never render chat refuse the key at config load instead of ignoring it: pretrain, embedding, classifier, reranker, cross_encoder, prm, asr, moe_lora_routing and unlearn (not applied under stream_layers, which has its own task-specific refusal). For example, soup doctor --config on a task: pretrain config that names a chat template prints data.chat_template is not used by task='pretrain'; remove it or choose a conversational training task and exits 2 (run on the v0.76.0 CLI).
OOM-probe auto batch size
training:
batch_size: auto # unchanged
auto_batch_size_strategy: probe # NEW: 'static' | 'probe' | 'auto' (default)Replaces the static memory formula with a real try-halve-then-double-to-ceiling loop.
The fit criterion changed in v0.74.0, and it matters most on Windows. Until then the probe's only test was "the synthetic step did not raise". Under the WDDM driver, which is native Windows and WSL2, the allocator does not raise on an over-commit: it spills into host memory. So a batch that ran an order of magnitude slower in shared memory was approved and cached. The probe now reads
max_memory_allocatedafter the step and refuses anything above what the process can actually reach on the device, and the cache key carries a version tag so entries written by the old probe are ignored rather than trusted. If you usedbatch_size: autoon Windows or WSL2 before v0.74.0, the cached size may never have been a fit.
Two floors arrived in v0.76.0, per upstream's changelog: batch_size: auto never resolves below 2 for task: kto, which refuses a batch of 1 at config load for every KTO config, and for the default contrastive embedding loss, where a one-row batch gives a loss and gradient of exactly 0.
Picked size is cached at ~/.soup/batch_cache.json keyed on (model, max_length, quantization, lora_r, gpu_name, gpu_memory_gb) so repeat runs short-circuit. Cache file gets best-effort 0o600 perms after atomic rename.
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.