Modules
07/30
LoRA

Contents

LoRA in LLMs

How a large language model learns a new task by training a small update instead of rewriting all of its weights.

A pretrained language model learns broad patterns from enormous amounts of text, but that general knowledge does not automatically make it suitable for every job. A team may need the model to follow a particular instruction format, understand a specialized domain, or respond in a consistent style. Fine-tuning uses examples from that narrower task to adjust the model's behavior.

The difficulty is scale. Updating every parameter of a large model requires substantial training memory, and each finished task produces another full-sized set of weights to store and deploy. LoRA was developed to make this adaptation smaller: preserve the pretrained model, then learn only the compact changes required by the new task.

The Change Hidden Inside Fine-Tuning

Fine-tuning begins with a model whose weights have already learned broad patterns from language. Training on a narrower dataset changes those weights so the model becomes better at a particular task, domain, or style.

Pick any weight matrix inside the model. Its fine-tuned version can be understood as the original pretrained matrix plus a change learned from the new data. Full fine-tuning stores the combined result as another complete matrix. LoRA keeps the original matrix fixed and learns only the change, using a much smaller representation.

Pretrained weights
The model's existing knowledgeshared and frozen
Task update
Learned and saved
At runtime, the model uses its original transformation together with the learned task update.

LoRA in one sentence

Keep the pretrained weights frozen, then train a compact update beside selected layers.

1. From Training Examples to a Weight Update

A fine-tuning example pairs an input with the answer the model should produce. The model makes a prediction, the training loss measures how far that prediction is from the desired answer, and backpropagation calculates which trainable weights should change.

inputRewrite politely: “Send the report today.”
target“Could you please send the report today?”

Full fine-tuning allows that update to flow into every model weight. This gives training broad freedom, but it also requires gradient and optimizer state for the full model and produces another full-sized set of weights for the task.

LoRA changes only the destination of the update. The same examples, loss, and backpropagation are used, but the pretrained weights are marked as frozen. Gradients update the small LoRA parameters instead, so the task is stored as an adapter rather than another copy of the model.

2. The Transformer Layers LoRA Can Change

A Transformer carries each token as a list of learned features called a hidden state. Linear layers transform those features using weight matrices. They appear throughout every Transformer block, especially inside attention and the feed-forward network.

Attention uses query, key, value, and output projections to gather information from other tokens. The feed-forward network, or FFN, then transforms each token independently: it expands the hidden state into a wider feature space, applies a gated nonlinearity, and reduces it to the original model width. In many LLMs, these FFN projections contain a large share of the block's parameters.

hidden state
Attention
gather context from other tokens
querykeyvalueoutput
residual
path
Feed-forward network
transform each token's features
expandgatereduce
block output

LoRA can be attached to selected projections in attention, the FFN, or both. It adds a parallel weight update to those linear layers. The surrounding attention calculation, activation functions, normalization, and residual paths remain unchanged.

3. Matrix Rank and the LoRA Path

Before following the LoRA path, three matrix properties are useful. A matrix is a rectangular table of numbers. Its shape tells us how many input features it receives and how many output features it produces. A weight with shape dout×dind_{\text{out}} \times d_{\text{in}} maps a hidden state with dind_{\text{in}} features to one with doutd_{\text{out}} features.

The rank of a matrix describes how many independent directions its transformation can express. A full-rank matrix can use every available direction. A low-rank matrix is more restricted because some rows or columns can be produced from combinations of others. LoRA deliberately places this restriction on the task update, while leaving the pretrained matrix untouched.

A standard linear projection
x
dind_{\text{in}}
W
dout×dind_{\text{out}} \times d_{\text{in}}
y
doutd_{\text{out}}
LoRA stores the update as two factors
B
dout×rd_{\text{out}} \times r
×
A
r×dinr \times d_{\text{in}}
=
ΔW = BA
dout×dind_{\text{out}} \times d_{\text{in}}
rank ≤ r

Notice that BABA has the same outer shape as the pretrained weight, so it can be added to that weight. Its rank cannot exceed the narrow inner dimension rr. This is the mathematical reason the two smaller matrices form a low-rank update.

An adapted linear layer computes two results from the same hidden state. The pretrained weight produces the result it would have produced before fine-tuning. Beside it, the LoRA adapter produces a correction learned from the new data. Adding the correction changes the layer's behavior without changing the pretrained weight itself.

The adapter creates its correction with two matrices, named AA and BB. The first reduces the hidden state to a narrow intermediate width. The second restores the original output width. Select the merged view below to see how this separate path can disappear after training.

One adapted linear layer

The base path and the LoRA path

The two paths are separate while the adapter is trained. After training, they can be combined into one weight.

Showing the training view.

input x
Pretrained weight W₀
frozen
base result
input x
A
reduce width
r
narrow
B
restore width
scaled correction
base result + scaled correction = output h

Gradients update A and B. The pretrained weight W₀ participates in the calculation but does not change.

A LoRA adapter is therefore not a second language model. It is the collection of these small update matrices across the selected layers. Several adapters can share the same frozen base model because each one stores only its own corrections.

4. Why the Update Is Low-Rank

Consider a linear layer with 4,096 input features and 4,096 output features. A freely trainable update for that layer would contain 4,096 × 4,096 values, or 16,777,216 parameters. LoRA sends the update through a much narrower space instead.

4,096 inputsA reducesrank 8B restores4,096 outputs

The middle width is the LoRA rank, written as rr. With rank 8, the two adapter matrices contain 65,536 parameters for this layer, about 0.39% as many as a full update matrix.

Written compactly, the task update is the product of the two trainable matrices:

ΔW=BA,NLoRA=r(din+dout)\Delta W = BA, \qquad N_{\text{LoRA}} = r(d_{\text{in}} + d_{\text{out}})

For an input hidden state xx, the frozen matrix W0W_0 produces the base result. The scaled LoRA correction is added beside it:

h=W0x+αrB(Ax)h = W_0x + \frac{\alpha}{r}B(Ax)

The pretrained matrix can still be full-rank

Low-rank describes the learned update BA. LoRA does not compress or factorize the frozen pretrained matrix W₀.

Interactive: How Rank Changes the Parameter Count

Select a typical Transformer projection and move the rank slider. The calculation covers one adapted weight matrix, making the relationship between matrix width, rank, and trainable parameter count explicit.

Parameter comparison

How much does rank change the adapter?

The comparison below covers one weight matrix. Move the slider to change the narrow middle width.

Full update matrix
4,096 × 4,096
16,777,216
trainable values
compared with
LoRA's two matrices
A: 8 × 4,096
+
B: 4,096 × 8
65,536
trainable values
At rank 8, the adapter stores 0.391% as many values as the full update for this matrix.
about 256× smaller

5. How LoRA Training Works

The task data, forward loss, backpropagation, and optimizer loop remain familiar. The important difference is which parameters are allowed to receive gradients and optimizer updates.

1
Prepare the base model

Load the pretrained weights and insert LoRA branches into the selected linear layers.

2
Freeze pretrained parameters

Mark the base weights as non-trainable so the optimizer excludes them.

3
Run the normal forward pass

Each adapted layer adds its LoRA output to the output of the frozen base projection.

4
Compute the task loss

Use the same objective required by the fine-tuning task, such as next-token cross-entropy.

5
Backpropagate through the adapters

Gradients flow through the network, but trainable parameter gradients are retained for A and B.

6
Update and save

The optimizer changes the adapter matrices, and the final checkpoint stores those matrices with their configuration.

Freezing a weight means the optimizer does not change it. The frozen matrix still participates in the forward pass and in computing gradients for earlier activations, so the base model remains part of training computation.

6. Initialization, Scaling, and the First Update

In the original LoRA formulation, AA is initialized with small random values and BB is initialized to zero. The initial adapter product is therefore zero:

B=0BA=0h=W0xB = 0 \quad \Longrightarrow \quad BA = 0 \quad \Longrightarrow \quad h = W_0x

Fine-tuning begins with exactly the base layer's behavior rather than a random perturbation. On the first backward pass, BB can receive a gradient because AxAx is generally nonzero. The gradient for AA initially passes through zero-valued BB, so AA starts changing after BB moves away from zero.

The scaling factor α/r\alpha/r separates the adapter's effective strength from the raw matrix values. LoRA dropout, when configured, is applied on the adapter path during training and disabled during evaluation.

Configuration belongs with the checkpoint

Rank, alpha, dropout, target module names, and adapter tensor shapes are required to reconstruct the trained update correctly.

7. Choosing Which Linear Layers to Adapt

LoRA is a method for adapting matrices, not a fixed list of layer names. The original paper studied attention projections extensively and commonly adapted query and value weights. Current model families use different module names and may place adapters on additional attention and FFN projections.

Attention projections
querykeyvalueoutput

These matrices influence how hidden states form attention scores, carry contextual values, and return attention results to the residual stream.

FFN projections
gateupdown

These matrices control the block's per-token feature transformation and often contain a large share of its parameters.

Adapting more module types increases the adapter's capacity and training cost. Adapting fewer modules creates a smaller checkpoint but imposes a tighter constraint. There is no architecture-independent target list because names, tensor shapes, grouped projections, and empirical behavior differ across model families.

A reliable configuration begins with the model implementation's actual named modules and a recipe validated for that architecture. Parameter counts should be inspected after adapter injection to confirm that only the intended weights are trainable.

8. Choosing Rank and Alpha

Rank controls the maximum dimensionality of each learned update. Alpha controls its scale. They affect different parts of the adapter and should be recorded as separate hyperparameters.

Rank r
Capacity and size

Increasing rank adds parameters linearly and permits a higher-rank update. The useful value depends on task complexity, data, target modules, and the base model.

Alpha α
Update scale

In standard LoRA, the branch is multiplied by α/r. Changing alpha changes the effective contribution without changing the parameter count.

A larger rank is not automatically better. It can improve expressiveness while increasing memory, checkpoint size, and optimization freedom. Rank, learning rate, alpha, dropout, target modules, and dataset quality interact, so comparisons should change one controlled configuration at a time and evaluate on held-out data.

9. What a LoRA Checkpoint Must Save

A LoRA checkpoint is more than two anonymous tensors. It must identify how those tensors attach to the base model and how their output is scaled.

Adapter tensors

The learned A and B matrices for every adapted module

Base model identity

The exact compatible model or revision whose weights remain frozen

Target modules

The layer names and locations where adapters must be inserted

LoRA configuration

Rank, alpha, dropout, bias policy, and relevant implementation options

Tokenizer and task assets

The vocabulary, special tokens, prompt format, and any added embeddings

Training provenance

Dataset, objective, license, evaluation, and framework version information

Loading an adapter onto a different base revision can silently produce incorrect behavior even when tensor shapes happen to match. The adapter and its base model form one versioned artifact relationship.

10. Merging and Serving Multiple Adapters

After training, the scaled update has the same shape as the original matrix. A serving system can therefore merge it into a copy of the base weight:

Wmerged=W0+αrBAW_{\text{merged}} = W_0 + \frac{\alpha}{r}BA
Merged adapter

One standard weight matrix serves the adapted model, so the LoRA branch adds no inference operation. Switching tasks requires another merged copy or a merge change.

Separate adapter

One base model can load or select many small adapters. The runtime must execute the branch and coordinate batches whose requests may use different adapters.

Merging does not discard the need for the original adapter artifact if future unmerging, auditing, or reuse is required. Production systems commonly preserve immutable base weights and create derived merged weights separately.

11. Account for LoRA Memory Correctly

LoRA's largest savings concern trainable parameter state and task-specific storage. It does not make every part of fine-tuning or inference small.

Base weights

The complete pretrained model still has to be loaded unless another technique, such as quantization or sharding, changes its representation.

Gradients and optimizer state

These are required for the adapter parameters rather than every frozen base weight, producing the main training-state reduction.

Activations

Forward activations needed for backpropagation still consume memory and depend on batch size, sequence length, checkpointing, and model architecture.

Task checkpoints

Each task stores small adapter weights and metadata instead of another complete fine-tuned model.

Inference weights

A merged model has the base model size. An unmerged deployment holds the base plus the selected adapter tensors.

The original LoRA paper reported large reductions for its studied models and configurations, including a GPT-3 example with far fewer trainable parameters and smaller task checkpoints. Those figures are paper-specific results rather than universal guarantees. The actual reduction depends on selected modules, rank, optimizer, precision, and training system.

13. Minimal LoRA Linear Layer

This compact PyTorch-style implementation shows the tensor shapes, frozen base path, zero-output initialization, scaling, dropout, and merge operation. A production library also handles module replacement, adapter naming, mixed precision, distributed training, serialization, bias policies, and safe merge tracking.

class LoRALinear(nn.Module):
    def __init__(self, base, rank=8, alpha=16, dropout=0.05):
        super().__init__()
        self.base = base
        self.base.weight.requires_grad_(False)
        if self.base.bias is not None:
            self.base.bias.requires_grad_(False)

        d_out, d_in = self.base.weight.shape
        self.A = nn.Parameter(torch.empty(rank, d_in))
        self.B = nn.Parameter(torch.zeros(d_out, rank))
        nn.init.normal_(self.A, mean=0.0, std=0.02)

        self.scale = alpha / rank
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        base_output = self.base(x)
        rank_features = F.linear(self.dropout(x), self.A)
        adapter_output = F.linear(rank_features, self.B)
        return base_output + self.scale * adapter_output

    @torch.no_grad()
    def merge_into_base(self):
        self.base.weight.add_(self.scale * (self.B @ self.A))
Shape checks
  • A has shape [r, d_in].
  • B has shape [d_out, r].
  • B @ A matches the base weight shape.
  • The adapter output matches the base output shape.
Training checks
  • Only intended adapter parameters require gradients.
  • The initial adapter output is exactly zero.
  • Saved configuration reproduces the same scaling.
  • Merged and unmerged outputs agree within numerical tolerance.