Search Tech Journey

Find topics, journeys and posts

6-month learning plan123 / 130
back to blog
llmadvanced 55m read

S123 · Fine-Tuning — LoRA, QLoRA, PEFT, When NOT to Fine-Tune

The engineering behind adapting foundation models. Why LoRA works, how QLoRA squeezes a 70B model onto a single GPU, and the five situations where you should NOT fine-tune.

LLMsM14 · LLMs & Applications· Session 123 of 130 90 min

🎯 Fine-tune an open model with LoRA on one GPU, and know when NOT to fine-tune in the first place.

Why this session exists

Fine-tuning is the shiniest button in the LLM toolbox and the most commonly misused one. Teams spend six figures fine-tuning a 70B model and end up with something worse than a 20-line prompt with three examples. This session gives you the taste for when the shiny button is right — and the discipline to not press it the other 80% of the time.

You will be able to
  • Explain in one breath why LoRA can update <1% of the parameters and still work.
  • Pick between LoRA, QLoRA, and full fine-tuning given a GPU budget, dataset size, and quality target.
  • Recognise the five 'do NOT fine-tune, do this instead' patterns.
  • Run a working LoRA fine-tune on a small model in under 10 minutes on a free GPU.
  • Serve dozens of adapters over one base model without duplicating GPU memory.

Prerequisites

  • S105 · Transfer Learning & Fine-Tuning (classical DL background)
  • S112 · Transformers — BERT/GPT/T5 (you need to know what Q, K, V projections are)
  • S122 · LLM Evaluation (you can't tell if fine-tuning helped without an eval loop)


(a) Intuition · 5 min

Bolt-on tuning vs rebuilding the engine
🌍 Real world

You own a Ferrari and want it to drive well on Indian roads. Option one: rebuild the engine for local fuel, roads, and traffic. Expensive, risky, requires a factory. That's full fine-tuning: 280 GB of weights to update, 8× A100s, weeks.

Option two: bolt on a 500-rupee aftermarket module that adjusts throttle and steering for local conditions. The Ferrari underneath is untouched. That's LoRA: 1% of parameters, one consumer GPU, hours. Tomorrow you can swap the module for a "Delhi mode" one without touching the engine.

💻 Code world

LoRA freezes the giant weight matrix `W` (say 4096×4096 = 16M numbers) and adds two skinny matrices `A` (4096×r) and `B` (r×4096) with r small (typically 8–64). Their product `B·A` is the adjustment. You only train `A` and `B` — roughly 0.1–1% of the original parameter count.

QLoRA adds the trick of quantising the frozen base model to 4-bit, so a 70B model fits on one 48 GB GPU during training. PEFT is Hugging Face's umbrella library that gives you LoRA/QLoRA/prefix tuning behind one API.

The three claims that make LoRA work
  • The weight update during fine-tuning is low intrinsic rank — you don't need a full-rank delta to represent it.
  • You can therefore represent the delta as B·A with rank r ≪ dim(W), and lose almost nothing.
  • Because the base W is frozen, you can swap adapters at inference time and multiplex hundreds of 'fine-tunes' onto one base model in GPU memory.
  1. 2018
    ULMFiT + BERT fine-tuning
    Fine-tuning goes mainstream. Every downstream task = update every parameter of BERT. Works fine at 110M params.
  2. 2020
    GPT-3 · 175B params
    Full fine-tuning becomes infeasible for most teams. In-context learning fills the gap for a while.
  3. 2021
    LoRA · Microsoft
    Hu et al. show low-rank updates match full FT quality at 1% cost. The paper is 8 pages; the impact is a decade.
  4. 2023
    QLoRA · Dettmers et al.
    Add 4-bit quantisation on the frozen base. Now you fine-tune a 65B model on a single 48 GB card.
  5. 2023
    PEFT library · Hugging Face
    One API for LoRA, QLoRA, prefix, prompt, IA³ tuning. Adapter merging + hub-based distribution.
  6. 2024
    Multi-LoRA serving · vLLM/S-LoRA
    Serve hundreds of adapters over one base model in the same GPU. Personalisation at scale becomes economical.

(b) Visual walkthrough · 15 min

The math, drawn

The forward pass becomes y = W·x + (B·A)·x. Only A and B receive gradients. At inference you can merge B·A into W (zero latency cost) or keep them separate (hot-swap adapters).

Cost model for Llama-2-7B (rough, for your gut)

Full FT (fp16)

The old way

  • Trainable: 7B (100%)
  • GPU memory ~112 GB
  • Checkpoint 14 GB
  • ~8 h on 8×A100
  • Best-in-class quality
LoRA r=16 (fp16)

The default answer

  • Trainable: ~42M (0.6%)
  • GPU memory ~28 GB
  • Adapter ~84 MB
  • ~2 h on 1×A100
  • Within 1–2pp of full FT on most tasks
QLoRA r=16 (4-bit base)

The GPU-poor answer

  • Trainable: ~42M (0.6%)
  • GPU memory ~8 GB
  • Adapter ~84 MB
  • ~4 h on 1×A6000
  • Within 2–3pp of LoRA on most tasks
Prompt tuning / P-tuning

The extreme

  • Trainable: <0.01%
  • Tiny memory + tiny adapter
  • Only learns a soft prompt prefix
  • Fastest to iterate
  • Weakest — good for narrow tasks

The decision tree — read top to bottom

1
1 · <500 labelled examples?

STOP. Don't fine-tune. Few-shot prompt + RAG. Come back when you have data.

2
2 · Need to teach facts or knowledge?

STOP. Use RAG. Fine-tuning is unreliable for facts and impossible to update without retraining.

3
3 · Need consistent output format or a specific tone?

GOOD candidate. Fine-tune on 1–10k labelled examples.

4
4 · One GPU with 24 GB or less?

Use QLoRA. Otherwise LoRA is simpler and slightly higher-quality.

5advanced
5 · Want to serve dozens of variants (per customer, per product)?

LoRA adapters + multi-LoRA serving (vLLM, S-LoRA). One base model, many personalities.

6rare
6 · Building your own foundation model?

Full FT (or pretraining). Reserved for the few teams with 8+ H100 clusters.

Where the trainable params live inside a transformer

LoRA target_modules — what to pick

q_proj + v_proj (LoRA paper default)
Cheapest. Works well for style tasks. What Meta and Microsoft originally recommended.
cheapest
q_proj + k_proj + v_proj + o_proj
All attention projections. Better quality on reasoning tasks. ~2× params vs the default.
attention
+ gate_proj + up_proj + down_proj
All attention AND all MLP projections. The 'modern default' since 2024. Best quality; ~5× params vs the LoRA-paper default.
modern default
Embeddings + lm_head
Very rarely needed. Only if you're teaching new tokens or a new language.
special

Hyperparameter cheat-sheet (start here)

r          = 8-16     (16 is a safe default for 7-13B models)
lora_alpha = 2 * r    (so the effective scale is 2)
dropout    = 0.05
lr         = 1e-4     (LoRA is 10x more LR-tolerant than full FT)
epochs     = 2-3      (more usually overfits on <10k examples)
batch      = fit into memory, use grad accumulation
target     = all attention + MLP projections

Common misconception
✗ What most people think

"Fine-tuning teaches the model new facts. If it doesn't know our internal product details, we should fine-tune on our documentation."

✓ What is actually true

Fine-tuning is far better at teaching behaviour — format, style, task convention, domain vocabulary — than at installing reliable, retrievable facts. Facts learned from a small fine-tuning set are learned weakly, blend with pretrained knowledge, and are recalled unreliably; worse, the model becomes more confident in the domain while still hallucinating specifics. Knowledge belongs in retrieval; behaviour belongs in weights.

Why the myth is so sticky

Because "training on our data" maps onto an intuitive database model of learning, and because fine-tuning genuinely does make output sound domain-native — which reads as having learned the domain. The distinction between sounding like your docs and knowing what is in them is invisible in casual testing and expensive in production, where the model confidently invents a plausible SKU or API parameter that follows your naming conventions perfectly.

Prove it to yourself

Separate the two effects on your own fine-tune before deciding it worked:

# Split your eval into two sets and score them separately.
#
# BEHAVIOUR set: does it use the right format, tone, terminology,
#   structure? Score with a rubric. Fine-tuning should move this a lot.
#
# FACT set: closed questions with verifiable answers taken from the
#   fine-tuning corpus. Score exact match. Then run the SAME set with
#   the base model + retrieval.
#
# Typical result: fine-tune wins decisively on behaviour, retrieval
# wins decisively on facts. If you see that, you need both -- not a
# choice between them.
From first principles
Start with the question

Why does LoRA work? Constraining updates to rank 8 out of thousands of dimensions should destroy most of the model's ability to adapt — why does it match full fine-tuning?

  1. 1
    Pretraining has already learned general representations. Fine-tuning does not need to build new features; it needs to reweight and recombine features that already exist.
    forced by · the target task is drawn from a distribution the pretrained model already partially models
  2. 2
    So the required weight update ΔW is small in magnitude relative to W, and — more importantly — is structured rather than arbitrary, because it expresses a small number of consistent adjustments applied across the data.
    forced by · a single task imposes a single consistent bias, not thousands of independent ones
  3. 3
    A consistent adjustment repeated across many inputs is, by construction, low-rank: the update matrix's rows are dominated by a few directions.
    forced by · rank counts independent directions of change, and a coherent task-level shift needs few
  4. 4
    Therefore ΔW can be factorised as BA with B of shape (d, r) and A of shape (r, k), r << d. Parameters drop from d×k to r(d+k) — for d=k=4096 and r=8, from 16.8M to 65K, about 0.4%.
    forced by · the factorisation is exactly the statement that the update lives in an r-dimensional subspace
  5. 5
    Initialise B to zero so BA = 0 at step 0. The adapted model is then identical to the base model before training, so no capability is disturbed and there is no warmup shock.
    forced by · starting from the exact base function means fine-tuning is a strict refinement rather than a perturbation
  6. 6
    And since the frozen base weights need no gradients or optimiser state, memory drops by far more than the parameter ratio suggests — Adam alone stores two moments per trainable parameter.
    forced by · optimiser state, not weights, dominates fine-tuning memory
⇒ Therefore

Therefore LoRA works because task adaptation is intrinsically low-rank, and it is cheap because freezing the base eliminates the optimiser state that dominates memory.

And note what this predicts: (1) the more the target task differs from pretraining, the higher r must be — so a rank that works for tone adaptation will underfit a genuinely new capability; (2) because W + BA is just a sum, adapters can be merged into the base at inference for zero added latency, or kept separate and hot-swapped per request. Multi-tenant serving of hundreds of task-specific adapters over one base model follows directly from the factorisation.

Mental modelWeights are behaviour, context is knowledge

Two places information can live. In the weights: slow to write, expensive to change, impossible to audit or delete, but free at inference and always available. In the context: instant to write, trivially updated and deleted, fully auditable and citable, but paid for in tokens on every request.

The routing rule follows from those properties alone. Anything that changes — prices, docs, policies, per-customer data — must live in context. Anything that is a stable way of behaving should live in weights.

  • LoRA on attention projections (q, v at minimum) is the standard starting point; r=8–16 for style and format, higher for genuinely new capability.
  • QLoRA quantises the frozen base to 4-bit while training adapters in higher precision — the base is never updated, so quantisation error does not accumulate. This is what puts large models on a single GPU.
  • Catastrophic forgetting is real: fine-tuning narrowly degrades general capability. Always keep a general-capability eval in your regression set, not just the target task.
  • Data quality dominates data quantity. A thousand carefully curated examples routinely beat a hundred thousand scraped ones, because the model learns the average of what you show it.
🔔 Fires when you see

Fire this model the moment you see: "let's fine-tune on our docs" · a model that must know today's data · a compliance requirement to delete specific information · per-customer customisation at scale · a fine-tune that improved the target task and quietly broke everything else.

The tradeoff

You need a model that behaves domain-specifically and knows current internal data. RAG, LoRA fine-tuning, or full fine-tuning?

RAG
+ you gain knowledge updates the instant the source updates; sources are citable, which is often a hard requirement for review or compliance; deletion is real deletion; and it works with any base model, so upgrades are free
− you pay every request pays retrieval latency and the token cost of injected context; quality is capped by the retriever; and it cannot teach output format, tone, or domain conventions
pick when the information changes, must be cited, or must be deletable — and always, for facts
LoRA fine-tuning
+ you gain teaches format, tone and task convention durably at a small fraction of full fine-tuning's memory and time; adapters are megabytes, so you can keep many and swap per tenant; merging gives zero inference overhead
− you pay needs a curated dataset and an eval you trust; adds a training and versioning pipeline; ties you to a base model version, so upgrading means retraining; and it will not reliably install facts
pick when prompt engineering has plateaued on a behavioural requirement, and you have or can build several hundred to a few thousand high-quality examples
Full fine-tuning
+ you gain maximum adaptation capacity — the only option when the target domain is genuinely far from pretraining (a new language, a formal notation, a modality-shifted task)
− you pay needs optimiser state for every parameter, so multiples of the model size in GPU memory; highest catastrophic-forgetting risk; produces a full model copy per task, which does not scale across tenants
pick when a large, genuinely out-of-distribution corpus and the compute to match — rare, and usually the wrong answer at first ask
What a senior engineer actually does

Almost every real system wants RAG and LoRA, because they solve orthogonal problems: retrieval supplies what is true right now, the adapter supplies how to say it. Framing them as alternatives is the most common and most expensive mistake in this area.

Order of operations matters too: prompt engineering first, then retrieval, then LoRA — because each step gives you the eval set and the failure taxonomy the next step needs. Fine-tuning before you can articulate precisely what is wrong just bakes your current confusion into the weights, where it is much harder to remove.


(c) Hands-on · 25 min

LoRA fine-tune a small model on a toy instruction dataset. Runs on a free Colab T4 in ~5 min.

#!/usr/bin/env python3
# lora_pirate.py — teach TinyLlama to answer like a pirate, using LoRA.
# pip install transformers peft datasets accelerate bitsandbytes
import torch
from datasets import Dataset
from peft import LoraConfig, PeftModel, TaskType, get_peft_model
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    Trainer,
    TrainingArguments,
)
 
MODEL = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
ADAPTER_DIR = "/tmp/pirate-lora"
 
# ---------- 1. Load base ----------
tok = AutoTokenizer.from_pretrained(MODEL)
tok.pad_token = tok.eos_token
base = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="auto")
 
# ---------- 2. Wrap with LoRA — <1% trainable ----------
lora_cfg = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"],  # try adding k_proj, o_proj, gate/up/down for higher quality
)
model = get_peft_model(base, lora_cfg)
model.print_trainable_parameters()
# ~ trainable: 1.1M || total: 1.1B || trainable%: 0.10%
 
# ---------- 3. Tiny dataset — answer in pirate style ----------
raw = [
    {"prompt": "What is Python?",           "response": "Arr, 'tis a fine programmin' language, matey!"},
    {"prompt": "What is a database?",       "response": "Yarr! A treasure chest for yer data, ye landlubber!"},
    {"prompt": "What is machine learning?", "response": "Ahoy! Teachin' the ship's parrot to predict the seas!"},
    {"prompt": "What is a compiler?",       "response": "Arr, a scurvy translator turnin' yer scribbles into cannon fire!"},
    {"prompt": "What is Docker?",           "response": "Yarr! A magic barrel that carries yer whole ship inside!"},
] * 30  # replicate for a few epochs of signal
 
def format_example(ex):
    text = (
        f"<|user|>\n{ex['prompt']}\n"
        f"<|assistant|>\n{ex['response']}{tok.eos_token}"
    )
    out = tok(text, truncation=True, max_length=128, padding="max_length")
    out["labels"] = out["input_ids"].copy()
    return out
 
ds = Dataset.from_list(raw).map(format_example, remove_columns=["prompt", "response"])
 
# ---------- 4. Train ----------
args = TrainingArguments(
    output_dir=ADAPTER_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=4,
    learning_rate=1e-4,        # 10x higher than full FT
    logging_steps=10,
    save_strategy="epoch",
    report_to="none",
)
Trainer(model=model, args=args, train_dataset=ds).train()
model.save_pretrained(ADAPTER_DIR)   # only the adapter — ~5MB on disk
 
# ---------- 5. Reload adapter on a fresh base and generate ----------
del model
fresh_base = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="auto")
served = PeftModel.from_pretrained(fresh_base, ADAPTER_DIR)
served.eval()
 
for q in ["What is a neural network?", "What is Kubernetes?"]:
    prompt = f"<|user|>\n{q}\n<|assistant|>\n"
    ids = tok(prompt, return_tensors="pt").input_ids
    with torch.no_grad():
        out = served.generate(ids, max_new_tokens=40, do_sample=False)
    print(">>>", tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True))

What each block is doing

Anatomy of the fine-tune

LoraConfig · r=8, alpha=16
Rank 8 delta with scale factor alpha/r = 2. Together with dropout=0.05 this is the canonical safe starting point.
hyperparams
target_modules=['q_proj','v_proj']
The LoRA-paper default. Bump to all attention + MLP projections for +1–2pp quality at ~5× trainable params.
coverage
labels = input_ids
Causal LM: the label at position t is the token at position t+1, shifted internally. Copying input_ids is the standard idiom.
loss
learning_rate=1e-4
LoRA is ~10× more LR-tolerant than full FT (which uses 1e-5 to 5e-5). Push it further at your peril.
training
save_pretrained(ADAPTER_DIR)
Writes only the LoRA weights — ~5 MB on disk vs ~2 GB for the full model. This is what you ship.
artefact
PeftModel.from_pretrained(base, ADAPTER_DIR)
Loads the adapter on top of a fresh base. Same base + different adapters = many personalities, one GPU.
serving
Try itFeel adapter hot-swapping — the reason multi-LoRA serving exists

Train a second adapter on Shakespeare-style responses (e.g. "Verily, 'tis a most wondrous language, good sir!"). Save to /tmp/shakespeare-lora. Then in one script, load the base once and swap between adapters:

served_pirate = PeftModel.from_pretrained(base, "/tmp/pirate-lora", adapter_name="pirate")
served_pirate.load_adapter("/tmp/shakespeare-lora", adapter_name="shakespeare")
served_pirate.set_adapter("pirate")       # generate as pirate
served_pirate.set_adapter("shakespeare")  # generate as Shakespeare — same base weights in memory
💡 Hint · Save a second adapter trained on formal/Shakespearean data, then reload the same base with adapter A vs adapter B. Same 1.1B model in memory; two different personalities.

(d) Production reality · 15 min

War story A fintech · 2024 (industry common failure)$50k + 3 engineer-months
🔥 What broke

Team spent 3 months fine-tuning Llama-2-70B on internal policy documents. Model produced confident, plausible answers that were wrong 40% of the time — mixing up policy versions, dates, and jurisdictions.

Root cause: fine-tuning teaches style, not facts. The model memorised patterns like "our refund policy is 30 days" but conflated v3 and v5 of the policy across similar contexts.

🧯 The fix
Discarded the fine-tune. Built a RAG system on the same docs with citation-required prompting. Accuracy jumped to 92% and updates became a re-index instead of a re-train. Total serving cost: ~$200/month for embeddings + vector DB.
🎓 Lesson to steal
Fine-tuning ≠ knowledge injection. If your ask is 'the model needs to know X', RAG almost always wins. Fine-tune for style, tone, format, and behaviour — not facts.
War story Multi-tenant SaaS · 2024 (common failure)200 per-customer adapters
🔥 What broke
Team shipped 200 per-customer LoRA adapters, one per tenant. Serving became a nightmare because each adapter swap flushed the KV cache and re-materialised weights on GPU. Cold-start jumped from 100 ms to 4 s per tenant switch.
🧯 The fix
Migrated to vLLM multi-LoRA serving (based on the S-LoRA paper). One base model resident on GPU; adapters streamed in as tiny weight patches; batched inference across tenants in the same forward pass. P95 dropped from 4 s to 220 ms.
🎓 Lesson to steal
The moment you have >5 adapters, use a multi-LoRA serving stack. Reinventing this by "unload/reload PeftModel per request" is a well-known trap.
War story Open-source project maintainersreproducibility crisis
🔥 What broke
Users report a QLoRA training script "randomly" produces NaN losses on the same data on different GPUs. Reproducibility broken.
🧯 The fix

Root cause: 4-bit quantisation with certain BF16 CUDA kernels + certain `bitsandbytes` versions triggers overflow. Fix:

bnb_config = BitsAndBytesConfig(
  load_in_4bit=True,
  bnb_4bit_quant_type="nf4",
  bnb_4bit_compute_dtype=torch.bfloat16,
  bnb_4bit_use_double_quant=True,
)

And pin `bitsandbytes`, `transformers`, and `peft` in `requirements.txt`.

🎓 Lesson to steal
QLoRA works but the quantisation stack is fragile. Pin versions, use `nf4`, and log a "loss finite?" check every step so you fail loud instead of silent.

Common failure modes to log for

Where this shows up in the rest of the plan

Fine-tuning fits between prompt eng and RAG in the LLM app stack
S119 · Prompt Engineering
Always try this first — usually enough.
S117 · RAG
The right tool for 'model needs to know X'.
S122 · LLM Evaluation
The prerequisite for any fine-tuning decision.
S124 · LLM Serving
Where multi-LoRA + KV cache management lives.
S125 · Multimodal LLMs
Vision/audio adapters use the same PEFT pattern.
S130 · Design an AI Chat Product
Capstone — decide LoRA vs RAG vs prompt for each user story.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

  1. In one sentence, why does LoRA work?
  2. Give a real example of when you'd fine-tune and a real example of when you'd RAG instead.
  3. Name one production trap unique to running many adapters at once.

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.