From ct
Covers operational pitfalls in LLM fine-tuning: LoRA/QLoRA merge issues, preference optimization (DPO/ORPO/SimPO/GRPO), chat-template bugs, loss masking, sequence packing, eval contamination, and toolchain choices (TRL, axolotl, unsloth, torchtune).
How this skill is triggered — by the user, by Claude, or both
Slash command
/ct:fine-tuning-llmsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Concise operational pointers for deep post-training of open-weights LLMs.
Concise operational pointers for deep post-training of open-weights LLMs.
Assumes you already know what an LLM is, basic transformer training, and HuggingFace Trainer. This skill covers the operational layer — the parts models tend to gloss over: LoRA mechanics and merge pitfalls, chat-template silent killers, packing, contamination, the toolchain (TRL/PEFT/axolotl/unsloth/llama-factory/torchtune).
Load when the question is about:
SFTTrainer / DPOTrainer / GRPOTrainer in TRLr, lora_alpha, target_modules, lora_dropout for a new adapterDo NOT load for: pure inference / serving / vLLM tuning, prompt engineering of frontier closed models, RAG pipelines, embedding model training, tokenizer training, pretraining from scratch.
LoRA freezes base, learns B @ A of rank r, applied as delta = (alpha/r) * B A x. The scale alpha/r is the load-bearing knob — doubling r without doubling alpha halves the effective update.
lora_alpha = 2r. PEFT defaults r=8, alpha=8; community SFT default r=16, alpha=32; for larger behavioral shifts r=64, alpha=128. Past r=128 rarely beats full FT and erodes the parameter savings.target_modules: classic LoRA hits attention only — q_proj/k_proj/v_proj/o_proj. Modern recipes target all linear layers including gate_proj/up_proj/down_proj. PEFT shortcut: target_modules="all-linear". The QLoRA paper showed all-linear is required to hit full-FT parity.lora_dropout: 0.0 with ≥500 examples; 0.05 light reg; 0.1 only for tiny datasets / heavy overfit.bias="none" is standard; "lora_only" adds learnable bias on adapted layers.modules_to_save for embed_tokens / lm_head when training new tokens.Merge tradeoffs: model.merge_and_unload() collapses adapter into base for zero-overhead inference but is permanent. Keep separate when serving multiple LoRAs (one base, hot-swap adapters).
QLoRA merge footgun: training under QLoRA dequantizes nf4 → bf16 for the forward pass, so the adapter is fit to bf16 weights. Merging back into the nf4 4-bit checkpoint produces a precision mismatch and silent quality regression. Fix: dequantize the base to bf16, merge, then re-quantize with AWQ/GPTQ (not nf4).
BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16)
double_quant quantizes the per-block constants too, saving ~0.4 bits/param (~3 GB on a 65B).paged_adamw_8bit uses NVIDIA unified memory to spill optimizer state to host RAM — prevents OOM on long-sequence steps but costs PCIe bandwidth on spikes.Footgun: at long context, activation memory dominates — quantizing weights does nothing for it. Use gradient checkpointing (use_reentrant=False), FlashAttention 2/3, and sequence packing instead. bitsandbytes ≥ 0.43 supports FSDP + 4-bit; older versions force DDP only. Compute dtype must match the optimizer expectation — mixing fp16 compute with paged_adamw_32bit yields silent NaNs on Llama 3 (use bf16 throughout).
Preference-tuning method choice (DPO/IPO/KTO/ORPO/SimPO/GRPO) is fast-moving — consult current papers; the LoRA mechanics and chat-template content below applies regardless of objective.
tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) is the only safe way to render training and inference inputs. ChatML (<|im_start|>role\n...<|im_end|>) for Mistral/Qwen lineage; Llama 3 uses <|begin_of_text|><|start_header_id|>role<|end_header_id|>\n\n...<|eot_id|>. Each tokenizer ships its own Jinja in tokenizer.chat_template.
Footguns:
apply_chat_template always added the generation prompt regardless of add_generation_prompt=False; fixed upstream but stale in some unsloth bnb-4bit mirrors.return_assistant_tokens_mask=True is buggy on tiktoken-based Llama 3 tokenizers — affects completion-only masking.Completion-only: set prompt token labels to -100 so CrossEntropy ignores them. TRL SFTTrainer does this automatically when completion_only_loss=True on prompt-completion datasets, or via DataCollatorForCompletionOnlyLM(response_template=...). Training on the full sequence (prompt included) hurts instruction-following — gradient spent echoing inputs the model already perfectly conditions on.
Sequence packing: concatenate examples up to max_seq_length to remove padding waste (typically 30–50% of tokens). Naive packing is wrong — attention bleeds across examples. Solution: FlashAttention 2's variable-length API (flash_attn_varlen_func) takes cu_seqlens (cumulative lengths) and applies a block-diagonal mask in O(Σ s_i²) instead of O((Σ s_i)²). TRL SFTConfig(packing=True, packing_strategy="ffd") and unsloth's "uncontaminated packing" both wire this correctly. Insert EOS between concatenated examples or the model learns documents flow into each other.
GPT-3 set the n-gram overlap convention at 13-grams; standard since. n-gram is necessary but not sufficient — paraphrases, translations, and number swaps slip past. LLM-judge decontamination (Yang et al.) catches more. Removing contaminated GSM8K examples drops some models' accuracy ~13pp.
The HF Open LLM Leaderboard had repeated contamination scandals (2023–24); v2 moved to MMLU-Pro, GPQA, MUSR, MATH-Lvl5, IFEval, BBH precisely because v1 saturated and leaked. Treat MMLU/GSM8K/HumanEval scores from any 2024+ release as unreliable absolute — useful only as relative deltas on your own held-out splits.
For new evals: GPQA-Diamond, AIME, LiveCodeBench (rotating), SWE-bench Verified, Arena-Hard-v0.1.
Catastrophic forgetting signs: MMLU drops 3–10pp after task FT, refusal patterns shift, chat format degrades. Mitigations:
LR magnitudes for SFT (community defaults):
Schedules: cosine with 3–10% warmup is the safe default. Linear is fine for short FT. WSD (warmup-stable-decay) — constant LR + late decay — is now competitive; better when total step count isn't fixed up front. Decay-to-zero (D2Z) can save up to 60% pretraining compute vs cosine-10×.
Effective batch size = per_device_train_batch_size × gradient_accumulation_steps × DP world. Preference-tuning is batch-size sensitive — under-batched runs collapse to noise. With grad accum and a ref_model, ref-model forwards still happen per micro-batch — VRAM cost stays.
LLM-judge bias: AlpacaEval favors longer outputs — use AlpacaEval-LC (length-controlled) or Arena-Hard which stratifies. Position bias (first answer wins ~3–5%), self-enhancement (judge prefers own family), verbosity, formatting (markdown vs plain). Mitigations: pairwise with order-swap, hidden-CoT judge, multi-judge ensembles, length normalization in the prompt. Reward-model overfit shows as reward hacking; hold out fresh preference set, watch judge agreement drop.
Sequencing: prototype on unsloth single-GPU → scale on axolotl multi-GPU/multi-node → rebuild in torchtune for surgical PyTorch control.
Before recommending a non-trivial fine-tuning change (LoRA rank, schedule, packing strategy):
completion_only_loss, Axolotl QAT in v0.8, unsloth uncontaminated packing mid-2025)Tuning without measurement is worse than defaults.
npx claudepluginhub pvillega/claude-templates --plugin ctGuides LLM fine-tuning with LoRA/QLoRA, dataset preparation, hyperparameter tuning, evaluation, and deployment. Useful for adapting foundation models to custom tasks.
Configures and runs LLM fine-tuning using LoRA/QLoRA adapters, prepares JSONL datasets, sets hyperparameters, and handles deployment and evaluation.
Provides guidance on fine-tuning large language models using LoRA, QLoRA, PEFT, instruction tuning, and full fine-tuning strategies. Includes patterns, common pitfalls, and validation rules.