Type at least 2 letters. Enter opens the first result, Esc closes.

    Linear Algebra · Lesson 15 · Linear algebra for modern ML

    Linear algebra of modern ML: autodiff, LoRA/GaLore, tensor decompositions, attention

    Advanced 85 min 5 interactive figures 4 videos Colab notebook

    Before this lesson: lesson 01 (matrices as maps and the views of matrix multiplication), lesson 02 (rank) and lesson 08 (the SVD and Eckart–Young–Mirsky). Time: about 85 minutes, plus the notebook.


    Why this matters #

    Four central tools of modern ML are linear algebra in disguise. Every gradient is a chain of transposed Jacobians applied to a vector (VJPs). LoRA and GaLore exploit low rank, which is Eckart–Young at work. CP and Tucker extend the SVD to data with more than two indices, such as climate fields over space × time × variable × ensemble member. An attention head is a low-rank bilinear form (which token to read from) plus a low-rank linear map (what to copy), acting on a residual stream that is a vector space. The same ideas decide memory and compute when you fine-tune or train a scientific foundation model.

    1. Predict first #

    2. The picture #

    A derivative is a linear map. Near xx, f(x+δ)≈f(x)+Jδf(x + \delta) \approx f(x) + J\delta, with the Jacobian J∈Rm×nJ \in \mathbb{R}^{m\times n}: the matrix of all partial derivatives Jij=∂fi/∂xjJ_{ij} = \partial f_i/\partial x_j. For a chain f=fL∘⋯∘f1f = f_L \circ \dots \circ f_1 the chain rule gives a product J=JL⋯J1J = J_L\cdots J_1. You never need JJ itself, only JJ times a vector, and the product can be evaluated from either end:

    • right to left with an input direction vv: JL(⋯(J1v))J_L(\cdots(J_1v)). This is forward mode (JVP);
    • left to right with an output weighting uu: ((u⊤JL)JL−1)⋯J1((u^\top J_L)J_{L-1})\cdots J_1. This is reverse mode (VJP), and backpropagation is its special case u=1u = 1 for a scalar loss.

    Each order costs only matrix-vector products. Which one wins depends on the shape of JJ.

    Interactive figure: loading…

    Build the Jacobian one pass at a time. Press Run all passes in both modes. A forward pass (JVP) with v=ekv = e_k gives column kk of JJ; a reverse pass (VJP) with u=eku = e_k gives row kk. For a scalar loss (m=1m = 1) one reverse pass gives the whole gradient. Switch to the simulator shape (n=3n = 3 inputs, m=12m = 12 outputs) and forward mode wins.

    Why reverse mode wins for a scalar loss: the multiplication order. Matrix multiplication is associative, so J3J2J1J_3J_2J_1 is the same matrix whatever pair you multiply first. The cost is not the same. A product of an a×ba\times b matrix and a b×cb\times c matrix needs abcabc multiplications. Take a loss after two hidden layers of width 1000 on an input of size 1000: J3J_3 is 1×10001\times1000, J2J_2 and J1J_1 are 1000×10001000\times1000.

    1. From the left (reverse). J3J2J_3J_2 is (1×1000)(1000×1000)(1\times1000)(1000\times1000): 10610^6 multiplications, and the result is again a 1×10001\times1000 row. Then (J3J2)J1(J_3J_2)J_1: another 10610^6. Total 2×1062\times10^6.
    2. From the right (forward). J2J1J_2J_1 is (1000×1000)(1000×1000)(1000\times1000)(1000\times1000): 10910^9 multiplications. Then J3(J2J1)J_3(J_2J_1): 10610^6. Total about 10910^9, 500 times more.

    Starting at the scalar loss keeps every intermediate result a single row vector. Starting at the input carries one column per input, so the work grows with the number of inputs.

    Interactive figure: loading…

    Choose the order and press Next product. The blocks J4,J3,J2,J1J_4, J_3, J_2, J_1 are drawn to scale (height = rows, width = columns); the highlighted pair is multiplied next. With the loss shape, reverse needs 224 multiplications and forward 1632. Switch to the simulator shape (2 inputs, 12 outputs): now forward is cheaper. The readout also gives the cheapest order of all.

    Quick check

    J=J3J2J1J = J_3J_2J_1 with J3J_3 of size 1×10001\times1000 and J2J_2, J1J_1 of size 1000×10001000\times1000. You multiply from the left: (J3J2)J1(J_3J_2)J_1. How many multiplications?

    Low rank = few outer products. A rank-rr matrix BA=∑i=1rbiai⊤BA = \sum_{i=1}^r b_ia_i^\top is stored with r(d+k)r(d + k) numbers instead of dkdk. LoRA restricts a weight update to this form.

    Interactive figure: loading…

    Spend a rank budget. Move the rank slider rr. The middle map is the best rank-rr matrix BABA (the truncated SVD), and the right map is what it misses. When the singular values of the target update decay fast, a small rr captures almost all of it with 2dr≪d22dr \ll d^2 parameters. Choose flat and low rank fails: the relative error falls only like 1−r/d\sqrt{1 - r/d}.

    A tensor is an array with more indices. The simplest tensor is a rank-1 tensor a∘b∘ca \circ b \circ c with entries aibjcka_ib_jc_k. A sum of RR of them is a CP model; a small core with a basis per index is a Tucker model.

    Attention moves information between tokens. Token ii forms a weighted average of (projected) other tokens. The weights come from a bilinear form "query of ii against key of jj"; the projection decides what is copied and where it is written.

    3. Definitions and main results #

    JVP and VJP #

    Let f:Rn→Rmf : \mathbb{R}^n \to \mathbb{R}^m be differentiable at xx, with Jacobian J=∂f/∂x∈Rm×nJ = \partial f/\partial x \in \mathbb{R}^{m\times n}.

    • The JVP (Jacobian-vector product) maps a tangent v∈Rnv \in \mathbb{R}^n to Jv∈RmJv \in \mathbb{R}^m.
    • The VJP (vector-Jacobian product) maps a cotangent u∈Rmu \in \mathbb{R}^m to J⊤u∈RnJ^\top u \in \mathbb{R}^n (the row vector u⊤Ju^\top J).

    Notation: for a scalar loss LL, a bar denotes its gradient with respect to a variable: yˉ=∂L/∂y\bar y = \partial L/\partial y. If y=f(x)y = f(x), the chain rule gives xˉ=J⊤yˉ\bar x = J^\top\bar y: a VJP.

    • Forward mode carries (value, tangent) pairs through the computation, in the same order. Each primitive operation has a JVP rule.
    • Reverse mode runs the computation once and records the graph and its intermediate values (the tape). A backward pass then applies each primitive's VJP rule in reverse order, starting from Lˉ=1\bar L = 1. Where a variable feeds several operations, the contributions add.

    Theorem (VJP and JVP of a linear layer) #

    Let y=Wxy = Wx with W∈Rm×nW \in \mathbb{R}^{m\times n}, x∈Rnx \in \mathbb{R}^n, and yˉ=∂L/∂y\bar y = \partial L/\partial y. Then

    xˉ=W⊤yˉ,Wˉ=yˉx⊤ (rank 1),y˙=Wx˙+W˙x (JVP).\bar x = W^\top\bar y, \qquad \bar W = \bar yx^\top \ (\text{rank } 1), \qquad \dot y = W\dot x + \dot Wx \ (\text{JVP}).

    Proof.

    1. Product rule: dy=dW x+W dxdy = dW\,x + W\,dx. This is the JVP with (x˙,W˙)=(dx,dW)(\dot x, \dot W) = (dx, dW).
    2. The loss changes by dL=yˉ⊤dy=yˉ⊤W dx+yˉ⊤dW xdL = \bar y^\top dy = \bar y^\top W\,dx + \bar y^\top dW\,x.
    3. First term: yˉ⊤W dx=(W⊤yˉ)⊤dx\bar y^\top W\,dx = (W^\top\bar y)^\top dx, so xˉ=W⊤yˉ\bar x = W^\top\bar y.
    4. Second term: yˉ⊤dW x=tr⁡(yˉ⊤dW x)=tr⁡(xyˉ⊤dW)=⟨yˉx⊤,dW⟩F\bar y^\top dW\,x = \operatorname{tr}(\bar y^\top dW\,x) = \operatorname{tr}(x\bar y^\top dW) = \langle\bar yx^\top, dW\rangle_F, with the Frobenius inner product ⟨P,Q⟩F=tr⁡(P⊤Q)\langle P, Q\rangle_F = \operatorname{tr}(P^\top Q). So Wˉ=yˉx⊤\bar W = \bar yx^\top. ■\blacksquare

    Check every shape. The safest way to remember these formulas is to write the shape next to every symbol. A gradient always has the shape of its variable, and in each case only one product of the available matrices has that shape.

    quantity shape formula shape check
    xx, xˉ\bar x n×1n\times1 xˉ=W⊤yˉ\bar x = W^\top\bar y (n×m)(m×1)=n×1(n\times m)(m\times1) = n\times1
    WW, Wˉ\bar W m×nm\times n Wˉ=yˉx⊤\bar W = \bar yx^\top (m×1)(1×n)=m×n(m\times1)(1\times n) = m\times n
    XX, Xˉ\bar X (batch of BB columns) n×Bn\times B Xˉ=W⊤Yˉ\bar X = W^\top\bar Y (n×m)(m×B)=n×B(n\times m)(m\times B) = n\times B
    WW, Wˉ\bar W (batch) m×nm\times n Wˉ=YˉX⊤=∑byˉbxb⊤\bar W = \bar YX^\top = \sum_b\bar y_bx_b^\top (m×B)(B×n)=m×n(m\times B)(B\times n) = m\times n

    For a batch Y=WXY = WX with examples as columns, the weight gradient Wˉ=YˉX⊤\bar W = \bar YX^\top is a sum of BB outer products, so its rank is at most the batch size BB. For any product C=ABC = AB: Aˉ=CˉB⊤\bar A = \bar CB^\top and Bˉ=A⊤Cˉ\bar B = A^\top\bar C. PyTorch's nn.Linear uses rows, y=xW⊤+by = xW^\top + b; the same rules hold, transposed.

    Quick check

    Y=WXY = WX with WW of size 4×34\times3 and XX of size 3×53\times5 (5 examples as columns). The upstream gradient is Yˉ\bar Y (4×54\times5). Which expression is Wˉ\bar W?

    Cost of training. The forward product WxWx costs 2mn2mn flops per example; the backward pass needs W⊤yˉW^\top\bar y and yˉx⊤\bar yx^\top, another 2mn2mn each. So a training step costs about three forward passes, which gives the usual estimate "training flops ≈6×\approx 6 \times parameters ×\times tokens".

    Forward or reverse mode? #

    The full Jacobian needs nn JVPs (one per column) or mm VJPs (one per row). Each pass costs a small constant times the cost of ff: below 6 and typically 2–3 (Griewank and Walther, cited by Baydin et al.).

    forward mode (JVP) reverse mode (VJP)
    one pass gives JvJv, one input direction u⊤Ju^\top J, one output weighting
    full Jacobian nn passes mm passes
    extra memory a tangent per value, no tape all intermediates of the forward pass
    wins when few inputs, many outputs few outputs, many inputs

    Training has m=1m = 1 (a scalar loss) and nn up to 101210^{12} parameters, so one reverse pass gives the whole gradient. The price is memory: reverse mode stores the activations. Checkpointing stores only some of them and recomputes the rest in the backward pass. A Hessian-vector product is the JVP of the gradient (forward over reverse): it costs a few gradient evaluations and never forms the Hessian, which is what Lanczos needs (lesson 14).

    LoRA #

    LoRA (low-rank adaptation; Hu et al., 2021) freezes a pretrained W0∈Rd×kW_0 \in \mathbb{R}^{d\times k} and learns a low-rank update:

    h=W0x+αrBAx,B∈Rd×r, A∈Rr×k, r≪min⁡(d,k).h = W_0x + \tfrac{\alpha}{r}BAx, \qquad B \in \mathbb{R}^{d\times r},\ A \in \mathbb{R}^{r\times k},\ r \ll \min(d, k).

    In two plain sentences. BB starts at zero, so BA=0BA = 0 and the model at step 0 is exactly the pretrained model; fine-tuning starts from what the model already knows. The fixed factor α/r\alpha/r scales the update so that, with Adam, the first updates have about the same size for every rank rr, and you do not need to tune the learning rate again when you change rr.

    The details:

    • Parameters: r(d+k)r(d + k) instead of dkdk. Gradients and Adam states exist only for these.
    • Rank: rank⁡(BA)≤r\operatorname{rank}(BA) \le r by the outer-product view. Both AA and BB are trained, so the column space of BABA moves during training. Only the rank bound is fixed; LoRA does not fix one low-rank subspace.
    • Initialization: AA random Gaussian, B=0B = 0, so BA=0BA = 0 and training starts exactly at W0W_0. Let G=∂L/∂WG = \partial L/\partial W be the gradient with respect to the effective weight W=W0+αrBAW = W_0 + \tfrac{\alpha}{r}BA. Then dW=αr(dB A+B dA)dW = \tfrac{\alpha}{r}(dB\,A + B\,dA) and dL=⟨G,dW⟩FdL = \langle G, dW\rangle_F give
    Bˉ=αrGA⊤,Aˉ=αrB⊤G.\bar B = \tfrac{\alpha}{r}GA^\top, \qquad \bar A = \tfrac{\alpha}{r}B^\top G.

    At the start Aˉ=0\bar A = 0 but Bˉ≠0\bar B \ne 0, so BB moves first. If both were zero, both gradients would stay zero forever.

    • Scaling α/r\alpha/r keeps the size of the early updates, and so the best learning rate, roughly independent of rr. Hu et al. set α\alpha once and do not tune it.
    • Inference: merge W=W0+αrBAW = W_0 + \tfrac{\alpha}{r}BA: no extra latency. One frozen base can serve many adapters, and it can be stored quantized (QLoRA).

    Why low-rank updates are plausible #

    1. Structure of the gradient. By the theorem above, one example gives a rank-1 gradient yˉx⊤\bar yx^\top, and a batch of TT tokens a gradient of rank at most TT. In large batches TT exceeds dd, so this bound alone proves little; but the singular values of gradients and of fine-tuning updates decay fast in practice.
    2. Intrinsic dimension. Fine-tuning RoBERTa inside a random 200-dimensional subspace of parameter space reached 90% of full fine-tuning performance on MRPC (Aghajanyan et al., 2020).
    3. The LoRA paper (§7). Very small ranks work (r=1r = 1 for GPT-3 when adapting WqW_q and WvW_v); the update amplifies directions that W0W_0 already contains but does not emphasize.
    4. LoRA Without Regret (Thinking Machines, 2025). LoRA applied to all layers, especially the MLPs, matches full fine-tuning on small and medium supervised datasets, and in reinforcement learning even at rank 1 (policy-gradient RL gives only O(1)O(1) bits of information per episode). It falls behind when the dataset exceeds the adapter's capacity. Its best learning rate is about 10 times that of full fine-tuning.

    These are empirical findings, not theorems.

    GaLore: full-rank training with a low-rank optimizer state

    GaLore (Zhao et al., 2024) trains the full W∈Rm×nW \in \mathbb{R}^{m\times n} (m≤nm \le n) but keeps Adam's state in a low-rank space. Every TT steps (T=200T = 200 in the paper) it computes the SVD of the current gradient GG and sets P∈Rm×rP \in \mathbb{R}^{m\times r} to the top rr left singular vectors. At every step:

    R=P⊤G∈Rr×n,N=Adam step computed on R,W←W−η α PN.R = P^\top G \in \mathbb{R}^{r\times n}, \qquad N = \text{Adam step computed on } R, \qquad W \leftarrow W - \eta\,\alpha\,PN.

    Adam's two moments have size r×nr \times n instead of m×nm \times n. Each update has rank ≤r\le r, but PP changes, so the sum of updates can have full rank.

    LoRA GaLore
    what is low rank the update ΔW=BA\Delta W = BA the gradient, for the optimizer state
    trainable parameters r(d+k)r(d + k) all mnmn
    saves gradients, optimizer state, per-task storage optimizer state
    total update rank ≤r\le r can be full rank
    typical use fine-tuning, many adapters memory-limited pretraining or full fine-tuning

    Tensors: fibers, unfoldings, CP and Tucker #

    A third-order tensor X∈RI×J×K\mathcal X \in \mathbb{R}^{I\times J\times K} is an array with entries xijkx_{ijk}. The outer product a∘b∘ca \circ b \circ c has entries aibjcka_ib_jc_k (a rank-1 tensor).

    • A mode-nn fiber fixes all indices but the nn-th (for n=1n = 1: the vector x:jkx_{:jk}).
    • The mode-nn unfolding X(n)X_{(n)} is the matrix whose columns are the mode-nn fibers: I×JKI \times JK for n=1n = 1. We order the columns with the first remaining index varying fastest (Kolda and Bader). The column order is a convention; it changes neither the rank nor the left singular vectors.
    • The mode-nn product X×nU\mathcal X \times_n U multiplies every mode-nn fiber by UU: (X×1U)(1)=UX(1)(\mathcal X \times_1 U)_{(1)} = UX_{(1)}.

    One small tensor, step by step. Let X∈R2×2×2\mathcal X \in \mathbb{R}^{2\times2\times2} have the two frontal slices (k=1k = 1 and k=2k = 2)

    x::1=(1234),x::2=(5678).x_{::1} = \begin{pmatrix} 1 & 2 \\ 3 & 4\end{pmatrix}, \qquad x_{::2} = \begin{pmatrix} 5 & 6 \\ 7 & 8\end{pmatrix}.
    1. Mode-1 fibers (index ii varies): the columns of each slice. In the order (j,k)=(1,1),(2,1),(1,2),(2,2)(j, k) = (1,1), (2,1), (1,2), (2,2) they are (1,3)(1, 3), (2,4)(2, 4), (5,7)(5, 7), (6,8)(6, 8).
    2. Mode-2 fibers (index jj varies): the rows of each slice. In the order (i,k)=(1,1),(2,1),(1,2),(2,2)(i, k) = (1,1), (2,1), (1,2), (2,2) they are (1,2)(1, 2), (3,4)(3, 4), (5,6)(5, 6), (7,8)(7, 8).
    3. Mode-3 fibers (index kk varies): from the front slice to the back slice. In the order (i,j)=(1,1),(2,1),(1,2),(2,2)(i, j) = (1,1), (2,1), (1,2), (2,2) they are (1,5)(1, 5), (3,7)(3, 7), (2,6)(2, 6), (4,8)(4, 8).
    4. Put the fibers side by side as columns:
    X(1)=(12563478),X(2)=(13572468),X(3)=(13245768).\begin{gathered} X_{(1)} = \begin{pmatrix} 1 & 2 & 5 & 6 \\ 3 & 4 & 7 & 8\end{pmatrix}, \qquad X_{(2)} = \begin{pmatrix} 1 & 3 & 5 & 7 \\ 2 & 4 & 6 & 8\end{pmatrix}, \\ X_{(3)} = \begin{pmatrix} 1 & 3 & 2 & 4 \\ 5 & 7 & 6 & 8\end{pmatrix}. \end{gathered}
    1. A mode product. With U=(1    1)U = (1 \;\; 1) (1×21\times2), X×1U\mathcal X \times_1 U adds the two entries of every mode-1 fiber: UX(1)=(4    6    12    14)UX_{(1)} = (4 \;\; 6 \;\; 12 \;\; 14). The result is a 1×2×21\times2\times2 tensor with slices (4    6)(4 \;\; 6) and (12    14)(12 \;\; 14).
    Interactive figure: loading…

    Choose a mode and press Next fiber. The coloured lines in the 3×4×23\times4\times2 cube are the mode-nn fibers; the black one is the selected fiber, and it becomes the outlined column of the unfolding below. With counting 1…24, the mode-1 columns read the numbers in order. Choose ranks differ: the three unfoldings have ranks (1,2,2)(1, 2, 2), so the entries of the multilinear rank need not be equal.

    CP (CANDECOMP/PARAFAC) of rank RR:

    X≈∑r=1Rar∘br∘cr,xijk≈∑rairbjrckr,\mathcal X \approx \sum_{r=1}^R a_r \circ b_r \circ c_r, \qquad x_{ijk} \approx \sum_r a_{ir}b_{jr}c_{kr},

    with factor matrices AA (I×RI\times R), BB (J×RJ\times R), CC (K×RK\times R): R(I+J+K)R(I + J + K) parameters (plus RR weights λr\lambda_r if the columns are normalized). The CP rank is the smallest RR with equality.

    Tucker with core size R1×R2×R3R_1\times R_2\times R_3:

    X≈G×1A×2B×3C,xijk≈∑p,q,sgpqsaipbjqcks,\mathcal X \approx \mathcal G \times_1 A \times_2 B \times_3 C, \qquad x_{ijk} \approx \sum_{p,q,s} g_{pqs}a_{ip}b_{jq}c_{ks},

    with R1R2R3+IR1+JR2+KR3R_1R_2R_3 + IR_1 + JR_2 + KR_3 parameters. CP is Tucker with a superdiagonal core (grrr=λrg_{rrr} = \lambda_r, all other entries 0). For a matrix (N=2N = 2 indices), Tucker is UGV⊤UGV^\top and the SVD is the case with a diagonal core. The multilinear rank is (rank⁡X(1),rank⁡X(2),rank⁡X(3))(\operatorname{rank}X_{(1)}, \operatorname{rank}X_{(2)}, \operatorname{rank}X_{(3)}); unlike row and column rank of a matrix, its entries can differ.

    Quick check

    X\mathcal X is a 3×4×23\times4\times2 tensor. What is the shape of its mode-2 unfolding X(2)X_{(2)}?

    Truncated HOSVD. For each mode, UnU_n = the top RnR_n left singular vectors of X(n)X_{(n)}. Then

    G=X×1U1⊤×2U2⊤×3U3⊤,X^=G×1U1×2U2×3U3=X×1P1×2P2×3P3,\mathcal G = \mathcal X \times_1 U_1^\top \times_2 U_2^\top \times_3 U_3^\top, \qquad \hat{\mathcal X} = \mathcal G \times_1 U_1 \times_2 U_2 \times_3 U_3 = \mathcal X \times_1 P_1 \times_2 P_2 \times_3 P_3,

    with the orthogonal projectors Pn=UnUn⊤P_n = U_nU_n^\top. It is the analogue of UkUk⊤AVkVk⊤U_kU_k^\top AV_kV_k^\top for a matrix. The core is not diagonal in general: a tensor cannot be diagonalized by one orthogonal change of basis per mode.

    Theorem (truncated HOSVD is quasi-optimal). For an NN-th order tensor, let Xbest\mathcal X_{\text{best}} be the best approximation with multilinear rank ≤(R1,…,RN)\le (R_1, \dots, R_N). Then

    ∥X−X^∥F2≤∑n=1N∑i>Rnσi(X(n))2≤N ∥X−Xbest∥F2.\|\mathcal X - \hat{\mathcal X}\|_F^2 \le \sum_{n=1}^N\sum_{i > R_n}\sigma_i(X_{(n)})^2 \le N\,\|\mathcal X - \mathcal X_{\text{best}}\|_F^2.
    Proof for N=3N = 3 (telescoping and Eckart–Young per mode)
    1. Telescope: X−X^=X×1(I−P1)+X×1P1×2(I−P2)+X×1P1×2P2×3(I−P3)\mathcal X - \hat{\mathcal X} = \mathcal X \times_1 (I - P_1) + \mathcal X \times_1 P_1 \times_2 (I - P_2) + \mathcal X \times_1 P_1 \times_2 P_2 \times_3 (I - P_3).
    2. The three terms are orthogonal: the first has mode-1 fibers in the range of I−P1I - P_1, the other two in the range of P1P_1; the second and third differ in the same way in mode 2. So the squared norms add.
    3. Mode products in different modes commute, and an orthogonal projection does not increase the norm. So, for example, ∥X×1P1×2(I−P2)∥≤∥X×2(I−P2)∥\|\mathcal X \times_1 P_1 \times_2 (I - P_2)\| \le \|\mathcal X \times_2 (I - P_2)\|.
    4. ∥X×n(I−Pn)∥F2=∥(I−Pn)X(n)∥F2=∑i>Rnσi(X(n))2\|\mathcal X \times_n (I - P_n)\|_F^2 = \|(I - P_n)X_{(n)}\|_F^2 = \sum_{i > R_n}\sigma_i(X_{(n)})^2, because PnP_n projects on the top RnR_n left singular vectors (Eckart–Young, lesson 08). This gives the first inequality.
    5. Any Y\mathcal Y with multilinear rank ≤(Rn)\le (R_n) has rank⁡Y(n)≤Rn\operatorname{rank}Y_{(n)} \le R_n, so by Eckart–Young ∥X−Y∥F2=∥X(n)−Y(n)∥F2≥∑i>Rnσi(X(n))2\|\mathcal X - \mathcal Y\|_F^2 = \|X_{(n)} - Y_{(n)}\|_F^2 \ge \sum_{i > R_n}\sigma_i(X_{(n)})^2 for every nn. Take Y=Xbest\mathcal Y = \mathcal X_{\text{best}} and add over nn. ■\blacksquare

    So the truncated HOSVD is within a factor N\sqrt N of the best Tucker model, but in general not optimal: there is no Eckart–Young theorem for tensors. HOOI (alternating least squares on the factors, started from the HOSVD) usually improves it.

    CP rank is harder than matrix rank. It can depend on the field (a real 2×2×22\times2\times2 tensor can have rank 3 over R\mathbb{R} and 2 over C\mathbb{C}), and computing it is NP-hard. A best rank-RR approximation may not exist (rank-2 tensors can converge to a rank-3 tensor: degeneracy), and the best rank-1 term need not be part of the best rank-2 model, so greedy deflation fails. But CP is often essentially unique (Kruskal's condition), so its factors can be interpreted; a matrix factorization BABA is not unique.

    Attention in matrix form #

    We use the column convention of Elhage et al. (2021). Token ii is the residual-stream vector xi∈Rdx_i \in \mathbb{R}^d, and X=[x1⋯xn]∈Rd×nX = [x_1 \cdots x_n] \in \mathbb{R}^{d\times n}. One head of width dhd_h has WQ,WK,WV∈Rdh×dW_Q, W_K, W_V \in \mathbb{R}^{d_h\times d} and WO∈Rd×dhW_O \in \mathbb{R}^{d\times d_h}. With qi=WQxiq_i = W_Qx_i, kj=WKxjk_j = W_Kx_j, vj=WVxjv_j = W_Vx_j:

    sij=qi⊤kjdh=xi⊤WQK xjdh,WQK=WQ⊤WK,Aij=esij∑j′esij′,s_{ij} = \frac{q_i^\top k_j}{\sqrt{d_h}} = \frac{x_i^\top W_{QK}\,x_j}{\sqrt{d_h}}, \quad W_{QK} = W_Q^\top W_K, \qquad A_{ij} = \frac{e^{s_{ij}}}{\sum_{j'}e^{s_{ij'}}},
    hi=WO∑jAijvj=∑jAij WOVxj,WOV=WOWV,H=WOV X A⊤∈Rd×n.h_i = W_O\sum_jA_{ij}v_j = \sum_jA_{ij}\,W_{OV}x_j, \quad W_{OV} = W_OW_V, \qquad H = W_{OV}\,X\,A^\top \in \mathbb{R}^{d\times n}.

    A causal mask sets sij=−∞s_{ij} = -\infty for j>ij > i. The layer adds HH to the residual stream: X←X+∑headsHX \leftarrow X + \sum_{\text{heads}} H (then an MLP adds its output in the same way).

    • WQKW_{QK} (the QK circuit) is a d×dd \times d matrix of rank ≤dh\le d_h. The score is a bilinear form: the query side reads the subspace C(WQ⊤)C(W_Q^\top) of the residual stream, the key side reads C(WK⊤)C(W_K^\top).
    • WOVW_{OV} (the OV circuit) is a d×dd \times d map of rank ≤dh\le d_h. In its SVD WOV=UΣV⊤W_{OV} = U\Sigma V^\top, the right singular vectors say what is read from the source token and the left singular vectors say where it is written in the destination token.
    • Only these products matter. For any invertible M,M′∈Rdh×dhM, M' \in \mathbb{R}^{d_h\times d_h}, replacing WQ→MWQW_Q \to MW_Q, WK→M−⊤WKW_K \to M^{-\top}W_K, WV→M′WVW_V \to M'W_V, WO→WOM′−1W_O \to W_OM'^{-1} changes no output. Queries, keys and values are intermediate results of two low-rank matrices.
    • Linear once AA is fixed. In vectorized form vec⁡(H)=(A⊗WOV)vec⁡(X)\operatorname{vec}(H) = (A \otimes W_{OV})\operatorname{vec}(X) (lesson 16 proves the rule vec⁡(AXB)=(B⊤⊗A)vec⁡X\operatorname{vec}(AXB) = (B^\top \otimes A)\operatorname{vec}X): AA mixes tokens, WOVW_{OV} acts on each token's vector. The softmax inside AA is the only nonlinear part.
    • Heads are independent and additive. "Concatenate the head results rh=∑jAijhvjhr^h = \sum_j A^h_{ij}v^h_j, then multiply by WOW_O" equals ∑hWOhrh\sum_h W_O^hr^h, where WOhW_O^h is the block of columns of WOW_O for head hh. Every head reads from the residual stream with linear maps and writes back into a subspace of dimension at most dhd_h (usually dh=d/Hd_h = d/H for HH heads).
    • Low rank is in the logits, not in AA. S=X⊤WQKX/dhS = X^\top W_{QK}X/\sqrt{d_h} (n×nn\times n) has rank ≤dh\le d_h, but the entrywise exponential makes AA full rank in general.
    • Why dh\sqrt{d_h}. If qq and kk have independent entries with mean 0 and variance 1, then q⊤kq^\top k has variance dhd_h. Dividing by dh\sqrt{d_h} keeps the logits of order 1, so the softmax does not saturate.
    Interactive figure: loading…

    One head, six tokens, dh=2d_h = 2. The readout shows rank⁡(βS)=2\operatorname{rank}(\beta S) = 2 but rank⁡A=6\operatorname{rank} A = 6: the low rank is in the logits, not in AA. Move the scale β\beta (an inverse temperature) to 0: every row of AA becomes uniform, AA has rank 1, and the output is the mean of the values. Move it up: the rows become nearly one-hot and the output jumps to one value vector. Turn on the causal mask and pick t1t_1: the first token can only attend to itself.

    Quick check

    One head has dh=64d_h = 64, and the sequence has n=2048n = 2048 tokens. What can you say about the ranks of the logit matrix SS and of the attention matrix A=softmax⁡(S)A = \operatorname{softmax}(S) (row by row)?

    Code uses rows. With Xr=X⊤∈Rn×dX_r = X^\top \in \mathbb{R}^{n\times d} and Q=XrWQ⊤Q = X_rW_Q^\top (what nn.Linear computes), the logits are QK⊤/dh=XrWQ⊤WKXr⊤/dhQK^\top/\sqrt{d_h} = X_rW_Q^\top W_KX_r^\top/\sqrt{d_h}: the same WQKW_{QK}. The output rows are AXrWV⊤WO⊤=AXrWOV⊤AX_rW_V^\top W_O^\top = AX_rW_{OV}^\top: the map is the same, written as a right product with the transpose.

    Answers to section 1. (1) LL: reverse mode, one pass (m=1m = 1). gg: forward mode, 3 passes (n=3n = 3), instead of 10610^6 reverse passes. (2) rank⁡WQK≤128\operatorname{rank}W_{QK} \le 128. The logit matrix also has rank ≤128\le 128, but AA itself can have full rank 1000.

    4. Worked example #

    A. One linear layer, both modes. W=(210113)W = \begin{pmatrix} 2 & 1 \\ 0 & 1 \\ 1 & 3\end{pmatrix} (3×23\times2), x=(1,2)x = (1, 2), upstream gradient yˉ=(1,−1,1)\bar y = (1, -1, 1).

    • Forward: y=Wx=1⋅(2,0,1)+2⋅(1,1,3)=(4,2,7)y = Wx = 1\cdot(2, 0, 1) + 2\cdot(1, 1, 3) = (4, 2, 7).
    • VJP for xx: xˉ=W⊤yˉ=(2⋅1+0⋅(−1)+1⋅1, 1⋅1+1⋅(−1)+3⋅1)=(3,3)\bar x = W^\top\bar y = (2\cdot1 + 0\cdot(-1) + 1\cdot1,\ 1\cdot1 + 1\cdot(-1) + 3\cdot1) = (3, 3). Shape: (2×3)(3×1)=2×1(2\times3)(3\times1) = 2\times1, like xx.
    • VJP for WW: Wˉ=yˉx⊤=(12−1−212)\bar W = \bar yx^\top = \begin{pmatrix} 1 & 2 \\ -1 & -2 \\ 1 & 2\end{pmatrix}, shape 3×23\times2 like WW, rank 1 (every row is a multiple of x⊤=(1,2)x^\top = (1, 2)).
    • Check: for L=yˉ⊤WxL = \bar y^\top Wx, increasing x1x_1 by 10−610^{-6} increases LL by 3×10−6=xˉ1⋅10−63\times10^{-6} = \bar x_1\cdot10^{-6}.
    • JVP with x˙=e1\dot x = e_1, W˙=0\dot W = 0: y˙=We1=(2,0,1)\dot y = We_1 = (2, 0, 1), the first column of J=WJ = W. The full 3×23\times2 Jacobian takes 2 JVPs or 3 VJPs.

    B. LoRA and GaLore memory. A 4096×40964096 \times 4096 attention projection with r=16r = 16: LoRA trains 16⋅8192=131,07216 \cdot 8192 = 131{,}072 numbers instead of 16,777,21616{,}777{,}216 (0.78%), and Adam keeps 262,144262{,}144 moment entries instead of 33,554,43233{,}554{,}432. GaLore with r=128r = 128 on a 4096×110084096 \times 11008 MLP matrix trains all 45,088,76845{,}088{,}768 weights, but its Adam moments have 2⋅128⋅11008=2,818,0482 \cdot 128 \cdot 11008 = 2{,}818{,}048 entries, plus 4096⋅128=524,2884096 \cdot 128 = 524{,}288 for PP: 3.7% of the 90,177,53690{,}177{,}536 entries of full Adam.

    Quick check

    LoRA with rank r=8r = 8 adapts one 4096×40964096\times4096 weight matrix. How many trainable numbers does it add?

    C. A tensor. For the 2×2×22\times2\times2 tensor of section 3, each of the three unfoldings has rank 2, so the multilinear rank is (2,2,2)(2, 2, 2). For counts, take a climate data set with 1000 grid points × 365 days × 10 variables (3,650,0003{,}650{,}000 numbers). CP with R=20R = 20 needs 20⋅(1000+365+10)=20⋅1375=27,50020 \cdot (1000 + 365 + 10) = 20\cdot1375 = 27{,}500 parameters; Tucker with core 20×10×520\times10\times5 needs 20⋅10⋅5+1000⋅20+365⋅10+10⋅5=1000+20,000+3,650+50=24,70020\cdot10\cdot5 + 1000\cdot20 + 365\cdot10 + 10\cdot5 = 1000 + 20{,}000 + 3{,}650 + 50 = 24{,}700.

    D. One attention head by hand. Take d=3d = 3, dh=1d_h = 1, two tokens x1=e1x_1 = e_1 ("has feature 1") and x2=e2x_2 = e_2 ("looks for feature 1"), and a causal mask. Let WQ=e2⊤W_Q = e_2^\top, WK=2e1⊤W_K = 2e_1^\top, WV=e1⊤W_V = e_1^\top, WO=e3W_O = e_3. Then

    WQK=WQ⊤WK=2e2e1⊤,WOV=WOWV=e3e1⊤,W_{QK} = W_Q^\top W_K = 2e_2e_1^\top, \qquad W_{OV} = W_OW_V = e_3e_1^\top,

    both of rank 1=dh1 = d_h. The logits are s11=x1⊤WQKx1=0s_{11} = x_1^\top W_{QK}x_1 = 0, s21=2s_{21} = 2, s22=0s_{22} = 0 (s12s_{12} is masked), so

    A=(100.8810.119)(e2/(e2+1)=0.881).A = \begin{pmatrix} 1 & 0 \\ 0.881 & 0.119\end{pmatrix} \quad (e^2/(e^2 + 1) = 0.881).

    WOVx1=e3W_{OV}x_1 = e_3 and WOVx2=0W_{OV}x_2 = 0, so h1=e3h_1 = e_3 and h2=0.881 e3h_2 = 0.881\,e_3. Token 2 found the token with feature 1 and copied it into coordinate 3 of its own residual vector. (Token 1 can only attend to itself, so it copies its own feature.) The logit matrix (0020)\begin{pmatrix} 0 & 0 \\ 2 & 0\end{pmatrix} (before the mask) has rank 1, while det⁡A=0.119≠0\det A = 0.119 \ne 0: AA has rank 2.

    5. In code #

    The worked example A, both modes, and a Hessian-vector product, with NumPy and hand-written JVP and VJP rules. An autodiff library (torch.func.vjp, jax.vjp, jax.jvp) returns the same numbers.

    import numpy as np
    
    # Worked example A: y = W x, with the upstream gradient ybar
    W = np.array([[2., 1.], [0., 1.], [1., 3.]])
    x = np.array([1., 2.])
    ybar = np.array([1., -1., 1.])
    
    y = W @ x
    xbar = W.T @ ybar                # VJP with respect to x
    Wbar = np.outer(ybar, x)         # VJP with respect to W: rank 1, shape of W
    print(y, xbar)                   # [4. 2. 7.] [3. 3.]
    print(Wbar.tolist(), np.linalg.matrix_rank(Wbar))   # [[1.0, 2.0], [-1.0, -2.0], [1.0, 2.0]] 1
    
    # finite-difference check of xbar for L = ybar^T W x
    L = lambda W, x: ybar @ (W @ x)
    h = 1e-6
    print(round((L(W, x + h * np.array([1., 0.])) - L(W, x)) / h, 6))   # 3.0 = xbar[0]
    
    # forward mode: one JVP with dx = e1, dW = 0 gives the first column of J = W
    print(W @ np.array([1., 0.]) + np.zeros_like(W) @ x)   # [2. 0. 1.]
    
    # a small nonlinear map f: R^2 -> R^3 and its Jacobian, by columns (JVPs) or rows (VJPs)
    W1 = np.array([[1., -1.], [0.5, 2.]]); W2 = W
    def f(x): return W2 @ np.tanh(W1 @ x)
    def jvp(x, v):                   # forward mode: push v through the chain
        a = W1 @ x
        return W2 @ ((1 - np.tanh(a) ** 2) * (W1 @ v))
    def vjp(x, u):                   # reverse mode: pull u back through the chain
        a = W1 @ x
        return W1.T @ ((1 - np.tanh(a) ** 2) * (W2.T @ u))
    J_fwd = np.column_stack([jvp(x, e) for e in np.eye(2)])   # n = 2 JVPs
    J_rev = np.vstack([vjp(x, e) for e in np.eye(3)])         # m = 3 VJPs
    print(np.allclose(J_fwd, J_rev))                          # True
    
    # Hessian-vector product without forming H: the JVP of the gradient.
    # An AD library does it exactly (forward over reverse); here a central difference of the gradient.
    S = np.array([[3., 1.], [1., 2.]])
    grad = lambda z: S @ z + np.array([np.cos(z[0]), 0.])     # gradient of 0.5 z^T S z + sin(z0)
    v = np.array([1., -1.])
    eps = 1e-5
    Hv = (grad(x + eps * v) - grad(x - eps * v)) / (2 * eps)
    H = S + np.diag([-np.sin(x[0]), 0.])
    print(np.allclose(Hv, H @ v, atol=1e-8))                  # True
    

    The cost of the two multiplication orders for the chain of section 2:

    import numpy as np
    
    def chain_cost(shapes, from_left):
        """Multiplications to multiply a chain of matrices (list of (rows, cols)) from one end."""
        shapes = list(shapes)
        total = 0
        while len(shapes) > 1:
            if from_left:                      # reverse mode: start at the output
                (a, b), (_, c) = shapes[0], shapes[1]
                shapes[:2] = [(a, c)]
            else:                              # forward mode: start at the input
                (a, b), (_, c) = shapes[-2], shapes[-1]
                shapes[-2:] = [(a, c)]
            total += a * b * c
        return total
    
    chain = [(1, 1000), (1000, 1000), (1000, 1000)]            # J3, J2, J1
    print(chain_cost(chain, from_left=True))                   # 2000000
    print(chain_cost(chain, from_left=False))                  # 1001000000
    

    LoRA gradients at initialization, and why GaLore's total update is not low rank:

    import numpy as np
    
    rng = np.random.default_rng(0)
    d, k, r, alpha, n_tok = 64, 48, 4, 8.0, 32
    W0 = rng.standard_normal((d, k))              # frozen
    A = rng.standard_normal((r, k)) / np.sqrt(k)  # random init
    B = np.zeros((d, r))                          # zero init
    X = rng.standard_normal((k, n_tok))           # 32 input tokens as columns
    T = rng.standard_normal((d, n_tok))           # targets
    
    # loss 0.5 ||W X - T||_F^2 with W = W0 + (alpha/r) B A; at init W = W0
    G = (W0 @ X - T) @ X.T                        # dL/dW: a sum of 32 outer products
    Bbar = (alpha / r) * G @ A.T                  # dL/dB
    Abar = (alpha / r) * B.T @ G                  # dL/dA
    print(np.abs(Abar).max(), np.abs(Bbar).max() > 0)   # 0.0 True: only B moves at the first step
    print(np.linalg.matrix_rank(G))               # 32: rank <= number of tokens (here < min(d, k) = 48)
    print(r * (d + k), d * k)                     # 448 trainable numbers instead of 3072
    
    # check dL/dB with a finite difference in one entry
    def loss(B):
        W = W0 + (alpha / r) * B @ A
        return 0.5 * np.sum((W @ X - T) ** 2)
    E = np.zeros_like(B); E[3, 1] = 1e-6
    print(np.isclose((loss(B + E) - loss(B - E)) / 2e-6, Bbar[3, 1]))   # True
    
    # GaLore-style: each update has rank <= r, but the subspace changes, so the sum does not
    updates = []
    for t in range(4):
        Gt = rng.standard_normal((d, k))                        # stand-in for the gradient at refresh step t
        P = np.linalg.svd(Gt, full_matrices=False)[0][:, :r]    # top-r left singular vectors
        updates.append(P @ (P.T @ Gt))                          # project, (optimizer step), project back
    print([int(np.linalg.matrix_rank(u)) for u in updates])     # [4, 4, 4, 4]
    print(np.linalg.matrix_rank(sum(updates)))                  # 16: the total update is not rank 4
    

    CP, unfoldings and the truncated HOSVD (no library needed beyond the SVD):

    import numpy as np
    
    rng = np.random.default_rng(0)
    I, J, K = 30, 20, 10
    
    def unfold(X, n):
        """Mode-n unfolding: mode-n fibers as columns, first remaining index fastest."""
        return np.moveaxis(X, n, 0).reshape(X.shape[n], -1, order="F")
    
    # the 2x2x2 example of section 3 (x[i, j, k], slices along the last index)
    X2 = np.zeros((2, 2, 2)); X2[:, :, 0] = [[1, 2], [3, 4]]; X2[:, :, 1] = [[5, 6], [7, 8]]
    print([unfold(X2, n).astype(int).tolist() for n in range(3)])
    # [[[1, 2, 5, 6], [3, 4, 7, 8]], [[1, 3, 5, 7], [2, 4, 6, 8]], [[1, 3, 2, 4], [5, 7, 6, 8]]]
    
    # A CP tensor of rank 3: x_ijk = sum_r a_ir b_jr c_kr
    A, B, C = rng.standard_normal((I, 3)), rng.standard_normal((J, 3)), rng.standard_normal((K, 3))
    X_cp = np.einsum("ir,jr,kr->ijk", A, B, C)
    print([int(np.linalg.matrix_rank(unfold(X_cp, n))) for n in range(3)])   # [3, 3, 3]
    print(3 * (I + J + K), I * J * K)                                         # 180 parameters vs 6000 entries
    
    # Truncated HOSVD of a noisy tensor with multilinear rank (3, 3, 3)
    X = X_cp + 0.1 * rng.standard_normal((I, J, K))
    ranks = (3, 3, 3)
    U = [np.linalg.svd(unfold(X, n), full_matrices=False)[0][:, :ranks[n]] for n in range(3)]
    G = np.einsum("ijk,ip,jq,ks->pqs", X, *U)                 # core G = X x1 U1ᵀ x2 U2ᵀ x3 U3ᵀ
    X_hat = np.einsum("pqs,ip,jq,ks->ijk", G, *U)             # X_hat = G x1 U1 x2 U2 x3 U3
    err = np.linalg.norm(X - X_hat)
    tails = [np.sum(np.linalg.svd(unfold(X, n), compute_uv=False)[ranks[n]:] ** 2) for n in range(3)]
    lower = np.sqrt(max(tails))     # Eckart-Young per mode: no Tucker model of this size does better
    print(G.shape, round(lower, 3), round(err, 3), round(np.sqrt(sum(tails)), 3), round(np.sqrt(3) * lower, 3))
    # (3, 3, 3) 7.263 7.606 12.04 12.58: lower <= err <= sqrt(sum of tails) <= sqrt(3) * lower
    

    Attention: multi-head attention computed the way a library does it (rows, heads concatenated, then WOW_O), checked against WQKW_{QK}, WOVW_{OV} and the sum over heads:

    import numpy as np
    
    rng = np.random.default_rng(0)
    d, H, n = 16, 4, 6                     # model width, heads, tokens
    dh = d // H
    Wq, Wk, Wv, Wo = (rng.standard_normal((d, d)) / np.sqrt(d) for _ in range(4))
    Xr = rng.standard_normal((n, d))       # library layout: tokens as rows
    
    def softmax_rows(S):
        E = np.exp(S - S.max(axis=1, keepdims=True))
        return E / E.sum(axis=1, keepdims=True)
    
    # how a library computes multi-head attention: rows, split into heads, concatenate, then W_O
    Qr, Kr, Vr = Xr @ Wq.T, Xr @ Wk.T, Xr @ Wv.T
    heads = []
    for h in range(H):
        c = slice(h * dh, (h + 1) * dh)
        A_h = softmax_rows(Qr[:, c] @ Kr[:, c].T / np.sqrt(dh))
        heads.append(A_h @ Vr[:, c])
    out = np.concatenate(heads, axis=1) @ Wo.T            # n x d
    
    # the same layer in the column convention: a sum over heads of W_OV X Aᵀ
    X = Xr.T                                               # d x n, column i = token i
    total = np.zeros((d, n))
    for h in range(H):
        rows = slice(h * dh, (h + 1) * dh)
        W_QK = Wq[rows].T @ Wk[rows]                       # d x d, rank <= dh
        W_OV = Wo[:, rows] @ Wv[rows]                      # d x d, rank <= dh
        S = X.T @ W_QK @ X / np.sqrt(dh)                   # logits s_ij = x_iᵀ W_QK x_j / sqrt(dh)
        A = softmax_rows(S)
        total += W_OV @ X @ A.T                            # head output, d x n
        if h == 0:
            rk = np.linalg.matrix_rank
            print(rk(W_QK), rk(W_OV), rk(S), rk(A))        # 4 4 4 6
            vec = lambda M: M.reshape(-1, order="F")       # vec stacks columns
            print(np.allclose(np.kron(A, W_OV) @ vec(X), vec(W_OV @ X @ A.T)))   # True
            M = rng.standard_normal((dh, dh))              # reparametrize Q and K
            print(np.allclose((M @ Wq[rows]).T @ (np.linalg.inv(M).T @ Wk[rows]), W_QK))  # True
    print(np.allclose(total, out.T))      # True: multi-head = sum of the heads' W_OV X Aᵀ
    

    6. Where it shows up #

    • Autodiff in science. Variational data assimilation (4D-Var) in weather prediction runs a tangent-linear model (JVPs) and an adjoint model (VJPs): reverse mode, written by hand long before deep learning. Adjoint methods give gradients of a PDE solution with respect to many parameters at the cost of one extra solve. Forward mode fits the sensitivities of a simulator to a few physical parameters.
    • Fine-tuning foundation models. Adapting a pretrained Earth-system model to a new region or variable with LoRA keeps one frozen backbone and one small adapter per task.
    • Tensors in ML for science. Ensemble forecasts (member × variable × grid point × time) are compressed with Tucker/HOSVD, with an error you can choose. The TFNO model of the neuraloperator library stores the spectral weights of a Fourier neural operator in Tucker form. CP gives interpretable components because it is often essentially unique.
    • Attention in practice. The KV cache stores the keys and values of every past token; multi-query and grouped-query attention share them across heads, and DeepSeek's multi-head latent attention (2024) caches a low-rank latent vector instead. Interpretability work reads heads through the SVDs of WOVW_{OV} and WQKW_{QK}.

    7. Common mistakes #

    • "Backprop computes the Jacobian." One reverse pass computes one VJP, u⊤Ju^\top J. The full Jacobian needs mm passes.
    • Wrong shapes in the VJP. Wˉ=yˉx⊤\bar W = \bar yx^\top has the shape of WW (m×nm\times n); xyˉ⊤x\bar y^\top is its transpose. In row convention (Y=XW⊤Y = XW^\top): Wˉ=Yˉ⊤X\bar W = \bar Y^\top X and Xˉ=YˉW\bar X = \bar YW.
    • Initializing both LoRA factors to zero. Both gradients vanish and nothing trains.
    • "GaLore reduces the trainable parameters." It reduces optimizer memory; all weights train.
    • Treating the truncated HOSVD as optimal, or truncating a CP model and expecting the best lower-rank CP model. Neither holds for tensors. (Also, CP rank is one number; multilinear rank is one number per mode.)
    • "The attention matrix is low rank." The logits are (rank ≤dh\le d_h); AA after the softmax is usually not.
    • Mixing conventions. WQ⊤WKW_Q^\top W_K (weights act on columns) and WQWK⊤W_QW_K^\top (row-convention weights of shape d×dhd\times d_h) are the same WQKW_{QK} written two ways; check shapes before you transpose.
    • Row-major unfolding. NumPy's default reshape is row-major. With order="C" the columns of X(n)X_{(n)} come in another order; the rank and the left singular vectors stay the same, but the matrix differs from the Kolda–Bader convention used in papers.

    8. Check yourself #

    1. Give the VJP of C=ABC = AB (A∈Rm×pA \in \mathbb{R}^{m\times p}, B∈Rp×nB \in \mathbb{R}^{p\times n}) and check the shapes.
      AnswerAˉ=CˉB⊤\bar A = \bar CB^\top (m×nm\times n times n×pn\times p = m×pm\times p) and Bˉ=A⊤Cˉ\bar B = A^\top\bar C (p×mp\times m times m×nm\times n = p×np\times n). From dL=⟨Cˉ,dA B+A dB⟩FdL = \langle\bar C, dA\,B + A\,dB\rangle_F.
    2. How do you get HvHv for the Hessian HH of a scalar loss without forming HH? What does it cost?
      AnswerTake the JVP of the gradient function in direction vv (forward over reverse): Hv=ddt∇L(x+tv)∣t=0Hv = \tfrac{d}{dt}\nabla L(x + tv)|_{t=0}. It costs a small constant times one gradient evaluation, with memory like one backward pass.
    3. LoRA with r=32r = 32 on a 5120×51205120\times5120 matrix: how many trainable parameters, and what fraction of the matrix?
      Answer32⋅(5120+5120)=327,68032\cdot(5120 + 5120) = 327{,}680 of 26,214,40026{,}214{,}400: 1.25%.
    4. Why does LoRA set B=0B = 0 and AA random, and not A=B=0A = B = 0?
      AnswerBA=0BA = 0 makes training start at W0W_0. The gradients are Bˉ=αrGA⊤\bar B = \tfrac{\alpha}{r}GA^\top and Aˉ=αrB⊤G\bar A = \tfrac{\alpha}{r}B^\top G. With random AA, Bˉ≠0\bar B \ne 0; with A=B=0A = B = 0 both gradients are zero and stay zero.
    5. Show that CP is a special case of Tucker, and compare the parameter counts for I=J=K=50I = J = K = 50, CP rank 5, Tucker core 5×5×55\times5\times5, and the full tensor.
      AnswerA superdiagonal core (grrr=λrg_{rrr} = \lambda_r, other entries 0) turns ∑p,q,sgpqsaipbjqcks\sum_{p,q,s}g_{pqs}a_{ip}b_{jq}c_{ks} into ∑rλrairbjrckr\sum_r\lambda_ra_{ir}b_{jr}c_{kr}. Counts: CP 5⋅150=7505\cdot150 = 750, Tucker 125+750=875125 + 750 = 875, full 125,000125{,}000.
    6. Show that WQKW_{QK} does not change when WQ→MWQW_Q \to MW_Q and WK→M−⊤WKW_K \to M^{-\top}W_K for an invertible M∈Rdh×dhM \in \mathbb{R}^{d_h\times d_h}. What is the rank of the logit matrix for n=2048n = 2048 tokens and dh=64d_h = 64?
      Answer(MWQ)⊤(M−⊤WK)=WQ⊤M⊤M−⊤WK=WQ⊤WK(MW_Q)^\top(M^{-\top}W_K) = W_Q^\top M^\top M^{-\top}W_K = W_Q^\top W_K. The logit matrix X⊤WQKX/dhX^\top W_{QK}X/\sqrt{d_h} has rank at most 64.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. What do a JVP and a VJP compute for f:Rn→Rmf: \mathbb{R}^n \to \mathbb{R}^m with Jacobian JJ?

    2. Why does training use reverse mode?

    3. What does reverse mode cost in memory, and how do you reduce it?

    4. Give xˉ\bar x and Wˉ\bar W for y=Wxy = Wx.

    5. How many trainable numbers does LoRA with rank rr add to a d×kd\times k matrix?

    6. Why does LoRA initialize B=0B = 0 and AA random?

    7. What does GaLore make low rank, and what does it save?

    8. Define the mode-nn unfolding of a tensor.

    9. Write a rank-RR CP model and its parameter count for an I×J×KI\times J\times K tensor.

    10. What are WQKW_{QK} and WOVW_{OV} of an attention head, and what is their largest possible rank?

    Open the review →

    9. Interview angle #

    • "Why does deep learning use reverse mode?" — the loss is a scalar, so one VJP gives all gradients at a few times the forward cost; forward mode would need one pass per parameter. The cost is memory for the activations.
    • "Estimate the training compute of a 7B model on 101210^{12} tokens." — 6×7×109×1012=4.2×10226 \times 7\times10^9 \times 10^{12} = 4.2\times10^{22} flops (2 per parameter per token forward, 4 backward).
    • "LoRA or GaLore for pretraining under a memory limit?" — GaLore: it trains all weights and the total update can have full rank; LoRA's update is limited to rank rr.
    • "What are the QK and OV circuits?" — WQ⊤WKW_Q^\top W_K decides where a token looks, WOWVW_OW_V decides what is moved and where it is written; both have rank ≤dh\le d_h; with AA fixed the head is linear, (A⊗WOV)(A \otimes W_{OV}).
    • "CP or Tucker for a 4-D climate data set?" — Tucker/HOSVD for compression (orthonormal factors from SVDs, error controlled, within N\sqrt N of optimal); CP for interpretable components (unique), with harder fitting and no guarantee that a best model exists.

    10. Practice #

    The notebook for this lesson runs in Colab with NumPy only. It has five exercises, each with a check cell and a folded solution:

    1. Write the VJP of a matrix product C=ABC = AB and check it against finite differences.
    2. Build the Jacobian of a small two-layer map from JVPs (columns) and from VJPs (rows), and count the passes.
    3. Compute the LoRA gradients Bˉ\bar B, Aˉ\bar A from GG, and see that only BB moves at the first step.
    4. Write the mode-nn unfolding and the truncated HOSVD, and check the quasi-optimality bounds.
    5. Write one attention head in the column convention, reproduce worked example D, and check that multi-head attention is a sum of heads.

    11. Watch and read #

    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Open on YouTube · the player loads from youtube-nocookie.com when you press play