Paper: "LoRA: Low-Rank Adaptation of Large Language Models" (Hu et al., 2021) ArXiv: https://arxiv.org/abs/2106.09685 Key Insight: Instead of fine-tuning all parameters, add small trainable matrices to frozen weights. Uses 99.9% fewer parameters, 10× less memory, and achieves competitive results.
The Problem: Fine-Tuning is Expensive
Full Fine-Tuning a 7B LLaMA:
- All 7B parameters are trainable
- Gradient memory: 7B × 4 bytes × 4 (FP32 forward + backward) = 112GB
- Can't fit on single GPU
LoRA Alternative:
- Freeze all parameters
- Add low-rank matrices (~0.1% parameters)
- Only 10-100MB GPU memory needed
LoRA Mechanism
For a weight matrix W, instead of learning ΔW directly, learn:
W' = W + ΔW
ΔW = A × B
where:
W is (d_out, d_in) frozen
A is (d_out, r) trainable
B is (r, d_in) trainable
r << min(d_out, d_in) (low rank)
The key: multiplying small matrices is much cheaper than updating large weights.
Implementation
import torch
import torch.nn as nn
class LoraLayer(nn.Module):
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
self.rank = rank
self.alpha = alpha
# Original weight (frozen)
self.weight = nn.Parameter(
torch.empty(out_features, in_features),
requires_grad=False
)
# LoRA matrices (trainable)
self.lora_A = nn.Parameter(torch.randn(in_features, rank) / rank)
self.lora_B = nn.Parameter(torch.zeros(rank, out_features))
def forward(self, x):
# x: (batch, ..., in_features)
# Original weight + low-rank update
# y = x @ W.T + (x @ A) @ B.T * (alpha / rank)
output = x @ self.weight.t()
output += (x @ self.lora_A) @ self.lora_B.t() * (self.alpha / self.rank)
return output
class LoraLinear(nn.Module):
"""Replace nn.Linear with LoRA version"""
def __init__(self, in_features, out_features, rank=8):
super().__init__()
self.linear = nn.Linear(in_features, out_features)
self.lora_A = nn.Parameter(torch.randn(in_features, rank) / rank)
self.lora_B = nn.Parameter(torch.zeros(rank, out_features))
self.rank = rank
def forward(self, x):
return self.linear(x) + (x @ self.lora_A) @ self.lora_B.t()
# Usage
base_model = AutoModelForCausalLM.from_pretrained('llama-7b')
# Freeze all parameters
for param in base_model.parameters():
param.requires_grad = False
# Replace some Linear layers with LoRA
for name, module in base_model.named_modules():
if isinstance(module, nn.Linear) and any(x in name for x in ['attention', 'mlp']):
# Convert to LoRA
lora_module = LoraLinear(
module.in_features,
module.out_features,
rank=8
)
# Copy weight
lora_module.linear.weight.data = module.weight.data.clone()
# Replace in model
parent = base_model
attr_name = name.split('.')[-1]
setattr(parent, attr_name, lora_module)
# Now fine-tune with tiny memory footprint
optimizer = torch.optim.Adam(
[p for p in base_model.parameters() if p.requires_grad],
lr=1e-4
)
for epoch in range(3):
for batch in finetune_dataset:
loss = base_model(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Memory Comparison
Fine-tuning LLaMA 7B:
Full Fine-Tuning:
- Trainable params: 7,000M
- Gradient memory: 112GB
- Inference: Full 7B loaded
LoRA (r=8):
- Trainable params: 7.1M (0.1% of total!)
- Gradient memory: 56MB
- Inference: Same as base (can merge LoRA into W)
Benchmark Results
Task: Text generation (WikiQA, WikiBio)
GPT-3 (175B): 100% (baseline)
Full FT on 7B: 87.5%
LoRA on 7B (r=8): 87.1% (only 0.4% worse!)
LoRA on 7B (r=16): 87.4% (competitive!)
LoRA matches full fine-tuning with 99.9% fewer parameters!
Why Low-Rank Works
Hypothesis: Fine-tuning also results in low-rank updates.
Intuition:
- Language models already have learned rich representations
- Fine-tuning for a new task is a small adaptation
- This adaptation likely lives in a low-rank subspace
- So we don't need to update all parameters, just the directions that matter
This is why LoRA works so well—fine-tuning naturally has low rank!
Our Analysis
LoRA is revolutionary because it makes LLM fine-tuning democratized. Before LoRA, you needed massive GPUs to fine-tune. Now, anyone can fine-tune a 7B model on a single GPU (24GB VRAM). This is the reason open-source LLM fine-tuning exploded.
Practical Tip: LoRA is now standard:
# HuggingFace integration
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=['q_proj', 'v_proj'],
lora_dropout=0.05
)
model = get_peft_model(base_model, config)
References
- Paper: LoRA (Hu et al., 2021)
- Extensions: QLoRA (4-bit + LoRA), DoRA (directional), etc.
- Implementation: https://github.com/microsoft/LoRA
Conclusion
LoRA proves that you don't need to update all parameters to adapt large models. This opened the door for widespread LLM customization. Understanding the low-rank hypothesis is key to understanding why this works and how to improve it. Next: we'll explore multimodal research and vision-language models.

