SFT datasets, chat templates, loss masking, sample packing, Axolotl / TRL / Unsloth / torchtune — everything between a base model and a well-behaved assistant.
Supervised fine-tuning (SFT) updates a pre-trained base model's weights on a labelled dataset of (input, desired-output) pairs. It is the first training stage that turns a next-token predictor into something that behaves like an assistant. Understanding when to fine-tune versus reaching for a cheaper tool is the first decision in any LLM deployment.
If a 2 000-token system prompt plus 5 few-shot examples achieves 90 % of the target quality, ship the prompt. Fine-tune when you need the remaining 10 %, or when the prompt would need to be embedded in millions of calls per day and cost becomes prohibitive.
Dataset quality dominates SFT outcomes far more than model size or training duration. The field has converged on two structural formats.
| Format | Shape | Canonical examples | Typical size (deduplicated) |
|---|---|---|---|
| Alpaca / instruction | {"instruction":..., "input":..., "output":...} | Alpaca (52 k), Dolly-15k, OpenHermes-2.5, SlimOrca | 10 k–1 M rows |
| ShareGPT / conversation | {"conversations":[{"from":"human","value":...},{"from":"gpt","value":...}]} | ShareGPT90k, WizardLM-Evol, Capybara | 50 k–500 k turns |
| OpenAI messages | {"messages":[{"role":"system",...},{"role":"user",...},{"role":"assistant",...}]} | Tulu-3-SFT, Orca-3 | 300 k–1 M |
Most frameworks accept multiple formats and convert internally. Axolotl's datasets: block takes a type: key (alpaca, sharegpt, completion, context_qa, …) and handles the mapping. TRL's DataCollatorForCompletionOnlyLM works on the tokenised stream after template application.
Deduplication: MinHash + LSH at character n-gram level — Slimpajama found ~30 % overlap in raw C4. Length filtering: discard <10 tokens (label noise) and >8 192 tokens unless you intend long-context training. Language filtering: fastText lid.176 at threshold 0.65. PII scrubbing: regex + NER for emails, phone numbers, SSNs — especially if dataset comes from web crawls. Perplexity-based quality: score with a small KenLM or GPT-2 model; discard top-5 % outliers.
A chat template is the byte-level contract that maps structured conversation turns into a flat token sequence the model was trained on. Using the wrong template is silent — training completes, but quality collapses at inference because the model sees token patterns it was never optimised against.
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
What is gradient checkpointing?<|im_end|>
<|im_start|>assistant
Gradient checkpointing trades compute for memory ...<|im_end|>
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are a helpful assistant.<|eot_id|>
<|start_header_id|>user<|end_header_id|>
What is gradient checkpointing?<|eot_id|>
<|start_header_id|>assistant<|end_header_id|>
Gradient checkpointing trades compute for memory ...<|eot_id|>
<|im_start|>system\n
You are Qwen, created by Alibaba Cloud.<|im_end|>\n
<|im_start|>user\n
What is gradient checkpointing?<|im_end|>\n
<|im_start|>assistant\n
Gradient checkpointing trades compute for memory ...<|im_end|>\n
From transformers≥4.34 every tokeniser ships a Jinja2 template in tokenizer_config.json under the chat_template key. Call tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) and you get the correctly formatted string. Never hand-format — template bugs are the single largest source of SFT regression.
In a standard causal-LM training loop the cross-entropy loss is computed over every token in the sequence. For SFT you almost always want to mask out the prompt tokens so that the model only learns to predict assistant responses. Computing loss on human turns is wasteful at best and actively harmful (the model learns to generate user-side content) at worst.
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM
# ChatML assistant header tokens
response_template = "<|im_start|>assistant\n"
collator = DataCollatorForCompletionOnlyLM(
response_template=response_template,
tokenizer=tokenizer,
mlm=False,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=collator, # ← this is all you need
)
In Axolotl's YAML config, set train_on_inputs: false. Axolotl detects the template automatically from the model's tokeniser config and masks prompt tokens before packing. Setting this to true trains on all tokens — useful only for completion-style datasets with no user/assistant role distinction.
Naively, each batch entry is one conversation padded to the maximum sequence length. On typical instruction datasets where most examples are 256–512 tokens, padding a 2 048-token context wastes 75–87 % of the compute budget on <pad> tokens that produce zero gradient signal.
Sample packing (also called bin packing) concatenates multiple short samples end-to-end within a single sequence until the context length is filled, with attention masking to prevent cross-contamination between packed items.
Without correct masking, token N in example 2 attends to tokens from example 1, causing cross-contamination. The fix is a block-diagonal attention mask: each packed sub-sequence attends only to itself. Flash Attention 2 (flash_attn≥2.1) supports variable-length sequences natively via cu_seqlens — this is the most compute-efficient path.
| Config key | Axolotl | TRL SFTTrainer | torchtune |
|---|---|---|---|
| Enable packing | sample_packing: true | packing=True | packed: true in dataset config |
| Cross-attn prevention | auto via Flash Attn 2 | manual DataCollatorForSeq2Seq | built-in cu_seqlens path |
| Typical throughput gain | 2–4× on short datasets | 2–3× | 2–4× |
If your dataset has very long examples (>3 000 tokens on average) packing offers diminishing returns and the bookkeeping overhead can cost more than the padding it saves. Evaluate effective tokens-per-second with and without packing on your actual dataset before committing.
Four frameworks dominate open-source SFT. Each makes different tradeoffs between configurability, speed, and ease of use.
Config-file driven. One config.yaml specifies everything: base model, dataset type, template, LoRA rank, scheduler, sample packing, Flash Attn, DeepSpeed/FSDP. Preferred for production pipelines and multi-dataset merging. Supports deepspeed_zero3, gradient checkpointing, 4-bit QLoRA out of the box. axolotl train config.yaml → HF Hub push.
Python-API driven. Extends transformers.Trainer. Integrates with peft for LoRA, bitsandbytes for QLoRA, and the full HF ecosystem. SFTTrainer + DataCollatorForCompletionOnlyLM covers 90 % of SFT tasks. TRL ≥0.10 added GRPOTrainer, DPOTrainer, PPOTrainer — all in one package.
Speed-optimised. Hand-written Triton kernels for attention, RoPE, and cross-entropy. Claims 2× throughput vs native HF on single-GPU consumer hardware (A100 40 GB, 3090, 4090). Drop-in replacement: from unsloth import FastLanguageModel. Supports Llama 3.x, Mistral, Gemma, Phi-3, Qwen-2.5. Free tier covers most consumer GPUs; Pro tier for multi-GPU.
Framework-native. Pure PyTorch, no HF Trainer. Ships ready-made recipes: full_finetune_single_device, lora_finetune_distributed. Config via YAML. Designed for reproducibility and easy recipe modification. Excellent for research baselines where you want full control over the training loop. Added Llama-3.1 405B support and QAT (Quantisation-Aware Training) in torchtune 0.4.
For quick experiments on a single GPU: Unsloth (fastest iteration). For production pipelines with complex dataset mixes: Axolotl. For HF-ecosystem integration (PEFT, evaluate, trl reward training): TRL. For research with custom training loops: torchtune.
Training loss falling does not mean the model is getting better at the task you care about. A comprehensive evaluation strategy triangulates across three axes: intrinsic metrics, downstream benchmarks, and human / model preference ratings.
| Metric | What it measures | Tooling | Caveats |
|---|---|---|---|
| Eval loss / PPL | Generalisation to held-out splits of the training distribution | HF Trainer eval_dataset | Only valid if eval set is drawn from same distribution; doesn't detect mode collapse |
| MT-Bench | Multi-turn instruction following, scored by GPT-4 on 8 categories | fastchat eval harness | LLM-as-judge bias; GPT-4 scores can drift between API versions |
| AlpacaEval 2.0 | Length-controlled win rate vs GPT-4 Turbo on 805 prompts | alpaca_eval library | Measures verbosity preference; reward-hacked by longer outputs |
| IFEval | Strict instruction-following on programmatically verifiable constraints (word count, JSON, format) | lm-evaluation-harness | Covers only verifiable format constraints, not semantic quality |
| MMLU / BBH | Knowledge retention after fine-tuning | lm-evaluation-harness | Watch for catastrophic forgetting of base capabilities |
SFT on a narrow domain dataset commonly degrades performance on unrelated benchmarks. Run MMLU and BBH before and after every SFT run. If accuracy drops >2 % absolute, consider reducing learning rate, shortening training, or mixing in general-domain data. The typical culprit is a too-high learning rate (3e-5 for 7B models; 1e-5 for 70B+ models) or too many epochs on a small dataset.
VRAM is the hard constraint. Before writing a single config line, compute the memory budget:
# Per-parameter costs for bf16 + full-precision optimiser state (AdamW)
# 2 bytes (bf16 param) + 4 bytes (fp32 copy) + 8 bytes (Adam m + v) = 14 bytes/param
# Activations = batch_size * seq_len * hidden_dim * num_layers * ~2 bytes
params_7b = 7e9
base_vram = params_7b * 14 # ≈ 98 GB — needs 2×A100 80 GB minimum
# With gradient checkpointing (store only layer boundaries):
# activation VRAM drops ~8x, adds ~30% compute overhead
# QLoRA (4-bit base + bf16 LoRA adapters):
qlora_vram = params_7b * 0.5 + 0.01 * params_7b * 14 # ≈ 4.5 GB + 1 GB adapters
| Model size | Full SFT bf16 | QLoRA 4-bit | Practical GPU |
|---|---|---|---|
| 7B | ~98 GB | ~10 GB | 1× A100 40 GB (QLoRA) or 1× 3090/4090 (QLoRA) |
| 13B | ~182 GB | ~16 GB | 1× A100 40 GB (QLoRA) or 2× A100 (full) |
| 70B | ~980 GB | ~48 GB | 1× A100 80 GB (QLoRA) or 4× H100 (full) |
| 405B | ~5.7 TB | ~200 GB | 8× H100 minimum (full); 4× H100 (QLoRA + pipeline) |
FSDP (FullyShardedDataParallel): shards parameters, gradients, and optimiser states across GPUs — linear VRAM scaling, requires fast NVLink. DeepSpeed ZeRO-3: similar sharding but with optional CPU offload for optimiser states. Tensor parallelism: splits individual layers across GPUs — needed for 70B+ full SFT. Axolotl supports all three via config; torchtune ships FSDP2 recipes natively.
tokenizer.apply_chat_template() from transformers≥4.34. Never hand-format; template bugs are silent and devastating.train_on_inputs: false (Axolotl) or use DataCollatorForCompletionOnlyLM (TRL). Loss on user turns wastes compute and teaches the wrong distribution.Deck 02 covers LoRA and its variants — the adapter techniques that make SFT practical on consumer hardware without touching the base weights. Deck 03 shows what SFT alone can't fix and why RLHF and PPO were invented.