Backends & Performance
Soup has three training backends: transformers (default), unsloth (2-5x faster on CUDA), and mlx (native Apple Silicon, new in v0.25.0).
Apple Silicon MLX Backend (v0.25.0)
Train on M1–M4 Macs without CUDA or emulation:
pip install "soup-cli[mlx]"base: mlx-community/Llama-3.1-8B-Instruct-4bit
backend: mlx
task: sft # sft ONLY: the MLX backend refuses every other taskSee the MLX backend guide for supported tasks, diagnostics, and MLX-native recipes.
Unsloth Backend (2-5x Faster Training)
Use the Unsloth backend for significantly faster training and up to 80% less VRAM:
pip install "soup-cli[fast]"Add one line to your config:
base: meta-llama/Llama-3.1-8B-Instruct
task: sft
backend: unsloth # 2-5x faster, -80% VRAM
data:
train: ./data/train.jsonl
format: alpaca
training:
epochs: 3
lr: 2e-5
quantization: 4bit
lora:
r: 64
alpha: 16Works with all training tasks: SFT, DPO, GRPO, PPO, KTO, ORPO, SimPO, IPO, and Pretrain.
Tip: Soup auto-detects unsloth. When installed, you'll see a hint during
soup trainif you haven't enabled it yet.
Performance Optimizations
training:
use_liger: true # Liger Kernel fused ops (measured once: 12.9% less VRAM, 1.051x throughput)
use_flash_attn: true # FlashAttention v2/v3 auto-detection
gradient_checkpointing: true # Required for long sequencesBoth keys are read by the SFT trainer only. The Liger figure is a single measurement from v0.73.0 and replaced an earlier "20-60% memory savings" claim that upstream corrected in the same release, so treat it as a data point on one setup, not a promise.
use_flash_attn needs a CUDA device and asks transformers which implementation is installed: FlashAttention 3 if it reports one (that is the separate flash_attn_3 package, not flash-attn), otherwise FlashAttention 2, otherwise nothing. Before v0.73.0 the FlashAttention 3 branch tested flash_attn.__version__ for a 3.x number, which no real install reports, so on Hopper hardware the setting silently fell back to FlashAttention 2 or SDPA while the docs advertised 3. v0.73.0 fixed the detection (#334). Upstream is explicit that this makes detection honest and does not make FlashAttention 3 measurable on its test hardware, so this site publishes no FlashAttention 3 speed or memory figure.
Install optional packages:
pip install "soup-cli[liger]" # Liger Kernel
pip install flash-attn --no-build-isolation # FlashAttention
pip install "soup-cli[ring-attn]" # Ring FlashAttentionLong-Context Training (128k+)
training:
rope_scaling_type: yarn # linear, dynamic, yarn, longrope, llama3
gradient_checkpointing: true
data:
max_length: 131072 # Up to 1M tokens supportedOnly the SFT trainer reads rope_scaling_type, and only on its plain text transformers path (not with stream_layers, vision, audio or the Unsloth backend, none of which call it), and only when data.max_length is larger than the model's native context: the scaling factor is that ratio, worked out for you. The trainer writes it into model.config after the model has been loaded and prints Long-context enabled. Rotary embeddings are built when the model is constructed, and in a check on a small randomly initialised Llama (transformers 5.17; yarn, linear and dynamic) that later edit changed neither the rotary frequencies nor the outputs. That check was not run on a real checkpoint, so do not rely on this key to extend a model's context at v0.75.2. yarn_factor, yarn_attn_factor, yarn_beta_fast and yarn_beta_slow are accepted by the schema when rope_scaling_type is yarn, but nothing in v0.75.2 reads them, so setting them does not change the run.
use_ring_attention is in the same state. It is accepted and checked before training (CUDA, at least two GPUs, ring-flash-attn installed, max_length of 8192 or more), and nothing in v0.75.2 applies Ring FlashAttention to the model afterwards, so sequence parallelism does not happen.
Multi-GPU Training
DeepSpeed
soup train --config soup.yaml --deepspeed zero2 # ZeRO Stage 2
soup train --config soup.yaml --deepspeed zero3 # ZeRO Stage 3
soup train --config soup.yaml --deepspeed zero2_offload # Stage 2 + optimizer offload
soup train --config soup.yaml --deepspeed zero3_offload # Stage 3 + CPU parameter offload (v0.73.0)FSDP2 (PyTorch Native)
soup train --config soup.yaml --fsdp full_shard # Like ZeRO-3
soup train --config soup.yaml --fsdp shard_grad # Like ZeRO-2
soup train --config soup.yaml --fsdp full_offload # With CPU offloadNo GPU at all: train on a serverless cloud one
The rest of this site is about making a small card work. If you have no card, --cloud modal renders a self-contained Modal app from your soup.yaml and runs it on a per-second-billed GPU.
pip install "soup-cli[modal]" # only needed to submit live
# Plan-only, the DEFAULT: write the stub and print the modal run command.
soup train --config soup.yaml --cloud modal --gpu a100
# Submit live (authenticate once with `modal setup`, or set
# MODAL_TOKEN_ID and MODAL_TOKEN_SECRET).
soup train --config soup.yaml --cloud modal --gpu a100 --cloud-submit--gpu accepts t4, l4, a10g, a100 (the default), a100-80gb, l40s and h100.
Plan-only is the default and that is deliberate: nothing is submitted, and nothing is billed, until you add --cloud-submit. You get the generated soup_modal_app.py and the exact command, so you can read what would run before it does.
Your config is base64-embedded as data, not interpolated as code, so nothing in the YAML can execute in the generated stub and no secret is written into it. The image pins soup-cli[train] to the version you are running locally, writes the embedded config inside the container, and runs soup train on the chosen GPU.
Where the checkpoints end up is the part that was missing until v0.75.1, and the answer used to be nowhere. The container is ephemeral, so when the run finished it took the outputs with it, while the entrypoint printed a line telling you to download checkpoints from a directory that had none. Outputs now go to a Modal volume named soup-outputs, created on first use, in a directory named after the run, committed in a finally so a crashed run keeps what it produced, and downloaded into your local output directory at the end including after a failure. If that download is the thing that fails, the command prints the modal volume get line to retry it by hand.
Both cloud paths now refuse an output that leaves the working directory, at plan time. An absolute path, or one reaching upward with .., is rejected before anything is provisioned rather than after. That matters most for the case it was written for: a soup.yaml shared between people, where the output path was written on somebody else's machine.
Lambda Cloud (v0.74.0)
soup train --config soup.yaml --cloud lambda--gpu takes one of exactly four Lambda instance types: a10, a6000, a100 and h100. That list is shorter than Modal's, so a value that works there (t4, l4, a10g, l40s) is refused here. You need LAMBDA_API_KEY, LAMBDA_SSH_KEY_NAME and LAMBDA_SSH_PRIVATE_KEY in the environment, with the public key already registered in your Lambda account; LAMBDA_REGION is optional and defaults to us-tx-1. Output paths must be relative. Keep the local controller running until it reports termination: shutting down the guest does not stop the billing, the controller does.
Plan-only by default, like Modal. The design decision worth knowing is where the credential lives: the API key never enters the instance. Termination belongs to a local controller, and its finally both terminates the instance and polls to confirm that the termination actually happened, rather than firing the call and assuming.
That guarantee had a hole until v0.75.1, and it cost money rather than data. Pressing Ctrl+C during
--cloud-submitkilled the controller a quarter of a second later, while it was still inside thefinallythat terminates the paid instance, so the instance could keep running and keep billing. Two narrower windows survived the first fix and were closed after it: a signal arriving between the process being spawned and the guarded region being entered, and one landing on the "still waiting" notice, which is written from the interrupt handler itself.soupnow waits for the controller across the interrupt. Check your Lambda console for a still-running instance if you ever interrupted a submit on an earlier version. Two scopes upstream states rather than implies: the fix is Linux and macOS, because the POSIX signal test needs a real process group and is skipped on Windows, and the one way left to skip the cleanup is to kill the controller process itself from another terminal, which can still leave the instance running. Upstream is explicit that the Lambda submission path still needs a paid live-validation pass before it can be described as provider-validated; plan-only rendering and the lifecycle boundaries are covered by offline tests.
--cloud takes modal or lambda, and nothing else. --cloud runpod is a recognised name with no implementation behind it: it prints RunPod cloud training is not yet live; use --cloud modal or lambda and exits 2, and any other value is refused as an invalid --cloud, also with exit 2.
Quantization-Aware Training (QAT)
Train with simulated quantization for better post-quantization quality:
pip install "soup-cli[qat]"training:
quantization: 4bit
quantization_aware: true # Enable QATQAT is ~5-10% slower training but produces significantly better quality when deploying with aggressive quantization. Not compatible with the unsloth backend.
Advanced LoRA Variants
DoRA (Weight-Decomposed LoRA)
training:
lora:
r: 64
alpha: 16
use_dora: trueLoRA+ (Differentiated Learning Rates)
training:
lr: 2e-5
loraplus_lr_ratio: 16.0 # lr_B = lr x 16
lora:
r: 64
alpha: 16GaLore (Memory-Efficient Full-Parameter Training)
training:
quantization: none # Required: incompatible with quantization
use_galore: true
galore_rank: 128
galore_update_proj_gap: 200
galore_scale: 0.25Note: GaLore requires
quantization: noneandbackend: transformers(not unsloth).
NEFTune (Noisy Embeddings)
Add noise to embedding vectors during training for better generalization:
training:
neftune_alpha: 5.0 # Noise magnitude (0.0–50.0, typical: 5–15)NEFTune has been shown to improve instruction-following quality without extra data or compute. It is read by the SFT, DPO, KTO, ORPO, SimPO, IPO and BCO trainers; on any other task the key is accepted and ignored.
rsLoRA (Rank-Stabilized LoRA)
Scale LoRA outputs by 1/sqrt(r) instead of 1/r for better training at high ranks:
training:
lora:
r: 128 # Higher ranks benefit most from rsLoRA
alpha: 64
use_rslora: trueTip: Combine NEFTune + rsLoRA + Unsloth backend for the best training quality and speed.
MoE Model Support
Fine-tune Mixture of Experts models (Mixtral, Qwen3-30B-A3B, DeepSeek V3):
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: 4bitSoup auto-detects MoE architectures. moe_lora and moe_aux_loss_coeff are read by the SFT and Pretrain trainers (and moe_lora also by the shared layer-streaming setup); on any other task they are accepted and ignored.
Adapting routed expert tensors directly (v0.74.0)
moe_lora targets expert and attention modules. Some architectures keep their experts as raw 2-D or 3-D nn.Parameter tensors instead, which a module-level target cannot reach, so lora.target_parameters adapts those tensors through PEFT:
training:
lora:
r: 16
alpha: 32
dropout: 0 # required: PEFT cannot wrap a raw parameter with dropout
target_modules: auto
target_parameters: auto # routed gate_up_proj + down_proj tensors
rank_pattern:
experts.gate_up_proj: 2
experts.down_proj: 2auto resolves the architecture's registered parameter mapping, currently Qwen4-Exp routed experts, and fails closed when an architecture has none; an explicit list of parameter-name suffixes is also accepted, capped at 256 entries. The scope is narrow and every bound is refused when the config is read, not minutes into a run: task: sft or pretrain, backend: transformers, modality: text, plain LoRA or rsLoRA with init_strategy: random and use_dora: false, and not with training.stream_layers: true, which stays resident-only for now.
Two operational notes upstream states rather than leaves to be discovered: torch.compile may recompile or graph-break around parameter wrappers, and a parameter-targeted MoE adapter materializes a contribution for every expert at inference, so merge it into the base for deployment unless you need to hot-swap adapters.
Curriculum Learning (v0.23.0+)
Sort the training rows by length, shortest first, before they reach the trainer (the note under the example says what the trainer then does with that order):
training:
curriculum: true
curriculum_metric: length # Sort by sequence length
curriculum_buckets: 5 # 1-20 difficulty bucketsThis sort is applied by the SFT trainer only, and it always orders rows by length, shortest first. curriculum_metric: perplexity or loss is accepted but prints a notice and falls back to the same length sort, and curriculum_buckets only appears in that printout. Only the curriculum_dynamic callback reads the other two metrics. The sort reorders the list handed to the trainer, but Soup does not change the trainer's default sampler, which is random (checked on transformers 5.17, the stack v0.75.2 installs), so the model does not see the rows in that order. Do not expect a short-to-long schedule from this key.
Freeze Training (v0.24.0+)
Freeze bottom layers to reduce compute while preserving base knowledge (the SFT trainer applies this; other tasks accept the keys and ignore them):
training:
freeze_layers: 16 # Freeze bottom 16 layers
# freeze_ratio: 0.5 # Or freeze 50% of layersLoss Watchdog (v0.24.0+)
Automatically stop training if loss spikes or diverges:
training:
loss_watchdog: true
loss_watchdog_threshold: 5.0 # Stop if loss exceeds threshold
loss_watchdog_patience: 3 # Wait N steps before stoppingThe watchdog is attached by the SFT trainer and by most of the others. task: prm, task: unlearn, task: moe_lora_routing, task: tts and the MLX backend accept these keys and never read them. Training stability lists the trainers that do.
Sample Packing (v0.23.0+)
Pack multiple short samples into one sequence for efficient training:
training:
packing: true # Enable sample packing (SFT and Pretrain)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.