Skip to the lesson
FixTheRobot Explore Robotics / LLM / LM6 Fine-Tuning for Classification / LM6-T3

LLM · Theory · LM6

LM6-T3 — Frozen and Trainable Parameters

You do not have to update all 124 million weights. The question is how many you can afford, and how many you need.

  • Lesson 3 of 5
  • Module 6: Fine-Tuning for Classification
  • Theory
  • Difficulty: Intermediate
  • About 8 minutes to read
  • Prerequisite: LM6-T2

Fine-tuning starts from a model that is already good at language. Updating every weight is the most flexible choice and also the most expensive: it needs memory for gradients and optimizer state for every parameter, and it risks erasing what pretraining learned. Freezing most of the model and training only a small part is cheaper and safer, at some cost in quality. This lesson shows the options, counts what each one trains and costs for GPT-2 small, and explains the mechanics in PyTorch. The notebook LM6-L2 compares them on a toy task, and Module 8 continues with parameter-efficient methods that sit between these extremes.

Learning objectives

After this lesson, you should be able to:

  • Describe head-only, partial and full fine-tuning and what each trains.
  • Freeze and unfreeze parameters in PyTorch and count the trainable ones.
  • Estimate the memory for weights, gradients and optimizer state under each strategy.
  • Choose a strategy from data size, task difficulty and budget.

The idea: spend the update budget where it counts

A pretrained backbone computes a representation of the text. The classification head reads that representation. If the representation already separates your classes, only the head needs to learn, and training a few hundred parameters on a small dataset is fast and hard to overfit. If the classes depend on features the backbone does not expose well, the head alone is not enough and some of the backbone must change.

Three fine-tuning strategiesThree columns, each showing a stack of the same layers: embeddings, block 1 to block 12, final norm, classification head. In head-only training only the head is trainable. In last-block training the last block, the final norm and the head are trainable. In full fine-tuning everything is trainable. Frozen layers are grey; trainable layers are coloured.Head onlyembeddingsblocks 1 … 11block 12final normhead1,538 trainableLast block + headembeddingsblocks 1 … 11block 12final normhead7.09 M trainableFull fine-tuningembeddingsblocks 1 … 11block 12final normhead124.4 M trainable
Head only, last block plus head, and full fine-tuning on a 12-block GPT. Grey layers are frozen.

The options

  • Head only (sometimes called linear probing): the backbone is frozen. Cheapest, safest against forgetting, and fastest because the frozen backbone needs no gradient. Quality depends entirely on the quality of the pretrained representation for your task. On hard tasks it can be far behind; on the toy task of the notebook it fails completely.
  • Partial fine-tuning: train the head and the last one or few blocks and the final norm. The upper layers are the most task-specific, so adapting them costs little and often recovers most of the quality of full fine-tuning.
  • Full fine-tuning: train everything. Most flexible and most expensive; needs the most care with the learning rate and data size.
  • Parameter-efficient methods (adapters, low-rank updates): keep the weights frozen and train small added modules. Quality close to full fine-tuning at a fraction of the memory (Module 8).

How freezing works

Every parameter has a flag requires_grad. When it is False, autograd does not compute a gradient for it, the optimizer never sees it, and it holds no optimizer state. If the whole backbone is frozen, backpropagation stops at the head and the backbone only runs forward. Freezing is therefore a saving of memory and compute as well as a constraint.

Learning rates by group

A randomly initialised head needs a larger learning rate (for example 10−3) than pretrained layers (10−5 to 5·10−5). Optimizers accept parameter groups with their own rates. Some recipes decay the rate layer by layer from top to bottom, so that lower layers, which hold more general features, change least.

Caching features

If the backbone is frozen and dropout is off, its output for a given text never changes. You can run the backbone once over the dataset, store the pooled vectors, and train the head on those vectors for many epochs at almost no cost. This makes head-only experiments, hyperparameter sweeps and error analysis very fast.

The mathematics: what each strategy costs

Let P be the total number of parameters and Ptrain the number that are trainable. In float32 with Adam, the memory for weights, gradients and optimizer state is

memory for weights, gradients and Adam state
M≈4⋅P+(4+8)⋅Ptrain  bytesM\approx 4\cdot P+(4+8)\cdot P_{\text{train}}\;\mathrm{bytes}
4·P
the weights, 4 bytes each; all parameters, frozen or not, are stored.
4·Ptrain
gradients, only for trainable parameters.
8·Ptrain
Adam’s two moving averages (4 bytes each), only for trainable parameters.
activations
not included: they depend on batch size and sequence length. With a frozen backbone and no gradient through it they are not stored at all.

For GPT-2 small (P = 124,412,160, a tied head) with a 2-class head:

GPT-2 small, float32 (activations extra)
StrategyTrainable parametersShareMemory (weights + gradients + Adam)
Head only1,5380.001%498 MB + 18 KB ≈ 498 MB
Last block + final norm + head7,088,6425.7%498 MB + 85 MB ≈ 583 MB
Full fine-tuning124,412,160100%498 MB + 1.49 GB ≈ 1.99 GB

The last block has 7,085,568 parameters (Lesson LM4-T5); adding the final norm (1,536) and the head (1,538) gives 7,088,642. Head-only training needs little more than the model itself, which is why it fits on any machine that can run the model.

Compute. The forward pass costs about 2·P FLOPs per token whatever you train. The backward pass costs about 4·P per token if gradients flow through the whole network, about 4·Pbelow less if the lower part is frozen and cut out of the backward pass. With only the head trainable no backward pass through the backbone is needed, so a training step costs about a third of a full step.

Capacity versus data. The more parameters you train, the more examples you need to avoid overfitting. A rough guide: head only works with tens to hundreds of examples, partial fine-tuning with hundreds to thousands, full fine-tuning with thousands and more, but task difficulty changes all three.

A worked example: freezing and counting in PyTorch

The code builds a small stand-in backbone (two blocks as linear layers) and a head, then applies the three strategies and counts the trainable parameters and the optimizer memory. Parameter groups with different learning rates are shown at the end.

import torch
import torch.nn as nn

d, K = 64, 2
backbone = nn.ModuleDict({
    "embed": nn.Embedding(100, d),
    "block1": nn.Linear(d, d),
    "block2": nn.Linear(d, d),
    "norm": nn.LayerNorm(d),
})
head = nn.Linear(d, K)
model = nn.ModuleDict({"backbone": backbone, "head": head})

def set_trainable(model, mode):
    for p in model.parameters():
        p.requires_grad = False
    groups = [model["head"]]
    if mode in ("last", "all"):
        groups += [model["backbone"]["block2"], model["backbone"]["norm"]]
    if mode == "all":
        groups += [model["backbone"]["embed"], model["backbone"]["block1"]]
    for g in groups:
        for p in g.parameters():
            p.requires_grad = True
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

total = sum(p.numel() for p in model.parameters())
for mode in ("head", "last", "all"):
    n = set_trainable(model, mode)
    mb = (4 * total + 12 * n) / 1e6
    print(f"{mode:5s} trainable {n:6d} of {total}  ->  {mb:.2f} MB (weights + gradients + Adam)")
# head  trainable    130 of 14978 ...
# last  trainable   4418 of 14978 ...
# all   trainable  14978 of 14978 ...

# frozen parameters get no gradient
set_trainable(model, "head")
x = torch.randint(0, 100, (3,))
h = model["backbone"]["norm"](model["backbone"]["block2"](model["backbone"]["block1"](model["backbone"]["embed"](x))))
loss = model["head"](h).sum()
loss.backward()
print(model["backbone"]["block1"].weight.grad is None, model["head"].weight.grad is not None)   # True True

# parameter groups: a larger learning rate for the new head than for pretrained layers
set_trainable(model, "last")
opt = torch.optim.AdamW([
    {"params": model["head"].parameters(), "lr": 1e-3},
    {"params": list(model["backbone"]["block2"].parameters()) + list(model["backbone"]["norm"].parameters()), "lr": 3e-5},
], weight_decay=0.01)
print([g["lr"] for g in opt.param_groups])           # [0.001, 3e-05]

Only parameters with requires_grad = True are given to the optimizer here, so frozen weights hold no Adam state. If you pass all parameters, PyTorch skips those without gradients but you should still filter, so that nothing changes silently if a flag is flipped later.

Choosing

A rough decision guide
Ifthen
you have under a few hundred labelled examples and the task is close to language modelling (topic, sentiment)start with head only on cached features
head-only quality is not enough and you have thousands of examplesunfreeze the last blocks, with a lower learning rate for them
the task is far from the pretraining data (a new domain, code, another language)full fine-tuning, or a parameter-efficient method (Module 8)
memory is tighthead only, or low-rank adapters; avoid full Adam state

Engineering implications and limitations

Freeze before building the optimizer. Set requires_grad first, then create the optimizer from the parameters that require gradients. Changing the flags after the optimizer exists does not change its parameter list.

Keep frozen parts in eval mode. A frozen backbone with dropout still on gives noisy features and is not what you evaluate. Call backbone.eval() if it is fully frozen; layer norm and dropout behave differently in the two modes.

Use torch.no_grad() for the frozen part. Wrapping the backbone forward pass in no_grad avoids storing its activations at all, which is a large memory saving.

Verify what trains. Print the names of parameters with requires_grad and their count at the start of every run. Many silent failures come from layers that were meant to train and did not (or the reverse).

Weight tying and freezing. If the output head is tied to the token embedding, freezing one freezes the other. A classification head is new and separate, so this does not matter for classification; it matters in instruction tuning (Module 7).

Gradual unfreezing. Train the head first, then unfreeze one block at a time from the top while lowering the learning rate. It can stabilise training on small data at the cost of more phases.

Compare against the baseline. A logistic regression on cached pooled features is a head-only classifier; if full fine-tuning does not beat it by a meaningful margin on held-out data, the extra cost is not justified.

Common misconceptions

“Frozen layers do not run.”

They run in the forward pass; only their gradients and updates are skipped.

“Freezing always hurts quality.”

With little data it often helps, by preventing overfitting and forgetting.

“Memory depends only on the number of parameters.”

It also depends on how many are trainable (gradients, optimizer state) and on activations.

“One learning rate is fine for the whole model.”

The new head and the pretrained layers usually need different rates.

“Head-only training is useless for hard tasks.”

It is a useful baseline and sometimes enough, but on tasks far from pretraining it is typically behind.

Engineering takeaways

  1. Head only, partial and full fine-tuning train 0.001%, 5.7% and 100% of GPT-2 small’s parameters (1,538; 7,088,642; 124,412,160).
  2. Frozen parameters are stored but have no gradient and no optimizer state: M ≈ 4P + 12·Ptrain bytes in float32 with Adam.
  3. Set requires_grad before creating the optimizer, use parameter groups for different learning rates, and print what is trainable.
  4. Cache the pooled features of a frozen backbone to experiment on the head almost for free.
  5. More trainable parameters need more labelled data; parameter-efficient methods (Module 8) sit in between.

References and further reading

See Chapter 6 of the book (initializing a model with pretrained weights, and finetuning the model on supervised data, including the choice of layers to train).

Knowledge check

Three conceptual questions. Write an answer, then reveal the explanation. Your answers stay in your browser.

Question 1

A model has 350 million parameters and you fine-tune 10 million of them with Adam in float32. Roughly how much memory do the weights, gradients and optimizer state need?

Explanation

4 × 350M = 1.4 GB for the weights, plus 12 × 10M = 120 MB for gradients and Adam state: about 1.52 GB.

Question 2

You set requires_grad = False on the backbone after creating the optimizer with all parameters. Does the optimizer still update the backbone?

Explanation

No update happens for parameters with no gradient (their grad stays None, so the step skips them), but the optimizer still holds them in its parameter list and may have allocated state; filter parameters by requires_grad when creating the optimizer, and set the flags first.

Question 3

Why can a head-only classifier be trained on cached features much faster than a full fine-tuning run?

Explanation

The frozen backbone with dropout off gives the same features every time, so they can be computed once and stored; training then only touches the small head, with no backbone forward or backward pass per step.

What’s next

Next: preparing the labelled data and measuring the classifier with the right metrics.

Practice this lesson

Fine-Tune a Language Model for Classification

Add a classification head to a small GPT, compare frozen and fine-tuned variants and analyse the errors.

Next lesson

LM6-T4 — Dataset Preparation, Labels and Metrics

Prepare labelled data, balance classes and choose accuracy, precision, recall and F1 correctly.

Nothing below is required to finish this lesson.