LoRA and Parameter-Efficient Fine-Tuning

By the end of this chapter you will understand why a fine-tune of a 7B model can be a 20 MB file, how to write the LoRA layer in ten lines, what every knob (rank, alpha, targets, learning rate) actually does, when LoRA quietly underperforms full fine-tuning, and how to choose among the dozen variants without reading the dozen papers.

Fine-tuning a model means updating its weights. Updating weights with AdamW means storing, per parameter, the weight, its gradient, and two optimizer moments: about 16 bytes in mixed precision. For a 70B model that is 1.1 TB of GPU memory before you have loaded a single training example. For a 7B model it is 112 GB, more than any single consumer GPU has. And at the end you get a second 140 GB copy of the model that differs from the original by a little.

Here is the question that leads to LoRA: does fine-tuning really need to touch every one of those billions of numbers, or is the change the model needs to learn much smaller than the model itself?

How Intrinsic Dimensionality Gave Birth To LoRA

Li et al. (2018) asked a strange question about neural networks: if you are only allowed to train in a random $d$-dimensional subspace of the full parameter space, how big does $d$ need to be before you reach (say) 90% of full-training accuracy? Concretely they set $\theta = \theta_0 + P\,\theta_d$ with $P$ a fixed random matrix from $\mathbb{R}^d$ into the full parameter space and trained only the $d$ numbers in $\theta_d$. They called the smallest working $d$ the intrinsic dimension of the task, and found it was often hundreds or thousands, for networks with hundreds of thousands of parameters.

Aghajanyan, Zettlemoyer and Gupta (2020) ran the same experiment on pretrained language models being fine-tuned, and the numbers were striking: RoBERTa-large (355M parameters) could reach 90% of full fine-tuning accuracy on a sentence-pair task by training in a random subspace of only about 200 dimensions. Larger pretrained models had lower intrinsic dimension, not higher. Pretraining had put the model somewhere from which the task-specific adjustment was tiny.

That is the observation behind every parameter-efficient method: the update $\Delta W$ that fine-tuning learns is low-dimensional. Random subspaces are a clumsy way to exploit it (a random projection into a 355M-dimensional space is expensive to even store). Hu et al. asked whether there was a structured, cheap subspace that worked at least as well.

all parameters: billions of dims θ₀ (pretrained) θ₀ + ΔW a low-dim subspace that reaches it Aghajanyan et al. 2020: RoBERTa-large reaches 90% of full fine-tuning accuracy moving in ~200 random directions out of 355M. the needed update is small, even if the model is huge
Figure 1. Intrinsic dimensionality. Fine-tuning moves the pretrained weights $\theta_0$ a short way to $\theta_0 + \Delta W$, and that move lies (approximately) in a subspace with a few hundred dimensions. LoRA is a cheap, structured way to parameterize such a subspace, one weight matrix at a time.

LoRA: Low Rank Adaptation

Take one weight matrix in the model, say a $d\times k$ projection $W_0$ in an attention layer. Full fine-tuning learns a new matrix $W_0 + \Delta W$ with $\Delta W$ also $d\times k$. Hu et al. (2021) constrain $\Delta W$ to have low rank $r$ by writing it as a product of two thin matrices:

$$h = W_0 x + \Delta W x = W_0 x + B A x, \qquad B\in\mathbb{R}^{d\times r},\ A\in\mathbb{R}^{r\times k},\ r \ll \min(d,k)$$

What just happened: $W_0$ is frozen. Only $A$ and $B$ are trained. $A$ first squeezes the $k$-dimensional input down to $r$ numbers, then $B$ expands those $r$ numbers back up to $d$. The product $BA$ is a full $d\times k$ matrix, but it has rank at most $r$, so it can only represent $\Delta W$'s that "live in" $r$ directions. Given intrinsic dimensionality, that is exactly the kind of $\Delta W$ we expect.

h = W₀d × k, frozen x + Bd × r × Ar × k x trained: r(d + k) numbers frozen: d·k numbers
Figure 2. The LoRA layer. The blue matrix is the pretrained weight and never changes. The two thin yellow matrices are the only trainable parameters; their product is a $d\times k$ update of rank $\le r$. The input goes through both paths and the results are added.
Worked example: how many parameters?

A single $4096\times4096$ projection (any of $W_Q, W_K, W_V, W_O$ in a Llama-7B-class model) has $4096 \times 4096 = 16{,}777{,}216$ parameters.

A rank-8 LoRA on it has $B$: $4096\times8 = 32{,}768$ and $A$: $8\times4096 = 32{,}768$, total $65{,}536$. That is $256\times$ fewer. Rank 64 is still $32\times$ fewer.

For the whole model: Llama-2-7B has 32 layers, each with four $4096\times4096$ attention projections and three MLP matrices of $4096\times11008$. LoRA rank 8 on everything is $32\times[4\times 8(4096+4096) + 3\times 8(4096+11008)] \approx 32\times(262{,}144 + 362{,}496) \approx 20$M trainable parameters, about $0.3\%$ of the model, and a 40 MB adapter file in bf16.

Symbols
$W_0\in\mathbb{R}^{d\times k}$ = frozen weight
$A\in\mathbb{R}^{r\times k}$, $B\in\mathbb{R}^{d\times r}$ = adapters
$r$ = rank, $\alpha$ = scale
STEP 1
Freeze
Load the pretrained model; set requires_grad=False on everything. No optimizer state is allocated for these weights.
STEP 2
Attach
For each chosen linear layer, add $A$ (random init) and $B$ (zeros). Forward: $h = W_0x + \frac{\alpha}{r}BAx$.
STEP 3
Train
Ordinary SFT / RL loop, but the optimizer only sees $A$ and $B$: a few percent of the parameters, a few percent of the optimizer memory.
STEP 4
Merge or swap
Fold $\frac{\alpha}{r}BA$ into $W_0$ for zero-overhead inference, or keep adapters separate and serve many of them from one base model.
InteractiveRank explorer: how small is the adapter?drag d, k and r

The blocks are drawn to scale (up to the width of the panel). Notice how $r$ has to become a sizeable fraction of $d$ before the yellow blocks are anything but slivers.

Why low rank is a reasonable bet

A rank-$r$ matrix is one whose columns all live in an $r$-dimensional subspace. Equivalently (via the singular value decomposition) it is a sum of $r$ outer products $\sigma_i u_i v_i^\top$. The claim LoRA makes is that the fine-tuning update, whatever it is, is well approximated by its top few singular components. Hu et al. checked this directly: they trained full-rank updates on GPT-3 and found that the top singular directions of the learned $\Delta W$ captured most of it, and that LoRAs of rank 1 to 64 on the attention projections gave nearly indistinguishable downstream accuracy.

The demo below makes the "top few components capture most of it" idea concrete on a small matrix. A matrix with real structure is well approximated by a rank far below its size; pure noise is not. Fine-tuning updates are, empirically, closer to the first kind.

InteractiveLow-rank approximation of a 12×12 matrixslide r; add noise

The left matrix is a seeded rank-3 matrix plus Gaussian noise. The right is its best rank-$r$ approximation (top $r$ singular components). The bar chart is the singular value spectrum: the "elbow" tells you the true rank.

import math, torch, torch.nn as nn

class LoRALinear(nn.Module):
    """Wraps a frozen nn.Linear with a trainable low-rank update: h = W0 x + (alpha/r) B A x."""
    def __init__(self, base: nn.Linear, r: int = 8, alpha: float = 16.0, dropout: float = 0.05):
        super().__init__()
        self.base = base
        for p in self.base.parameters():
            p.requires_grad_(False)                     # freeze W0 (and bias)
        d, k = base.out_features, base.in_features
        self.A = nn.Parameter(torch.empty(r, k))
        self.B = nn.Parameter(torch.zeros(d, r))        # B = 0  =>  BA = 0 at start
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
        self.scale = alpha / r
        self.drop = nn.Dropout(dropout)

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

    @torch.no_grad()
    def merge(self) -> nn.Linear:
        """Fold the adapter into the weight; returns a plain Linear with zero extra inference cost."""
        self.base.weight += self.scale * (self.B @ self.A)
        return self.base

code/lumen/lora.py has this class plus apply_lora(model, targets=[...], r, alpha) that walks a model and replaces the named linear layers, and merge_all to undo it.

Scaling and Initialization

Three details in that code are not decoration.

$B$ starts at zero. With $B=0$, $BA = 0$, so at step 0 the model is exactly the pretrained model. Training starts from a known-good point and moves away only as fast as the gradient asks. If both $A$ and $B$ were random, the initial $BA$ would be random noise injected into every adapted layer, and you would spend the first part of training undoing the damage. (Why not $A = 0$ instead? Then the gradient with respect to $B$ would be $\partial\mathcal L/\partial h\cdot (Ax)^\top = 0$ and the gradient with respect to $A$ would be $B^\top\partial\mathcal L/\partial h\, x^\top$, also zero: nothing would ever move. One of them must be nonzero, and the convention is $A$ random, $B$ zero.)

The $\alpha/r$ scale. The update is $\frac{\alpha}{r}BA$, not $BA$. The idea from the paper: if you double $r$, the product $BA$ is a sum of twice as many rank-1 terms and its typical magnitude grows; dividing by $r$ compensates, so you can change the rank without retuning the learning rate. $\alpha$ is then just a constant you set once (commonly $\alpha = 16$ or $\alpha = 2r$). Kalajdzievski (2023) argued that the correct compensation is $\alpha/\sqrt{r}$, not $\alpha/r$, if you want the update's scale to be truly rank-independent (the sum of $r$ random terms grows like $\sqrt r$, not $r$); this rsLoRA scaling lets higher ranks actually learn more instead of being throttled, and libraries offer it as an option.

Dropout on the adapter input (5–10%) is the only regularization most LoRA runs use; the low rank itself is already a strong regularizer.

And the learning rate: because the adapters are small and start at zero, LoRA wants a learning rate roughly $10\times$ higher than full fine-tuning of the same model; $1$–$3\times10^{-4}$ is the usual range where full fine-tuning would use $1$–$2\times10^{-5}$. Thinking Machines' "LoRA Without Regret" post (discussed below) reports this $10\times$ ratio holding remarkably consistently across models and tasks.

Common confusion: $\alpha$ is not a hyperparameter you sweep independently of $r$

Only the ratio $\alpha/r$ (or $\alpha/\sqrt r$) enters the forward pass, and with Adam, which normalizes gradient magnitudes, a change of the scale acts mostly like a change of learning rate. Pick one convention (e.g. $\alpha = 2r$) and tune the learning rate. Sweeping $\alpha$, $r$ and the learning rate as three independent knobs mostly wastes GPU hours.

Merging and Serving

Here is the part that made LoRA the default rather than one PEFT method among many. Adapter methods that insert extra layers (the older "adapter" modules of Houlsby et al. 2019, or prefix tuning) make inference slower forever, because the extra computation is on the path. LoRA's update is a matrix added to a matrix, so once training is done you can compute

$$W' = W_0 + \frac{\alpha}{r}BA$$

once, store $W'$ in place of $W_0$, and delete $A$ and $B$. The fine-tuned model has exactly the same architecture, size and latency as the base model. Zero inference overhead. And the merge is reversible: $W_0 = W' - \frac{\alpha}{r}BA$.

InteractiveMerge visualizer: from W and BA to a single matrixstep through

A $6\times6$ frozen weight and a rank-2 adapter, with real numbers. Step through to see what is computed at each stage and what is stored on disk or in GPU memory.

Serving many adapters from one base

The flip side of merging is not merging. Because the base weights are untouched, a single copy of the base model in GPU memory can serve hundreds of different fine-tunes: keep each customer's $(A, B)$ pair in memory (a few tens of MB each), and for each request compute $W_0x$ once for the batch and $B_iA_ix$ for that request's adapter $i$. S-LoRA (Sheng et al. 2023) and Punica (Chen et al. 2023) built the batched kernels that make this efficient, and it is now a standard feature of vLLM and similar servers. This is why "fine-tuning as a service" is economically possible: the marginal cost of hosting one more fine-tune is an adapter, not a model.

Which Layers to Adapt?

Hu et al. originally adapted only the attention projections, and among those found that $W_Q$ and $W_V$ together at rank 4 beat any single matrix at rank 8 for the same parameter budget, with all four attention matrices at rank 2 about as good. The MLP layers were left frozen, mostly to keep the study simple.

Practice moved on. Several findings, reported consistently across QLoRA (Dettmers et al. 2023), the Thinking Machines post and many practitioners' sweeps: adapting all linear layers, including the MLP (and MoE expert) matrices, matters more than rank. A rank-8 LoRA on every linear layer generally beats a rank-64 LoRA on attention only. The intuition: the MLP layers hold most of the parameters and most of the "knowledge"; leaving them frozen caps what the fine-tune can change. For MoE models, adapt the experts (each expert is a small MLP) rather than only the shared attention.

TargetsTypical resultWhen
$W_Q, W_V$ onlyFine for light style/format tasks; caps quicklyHistoric default; smallest adapters
$W_Q, W_K, W_V, W_O$Slightly better than q,v at equal budgetWhen MLP adaptation is too costly
All linear layers (attention + MLP)Best; matches full FT in the low-regret regimeDefault in 2026
+ embeddings / LM headNeeded only when adding tokens or a new languageVocabulary changes

Rank. For instruction tuning and most task adaptation, $r\in[8, 64]$; results are flat across that range once all layers are targeted. Increase rank when the dataset is large (hundreds of millions of tokens or more) or the task needs the model to absorb a lot of new material; the "capacity" of a LoRA is roughly its parameter count, and a rank that is too small simply stops learning before the data runs out.

QLoRA

LoRA removes the optimizer-state and gradient memory, but the frozen base model still has to sit in GPU memory in 16-bit: 14 GB for 7B, 130 GB for 65B. Dettmers et al. (2023) asked: since the base is frozen anyway, why store it at 16 bits? QLoRA keeps the frozen base in 4-bit, dequantizes each weight matrix to bf16 on the fly when it is needed for a matmul, and trains bf16 LoRA adapters on top. The result: fine-tuning a 65B model on a single 48 GB GPU, with quality that the paper reports matches 16-bit LoRA. Three ingredients:

NF4: a 4-bit type shaped like the weights

With 4 bits you have 16 possible values per weight. The question is which 16. Ordinary int4 spaces them evenly, but neural network weights are approximately Gaussian: dense near zero, sparse in the tails, so evenly spaced levels waste half their codes on values that almost never occur. NormalFloat4 places the 16 levels at the quantiles of a standard normal: chosen so each of the 16 bins contains equal probability mass under $\mathcal N(0,1)$. Levels are dense near zero (where the weights are) and sparse in the tails. To use it, each block of 64 weights is divided by its absolute maximum (so it lies in $[-1,1]$), and each scaled value is rounded to the nearest NF4 level. The paper reports NF4 beating int4 and 4-bit float at equal size.

Double quantization

Every block of 64 weights stores its absmax as a 32-bit float: $32/64 = 0.5$ extra bits per weight, which is a 12% overhead on a 4-bit format. Double quantization quantizes those absmax constants themselves to 8 bits, in blocks of 256, with a second-level scale. The overhead falls to about $0.127$ bits per weight, saving roughly $0.37$ bits per parameter, about 3 GB on a 65B model.

Paged optimizers

Even with a 4-bit base, memory spikes during long sequences (activations, gradient checkpoints) can exceed the GPU. QLoRA uses NVIDIA unified memory to page the optimizer states to CPU RAM when the GPU runs out and back when there is room, the same way an OS pages memory to disk. It costs some speed on the steps where it triggers and prevents an out-of-memory crash on the rest.

W₀ in NF44 bits + block scales dequant W₀ in bf16only for this matmul W₀ xfrozen path B, A in bf16trainable, tiny (α/r) B A xgradients flow here h = sum
Figure 3. A QLoRA layer. The frozen base is stored in 4-bit NF4 and dequantized to bf16 one matrix at a time, just long enough to multiply; the adapters stay in bf16 and are the only tensors that receive gradients. Memory for the base drops about 4× with, as reported, no loss in fine-tuning quality.
Symbols (QLoRA)
NF4 = 4-bit normal-float
block = 64 weights, one absmax
double quant = 8-bit absmax in blocks of 256
STEP 1
Quantize once
Load the base in 16-bit, block-quantize every weight matrix to NF4 with double quantization, free the 16-bit copy.
STEP 2
Forward
For each linear layer, dequantize its block to bf16, compute $W_0x$, discard; add $\frac{\alpha}{r}BAx$ from the bf16 adapters.
STEP 3
Backward
Gradients flow through the dequantized $W_0$ to the activations, but only $A, B$ accumulate gradients and optimizer state.
STEP 4
Page if needed
Optimizer states live in unified memory and are paged to CPU on spikes.
Memory arithmetic for a 7B model

Full fine-tune with AdamW in mixed precision: weights 14 GB + grads 14 GB + Adam moments 56 GB (fp32) + master weights 28 GB, roughly 110 GB. LoRA (all layers, $r=16$, about 40M params): base 14 GB + adapters and their optimizer state under 1 GB + activations. QLoRA: base about 3.8 GB + the same adapters. A 24 GB consumer GPU runs QLoRA on 7B comfortably and LoRA on 7B with short sequences; a 13B model needs QLoRA.

When LoRA Fails

LoRA is not free. The constraint that makes it cheap, $\text{rank}(\Delta W)\le r$, is a real constraint, and there are tasks where the update the model needs does not fit.

Biderman et al. (2024), "LoRA Learns Less and Forgets Less", ran the cleanest comparison: Llama-2-7B, full fine-tuning versus LoRA, on two domains (code and math) in two regimes (instruction fine-tuning with tens of millions of tokens; continued pretraining with billions). Their findings, as reported:

  • LoRA learns less. In continued pretraining on code and math, LoRA fell substantially short of full fine-tuning in target-domain performance, and the gap grew with data. In instruction fine-tuning the gap was smaller but present.
  • LoRA forgets less. Full fine-tuning degraded the model's original abilities (measured on general benchmarks) much more than LoRA did. LoRA acts as a strong regularizer toward the base model, stronger than weight decay or dropout.
  • Full fine-tuning's updates are high-rank. They measured the rank of $\Delta W$ learned by full fine-tuning and found it 10–100× higher than the ranks people use for LoRA, and increasing over training. That is the direct reason LoRA learns less: the target update does not fit in the subspace.
  • Learning vs forgetting is a trade-off curve, and LoRA sits at a different point on it than full fine-tuning, not strictly below it.

So the failure mode is predictable: large domain shift, or lots of new knowledge. Teaching a model a new programming language, a new natural language, or a large body of facts from billions of tokens is a high-rank update. Teaching it a format, a style, a persona, a tool-calling convention, or a preference (which is most post-training) is low-rank, and LoRA is fine.

InteractiveIllustrative: learning vs forgetting as training growschange the rank

These curves are illustrative, drawn to match the qualitative findings of Biderman et al. (2024): full fine-tuning learns the target domain faster and further, and forgets the source domain more; LoRA learns less and forgets less; higher rank moves LoRA toward full fine-tuning on both axes. Do not read the numbers as measurements.

The RL angle

There is one regime where LoRA does not seem to lose at all: reinforcement learning. Thinking Machines' "LoRA Without Regret" (Schulman et al., 2025), a blog post with extensive experiments rather than a peer-reviewed paper, reports that for policy-gradient RL fine-tuning (the GRPO-style setups in the RL chapters), LoRA matched full fine-tuning even at very low rank, down to rank 1 in their runs. Their explanation is an information argument: a policy-gradient update extracts on the order of one bit of information per episode (was this trajectory better or worse than expected?), so even a long RL run absorbs far less information than a LoRA can store; the update is low-rank-ish because the signal is low-bandwidth. For supervised fine-tuning, by contrast, every token is a label and a large dataset can exceed the adapter's capacity, which is exactly the Biderman finding from the other direction.

Treat this as a well-supported empirical report from one lab rather than a theorem, but it matches what practitioners see: RL post-training with LoRA is now common, and it makes RL on large models feasible on modest hardware because the frozen base can be shared between the policy and the reference model.

LoRA Variants

Dozens of papers modify LoRA. Most fall into three buckets depending on what they are trying to fix: get more quality per parameter, start from a better initialization, or spend a given budget more wisely. Here is each with the one idea that defines it.

Performance variants: DoRA, MiSS

DoRA (Weight-Decomposed Low-Rank Adaptation; Liu et al. 2024) starts from an observation about full fine-tuning: it tends to change the direction of weight columns and their magnitude somewhat independently, while LoRA couples them. DoRA decomposes each weight matrix into a per-column magnitude vector $m$ and a direction matrix, applies LoRA to the direction only, and trains $m$ separately:

$$W' = m \odot \frac{W_0 + BA}{\|W_0 + BA\|_{\text{col}}}$$

The norm is taken column by column, so the LoRA update can rotate each column freely and $m$ sets its length. The extra cost is $k$ parameters per matrix (the magnitudes), and the paper reports consistent gains over LoRA at the same rank, especially at low ranks. It merges just like LoRA.

MiSS (Matrix Shard Sharing; Kang et al. 2024, the successor to their earlier "Bone" method) drops the $BA$ product entirely. It splits the weight matrix into equal-width shards and adds one shared trainable matrix, the size of a single shard and initialized to zero, to every shard. The update is therefore a block-repeated matrix rather than a low-rank one, with a parameter count comparable to LoRA. The authors report better performance-per-parameter than LoRA and faster training, and the method is available in Hugging Face PEFT; it is newer and less independently validated than DoRA.

Initialization variants: PiSSA, LoRA+

PiSSA (Principal Singular values and Singular vectors Adaptation; Meng et al. 2024) keeps LoRA's shape but changes the starting point. Instead of $A$ random and $B = 0$, take the SVD of the pretrained $W_0 = U\Sigma V^\top$, initialize $B = U_{:r}\Sigma_{:r}^{1/2}$ and $A = \Sigma_{:r}^{1/2}V_{:r}^\top$ from the top-$r$ singular components, and freeze the residual $W_0 - BA$ as the base. The forward pass at step 0 is still exactly $W_0$, but now the trainable part is the most important directions of the weight rather than a random subspace, and the authors report faster convergence and better final loss than LoRA, including in the QLoRA setting.

LoRA+ (Hayou, Ghosh & Yu 2024) is a training fix, not an architecture: they show through a width-scaling analysis that giving $A$ and $B$ the same learning rate is suboptimal, because the two matrices see gradients of very different scale ($B$ starts at zero and is multiplied by the small activations $Ax$). The fix is a single extra hyperparameter: $\eta_B = \lambda\,\eta_A$ with $\lambda \approx 16$. Reported gains are modest (a few percent) but the change is free.

Budget variants: AdaLoRA, VeRA, IA³

AdaLoRA (Zhang et al. 2023) observes that a fixed rank for every matrix in every layer is wasteful: some need more, some less. It parameterizes each update as $P\Lambda Q$ in SVD-like form (with an orthogonality penalty on $P$ and $Q$), scores each singular value's importance from gradients during training, and prunes the least important ones to meet a global parameter budget, so rank is allocated across the model rather than fixed.

VeRA (Vector-based Random Matrix Adaptation; Kopiczko, Blankevoort & Asano 2023) pushes the intrinsic-dimension idea to its limit. $A$ and $B$ are random, frozen, and shared across all layers; the only trainable parameters are two small vectors per layer that rescale the rows of the random matrices: $\Delta W = \Lambda_b B \Lambda_d A$. The adapter for a 7B model shrinks from tens of MB to well under a megabyte, at some quality cost on harder tasks. Useful when you need to store or ship thousands of adapters.

IA³ (Infused Adapter by Inhibiting and Amplifying Inner Activations; Liu et al. 2022) does not modify weights at all; it learns three vectors per layer that rescale activations: the keys, the values, and the hidden activations of the MLP, elementwise. That is roughly $3d$ parameters per layer, about $10\times$ fewer than a rank-8 LoRA, and it is also mergeable (a rescaling of $K$ is a rescaling of $W_K$'s rows). It was designed for few-shot learning with very small budgets and is less expressive than LoRA when you have data.

MethodTrainable partParams vs LoRAFixesMergeable?
LoRA$B A$the baselineyes
DoRA$BA$ + column magnitudes $m$≈1× (+$k$)decouples direction from magnitudeyes
MiSSone shared shard matrix≈1×update structure; speedyes
PiSSA$BA$ from top singular componentsinitialization: train the principal directionsyes
LoRA+$BA$, $\eta_B \gg \eta_A$learning-rate imbalance between $A$ and $B$yes
AdaLoRA$P\Lambda Q$, pruned≤1×, allocateduniform rank is wastefulyes
VeRAtwo scaling vectors; $A, B$ random frozen~0.01–0.1×adapter storageyes
IA³three activation-scaling vectors~0.1×few-shot, tiny budgetsyes
LoRA W₀ + DoRA m ⊙ W₀+BA ‖·‖ per column VeRA Λ_b Λ_d grey = random, frozen, shared PiSSA W₀ − BA + init from top-r SVD of W₀ IA³ W₀ ⊙ l (a vector) rescale activations only AdaLoRA P Λ Q, Λ pruned to a budget
Figure 4. The variants at a glance. Yellow is trainable and low-rank; pink is a trainable vector or diagonal; grey is random and frozen. DoRA adds a magnitude vector; VeRA trains only the pink diagonals; PiSSA re-splits $W_0$ so the yellow part starts as its principal components; IA³ never touches the weights; AdaLoRA decides how much yellow each layer deserves.

What Actually Works In Practice

The low-regret regime

Put the Biderman result and the Thinking Machines result together and a clear picture emerges. There is a regime, which the Thinking Machines post calls low-regret, where LoRA configured properly (all layers, adequate rank, $10\times$ learning rate, moderate batch size) matches full fine-tuning in both sample efficiency and final quality. As reported, that regime covers:

  • supervised fine-tuning on small-to-medium datasets (up to roughly the adapter's capacity in tokens, which for rank 32–128 on a 7B model is in the hundreds of millions of tokens or more);
  • essentially all RL fine-tuning, at any rank;
  • style, format, persona, tool-use and safety tuning, which are low-information updates.

Outside it, full fine-tuning wins: continued pretraining on billions of tokens of a new domain, and any task where you need to install a lot of new facts. The good news is that most post-training is inside the regime, which is why LoRA is the default and not a compromise.

PEFT methods for reinforcement learning

Three practical reasons LoRA and RL go together. First, the capacity argument above: RL's per-episode information is small, so a small adapter suffices. Second, memory: RLHF/GRPO training keeps a policy, a reference policy and often a reward model resident; with LoRA the policy and the reference share the same frozen base, and the reference is just "the base with adapters disabled," which halves the biggest memory line. Third, stability: the implicit regularization toward the base model reduces the reward-hacking drift that full-parameter RL is prone to, and makes the KL penalty easier to keep in range. The reported caveat is batch size: LoRA is less tolerant of very large batches than full fine-tuning, so keep effective batches moderate.

What to avoid

  • Attention-only targets with a high rank. The rank is not the bottleneck; the frozen MLPs are. Adapt everything at a modest rank instead.
  • Full fine-tuning learning rates. $10^{-5}$ with LoRA barely moves; you will conclude LoRA "doesn't work." Start at $10^{-4}$.
  • Huge batches to speed things up. LoRA's loss degrades with batch size more than full fine-tuning's, as reported, and the rank does not fix it.
  • Expecting new knowledge. If the goal is "make it know our 10 GB of internal docs," LoRA on the docs will underperform full fine-tuning, and both will underperform retrieval. Use the tool that fits.
  • Merging into a quantized base. $W' = W_0 + BA$ requires a full-precision $W_0$; merging into an NF4 base re-quantizes the sum and loses accuracy. Merge into the 16-bit weights, then quantize.
  • Sweeping $\alpha$, $r$ and the learning rate independently. See the confusion box above: fix $\alpha$'s convention, then sweep the learning rate, then (maybe) $r$.
TaskMethodRankLearning rateTargetsNotes
Style / format / persona SFTLoRA or DoRA8–161–2e-4all linearTiny data suffices; watch overfitting
Instruction tuning, 10k–1M examplesLoRA16–641–3e-4all linearrsLoRA scaling if using r ≥ 64
RL post-training (GRPO/PPO/DPO)LoRA8–32 (even 1)~10× the full-FT LRall linearReference = base with adapters off; moderate batch
Domain adaptation, 100M–1B tokensLoRA, high rank, or full FT128–2561e-4all linear + maybe embeddingsCompare to full FT on a held-out set before committing
Continued pretraining, >1B tokensFull fine-tuning1–5e-5LoRA learns less here (Biderman et al. 2024)
Large model on one consumer GPUQLoRA16–641–2e-4all linearNF4 + double quant; paged AdamW
Thousands of tenants' adaptersLoRA (unmerged) or VeRA8–161e-4attention + MLPMulti-LoRA serving; VeRA if storage bound

The Bottom Line

LoRA works because fine-tuning updates are low-dimensional, and it won because it is mergeable: the fine-tuned model costs nothing extra to run. Configure it with all linear layers as targets, a rank of 8–64, $\alpha$ fixed by convention, $B=0$, and a learning rate about ten times what full fine-tuning would use; do that and, for most post-training, it matches full fine-tuning at a fraction of the memory. QLoRA extends the same recipe to models that would not otherwise fit on your GPU. Where it falls short is where the update is genuinely high-rank: absorbing lots of new knowledge from lots of new tokens. The variants are mostly small refinements; DoRA and PiSSA are the ones worth trying first, LoRA+ is a free tweak, and VeRA and IA³ are for when the adapter's size, not its quality, is the constraint.

Practice

Exercise 1 — build LoRA and verify merging

Implement LoRALinear and apply_lora in code/lumen/lora.py (or check the provided one), attach rank-4 adapters to every linear layer of the GPT-2 small from code/lumen/gpt2.py, and confirm (a) that the model's outputs are bit-identical to the base at step 0, (b) that only the adapter parameters have requires_grad, and (c) that after 100 training steps, merging and running the plain model gives the same logits (to atol=1e-5) as the unmerged model.

Solution sketch

Count trainable parameters with sum(p.numel() for p in model.parameters() if p.requires_grad): for GPT-2 small, rank 4 on the four attention and two MLP matrices per layer gives about 0.6M of 124M. For (c), be careful with the merge order and the scale $\alpha/r$; the most common bug is merging $AB$ instead of $BA$ or forgetting the scale.

Exercise 2 — rank vs targets

Fine-tune GPT-2 small on the instruction data from code/lumen/data.py four ways with equal parameter budgets: (i) $W_Q, W_V$ at rank 32, (ii) all attention at rank 16, (iii) all linear layers at rank 4, (iv) full fine-tuning. Compare held-out loss after one epoch. Then repeat (iii) at learning rates $10^{-5}$, $10^{-4}$, $10^{-3}$.

Solution sketch

Expect (iii) to beat (i) and (ii) at the same budget, and to be close to (iv). At $10^{-5}$ LoRA barely improves on the base; $10^{-4}$ is near-optimal; $10^{-3}$ may diverge or overfit. This reproduces the two most important practical findings of the chapter on a laptop.

Exercise 3 — see the rank of a real update

Fully fine-tune GPT-2 small for 500 steps, compute $\Delta W = W_{\text{ft}} - W_0$ for one MLP matrix, take its SVD with torch.linalg.svd, and plot the singular values. How many components capture 90% of the energy? Compare with the SVD of $W_0$ itself and with the SVD of the update from a rank-8 LoRA run of the same length.

Solution sketch

The full-FT update is far from rank 8, but its spectrum decays: typically a few dozen to a few hundred components carry 90% of the energy in a short run, and the number grows with training length, consistent with Biderman et al. The LoRA update has exactly 8 nonzero singular values by construction. This is the low-rank-approximation demo, on real weights.

Check yourself
A $4096\times4096$ weight has 16.8M parameters. How many trainable parameters does a rank-8 LoRA on it have?
$B$ is $4096\times8$ and $A$ is $8\times4096$: $r(d+k)$ parameters in total, 256× fewer than the full matrix.
Why is $B$ initialized to zero (and $A$ random) rather than both random or both zero?
With $B=0$ the model is unchanged initially, and the gradient with respect to $B$ is nonzero because $Ax \ne 0$. With both zero, every gradient would be zero and nothing would train.
What is the inference-time cost of a merged LoRA compared to the base model?
Merging folds the update into the weight once, offline. The extra matmuls only exist in the unmerged form, which is used when serving many adapters from one base.
What did Biderman et al. (2024) report about LoRA vs full fine-tuning?
The title says it: "LoRA Learns Less and Forgets Less." The gap in learning grows with data and domain shift; the regularization toward the base is the flip side.
What is the key idea of NF4 in QLoRA?
Weights are roughly Gaussian, so evenly spaced levels waste codes on rare tail values. NF4's levels are dense near zero and sparse in the tails, matching where the weights actually are. The adapters themselves stay in bf16.

Key takeaways

  • Fine-tuning updates are low-dimensional (intrinsic dimensionality), so constraining $\Delta W = BA$ to rank $r$ loses little for most tasks and cuts trainable parameters and optimizer memory by 100× or more.
  • $B = 0$ at init means training starts exactly at the base model; $\alpha/r$ (or $\alpha/\sqrt r$) scaling makes rank changes not require learning-rate changes; LoRA wants about $10\times$ the full-FT learning rate.
  • Merging $W' = W_0 + \frac{\alpha}{r}BA$ gives zero inference overhead; not merging enables serving many adapters from one base.
  • Adapt all linear layers (attention and MLP) at rank 8–64; targets matter more than rank.
  • QLoRA keeps the frozen base in NF4 (quantile-spaced 4-bit), double-quantizes the scales, and pages optimizer state, putting 65B fine-tuning on one 48 GB GPU.
  • LoRA learns less and forgets less: it is in its low-regret regime for most SFT and essentially all RL, and loses to full fine-tuning for large-scale new-knowledge or domain-shift training.

Further reading