Data Pipeline Pro (v0.42.0)
18 features across 6 parts. Axolotl + LLaMA-Factory data-layer parity.
5 new data formats
data:
train: ./data/train.jsonl
format: prm # PRM stepwise-supervised
# | pre_tokenized # LF tokenized_path / Axolotl empty
# | input_output # Axolotl template-free segments+labels
# | video
# | multimodal # axolotl content-parts schemaRemote URIs
7-entry allowlist for object stores. Bucket regex ^[a-zA-Z0-9][a-zA-Z0-9._\-]{0,62}$. Userinfo / fragment / query rejected.
data:
train: s3://my-bucket/train.jsonl # s3 | gs | gcs | az | abfs | abfss | oci
streaming: true
buffer_size: 10000
shards: 16Live fsspec loaders shipped in v0.53.8 (schema-only in v0.42.0).
A single Hugging Face Hub name streams too, since v0.75.0. data.streaming: true with data.train: org/dataset is forwarded to datasets.load_dataset(..., streaming=True), buffer_size shuffles that stream, and Soup then materialises up to 1,000,000 rows, printing a warning if the source is larger. Before v0.75.0 the flag was ignored for a Hub name and the whole dataset was loaded. buffer_size shuffles the train split only; a validation split is taken in order.
data.prompt_strategy — your own row transform
An Axolotl-style module.path:function_name spec. The import is resolved once at setup, so a bad path fails fast rather than mid-epoch, and the function is applied to each row before template rendering.
data:
train: ./data/train.jsonl
prompt_strategy: my_pkg.strategies:to_chatIt runs code you name, so it is operator-controlled by design: only point it at a module you trust. (The schema description for this field still says "schema-only in v0.42.0"; the executed loader has applied it since v0.53.7.)
Multi-dataset interleave
data.interleave selects the strategy, and it is validated at config load. It attaches to a list-valued data.train and is refused outright against a single path, so the sub-block never appears on its own:
# A FRAGMENT, not a config: this block goes under the data: key, beside
# a list-valued data.train. On its own at the root it is refused as an
# unknown key, because only a root-level lora: block is remapped.
data:
interleave:
strategy: probs # concat | under | over | probs
probs: [0.7, 0.3] # one entry per dataset, in data.train's orderProbs are validated as 2–32 entries summing to 1.0 ± 1e-6, each in (0, 1], and the list must be exactly as long as data.train. The full config is below.
This was a known gap until v0.74.0, and the history is worth one paragraph, because the site published the gap while it stood.
data.interleavehad validated, documented and parsed since v0.42.0 while nothing read it at training time, so every multi-dataset mixture silently trained ondata.train's single path.data.trainalso declared a single string, so naming several datasets was rejected outright at config load. v0.73.3 repaired only the visible half, the recipesoup data mix --optimizewrites, which had been emitting a list form its own loader rejected.
v0.74.0 wired it. data.train accepts str | list[str]; a list of two or more entries is combined by data.interleave into one row set, and a single path stays byte-identical to what it was.
v0.75.0 then changed where val_split sits in that order, and the consequence is worth knowing before you trust a validation number. Under over or probs the same row could land in both train and val, because the split ran after the padding and a padded copy is the same row. On the eager local-file and all-Hub-name paths val_split now runs first, per source, so that cannot happen. Two things follow, both upstream's own:
- The requested fraction is no longer exact under
overorprobs, because it is taken from each source's smaller, unpadded row count. Two sources of 1000 and 100 rows withoverandval_split: 0.1give 110 validation rows out of 1910, which is 5.8%, not 200 out of 2000. - Train and validation end up with different source mixtures. If you oversampled specifically to correct a source imbalance, your validation set still reflects the original, un-rebalanced skew.
concat and under never pad, so neither applies to them. The streaming path is not fixed at v0.75.0 and can still place one row on both sides of the split under over or probs.
data:
train:
- ./data/support.jsonl
- ./data/code.jsonl
interleave:
strategy: probs # concat | under | over | probs
probs: [0.7, 0.3]Streaming and Hugging Face Hub names work too, and the strategies are deliberately mapped so a name means the same thing on both paths: concat concatenates, under and over stop at the first exhausted or the last, and probs passes the probabilities through. That equivalence was verified by running one probs config through both paths and comparing the resulting proportions, rather than by two tests that each pass alone.
Two shapes are still refused by name: an all-Hub list with data.streaming: true, and any list mixing Hub names with local or remote entries. training.packing and training.multipack are rejected with a list too, at config-parse time, with a message naming the reason.
How entries are classified
Each entry of a data.train list is classified on its own. It is a local file if its suffix is .jsonl, .json, .csv, .parquet or .txt, a remote URI if it contains ://, and otherwise a Hugging Face Hub name. So teknium/OpenHermes-2.5 is a Hub name, not a file with a .5 suffix. A list may mix local files and remote URIs, or hold only Hub names, but Hub names beside the others are refused at config load. Remote entries need data.streaming: true, and streaming picks the file type per entry from its suffix.
A :// entry counts as remote whatever its scheme, and only the seven object-store schemes above load. An https:// or ftp:// entry passes config load and is refused by name when the data is loaded. Under streaming, probs samples from the sources, so the proportions are approached rather than met exactly.
When every entry is a Hub name, a Hub entry's own validation split becomes the combined validation set only if every entry has one. If only some do, Soup prints a warning, ignores all of them, and data.val_split applies instead.
Advanced masking + vocab expansion
add_new_tokens and new_special_tokens are applied by the SFT, DPO, IPO, ORPO, SimPO, KTO, BCO, GRPO and online DPO trainers (and task: preference, which hands off to the paired-loss ones); the TTS trainer handles new_special_tokens itself. Where they are applied, the tokens are added to the tokenizer and the embeddings are resized whenever a token was actually added. Every other task (pretrain, PPO, reward-model, distill and embedding among them) accepts the keys and ignores them. train_on_prompt is mutually exclusive with train_on_responses_only, which defaults to true; on the transformers backend, putting prompt tokens in the loss means setting train_on_responses_only: false, since nothing else reads train_on_prompt.
Accepted by the schema, but nothing in v0.75.2 reads it: mask_history, eval_on_each_dataset, split_thinking, image_min_pixels, image_max_pixels, image_resize_algorithm, video_fps, video_maxlen, extend_conversation, skip_prepare_dataset, remove_unused_columns and resize_vocab. Setting them validates and does not change training, but they are still part of the config hash that soup ship --config compares, so editing one makes --evidence from before the edit read as stale; in particular, split_thinking does not turn on any <think> masking, and remove_unused_columns (default true) is never passed to the Hugging Face trainer: the SFT, pretrain, DPO, ORPO, SimPO, KTO, IPO, BCO, GRPO, reward-model, embedding, distill, PRM, ASR and MoLE-routing trainers all set remove_unused_columns=False themselves as a constant.
data:
add_new_tokens: ["<thought>", "</thought>"]
new_special_tokens: ["<|im_end|>"]soup data ingest
Convert PDF / DOCX / MD / TXT into JSONL.
soup data ingest mybook.pdf --output mybook.jsonlInstall pypdf for PDFs and python-docx for DOCX; neither is in a Soup extra, and without them the command exits with pypdf not installed. Run: pip install pypdf. Markdown and plain text need nothing. A PDF becomes one row per page, a DOCX one row per non-empty paragraph, Markdown one row per heading ({"text", "section", "level", "source"}) and a text file one row. Input and output must sit under the current directory, and the output defaults to ./ingested.jsonl.
AOT tokenize cache
soup data preprocess soup.yaml --output ./cachePlans the cache key (16-char SHA-256 of dataset + tokenizer + max_length + format). The live tokenize loop shipped in v0.53.7.
The config file and --output must both sit under the current directory (exit 1 otherwise). If <output>/<cache key> already exists, the command prints Target already exists and exits 0 without tokenizing anything; --yes tokenizes again and replaces that directory.
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.