Skip to content
AI360XpertBeta

LoRA & QLoRA

Freeze the pretrained model and train only two tiny matrices per layer that add a small correction to its output. QLoRA compresses the frozen weights to 4-bit so the whole job fits on one GPU.

Frozen weight W plus a rank-1 LoRA correction turns input [2, 3] into output [3, 5], while only the two small matrices train
Frozen weight W plus a rank-1 LoRA correction turns input [2, 3] into output [3, 5], while only the two small matrices train

Why Does This Exist?

You have one consumer GPU and a 7 billion parameter model you want to turn into a legal-document summarizer. Full fine-tuning updates every weight, and each weight drags along a gradient plus two optimizer states, so the training bill runs several times the size of the model itself. On your card, that job doesn't fit.

Here is the observation that saves you. You're not teaching the model English. It already knows grammar, facts, and how to follow instructions. You only need to steer it: this tone, this format, keep these details. LoRA (low-rank adaptation) turns that steering into something tiny you train, while the 7 billion weights stay frozen. QLoRA goes one step further and stores those frozen weights in 4-bit, so the part you never touch stops eating your memory.

The one idea you need first: this is still supervised fine-tuning. You show the model curated input-output pairs and compute gradients. The only change is which weights receive those gradients: not the base model, just your two small matrices. This page covers that adapter mechanism plus the 4-bit trick. The wider family of such tricks lives in PEFT methods, and compression on its own lives in post-training quantization.

Think of It Like This

A transparency sheet over a printed map

You don't reprint a city map when your commute changes. You lay a transparency sheet over it and draw your new route in pen. Look through the sheet and you see the original streets plus your correction, combined. The map was never redrawn.

LoRA works the same way. The frozen weights are the printed map. The two small matrices are the transparency: the only thing you draw on. At read time the model looks through both layers, so the output looks adapted even though the map underneath never changed.

Where it stops: a transparency can't add streets that aren't on the map. If the base model never learned an idea, no small correction will put it there. The sheet steers; it doesn't reteach.

How It Actually Works

Pick one weight matrix inside the model, say an attention projection. Full fine-tuning would rewrite it. LoRA freezes it and bolts on a tiny side path that learns the correction instead.

The low-rank update

Call the frozen matrix WW, with shape dd by kk (rows by columns). Call the layer input xx, a vector of length kk. LoRA adds two new matrices: AA with shape rr by kk, which squeezes the input down, and BB with shape dd by rr, which stretches it back up. The number rr is the rank: a small whole number you choose, far below dd and kk. The adapted output hh is the frozen path plus the side path:

h=Wx+αrBAxh = Wx + \frac{\alpha}{r}BAx

In plain words: WxWx is what the frozen model always computed. AxAx squeezes the input into rr numbers. BB stretches those back into a full-sized correction. The knob α\alpha (alpha) sets how strongly the correction counts, and dividing by rr keeps that strength stable when you try different ranks. In the worked example below we set α=r\alpha = r, so the scale equals 11 and h=Wx+BAxh = Wx + BAx.

Two details matter in practice. BB starts at zero, so on day one the correction is exactly zero and the model behaves like the untouched base. And only AA and BB ever receive gradients or optimizer state. WW is read-only from the first step to the last.

Count the trainable numbers. A full update to WW needs d×kd \times k values. The LoRA pair needs d×r+r×kd \times r + r \times k. Take d=4096d = 4096, k=4096k = 4096, r=8r = 8: the full update is 4096×4096=16,777,2164096 \times 4096 = 16{,}777{,}216 numbers, while LoRA needs 4096×8+8×4096=32,768+32,768=65,5364096 \times 8 + 8 \times 4096 = 32{,}768 + 32{,}768 = 65{,}536. That ratio is 65,536/16,777,216=0.0039062565{,}536 / 16{,}777{,}216 = 0.00390625, about 0.39%0.39\%. Same layer, same input, under half a percent of the trainable count.

Why so few numbers can steer so much

This rests on one bet: the change a pretrained model needs for a new style or task is far simpler than the model itself. Grammar, facts, and instruction-following are already in WW. Tone, format, and which details to keep live in a much smaller space, and that is what AA and BB span.

Name the trade-off plainly. You buy cheap training with limited reach. When the job needs a deep shift, genuinely new knowledge, or behavior the base model never learned, the small correction runs out of room and full fine-tuning (or better data, or a stronger base) is the honest answer. Don't use LoRA to teach what isn't there. Use it to redirect what is.

What QLoRA adds

LoRA alone still parks the whole frozen model in GPU memory at full precision, and for a 7B model that frozen bulk dominates the bill. QLoRA shrinks it: each frozen weight is stored in 4-bit instead of 16-bit, cutting the frozen bytes to roughly one quarter. The adapters AA and BB stay in higher precision, because gradient updates need fine steps that 4-bit rounding would destroy. A paged optimizer absorbs the brief memory spikes of a busy batch by parking excess optimizer state in CPU memory instead of crashing.

Neither half works alone. LoRA collapses the trainable count; QLoRA collapses the frozen footprint. Together they put a 7B adaptation on one consumer card.

Worked example

Keep the shapes tiny so you can check every step by hand. Let WW be the 22 by 22 identity, x=[2,3]x = [2, 3], A=[[2,−1]]A = [[2, -1]] (11 by 22, so r=1r = 1), B=[[1],[2]]B = [[1], [2]] (22 by 11), and α=r=1\alpha = r = 1:

Frozen path: Wx=[1×2+0×3, 0×2+1×3]=[2,3]Wx = [1 \times 2 + 0 \times 3,\ 0 \times 2 + 1 \times 3] = [2, 3].

Squeeze: Ax=2×2+(−1)×3=4−3=1Ax = 2 \times 2 + (-1) \times 3 = 4 - 3 = 1. One number, as r=1r = 1 promises.

Stretch: B(Ax)=[1×1, 2×1]=[1,2]B(Ax) = [1 \times 1,\ 2 \times 1] = [1, 2].

Add: h=[2+1, 3+2]=[3,5]h = [2 + 1,\ 3 + 2] = [3, 5].

The diagram above draws exactly this: frozen [2,3][2, 3] plus correction [1,2][1, 2] gives [3,5][3, 5]. At realistic scale the same addition holds, except the correction comes from 65,53665{,}536 trained values sitting beside 16,777,21616{,}777{,}216 frozen ones.

Code

The snippet below runs the worked example and the realistic parameter count side by side, so you can confirm both numbers yourself:

def matvec(m: list[list[int]], v: list[int]) -> list[int]:    return [sum(row[j] * v[j] for j in range(len(v))) for row in m]

def lora_forward(    W: list[list[int]],    A: list[list[int]],    B: list[list[int]],    x: list[int],) -> list[int]:    base = matvec(W, x)  # frozen path, never trains    low = matvec(A, x)  # squeeze to rank r    up = matvec(B, low)  # stretch back to full size    return [b + u for b, u in zip(base, up)]

W = [[1, 0], [0, 1]]x = [2, 3]A = [[2, -1]]B = [[1], [2]]
print(lora_forward(W, A, B, x))  # -> [3, 5]print(4096 * 4096)  # -> 16777216print(4096 * 8 + 8 * 4096)  # -> 65536

The forward pass prints [3,5][3, 5], matching the hand computation. The counts print 16,777,21616{,}777{,}216 versus 65,53665{,}536, matching the 0.39%0.39\% ratio in the prose.

Watch Out For

Swapping the base model under a trained adapter

An adapter is a correction relative to one exact set of frozen weights. Point it at a different checkpoint, or even the same checkpoint under a different quantization, and the correction lands on outputs it was never computed against.

The symptom is quiet: training looked fine, but evals sag or the style drifts after the swap, with no error anywhere. Pin the base checkpoint and its quantization config alongside every adapter you keep, and train one adapter per base. Treat a base change as a retrain, not a file swap.

Training and merging with different adapter scales

The α/r\alpha / r scale is part of the adapter, not decoration. Train with one value and merge with another and the correction either vanishes or shouts: the merged model ignores your tuning or overshoots into repetitive, over-styled output.

The fix is bookkeeping. Store α\alpha next to rr with the adapter weights, apply the identical scale at training and at merge time, and quick-check the merged model on a fixed prompt before you ship it.

The Quick Version

  • LoRA freezes every pretrained weight and trains only two small matrices, AA and BB, whose product is added to the frozen output.
  • With rank 8 on a 4096 by 4096 weight, LoRA trains 65,536 values instead of 16,777,216, which is about 0.39 percent.
  • The trick works because steering a pretrained model needs far fewer dimensions than pretraining it, not because small matrices can invent new knowledge.
  • QLoRA stores the frozen weights in 4-bit while keeping the adapters in higher precision, cutting frozen bytes to roughly one quarter.
  • A paged optimizer parks brief memory spikes in CPU memory, and that plus 4-bit storage is what fits 7B adaptation on a single consumer GPU.
  • An adapter belongs to one exact base checkpoint and quantization; a swap means a retrain.