- Key idea
- Outer product u vᵀ is rank 1; rank = number of independent directions; SVD writes a matrix as a sum of rank-1 pieces by importance; truncating gives the best low-rank approximation; LoRA: ΔW = BA with r ≪ d
- Real world
- A times table is rank 1: every row is a multiple of 1, 2, 3, …, so 100 cells come from 20 numbers. A photo can be stored as a few dozen such pieces with little visible loss.
- In an LLM
- LoRA fine-tunes a 4,096 × 4,096 weight matrix by learning B (4,096 × 16) and A (16 × 4,096): 131,072 numbers instead of 16.8 million, 0.78%, and the update BA still has full size.
One pattern: the outer product
Multiply a column vector by a row vector and you get a whole matrix, the :
It is matrix multiplication with inner size 1: shape . Every row is the same row scaled by a number, and every column is scaled by a number. The whole table contains one pattern, and numbers are enough to rebuild all .
Rank: how many independent patterns
The of a matrix is the number of independent directions among its rows (it is always the same for its columns). Equivalently, it is the smallest number of outer products that add up to the matrix. An matrix has rank at most the smaller of and .
The transformations lesson gave the picture: a 2 × 2 matrix of rank 2 keeps the plane a plane; one of rank 1 squashes it onto a line, and its determinant is 0. In general, a rank- matrix sends everything into an -dimensional subspace, however big its input is.
The 10 × 10 multiplication table has 100 entries, but entry is just : it is the outer product of with itself, rank 1, rebuilt from 20 numbers (10 if you notice the two are the same). A spreadsheet of “price per item × number of items” for several products and quantities is the same: one pattern, rank 1. Real data is rarely exactly rank 1, but often close to a low rank, and that is what compression exploits.
Every matrix is a sum of rank-1 pieces
The (SVD) writes any matrix as
where the singular values say how big each piece is, and the vectors have length 1 and are at right angles to one another, and so are Keeping just the first pieces gives the closest matrix of rank to (the Eckart–Young theorem). Here is a picture as a matrix:
A rank- matrix of size needs numbers in this form (plus singular values) instead of . That saves a lot when is much smaller than and , and saves nothing when it isn’t.
LoRA: fine-tuning with a low-rank update
Fine-tuning a model means changing its weight matrices: . For a 4,096 × 4,096 matrix, has 16.8 million numbers, and a model has hundreds of such matrices. Storing gradients and optimiser state for all of them takes many times the model’s own memory.
(low-rank adaptation) keeps frozen and learns the change in factored form:
is a full-size matrix, but its rank is at most , and only and are trained. starts at zero, so training starts from exactly the original model.
In a model with width 4,096 (Llama 2 7B, Mistral 7B), one attention projection is 4,096 × 4,096 = 16,777,216 weights. A rank-16 LoRA learns (4,096 × 16) and (16 × 4,096): 131,072 numbers, 0.78% of the matrix. Applied to all the linear layers of a 7B model, a rank-16 LoRA trains about 40 million parameters, around 0.6% of the model, and the adapter file is about 80 MB in 16-bit numbers instead of 14 GB. After training, can be added into once, so the fine-tuned model runs exactly as fast as the original.
Are weight matrices low-rank?
Mostly not. GPT-2 small’s query matrix in layer 0 is 768 × 768 (all heads together); its first 92 SVD pieces hold half of its energy (the sum of squared singular values) and it needs 314 for 90%. A random matrix of the same size needs 140 and 391. So the trained matrix is more concentrated than noise, but nowhere near rank 16. LoRA works because the change needed for a fine-tune is close to low-rank, not the weights themselves. When it isn’t, for example when teaching a model a lot of new knowledge, full fine-tuning does better.
Facts
- . So with inner size has rank at most .
- : adding rank-1 pieces gives rank at most .
- In SVD form with orthogonal , (see rotations) and diagonal . Geometrically: rotate, stretch along the axes, rotate.
- Truncation error: . The widget’s “error” is the square root of this divided by .
In code
import numpy as np
u, v = np.array([1, 2, -1, 0]), np.array([2, 1, 0, -1, 1])
P = np.outer(u, v) # 4 × 5, rank 1
np.linalg.matrix_rank(P) # 1
U, s, Vt = np.linalg.svd(M, full_matrices=False)
k = 10
M_k = U[:, :k] * s[:k] @ Vt[:k] # best rank-k approximation
# LoRA with the peft library
from peft import LoraConfig, get_peft_model
cfg = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"])
model = get_peft_model(model, cfg)
model.print_trainable_parameters()In papers
- LoRA: Hu et al., 2021. Fine-tuning updates have low “intrinsic dimension”: Aghajanyan et al., 2020.
- QLoRA (LoRA on a 4-bit model): Dettmers et al., 2023; see NF4.
- Where LoRA falls short of full fine-tuning: Biderman et al., 2024.
- Starting LoRA from the top singular vectors: PiSSA, Meng et al., 2024.
- Best low-rank approximation: Eckart and Young, 1936.