LoRA and PEFT
Follow a frozen weight path and a trainable low-rank correction. Understand parameter savings, scaling, and merging.
Jump to a section
Why this matters
LoRA adapts a pretrained model by training a low-rank update to selected weight matrices while keeping the original weights frozen. Instead of updating a full matrix W, it learns two smaller matrices whose product is added to W.
The practical benefit is fewer trainable parameters and less optimizer state. Low rank is a constraint on the update, not a claim that every useful adaptation is exactly low-rank or that the base model becomes smaller.
Build the mental model
A linear layer normally computes . LoRA adds a parallel low-rank branch: , where projects down to a small rank and projects back up. The base is frozen; only and get gradients. Because is small (often 8–64), and together are a rounding error in size next to .
A small rank. A different space.
Change the adapter rank and watch its outputs open from a line into a plane and a volume.
The visual loads as you reach this section.
For compatible dense weights, merge the scaled update into before inference. This removes the separate adapter operations while retaining the original matrix shape. Keeping multiple adapters separate or using quantized weights can change that deployment choice.
Work through the math
For a frozen weight , LoRA reparameterizes the update as a low-rank product:
is a scaling constant (the effective update is ). Initialization matters: is random (e.g. Gaussian) and , so at the start and the model is exactly the pretrained base: training begins from the known-good point and only departs as needed.
Parameter count: full fine-tuning trains params per matrix; LoRA trains . For , : that's vs M: a ~256× reduction, per matrix. QLoRA pushes this further by quantizing the frozen base to 4-bit, so even the frozen weights cost little memory, enabling fine-tuning of very large models on a single GPU.
Key takeaway: LoRA bets that task adaptation lives in a low-rank subspace: freeze , train with at init. The trainable fraction depends on rank and which layers are adapted. Merging into compatible weights removes the adapter branch, not the base model's inference cost.
| Full fine-tuning | LoRA | QLoRA | |
|---|---|---|---|
| Params trained | All selected base weights | per adapted matrix | Adapter parameters, as in LoRA |
| Base model stored | In original precision | Frozen, original precision | Frozen, 4-bit quantized |
| Adapter memory for optimizer states | Very large (Adam moments) | Tiny | Tiny |
| Extra adapter operations | None | None after dense merge; present if unmerged | Depends on quantization and serving implementation |
| Typical use | Max quality, big budget | Task adapters on one GPU | Fine-tuning very large models cheaply |
Read the implementation
1import torch
2from torch import Tensor, nn
3
4
5class LoRALinear(nn.Module):
6 def __init__(self, base: nn.Linear, r: int = 8, alpha: int = 16) -> None:
7 super().__init__()
8 self.base = base
9 for p in self.base.parameters():
10 p.requires_grad_(False) # freeze the pretrained weights
11 d_out, d_in = base.weight.shape
12 self.A = nn.Parameter(torch.randn(r, d_in) * 0.01)
13 self.B = nn.Parameter(torch.zeros(d_out, r)) # B = 0 -> starts as base
14 self.scale = alpha / r
15
16 def forward(self, x: Tensor) -> Tensor:
17 return self.base(x) + self.scale * (x @ self.A.T @ self.B.T)
18
19
20layer = LoRALinear(nn.Linear(4096, 4096), r=8)
21trainable = sum(p.numel() for p in layer.parameters() if p.requires_grad)
22total = sum(p.numel() for p in layer.parameters())
23print(f"trainable {trainable:,} / {total:,} ({100*trainable/total:.2f}%)")
24# trainable 65,536 / 16,846,848 (0.39%)Questions and trade-offs
- Conceptual: Why can a low-rank update match full fine-tuning? (Low-rank updates worked well on the tasks studied in the LoRA paper. Their adequacy depends on the task, chosen layers, and rank; not every useful full update is low-rank.)
- Implementation: Why initialize B to zero? (So ΔW = BA = 0 at the start: training begins exactly from the pretrained model and only deviates as needed, which is stable.)
- Applied: Roughly how many parameters does LoRA train versus full fine-tuning for a d×k matrix? (r(d+k) vs d·k: often <1%, e.g. ~256× fewer for d=k=4096, r=8.)
- Systems-level: What does QLoRA add, and why does it matter? (It 4-bit-quantizes the frozen base so even the frozen weights are cheap to hold: enabling fine-tuning of very large models on one GPU.)
- Failure modes: Does LoRA add inference latency? (An unmerged adapter adds operations. Merging W' = W + (α/r)BA into compatible dense weights removes that extra branch; quantized deployment needs separate consideration.)
Check your understanding
Without looking: write the LoRA reparameterization with shapes of A and B, explain why B is initialized to zero, and give the parameter count versus full fine-tuning. Then state how to remove the inference overhead. Check against Stage 3.
Sources and further reading
- Hu et al., LoRA: low-rank updates, initialization, and merging.
- Dettmers et al., QLoRA: quantized frozen weights and adapter training.
Oliver Perrin
Machine Learning Engineer · Founder, LiminalML
LiminalML brings together concept explanations, mathematical examples, and working visuals. Find more writing by Oliver on the LiminalML Substack.
Keep exploring the idea.
Ask a follow-up in a guided session, work through the self-check, or open the visual in Studio to build on it.
Related concepts
Deep Learning
Transformer Architecture
Follow attention, feed-forward layers, normalization, and residual connections through a Transformer block.
LLMs
RLHF (Reinforcement Learning from Human Feedback)
Distinguish demonstrations, preference comparisons, reward models, and policy updates. Compare SFT, PPO, and DPO visuals.
Deep Learning
Attention Mechanisms
Turn compatibility scores into attention weights, then combine values. Explore the geometry, normalization, and limits of the mechanism.