Learn › Training & Alignment
·FREE·PAPER·17 min read

LoRA from Scratch — Fine-Tuning a Giant by Training Two Thin Matrices

Full fine-tuning of a 7B model needs over 100 GB of optimizer state for a job whose actual content is small. LoRA's claim is that the *change* a model needs is low-rank, so it can be stored as a product of two thin matrices, ΔW = BA. This article builds that from the cost problem, takes the equations apart one line at a time, explains why B starts at zero and what α/r is for, counts the parameters for a real model, and ends on the three decisions that actually matter: the rank, the target layers, and what merging does at inference time.

Key points

  • Full fine-tuning needs about 16 bytes per parameter, which for 6.74 billion parameters is on the order of 108 GB.
  • LoRA freezes WW and learns the update as the product of two thin matrices, ΔW=BA\Delta W = BA with r≪min⁡(d,k)r \ll \min(d, k).
  • BB starts at zero and AA is random, so at step 0 W′=WW' = W and the model begins as an exact copy of the pretrained one.
  • Update strength depends only on the ratio α/r\alpha/r, so raising rr without raising α\alpha shrinks the update.
  • At r=8r = 8 on q and v of a 7B model, trainable parameters are about 4.2M (0.062 %), and the saved adapter is about 16 MB.
ModalitytextTaskfinetune

LoRA: Low-Rank Adaptation of Large Language Models

Primary source — what this article is built on

undefined2021-06-17→undefined2026-09-235y 3mo later

LoRA: Low-Rank Adaptation of Large Language ModelsEdward J. Hu, Yelong Shen, Phillip Wallis et al. · 2021-06-17 · v2arXiv:2106.09685Paper page·PDF
Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-TuningarXiv:2012.13255Paper page·PDF
undefined

An important paradigm of natural language processing consists of large-scale pre-training on general domain data and adaptation to particular tasks or domains. As we pre-train larger models, full fine-tuning, which retrains all model parameters, becomes less feasible. Using GPT-3 175B as an example -- deploying independent instances of fine-tuned models, each with 175B parameters, is prohibitively expensive. We propose Low-Rank Adaptation, or LoRA, which freezes the pre-trained model weights and injects trainable rank decomposition matrices into each layer of the Transformer architecture, greatly reducing the number of trainable parameters for downstream tasks. Compared to GPT-3 175B fine-tuned with Adam, LoRA can reduce the number of trainable parameters by 10,000 times and the GPU memory requirement by 3 times. LoRA performs on-par or better than fine-tuning in model quality on RoBERTa, DeBERTa, GPT-2, and GPT-3, despite having fewer trainable parameters, a higher training throughput, and, unlike adapters, no additional inference latency. We also provide an empirical investigation into rank-deficiency in language model adaptation, which sheds light on the efficacy of LoRA. We release a package that facilitates the integration of LoRA with PyTorch models and provide our implementations and model checkpoints for RoBERTa, DeBERTa, and GPT-2 at https://github.com/microsoft/LoRA.


The cost problem that LoRA exists to solve

You have a 7-billion-parameter language model and a few thousand examples of the tone you want it to write in. The job is small. The bill is not.

Full fine-tuning means updating every one of those 7 billion numbers, and for each one the optimizer has to keep company. With Adam in mixed precision the standard tally per parameter is roughly 4 bytes of master weights, 4 bytes of gradient, and 8 bytes of Adam's two moment estimates — about 16 bytes per parameter, which for 6.74 billion parameters is on the order of 108 GB before you have stored a single activation. That does not fit on one 80 GB accelerator. Then, having paid for it, you get a full copy of the model as output: another 14 GB on disk per task, per customer, per experiment.

Something is obviously out of proportion. Teaching a model a tone, a format, or a domain vocabulary is not remotely as much information as the model already contains, yet full fine-tuning charges you as if it were. LoRA (Low-Rank Adaptation), introduced by Hu et al. in 2021, is the observation that you can pay in proportion to the change rather than in proportion to the model — and a concrete parameterisation that does it.

This article builds it from the ground: what "low rank" means and why anyone believed the update would be, the two-matrix substitution taken apart line by line, why one of the matrices starts at exactly zero, what the mysterious lora_alpha divides, the parameter count for a real model, and then the three knobs you will actually turn.

Prerequisite: rank, and what a low-rank matrix is missing

A quick definition, because everything below rests on it. The rank of a matrix is the number of linearly independent directions in it — the number of genuinely distinct rows (equivalently, columns), once you discard any that are just combinations of the others. A 4096×40964096 \times 4096 matrix can have rank up to 4096; it is low-rank if its rank is far below that.

The useful consequence is a factorisation. Any matrix M∈Rd×kM \in \mathbb{R}^{d \times k} of rank rr can be written as a product of a tall thin matrix and a short wide one:

M=B⏟d×r⋅A⏟r×kM = \underbrace{B}_{d \times r} \cdot \underbrace{A}_{r \times k}

In words: any rank-rr matrix can be rewritten, exactly, as the product of two thin matrices.

Count what that saves. Storing MM directly takes d×kd \times k numbers. Storing the pair takes dr+rk=r(d+k)d r + r k = r(d + k). For d=k=4096d = k = 4096 and r=8r = 8: 16,777,216 numbers become 65,536 — a factor of 256. And the arithmetic runs the other way too: whatever BB and AA contain, their product BABA cannot have rank above rr, because the information has to squeeze through an rr-dimensional bottleneck on the way. That is the constraint LoRA imposes, and the whole question is whether it is a constraint the task can live with.

FIG 1Move the rank r to rebuild a 28×28 matrix from two thin matrices. Compare r×56 parameters against the original 784 and watch how far the error falls even at small r — this is exactly the constraint LoRA places on ΔW

The intuition: the update is low-rank even though the model is not

Here is the leap. Nobody claims a pretrained weight matrix WW is low-rank — it is dense and uses its full capacity. LoRA's claim is about the difference between the fine-tuned weights and the pretrained ones:

ΔW=Wfine-tuned−Wpretrained\Delta W = W_{\text{fine-tuned}} - W_{\text{pretrained}}

In words: ΔW\Delta W is simply how far fine-tuning moved the weights.

The bet is that this is low-rank, or close enough. Why would it be? Because pretraining has already installed the general machinery — grammar, world knowledge, the ability to follow a pattern. Adapting to a downstream task does not rebuild that machinery; it reweights and steers it. Steering is a much lower-dimensional act than building.

That intuition has an empirical antecedent. Aghajanyan et al. (arXiv:2012.13255) measured the intrinsic dimensionality of fine-tuning: they constrained updates to a randomly chosen low-dimensional subspace and found that surprisingly few dimensions were enough to reach most of full fine-tuning's performance — and that larger pretrained models had lower intrinsic dimension. If a task's update genuinely lives in a small subspace, then forcing ΔW\Delta W to be low-rank is not throwing away something you needed; it is declining to pay for capacity the task never uses.

So the design follows: do not train WW. Train a low-rank ΔW\Delta W and add it.

The method, one line at a time

Line 1: the update is added, not overwritten

W′=W+ΔWW' = W + \Delta W
(1)

In words: the fine-tuned weights are the original weights plus a correction, and the original weights are left alone.

WW is the pretrained weight matrix, shape d×kd \times k. W′W' is the weight the fine-tuned model uses. ΔW\Delta W is the correction. Nothing has been gained yet — this is just naming the difference — but it sets up the key move: WW is never modified. It stays exactly as pretraining left it, and all learning happens inside ΔW\Delta W.

Line 2: the correction is forced to be low-rank

ΔW=BA,B∈Rd×r, A∈Rr×k, r≪min⁡(d,k)\Delta W = B A, \qquad B \in \mathbb{R}^{d \times r},\ A \in \mathbb{R}^{r \times k},\ r \ll \min(d, k)
(2)

In words: the correction can only be built by passing through a narrow channel of width rr.

This is the whole idea in one line. Read it right to left, the way the data flows: AA takes a kk-dimensional input and compresses it to rr dimensions; BB takes those rr numbers and expands them back out to dd. The number rr is the rank — the width of the bottleneck — and it is the one hyperparameter that defines LoRA's capacity. Because everything must pass through rr dimensions, rank⁡(BA)≤r\operatorname{rank}(BA) \le r is guaranteed by construction rather than by a penalty term.

Line 3: what actually runs in the forward pass

You never form ΔW\Delta W as a matrix. For an input xx, expand the product:

h=W′x=Wx+BAxh = W' x = Wx + BAx
(3)

In words: the layer's original output, plus the output of a small side path.

Two paths, summed. WxWx is the original layer, untouched and computed exactly as before. BAxBAx is the LoRA branch, evaluated as B(Ax)B(Ax) — first the cheap compression to rr numbers, then the expansion. The cost of the branch is r(d+k)r(d+k) multiply-adds against the original layer's dkdk, so at r=8r = 8 and d=k=4096d = k = 4096 the extra work is well under 1 %.

Line 4: freeze the base, train only the pair

min⁡A, B L(Wx+BAx)with W frozen\min_{A,\ B}\ \mathcal{L}\big(W x + B A x\big) \qquad \text{with } W \text{ frozen}
(4)

In words: to lower the loss you may move only AA and BB; WW is treated as a constant.

This is the line that produces every saving in the article. WW receives no gradient and no optimizer state; it is a constant. Gradients flow only into AA and BB, so the optimizer's Adam moments — the 8 bytes per parameter that dominated the bill — are allocated for r(d+k)r(d+k) numbers per layer instead of dkdk.

Note what does not get cheaper: the forward pass still runs through the full model, and activations must still be stored for backpropagation. LoRA cuts optimizer and gradient memory, not the base model's weight memory (you still load all 14 GB in bf16) and not the activation memory. This is the single most common misreading of what LoRA buys. The LoRA paper reports for GPT-3 175B a 10,000× reduction in trainable parameters and about a 3× reduction in GPU memory — the gap between those two numbers — more than three orders of magnitude — is exactly this distinction.

Line 5: initialisation — why BB starts at zero

AA is initialised randomly (Gaussian, or Kaiming-uniform in the reference implementation) and BB is initialised to all zeros. Therefore at step 0:

ΔW=BA=0⋅A=0⟹W′=W\Delta W = B A = 0 \cdot A = 0 \quad \Longrightarrow \quad W' = W

In words: at the moment training starts, the model produces exactly the same outputs as it would without LoRA.

The fine-tuned model begins as an exact copy of the pretrained one. No random noise is injected into a carefully trained network, so there is no initial loss spike and no need to warm back up to the base model's quality — training starts from the best available point and goes uphill from there.

Why zero only one of them, and not both? If AA and BB were both zero, the gradient of the loss with respect to each would be zero as well (each one's gradient is proportional to the other), and the pair would be stuck at zero forever — a dead symmetry. Zeroing exactly one breaks it: B=0B = 0 makes the output zero, while A≠0A \ne 0 keeps BB's gradient alive, so the first step moves BB off zero and the branch comes to life.

Line 6: the scaling factor α/r\alpha/r

In practice the branch is scaled:

ΔW=αr BA\Delta W = \frac{\alpha}{r}\, B A
(5)

In words: α\alpha sets how strong the correction is, and dividing by rr keeps that strength from drifting when you change the rank.

α\alpha (lora_alpha in every library) is a constant you set; it is divided by the rank. The reason is that BABA's typical magnitude grows with rr — a wider bottleneck sums more terms — so without the division, doubling the rank would also roughly double the effective size of the update, and your carefully tuned learning rate would no longer apply. Dividing by rr makes the learning rate approximately transferable across ranks, which is what turns rr into a capacity dial rather than a capacity-and-step-size dial.

The practical consequence is that only the ratio α/r\alpha/r matters for update strength. r=8, alpha=16 and r=16, alpha=32 have the same scale of 2 but different capacity. A frequent accident: someone raises rr from 8 to 64 to give the adapter more room, leaves α\alpha at 16, and silently shrinks the effective update by 8×, then concludes that higher rank "did nothing."

Counting it out on a real model

Take a 7B Llama-style model: hidden size d=4096d = 4096, 32 transformer layers. Apply LoRA at r=8r = 8 to the query and value projections, the paper's recommended pair. Each of those is 4096×40964096 \times 4096.

Per adapted matrix:

4096×8⏟B+8×4096⏟A=65,536 parameters\underbrace{4096 \times 8}_{B} + \underbrace{8 \times 4096}_{A} = 65{,}536 \text{ parameters}

In words: each 4096×4096 matrix needs only about 65 thousand trainable numbers.

Two matrices per layer, 32 layers:

65,536×2×32=4,194,304≈4.2M trainable parameters65{,}536 \times 2 \times 32 = 4{,}194{,}304 \approx 4.2\text{M trainable parameters}

In words: across the whole model, training touches only about 4.2 million numbers.

Against 6.74 billion total, that is 0.062 % — roughly one parameter in 1,600. And the consequences compound:

Full fine-tuning LoRA (r=8r=8, q and v)
Trainable parameters 6.74 B 4.2 M
Optimizer + gradient state ~108 GB ~50 MB
Artifact saved per task ~14 GB ~16 MB
Base model weights in memory ~14 GB ~14 GB (frozen)

The last row is the honest part: LoRA does not shrink the model. What it eliminates is the optimizer state, and what it changes operationally is the last-but-one row — a 16 MB adapter instead of a 14 GB checkpoint. That is why a single served base model can host hundreds of task-specific adapters, swapped per request, which is the deployment pattern LoRA really unlocked.

In code the substitution is unremarkable, which is part of the appeal:

import torch
import torch.nn as nn

class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r=8, alpha=16):
        super().__init__()
        self.base = base
        for p in self.base.parameters():   # freeze the pretrained weights
            p.requires_grad = False
        self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01)
        self.B = nn.Parameter(torch.zeros(base.out_features, r))  # zero => ΔW = 0
        self.scale = alpha / r

    def forward(self, x):
        return self.base(x) + self.scale * (x @ self.A.T) @ self.B.T

Three details carry the whole method: requires_grad = False on the base, zeros for B, and alpha / r as the scale.

Three pitfalls, which are also the only three decisions

Choosing the rank

rr is a capacity ceiling, and both directions have a failure mode. Too small and the update physically cannot express what the task needs — the model underfits no matter how long you train, because you have constrained the solution to a subspace that does not contain a good answer. Too large and you lose LoRA's advantages (more parameters, bigger adapter, more room to overfit a small dataset) without necessarily gaining quality.

The empirical finding that surprises people is how small "enough" usually is. The LoRA paper reports that r=1r = 1 was already competitive on their tasks, and that the top singular directions of adapters trained at r=8r=8 and r=64r=64 overlapped heavily — the extra rank was largely spent on directions that contributed little. The rule of thumb that follows: start at r=8r = 8 or 1616 for style, format, and tone; consider 3232–6464 when teaching genuinely new domain content. And since raising rr without raising α\alpha shrinks the update, change them together or hold the ratio fixed on purpose.

If you see underfitting at low rank, do not raise rr first — check the learning rate. LoRA typically wants a learning rate an order of magnitude higher than full fine-tuning (say 1e-41\text{e-}4 rather than 1e-51\text{e-}5), because the parameters being trained are few, freshly initialised, and scaled down by α/r\alpha/r. A great many "LoRA cannot learn this" reports are a full-fine-tuning learning rate left in the config.

Choosing which layers to adapt

LoRA is applied per matrix, so you decide which ones. The original paper, working under a fixed parameter budget, found that spreading a small budget over WqW_q and WvW_v beat concentrating it on one matrix at higher rank — coverage mattered more than depth. Current practice has moved further in that direction: targeting all the linear projections, attention (q,k,v,oq, k, v, o) and the MLP ones, generally works better when budget is not the binding constraint, and this is what tools mean by target_modules="all-linear".

The trap is silent under-application. If target_modules names a pattern that matches nothing in your architecture — easy to do, since projection names differ between model families — you get a model that trains, logs a falling loss from the handful of matrices that did match, and learns far less than you intended. Print the trainable parameter count before every run (print_trainable_parameters() in PEFT). If it reads 0.01 % when you expected 0.5 %, your pattern missed.

A second trap concerns anything the task needs that no adapted matrix can reach. If you extend the tokenizer with new tokens, the embedding and output-head rows for those tokens are frozen and random, and no amount of attention-layer adaptation fixes them; those modules have to be trained (or the tokens avoided).

Merging at inference time

Because the branch is linear and added, it can be folded into the base weights once training is done:

Wmerged=W+αrBAW_{\text{merged}} = W + \frac{\alpha}{r} B A

In words: once training ends, the correction can be added into the original weights, leaving one ordinary matrix.

Do that and the LoRA matrices disappear. The served model has the original architecture, the original parameter count, and — this is LoRA's headline advantage over bottleneck adapter layers, which insert extra modules into the forward pass — exactly zero added inference latency.

Three things go wrong here in practice.

Merging into a quantized base loses precision. With QLoRA the base weights are stored in 4-bit while AA and BB train in higher precision. Adding a 16-bit update into a 4-bit tensor means re-quantising, and the small update can be partly rounded away, which shows up as a quality drop between your evaluation (adapter attached to a dequantised base) and production (merged). Either keep the adapter unmerged, or merge into the original full-precision weights rather than the quantized copy.

Merging forfeits the thing you wanted LoRA for. An unmerged adapter can be swapped per request, letting one loaded base model serve many tasks; a merged one is a whole new model. Merge when you serve one task at high volume, keep adapters separate when you serve many.

Two adapters do not simply add. W+ΔW1+ΔW2W + \Delta W_1 + \Delta W_2 is well-defined arithmetic but not a reliable composition of two behaviours: the updates were each trained assuming the other was absent, and their directions can conflict. Concatenating two rank-8 adapters also gives something of rank up to 16, not rank 8. Treat multi-adapter merging as an experiment to evaluate, not an operation that works by construction.

How it is used in practice

The people who touch LoRA day to day are machine learning engineers adapting a published foundation model to their own tone, output format, or domain vocabulary. The typical trigger is one of two situations: prompt engineering alone will not make the output format stable, or there is no room to store and serve a full copy of the model per task. The same mechanism is also widely used with image-generation models to teach a style or a character.

The settings you actually touch. In Hugging Face PEFT, a handful of LoraConfig fields is the whole surface:

Pitfalls that cause incidents if you do not know them.

Questions you get in interviews and design reviews.

Summary

For the trade-off against full fine-tuning in practice, see LoRA vs. Full Fine-Tuning. For a close reading of the original paper's own measurements, see the LoRA paper deep dive. If the rank and factorisation language felt shaky, start with Linear Algebra for AI.

Exercises

  1. A layer is d×k=4096×11008d \times k = 4096 \times 11008 (a Llama MLP projection). How many trainable parameters does a LoRA adapter at r=16r = 16 add, and what fraction is that of the original matrix?
  2. You train with r=8, lora_alpha=16 and the results are good. A colleague copies your config, sets r=64 to "give it more capacity", changes nothing else, and reports that the model barely moved from the base. What happened, and what value of lora_alpha restores the original update strength?
  3. Explain why initialising both AA and BB to zero would prevent learning entirely, while initialising BB alone to zero does not.
  4. Your evaluation runs the adapter attached to a 4-bit quantized base and scores well. You merge the adapter for production and the score drops. Give the likely cause and two ways to avoid it.
Answers
  1. BB is 4096×164096 \times 16 and AA is 16×1100816 \times 11008, so 16×(4096+11008)=241,66416 \times (4096 + 11008) = 241{,}664 parameters. The original matrix has 4096×11008=45,088,7684096 \times 11008 = 45{,}088{,}768, so the adapter is about 0.54 % of it.
  2. Only the ratio α/r\alpha/r sets the update scale. The original scale was 16/8=216/8 = 2; after the change it is 16/64=0.2516/64 = 0.25, an 8× weaker update, so the model stays close to the base. To restore a scale of 2 at r=64r = 64, set lora_alpha = 128. (Note that this restores the strength, not the original behaviour — the adapter now also has 8× the capacity, and the learning rate may want retuning.)
  3. The LoRA branch output is BAxBAx. The gradient with respect to BB is proportional to AA, and the gradient with respect to AA is proportional to BB. If both are zero, both gradients are zero, so neither ever moves — the pair is stuck at the origin permanently. Setting B=0B = 0 alone makes the branch's output zero (so the model starts as an exact copy of the pretrained one) while A≠0A \neq 0 keeps BB's gradient nonzero, so the first optimizer step lifts BB off zero and both matrices become trainable thereafter.
  4. Merging adds a higher-precision update into 4-bit weights, so the sum must be re-quantised and part of the small update is rounded away — the merged model is not the model you evaluated. Avoid it either by keeping the adapter unmerged at serving time (evaluate and serve the same configuration), or by merging into the original full-precision weights instead of the quantized copy. In every case, re-run the evaluation on the exact artifact you intend to ship.
🎬 Watch this explained (Manim, captioned)

References

  1. Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu et al.. (2021-06-17) LoRA: Low-Rank Adaptation of Large Language Models. arXiv:2106.09685Paper page·PDF
  2. Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning. arXiv:2012.13255Paper page·PDF

This article is written from the source paper above. Where they differ, the original is authoritative.

Published by TechAthletes · Editorial policy · How we cite · Corrections

Comments

Sign in to comment