Home / Fundamentals / Post-training & Alignment

Post-training & AlignmentMidQuestion 2 of 7

How does LoRA work? Work through its parameter and memory savings, and explain QLoRA.

30-second answer

LoRA freezes W and learns ΔW = (α/r)·BA with A (r × d_in) random and B (d_out × r) zero-initialized, so training starts from the base model and the update can be merged afterwards with no inference cost. Each adapted matrix adds r(d_in + d_out) parameters: for a 7B model with r = 16 on all linear layers, about 40M (~0.6%), so optimizer state drops from ~112 GB to under 1 GB. Adapters are megabytes and can be served per tenant. QLoRA keeps the frozen base in 4-bit NF4 and adapters in bf16, fitting 65B fine-tuning on one 48 GB GPU.

Try answering out loud before reading on. The walkthrough below is paced for a 40-minute interview round.

0–5 min Clarify and frame

Framing: "I'll give the formulation and why low rank is plausible, the initialization and scaling details, compute parameter and memory savings, show code, then QLoRA and serving."

5–15 min Core explanation

Fine-tuning updates are empirically low in "intrinsic dimension": the change needed to adapt a large model lies close to a low-dimensional subspace. LoRA constrains each weight update to rank r:

h = W x + (α / r) · B (A x)
W frozen (d_out × d_in);  A: r × d_in (random init);  B: d_out × r (zero init)

Scroll sideways to see the whole diagram.

A LoRA-adapted linear layerComponents: Input x (d_in); W (frozen) (d_out × d_in); A (trainable) (r × d_in · random init); B (trainable) (d_out × r · zero init); h = Wx + (α/r)·BAx (merge: W + (α/r)BA). Flow: Input x to W (frozen); Input x to A (trainable); A (trainable) to B (trainable) (r-dim); W (frozen) to h = Wx + (α/r)·BAx; B (trainable) to h = Wx + (α/r)·BAx (× α/r).Input xd_inW (frozen)d_out × d_inA (trainable)r × d_in · random initB (trainable)d_out × r · zero inith = Wx + (α/r)·BAxmerge: W + (α/r)BAr-dim× α/r
A LoRA-adapted linear layer. The pre-trained weight W stays frozen. A trainable low-rank path projects the input down to r dimensions with A and back up with B; B starts at zero so training begins from the base model. The two paths are summed, and after training BA can be merged into W.
class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r=16, alpha=32, dropout=0.05):
        super().__init__()
        self.base = base.requires_grad_(False)
        self.A = nn.Parameter(torch.empty(r, base.in_features))
        self.B = nn.Parameter(torch.zeros(base.out_features, r))
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
        self.scale, self.drop = alpha / r, nn.Dropout(dropout)

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

    def merge(self):
        self.base.weight.data += self.scale * (self.B @ self.A)
  • B = 0 means the model is unchanged at step 0.
  • α / r scaling keeps the effective update size stable as r changes.

15–25 min Deep dive: arithmetic

Per adapted matrix: r · (d_in + d_out) parameters.

Llama-2-7B shape (h = 4096, 32 layers, MLP 11,008), r = 16, all linear layers:

attention q, k, v, o: 4 × 16 × (4096 + 4096) = 524k
MLP gate, up, down:   3 × 16 × (4096 + 11008) = 725k
per layer ≈ 1.25M  × 32 layers ≈ 40M trainable (~0.6%)

Memory comparison:

Full fine-tuning LoRA (bf16 base) QLoRA (4-bit base)
Base weights 14 GB (trainable) 14 GB (frozen) ~3.5–4 GB
Gradients + Adam (16 B/param on trainable) ~98 GB more (112 total) ~0.64 GB ~0.64 GB
Activations Same Same Same

So a 7B LoRA fine-tune fits on one 24–48 GB GPU, and QLoRA fits larger models: the QLoRA paper fine-tuned a 65B model on a single 48 GB GPU.

QLoRA details: NF4 (4-bit levels at quantiles of a normal distribution), double quantization (quantizing the per-block scales too), paged optimizers to survive memory spikes; compute happens in bf16 after dequantizing each block on the fly.

25–32 min Trade-offs and choices

  • LoRA learns less and forgets less than full fine-tuning. For style, format and narrow tasks it matches full fine-tuning; for large shifts (new language, continued pre-training on a new domain, heavy code), full fine-tuning tends to win.
  • Modules: all linear layers > attention-only at equal budget.
  • Rank: 8–64; increasing rank beyond what the data supports doesn't help.
  • LR: ~10× the full fine-tuning LR.
  • Serving: merge for one task (no overhead), or keep adapters separate to serve many tenants on one base with batched multi-adapter kernels; adapters are ~80 MB for the 7B example in bf16.

32–37 min Production example

A SaaS company needed per-customer writing styles for 300 customers. Full fine-tunes would mean 300 model copies. Instead they trained an r = 16 LoRA per customer (~80 MB each) and served them all from one base model per GPU pool with multi-adapter batching, loading adapters on demand with an LRU cache. Cost was close to serving the base model alone.

37–40 min Likely follow-ups

  • Why initialize B to zero and not A? One of them must be zero to start from the base model; with A random, gradients flow to B from the first step.
  • What's DoRA? Decomposes weights into magnitude and direction and applies LoRA to the direction; often slightly better.
  • Does merging change quality? No, it's algebraically identical (up to rounding); with QLoRA, merging into a 4-bit base requires care.

What interviewers listen for

  • Correct formulation with init and scaling.
  • Parameter and memory arithmetic.
  • When LoRA is not enough.

Red flags

  • "LoRA reduces inference cost" (it doesn't, unless serving many adapters on one base).
  • Not knowing what's frozen.