v0.76.1: the bundled DPO examples train again
v0.76.1 was tagged on 2026-10-14. It is a patch on the v0.76.0 line and, like v0.75.1 and v0.75.2, a backport: its tree is the v0.76.0 tag plus nine commits, and the rest of what has been merged on the development branch since v0.76.0 is left for the next minor release. Upstream gave it no codename. In its own words, it "makes the bundled DPO examples train again, puts the same nesting and expansion limits, and a size cap, on every YAML file Soup reads, fixes the declared Typer floor, and loads Hub dataset ids that contain a dot. Everything in v0.76.0 is unchanged."
All seven changes are maintainer pull requests (#1825, #1699, #1826, #1858, #1866, #1827, #1889), plus a correction to three changelog fragments (#1894). The defect this page is named after was reported from outside, by @dryn85-max (#1849), and the post-repair 8B row that upstream's docs now quote was measured by @umran666 (#1103).
Coming from 0.75.x? Read v0.76.0 first. That release refuses settings Soup accepted and never applied, so a config that loaded before can stop loading. v0.76.1 changes none of that, and it adds one refusal and one new drop of its own, both below: a
soup.yamlover a size, nesting or expansion limit stops the load, and a preference row that cannot be written in one layout is dropped at load with a warning.
What to do
pip install -U "soup-cli[train]==0.76.1"
soup doctor --config soup.yaml- Re-train one kind of run, if it matters to you: a
task: reward_modelrun on preference rows with a stringpromptbesidechosen/rejectedmessage lists that repeat the user turn. It trained on a text with the question in it twice. See Re-run this. No other change in this release calls for a re-run. - Train again what failed. On v0.76.0,
task: dpo,ipo,bcoandpreferencefailed inside trl on those rows after the model had loaded, so there is no finished run to replace. - Check a large or deeply nested
soup.yaml. Asoup.yamlover 1 MiB, or nested deeper than 100 levels, now stops the load with one line naming the file.soup doctor --configprints that line and exits 2;soup trainexits 1. - Check your preference data. A row that cannot be written in one layout is now dropped at load, with a warning that names the first one.
soup data validate data.jsonl --format dpo(or--format kto) counts every such row and prints the reasons for the first three. It exits 0 while any row is still valid, unless you give it--min-valid-fraction. - An old Typer is handled at install time. Upgrading to v0.76.1 upgrades a Typer below 0.15.4, or names the conflict. On v0.76.0 the upgrade left it in place: below Typer 0.12.4 no command started, and 0.12.4 to 0.15.3 started only next to Click 8.1 or older.
- The audit log masks two more options. From v0.76.1 the values of
--slack-urland--discord-urlare written as<redacted>. Lines written by v0.76.0 or earlier are not rewritten.
Re-run this
| What was wrong | Who is affected | PR |
|---|---|---|
Upstream: "reward_model no longer trains on a text with the question in it twice." On v0.76.0 the training text built for the first row of examples/data/chat_preferences.jsonl began What is machine learning? What is machine learning? Machine learning is...; on v0.76.1 it begins What is machine learning? Machine learning is.... Both were read from the trainer's data preparation at each tag; no model was trained to check it. | task: reward_model on rows with a string prompt beside chosen / rejected message lists that repeat the user turn, which is how both bundled DPO examples are written (the bundled examples/configs/rlhf_step2_reward.yaml trains on one of them). Verified on v0.76.0; the code that produced it is unchanged at every earlier tag this site's audit read, back to v0.40.0. Re-train such a reward model if it matters to you. | #1849, #1866 |
On v0.76.0, dpo, ipo, bco and preference failed inside trl on the same rows, and upstream says orpo and simpo "prepare the same tokens as before", so neither group has a finished run to replace.
The bundled DPO examples train again
What was wrong. Both bundled files, examples/data/dpo_sample.jsonl and examples/data/chat_preferences.jsonl, and any dataset written the same way, put a string prompt beside chosen / rejected message lists that repeat the user turn. On v0.76.0, on those rows task: dpo, ipo, bco and preference "failed inside trl after the model had loaded, with an error that changed with PYTHONHASHSEED ('list' object has no attribute 'endswith', or a TypeError)" (#1849, #1866). soup train --dry-run does not reach the trainer, so it passed on dpo_sample.jsonl on both releases. orpo and simpo trained.
What changed. The dpo and kto converters now hand the trainer every preference row in one layout: all strings, or all message lists with the prompt turns in prompt only. How a row is read:
prompt,chosenandrejectedall strings: text, passed on as written. An all-text dataset is unchanged.prompta message list: conversational. A string answer becomes one assistant turn, and a prompt that both answers repeat and continue is taken out of them.prompta string beside message-list answers: the leading turns the two answers share are the prompt, and the string is not read. With no shared turn, the string becomes the user turn.- No
prompt, withdata.format: dposet: the shared leading turns are the prompt, or, with none shared, the row'smessageslist. Through v0.76.0 such a row was dropped withdpo.prompt must not be null. Auto-detection still needs apromptkey. ktorows follow the same layout rule overpromptandcompletion, except thatpromptis still required: a string prompt beside a message-list completion is the user turn, unless the completion leads with turns of its own before its first assistant turn, and then those turns are the prompt.
A null or absent message content is still accepted: rows with such a turn trained before and still do.
A string prompt beside message-list answers is not read when the two answers share leading turns. Those turns are the prompt, the way orpo and simpo already read such a row, so a question written only in that string has to be in the conversations as well.
Rows dropped at load, with a reason. A row that cannot be written in one layout is dropped: one answer a string and the other a message list, a malformed message, or no prompt to be found. The loader prints one warning that names the first dropped row and, for a local file, the soup data validate command to run, for example:
Warning: 2 of 5 rows dropped: they do not convert as 'dpo'. First: row 2: dpo.chosen is a string and dpo.rejected is a message list: give both as strings or both as message lists.
List them all with: soup data validate mixed.jsonl --format dpoThe load then continues and exits 0, so a dataset can shrink at load, with a warning, never silently. If no row converts, soup train stops with No training rows: ... loaded 0 train samples as format 'dpo'. and exits 1. Whatever the hint says, soup data validate counts every dropped row but prints the reasons for the first three only, then ... and N more, and it exits 0 while any row is valid; pass --min-valid-fraction to make it exit 2 below a fraction you choose. Other reasons you may see, as v0.76.1 prints them:
dpo.prompt is missing and dpo.chosen and dpo.rejected share no leading turn to use as the prompt: add 'prompt' (a string or a message list)dpo.chosen[0].role must be a non-empty stringdpo.chosen[0].content must be a string or a list of content blocks, got dict (write a JSON tool result as a JSON string)kto.completion has no turn after the 1 that lead it: there is no assistant answer
Mixed datasets load as conversations, with a note. A split that holds plain-text preference rows beside conversational ones is rewritten so the whole split goes through the chat template. Each split is checked on its own:
Note: 1 of 3 train rows is a plain-text preference row in a dataset whose other rows are conversations. It was rewritten as one user turn and one assistant turn per answer, so the whole split goes through the chat template.The other tasks. orpo and simpo "prepare the same tokens as before". reward_model sees the question once (see Re-run this). Upstream's tests check that online_dpo, grpo and ppo read the same prompts from the bundled files as before, and online_dpo now also reads a prompt written as a message list; upstream's end-to-end check did not run those three.
YAML limits, and a soup.yaml over one stops the load
Since v0.76.1 every YAML file Soup reads goes through one loader (#1827). Nesting is limited to 100 levels, and the document's top-level mapping counts as the first, so 99 levels of brackets under a top-level key is the deepest structure that loads. Expansion is limited to one node per source byte plus 10,000 for anchors, aliases and merge keys, with merge keys chained at most 100 levels. Each reader keeps its own size cap:
| What is read | Size cap |
|---|---|
The training-config readers (soup.yaml), and readers that had no cap before | 1 MiB |
soup data mix recipes, soup export --quant-config files, RA-DIT configs, sweep files | 256 KiB |
| Agent specs | 5 MiB |
| Can manifests | 16 MiB |
| README front matter | 64 KiB |
The config read by soup ship | 4 MiB |
A training config is normally kilobytes, so these limits matter for generated files, heavy anchor reuse and YAML you did not write.
A soup.yaml that is over a limit, cannot be read or is not valid YAML stops the load with one line naming the file and the problem. On v0.76.0 a 1.1 MiB config loaded, and a syntax error surfaced as the parser's multi-line message followed by Run with --verbose for the full traceback. What v0.76.1 prints:
Config file soup.yaml is larger than 1048576 bytes; refusing
Config file soup.yaml nests collections more than 100 levels deep; refusing
Config file soup.yaml is not valid YAML: expected ',' or ']', but got '<stream end>' (line 14, column 1)
Config file soup.yaml is not valid YAML: cannot build this value: day is out of range for month (line 13, column 7)
Config file soup.yaml is a directory, not a filesoup train exits 1. soup doctor --config prints the same line, then Config could not be loaded (see above)., and exits 2. These are new refusals: a config that loaded on v0.76.0 can stop loading, and soup doctor --config soup.yaml is the first check, as it was for v0.76.0.
The other readers report through their own handlers. soup plan and soup apply exit 2 on a limit (a syntax error stays 1). soup eval gate given a suite over a limit prints Cannot load suite: ... and exits 3 (a syntax error stays 1); soup train --gate prints Invalid --gate suite: ... and exits 1. soup ship --config reads its config under its own 4 MiB cap. A syntax error in soup plan, soup ship --config or a soup eval gate suite still prints the parser's multi-line message. soup push and the template detection of soup deploy ollama, which read soup.yaml only for metadata, skip a file over a limit rather than refusing.
The Typer floor is 0.15.4
Soup declares typer>=0.15.4 since v0.76.1 (#1802, #1858). Below Typer 0.12.4 no command can start, soup --help and soup version included, and Typer 0.12.4 to 0.15.3 start the CLI only next to Click 8.1 or older. Upgrading Soup now upgrades such a Typer, or names the conflict, at install time, and soup doctor reports it as outdated (need >=0.15.4) with an upgrade line. That report is an advisory: it does not change soup doctor's exit code. Below Typer 0.12.4 soup doctor cannot start either.
Through v0.76.0 the declared floor was typer>=0.9.0, so an install over an older Typer succeeded and its first command could fail. A fresh install was never affected, since a resolver already picks the newest Typer. click>=8.0.0 "was measured again and holds", and one CI cell now installs exactly the declared Typer and Click floors and starts the CLI on them. Python stays >=3.10,<3.13, and no other dependency bound moved.
A Hub dataset id with a dot loads from the Hub
data.train: teknium/OpenHermes-2.5 "failed with Data file not found because the loader read the dot as a file extension" (#1889). A single-string data.train now takes the same route as the same name inside a data.train list:
- a supported file extension (
.jsonl,.json,.csv,.parquet,.txt) means a local file; - a string containing
://is a remote URI; - anything else is a Hub id.
One addition for the single string: an existing file with an unsupported suffix stays local, so a real data.tsv still gets Unsupported file format. A path with an unsupported suffix that does not exist, such as the typo data/train.jsnol, now goes to the Hub lookup, as it already did inside a data.train list, instead of failing with Data file not found; upstream says that offline it reports a connection error. See How entries are classified.
The audit log masks --slack-url and --discord-url
The audit log now finds credential options from the command tree and also masks long options by name (#1827). So the values of --slack-url and --discord-url are written as <redacted>, because a webhook URL is all it takes to post to the channel, and --min-tokens (soup data clean), a length threshold, is no longer masked. The name rule itself is unchanged: the value of a long option whose name contains token, key, secret, password, passwd or credential is masked, apart from a short list of names that never carry a credential, and since v0.76.1 that holds for a long option the command does not declare as well.
Lines already in ~/.soup/audit.jsonl are not rewritten, so a webhook URL logged by v0.76.0 or earlier is still there as typed. See Governance.
This release is not accompanied by a security advisory. The supported version for security fixes is still 0.76.x.
Docs
- The 8B NF4 figure is stated with its clock (#1207, #1699). Upstream's README and performance docs now print the Llama-3.1-8B NF4 laptop row, measured on v0.72.2, beside the post-#331 row, and no longer say the re-measurement is pending. The post-#331 row, measured by @umran666 on another RTX 3050 Laptop 4 GB (#1103), reads 208.6 tok/s at 1935 to 1957 MHz in a 2.40 GB peak; the 119.6 tok/s row was taken at 952 MHz, before the repair. The two are not directly comparable. In upstream's words, that is 59% of its same-session GEMM ceiling against 68%: about 14% slower per unit clock, so the raw 1.74x gain is the clock, not a speedup. Large-layer streaming also changed the memory pattern between the two rows, so even that fraction is not like-for-like, and the record attributes the shortfall to none of the repair, the memory change or the stack. The table is on Layer streaming.
- The What's New block of the Turkish, Arabic, Japanese and Chinese READMEs now describes v0.76.0 (#1825).
- Post-release corrections to upstream's v0.76.0 docs, with no code change (#1826).
Known limitations
These are upstream's, in plain terms.
- A typo in a single-string
data.trainnow goes to the Hub. A path with an unsupported suffix that does not exist (data/train.jsnol) is looked up as a Hub id, as it already was inside adata.trainlist, instead of failing withData file not found. Offline it reports a connection error. An existing file with an unsupported suffix (data.tsv) still getsUnsupported file format. - v0.76.0's warnings and refusals are unchanged. The settings v0.76.0 accepts with a warning at load (for example
lr_groups,early_stop_patience,forgetting_eval_steps) andsoup export --format awq/--format gptqare still accepted with a warning; upstream says v0.77 will refuse or remove them. Coming from 0.75.x, runsoup doctor --config soup.yamlfirst. - A string
promptbeside message-list answers is not read when the two answers share leading turns. Those turns are the prompt, asorpoandsimpoalready read such a row, so a question written only in that string has to be in the conversations as well. - The YAML nesting limit counts the document's top-level mapping as the first level, so 99 brackets under a top-level key is the deepest structure that loads.
- The
pixtral-dporecipe still cannot train (#1867). This patch does not change it. - Upstream's
docs/data.mddoes not yet describe the one-layout rule for preference rows. This page does, from the changelog's wording.
What was checked, and what was not
No gated work ships in this release, so no record is added under benchmarks/. The preprint (concept DOI 10.5281/zenodo.21771064) states no measured number that this patch changes, and its scope is unchanged. #1699 restates the paper's 8B row with the clock it was taken at, and #1207 tracks the amendment of the preprint as a new version.
How upstream checked the patch. The DPO fix was run end to end on a tiny random model on CPU, without quantization: dpo, ipo, bco, kto, preference, orpo, simpo and reward_model, under two hash seeds, with a control run on v0.76.0 that failed. Not exercised in that check:
online_dpo,grpoandppo(they need a judge or a reward model);- runs on a GPU, or with 4-bit and 8-bit bases;
- the oversize-file refusals of the YAML readers other than the training config;
- the Typer and Click floor variants beyond Typer 0.15.4 with Click 8.0.0.
The test suite was not run in full on the maintainer's machine; the CI run on the release branch covers it. Test files go from 875 to 883, and the catalog stays at 170 recipes.
See also
- v0.76.0: settings that never took effect now stop the load: the release this patch is cut from, and its Re-run table.
- Data formats: preference rows.
- Data Pipeline Pro: how a
data.trainentry is classified. - Configuration and Settings Soup refuses or warns about at load: what stops a config from loading.
- Exit codes: the full table.
- Governance & Provenance: the audit log.
- What's new across v0.71 to v0.76.
- The upstream changelog at the tag.
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.