LLM Fine-Tuning & PEFT

LLM Fine-Tuning & PEFT

Adapting a pretrained LLM to a specific task, domain, format, or behavior by continuing training on labeled examples — and doing it cheaply with parameter-efficient fine-tuning (PEFT), which freezes the base model and trains a tiny set of new weights instead of all of them.

Five things this reference answers:

  1. Should I even fine-tune? Full FT vs PEFT vs RAG vs prompting.
  2. How does LoRA work, and how do I set its knobs? rank, alpha, target modules, init.
  3. Which PEFT method? LoRA family (QLoRA / DoRA / rsLoRA / LoRA+), adapters, (IA)^3, prefix/P-tuning/prompt-tuning.
  4. How do I run it? The HuggingFace PEFT workflow, SFT data prep + chat templating, the tooling stack.
  5. How do I ship it? Merge vs swap for serving, catastrophic-forgetting mitigation, and evaluating the result.

Scope boundary (read first)


Part 1 — Should you fine-tune at all? (the decision framework)

Fine-tuning is the most expensive and slowest of the three adaptation levers. Climb the ladder; stop at the first rung that clears your quality bar.

Lever Changes Cost / time Best at Cannot
Prompt engineering The input only Hours; ~free Fast iteration, behavior you can describe in instructions + few-shot Enforce format/style reliably at scale; teach genuinely new skills
RAG The input (adds retrieved context) Days; infra + per-query retrieval cost Injecting fresh / proprietary / large factual knowledge; citations; data that changes Change behavior, style, format, or reasoning patterns
Fine-tuning The weights Weeks; training + 6x-ish inference if you add an adapter layer Behavior / format / style / tone / domain reasoning; distilling a big model’s behavior into a cheaper small one Add facts that change daily (they bake in stale); fix a problem RAG+prompting already solves

The canonical order (2025 consensus): start with prompt engineering → add RAG when you need current/proprietary knowledge → fine-tune only when behavior stays inconsistent after prompts and RAG, or when a small fine-tuned model is cheaper than a large general one on your narrow task.

Knowledge vs behavior is the load-bearing distinction. RAG is for what the model knows; fine-tuning is for how the model acts. Fine-tuning is a poor way to inject facts (they go stale and the model still hallucinates around them) and RAG is a poor way to fix formatting/tone.

They compose. The highest-performing production systems often do both: fine-tune to shape behavior/format/domain reasoning, RAG to supply current facts at inference. Fine-tuning and RAG are not mutually exclusive.

Good fine-tune use cases: consistent structured output prompts can’t enforce; domain-specific reasoning absent from pretraining; style/tone calibration beyond what prompts achieve; cost optimization (a fine-tuned 8B beating a prompted 70B on your task at a fraction of the inference cost); behavior cloning / distillation.


Part 2 — Full fine-tuning vs PEFT

Full fine-tuning (FFT) updates every weight. PEFT freezes the base and trains a small add-on (often <1% of params). The trade-off is memory/cost vs peak capacity.

Memory. FFT of a 7B model needs ~100-120 GB VRAM (weights + gradients + Adam’s two moments + activations, all in fp16/bf16 → roughly 16-20 bytes/param). The same model fine-tunes with QLoRA on a single 24 GB RTX 4090. PEFT broadly cuts training memory 10-20x while retaining 90-95%+ of FFT quality on typical adaptation tasks. You also store a few-MB adapter instead of a full model checkpoint per task.

When PEFT (LoRA) is ~equal to FFT: instruction-following, style transfer, classification, most NLU (GLUE/SuperGLUE). Well-configured LoRA reaches 95-100% of FFT here.

When FFT still wins: large new-knowledge infusion (continued pretraining on 20B tokens) and hard generative skills (code, math). The “LoRA Learns Less and Forgets Less” paper (Biderman et al., 2024) found LoRA substantially underperforms FFT on programming and math in both instruction-tuning (~100K pairs) and continued-pretraining (20B tokens) regimes, because full fine-tuning learns weight perturbations with a rank 10-100x higher than typical LoRA configs, so low rank is genuinely capacity-limited there.

The upside of “learning less”: the same paper shows LoRA forgets less. It better preserves the base model’s out-of-domain capabilities and maintains more diverse generation, acting as a stronger regularizer than weight decay or dropout. So the FFT-vs-LoRA choice is a plasticity-vs-stability trade: FFT for max new capability, LoRA when retaining general ability and avoiding forgetting matters.

Closing the gap: a 2024-2026 line of work (“LoRA vs Full Fine-tuning: An Illusion of Equivalence”) argues even when LoRA matches FFT on the target metric it does so via “intruder dimensions” (new singular directions unlike the pretrained weights) which drive forgetting. The practical levers: raise the rank and apply LoRA to all linear layers (Part 4) to behave more like FFT, or use the intruder-dimension mitigation (Part 8).


Part 3 — LoRA mechanics (the one method to understand deeply)

LoRA (Low-Rank Adaptation; Hu et al., 2021) freezes the pretrained weight matrix W ∈ R^(d×k) and learns a low-rank update: W' = W + ΔW = W + (α/r)·BA, where B ∈ R^(d×r), A ∈ R^(r×k), and r ≪ min(d,k). Only A and B train. The hypothesis: the update a model needs for a downstream task has low “intrinsic rank,” so a thin BA product captures it with a fraction of the params.

The four knobs

Initialization

Default PEFT init: A ~ Kaiming-uniform, B = zerosBA = 0 at start, so the adapter begins as an identity transform (training starts exactly at the base model — critical for stability). init_lora_weights="gaussian" uses a Gaussian A (Diffusers convention). Data-driven inits that converge faster / preserve knowledge better: PiSSA (principal singular values/vectors of W), OLoRA (QR decomposition), EVA (SVD of input activations + adaptive per-layer rank via rho), CorDA (task- or knowledge-oriented decomposition, KPM mode mitigates forgetting), LoRA-GA (aligns to FFT gradient), and LoftQ (init to minimize quantization error for QLoRA).

Why LoRA is “free” at inference

Because ΔW = (α/r)BA is just a matrix, you can fold it into W after training (W' = W + ΔW) → a standalone model with zero added latency or params. This is the merge path (Part 7). Keep it unmerged only when you need to swap adapters.


Part 4 — The LoRA family: QLoRA, DoRA, rsLoRA, LoRA+

These keep LoRA’s low-rank update but fix a specific weakness.

QLoRA (Dettmers et al., 2023) — memory

Fine-tune LoRA adapters on top of a base model quantized to 4-bit, so the frozen weights occupy ~1/4 the VRAM while gradients flow through them in bf16. Three ingredients: (1) NF4 (4-bit NormalFloat, information-theoretically optimal for the ~normally-distributed weights — internals live in llm-compression.md), (2) double quantization (quantize the quantization constants too), (3) paged optimizers (page optimizer state to CPU to survive memory spikes). Result: fine-tune a 65-70B model on a single 48 GB GPU with quality matching 16-bit LoRA and 16-bit FFT. Enable in PEFT by loading the base with a bitsandbytes 4-bit quantization_config, then attaching LoRA as usual. Pair with LoftQ init for best quantized-training quality. “QDoRA” = QLoRA + DoRA.

DoRA (Liu et al., ICML 2024) — low-rank quality

Weight-Decomposed LoRA. Decompose each weight into magnitude (a scalar vector) and direction; let LoRA update only the direction while a separate learnable parameter handles magnitude. This decoupling makes DoRA’s learning pattern closer to FFT and beats LoRA especially at low rank (r=4-8) on commonsense reasoning and multimodal tasks, with no extra inference cost once merged. Enable: LoraConfig(use_dora=True). Caveats: bigger training overhead than plain LoRA (mitigated by DoraCaching / ephemeral_gpu_offload); supports linear/embedding/Conv2d only; merge for inference to erase the overhead.

rsLoRA (Kalajdzievski, 2023) — stable high rank

Rank-Stabilized LoRA changes the scaling from α/r to α/√r. With the original α/r, gradients collapse as r grows, so large ranks learn no better than small ones (the reason “just raise the rank” historically failed). With α/√r gradients stay healthy and higher ranks finally pay off: better perplexity/quality at large r, zero inference cost. Enable: LoraConfig(use_rslora=True). Use it whenever you want r ≥ 32.

LoRA+ (Hayou et al., 2024) — efficient feature learning

Vanilla LoRA updates A and B with the same learning rate, which is provably suboptimal for feature learning in wide models. LoRA+ uses a higher LR for B than A by a fixed ratio (loraplus_lr_ratio, e.g. 16). Result: ~1-2% accuracy and up to ~2x faster convergence at the same compute. Enable via create_loraplus_optimizer(...). (Related: a 2026 line of work argues careful LR tuning alone often suffices, so always tune LR before reaching for exotic variants.)

Picking within the family: start LoRA; tight on VRAM → QLoRA; low rank but want more quality → DoRA; want high rank to work → rsLoRA; want faster/slightly-better at no cost → LoRA+. They compose (e.g. QLoRA + rsLoRA + LoRA+).


Part 5 — The other PEFT families (non-LoRA)

PEFT methods differ in where they put the new parameters. (Survey framing: Han et al. 2024; HuggingFace PEFT.)

The mental model: LoRA/adapters/(IA)^3 = reparameterize the weights; prefix/P-tuning/prompt-tuning = learn a soft prompt, weights untouched. In 2024-2026 practice LoRA (and its family) is the default; (IA)^3 for extreme parameter thrift; prompt-based methods are mostly of historical / multi-task-serving interest. Prefix/prompt methods also consume context length at inference.


Part 6 — The HuggingFace PEFT workflow

peft is the standard library; it wraps any transformers model.

from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM

base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
config = LoraConfig(
    r=16, lora_alpha=32,
    target_modules="all-linear",   # QLoRA-style: every linear layer
    lora_dropout=0.05,
    use_rslora=True,               # α/√r scaling for stable higher rank
    # use_dora=True,               # weight-decomposed variant
    # init_lora_weights="pissa",   # data-driven init
    task_type="CAUSAL_LM",
)
model = get_peft_model(base, config)   # ~0.5-2% params now trainable
model.print_trainable_parameters()
# ... train with transformers Trainer or TRL SFTTrainer ...
model.save_pretrained("my-adapter")    # saves only the few-MB adapter

Knob summary in LoraConfig: r, lora_alpha, target_modules (or "all-linear"), lora_dropout, use_rslora, use_dora, init_lora_weights (True/"gaussian"/"pissa"/"olora"/"eva"/"loftq"/"corda"), rank_pattern/alpha_pattern (per-layer overrides), target_parameters (MoE experts), modules_to_save (fully-train extra modules like a new classifier head), trainable_token_indices (train just new special-token embeddings).

Multiple adapters on one base (Part 7): PeftModel.from_pretrained(base, id, adapter_name="a"), then model.load_adapter(id2, adapter_name="b"), model.set_adapter("b") to switch, model.disable_adapter() context for the raw base, model.delete_adapter("b") to drop. LoRA+ optimizer: create_loraplus_optimizer(model, optimizer_cls, lr, loraplus_lr_ratio).

PEFT supports LoRA + variants (DoRA/rsLoRA/PiSSA/…), adapters, (IA)^3, prefix-tuning, P-tuning, prompt-tuning, LoHa/LoKr, and more — same wrap-and-train shape.


Part 7 — Multi-LoRA serving: merge vs swap (+ adapter merging)

You trained an adapter. Two ways to serve it, and a third way to combine several.

Merge (merge_and_unload). Fold ΔW into W to get a standalone model with zero added latency. Use when one adapter serves all traffic. It is not in-place, so assign the return value. Lossy for quantized bases (merging fp16 deltas into a 4-bit base reintroduces error) and irreversible; for QLoRA, either serve unmerged or dequantize-then-merge. DoRA/MoE-LoRA should be merged to erase their inference overhead.

model = PeftModel.from_pretrained(base, "my-adapter")
model = model.merge_and_unload()      # standalone, no PEFT overhead
# Reversible variant: model.merge_adapter() ... model.unmerge_adapter()

Swap / multi-tenant (keep unmerged). Keep the frozen base resident once and hot-swap small adapters per request — N tasks served from 1 base + N few-MB adapters instead of N full models. The economic win behind LoRA serving. PEFT can even mix adapters within one batch via the adapter_names argument (base/adapter_fr/adapter_de rows in the same forward pass). At scale, the serving engine does this efficiently:

The kernel/runtime side of multi-LoRA (PagedAttention, continuous batching, KV-aware routing, autoscaling) is the sibling references/llm-inference-serving.md. This reference owns the decision (merge vs swap) and the adapter-combination math below.

Combining several adapters into oneadd_weighted_adapter(adapters=[...], weights=[...], combination_type=...). combination_type options:

aLoRA (Activated LoRA) is a serving-time variant that activates the adapter only after an invocation token, so it reuses the base model’s KV cache — an order-of-magnitude speedup when the base does most of the work and the adapter handles a checking/correcting sub-task. aLoRA cannot be merged by definition.


Part 8 — Catastrophic forgetting & mitigations

Catastrophic forgetting: fine-tuning on a narrow task degrades the base model’s general abilities (it overwrites pretrained knowledge). The classic symptom is a fine-tune that nails your task but loses MMLU points and general chat quality.

Mitigations, roughly strongest-first:

  1. Use PEFT, especially LoRA. Because the base is frozen and only a small add-on trains, parameter isolation structurally protects pretrained weights (the “LoRA forgets less” result, Part 2), and LoRA out-forgets weight decay and dropout. The single biggest lever.
  2. Experience replay / rehearsal. Mix a slice of general / prior-task data (or pretraining-style data) into the fine-tune set. The most effective standalone technique; recent work prioritizes rehearsing “collateral-damage” examples (ones the base got right but the fine-tune started getting wrong).
  3. Regularization toward the base. Weight decay, dropout, lower learning rate, fewer epochs, early stopping on a held-out set.
  4. Forgetting-aware init / structure. CorDA-KPM (knowledge-preserved init) and OPLoRA (orthogonal-projection LoRA) explicitly protect base knowledge; KappaTune targets only the most isotropic (high-entropy) layers, leaving specialized layers intact.
  5. Intruder-dimension reduction (reduce_intruder_dimension) — post-hoc remove the “intruder” singular directions a LoRA introduced; a tunable trade-off between task accuracy kept and base knowledge restored.

Always quantify it: run a general-capability benchmark (e.g. MMLU) on the base and the fine-tune. A >2-3 point drop signals forgetting (Part 10).


Part 9 — SFT data preparation & chat templating

The fine-tune’s quality is bounded by its data. Supervised fine-tuning (SFT) = training on (instruction/prompt → desired response) pairs so the model shifts from generic next-token prediction to following instructions in your format. (SFT is also the first stage of post-training that precedes RLHF/DPO → llm-alignment-post-training.md.)

Quality over quantity. A few thousand clean, diverse, correctly-formatted, deduplicated examples beat a noisy large set (the LIMA “less is more” finding). Curate for correctness, format consistency, and coverage of the behaviors you want; decontaminate against your eval set.

Dataset formats (TRL SFTTrainer conventions):

Chat templating is non-negotiable. Chat models were trained with an exact token format (role markers + special tokens, e.g. <|im_start|>user … <|im_end|>). The template is a Jinja string shipped on the tokenizer (tokenizer.apply_chat_template(...)). Mismatched formatting between fine-tuning and inference is the #1 silent fine-tune killer: train and serve with the same template and special tokens. When introducing genuinely new special tokens, resize_token_embeddings and train them (PEFT trainable_token_indices does this cheaply).

Completion-only / loss masking. You almost always want loss computed only on the assistant/response tokens, not the prompt — set the prompt-token labels to the ignore index -100 so cross-entropy skips them. This focuses learning on generating the response rather than memorizing the instruction. TRL’s SFTTrainer does completion-only masking for prompt-completion data by default; for conversational data use its assistant-only-loss option.

Packing. Concatenate short examples into full-length sequences to avoid wasted padding compute (packing=True) — watch that cross-example attention is masked.


Part 10 — Tooling stack

Tool Shape Pick when
HuggingFace PEFT + TRL Libraries (LoraConfig/get_peft_model + SFTTrainer) You want code-level control / are already in the HF stack; the canonical baseline
Unsloth Notebook-first, custom Triton kernels Single-GPU, limited VRAM; ~2x faster, ~50-70% less memory via hand-written kernels. No multi-GPU
Axolotl YAML-config wrapper over HF Reproducible team runs and multi-GPU; broad model + technique coverage
Llama-Factory Zero-code web UI (+ Unsloth backend option) Fastest path to a first run; 100+ model templates; “use if unsure”
torchtune PyTorch-native, abstraction-free recipes You want to modify the training loop in pure PyTorch / research

All support LoRA + QLoRA; the differences are ergonomics, kernel speed, and multi-GPU. For preference optimization (DPO/PPO) the same tools route into TRL / OpenRLHF → llm-alignment-post-training.md.


Part 11 — Evaluating a fine-tune

A fine-tune eval needs two prongs — and you must beat a real baseline.

  1. Task improvement. A held-out test set of your task (never seen in training), scored with a task-appropriate metric: exact-match/F1 for extraction, pass@k for code, an LLM-as-judge rubric for open-ended generation, classification metrics for labels. (Harness mechanics → da-7-machine-learning.)
  2. Capability-regression check. Run a general benchmark (e.g. MMLU) on the base and the fine-tune. A >2-3 point drop = catastrophic forgetting — address with Part 8 before shipping.

Always compare against the base model on the same held-out set to prove the fine-tune actually helped (and ideally against a strong prompted base — sometimes prompting alone matches it).

Detect overfitting: track validation loss during training and stop when it turns up (early stopping); a train-loss that keeps dropping while val-loss rises is the tell. Watch for benchmark contamination: the NeurIPS-2023 fine-tuning competition found top models heavily overfit popular benchmarks, so a clean, private held-out set is worth more than a public leaderboard number.


Anti-patterns

Troubleshooting

References