Data Engineering Pro (v0.69.0)
Dataset prep stops being "throw a JSONL at the trainer" and becomes a first-class engineering discipline. dbt-shaped DAG, Great-Expectations suite, Magpie synth, Persona-Hub diversity, and the arXiv 2510.13928 brain-rot detector.
soup build — dbt-shaped DAG of dataset transforms
# manifest.yaml
# Each model names a transform, not SQL. A seed model reads a source;
# every downstream model names the models it consumes in refs.
models:
- name: chats_raw
kind: incremental
source: data/thumbs_up.jsonl
transform: identity
- name: chats_nonempty
kind: incremental
refs: [chats_raw]
transform: drop_empty
- name: chats_train
kind: table
refs: [chats_nonempty]
transform: token_countsoup build manifest.yaml --dry-run # validates topology, exits 0 (LIVE today)
soup build manifest.yaml # materialises (live since v0.71.6)--output-dir (-o, default build_out, which must stay under the working directory) receives one <model>.jsonl for every table and incremental model, plus the SQLite state store .soup_build_state.sqlite. A view is derived in memory for the models that refs it and is never written to disk, which is why the last model above is a table: make the model you want to train on a table or an incremental. drop_empty drops rows whose text field is empty or blank (config.field names a different field); no built-in transform decontaminates anything.
- Closed
SUPPORTED_MODEL_KINDS = {incremental, table, view} - Topo-sort via Kahn's algorithm
- Re-tokenise only changed rows:
compute_row_hash(SHA-256 over canonical-JSON,idfield excluded) +incremental_diff(prev, new) → {added, changed, removed, unchanged} - DoS caps:
_MAX_MODELS=256,_MAX_REFS_PER_MODEL=32,_MAX_FILE_BYTES=1 MiB - Live as of v0.71.6 —
run_buildmaterialises datasets with five built-in transforms, and these are the whole list:identity,drop_empty,lowercase,add_fieldandtoken_count. Anything else must be a dotted import path (my_pkg.transforms:filter_low_quality), resolved from the CLI, which upstream flags as arbitrary code execution: only run a manifest you trust, and do not feed an untrusted one tosoup buildon a shared CI host. A name that is neither is caught by the runner, not by the plan, so--dry-runexits 0 on a manifest whose real run dies on the first unknown transform. The schema deliberately does not validate the function reference.tablerebuilds,viewre-derives,incrementalre-transforms only the rows whose content hash changed (SQLite state store, keyed by row hash + transform fingerprint). A custom transform is a function of exactly two positional arguments, the row and the model'sconfigmapping, and it returns a dict, orNoneto drop the row; the function is resolved, and its module imported, once when the build starts. It has to be importable by the Python that runssoup(installed, or onPYTHONPATH): in a test, a module that sat only in the working directory was not found. v0.71.6 also makessoup data gen-magpiegenerate live (ollama / vllm raw-completion, SSRF-hardened;anthropicrejected).
soup expect — Great Expectations for chat data (LIVE)
# suite.yaml: every entry is a mapping with a name; arguments go under args
expectations:
- name: expect_no_pii
- name: expect_token_length_between
args: {min_tokens: 32, max_tokens: 2048}
- name: expect_no_refusal_pattern
- name: expect_chosen_preferred_over_rejected_by_judge
args: {threshold: 0.7}soup expect ./data.jsonl ./suite.yaml
# exit 0 = pass; 2 = validation rejection; 3 = suite failureClosed allowlist: expect_no_pii (reuses v0.47 Presidio), expect_token_length_between, expect_no_refusal_pattern (reuses v0.56 refusal detector), expect_chosen_preferred_over_rejected_by_judge (the judge hook is Python-only; see below). A bare name is refused with exit 2 (expectations[0] must be a dict) and a - expect_x: {...} mapping is refused with exit 2 as well (expectation name must be non-empty). The length check takes min_tokens and max_tokens, defaulting to 1 through 1,048,576 (so with no arguments an empty row fails), and it counts whitespace-separated words, not tokenizer tokens. Unknown keys under args are ignored, so a misspelled bound leaves that bound off without an error. For the PII and length checks it walks text, content, output, prompt, instruction, response top-level keys + messages[].content arrays; input, conversations[].value, chosen, rejected and completion are not read. _MAX_SUITE_LEN=64. Drop into CI between soup data and soup train.
expect_chosen_preferred_over_rejected_by_judge does not call a judge from the command line. soup expect runs the suite without a judge function, so on the CLI the expectation only checks that every row has both chosen and rejected (a row missing either fails); threshold is accepted and never compared with anything. A pass is not a preference-quality check; for length bias and identical pairs use soup data lint. A real judge needs run_suite called from Python with a judge_fn.
soup data gen-magpie — synthetic data via chat-template-prefix harvest
soup data gen-magpie --base meta-llama/Llama-3-8B \
--provider ollama --target 50000 \
--output ./synth.jsonlMagpie trick: prime the base model with just the assistant chat-template prefix and let it complete the prompt itself. Reuses the v0.20 provider stack, minus Anthropic: Magpie primes the base model with a bare chat-template prefix, which a hosted chat API will not do, so --provider anthropic is refused here. Ollama and vLLM work. --base is sent to the provider as its model name, so with --provider ollama it has to be a model your Ollama server has pulled (the example id above is illustrative); a provider that is unreachable or does not know the model produces zero rows and a warning, not an error. _MAX_TARGET_ROWS=1_000_000, _MAX_BASE_MODEL_LEN=512. The live run_magpie loop and the v0.47 quality-filter chain shipped in v0.71.6, with --quality-filter on by default.
--max-tokens caps the tokens requested per generation (default 512, allowed 1 to 16384, otherwise exit 2). --base-url points at the provider and defaults to loopback (http://localhost:11434 for Ollama, http://localhost:8000 for vLLM). For Ollama it must still be a loopback host: a remote URL is refused with exit 2 (Remote Ollama instances are not supported). For vLLM, plain HTTP is accepted only for localhost, any other host needs HTTPS, and a private, link-local or reserved IP literal is refused on either scheme.
soup data persona-mix — Persona-Hub diversity sampler (LIVE)
soup data persona-mix --prompts ./prompts.jsonl \
--n 20000 --output ./diverse.jsonl
# Optional BYO: --personas tencent-200k.jsonl --styles styles.jsonlBundled 12 personas × 5 writing styles (BYO Tencent 200k corpus via --personas / --styles). Deterministic by seed (random.Random(seed)). compute_topic_diversity = Shannon entropy over pooled whitespace tokens. Atomic JSONL write + cwd-contained input + enforce_under_cwd_and_no_symlink on output. Caps: 100 MiB / 100k entries per loader.
soup data brain-rot — AI-slop detector (LIVE)
soup data brain-rot ./data.jsonl --strict --max-major-fraction 0.25
# exit 3 if MAJOR fraction > 0.25arXiv 2510.13928 brain-rot detector. Two pure-Python scorers:
score_triviality— token-diversity inversion +!!/??punctuation runs + low-effort token density + length penaltyscore_popularity_signal— clickbait phrase scan + emoji U+1F300–U+1FAFF density
Worst-signal-wins: 1.0 − max(triviality, popularity). Bands match v0.26/v0.56/v0.65: OK ≥ 0.85, MINOR ≥ 0.60, else MAJOR. refuse_if_rotten raises when MAJOR fraction exceeds threshold. English-keyword-only in v0.69.0; multilingual shipped in v0.71.6 behind --lang en|es|fr|de|ru|auto.
Cross-cutting hardening
Refactored 3 duplicate TOCTOU blocks behind a new shared paths.enforce_under_cwd_and_no_symlink helper (code-review CRITICAL fix). v0.70 + future releases reuse it.
Numbers
+262 tests in v0.69.0 (11,225 → 11,487) across 5 new test files. 3 POSIX-only symlink tests skip on Windows.
See also
- Anti-trend insurance (v0.68) — the TOCTOU helper this consolidates was first lifted here.
- Loop hardening (v0.70) —
soup expectgates data going into--reward-hack-detectorruns. - Eval depth (v0.65) — same OK / MINOR / MAJOR taxonomy.
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.