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):

yaml
data:
  train: data.jsonl
  train_on_responses_only: true   # default
  # OR per-message control:
  # train_on_messages_with_train_field: true

When 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.

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 through input_ids and 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--100 no-op. The same assumption was removed from soup 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.

bash
soup train --config soup.yaml --trust-remote-code

First-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.

One exception to that list: soup eval auto accepts --trust-remote-code, but nothing in v0.75.2 reads the value (see Model Evaluation).

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.

yaml
data:
  train: data.jsonl
  chat_template: chatml   # or: llama3, qwen2.5, mistral, gemma3, phi4, deepseek-r1, or a raw Jinja string

Raw Jinja strings are validated: null bytes, >64 KB, and filesystem-touching directives ({% include %}, {% import %}, {% from %}, {% macro %}, {% extends %}) are rejected at config-load.

OOM-probe auto batch size

yaml
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_allocated after 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 used batch_size: auto on Windows or WSL2 before v0.74.0, the cached size may never have been a fit.

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.