AI Knowledge Tree

LoRA, QLoRA, and DoRA

Freeze the pretrained weights, inject low-rank adapter matrices A and B, and train only their product delta W = BA; extend to 4-bit QLoRA and magnitude-direction DoRA.

⏱ 32 minPrerequisites:Supervised fine-tuning

Full fine-tuning updates all parameters of a pretrained model. For a 70B model at fp16, this requires 70×109×2≈14070 \times 10^9 \times 2 \approx 140GB of gradient memory alone -- far beyond a single GPU and impractical for most users.

Parameter-efficient fine-tuning (PEFT) methods update only a small subset of parameters while freezing the rest, a practical necessity for the supervised and preference fine-tuning stages[1]. LoRA (Hu et al., 2022) is the dominant method: it injects small, trainable rank-decomposition matrices alongside each frozen weight matrix[2].

LoRA: Low-Rank Adaptation

The core idea

For a pretrained weight matrix W0∈Rm×nW_0 \in \mathbb{R}^{m \times n} (e.g., the query projection in a transformer block), instead of training W0W_0 directly, freeze it and learn an additive low-rank update:

W=W0+ΔW=W0+BA,W = W_0 + \Delta W = W_0 + B A,

where B∈Rm×rB \in \mathbb{R}^{m \times r} and A∈Rr×nA \in \mathbb{R}^{r \times n}, with rank r≪min⁡(m,n)r \ll \min(m, n).

The forward pass becomes:

h=W0x+ΔWx=W0x+BAx.h = W_0 x + \Delta W x = W_0 x + B A x.

Only AA and BB are trained; W0W_0 is frozen.

Why low-rank?

The intrinsic dimensionality hypothesis (Aghajanyan et al., 2021): the change in weights during fine-tuning lies in a low-dimensional subspace. Empirically, setting rr to just 4-16 recovers most of the full fine-tuning performance.

Parameter count

For a weight matrix W0∈Rm×nW_0 \in \mathbb{R}^{m \times n} with LoRA rank rr:

  • LoRA parameters: r(m+n)r(m + n).
  • Full fine-tuning parameters: mnmn.
  • Reduction: r(m+n)/mn≈r/nr(m+n) / mn \approx r/n for m≈nm \approx n.

For a 70B model with d=8192d = 8192 and r=16r = 16:

  • Full fine-tuning: all 70B parameters.
  • LoRA (on Q, K, V, O projections in all layers): ≈16×2×8192×4×80≈84\approx 16 \times 2 \times 8192 \times 4 \times 80 \approx 84M parameters (∼0.1%\sim 0.1\% of total).

Initialization

  • A∼N(0,σ2)A \sim \mathcal{N}(0, \sigma^2) (small random).
  • B=0B = 0.

At initialization, ΔW=BA=0\Delta W = B A = 0, so the model starts identically to the pretrained model. This is crucial: the model initially produces the same outputs as before fine-tuning.

Scaling factor

In practice, the update is scaled by α/r\alpha / r:

h=W0x+αrBAx,h = W_0 x + \frac{\alpha}{r} B A x,

where α\alpha is a hyperparameter (often set equal to rr). This decouples the initialization scale from rr, making it easier to transfer the same learning rate across different ranks.

Where to apply LoRA

LoRA is applied to the attention weight matrices (WQ,WK,WV,WOW^Q, W^K, W^V, W^O) and optionally the FFN matrices. In practice, applying LoRA to all four attention projections gives the best quality-efficiency tradeoff.

Implementation

python · numpy
import numpy as np

class LoRALinear:
    def __init__(self, W0, rank, alpha=1.0, seed=0):
        """W0: pretrained weight (m, n). Frozen."""
        m, n   = W0.shape
        rng    = np.random.default_rng(seed)
        self.W0    = W0
        self.A     = rng.normal(0, 0.02, (rank, n))   # trainable
        self.B     = np.zeros((m, rank))               # trainable, init to 0
        self.scale = alpha / rank

    def forward(self, x):
        """x: (T, n) -> (T, m)"""
        base   = x @ self.W0.T                          # frozen path
        delta  = x @ self.A.T @ self.B.T * self.scale  # adapter path
        return base + delta

# Demo
m, n, r = 64, 32, 4
W0 = np.random.randn(m, n) * 0.02
layer = LoRALinear(W0, rank=r, alpha=r)

x = np.random.randn(10, n)  # 10 tokens, n features
h = layer.forward(x)
print("Output shape:", h.shape)
print("Delta (should be ~0 at init):", np.abs(layer.B @ layer.A).max().round(8))

QLoRA: quantized LoRA

QLoRA (Dettmers et al., 2023) extends LoRA to 4-bit quantized base models:

  1. Quantize the base model to 4-bit using NF4 (Normal Float 4), a data type designed to minimize quantization error for normally-distributed weights.
  2. Dequantize to bf16 in the forward pass (on-the-fly, per page of 64 weights).
  3. Apply LoRA in bf16 on top of the dequantized weights.
  4. Backpropagate only through the LoRA adapters.

Result: a 70B model fine-tunable on a single 48GB GPU (A6000), whereas full fine-tuning would require 8+ A100s at bf16.

Double quantization: quantize the quantization constants themselves (the per-block scaling factors) from fp32 to fp8, saving an additional ∼\sim0.4 bits per parameter.

DoRA: weight decomposition for LoRA

DoRA (Liu et al., 2024) decomposes the pretrained weight into magnitude and direction, and fine-tunes them separately:

W=m⏟magnitude⋅V∥V∥c⏟direction,W = \underbrace{m}_{\text{magnitude}} \cdot \underbrace{\frac{V}{\|V\|_c}}_{\text{direction}},

where m∈R1×nm \in \mathbb{R}^{1 \times n} is the column-wise norm and V/∥V∥cV/\|V\|_c is the normalized weight matrix. Fine-tuning uses LoRA for the direction component and a scalar for the magnitude:

W′=(m+Δm)⋅W0+BA∥W0+BA∥c.W' = (m + \Delta m) \cdot \frac{W_0 + BA}{\|W_0 + BA\|_c}.

DoRA consistently outperforms LoRA at the same rank, approaching full fine-tuning quality at ranks as low as r=16r = 16.

LoRA freezes the weight matrix and learns a low-rank update ΔW = BA, so trainable parameters are 2·d·r instead of d², a few percent even at generous ranks.
LoRA freezes the weight matrix and learns a low-rank update ΔW = BA, so trainable parameters are 2·d·r instead of d², a few percent even at generous ranks.

Merging LoRA weights

After fine-tuning, merge the adapter into the base model for inference:

Wmerged=W0+αrBA.W_{\text{merged}} = W_0 + \frac{\alpha}{r} B A.

No runtime overhead during inference -- the merged weight is identical in shape to W0W_0. Multiple LoRA adapters (for different tasks) can be merged independently or combined with task weights (LoRAHub).

Exercises

Exercise 1. A transformer applies LoRA to all four attention projections (WQ,WK,WV,WOW^Q, W^K, W^V, W^O), each of shape 4096×40964096 \times 4096, across 32 layers, with rank r=8r = 8. Count the trainable LoRA parameters and compare against full fine-tuning of just those projections.

Solution

Step 1. One LoRA-adapted matrix W0∈Rm×nW_0 \in \mathbb{R}^{m \times n} has parameters r(m+n)r(m + n) from B∈Rm×rB \in \mathbb{R}^{m \times r} and A∈Rr×nA \in \mathbb{R}^{r \times n}. Here m=n=4096m = n = 4096, r=8r = 8, so 8×(4096+4096)=8×8192=65,5368 \times (4096 + 4096) = 8 \times 8192 = 65{,}536 per matrix.

Step 2. There are 44 projections per layer and 3232 layers, so 4×32=1284 \times 32 = 128 adapted matrices. Total LoRA parameters:

128×65,536=8,388,608≈8.39M.128 \times 65{,}536 = 8{,}388{,}608 \approx 8.39\text{M}.

Step 3. Full fine-tuning of the same matrices costs mn=40962=16,777,216mn = 4096^2 = 16{,}777{,}216 each, so 128×16,777,216=2,147,483,648≈2.15B128 \times 16{,}777{,}216 = 2{,}147{,}483{,}648 \approx 2.15\text{B} parameters.

Step 4. The ratio is r(m+n)/mn=8×8192/40962=1/256≈0.39%r(m+n)/mn = 8 \times 8192 / 4096^2 = 1/256 \approx 0.39\%, matching the lesson's r/nr/n estimate for m≈nm \approx n.

Exercise 2. At initialization A∼N(0,σ2)A \sim \mathcal{N}(0, \sigma^2) and B=0B = 0, so ΔW=BA=0\Delta W = BA = 0 and the model reproduces the pretrained outputs. Show that the gradient of a scalar loss ℓ\ell with respect to AA is zero at this initialization, while the gradient with respect to BB is generally nonzero. Use ΔW=BA\Delta W = BA and the chain rule with G=∂ℓ/∂ΔWG = \partial \ell / \partial \Delta W.

Solution

Step 1. With ΔW=BA\Delta W = BA, the matrix chain rule gives ∂ℓ/∂A=B⊤G\partial \ell / \partial A = B^\top G and ∂ℓ/∂B=GA⊤\partial \ell / \partial B = G A^\top, where G=∂ℓ/∂ΔWG = \partial \ell / \partial \Delta W has the shape of ΔW\Delta W.

Step 2. At initialization B=0B = 0, so ∂ℓ/∂A=B⊤G=0\partial \ell / \partial A = B^\top G = 0. The AA adapter gets no gradient on the first step.

Step 3. The BB gradient is GA⊤G A^\top, with AA drawn from N(0,σ2)\mathcal{N}(0, \sigma^2), generally nonzero. So BB moves off zero first; once B≠0B \neq 0, the AA gradient B⊤GB^\top G becomes nonzero and both adapters train. This is why AA is initialized random (not zero): if both were zero, GA⊤=0G A^\top = 0 too and neither adapter would ever move.

Exercise 3 (code). Implement the LoRA forward pass with the α/r\alpha/r scaling in numpy and verify two facts: at initialization (B=0B = 0) the output equals the frozen base output, and after setting BB to a nonzero value the merged weight W0+(α/r)BAW_0 + (\alpha/r) BA reproduces the adapted forward pass exactly.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)
m, n, r, alpha = 64, 32, 4, 8.0
W0 = rng.normal(0, 0.02, (m, n))
A  = rng.normal(0, 0.02, (r, n))     # trainable
B  = np.zeros((m, r))                # init to 0
scale = alpha / r

x = rng.normal(0, 1, (10, n))        # 10 tokens

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

# At init B = 0 -> output equals the frozen base path.
out_init = forward(x, W0, A, B, scale)
assert np.allclose(out_init, x @ W0.T)

# After training B becomes nonzero; the merged weight matches the adapter forward.
B = rng.normal(0, 0.1, (m, r))
W_merged = W0 + scale * (B @ A)
assert np.allclose(forward(x, W0, A, B, scale), x @ W_merged.T)
print("merged-weight max abs error:",
      np.abs(forward(x, W0, A, B, scale) - x @ W_merged.T).max())

The init check confirms ΔW=0\Delta W = 0 at B=0B = 0, and the merge check confirms folding (α/r)BA(\alpha/r)BA into W0W_0 gives byte-equal outputs, so merging adds no inference overhead.

After fine-tuning, the adapter folds into the base weights and the model is ready to serve; the next lessons turn from parameter-efficiency to the preference signal that fine-tuning optimizes, starting with reward modeling.


References

  1. [1]Ouyang, L., et al. (2022). "Training language models to follow instructions with human feedback (InstructGPT)". NeurIPS. arXiv:2203.02155. The supervised and preference fine-tuning stages LoRA makes parameter-efficient.↩
  2. [2]Hu, E. J., et al. (2021). "LoRA: low-rank adaptation of large language models". ICLR. arXiv:2106.09685. The frozen-base, low-rank adapter method and its scaling-factor parameterization.↩