examples/README.md led with `soup train --config examples/configs/sft_basic.yaml`
and "Takes ~2-3 minutes on a consumer GPU". The command failed at validation,
and the fixture is 10 rows, so no training would have happened either way. That
timing was never measured; no timing claim replaces it.
Every command in the file was re-run against the installed CLI. Broken ones:
- `soup export output_sft_basic/ ...` -> requires -m/--model, not positional
- `soup merge output_sft_basic/ ...` -> requires -a/--adapter, not positional
- `soup data convert ... --from alpaca` -> no --from flag; source is auto-detected
- `soup data filter --input X` -> takes a positional path
- `soup data score ... --output Y` -> no --output; it prints a scorecard
- `soup data generate --provider ...` -> --prompt is required and was missing
- `soup data demo --list` -> no --list; bare `soup data demo` lists
Same three bugs were in synthetic_workflow.md and are fixed there too, so no
file in this directory documents a command that does not run.
The `soup data inspect` sample output was invented -- it claimed 50 entries for
a 10-row file and showed a row ("Identify the odd one out") that is not in it.
Replaced with the real output. Both config templates at the bottom of the file
taught the old flat schema, which is how a reader would have written a config
that fails to parse; both now show the current nesting.
The README is now split by what a file actually promises, which was the whole
problem: it presented a config with no data as if it were runnable.
Runnable examples -- parse and run as-is on the bundled fixtures. Stated
plainly: 5-10 rows, a smoke test, produces nothing useful.
Templates -- valid configs where you supply the data. vision_llama lives
here, and says why there is no fixture.
The Runnable table carries a VRAM column because "runnable" is hardware-
relative: the five TinyLlama configs are verified to clear the pre-flight on a
4 GB card, while dpo_example (~13 GB) and dpo_chat (~22 GB) cannot -- their
weights alone are 4.0 and 7.0 GB. Those numbers are what `soup train` predicts.
New examples/data/README.md, because Simon Willison read the folder as a
starting corpus and nothing in it said otherwise. First line says these are
format examples and test fixtures; a table gives the real row counts (10 / 5 /
8 / 5, recounted); then where to get real data, and roughly how much is needed
-- hundreds of rows for a format or style, thousands for a task the model
half-knows, and RAG rather than fine-tuning for new facts. No claim about how
volume scales with model size: we have not measured that.
|
||
|---|---|---|
| .. | ||
| configs | ||
| data | ||
| reward_hacking | ||
| README.md | ||
| synthetic_workflow.md | ||
| synthetic_workflow.yaml | ||
README.md
Soup Examples
Configuration examples and sample datasets.
Two kinds of files live here, and they promise different things:
- Runnable examples — parse and run as-is against the bundled fixtures in
data/. They exist to prove your setup works end-to-end. The fixtures are 5–10 rows, so these runs finish quickly and produce nothing useful — they are a smoke test, not training. Pointdata.trainat real data for that. - Templates — valid configs where you supply the data. They will not run until you do.
See data/README.md for what the fixtures are and where to get real data.
Runnable examples
| Config | Task | Base model | Fixture | Needs |
|---|---|---|---|---|
configs/sft_basic.yaml |
sft | TinyLlama-1.1B-Chat | alpaca_tiny.jsonl (10 rows) |
4 GB |
configs/grpo_reasoning.yaml |
grpo | TinyLlama-1.1B-Chat | reasoning_math.jsonl (5 rows) |
4 GB |
configs/rlhf_step1_sft.yaml |
sft | TinyLlama-1.1B-Chat | alpaca_tiny.jsonl (10 rows) |
4 GB |
configs/rlhf_step2_reward.yaml |
reward_model | TinyLlama-1.1B-Chat | chat_preferences.jsonl (5 rows) |
4 GB |
configs/rlhf_step3_ppo.yaml |
ppo | TinyLlama-1.1B-Chat | alpaca_tiny.jsonl (10 rows) |
4 GB |
configs/dpo_example.yaml |
dpo | Llama-3.1-8B-Instruct | dpo_sample.jsonl (8 rows) |
~13 GB |
configs/dpo_chat.yaml |
dpo | Llama-2-7b-chat | chat_preferences.jsonl (5 rows) |
~22 GB |
The five TinyLlama configs are verified to clear the pre-flight on a 4 GB card. The two
larger-base configs cannot — their weights alone are 4.0 GB and 7.0 GB — so the "Needs"
column is what soup train predicts for them, margin included.
Run any of them:
soup train --config examples/configs/sft_basic.yaml
Validate a config without training:
soup train --config examples/configs/sft_basic.yaml --dry-run
Gated models (Llama-2, Llama-3.1) need Hugging Face access and a login first:
huggingface-cli login. The TinyLlama configs need neither.
soup train estimates peak VRAM before it loads anything and refuses to start a run it
predicts won't fit, printing the breakdown and what to change. The TinyLlama configs are
sized for a small card (batch_size: 4, max_length: 512) — on a bigger one you can
raise both.
Basic SFT
sft_basic.yaml — instruction tuning with LoRA (r=16) on alpaca-format rows. The
smallest complete config in the repo; a good base to copy.
Preference alignment (DPO)
dpo_chat.yaml and dpo_example.yaml both train on prompt / chosen / rejected
pairs. dpo_example.yaml adds QLoRA (quantization: 4bit) for memory-efficient
training on a larger base, and dpo_beta: 0.1 sets how hard the KL penalty pulls
back toward the reference model.
Reasoning (GRPO)
grpo_reasoning.yaml generates num_generations: 4 completions per prompt and scores
them with the built-in accuracy reward. See
docs/training.md for custom reward functions.
Full RLHF pipeline
Three configs, run in order — step 3 reads the reward model that step 2 writes:
soup train --config examples/configs/rlhf_step1_sft.yaml
soup train --config examples/configs/rlhf_step2_reward.yaml
soup train --config examples/configs/rlhf_step3_ppo.yaml
Templates
Valid configs that need your data before they run.
Vision
configs/vision_llama.yaml — LLaMA-3.2-11B-Vision on
image + conversation pairs in LLaVA format. There is no vision fixture in this repo
because we do not commit image files, so data.train and data.image_dir are
placeholders. Replace both, then:
soup train --config examples/configs/vision_llama.yaml
Built-in templates
soup init writes a starter soup.yaml you fill in with your own data:
soup init --template kto # unpaired preference (thumbs up/down labels)
soup init --template orpo # reference-free alignment
soup init --template simpo # length-normalized preference
soup init --template ipo # regularized preference (squared hinge)
soup init --template pretrain # continued pre-training on raw text
soup init --template moe # Mixture-of-Experts (Qwen3, Mixtral, DeepSeek V3)
soup init --template longcontext # RoPE scaling for 128k+ context
soup init --template embedding # sentence embeddings (BGE, E5, GTE)
soup init --template vision # vision-language
soup init --template audio # audio/speech — needs pip install "soup-cli[audio]"
soup train
Full list: soup init --help. Available templates are audio, bco, chat, code,
embedding, eu-ai-act, hipaa, ipo, kto, longcontext, medical, moe,
orpo, pretrain, reasoning, rlhf, simpo, soc2, sr-11-7, tool-calling,
vision.
Synthetic-data workflow
Generate training data from a local LLM, filter and score it, then train on the result.
Walkthrough in synthetic_workflow.md; the config it trains with
is synthetic_workflow.yaml (a template — it reads
./synth_clean.jsonl, which the workflow produces).
Reward-hacking mitigation demo
reward_hacking/rewards.py provides synthetic reward
functions for the closed-loop mitigation feature (soup train --reward-hack-mitigation): a gameable length_hack_reward / sentinel_reward proxy
decoupled from a held-out true_score. Point a GRPO config's training.reward_fn at a
.py that re-exports one as reward_fn, enable reward_hack_detector: info_rm +
reward_hack_mitigation: kl_control, and watch mitigation_log.jsonl under the run's
output dir. See
docs/training.md.
Dataset formats
Soup auto-detects and normalizes:
- Alpaca:
instruction,input,output - ShareGPT:
conversationswithfrom/value - ChatML: OpenAI-style
messageswithrole/content - DPO/ORPO/SimPO/IPO:
prompt+chosen+rejected - KTO:
prompt+completion+label - LLaVA / ShareGPT4V:
image+conversations - Plaintext: raw
.txtor JSONL with atextfield (pre-training) - Audio:
audiopath +messages
Inspect a dataset
soup data inspect examples/data/alpaca_tiny.jsonl
Dataset Stats
┌────────────────────┬────────────────────────────┐
│ Metric │ Value │
├────────────────────┼────────────────────────────┤
│ Total samples │ 10 │
│ Columns │ instruction, input, output │
│ Avg length (chars) │ 180 │
│ Min length │ 60 │
│ Max length │ 368 │
│ Empty fields │ 0 │
│ Duplicates │ 0 │
└────────────────────┴────────────────────────────┘
Convert between formats
The source format is auto-detected; you name the target:
soup data convert examples/data/alpaca_tiny.jsonl \
--to chatml \
--output alpaca_as_chatml.jsonl
Directory structure
examples/
configs/ # YAML configs
sft_basic.yaml # runnable
dpo_chat.yaml # runnable
dpo_example.yaml # runnable
grpo_reasoning.yaml # runnable
rlhf_step1_sft.yaml # runnable
rlhf_step2_reward.yaml # runnable
rlhf_step3_ppo.yaml # runnable
vision_llama.yaml # template — bring your own images
data/ # format fixtures (see data/README.md)
alpaca_tiny.jsonl
chat_preferences.jsonl
dpo_sample.jsonl
reasoning_math.jsonl
reward_hacking/ # synthetic reward fns for the mitigation demo
synthetic_workflow.md # synthetic-data walkthrough
synthetic_workflow.yaml # template config for that walkthrough
Using your own data
- Put your data in one of the supported formats above.
- Point the config at it:
data:
train: /path/to/your/data.jsonl
format: alpaca # or sharegpt, chatml, dpo, llava, ... — omit for auto-detect
- Train:
soup train --config your_config.yaml
Config shape
Configs are nested: base, task, data, training, output at the top level.
config/schema.py is the single source of truth.
Minimal
base: TinyLlama/TinyLlama-1.1B-Chat-v1.0
task: sft
data:
train: ./your_data.jsonl
format: alpaca
max_length: 512
training:
epochs: 3
lr: 5e-4
batch_size: 16
lora:
r: 16
alpha: 32
output: ./output/
More options
base: meta-llama/Llama-2-7b-hf
task: dpo
backend: transformers # or unsloth, mlx
data:
train: ./dataset.jsonl
format: dpo
max_length: 2048
val_split: 0.1
training:
epochs: 2
lr: 1e-4
dpo_beta: 0.1
batch_size: 16 # or "auto" to probe for the largest that fits
gradient_accumulation_steps: 4
quantization: 4bit # 4bit, 8bit, none, gptq, awq, fp8, ...
scheduler: cosine
warmup_ratio: 0.1
gradient_checkpointing: true
lora:
r: 64
alpha: 128
dropout: 0.05
target_modules: auto
# use_dora: true # Weight-Decomposed LoRA
# use_rslora: true # rank-stabilized scaling
output: ./output_advanced/
After training
Batch inference
soup infer --model ./output_sft_basic/ --input prompts.jsonl --output results.jsonl
Merge the LoRA adapter into a full model
soup merge --adapter ./output_sft_basic/ --output ./merged_model/
Export to GGUF
soup export --model ./output_sft_basic/ --format gguf --quant q8_0 --output model.gguf
Requires a built llama.cpp — see docs/serving-and-export.md.
soup export --model ... --deploy ollama exports and registers with Ollama in one step.
Monitor with Weights & Biases
pip install wandb
soup train --config your_config.yaml --wandb
Common issues
"CUDA out of memory"
- Lower
training.batch_size, or set it to"auto"to probe for a size that fits - Add
training.quantization: 4bit - Add
training.gradient_checkpointing: true - Use a smaller base model
- Or stream the base layer-by-layer:
training.stream_layers: true(see docs/training.md)
"Dataset not found"
- Paths in a config resolve from the directory you run
soupin, not from the config's location. Run from the repo root, or use an absolute path. - Check the file parses:
soup data validate your_data.jsonl
"Model not found on Hugging Face"
- Check the model id spelling
- Gated models (Llama, Gemma) need
huggingface-cli loginand accepted terms
Config validation errors
soup train validates before doing any work. base: Field required or
data -> train: Field required means the config uses an old flat layout — see
Config shape above for the current nesting.