← All tutorials

Adapters

Adapting a frozen model to a new task by training small added modules.

On this page

1 Bottleneck adapters 2 LoRA (low-rank adaptation) 3 Compacter (Kronecker-factored adapters) 4 Results on sentiment classification 5 Other parameter-efficient fine-tuning methods 6 LoRA in practice 7 Pitfalls 8 Combining adapters with quantization 9 References

How do you teach a large model a new task without retraining the whole thing?

Full fine-tuning updates every parameter of a transformer, a stack of blocks where the positions of a sequence first exchange information and then each position is processed on its own, and keeps one full copy per task. This page uses RoBERTa-base as its example model: 12 layers with hidden size 768 and 125 million parameters, the learned numbers that make up its weights (BERT-base has the same shape and 110 million).

Worse, Adam, the optimizer that runs training, keeps extra state for every parameter:

GPU memory needed just to hold the weights and the optimizer state.
P + 2P = 3P
The weights take P, the model size in parameters, and the Adam state takes 2P. Adam adds 2 numbers per parameter, a running average of its gradient and a running average of its squared gradient. Weights plus Adam state take about three times the model size in GPU (graphics processing unit) memory, and the gradients add one more P.

And the model learns the new task and starts forgetting the old ones. That is catastrophic forgetting.

Adapters do the opposite: freeze the big model, add small trainable modules, and train only those (Figure 1).

Full fine-tuninga full copy per taskmodel for task 1model for task 2model for task 3Adaptersone frozen copyfrozen modelnever updatedadapters123trainedfrozen
Figure 1. One frozen model serves every task; each task adds only a small adapter.

Adapters train like ordinary fine-tuning, on every step of the new task's data, and the state a task leaves behind is its adapter (Figure 2). The gradient still has to pass through the frozen backbone to reach the adapters, and at serving time LoRA, the second method here, folds away.

training stepservingfrozen backboneno gradient,no Adam stateadaptergradients + Adam stateforward and backward passesrun through everythingbottleneck adapteradds its layers toevery forward passLoRAfolds into W onceLoRA adds nothingat serving time
Figure 2. Training passes through the frozen backbone; only the adapter learns.

1 Bottleneck adapters

The original idea (Houlsby et al. 2019) is simple. Each transformer layer has an attention block, where each position mixes in information from the other positions, and a feed-forward block, which processes each position on its own. After each one you insert a small two-layer MLP (multilayer perceptron) with a bottleneck: the hidden state, the vector that carries one position through the layer, is 768 numbers wide, and the adapter projects it down to 64 numbers and back up to 768.

Input hidden states
Self-attention (frozen)
Adapter 1 (trainable)
Feed-forward (frozen)
Adapter 2 (trainable)
Output to next block
frozen, never updated trainable adapter

Figure 3. Only the orange adapters learn; blue stays frozen.

With adapters the base stays frozen and a new task never revisits the old ones.

Transferring BERT (Bidirectional Encoder Representations from Transformers, a large pretrained language model) to GLUE (a standard suite of language-understanding tests), adapters land within 0.4% of full fine-tuning while adding only 3.6% parameters per task, against 100% for fine-tuning; the paper finds the same pattern across 26 text classification tasks.

The listing is the whole module; input and output are both hidden_dim wide.

import torch.nn as nn

class SimpleAdapter(nn.Module):
    def __init__(self, hidden_dim, bottleneck_dim):
        super().__init__()
        h, m = hidden_dim, bottleneck_dim
        self.down_proj = nn.Linear(h, m)
        self.relu = nn.ReLU()
        self.up_proj = nn.Linear(m, h)

    # x: (batch, seq, hidden_dim)
    def forward(self, x):
        z = self.relu(self.down_proj(x))
        # residual: frozen x plus update
        return x + self.up_proj(z)
  1. 1down_proj to the bottleneck, ReLU (negatives become 0, positives pass), up_proj back up.
  2. 2forward adds the input back, so the frozen signal passes untouched if the adapter does nothing.

A wider bottleneck adds capacity and parameters; a narrower one is cheaper but can underfit. On GLUE the paper picked 8, 64 or 256 per task (256 for the large MNLI, 8 for the small RTE), and fixing 64 cost 0.4 points on average.

The parameter count, with a hidden size of 768 and a bottleneck of 64; a bias is one learned number added to each output of a projection, on top of its weights.

one adapter = 99,136 numbersdown 49,152up 49,152+ 832 biases× 12 layers × 2 adapters = 2,379,264share of the 125M-parameter modelfrozen, never updated1.9% trainedthe other 98% frozen
Figure 4. Bottleneck adapters at hidden size 768, bottleneck 64.

Training two percent of the model leaves the other 98 percent where pretraining put it. That is why the model does not forget.

Training is the usual loop with one filter, so the optimizer keeps no state for the backbone. The learning rate is a placeholder.

for name, param in model.named_parameters():
    # all else frozen
    param.requires_grad = "adapter" in name
ps = model.parameters()
trainable = [p for p in ps if p.requires_grad]
optimizer = torch.optim.AdamW(trainable, lr=1e-4)
  1. 1Freezes every parameter whose name lacks "adapter".
  2. 2Gives the optimizer only the trainable adapter parameters.
frozen features, 768
64
down-project, trainable
64
ReLU
up-project, trainable
+
add back
out = frozen + update

Figure 5. Every trainable parameter sits in the two projections.

The adapter projects the frozen features down, applies a nonlinearity, projects them back up, and adds the result to its input as a small update.
h = Wup·σ(Wdown·x)
The frozen input x is projected down by Wdown to the bottleneck, passed through σ, then lifted by Wup; the result h is the learned update. The 64-number bottleneck is what keeps the adapter cheap: both matrices are thin, so the parameter count is about 2 × 768 × 64 instead of 768 × 768. The ReLU between them matters too, because without it the two matrices would collapse into one plain low-rank matrix. This line is the listing's down_proj, relu and up_proj in order.
y = x + h
The output is the input plus the update. Because the input passes straight through, an adapter whose weights are near zero leaves the frozen model's behaviour unchanged, so training starts from the pretrained model and only learns a correction. This line is the return in SimpleAdapter.
x the frozen input, untouchedWdown the down-projection, 768 numbers down to 64σ the ReLU nonlinearityWup the lift, 64 numbers back to 768h the learned updatey the output, frozen input plus the update· matrix times vector

2 LoRA (low-rank adaptation)

LoRA (low-rank adaptation, Hu et al. 2022) looks at what fine-tuning does to a weight matrix. It starts from the pretrained weight W and ends with W plus a big dense update, dense meaning its entries are filled in rather than mostly zero. The useful part of that update lives in a tiny subspace, a low dimensional slice of all possible updates, so LoRA learns it as two skinny matrices multiplied together.

Figure 1 from the LoRA paper: input x at the bottom feeds a frozen blue box of pretrained weights W on the left and two stacked orange parallelograms labeled A and B on the right, with a rank r brace between them; both paths combine at a plus sign into output h at the top.

Figure 6. The paper's Figure 1: "Our reparametrization. We only train A and B."

Reparametrization means writing the trained weight as the frozen W plus the learned BA detour, so the update lives in the two small matrices.

Freeze what the model already knows and learn only the change, as a detour through a tiny bottleneck.
W + BA
Read BA as one learned update: A projects the input down to rank r, B projects it back up, and the pair is added to the frozen pretrained weight W.

The r brace between the slabs is the rank, the entire budget of new parameters.

What LoRA trains for one weight matrix, against the full update.
r(d + k)
A holds r × d numbers and B holds k × r, where d is the input size, k the output size and r the rank.
d · k
What a full update trains for the same matrix. A smaller r means far fewer new parameters and a coarser approximation of the update.
dense W, 768 × 768 589,824 LoRA A and B, r = 8 12,288
Figure 7. At rank 8, LoRA trains about 2% of the matrix.

A starts as small random numbers from a bell curve centered at 0; Figure 6 names that curve N with spread σ (not the ReLU σ of Section 1), set by the code's * 0.01. B starts at zeros, so at step one the side path adds nothing to the pretrained model.

The code wraps a frozen linear layer self.linear with the side path through A then B.

import torch
import torch.nn as nn

class LoRALinear(nn.Module):
    def __init__(self, linear, rank=8, alpha=16.0):
        super().__init__()
        # frozen W
        self.linear = linear.requires_grad_(False)
        k = linear.out_features
        d = linear.in_features
        a0 = torch.randn(rank, d) * 0.01
        self.A = nn.Parameter(a0)
        # BA = 0 at start
        self.B = nn.Parameter(torch.zeros(k, rank))
        self.scale = alpha / rank

    # x: (..., d) -> (..., k)
    def forward(self, x):
        side = x @ self.A.T @ self.B.T
        return self.linear(x) + self.scale * side
  1. 1Keeps the original linear layer W frozen.
  2. 2Low-rank factors: A small random, B zero, so BA = 0 at start; scale is α/r.
  3. 3Output is the frozen path plus the scaled low-rank path.

Before serving, fold BA into W once and call the plain layer, so inference costs nothing extra:

# (k, r) @ (r, d) = (k, d)
delta = lora.scale * (lora.B @ lora.A)
with torch.no_grad():
    lora.linear.weight += delta
# serve this plain layer
merged = lora.linear
  1. 1Forms the full update BA, scaled.
  2. 2Adds it into W once, so serving uses one plain layer.

In practice LoRA wraps the query and value projections of each attention block and leaves the rest frozen. A projection is a learned matrix that turns each position into a new vector: the query says what a position is looking for, the key what it advertises, and the value what it passes along (see Attention). The listing wraps them in RoBERTa-base.

# RobertaModel, all frozen
model.requires_grad_(False)
for block in model.encoder.layer:
    attn = block.attention.self
    attn.query = LoRALinear(attn.query)
    attn.value = LoRALinear(attn.value)
# later modules stay trainable
  1. 1Freezes the whole RoBERTa encoder.
  2. 2Wraps the query and value projections of every attention block with LoRA.
one projection (q or v), rank 8 = 12,288A 6,144B 6,144× 12 layers × 2 projections = 294,912share of the 125M-parameter modelfrozen, never updated0.24% trainedthe rest frozen
Figure 8. LoRA on query and value in RoBERTa-base, rank 8.

The rank r sets those numbers. A task close to what the model already does, such as sentiment classification, needs little rank; a task far from it, such as reasoning or code, can keep using more.

Raising r gives the update more directions in which to change W, at r·(d + k) trainable numbers per matrix; lowering it can miss part of the update, and r = 0 is the frozen model. At r = min(d, k) the update is as free as full fine-tuning; LoRA's RoBERTa runs use r = 8 (its Table 9). In LoRA Table 6 (GPT-3 175B) r of 1 or 2 is enough, and WikiSQL and MultiNLI stay flat up to 64.

The second knob sets how strongly the learned update is added.
h = Wx + αr BAx
x is the layer input and h its output; α/r (scale in the code) scales BA. Raising α makes every step change the update BA more, much like a larger learning rate; lowering it shrinks every update, and at α = 0 the model stays frozen. Because α acts like the learning rate, the paper sets α to the first r it tries and does not tune it (the paper's Section 4.1).

At r = 64 LoRA trains as many parameters as a bottleneck adapter, still only a sixth of the dense matrix. Start at 8, go up only if the loss says so.

W frozen 768 × 768 + B 768 × r × A r × 768 r = 8 rank r
r = 8 · 12,288 params per projection

Figure 9. Illustrative curves; only the hard task keeps gaining.

A quarter of one percent of the model, and it usually lands within a point of full fine-tuning on classification-style tasks.

After the merge in the folding listing, the model runs exactly as fast as the original; bottleneck adapters cannot merge, because their nonlinearity blocks it, so their extra layers stay forever.

3 Compacter (Kronecker-factored adapters)

Adapters are already small. Compacter (Karimi Mahabadi et al. 2021) asks whether the adapter's own matrices can be cheaper. Compacter uses the Kronecker product: instead of storing a big dense matrix, store two small factors A and B (Compacter's own, not LoRA's) and let their product stand in for it.

In the Kronecker product A ⊗ B, each element of A scales a whole copy of B.

A10.50.2502 × 2⊗B3 × 3=A ⊗ B1 B0.5 B0.25 B0 B6 × 6
Figure 10. A 6 × 6 matrix from only 4 + 9 stored numbers.

One factor pair is enough to build a full adapter matrix:

A big adapter matrix from two small factors.
W768×64 = A8×8 ⊗ B96×8
W is one adapter weight matrix (not the frozen W of LoRA), ⊗ is the Kronecker product, and the subscripts are the shapes: every entry of A scales a whole copy of B.

Only the factors are stored, 8·8 + 96·8 = 832 numbers, against 768·64 = 49,152 for the dense matrix.

Compacter builds each adapter matrix as a sum of these products:

Compacter's adapter weight: shared slow factors times per-layer fast factors.
W = Σi=1..n Ai ⊗ Bi
The Ai are shared "slow" weights, reused across layers; the Bi are per-layer "fast" weights; Σ adds the n products.
Bi = si tiT
Each fast factor is rank one, built from a single pair of vectors si and ti, where the superscript T, the transpose, lays ti flat as a row.

Training only 0.047% of the model's parameters, it matches full fine-tuning on GLUE and beats it on SuperGLUE, GLUE's harder successor suite, and in low-resource settings, where only a little task data is available.

The listing uses one product with a full B; Compacter sums n of them and makes each B rank one. The big matrix is never built.

def kron_linear(x, A, B):
    # x @ (A ⊗ B) without building A ⊗ B
    # A: (m, n), B: (p, q), x: (batch, m*p)
    m, p = A.shape[0], B.shape[0]
    X = x.view(-1, m, p)
    Y = A.T @ X @ B
    # (batch, n*q)
    return Y.reshape(x.shape[0], -1)
  1. 1Reshapes each input row into an m by p matrix.
  2. 2Multiplies by the two small factors and flattens back: the same result as the full Kronecker product.
dense Houlsby adapter, 768 to 6498,304 weightsCompacter adapter, same shape1,600shared 8×8 A (64) + down B 96×8 (768)+ up B 8×96 (768)61× fewer parameters
Figure 11. The same 768-to-64 module, weight matrices only.

The cost is that the weight matrix is no longer fully free; it has to factor this way.

4 Results on sentiment classification

SST-2 (the Stanford Sentiment Treebank: movie-review sentences labeled positive or negative) accuracy for RoBERTa-base, as reported in the LoRA paper's Table 2. BitFit trains only the bias terms; AdapterDrop (Rücklé et al. 2021) is a bottleneck adapter that drops the adapters in the lowest layers. The full fine-tuning, BitFit and AdapterDrop numbers are ones LoRA took from prior work (Figure 12).

93%94%95%SST-2 accuracy (axis starts at 93%)Full fine-tuning125M trained94.8BitFit0.1M trained93.7AdapterDrop0.3M trained94.2 ± 0.1AdapterDrop0.9M trained94.7 ± 0.3LoRA, r = 8, q and v0.3M trained95.1 ± 0.2
Figure 12. LoRA Table 2; ± is the spread over random seeds.

LoRA trains 0.24 percent of the parameters and matches full fine-tuning on this task.

5 Other parameter-efficient fine-tuning methods

LoRA and Compacter are not the only way to keep the update small.

MethodWhat trainsWhen it helps
Prompt tuning (Lester et al. 2021)A short soft prompt: learned vectors placed in front of the real wordsVery large frozen models; it closes the gap to fine-tuning as models exceed billions of parameters
Prefix tuning (Li and Liang 2021)Learned key and value vectors set in front of the sequence in every attention layerGeneration tasks on large frozen models
BitFit (Ben Zaken et al. 2022)Only the bias termsThe smallest budget: 93.7% on SST-2 with 0.1M parameters in LoRA Table 2

6 LoRA in practice

A LoRA fine-tune is an ordinary training loop on a model whose chosen projections were swapped for LoRALinear, with an optimizer that holds only A, B and the task head. Microsoft's loralib packages the same steps: build the layer with lora.Linear (shown for one projection), freeze everything else in model, and save only the LoRA weights.

import torch
import loralib as lora

# replaces nn.Linear
q_proj = lora.Linear(768, 768, r=8)
lora.mark_only_lora_as_trainable(model)
ps = model.parameters()
trainable = [p for p in ps if p.requires_grad]
optimizer = torch.optim.AdamW(trainable, lr=1e-4)
# ... the usual training loop ...
# A and B only
sd = lora.lora_state_dict(model)
torch.save(sd, "lora.pt")
  1. 1loralib layer with rank 8, then freeze everything except the LoRA factors.
  2. 2Optimizer over the LoRA factors only.
  3. 3Saves only A and B.

7 Pitfalls

8 Combining adapters with quantization

Adapters shrink what is trained; Quantization, storing each weight with fewer bits, shrinks the frozen 98 percent that still sits in memory. QLoRA (Dettmers et al. 2023) combines the two: it trains LoRA adapters through a frozen base quantized to 4 bits, enough to fine-tune a 65B-parameter model on one 48 GB GPU. The attention projections LoRA wraps are taught in Attention.

9 References

Houlsby et al. Parameter-Efficient Transfer Learning for NLP. ICML 2019. arXiv:1902.00751

Hu et al. LoRA: Low-Rank Adaptation of Large Language Models. ICLR 2022. arXiv:2106.09685

Lester et al. The Power of Scale for Parameter-Efficient Prompt Tuning. EMNLP 2021. arXiv:2104.08691

Li and Liang. Prefix-Tuning: Optimizing Continuous Prompts for Generation. ACL 2021. arXiv:2101.00190

Ben Zaken et al. BitFit: Simple Parameter-efficient Fine-tuning for Transformer-based Masked Language-models. ACL 2022 (arXiv 2021). arXiv:2106.10199

Rücklé et al. AdapterDrop: On the Efficiency of Adapters in Transformers. EMNLP 2021. arXiv:2010.11918

Karimi Mahabadi et al. Compacter: Efficient Low-Rank Hypercomplex Adapter Layers. NeurIPS 2021. arXiv:2106.04647

Dettmers et al. QLoRA: Efficient Finetuning of Quantized LLMs. 2023. arXiv:2305.14314