Data Mixing Optimizer & Dynamic Curriculum (v0.48.0 — BETA)

soup data mix --optimize

A search over per-dataset mixture weights. By default the objective is a synthetic proxy rather than your data; --live swaps in short training runs.

bash
# Offline dry run: exercises the budget tracker and the recipe writer only. The proxy loss
# is a fixed function of the weights and never reads your data, so the weights mean nothing.
soup data mix --optimize \
  --datasets sft.jsonl,preference.jsonl,instruct.jsonl \
  --output mix.yaml

# The real search: each candidate mixture runs a short soup train from your base config.
soup data mix --optimize --live --base-yaml soup.yaml \
  --datasets sft.jsonl,instruct.jsonl --budget 2h --num-probes 12 --output mix.yaml

# --apply does not write a dataset. It prints the data: block to splice
# into your soup.yaml, so the mixture is resolved at training time.
soup data mix --apply mix.yaml

Without --live the search is synthetic. The default proxy loss is a fixed function of the weights, smallest at the uniform mixture, and it never reads your datasets, so the recipe it writes says nothing about them. --live requires --base-yaml (a soup.yaml that supplies base, task, training and output) and trains each candidate for a short run, capped per candidate at the budget divided by the probe count, between 60 seconds and 30 minutes. --budget (default 1h), --num-probes (1 to 256, default 8), --seed and --overwrite (an existing --output is refused without it) apply to both modes; --output defaults to mix_recipe.yaml, and --datasets takes at least two paths under the current directory. Install soup-cli[mix] for the scikit-optimize loop; without it a seeded random sampler is used.

The emitted recipe now renders data.train as the full dataset list, index-aligned with interleave.probs, rather than collapsing to the single highest-weighted dataset the way it had to before data.interleave was read at training time. Each entry is written as the dataset's absolute path, because the paths you pass are resolved first, so the recipe is tied to the directory it was generated in: rewrite data.train as relative paths before you commit it or move it to another machine.

BudgetTracker caps wall-clock and token budget per search.

Since v0.73.3 the emitted recipe loads. It names the single highest-weighted dataset in data.train and keeps the full ranked breakdown as a comment, because at the time data.train was typed as a single string. v0.74.0 closed that: data.train now accepts a list, and two or more local paths are combined through data.interleave before the validation split runs, with streaming and Hub-name lists covered too.

Dynamic curriculum learning

yaml
training:
  curriculum: true
  curriculum_dynamic: true
  curriculum_buckets: 5
  curriculum_metric: loss      # length | perplexity | loss

DynamicCurriculumPolicy recomputes per-bucket weights every N steps from per-bucket loss and gradient-norm statistics and records them in curriculum_history.jsonl. compute_bucket_weights clamps to a stable simplex. At v0.75.2 nothing feeds those weights back into the sampler: the callback's own docstring calls its deliverable the history record, so curriculum_dynamic: true records a trajectory and does not reorder the data.

Three keys shape what that policy computes. curriculum_dynamic_recompute_steps (default 50, 1 to 100000) is how often the weights are recomputed. curriculum_dynamic_temperature (default 1.0, above 0 and at most 100) is the softmax temperature on the uncertainty signal: higher flattens the weights and lower concentrates them on the hardest buckets. curriculum_dynamic_floor (default 0.05) is the smallest normalised weight any bucket can fall to; it must be above 0 and no larger than 1 / curriculum_buckets, so curriculum_buckets: 4 allows at most 0.25 and a larger floor is refused at config load. curriculum_dynamic itself requires curriculum: true.

Static curriculum: true is applied by the SFT trainer only, and it only sorts the training rows by text length. curriculum_metric: loss or perplexity prints a notice that the metric needs pre-computed scores and does the same length sort, and curriculum_buckets only matters to the dynamic callback. Soup installs no sampler of its own for the sorted rows, and the Trainer's default sampler is random (checked on transformers 5.17), so that order does not reach the model.

bash
soup runs curriculum-curve <run-id>
soup runs curriculum-curve <run-id> --history ./runs/$LAST/curriculum_history.jsonl

Renders the per-bucket weight curve over training.

Both features are BETA in v0.48.0. Symlink containment hardening lands across all file paths.

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.