ml-models / Language models / Fine-tuning and LoRA core

Fine-tuning and LoRA

A pretrained model knows a lot but does not yet do your task. Fine-tuning keeps training it on your data. This page compares training everything with training almost nothing: feature extraction, full fine-tuning, instruction tuning, and the parameter-efficient methods led by LoRA, whose low-rank update trains 4,194,304 of Llama 2 7B's 6,738,415,616 parameters. It ends with QLoRA, which fits that update on top of a 4-bit base.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Full fine-tuning versus feature extraction
  5. Instruction tuning
  6. Catastrophic forgetting
  7. LoRA, derived
  8. QLoRA
  9. Adapters, prefix tuning and prompt tuning
  10. Worked example
  11. A rank-1 LoRA update by hand
  12. LoRA on Llama 2 7B
  13. Memory for weights and optimizer state
  14. Masking the prompt in SFT
  15. Forgetting in one parameter
  16. Implementation
  17. Tradeoffs
  18. Connections
  19. Read first
  20. Read next
  21. References

Problem

Pretraining spends enormous compute to produce one general model. Every use of it afterwards, a support bot, a code reviewer, a classifier for legal clauses, needs a different behaviour. Training a new model per task from scratch throws the pretraining away. Continuing to train the whole model per task works, but each copy of a 7-billion-parameter decoder is about 13.5 GB in 16-bit, and training it with Adam needs several times that in GPU memory.

Two further problems come with training everything. A small task dataset can pull the weights far from where pretraining left them, and the model forgets what it knew: catastrophic forgetting. And a model that was only taught to continue text does not follow instructions; that has to be taught too. The question is how to adapt a large model cheaply, without breaking it, and keep many adaptations side by side.

Intuition

Think of the pretrained weights as an experienced employee. Feature extraction leaves them untouched and trains only a small head on their outputs, like asking for a report in a new format. Full fine-tuning retrains the whole person, which is powerful but expensive and can overwrite old skills. The parameter-efficient methods sit between: they freeze the original and train a small add-on.

LoRA's bet is that the change needed for a new task is simple even when the weights are not. A weight matrix in a 4096-wide model has 16.8 million entries, but the useful update may move it along only a few directions. So LoRA writes the update as a product of two thin matrices, B times A, with an inner dimension (the rank) of 8 or so. That is the low-rank approximation idea from the SVD, used not to compress a matrix but to restrict what training can change. Since B starts at zero, the model starts exactly where pretraining left it.

A LoRA layer: the frozen weight in parallel with a trainable low-rank path An input x of width k splits into two paths. The upper path is the frozen pretrained weight W zero, a large d by k square. The lower path is A, a thin r by k matrix that maps x down to r numbers, then B, a thin d by r matrix that maps them back up to d, then a multiplication by alpha over r. The two paths are added to give h of width d. B starts at zero, so the lower path contributes nothing at the start of training. Only A and B are trained. x width k W₀ d × k, frozen A: r × k r B: d × r B × α / r + h width d W₀x: what pretraining learned B starts at 0 → h = W₀x at step 0 only A and B get gradients

Mechanics

Full fine-tuning versus feature extraction

Feature extraction (a linear probe when the head is linear) freezes the network and trains only a new output layer on its final hidden states. It is cheap, cannot forget, and needs no backward pass through the body, but it can only use features that already exist. Full fine-tuning updates every weight, usually with a learning rate well below the pretraining one so the weights stay near their starting point; it is the most accurate when data is plentiful and the most expensive. In between, one can unfreeze only the top layers. Kumar et al. found that full fine-tuning with a randomly initialized head can distort the pretrained features while the head is still bad, and that training the linear probe first and then fine-tuning everything (LP-FT) holds up better out of distribution.

In PyTorch, freezing is module.requires_grad_(False), which sets requires_grad on every parameter; frozen tensors get no gradient and should be left out of the optimizer, so they cost no optimizer state.

Instruction tuning

A pretrained language model continues text; asked a question, it might continue with three more questions. Instruction tuning, also called supervised fine-tuning (SFT), trains it on pairs of an instruction and a good response, formatted with the chat template the model will see at inference. Wei et al. (FLAN) showed that fine-tuning on many tasks phrased as instructions improves zero-shot performance on tasks never seen in training, and InstructGPT used SFT on human-written demonstrations as the first stage before preference optimization.

The loss is the usual next-token cross-entropy, but only on the response tokens: prompt positions get the label −100, which is F.cross_entropy's default ignore_index, so they add nothing to the loss or its average. The model is trained to answer, not to reproduce the user's question.

Catastrophic forgetting

Gradient descent on task B moves the weights toward B's optimum with no regard for task A, and nothing in B's loss says A mattered. Remedies all keep the weights near the pretrained point: a small learning rate and few epochs; an L2 penalty toward the original weights; elastic weight consolidation (EWC, Kirkpatrick et al.), which weights that penalty by each parameter's Fisher information so important weights move least; mixing some original-style data into the fine-tuning set; and freezing most weights, which is what the parameter-efficient methods do structurally. Biderman et al. found that LoRA learns less of a new domain than full fine-tuning but also forgets less of the old one.

LoRA, derived

Take one linear layer with frozen weight W0 ∈ ℝd×k (PyTorch's nn.Linear stores it as [out_features, in_features] = [d, k]). Full fine-tuning learns an update ΔW of the same shape: dk numbers. LoRA constrains it to rank at most r by writing it as a product:

ΔW = (α / r) · B A,   B ∈ ℝd×r,   A ∈ ℝr×k,   r ≪ min(d, k)
h = W0x + (α / r) · B(A x)

Every column of BA is a combination of B's r columns, so its rank is at most r. The trainable count drops from dk to r(d + k). For a 4096 × 4096 attention projection at r = 8: 16,777,216 becomes 8 · 8192 = 65,536, 0.39% as many.

A starts random (Gaussian in the paper) and B starts at zero, so ΔW = 0 and training begins at the pretrained model; both cannot be zero, or no gradient would flow to either. The constant α / r scales the update so that changing r does not require retuning the learning rate much. Computing B(Ax) right to left never forms the d × k product. After training, W = W0 + (α/r)BA can be merged into a single matrix, so inference has no extra latency, and a different adapter can be swapped in by subtracting one product and adding another.

Hu et al. applied LoRA to the attention query and value projections and found very small ranks competitive; on GPT-3 175B it cut trainable parameters about 10,000-fold and training GPU memory about threefold relative to full fine-tuning with Adam. Later practice, including QLoRA, often applies it to every linear layer.

QLoRA

With LoRA, the frozen base still has to sit in GPU memory, 2 bytes per parameter in 16-bit. QLoRA (Dettmers et al.) stores the frozen base in 4 bits and trains LoRA adapters on top, backpropagating through the quantized weights, which are dequantized to bfloat16 for each matmul. Three pieces make it work:

  • 4-bit NormalFloat (NF4): a 4-bit data type whose 16 levels are quantiles of a normal distribution, matched to how pretrained weights are distributed. Each block of 64 weights shares one absmax scale (quantization).
  • Double quantization: the per-block scales are themselves quantized to 8 bits in blocks of 256, cutting their overhead from 32/64 = 0.5 bits per parameter to about 0.127.
  • Paged optimizers: optimizer state in unified memory that pages to CPU RAM during memory spikes.

The paper fine-tuned a 65B model on a single 48 GB GPU, and found adapters on all linear layers were needed to match 16-bit full fine-tuning.

Adapters, prefix tuning and prompt tuning

Adapters (Houlsby et al.) insert a small bottleneck MLP, down-project d → m, nonlinearity, up-project m → d, with a residual connection, twice in every transformer layer, and train only those. They are near-identity at initialization like LoRA, but they add layers to the forward pass, so they add inference latency and cannot be merged away.

Prefix tuning (Li and Liang) prepends a few trainable vectors to the keys and values of every attention layer: virtual tokens that every real token can attend to. Prompt tuning (Lester et al.) keeps only trainable embeddings at the input, a soft prompt; it becomes competitive with full fine-tuning as models grow to around ten billion parameters. Both use up context positions and are often less stable to train than LoRA.

ComponentShape/ParamsMeaningSource
nn.Linearweight [out, in], bias [out]The layer LoRA wraps; [d, k] in the formulas.linear.py 53–140
Linear.__init__Parameter(torch.empty((out_features, in_features)))Where the [out, in] layout is fixed.96–115
Linear.reset_parameterskaiming_uniform_(weight, a=sqrt(5))Default init, uniform in ±1/√in; a LoRA A needs its own, and B zeros.117–128
Linear.forwardF.linear(input, self.weight, self.bias)Reads self.weight, so a parametrization changes what it sees.130–134
register_parametrization(module, tensor_name, parametrization, *, unsafe=False)Makes module.weight return parametrization(original); checks shape and dtype are kept unless unsafe.parametrize.py 450–671
ParametrizationListoriginal, then the parametrizations in orderHolds the frozen original at module.parametrizations.weight.original.95–341
ParametrizationList.forwardx = self[0](self.original), then each later oneRecomputes the weight on every access unless cached.326–341
remove_parametrizationsleave_parametrized=TrueWrites the current value back as a plain parameter: the LoRA merge.696–796
cachedcontext managerComputes each parametrized tensor once per block instead of on every access.33–73
Module.requires_grad_requires_grad=TrueFreezes or unfreezes every parameter of a module.module.py 2938–2959
F.cross_entropyignore_index=-100Prompt tokens labelled −100 are skipped in SFT.functional.py 3478–3571
Where each fine-tuning method trains inside a transformer A stack from bottom to top: token embeddings, a decoder layer repeated N times, and the output head. Prompt tuning adds trainable soft-prompt vectors beside the token embeddings. Inside the layer, attention has q, k, v and o projections: LoRA attaches low-rank pairs to the projections, prefix tuning adds trainable key and value vectors to attention, and an adapter follows attention. The MLP has gate, up and down projections, with LoRA on all linear layers optional and a second adapter after the MLP. A linear probe trains only the head. Full fine-tuning trains everything. token embeddings soft prompt prompt tuning × N layers attention: q · k · v · o LoRA on q, v (+ k, o) adapter d → m → d MLP: gate · up · down LoRA on all linear (QLoRA) adapter d → m → d prefix K, V prefix tuning output head ← linear probe trains only this full fine-tuning: every box

Worked example

All numbers below come from tested helpers in ml_models_docs.py (ft_lora_toy, ft_model_params, ft_lora_trainable, ft_memory, ft_masked_loss, ft_forgetting). The Llama 2 7B count matches its published size, and the LoRA counts match what the Hugging Face PEFT library reports for the same settings.

A rank-1 LoRA update by hand

A frozen 4 × 3 layer W0 with rows [1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 1], input x = [2, 1, 1], rank r = 1, α = 2, so α/r = 2. After some training, A = [1, 0, -1] and B = [0.5, 0, -0.5, 1]ᵀ.

StepValue
At initialization, B = 0: h = W0x[2, 1, 1, 4]
Ax (1 × 3 times 3 → 1 number)[1]
(α/r) · B(Ax)[1, 0, -1, 2]
h = W0x + side path[3, 1, 0, 6]
ΔW = (α/r)BA[1, 0, -1] [0, 0, 0] [-1, 0, 1] [2, 0, -2]
Merged W = W0 + ΔW[2, 0, -1] [0, 1, 0] [-1, 0, 2] [3, 1, -1]
Merged Wx[3, 1, 0, 6]

The merged and unmerged outputs agree, which is why LoRA has no inference cost after merging. Every row of ΔW is a multiple of A = [1, 0, -1], so its rank is 1: the update can only respond to the input direction A picks out (here, the difference between the first and last input) and write along the direction B picks. It stores 7 numbers instead of 12; at 4096 × 4096 the same trick stores 65,536 instead of 16,777,216.

LoRA on Llama 2 7B

Llama 2 7B has width 4096, 32 layers, an MLP of width 11008 with gate, up and down projections, and a 32,000-token vocabulary with separate input and output embeddings: 202,383,360 parameters per layer and 6,738,415,616 in all. Trainable LoRA parameters by target and rank:

Targetsr = 1r = 4r = 8r = 16r = 64
q, v524,2882,097,1524,194,3048,388,60833,554,432
q, k, v, o1,048,5764,194,3048,388,60816,777,21667,108,864
all linear2,498,5609,994,24019,988,48039,976,960159,907,840

The q, v column at r = 8 is 32 layers × 2 matrices × 65,536 = 4,194,304, or 0.0622% of the model. Counts are linear in r, and the MLP matrices (11008 × 4096) dominate the all-linear row. Every method on one table:

MethodTrainable parametersShare of the model
Full fine-tuning6,738,415,616100.0000%
LoRA r = 8 on q, v4,194,3040.0622%
LoRA r = 8 on all linear layers19,988,4800.2966%
Adapters, bottleneck 64, two per layer33,820,6720.5019%
Prefix tuning, 10 positions per layer2,621,4400.0389%
Prompt tuning, 20 soft tokens81,9200.0012%
Linear probe, 2 classes8,1940.0001%

An adapter with bottleneck 64 costs 2 · 64 · 4096 + 64 + 4096 = 528,448 parameters including biases. Prefix tuning stores a key and a value vector per prefix position per layer; prompt tuning stores one embedding per soft token, once.

Memory for weights and optimizer state

Full fine-tuning with Adam in mixed precision needs about 16 bytes per trained parameter: 16-bit weights and gradients (2 + 2), a 32-bit master copy (4) and Adam's two moments (4 + 4), as in the ZeRO paper's accounting (mixed precision, optimizers). With LoRA, frozen weights need only their 2 bytes; with QLoRA, 4 bits plus 0.127 bits of scales. Activations are left out; they depend on batch and sequence length and are the same for all three.

Method (Llama 2 7B, LoRA r = 8 on q, v)BytesGiB
Full fine-tuning107,814,649,856100.4
LoRA, frozen 16-bit base13,543,940,09612.6
QLoRA, frozen 4-bit NF4 base3,543,249,5373.3

The idealized QLoRA figure counts every weight at 4 bits; real implementations usually keep the embeddings and output head in 16-bit, which adds about 0.5 GiB here. The pattern is the point: full fine-tuning needs a multi-GPU node, LoRA one large GPU, QLoRA a consumer card.

Masking the prompt in SFT

A seven-token example, with the probability the model gives each token when predicting it:

PositionTokenLabel (target, or −100)p(token)−ln p
0<user>-1000.023.912
1Name-1000.12.303
2a-1000.31.204
3prime-1000.052.996
4<assistant>-1000.90.105
5SevenSeven0.40.916 (counted)
6..0.80.223 (counted)

Only the two response tokens count: the loss is (0.916 + 0.223) / 2 = 0.570. Averaged over all seven tokens it would be 1.666, dominated by the user's prompt, which the model is not supposed to learn to write.

Forgetting in one parameter

A model with one weight w. Task A's loss is (w − 1)², minimized by pretraining at w = 1; task B's is (w − 3)². Fine-tuning on B with a penalty λ(w − 1)² toward the pretrained weight settles at w = (3 + λ · 1) / (1 + λ):

λwLoss on ALoss on B
03.0004.0000.000
0.52.3331.7780.444
12.0001.0001.000
41.4000.1602.560

With no penalty, B is solved perfectly and A's loss jumps from 0 to 4: forgetting. Every remedy picks a point on this trade-off. EWC sets a separate λ per weight from how much task A depends on it, and LoRA restricts the directions w can move in rather than the distance.

Why must LoRA initialize exactly one of A and B to zero, not both?

One zero makes ΔW = 0, so training starts at the pretrained model. Both zero would make both gradients zero: the gradient for B is proportional to A x and the gradient for A to Bᵀ times the output gradient, so neither would ever move.

How many trainable parameters does LoRA r = 16 on q, k, v and o add to Llama 2 7B, and could you have predicted it from the r = 8 q, v figure?

16,777,216. Doubling the rank doubles the count and doubling the number of 4096 × 4096 targets doubles it again: 4 × 4,194,304.

Implementation

LoRA with torch.nn.utils.parametrize: the parametrization receives the frozen weight and returns W0 + (α/r)BA, so nn.Linear needs no changes, and remove_parametrizations performs the merge.

import torch
import torch.nn as nn
import torch.nn.utils.parametrize as P


class LoRA(nn.Module):
    def __init__(self, out_features, in_features, r=8, alpha=16):
        super().__init__()
        self.A = nn.Parameter(torch.randn(r, in_features) * 0.01)
        self.B = nn.Parameter(torch.zeros(out_features, r))
        self.scale = alpha / r

    def forward(self, W):
        return W + self.scale * (self.B @ self.A)


def add_lora(model, names=("q_proj", "v_proj"), r=8, alpha=16):
    model.requires_grad_(False)
    targets = [m for n, m in model.named_modules() if isinstance(m, nn.Linear) and n.endswith(names)]
    for layer in targets:
        P.register_parametrization(layer, "weight", LoRA(layer.out_features, layer.in_features, r, alpha))
    return [p for p in model.parameters() if p.requires_grad]


def merge_lora(model):
    for m in model.modules():
        if P.is_parametrized(m, "weight"):
            P.remove_parametrizations(m, "weight", leave_parametrized=True)


trainable = add_lora(model)
optimizer = torch.optim.AdamW(trainable, lr=2e-4)
# ... train on (input_ids, labels) with prompt positions set to -100 ...
merge_lora(model)

The original weight moves to layer.parametrizations.weight.original and keeps requires_grad=False, so only A and B reach the optimizer. This version forms the full d × k weight on every forward pass (wrap the forward in P.cached() to do it once per pass when a weight is used more than once); production libraries such as PEFT instead wrap the layer and compute base(x) + scale * (x @ A.T @ B.T), which never builds the large product. The SFT loss is F.cross_entropy(logits.view(-1, V), labels.view(-1)), whose default ignore_index=-100 drops the prompt.

Tradeoffs

  • Full fine-tuning has the most capacity and is the reference for quality on large, shifted datasets, at full memory cost, one full model copy per task, and the most forgetting.
  • Feature extraction is cheapest and cannot forget, but cannot learn features the base lacks.
  • LoRA trains a fraction of a percent, stores a few megabytes per task, merges to zero inference cost, and forgets less; it can trail full fine-tuning when the new task is far from pretraining. Rank, α and which layers to target are the knobs.
  • QLoRA cuts base-model memory about fourfold again, at the cost of dequantization work every step and an adapter trained against quantized weights; merging back into a 16-bit model is then an approximation.
  • Adapters are modular but add latency; prefix and prompt tuning are tiny but use context positions and train less reliably, especially on smaller models.
  • Instruction data quality matters more than quantity: SFT copies the style and the mistakes of its demonstrations.

Connections

References

  • Hu et al. LoRA: Low-Rank Adaptation of Large Language Models. ICLR 2022. arXiv:2106.09685
  • Dettmers et al. QLoRA: Efficient Finetuning of Quantized LLMs. NeurIPS 2023. arXiv:2305.14314
  • Houlsby et al. Parameter-Efficient Transfer Learning for NLP. ICML 2019. arXiv:1902.00751
  • Li and Liang. Prefix-Tuning: Optimizing Continuous Prompts for Generation. ACL 2021. arXiv:2101.00190
  • Lester, Al-Rfou and Constant. The Power of Scale for Parameter-Efficient Prompt Tuning. EMNLP 2021. arXiv:2104.08691
  • Wei et al. Finetuned Language Models Are Zero-Shot Learners (FLAN). ICLR 2022. arXiv:2109.01652
  • Ouyang et al. Training language models to follow instructions with human feedback (InstructGPT). NeurIPS 2022. arXiv:2203.02155
  • Kirkpatrick et al. Overcoming catastrophic forgetting in neural networks. PNAS 2017. arXiv:1612.00796
  • Kumar et al. Fine-Tuning can Distort Pretrained Features and Underperform Out-of-Distribution. ICLR 2022. arXiv:2202.10054
  • Biderman et al. LoRA Learns Less and Forgets Less. TMLR 2024. arXiv:2405.09673
  • Touvron et al. Llama 2: Open Foundation and Fine-Tuned Chat Models. 2023. arXiv:2307.09288
  • Rajbhandari et al. ZeRO: Memory Optimizations Toward Training Trillion Parameter Models. SC 2020. arXiv:1910.02054
  • PyTorch v2.15.0: Linear, register_parametrization, remove_parametrizations, cached, cross_entropy.