XP: 0
⚙️ Part 1 · Module 8

LoRA, PEFT & Quantization

Fine-tuning a whole big model still needs a lot of GPU. LoRA trains a tiny set of "adapter" weights instead — often under 1% of the model — for nearly the same result. Add quantization to shrink the model itself, and you can fine-tune huge models on a single consumer GPU. This is how it's actually done today.
The problem

Full fine-tuning is expensive

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.

UMBRELLA CATEGORY

PEFT

The overall strategy: keep most or all base-model weights frozen and train a small, efficient addition.

LoRAprefix tuningprompt tuningadapters
CONTAINS →
ONE PEFT METHOD

LoRA

Add two trainable low-rank matrices, A and B, beside selected frozen weight matrices. Only these additions learn.

Not synonyms: every LoRA fine-tune is PEFT, but not every PEFT fine-tune uses LoRA. It is like saying “vehicle” versus “bicycle.”
Analogy — sticky notes on a textbook

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.

🎮 Game 1

Full fine-tune vs LoRA — the memory gap

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.

Trainable parameters & memory
model size: 7B parameters
Full fine-tune
7B
trainable params
~84 GB GPU
LoRA
4.2M
trainable params (~0.06%)
~16 GB GPU
The clever trick

How LoRA works: two tiny matrices

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.

python · the LoRA idea
# 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*d
Why it works: "rank"

The 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.

🎮 Game 2

The rank knob (r)

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.

A 1,000,000-param layer via LoRA
rank r = 8
Shrinking the model

Quantization — use fewer bits per number

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.

Analogy — rounding prices

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.

🎮 Game 3

Pick the precision

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.

Bits per weight → model size
Click a precision.
🎮 Game 4

Swap adapters on one base model

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.

One base + interchangeable adapters
The base stays; only the little adapter changes.
🧊 Base model (frozen, 7B) — stored once
+ one tiny adapter (a few MB):
👆 Click an adapter to load it.
In practice

LoRA in real code — train a movie-review adapter

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.

Mission: teach a frozen model to detect movie-review mood

Run the cells, then change the two custom reviews and see whether your adapter agrees with you.

python · colab cell 1 · install
# 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 accelerate
python · colab cell 2 · real LoRA training
import 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")
Real classroom run
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.
1.09%of parameters trained
↓ 46%loss: 0.5230 → 0.2805
74.3%unseen validation accuracy

Failure detective: why was the second review wrong?

“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.

What makes this PEFT + LoRA?

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.

About the Hugging Face warning

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.

The DistilBERT load report is expected

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.

🎮 Game 5 — spot what trains

In a LoRA setup, click each component and say whether it trains or stays frozen. Build the mental model of what's actually moving.

Trains or frozen?
Question 1 of 4
Field guide

Prompt, LoRA, QLoRA, or full fine-tuning?

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.

Prompt only

No training data · fastest · cheapest

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.

LoRA

Small adapter · base model in normal precision

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.

QLoRA

4-bit frozen base + LoRA adapter

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.

Full fine-tune

All weights train · maximum cost and control

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.

A common mistake

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.

🎮 Game 6

Which approach should you use?

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.

Match the method to the situation
Scenario 1 of 4
🎯 Check your understanding

Quick challenge

1. What does PEFT stand for / do?
2. How does LoRA reduce trainable parameters?
3. What does the rank r control in LoRA?
4. What does quantization do?
5. What is QLoRA?
Takeaway

What you learned

Full fine-tuning is memory-hungry. PEFT trains only a tiny set of new params. LoRA — the popular PEFT method — freezes the big weight 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.
🎓 Half 1 complete — you can build AND adapt models.

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.