In Module 7, fine-tuning updated all the model's weights. For a 7-billion-parameter model, that means storing gradients and optimizer states for all 7 billion — often tens of GB of GPU memory, out of reach for most people. And every fine-tune produces a full copy of the model.
PEFT means Parameter-Efficient Fine-Tuning. It is a broad family of techniques that adapt a model while training only a small number of parameters. LoRA means Low-Rank Adaptation. It is one specific, popular method inside the PEFT family.
The overall strategy: keep most or all base-model weights frozen and train a small, efficient addition.
Add two trainable low-rank matrices, A and B, beside selected frozen weight matrices. Only these additions learn.
Full fine-tuning = rewriting the entire textbook for each new course. LoRA = leaving the textbook untouched and adding a few sticky notes in the margins. The book's knowledge stays; your small notes adapt it. Cheap, fast, and you can peel off one set of notes and stick on another.
Slide the model size and watch what each approach needs. LoRA trains a fraction of a percent of the weights, so its memory stays tiny even as the model grows huge.
Instead of updating a big weight matrix W directly, LoRA freezes W and adds a small learned "side path": two skinny matrices A and B. Their product B·A has the same shape as W, but A and B together have far fewer numbers. During fine-tuning, only A and B are trained.
# original: output = x @ W (W is frozen, huge)
# LoRA: output = x @ W + x @ (B @ A) * scale
# └── the only trained part ──┘
# A: (d × r), B: (r × d), where rank r is TINY (e.g. 8)
# so B@A has W's shape but only 2*d*r params instead of d*dThe insight (from the LoRA paper): the change a fine-tune makes to W is "low-rank" — it can be captured by a much smaller matrix. The rank r is the knob: small r (like 8) = tiny & cheap; bigger r = more capacity. A 1000×1000 matrix (1M params) becomes two 1000×8 matrices = 16K params — 60× fewer, same shape.
Slide the rank r for a 1000×1000 weight matrix and watch how few parameters LoRA actually trains vs. the full matrix. Even at r=64 it's a tiny fraction.
The other half of the trick. A model's weights are usually stored as 32-bit or 16-bit numbers. Quantization stores them with fewer bits — 8-bit or even 4-bit — cutting memory 4–8× with only a small quality loss. Combined with LoRA, this is QLoRA: fine-tune a huge model on one consumer GPU.
Storing 3.14159265 (full precision) vs. 3.14 (quantized). You lose a little accuracy, but each number takes far less space — and for a model with billions of weights, that saving is enormous. Most of the time the model barely notices.
Choose how many bits per weight and see the memory for a 7B model — plus the quality trade-off. 4-bit (QLoRA) is the popular sweet spot.
The superpower of LoRA: one big frozen base model, many tiny adapters. Store the base once, then swap in whichever small adapter you need — legal assistant, code helper, pirate — instantly. Click adapters to swap.
This complete Colab exercise uses Hugging Face peft to add LoRA to a compact DistilBERT classifier. It downloads real Rotten Tomatoes reviews, trains only the adapter parameters, evaluates accuracy, tests your own reviews, and saves the small adapter.
Run the cells, then change the two custom reviews and see whether your adapter agrees with you.
# Colab currently includes an old optional torchao package that conflicts
# with recent PEFT. Basic LoRA does not use torchao, so remove it.
!pip -q uninstall -y torchao
!pip -q install -U transformers datasets peft accelerateimport torch
from torch.utils.data import DataLoader
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
)
from peft import LoraConfig, TaskType, get_peft_model
device = "cuda" if torch.cuda.is_available() else "cpu"
model_name = "distilbert/distilbert-base-uncased" # maintained 67M model
# 1. Load a real sentiment dataset and keep a small classroom-sized subset.
# Use the full Hub repository ID; the old "rotten_tomatoes" shortcut can
# produce an HfUriError with newer datasets/huggingface_hub versions.
dataset = load_dataset("cornell-movie-review-data/rotten_tomatoes")
train_data = dataset["train"].shuffle(seed=42).select(range(1200))
valid_data = dataset["validation"].select(range(300))
# 2. Load the tokenizer from the same maintained DistilBERT repository.
tokenizer = AutoTokenizer.from_pretrained(model_name)
def tokenize(batch):
return tokenizer(batch["text"], truncation=True, max_length=128)
train_data = train_data.map(tokenize, batched=True, remove_columns=["text"])
valid_data = valid_data.map(tokenize, batched=True, remove_columns=["text"])
train_data = train_data.rename_column("label", "labels")
valid_data = valid_data.rename_column("label", "labels")
collator = DataCollatorWithPadding(tokenizer=tokenizer, return_tensors="pt")
train_loader = DataLoader(train_data, batch_size=16, shuffle=True, collate_fn=collator)
valid_loader = DataLoader(valid_data, batch_size=32, collate_fn=collator)
# 3. Load the base model. At this point it is an ordinary DistilBERT classifier.
base_model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=2
)
# 4. Choose LoRA, which is ONE method provided by the PEFT library.
# DistilBERT calls its attention projections "q_lin" and "v_lin".
lora_config = LoraConfig(
task_type=TaskType.SEQ_CLS,
r=8, # adapter capacity
lora_alpha=16, # scales the adapter update
lora_dropout=0.05,
target_modules=["q_lin", "v_lin"],
)
model = get_peft_model(base_model, lora_config).to(device)
model.print_trainable_parameters() # verify that only a small fraction trains
# 5. This is the familiar training loop. Gradients exist only for the
# LoRA adapter and classification head; the base transformer stays frozen.
optimizer = torch.optim.AdamW(
(p for p in model.parameters() if p.requires_grad),
lr=5e-4,
)
model.train()
for epoch in range(3):
running_loss = 0.0
for batch in train_loader:
batch = {key: value.to(device) for key, value in batch.items()}
optimizer.zero_grad()
output = model(**batch)
output.loss.backward()
optimizer.step()
running_loss += output.loss.item()
print(f"epoch {epoch + 1} | loss {running_loss / len(train_loader):.4f}")
# 6. Evaluate on reviews the adapter did not train on.
model.eval()
correct = total = 0
with torch.no_grad():
for batch in valid_loader:
batch = {key: value.to(device) for key, value in batch.items()}
predictions = model(**batch).logits.argmax(dim=-1)
correct += (predictions == batch["labels"]).sum().item()
total += predictions.numel()
print(f"validation accuracy: {correct / total:.1%}")
# 7. Fun test: replace these with your own difficult or sarcastic reviews.
reviews = [
"A clever, warm movie I would happily watch again.",
"Two hours of my life that I will never get back.",
]
inputs = tokenizer(reviews, padding=True, truncation=True, return_tensors="pt").to(device)
with torch.no_grad():
probabilities = model(**inputs).logits.softmax(dim=-1).cpu()
for review, probability in zip(reviews, probabilities):
label = "POSITIVE" if probability[1] > probability[0] else "NEGATIVE"
confidence = probability.max().item()
print(f"{label:8} {confidence:.1%} | {review}")
# 8. Save only the adapter files, not another copy of the base model.
model.save_pretrained("movie-mood-lora-adapter")trainable params: 739,586 || all params: 67,694,596 || trainable%: 1.0925 epoch 1 | loss 0.5230 epoch 2 | loss 0.3705 epoch 3 | loss 0.2805 validation accuracy: 74.3% POSITIVE 99.4% | A clever, warm movie I would happily watch again. POSITIVE 63.1% | Two hours of my life that I will never get back.
“Two hours of my life that I will never get back” is negative, but it expresses sentiment indirectly. With only 1,200 training reviews, the adapter may associate neutral words such as “life” and “back” with positive examples and miss the complete idiom.
This is valuable evidence: falling training loss proves that learning happened, while 74.3% validation accuracy and this failed example show that the learned rule does not generalize perfectly. More representative data is usually more helpful than merely adding epochs.
Try an obvious negative review first: A dull, tedious mess with terrible acting. Then test sarcasm and idioms to discover the model's boundary.
peft is the library implementing the broader PEFT approach. LoraConfig selects LoRA specifically. get_peft_model freezes the base and inserts LoRA matrices into the selected attention layers.
You are sending unauthenticated requests is only a rate-limit warning for this public dataset and model. It is not the cause of the error. You may add an HF_TOKEN for faster downloads, but this exercise does not require one.
UNEXPECTED vocab_* means the original masked-word prediction head is not needed here. MISSING classifier means a fresh two-class sentiment head was created. Training will learn that new head, so neither message is a failure.
In a LoRA setup, click each component and say whether it trains or stays frozen. Build the mental model of what's actually moving.
Use the lightest method that reliably changes the behavior you need. Training is useful for teaching repeatable behavior or domain patterns; it is not the best way to inject frequently changing facts.
Use when: the model already has the ability and only needs instructions, examples, formatting, or supplied context.
Real example: Turn support-ticket text into a fixed JSON schema, or summarize a document in an executive tone.
Use when: you have hundreds or thousands of examples and want a stable style, task, or domain behavior without updating the full model.
Real example: Teach one 7B base model separate adapters for medical note formatting, legal clause classification, and your company's support voice.
Use when: LoRA is appropriate, but the base model is too large to fit comfortably in GPU memory at 16-bit precision.
Real example: Fine-tune a 13B model from 2,000 cybersecurity incident reports on one 16–24 GB consumer GPU.
Use when: you have substantial high-quality data, strong compute, and evidence that adapter methods cannot reach the required quality.
Real example: A research lab continues pretraining a smaller model on a large new language corpus, changing knowledge throughout the network.
Do not fine-tune merely to add current facts such as today's prices, policies, or inventory. Use retrieval/RAG for changing knowledge. Fine-tune when you need to change how the model behaves.
You now know four options: prompt-only, full fine-tune, LoRA, QLoRA. Answer for each scenario — which fits best? This is the real decision you'll make on projects.
W and learns two small low-rank matrices (A, B) as a side path; the rank r tunes their size (often just 8). Quantization stores weights in fewer bits (4/8-bit) to shrink the model. Together = QLoRA: fine-tune huge models on one GPU. Bonus: one frozen base + many swappable few-MB adapters. In code it's ~5 lines with peft.Tensors → autograd → training → data → makemore → GPT → fine-tuning → LoRA/QLoRA. You now understand how models are built and how they're efficiently specialized in the real world. Half 2 shifts to building systems around models: embeddings for search, vector databases, RAG, and memory.
Next up → Module 9: Embeddings for Search — the bridge into Half 2. You know embeddings as meaning-vectors (fundamentals Module 4); now we use them as a search tool: finding text by meaning, not keywords. The foundation of vector databases and RAG.