Build an LLMMiddleLesson 526 min read

Step 16 · LoRA, the path most people take

Train 0.1% of the parameters, get most of the benefit, on one consumer GPU. This is the module with the highest practical value in the whole track.

Lesson in motion

In 60 seconds

Step 16 · LoRA, the path most people take

Train 0.1% of the parameters, get most of the benefit, on one consumer GPU. This is the module with the highest practical value in the whole track.

1/5
In simple words
Instead of rewriting the whole textbook, you write sticky notes in the margin. Far less work, and you can peel them off later.
Full fine-tuning updates every weight, which needs about eight times the model's size in memory. LoRA freezes the original weights and learns a small low-rank correction beside them.
Frozen Wd x d, unchanged4096 x 4096 = 16.7M+Ad x rBr x dr = 16 -> 131ktrained0.8% of the weightssame task qualityoutput = Wx + BAx · the frozen path is untouched
Why it works. The change a fine-tune needs to make turns out to be low-rank — it lives in a small subspace. So you can represent it with two thin matrices instead of one fat one.
LoRA from scratchpython
class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r=16, alpha=32, dropout=0.05):
        super().__init__()
        self.base = base
        for p in self.base.parameters():
            p.requires_grad = False              # freeze the original

        self.A = nn.Parameter(torch.zeros(r, base.in_features))
        self.B = nn.Parameter(torch.zeros(base.out_features, r))
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
        # B starts at zero, so the adapter is a no-op at step 0
        self.scale = alpha / r
        self.drop = nn.Dropout(dropout)

    def forward(self, x):
        return self.base(x) + self.drop(x) @ self.A.T @ self.B.T * self.scale

    def merge(self):
        """Fold the adapter into the base weight for zero-overhead serving."""
        self.base.weight.data += (self.B @ self.A) * self.scale
        return self.base
Do this
Note that B is initialised to zero. That means the adapted model starts exactly equal to the original — no random perturbation, no warmup shock. It is a small detail that makes LoRA remarkably well behaved.

QLoRA: even the frozen weights get smaller

Quantise the frozen base model to 4 bits, keep the LoRA adapters in bfloat16, and train. A 70B model becomes fine-tunable on a single 48 GB card; a 7B model fits comfortably on a 16 GB one.
QLoRA in practicepython
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",              # normal-float 4, best for weights
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,         # quantise the quantisation constants
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B", quantization_config=bnb, device_map="auto")
model = prepare_model_for_kbit_training(model)

model = get_peft_model(model, LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05, bias="none",
    task_type="CAUSAL_LM",
    # adapt attention AND the feed-forward layers -- ignoring the FFN
    # is the single most common reason a LoRA underperforms
    target_modules=["q_proj","k_proj","v_proj","o_proj",
                    "gate_proj","up_proj","down_proj"],
))
model.print_trainable_parameters()    # ~ 42M of 8B = 0.5%

Choosing rank

RankTrainable shareRight for
4–8~0.1%Style, tone, output format
16–32~0.5%Most tasks — start here
64–128~2%Big behaviour shifts, new domains
256+~5%Approaching full fine-tuning; consider whether you need it
Watch out
Set alpha to roughly twice the rank and change one of them at a time. Tuning both at once is how people convince themselves LoRA does not work.

Why this is the highest-leverage module here

  • Cheap. A useful fine-tune costs a few dollars of GPU time.
  • Portable. An adapter is tens of megabytes. Ship dozens, swap at runtime, serve many customers from one base model.
  • Reversible. Unhappy with it? Remove the adapter. The base model was never touched.
  • Mergeable. Fold it into the weights for serving and pay zero inference overhead.
  • Composable. Adapters can be stacked or blended, with mixed results but real utility.
Danger
Security note that belongs in Track B: a LoRA adapter is executable behaviour, distributed as a small file. Downloading a stranger's adapter is closer to running their code than to loading data. It can install backdoors, remove refusals, or add a trigger phrase. Treat adapters with the same suspicion as dependencies — Module 15.

Watch and read more

Lab

A LoRA fine-tune that beats a full one on your task, for a hundredth of the cost.

~25 min

The problem

Fine-tune the same model twice: LoRA r=16 and full fine-tuning. Compare task quality, general-benchmark retention, wall-clock time and peak memory. Then ablate the target modules — attention only vs attention plus FFN.
Starter codepython
target_modules=["q_proj","k_proj","v_proj","o_proj",
                "gate_proj","up_proj","down_proj"]   # drop the last three and compare

You are done when

Hard questions

Try to answer before you reveal. If you can answer these, you understood the lesson.

Q1LoRA r=16 matches full fine-tuning on your task. Give a task where it would not, and explain why.Reveal
Teaching genuinely new knowledge or a new language, rather than adapting behaviour. LoRA constrains the update to a rank-16 subspace, which is ample for style, format and task adaptation — those turn out to be low-rank changes — but too narrow to install substantial new representations. Signature: training loss plateaus well above what full fine-tuning reaches, and raising the rank keeps helping. If raising rank stops helping, you were never rank-limited.

Please sign in to continue.

Questions people ask

Does LoRA match full fine-tuning?

For most task adaptation, close enough that the difference is hard to measure. Full fine-tuning pulls ahead when you are teaching a genuinely large amount of new behaviour or training on very large datasets.

Which modules should I adapt?

Attention projections plus the feed-forward layers. Adapting attention only is the most common configuration mistake and leaves substantial quality on the table.

Can I merge several adapters?

Yes, by weighted averaging, and results vary. Adapters trained for conflicting behaviours interfere. Serving them separately and routing is more predictable.

Does quantising to 4 bits hurt?

Measurably but modestly with NF4 double quantisation, and the ability to fine-tune a model you otherwise could not touch usually outweighs it. For serving, 8-bit or 4-bit quantisation of the final model is standard practice.

Lesson test

5 questions. Get 3 right (60%) to pass and complete this lesson.

Sign in with your phone number to take the test and save your progress