soup/docs/training.md

1145 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Training Tasks & Methods
[← Back to the Soup README](../README.md)
> SFT, DPO/GRPO/PPO/KTO/ORPO/SimPO/IPO/BCO, tool-calling, PRM, pre-training, distillation, classification, vision/audio/TTS, unlearning, RAFT/RA-DIT, and the loop-hardening detectors.
**Contents:**
- [Loop Hardening](#loop-hardening)
- [Unlearning (`task='unlearn'`, NPO / SimNPO / RMU)](#unlearning-taskunlearn-npo--simnpo--rmu)
- [Continued Pre-training](#continued-pre-training)
- [Knowledge Distillation](#knowledge-distillation)
- [Sequence Classification](#sequence-classification)
- [Reasoning Effort + EOT Control](#reasoning-effort--eot-control)
- [EBFT / GDPO Loss Variants](#ebft--gdpo-loss-variants)
- [GRPO Objective Variants](#grpo-objective-variants)
- [Process Reward Model (PRM)](#process-reward-model-prm)
- [Weighted Multi-Objective Preference Loss](#weighted-multi-objective-preference-loss)
- [MoE Model Support](#moe-model-support)
- [Vision / Multimodal Fine-tuning](#vision--multimodal-fine-tuning)
- [Audio / Speech Fine-tuning](#audio--speech-fine-tuning)
- [GRPO Plus — Objective Variants, Long-Context RL, Multi-Turn Agents](#grpo-plus--objective-variants-long-context-rl-multi-turn-agents)
- [DPO Training](#dpo-training)
- [Preference Variety — BCO + Unified Dispatcher + KL Variants](#preference-variety--bco--unified-dispatcher--kl-variants)
- [GRPO Training (Reasoning)](#grpo-training-reasoning)
- [Tool-Calling Fine-Tuning](#tool-calling-fine-tuning)
- [PPO / Full RLHF Pipeline](#ppo--full-rlhf-pipeline)
- [KTO Training (Unpaired Preferences)](#kto-training-unpaired-preferences)
- [ORPO Training (No Reference Model)](#orpo-training-no-reference-model)
- [SimPO Training (Simple Preference)](#simpo-training-simple-preference)
- [IPO Training (Regularized Preference)](#ipo-training-regularized-preference)
- [RAFT — Retrieval-Augmented Fine-Tuning](#raft--retrieval-augmented-fine-tuning)
- [RA-DIT — Retrieval-Augmented Dual Instruction Tuning](#ra-dit--retrieval-augmented-dual-instruction-tuning)
- [Curriculum-Aware Training (BETA)](#curriculum-aware-training-beta)
- [TTS Fine-Tuning (BETA, v0.52.0)](#tts-fine-tuning-beta-v0520)
- [Classifier / Reranker / Cross-Encoder Training (BETA, v0.52.0)](#classifier--reranker--cross-encoder-training-beta-v0520)
- [Knowledge Distillation (BETA, v0.52.0)](#knowledge-distillation-beta-v0520)
- [EBFT + GDPO (BETA, v0.52.0)](#ebft--gdpo-beta-v0520)
- [gpt-oss `reasoning_effort` + `train_on_eot` (v0.52.0)](#gpt-oss-reasoning_effort--train_on_eot-v0520)
---
## Loop Hardening
Six surfaces protect the training loop from the failure modes that cost a real GPU-hour. The schema + math kernels shipped in v0.70.0; the live trainer-callback wiring shipped in **v0.71.11**, validated end-to-end on SmolLM2-135M.
```bash
# Reward-hacking detector — auto-halt when the policy starts gaming the RM
# (InfoRM cluster-separation index, Wang et al. 2024 arXiv:2402.09345)
soup train --config soup.yaml \
--reward-hack-detector info_rm --reward-hack-halt # halt on HACK verdict
# Cross-tokenizer distillation — Llama -> Mistral, no shared vocab needed
# (Universal Logit Distillation, Boizard et al. 2024 arXiv:2402.12030)
soup train --config soup.yaml --uld-strategy wasserstein
# Cross-tokenizer distillation for FULLY-DISJOINT tokenizers (v0.71.18) —
# aligns student/teacher token sequences over decoded character spans, so you
# can distill a GPT-2 BPE student from a Llama SentencePiece teacher.
# training:
# uld_strategy: wasserstein_aligned
# MiniLLM reverse-KL on-policy distillation — bundles 3 stability tricks
# (Gu et al. 2024 arXiv:2306.08543)
soup train --config soup.yaml --minillm-enabled \
--minillm-teacher-mix-ratio 0.3 \
--minillm-pretrain-anchor-weight 0.1 \
--minillm-pretrain-anchor-path ./pretrain.jsonl
# MiniLLM TRUE on-policy rollout (v0.71.18, Gu et al. §3.1) — sample a fresh
# autoregressive rollout from the per-token teacher/student mixture each step,
# then length-normalised reverse-KL. training.minillm_rollout_length tunes the
# rollout (auto min(max_length, 32)).
soup train --config soup.yaml --minillm-enabled --minillm-on-policy
# Mid-epoch checkpoint for PPO/GRPO — TorchTune punts this; Soup ships it
soup train --config grpo.yaml \
--rl-checkpoint-save-every-steps 500 \
--rl-checkpoint-keep-last 3 \
--rl-checkpoint-include-optimizer
# Iterative DPO loop driver — sample -> RM-score -> re-pair -> retrain
# (drop --plan-only to run the loop; --plan-only just renders the per-round plan)
soup iterative-dpo \
--base-model meta-llama/Llama-3.1-8B \
--reward-model ./output_rm \
--prompts ./prompts.jsonl \
--output-dir ./iterative_dpo_out \
--rounds 5 \
--pairs-per-round 1000
# RAGEN echo-trap detector — auto-halt when trajectories collapse to self-repetition
# (Zhu et al. 2025 arXiv:2504.14437)
soup train --config grpo.yaml \
--echo-trap-enabled \
--echo-trap-threshold 0.6 \
--echo-trap-halt \
--echo-trap-tokenizer-aware
```
`--echo-trap-tokenizer-aware` switches echo-trap n-grams from whitespace tokens to the active tokenizer's integer ids. This catches subword repetition that punctuation-heavy decoded text can hide, but the score becomes tokenizer-specific rather than vocabulary-agnostic.
Every detector composes with v0.34 `soup why` (anomaly explainer), v0.32 spike recovery, and the v0.53.11 #127 `GRPOStabilityCallback` so a single GRPO run can have InfoRM + echo-trap + spike-recovery + in-place ref-model EMA all active simultaneously without duplicating trajectory / state collection. The reward-hack and echo-trap callbacks read the per-step rewards through a shared, thread-safe capture buffer (Soup wraps your reward functions so it never has to monkeypatch TRL); `rm_ensemble` needs ≥2 reward functions to compute a divergence. The MiniLLM teacher-mix is an offline distribution-blend analog of the paper's on-policy teacher-mixed *sampling*, and ULD compares the distributions after clamping teacher ids to the teacher vocab (correct for same-family / extended-vocab pairs; a genuinely different tokenization needs a sequence-alignment step). The reference-model EMA (`--ref-model-ema-alpha`) updates in place — no full `state_dict` round-trip — so it is cheap at 70B+ scale.
## Unlearning (`task='unlearn'`, NPO / SimNPO / RMU)
GDPR right-to-be-forgotten + CSAM/PII leak response, productized. Three method backends:
- **NPO** — Negative Preference Optimization (DPO-shaped negative-only loss; needs a reference model).
- **SimNPO** — length-normalised NPO without a ref model (faster, more stable on long sequences).
- **RMU** — Representation Misdirection Unlearning (residual-stream noise on forget inputs).
```yaml
# unlearn.yaml
base: HuggingFaceTB/SmolLM2-135M
task: unlearn
data:
train: traces.jsonl
forget_set: gdpr_deletion_set.jsonl # rows to unlearn (messages / prompt+completion / text)
retain_set: capability_anchors.jsonl # optional — anchors general capability
training:
unlearn_method: npo # or simnpo / rmu
unlearn_alpha: 0.5 # retain-set weighting [0.0, 10.0]
```
```bash
# Run the unlearn loop (validated on SmolLM2-135M — NPO/SimNPO drive forget loss down).
soup train --config unlearn.yaml --yes
# Score the run on TOFU / MUSE / WMDP (OK / MINOR / MAJOR verdict).
soup eval unlearning <run-id> --benchmark tofu --evidence evidence.json --output report.json
```
`soup train --task unlearn` is live (v0.71.9): it loads a LoRA-wrapped policy, a frozen reference copy (NPO / RMU), and the forget / retain JSONL sets, then optimises the per-method loss — NPO's `(2/β)·mean(-logσ(-β·(π_logp ref_logp)))` drives the policy's forget-set log-prob below the reference (= forgetting), while the retain set anchors capability. Run NPO/SimNPO **with** a `retain_set` — without one the policy has no utility anchor and Soup warns loudly.
Three orthogonal axes: **Forget Quality** (pre/post forget-loss delta), **Model Utility** (retain-accuracy preserved), **PrivLeak** (membership-inference AUC distance from 0.5). Bundled mini-fixtures for all three benchmarks ship in the box (v0.71.1 added MUSE + WMDP alongside the existing TOFU set), so `--benchmark muse|wmdp` runs without supplying evidence. The WMDP forget-set probes ship **redacted** (placeholder prompts + `REFUSED` responses) — Soup never bundles verbatim hazardous-knowledge content.
## Continued Pre-training
Continue training a model on raw text for domain adaptation:
```yaml
base: meta-llama/Llama-3.1-8B
task: pretrain
data:
train: ./data/corpus.jsonl # {"text": "..."} or plain .txt files
format: plaintext
max_length: 4096
training:
epochs: 1
lr: 1e-5
quantization: 4bit
```
```bash
soup init --template pretrain
soup train
```
## Knowledge Distillation
Train a small student model to match a larger teacher's output distribution.
```yaml
base: HuggingFaceTB/SmolLM2-135M
task: distill
modality: text
backend: transformers
data:
train: ./data/chat.jsonl
max_length: 2048
chat_template: chatml
training:
teacher_model: meta-llama/Llama-3.1-8B
distill_divergence: forward_kl # kl | forward_kl | reverse_kl | js
distill_temperature: 2.0
epochs: 3
lr: 5e-5
quantization: 4bit # quantizes student only
```
Loss = student CE + (T**2) × KL(teacher_logits / T || student_logits / T).
Teacher is loaded once, frozen via `requires_grad_(False)` + `.eval()`, and its
inputs / logits are auto-bridged across CPU / CUDA devices.
Set `distill_mode: sequence` (default `token`) to train on the teacher's **generated
continuations** instead of per-token logit matching — a hard-label, cross-tokenizer-friendly
KD that works when student and teacher do not share a vocabulary. `sequence` mode is mutually
exclusive with the cross-tokenizer `uld_strategy` logit path (they are different objectives over
the same task; the trainer rejects the combination at setup). (v0.71.12)
## Sequence Classification
Train a classifier head on top of any base model — supports single-label,
multi-label, and cross-encoder reranking.
```yaml
base: BAAI/bge-base-en-v1.5
task: classifier # or `reranker`, `cross_encoder`
modality: text
backend: transformers
data:
train: ./data/labelled.jsonl # rows: {"text": "...", "label": "spam"} or {"text": "...", "label": [0, 1, 0]}
max_length: 256
training:
num_labels: 3
classifier_kind: single_label # or `multi_label`
label_names: [ham, spam, promo] # required when labels are strings
epochs: 5
lr: 2e-5
batch_size: 32
```
Routes `classifier` / `reranker` / `cross_encoder` through
`AutoModelForSequenceClassification`. Multi-label heads cap at 1024 entries per
row, dedup via set conversion, and reject null bytes in label strings.
Add a `lora:` section to train a **frozen encoder + LoRA adapter** classifier instead of the
full model — the small adapter plus the (freshly-initialised) classification head train, the
encoder backbone stays frozen:
```yaml
training:
num_labels: 3
lora:
r: 16
alpha: 32
```
(v0.71.12)
## Reasoning Effort + EOT Control
gpt-oss-style reasoning-effort control for instruction tuning.
```yaml
training:
reasoning_effort: high # low | medium | high
train_on_eot: true # do NOT mask the EOT/EOS token in the loss
```
`reasoning_effort` injects `<|reasoning_effort|>high<|/reasoning_effort|>` into
the system turn (creating one if absent). `train_on_eot=True` makes the model
learn when to stop generating by training on the trailing EOS token instead of
masking it out. Both are gated to the SFT-family of tasks.
## EBFT / GDPO Loss Variants
Entropy-regularised SFT (`ebft_variant: structured | strided`) and generalised
DPO (`gdpo_variant: standard | length_normalized | margin`) — both attach
idempotently via `compute_loss` wrappers and auto-fire when the corresponding
variant field is set on `TrainingConfig`.
```yaml
# SFT with EBFT structured
training:
ebft_variant: structured
ebft_temperature: 1.0
# DPO with GDPO length_normalized
task: dpo
training:
gdpo_variant: length_normalized
dpo_beta: 0.1
```
## GRPO Objective Variants
Soup ships live math kernels for 6 GRPO objective variants in addition to the
default. Set `grpo_variant` in `training` and the trainer automatically
subclasses `trl.GRPOTrainer` to route `compute_loss` through the matching
kernel:
```yaml
task: grpo
training:
reward_fn: accuracy
num_generations: 4
grpo_variant: gspo # group-stabilised importance ratio
# or: dapo / dr_grpo / bnpo / rft / two_sided
# grpo_delta: 0.2 # required when grpo_variant=two_sided
```
Variants:
- **standard** — DeepSeek-R1-style baseline (delegates to TRL's `compute_loss`).
- **gspo** — group-stabilised importance ratio with per-batch control variate.
- **dapo** — decoupled asymmetric clipping (`eps_lo=0.2, eps_hi=0.28`).
- **dr_grpo** — token-sum without per-sample length normalisation.
- **bnpo** — length-normalised PPO surrogate.
- **two_sided** — symmetric clipping with operator-supplied `grpo_delta`.
- **rft** — rejection-sampling fine-tuning (only positive-advantage tokens contribute).
The stability callback (EMA ref-model update, replay buffer, TIS alert counter)
attaches automatically when any of `ref_model_ema_alpha` / `replay_buffer_size`
/ `tis_threshold` / etc. is set.
## Process Reward Model (PRM)
Train a scalar reward head over stepwise-supervised reasoning chains. Data
format is the v0.42.0 `prm` shape — one row per `{prompt, completions: [step1,
step2, ...], labels: [r1, r2, ...]}`:
```yaml
task: prm
data:
format: prm
train: ./prm_train.jsonl
max_length: 2048
training:
epochs: 1
lr: 1.0e-5
```
The trainer loads `AutoModelForCausalLM`, attaches an `nn.Linear(hidden, 1)`
reward head, and computes MSE between predicted scalars at step-boundary tokens
and the per-step labels.
## Weighted Multi-Objective Preference Loss
Mix DPO / SimPO / ORPO / IPO terms in one training run by setting
`preference_loss_weights` (must sum to 1.0):
```yaml
task: preference
training:
preference_loss_weights:
dpo: 0.6
simpo: 0.4
```
The combine wrapper reads policy + reference summed log-probs from the inner
TRL trainer's per-batch inputs and computes a true weighted sum via the
in-tree `compute_dpo_term` / `compute_simpo_term` / `compute_orpo_term` /
`compute_ipo_term` kernels. BCO cannot be mixed with paired losses (data
format incompatible — rejected at config load).
## MoE Model Support
Fine-tune Mixture of Experts models (Mixtral, Qwen3-30B-A3B, DeepSeek V3) with ScatterMoE LoRA — applies LoRA to both attention layers and expert FFN layers:
```yaml
base: Qwen/Qwen3-30B-A3B
task: sft
training:
moe_lora: true # target expert + attention layers
moe_aux_loss_coeff: 0.01 # router load-balancing loss
quantization: 4bit
```
Soup auto-detects MoE architectures. Works with all training tasks.
```bash
soup init --template moe
soup train
```
## Vision / Multimodal Fine-tuning
Fine-tune vision-language models (LLaMA-3.2-Vision, Qwen2-VL, Pixtral) on image+text data:
```bash
# Install vision support
pip install 'soup-cli[vision]'
# Create a vision config
soup init --template vision
# Train
soup train --config soup.yaml
```
```yaml
base: meta-llama/Llama-3.2-11B-Vision-Instruct
task: sft
modality: vision
data:
train: ./data/vision_train.jsonl
format: llava
image_dir: ./data/images
val_split: 0.1
training:
epochs: 3
lr: 1e-5
quantization: 4bit
lora:
r: 64
alpha: 16
```
**Supported vision data formats:**
**LLaVA:**
```json
{"image": "photo.jpg", "conversations": [{"from": "human", "value": "<image>\nDescribe this image."}, {"from": "gpt", "value": "A cat on a mat."}]}
```
**ShareGPT4V:**
```json
{"image": "chart.png", "conversations": [{"from": "human", "value": "<image>\nWhat does this show?"}, {"from": "gpt", "value": "Quarterly revenue."}]}
```
`soup data inspect` automatically shows image statistics (count, formats, missing files) for vision datasets.
## Audio / Speech Fine-tuning
Fine-tune audio-language models (Qwen2-Audio, Whisper) on audio+text data:
```bash
# Install audio support
pip install 'soup-cli[audio]'
# Create an audio config
soup init --template audio
# Train
soup train --config soup.yaml
```
```yaml
base: Qwen/Qwen2-Audio-7B-Instruct
task: sft
modality: audio
data:
train: ./data/audio_train.jsonl
format: audio
audio_dir: ./data/audio
val_split: 0.1
training:
epochs: 3
lr: 1e-5
quantization: 4bit
lora:
r: 64
alpha: 16
```
**Audio data format:**
```json
{"audio": "recording.wav", "messages": [{"role": "user", "content": "Transcribe this audio."}, {"role": "assistant", "content": "Hello world."}]}
```
## GRPO Plus — Objective Variants, Long-Context RL, Multi-Turn Agents
Soup ships seven GRPO objective variants, between-rollouts vLLM standby, four agent-rollout backends, seven stability/efficiency knobs, plus Process Reward Models and Vision-RL.
```yaml
# soup.yaml — DAPO with replay buffer and TIS truncation masking
base: meta-llama/Llama-3.1-8B-Instruct
task: grpo
data:
train: ./prompts.jsonl
format: chatml
training:
reward_fn: accuracy
num_generations: 8
# New: GRPO objective variants
grpo_variant: dapo # one of: gspo / dapo / dr_grpo / bnpo / two_sided / rft / standard
# grpo_delta: 0.2 # required when grpo_variant: two_sided
grpo_fp16: true # FP16 RL (unsloth parity)
# Long-context + memory-efficient RL
long_context_grpo: true # wires Tiled MLP when available
vllm_sleep_mode: true # between-rollouts vLLM standby — LIVE (vLLM >= 0.7)
# Multi-turn agent rollout — openenv is LIVE: your function's rows replace the prompt dataset
rollout_backend: openenv # one of: art / ruler / nemo_gym / openenv
rollout_func: my_module:my_rollout # module:function resolver (openenv; trusted operator code)
# Stability / efficiency knobs
ref_model_ema_alpha: 0.99 # EMA sync policy → reference
replay_buffer_size: 2048
async_grpo_prefetch: true # overlap rollout + train
tis_threshold: 2.0 # truncated importance sampling
mask_truncated_completions: true # paired with tis_threshold
defer_rerolling: true
skip_zero_advantage: true
off_policy_mask_threshold: 0.5
```
Process Reward Models (stepwise-supervised):
```yaml
# soup.yaml
base: meta-llama/Llama-3.1-8B
task: prm # New: Process Reward Model
data:
train: ./prm_dataset.jsonl
format: prm # stepwise-supervised data shape
training:
epochs: 3
lr: 1e-5
```
Vision RL on Qwen2-VL / Pixtral / InternVL:
```yaml
# soup.yaml
base: Qwen/Qwen2-VL-7B-Instruct
task: grpo
modality: vision
data:
train: ./vlm_prompts.jsonl
format: llava
training:
reward_fn: accuracy
vision_grpo: true # VLM-RL opt-in
```
All flags ship as schema gates in v0.50.0; live loss kernels, vLLM sleep-mode plumbing, ART/RULER/NeMo Gym/OpenEnv launchers, and the PRM trainer wrapper land in v0.50.1 — schema accepts the values now so configs are stable.
## DPO Training
Train with preference data using Direct Preference Optimization:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: dpo
data:
train: ./data/preferences.jsonl
format: dpo
training:
epochs: 3
dpo_beta: 0.1
lora:
r: 64
alpha: 16
quantization: 4bit
```
## Preference Variety — BCO + Unified Dispatcher + KL Variants
Five preference losses live behind one config knob. Pick a loss without
renaming your task, anneal β over training, and periodically refresh the
frozen reference.
### BCO (Binary Classifier Optimization)
Same input format as DPO; rows are split internally to TRL's BCO
unpaired schema (`{prompt, completion, label}`).
```yaml
task: bco
data:
train: ./data/preferences.jsonl
format: dpo
training:
bco_beta: 0.1
```
### Unified preference dispatcher
Use `task: preference` + `training.preference_loss` to swap losses
without touching `task`. Hyperparameter sweeps over the loss type
itself become trivial.
```yaml
task: preference
data:
train: ./data/preferences.jsonl
format: dpo
training:
preference_loss: dpo # or simpo, orpo, ipo, bco
```
Legacy `task: dpo` / `task: simpo` / etc. remain first-class — the
unified surface is additive.
### KL-controlled DPO variants
Anneal β over training, periodically refresh the reference model:
```yaml
task: dpo # or task: preference + preference_loss: dpo, or task: ipo
training:
dpo_beta: 0.1
dpo_beta_schedule: linear # linear | cosine | exponential
dpo_beta_end: 0.01
dpo_ref_regen_epochs: 2 # copy student → ref model every 2 epochs
```
Both controls are gated to DPO-family tasks (`dpo`, `ipo`, or
`preference` with `preference_loss in {dpo, ipo}`); transformers
backend only.
### Multi-objective preference loss (schema-only in v0.40.0)
```yaml
task: preference
training:
preference_loss_weights: {dpo: 0.7, bco: 0.3}
```
Schema validates 25 entries summing to 1. Live runtime weighted-loss
combination is wired in v0.40.1; v0.40.0 fails fast with an actionable
`NotImplementedError` if you actually try to train (same stub-then-live
pattern as v0.27.0 MII / v0.37.0 multipack / v0.38.0 quant menu /
v0.39.0 ReLoRA).
## GRPO Training (Reasoning)
Train reasoning models with Group Relative Policy Optimization (DeepSeek-R1 style):
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: grpo
data:
train: ./data/reasoning_train.jsonl
format: sharegpt
max_length: 4096
training:
epochs: 3
lr: 1e-5
grpo_beta: 0.1
num_generations: 4
reward_fn: accuracy # or 'format', or path to custom .py
lora:
r: 64
alpha: 16
quantization: 4bit
```
```bash
# Create a reasoning config
soup init --template reasoning
# Train
soup train --config soup.yaml
```
**Built-in reward functions:**
- `accuracy` — checks if the final answer matches expected (supports `####` and `\boxed{}` formats)
- `format` — checks for structured `<think>...</think>` reasoning blocks
**Custom reward functions** — point to a Python file:
```python
# my_reward.py
def reward_fn(completions, **kwargs):
"""Score each completion. Return list of floats."""
return [1.0 if "correct" in c[-1]["content"] else 0.0 for c in completions]
```
```yaml
training:
reward_fn: ./my_reward.py
```
### Verifiable Rewards (RLVR)
Use `reward_fn: verifiable` with a `verifiable_domain` for deterministic, math-checkable rewards — no judge model, no heuristics. Great for GRPO on math, code, or structured-output tasks.
```yaml
training:
reward_fn: verifiable
verifiable_domain: math # or: code, json_schema
num_generations: 4
```
Three built-in domains:
| Domain | What it checks |
|---|---|
| `math` | Extracts the final numeric answer (supports `####`, `\boxed{}`) and compares via `float()` equality — no `eval()` on user output |
| `code` | Executes generated Python with a 5s timeout, 512 MB RLIMIT on POSIX, `python -I -S`, socket patch, ephemeral cwd. Output capped at 10KB. Warning panel on first use |
| `json_schema` | Validates output against a JSON Schema provided per-example in the dataset |
> **Note:** `code` domain runs untrusted generations. Soup sandboxes aggressively but never trust it for production-grade isolation — run in a VM or container for public data.
## Tool-Calling Fine-Tuning
Train models to emit structured function calls (OpenAI-style `tool_calls` with JSON arguments).
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: sft
data:
train: ./data/tool_calls.jsonl
format: tool-calling
training:
epochs: 3
lr: 2e-5
quantization: 4bit
```
**Tool-calling data format:**
```json
{"messages": [
{"role": "user", "content": "What's the weather in Paris?"},
{"role": "assistant", "tool_calls": [
{"id": "c1", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}
]}
]}
```
Arguments are parsed as JSON only — never `eval()`. `soup eval custom` can score tool-call accuracy (function name + argument JSON equality).
```bash
soup init --template tool-calling
```
## PPO / Full RLHF Pipeline
Train models with the full RLHF pipeline: SFT warmup → Reward Model → PPO alignment.
```bash
# Create an RLHF config
soup init --template rlhf
```
**Step 1: SFT warmup** — fine-tune a base model on your data:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: sft
data:
train: ./data/train.jsonl
format: alpaca
output: ./output_sft
```
**Step 2: Train reward model** — learn preferences from human feedback:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: reward_model
data:
train: ./data/preferences.jsonl
format: dpo
output: ./output_rm
```
**Step 3: PPO alignment** — optimize the policy using the reward model:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: ppo
data:
train: ./data/prompts.jsonl
format: chatml
training:
reward_model: ./output_rm
ppo_epochs: 4
ppo_clip_ratio: 0.2
ppo_kl_penalty: 0.05
lora:
r: 64
alpha: 16
quantization: 4bit
output: ./output_ppo
```
PPO supports two reward sources:
- **Reward model** (`reward_model`): pre-trained reward model (from step 2)
- **Reward function** (`reward_fn`): callable function (same as GRPO — `accuracy`, `format`, or custom `.py`)
## KTO Training (Unpaired Preferences)
Train with unpaired preference data — no need for chosen+rejected pairs:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: kto
data:
train: ./data/kto_train.jsonl
format: kto
training:
epochs: 3
kto_beta: 0.1
lora:
r: 64
alpha: 16
quantization: 4bit
```
**KTO data format:**
```json
{"prompt": "What is 2+2?", "completion": "4", "label": true}
{"prompt": "What is 2+2?", "completion": "Fish", "label": false}
```
## ORPO Training (No Reference Model)
ORPO combines SFT and alignment in one step — no reference model needed:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: orpo
data:
train: ./data/preferences.jsonl
format: dpo
training:
epochs: 3
orpo_beta: 0.1
lora:
r: 64
alpha: 16
quantization: 4bit
```
## SimPO Training (Simple Preference)
SimPO uses length-normalized log probabilities as implicit rewards — reference-free:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: simpo
data:
train: ./data/preferences.jsonl
format: dpo
training:
epochs: 3
simpo_gamma: 0.5
cpo_alpha: 1.0
lora:
r: 64
alpha: 16
quantization: 4bit
```
## IPO Training (Regularized Preference)
IPO is a theoretically grounded DPO variant with stronger regularization:
```yaml
base: meta-llama/Llama-3.1-8B-Instruct
task: ipo
data:
train: ./data/preferences.jsonl
format: dpo
training:
epochs: 3
ipo_tau: 0.1
lora:
r: 64
alpha: 16
quantization: 4bit
```
## RAFT — Retrieval-Augmented Fine-Tuning
When you need a model to *cite* the document it's reading instead of hallucinating, RAFT (Stanford 2024) is the canonical recipe. Each training row carries a query, a golden document, a list of distractor documents, and the answer — the model learns to attend to the relevant doc while ignoring the noise.
```yaml
# soup.yaml
data:
train: ./data/raft.jsonl
format: raft
training:
citation_faithful: true # enable citation precision/recall scoring
citation_style: bracket # cite as [doc-1] inline
citation_recall_threshold: 0.8 # gate final save on recall >= 80%
```
```jsonl
# RAFT JSONL row shape
{"query": "When was Python released?", "golden_doc": "Python was released in 1991 by Guido van Rossum.", "distractor_docs": ["Ruby was released in 1995.", "Java was released in 1995."], "answer": "1991 [doc-1]"}
```
```bash
# Ready-made 8B Llama recipe
soup recipes show raft-llama3-8b
soup recipes use raft-llama3-8b
```
Citation scoring is exposed as a pure kernel for the eval gate:
```python
from soup_cli.utils.citation_faithful import score_citations
score = score_citations(
predicted="The answer is 1991 [doc-1].",
expected_ids=("doc-1",),
)
# CitationScore(precision=1.0, recall=1.0, f1=1.0, predicted_count=1, expected_count=1)
```
Citation-faithful FT is gated to `task in {sft, pretrain}` + `data.format='raft'` — misconfigured runs fail at config load with a named-field message.
Under the hood, a `format: raft` run trains **answer-only**: each row is composed into a prompt (golden + distractor docs, shuffled deterministically by `data.raft_shuffle_seed`, each labelled `[doc-N]`) followed by the answer; the prompt span is masked out of the loss and — when `citation_faithful: true` — the bracketed `[doc-id]` spans in the answer get a boosted per-token loss weight. Rows whose prompt fills `max_length` (answer fully truncated) are dropped with a warning rather than silently shrinking the dataset.
By default the document order is fixed for the whole run. Set `data.raft_epoch_shuffle: true` to **re-permute** the golden + distractor documents *each epoch* (a per-epoch salt folded into the shuffle seed) so the model can't latch onto a fixed citation slot — useful for multi-epoch runs. `epoch=0` reproduces the legacy single-permutation order exactly, so enabling it never changes the first epoch. (v0.71.17)
Score a trained model's citations from the CLI:
```bash
# {predicted, expected_ids} rows, OR RAFT rows scored against their own golden [doc-N]
soup eval citation preds.jsonl --style bracket
# RAFT rows: pass the train-time shuffle seed so the golden id lines up
soup eval citation raft.jsonl --shuffle-seed 0 --output citation.json
```
`soup diagnose` also gains a `citation` failure mode that flags a model that stopped citing the supporting document.
## RA-DIT — Retrieval-Augmented Dual Instruction Tuning
RA-DIT (Meta 2023) is the two-stage version of RAFT: first train a sentence-transformer retriever (contrastive), then fine-tune the generator on the RAFT-style rows. Two recipes ship paired:
```bash
# Stage 1 — train the retriever (uses Soup's v0.16 embedding trainer)
soup recipes use ra-dit-retriever
soup train
# Stage 2 — train the generator on RAFT data, pointing at the retriever
soup recipes use ra-dit-llama3-8b
soup train
```
The schema enforces stage-task pairing — `ra_dit_stage: retriever` requires `task: embedding`; `ra_dit_stage: generator` requires `task: sft`. A misconfigured recipe fails at config load with a named-field message.
Run both stages in one command with `soup ra-dit`:
```bash
soup ra-dit --retriever-config retriever.yaml --generator-config generator.yaml
# preview the plan + the resolved retriever link without training:
soup ra-dit -r retriever.yaml -g generator.yaml --plan-only
```
It trains the retriever, then **records** that trained retriever as the generator's paired retriever (writing its output dir into the generator's `training.ra_dit_retriever_model`) and trains the generator RAFT-style. The recorded retriever is the one used at deploy/serve time — stage-2 does not fuse the retriever weights. A plain `soup train` of a generator-stage config with no retriever model set **auto-links** the most-recent RA-DIT retriever run from the Registry; pass `--retriever-model <m>` to override.
## Curriculum-Aware Training (BETA)
Layer dynamic re-weighting on top of the static `curriculum` bucketer. Every N steps the trainer aggregates per-sample loss + grad-norm into a per-bucket uncertainty signal, runs it through a softmax (temperature-controlled) with floor (water-filling so no bucket drops below `curriculum_dynamic_floor`), and re-weights the sampler. Empty buckets fall back to the median of populated buckets; degenerate inputs return uniform.
```yaml
training:
curriculum: true # static bucketer (v0.23.0)
curriculum_buckets: 4
curriculum_metric: perplexity # length (default) | loss | perplexity
curriculum_dynamic: true # NEW — dynamic re-weighting
curriculum_dynamic_recompute_steps: 50 # refresh every 50 global steps
curriculum_dynamic_floor: 0.05 # min weight per bucket
curriculum_dynamic_temperature: 1.0 # softmax temp on uncertainty
```
**Bucketing by difficulty percentile (v0.71.5).** When `curriculum_metric` is `loss` or `perplexity`, the dynamic callback assigns each step's sample to a bucket by its *rank* within a rolling 512-step window of the difficulty signal (perplexity = `exp(min(loss, 50))`), instead of the round-robin fallback used for `length`. This keeps the buckets calibrated to the live loss distribution rather than a static length sort. `length` (the default) keeps the round-robin assignment.
Visualise the recorded bucket-weight evolution with `soup runs curriculum-curve <run_id>`.
DDP / grad-accum safety: multi-rank launches must wire an `all_reduce` hook on per-bucket stats (a cross-validator rejects un-coordinated multi-rank runs upfront). Multi-trainer expansion beyond `sft` / `pretrain` is tracked for v0.48.1.
## TTS Fine-Tuning (`task='tts'`, BETA, live in v0.71.20)
Live as of v0.71.20 (lifted from the v0.52.0 schema stub). The five families
(`orpheus`, `sesame_csm`, `llasa`, `spark`, `oute`) are all decoder language
models, so a TTS fine-tune is **next-token cross-entropy over interleaved
`[text][audio-codec-token]` chat sequences** — the same objective the SFT
trainer already runs. `TTSTrainerWrapper` reuses the SFT model/tokenizer/LoRA/CE
machinery and adds two TTS-specific pieces: per-family emotion-control
templating and registration of operator-supplied codec special tokens.
There are two workflows:
**Pre-encoded chat (live, validated).** Run the family's audio codec **offline**
so the assistant turn already contains the discrete codec-token string, then
train with `data.format: chat`. This is plain cross-entropy and runs on any GPU
(validated end-to-end on SmolLM2-135M-Instruct).
```yaml
base: HuggingFaceTB/SmolLM2-135M-Instruct # or canopylabs/orpheus-3b-0.1-ft
task: tts
modality: audio_out
data:
train: ./data/tts_pre_encoded.jsonl # assistant turns carry codec tokens
format: chat
new_special_tokens: ["<|codec_0|>", "<|codec_1|>"] # your codec vocab
training:
tts_family: orpheus
tts_emotion: neutral # Orpheus + Oute only
lora: true
```
Operator-supplied `data.new_special_tokens` are registered (deduplicated, only
tokens not already in the vocab) and the embedding matrix is resized through the
(possibly PEFT-wrapped) model so the codec-token ids have rows. Orpheus + Oute
support emotion conditioning via `training.tts_emotion` from a per-family
allowlist (Orpheus: neutral / happy / sad / angry / excited / calm / whisper /
laugh; Oute: neutral / happy / sad / angry / calm / excited) — the wrapper
prepends the family's emotion control string to the first user turn.
**Live-codec (hardware/dependency-gated).** Setting `data.format: audio` asks
the trainer to encode raw audio into codec tokens **at train time**, which needs
the family's heavyweight codec package (`snac` for Orpheus, `moshi` for
Sesame-CSM, `xcodec2` for Llasa, `sparktts` for Spark, `outetts` for Oute). This
path surfaces a friendly per-family `RuntimeError` naming the required `pip
install` and is not validated on the maintainer's box — use the pre-encoded
workflow above for a runnable fine-tune.
Five ready-made recipes ship: `orpheus-tts-sft`, `sesame-csm-tts`, `llasa-tts`,
`spark-tts`, `oute-tts` — copy with `soup recipes use <name>`. Cross-validators
reject the `mlx` backend, `modality != audio_out`, and emotion tags outside the
per-family allowlist.
## Classifier / Reranker / Cross-Encoder Training (BETA, v0.52.0)
Three new task types build on the existing embedding trainer: `task: classifier` (single-label or multi-label sequence classification), `task: reranker` (pointwise retrieval scoring), `task: cross_encoder` (paired-input scoring). Schema-only; live trainer wrapper in v0.52.1.
```yaml
base: BAAI/bge-base-en-v1.5
task: classifier
data:
train: ./data/classification.jsonl
training:
num_labels: 3
classifier_kind: single_label
label_names: [negative, neutral, positive]
```
`num_labels` is bounded `[1, 1024]` with explicit bool-before-int rejection; `label_names` (optional) must be unique, ≤128 chars each, and match `num_labels` in length when set.
## Knowledge Distillation (BETA, v0.52.0)
New `task: distill` with `training.teacher_model` (HF id or local path), `training.distill_divergence` (`kl` / `forward_kl` / `reverse_kl` / `js``kl` canonicalises to `forward_kl`), and `training.distill_temperature` (bounded `[0.05, 100.0]`, finite-only). Schema-only; live loop in v0.52.1.
```yaml
base: meta-llama/Llama-3.2-1B
task: distill
data:
train: ./data/distill.jsonl
training:
teacher_model: meta-llama/Llama-3.1-8B
distill_divergence: forward_kl
distill_temperature: 2.0
```
The cross-validator rejects `task='distill'` without `teacher_model`, and rejects `teacher_model` / `distill_*` fields when `task` is anything other than `distill`.
## EBFT + GDPO (BETA, v0.52.0)
Energy-Based Fine-Tuning (axolotl) lands as `training.ebft_variant ∈ {structured, strided}` + `training.ebft_temperature` (bounded `[1e-4, 100.0]`). Gated to `task: sft`. Generalized DPO lands as `training.gdpo_variant ∈ {standard, length_normalized, margin}` — gated to `task ∈ {dpo, preference}`. Live loss kernels in v0.52.1.
## gpt-oss `reasoning_effort` + `train_on_eot` (v0.52.0)
`training.reasoning_effort: low | medium | high` injects a system-prefix token at training time for gpt-oss models; `training.train_on_eot: true` includes explicit EOT/EOS control tokens in the SFT loss (axolotl `train_on_eot`). Both are gated to the SFT-family task set (`sft` / `pretrain` / `distill` / `classifier` / `reranker` / `cross_encoder`) — setting them on DPO / GRPO / PPO / etc. fails at config load. Live formatter wiring in v0.52.1.
## MoLE — Per-Token Adapter Routing (`task='moe_lora_routing'`)
Train a small **gating network** that routes each token to a weighted blend of N frozen task
LoRAs (Mixture of LoRA Experts, Wu et al. 2024). The base model and every task adapter stay
frozen — only the router learns which adapter(s) each token should use.
```yaml
base: HuggingFaceTB/SmolLM2-135M
task: moe_lora_routing
modality: text
backend: transformers
data:
train: ./data/chat.jsonl
max_length: 512
training:
mole_task_adapters: # 2-64 LoRA adapter paths (HF ids or local dirs)
- ./adapters/math
- ./adapters/code
- ./adapters/chat
mole_top_k: 2 # 1 <= top_k <= len(mole_task_adapters)
mole_temperature: 1.0 # [1e-6, 100.0]
epochs: 1
```
The gate is the only trainable parameter; it is saved as `mole_gate.pt` alongside the run.
`compute_loss` runs N+1 forwards per step (base + each adapter under `torch.no_grad()`, blended
by the per-token gate weights) so step time scales with the number of task adapters. Training
only — there is no serve-time MoLE path yet. (v0.71.12)
## Architecture Knobs — Mixture-of-Depths, LLaMA Pro, LongLoRA
Three architecture transforms that were schema-only are now live for SFT / Pretrain on
Llama / Qwen / Mistral (LongLoRA also covers Phi). All apply at trainer setup:
```yaml
training:
# Mixture-of-Depths (arXiv 2404.02258): route only the top-k tokens through each
# block. capacity_factor is the fraction of tokens that get the residual update.
use_mod: true
mod_capacity_factor: 0.125
# LLaMA Pro: append zero-initialised identity decoder blocks and train only the new
# ones (freeze_trainable_layers freezes the originals).
expand_layers: 4
freeze_trainable_layers: 4
# LongLoRA S²: shifted-sparse attention on the Q/K projections for long-context tuning.
use_longlora: true
```
`use_mod` / `expand_layers` attach AFTER `get_peft_model` so the new routers / blocks are
trainable. Unsupported architectures warn + skip (MoD, block expansion); `use_longlora` is
rejected at the schema gate for non-supported arches and for `use_ring_attention` / FlashAttention-3.
Pick one of MoD / LLaMA Pro / LongLoRA per run. (v0.71.12)