Fine-tuning LLMs: LoRA vs Full Fine-tuning - Trade-offs and When to Use Each
Comprehensive guide to LLM fine-tuning techniques, comparing LoRA (Low-Rank Adaptation) with full fine-tuning, including performance metrics and cost analysis.

Fine-tuning is how you make LLMs work for your specific domain. But should you use LoRA or full fine-tuning? The answer is more nuanced than it seems.
The Fine-tuning Landscape
Modern LLMs have billions of parameters. Fine-tuning all of them is expensive:
GPT-3 (175B parameters):
- Full Fine-tuning:
├─ Memory: 700GB+
├─ GPU: 100+ A100s
└─ Cost: $100K+ per run
- With LoRA (1% params):
├─ Memory: 10GB
├─ GPU: 1 A100
└─ Cost: $100 per run
Enter: LoRA (Low-Rank Adaptation)
Understanding LoRA
The Concept
Instead of updating all weights, LoRA updates only low-rank decomposition matrices:
Standard Fine-tuning:
W (original weights) → W + ΔW (full gradient updates)
LoRA Fine-tuning:
W (frozen) → W + BA (low-rank updates)
where:
- B: m × r matrix (learned)
- A: r × n matrix (learned)
- r: rank (typically 8-64)
Mathematics
# Standard attention: y = W·x
# LoRA: y = W·x + B·A·x
# Where B·A has much fewer parameters than W
# Example with 768×768 weight matrix:
# Full: 768 × 768 = 589,824 parameters
# LoRA (rank=8): (768×8) + (8×768) = 12,288 parameters
# Reduction: 97.9% fewer parameters!
LoRA vs Full Fine-tuning: Head-to-Head
Performance Comparison
On GLUE benchmark (NLU tasks):
| Task | Full FT | LoRA-8 | LoRA-16 | LoRA-32 |
|---|---|---|---|---|
| MNLI | 91.2 | 90.8 | 91.0 | 91.2 |
| QQP | 92.3 | 91.9 | 92.1 | 92.3 |
| CoLA | 68.1 | 66.5 | 67.8 | 68.1 |
| Average | 84.3 | 83.1 | 83.8 | 84.2 |
Finding: LoRA-32 reaches 99.88% of full fine-tuning performance
Resource Requirements
Training 7B parameter model on 10K examples:
Full FT LoRA-8 LoRA-32 LoRA-64
Memory 24 GB 8 GB 10 GB 12 GB
Training Time 4 hours 45 min 50 min 55 min
GPU 1 × A100 1 × A100 1 × A100 1 × A100
Cost $9.60 $1.20 $1.25 $1.40
Parameters 7B 0.47M 1.9M 3.9M
Inference Performance
Original model latency: 100ms
Full Fine-tuned: 100ms (no change)
LoRA: 105-110ms (5-10% slower)
Explanation: LoRA adds a small linear operation
When to Use LoRA
✅ Use LoRA When:
-
Budget Constrained
- Cost: $1-2 per experiment vs $10-20 full FT
- Enables many experiments
-
Quick Experimentation
- 10x faster training
- Iterate quickly
-
Multiple Tasks
- LoRA adapters are small (1-5MB)
- Can load different adapters for different tasks
- Multi-task switching without reloading full model
-
Limited Hardware
- Single GPU instead of multi-GPU cluster
-
Production Efficiency
- Smaller adapters to store
- Faster loading
- Easy versioning
✅ Use Full Fine-tuning When:
-
Maximum Performance Needed
- 1-2% performance difference matters
- Medical/legal applications
-
Significant Domain Shift
- Custom domain very different from training data
- Needs extensive weight updates
-
No Hardware Constraints
- Already have compute cluster
- Cost not a factor
-
Long-context Changes
- Changing positional embeddings
- Model architecture modifications
Practical Implementation
LoRA Fine-tuning with Hugging Face
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model
import torch
# Model and tokenizer
model_name = "meta-llama/Llama-2-7b"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# LoRA configuration
lora_config = LoraConfig(
r=8, # Rank
lora_alpha=16, # Scaling
target_modules=["q_proj", "v_proj"], # Which layers
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# Get PEFT model
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 1,310,720 || all params: 6,738,415,616 || trainable%: 0.019
# Training
training_args = TrainingArguments(
output_dir="./lora-results",
num_train_epochs=3,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
gradient_accumulation_steps=4,
learning_rate=2e-4,
fp16=True,
save_strategy="epoch",
eval_strategy="epoch",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
data_collator=data_collator,
)
trainer.train()
# Save adapter
model.save_pretrained("./my-lora-adapter")
# Load and use
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained("./my-lora-adapter")
Multi-task LoRA Switching
from peft import PeftMixedModel
# Load base model once
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
# Load different adapters for different tasks
adapters = {
"translation": "./adapters/translation",
"summarization": "./adapters/summarization",
"qa": "./adapters/qa"
}
def infer(text, task):
model.load_adapter(adapters[task])
outputs = model.generate(tokenizer(text, return_tensors="pt"))
return tokenizer.decode(outputs[0])
# Use different adapters seamlessly
result1 = infer("Translate to French: Hello", "translation")
result2 = infer("Summarize: ...", "summarization")
Advanced LoRA Techniques
1. QLoRA - Quantized LoRA
Combine LoRA with quantization:
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
# Now train with LoRA
lora_config = LoraConfig(...)
model = get_peft_model(model, lora_config)
Result: 4-bit quantization + LoRA = 7B model on 6GB VRAM!
2. DoRA - Decomposed LoRA
Mix full fine-tuning with LoRA:
# Weight = m × (V + BA)
# where m is trainable magnitude
lora_config = LoraConfig(
r=8,
use_dora=True, # Enable DoRA
target_modules=["q_proj", "v_proj"]
)
Benefit: Better performance (99% vs 99.88%) at minimal cost
Cost-Benefit Analysis
Scenario: Fine-tune GPT-3.5 on Customer Support
Goal: Improve customer support response quality
Option 1: Full Fine-tuning
├─ Cost: $500 (compute)
├─ Time: 2 days
├─ Performance: +2.1% accuracy
└─ Iterations possible: 1-2
Option 2: LoRA
├─ Cost: $50 (compute)
├─ Time: 4 hours
├─ Performance: +2.0% accuracy
└─ Iterations possible: 20+
Decision: LoRA wins
- Can iterate 20 times with same budget
- Try 20 different approaches
- Find better solution
- Still 10x cheaper
Multi-Adapter Deployment
Production setup:
# Kubernetes deployment with multiple LoRA adapters
deployment:
model:
base: meta-llama/Llama-2-7b
adapters:
- name: customer-support
path: s3://adapters/customer-support
memory: 150MB
- name: technical-docs
path: s3://adapters/technical-docs
memory: 150MB
- name: billing
path: s3://adapters/billing
memory: 150MB
# Total size: base model 13GB + adapters 450MB
# Instead of: base model × 3 = 39GB
# Savings: 65%
When LoRA Falls Short
⚠️ LoRA limitations:
- Architectural changes: Adding layers needs full FT
- Token classification: Position-specific tasks → weaker
- Very different domains: Sometimes needs 50+ rank
- Vision-language: Multi-modal fusion needs full FT
Recommended Approach
1. Start with LoRA (rank=8)
- Budget: $100
- Time: 1 hour
- If good enough → Done!
2. If underperforming, try LoRA (rank=32)
- Budget: $200
- Time: 2 hours
- If good enough → Done!
3. If still underperforming, try full FT
- Budget: $2K
- Time: 1 day
- Maximum performance
Conclusion
LoRA is the default choice for most applications because:
- ✅ 95%+ performance of full fine-tuning
- ✅ 10-20x cheaper
- ✅ 10-20x faster
- ✅ Massively more flexible (multi-task)
- ✅ Production-ready
Use full fine-tuning only when the 1-2% performance gain is worth the 10-20x cost increase.
For most real-world applications, LoRA is the smart choice.