Data Tools

Soup includes powerful CLI tools for preparing training datasets. Full augmentation guide: Data augmentation.

Inspect

bash
soup data inspect ./data/train.jsonl

Shows dataset statistics: sample count, token distribution, field analysis. For vision datasets, automatically shows image statistics (images referenced, missing image field, found on disk, outside the image directory, formats). --rows N (-r) sets how many sample rows it prints; the default is 5.

--image-dir DIR (new in v0.76.0) names the directory the images live in. Without it, relative image paths resolve against the data file's directory, the same way training resolves them, and an image that training would refuse as outside that directory is reported as outside rather than counted as found. See Data formats for the rules.

Validate

bash
soup data validate ./data/train.jsonl
soup data validate ./data/train.jsonl --format alpaca
soup data validate ./data/train.jsonl --min-valid-fraction 0.9

Checks for missing fields, encoding issues, and format compliance. Auto-detects format when --format is not specified.

It agrees with the loader now, and it did not before v0.75.0. The validator used to judge a row by which top-level keys were present, while load_dataset runs the real converters and drops any row that comes back empty. Those are two different notions of valid, and they disagreed in two directions: the six formats with no key signature at all — prm, pre_tokenized, input_output, video, multimodal and raft — skipped validation entirely and reported every row valid regardless of content, and for the formats that were checked, key presence passed rows a converter drops on a value, such as a null field or a non-dict message. A file could validate clean and then fail in the middle of a training run.

The valid-row count is now computed by running the real conversion path per row, so the agreement is structural rather than a second, weaker check that drifts. When a row is dropped, the converter's own reason is surfaced for the first few offenders, row 3: chatml message must be a dict, so the report says which rows and why rather than only a count. On upstream's own datasets this shifted 120 of 360 file-by-format verdicts, every one of them an over-count on a file that genuinely had zero valid rows in that format.

Since v0.76.0 soup train (and --dry-run) prints the same count when its loader drops rows, Warning: N of M rows dropped: they do not convert as '<format>'. First: row <i>: <reason>., with the soup data validate command that lists them all, and it exits 1 before loading the model when the data yields no training rows at all. See Data formats.

The exit codes matter, because this is the first step of the PR gate soup ci init writes:

ExitMeaning
0At least one row is usable, and any --min-valid-fraction is met. A partially valid dataset still exits 0 when no minimum is given
2A non-empty dataset with no usable rows, or one below --min-valid-fraction. This is the gate failing
3An input or usage error: a missing file, a format that cannot be detected (an empty file with no --format included), an unknown --format, a --min-valid-fraction outside [0.0, 1.0], or a mistyped flag

Since v0.76.0 a bad invocation of soup data validate exits 3, so 2 always means the data failed the check. Through v0.75.2 an input error exited 1 and a mistyped flag exited 2, the same code as a failed gate. A CI step that tests for exit 1 as "bad input" no longer matches; one that treats any non-zero as failure still works. One case still exits 1: a file extension the loader does not support (anything outside .jsonl, .json, .csv, .parquet and .txt) ends in an unhandled error, not a usage error. The other commands that follow the same 0 / 2 / 3 rule are listed on Exit codes.

--min-valid-fraction takes a float in [0.0, 1.0] and defaults to 0.0, meaning no minimum. Two of those behaviours are new in v0.75.0: before it, a file with zero usable rows exited 0, and --format bogus exited 0 while reporting "2/2 rows valid for bogus format", which quietly turned the CI gate into a pass for any file. An unknown format is an input error rather than a failed gate, deliberately, so a typo cannot be mistaken for a real rejection.

The accepted values for --format are auto plus the nineteen convertible formats: alpaca, sharegpt, chatml, dpo, kto, llava, sharegpt4v, plaintext, embedding, audio, tool-calling, prm, pre_tokenized, input_output, video, multimodal, raft, asr and cross_encoder (the last is new in v0.76.0 and is valid only with task: cross_encoder). The flag's own --help text lists only seven of them and is stale; this list is the allowlist the code checks against.

Clean

bash
soup data clean raw.jsonl --dry-run
soup data clean raw.jsonl

soup data clean (new in v0.76.0) strips control characters, zero-width spaces and byte-order marks, drops rows with an empty assistant turn, and optionally strips boilerplate, closes unclosed code fences, repairs tool-call JSON and prunes echo turns. It writes a new file and never modifies the input. The full reference is on Clean a dataset with soup data clean.

Convert

bash
soup data convert ./data/train.jsonl --to sharegpt --output converted.jsonl

Transform between alpaca, sharegpt, and chatml formats.

Merge

bash
soup data merge data1.jsonl data2.jsonl --output merged.jsonl --shuffle

Combine multiple datasets with optional shuffling.

Deduplicate

bash
# Requires: pip install "soup-cli[data]"
soup data dedup ./data/train.jsonl --threshold 0.8

# Semantic near-dup removal over embedding cosine (v0.71.36, needs [train]):
soup data dedup ./data/train.jsonl --semantic -o clean.jsonl

Remove near-duplicate samples using MinHash, or add --semantic to catch reworded duplicates MinHash's shingling scores as distinct. See Data Moat II for the semantic path, plus soup data topics (coverage map) and soup data canary (memorization probe). --output defaults to <input>_deduped.jsonl and --field limits what is hashed or embedded to one field. --device (auto, cpu or cuda) and --embed-model (default sentence-transformers/all-MiniLM-L6-v2) apply to --semantic only.

Extended Statistics

bash
soup data stats ./data/train.jsonl

Length distribution with histograms, token counts, and language detection.

Synthetic Data Generation

bash
# Generate using OpenAI API
soup data generate --prompt "Create math word problems" --count 100 --format alpaca

# Use a different model
soup data generate --prompt "Medical Q&A pairs" --model gpt-4o --count 500

# Deduplicate against existing data
soup data generate --prompt "..." --count 200 --dedup-with existing.jsonl

# Use seed examples to guide style
soup data generate --prompt "..." --seed examples.jsonl --count 100

# Use a local server (soup serve, Ollama, etc.)
soup data generate --prompt "..." --provider server --api-base http://localhost:11434/v1

--api-base follows one rule for the providers that take a URL (openai, server and vllm): plain http only for loopback (localhost, 127.0.0.1 or ::1), https for a remote server, and, since v0.75.2, never an IP address in a private, link-local or reserved range, on either scheme (the error reads api_base: private/link-local/reserved IP hosts are not allowed (SSRF protection)). A host written only as numbers that is not a valid IPv4 address (10.0.0.256, 4294967296) is refused too (v0.76.0), with api_base: an all-numeric host that is not a valid IPv4 address is not allowed (SSRF protection). Reach a server on your network by its hostname over HTTPS. 0.0.0.0 is not loopback, so use localhost. The ollama provider accepts loopback only.

Multi-Provider Support (v0.20.0+)

bash
# Generate via local Ollama instance
soup data generate --prompt "..." --provider ollama --model llama3.1
soup data generate --prompt "..." --ollama-model llama3.1  # shorthand

# Generate via Anthropic Claude API (set ANTHROPIC_API_KEY env var)
soup data generate --prompt "..." --provider anthropic --model claude-3-haiku-20240307

# Generate via local vLLM server
soup data generate --prompt "..." --provider vllm --model meta-llama/Llama-3.1-8B-Instruct

Domain Templates (v0.20.0+)

bash
# Code instruction pairs (Python, JS, Go, Rust, Java)
soup data generate --prompt "..." --template code --language Python --task-type function

# Multi-turn conversations
soup data generate --prompt "..." --template conversation --turns 6 --topic "science"

# QA from context document
soup data generate --prompt "..." --template qa --context document.txt

# Preference data (DPO/KTO/ORPO)
soup data generate --prompt "..." --template preference --pref-task dpo

# Chain-of-thought reasoning (GRPO)
soup data generate --prompt "..." --template reasoning --domain math

--template reasoning tells the generator how to end each solution, so the golds can be read by GRPO's accuracy and verifiable math rewards: math rows end with #### <number> on its own line, and logic and code rows with an Answer: <answer> line. Before v0.76.0 the prompt asked only for "the final answer", and a gold such as So there are 42 apples. made soup train refuse the dataset before generation (#1349). --dedup-with also applies to --template preference rows (DPO and KTO formats) since v0.76.0, together with within-run duplicates; before that, preference output skipped deduplication, so an earlier preference generation may contain duplicates.

The template flags default to --language Python, --task-type function (also debug, explain, refactor, test), --turns 4 (2 to 10), --topic "general knowledge", --pref-task dpo (also kto, orpo) and --domain math (also logic, code). The options that apply to every template:

FlagDefaultWhat it does
--output, -ogenerated.jsonlWhere the rows are written
--format, -falpacaRow shape: alpaca, sharegpt or chatml
--batch-size5Examples requested per API call
--temperature, -t0.8Sampling temperature
--requests-per-minute, --rpm60Rate limit on API calls
--api-keyOPENAI_API_KEYRead from that environment variable when the flag is omitted; prefer the variable
--trust-remote-codeoffLocal provider only: allow a model that ships custom Python (default deny)

Quality Pipeline (v0.20.0+)

bash
# Auto-validate after generation (remove malformed entries)
soup data generate --prompt "..." --validate

# Auto-filter by quality (coherence scoring)
soup data generate --prompt "..." --filter

# Auto-dedup (MinHash, requires: pip install "soup-cli[data]")
soup data generate --prompt "..." --dedup

# Full quality pipeline: validate + filter + dedup
soup data generate --prompt "..." --quality-pipeline

Quality Filter

bash
# Filter by coherence score
soup data filter ./data/train.jsonl --coherence 0.3

# Filter by perplexity + coherence
soup data filter ./data/train.jsonl --perplexity 500 --coherence 0.3

# Add scores without removing samples
soup data filter ./data/train.jsonl --score-only

Uses perplexity + coherence scoring to identify low-quality samples.

Data Sampling (v0.23.0+)

bash
# Random sample
soup data sample ./data/train.jsonl --strategy random -n 1000

# Diverse sample (TF-IDF clustering)
soup data sample ./data/train.jsonl --strategy diverse -n 500

# Hard examples (by length)
soup data sample ./data/train.jsonl --strategy hard -n 500

Give --n (a row count) or --pct (a percentage from 0 to 100). With neither the command exits 1, and when both are given --pct wins. --seed makes random and diverse reproducible; hard is deterministic and ignores it. The output defaults to <input>_sampled_<strategy>.jsonl beside the input (the flag's own help text says <input>_sampled.jsonl and is stale), and an explicit --output must sit under the current directory.

Data Splitting (v0.23.0+)

bash
# Split into train/val/test. --val and --test are integer PERCENTAGES,
# not fractions; the train remainder is implied, and --train is accepted
# for command parity rather than read.
soup data split ./data/train.jsonl --val 10 --test 10

# Absolute row counts instead of percentages, with a reproducible seed
soup data split ./data/train.jsonl --val 500 --test 500 --absolute --seed 42

# Stratified split. --stratify takes the field to stratify on, as a value.
soup data split ./data/train.jsonl --val 10 --stratify category

# Semantic stratification (v0.73.2): cluster rows by TF-IDF + K-Means, then
# split within each cluster. Cannot be combined with --stratify.
soup data split ./data/train.jsonl --val 10 --stratify-semantic --num-clusters 8

--stratify-semantic is lexical, not an embedding model: it clusters a TF-IDF matrix of each row's text with K-Means (5 clusters unless --num-clusters says otherwise), needs scikit-learn from pip install "soup-cli[data]", and refuses a dataset over 50,000 rows. --num-clusters without --stratify-semantic only prints a warning.

HuggingFace Dataset Hub (v0.24.0+)

bash
# Search for datasets
soup data search "math reasoning"

# Preview remote dataset metadata
soup data preview tatsu-lab/alpaca

# Download to local JSONL
soup data download tatsu-lab/alpaca --output ./data/alpaca.jsonl --samples 1000

Dataset Registry (v0.24.0+)

Keep a local, named record of datasets (name, path and format) in ~/.soup/datasets.json:

bash
# Register a dataset (the path must sit under the current directory)
soup data register my-chat-data --path ./data/chat.jsonl --format chatml

# List registered datasets
soup data registry

# Remove an entry
soup data unregister my-chat-data

The registry is a lookup you list with soup data registry. soup train does not resolve registered names. data.train: registry:my-chat-data and data.train: my-chat-data are both treated as Hugging Face dataset ids (the console prints Loading from HuggingFace: ...), because neither has a file suffix, and both fail there. Put the file path in data.train. (soup data register --help still says "for use in soup.yaml"; that text is out of date.)

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.