# 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 --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": "\nDescribe this image."}, {"from": "gpt", "value": "A cat on a mat."}]} ``` **ShareGPT4V:** ```json {"image": "chart.png", "conversations": [{"from": "human", "value": "\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 2–5 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 `...` 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 ` 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 `. 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 `. 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)