LoRA Mathematics › Worked Example: Full Forward and Backward Pass

Michael BrenndoerferDecember 1, 20256 min read
Language AI HandbookAll chapters
Part 35 of 67Parameter-Efficient Fine-tuning
Chapter 256LoRA Mathematics › Worked Example: Full Forward and Backward Pass
Reading section 7 of 12 · Worked Example: Full Forward and Backward Pass · 6 minSection 7 of 12 · 6 min

Book contents

Section 7 of 12 · 6 min read

LoRA Mathematics › Worked Example: Full Forward and Backward PassOne section · Sections in this chapter7 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Concrete numbers solidify the mathematical concepts. This example connects the abstract formulas to actual numerical operations you can verify by hand, making the flow from initialization through gradient computation concrete and verifiable.

We consider a small weight matrix W0∈R4×3W_0 \in \mathbb{R}^{4 \times 3} with a rank r=2r = 2 adaptation. The small dimensions make the calculations manageable while capturing all the needed structure of the real application.

Setup and Initialization

We start with a pre-trained weight matrix and initialize the LoRA matrices following the standard scheme. In a real application, W0W_0 would be a slice of a transformer's weight matrix, but a small example suffices to illustrate the mathematics.

W0=[1.00.5−0.30.21.00.4−0.10.31.00.5−0.20.1]W_0 = \begin{bmatrix} 1.0 & 0.5 & -0.3 \\ 0.2 & 1.0 & 0.4 \\ -0.1 & 0.3 & 1.0 \\ 0.5 & -0.2 & 0.1 \end{bmatrix}

where W0∈R4×3W_0 \in \mathbb{R}^{4 \times 3} encodes the pre-trained transformation: input dimension k=3k = 3, output dimension d=4d = 4.

For the LoRA matrices with rank r=2r = 2, we follow the asymmetric initialization scheme: AA gets small random values, BB gets zeros:

A=[0.3−0.50.20.40.1−0.3],B=[00000000]A = \begin{bmatrix} 0.3 & -0.5 & 0.2 \\ 0.4 & 0.1 & -0.3 \end{bmatrix}, \quad B = \begin{bmatrix} 0 & 0 \\ 0 & 0 \\ 0 & 0 \\ 0 & 0 \end{bmatrix}

where A∈R2×3A \in \mathbb{R}^{2 \times 3} is the down-projection (rank ×\times input) and B∈R4×2B \in \mathbb{R}^{4 \times 2} is the up-projection (output ×\times rank). The total trainable parameters are r(d+k)=2(4+3)=14r(d+k) = 2(4+3) = 14, compared to dk=12dk = 12 for the full matrix (a small example; for real models the savings are vastly larger).

We set α=2\alpha = 2 and r=2r = 2, making the scaling factor αr=1.0\frac{\alpha}{r} = 1.0 for simplicity.

Step 1: Forward Pass at Initialization

Given input x=[1.0,0.5,−0.2]Tx = [1.0, 0.5, -0.2]^T, we trace through the three stages:

Stage 1, pre-trained output: We compute W0xW_0 x by multiplying the frozen weight matrix with the input.

(W0x)1=1.0(1.0)+0.5(0.5)+(−0.3)(−0.2)=1.00+0.25+0.06=1.31(W0x)2=0.2(1.0)+1.0(0.5)+0.4(−0.2)=0.20+0.50−0.08=0.62(W0x)3=(−0.1)(1.0)+0.3(0.5)+1.0(−0.2)=−0.10+0.15−0.20=−0.15(W0x)4=0.5(1.0)+(−0.2)(0.5)+0.1(−0.2)=0.50−0.10−0.02=0.38\begin{aligned} (W_0 x)_1 &= 1.0(1.0) + 0.5(0.5) + (-0.3)(-0.2) = 1.00 + 0.25 + 0.06 = 1.31 \\ (W_0 x)_2 &= 0.2(1.0) + 1.0(0.5) + 0.4(-0.2) = 0.20 + 0.50 - 0.08 = 0.62 \\ (W_0 x)_3 &= (-0.1)(1.0) + 0.3(0.5) + 1.0(-0.2) = -0.10 + 0.15 - 0.20 = -0.15 \\ (W_0 x)_4 &= 0.5(1.0) + (-0.2)(0.5) + 0.1(-0.2) = 0.50 - 0.10 - 0.02 = 0.38 \end{aligned}

So W0x=[1.31,0.62,−0.15,0.38]TW_0 x = [1.31, 0.62, -0.15, 0.38]^T.

Stage 2, LoRA intermediate: We compute z=Axz = Ax, projecting the input down to the rank-2 bottleneck.

z1=0.3(1.0)+(−0.5)(0.5)+0.2(−0.2)=0.30−0.25−0.04=0.01z2=0.4(1.0)+0.1(0.5)+(−0.3)(−0.2)=0.40+0.05+0.06=0.51\begin{aligned} z_1 &= 0.3(1.0) + (-0.5)(0.5) + 0.2(-0.2) = 0.30 - 0.25 - 0.04 = 0.01 \\ z_2 &= 0.4(1.0) + 0.1(0.5) + (-0.3)(-0.2) = 0.40 + 0.05 + 0.06 = 0.51 \end{aligned}

So z=[0.01,0.51]Tz = [0.01, 0.51]^T. These two numbers are the bottleneck representation: all the information the LoRA path has about the input is compressed into these two values.

Stage 3, LoRA output: We compute Δh=Bz\Delta h = Bz. Since B=0B = 0, this gives Δh=[0,0,0,0]T\Delta h = [0, 0, 0, 0]^T.

Stage 4, final output: Adding the pre-trained output and the (zero) LoRA output:

h=W0x+αrBz=[1.31,0.62,−0.15,0.38]T+1.0⋅[0,0,0,0]T=[1.31,0.62,−0.15,0.38]Th = W_0 x + \frac{\alpha}{r} Bz = [1.31, 0.62, -0.15, 0.38]^T + 1.0 \cdot [0, 0, 0, 0]^T = [1.31, 0.62, -0.15, 0.38]^T

At initialization, the output equals the pre-trained output exactly. This confirms that zero-initializing BB preserves the pre-trained model's behavior: no disruption to the knowledge encoded in W0W_0.

Step 2: Gradient Computation at Initialization

With upstream gradient g=[0.1,−0.2,0.3,0.1]Tg = [0.1, -0.2, 0.3, 0.1]^T from the loss and layers above, we compute gradients for both LoRA matrices.

For matrix BB:

∂L∂B=αrgzT=1.0⋅[0.1−0.20.30.1][0.010.51]=[0.0010.051−0.002−0.1020.0030.1530.0010.051]\begin{aligned} \frac{\partial \mathcal{L}}{\partial B} &= \frac{\alpha}{r} g z^T \\ &= 1.0 \cdot \begin{bmatrix} 0.1 \\ -0.2 \\ 0.3 \\ 0.1 \end{bmatrix} \begin{bmatrix} 0.01 & 0.51 \end{bmatrix} \\ &= \begin{bmatrix} 0.001 & 0.051 \\ -0.002 & -0.102 \\ 0.003 & 0.153 \\ 0.001 & 0.051 \end{bmatrix} \end{aligned}

where:

  • gg: the upstream gradient vector giving the error signal from the loss
  • zT=(Ax)Tz^T = (Ax)^T: the transpose of the bottleneck representation computed in the forward pass
  • The outer product structure means each entry of the gradient receives a signal proportional to the corresponding upstream gradient component and bottleneck activation

For matrix AA:

∂L∂A=αrBTgxT=1.0⋅[00000000][0.1−0.20.30.1][1.00.5−0.2]=[000000]\begin{aligned} \frac{\partial \mathcal{L}}{\partial A} &= \frac{\alpha}{r} B^T g x^T \\ &= 1.0 \cdot \begin{bmatrix} 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \end{bmatrix} \begin{bmatrix} 0.1 \\ -0.2 \\ 0.3 \\ 0.1 \end{bmatrix} \begin{bmatrix} 1.0 & 0.5 & -0.2 \end{bmatrix} \\ &= \begin{bmatrix} 0 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix} \end{aligned}

where BT=0B^T = 0 because BB is currently zero, making the entire gradient for AA exactly zero.

As expected from our theoretical analysis, BB receives non-zero gradients while AA's gradient is zero at initialization. The gradient for BB is nonzero because it does not depend on the current value of BB, only on the upstream signal gg and the intermediate activation zz. The gradient for AA is zero because it involves BTB^T, which is currently the zero matrix.

Step 3: First Update and Bootstrap

After one gradient descent step with learning rate η=0.01\eta = 0.01:

Bnew=B−η∂L∂B=[00000000]−0.01[0.0010.051−0.002−0.1020.0030.1530.0010.051]=[−0.00001−0.000510.000020.00102−0.00003−0.00153−0.00001−0.00051]B_{\text{new}} = B - \eta \frac{\partial \mathcal{L}}{\partial B} = \begin{bmatrix} 0 & 0 \\ 0 & 0 \\ 0 & 0 \\ 0 & 0 \end{bmatrix} - 0.01 \begin{bmatrix} 0.001 & 0.051 \\ -0.002 & -0.102 \\ 0.003 & 0.153 \\ 0.001 & 0.051 \end{bmatrix} = \begin{bmatrix} -0.00001 & -0.00051 \\ 0.00002 & 0.00102 \\ -0.00003 & -0.00153 \\ -0.00001 & -0.00051 \end{bmatrix}

Now B≠0B \neq 0. On the next forward pass, BTgB^T g will produce a non-zero vector, and AA will begin receiving gradients. The bootstrap phase is complete: after just one step, both matrices are active participants in the optimization.

This example illustrates the entire LoRA training loop in miniature: the forward pass flows through the bottleneck, the backward pass produces asymmetric gradients, and the first update breaks the symmetry to enable joint optimization. All subsequent steps involve co-evolution of AA and BB.