Foundations to FrontierPart IX: The graduate coreBezhig

G14: Attention, embeddings, and transformer reasoning

6 exercises, each with a complete solution. 1 figure. 1 code listing.

On this page
  1. Prerequisites and observable goals
  2. The central question: which values should this position combine?
  3. A numerical example before the general architecture
  4. Mask before normalization
  5. Differentiate the mixing operation
  6. What a transformer adds, and what it does not establish
  7. Count computation and memory before claiming acceleration
  8. Independent practice
  9. Smallest useful hints
  10. Complete solutions: G14
  11. Readiness and continuation

Prerequisites and observable goals#

Before

G05 matrix products; G12 probability and stable loss; G13 chain-rule sensitivities.

After

Compute and differentiate a small attention operation, enforce causal masking, account for memory costs, and distinguish a forward attention lab from a trained language model.

The central question: which values should this position combine?#

Attention constructs a data-dependent weighted combination of value vectors. It does not look up a guaranteed fact or produce a proof that a token is important. The original transformer paper defines scaled dot-product attention and multi-head composition. This chapter develops the operation on small original examples; the lab implements forward attention, not a pretrained model or a complete transformer training system. [G-ATTENTION]

Let Q∈Rn×dkQ\in\R^{n\times d_k} contain query rows, K∈Rm×dkK\in\R^{m\times d_k} key rows, and V∈Rm×dvV\in\R^{m\times d_v} value rows. The number of queries nn need not equal the number of keys mm. Key and value row counts must agree because every score column weights the corresponding value row. Compute

S=QKTdk∈Rn×m,P=softmax⁡row(S),O=PV∈Rn×dv.S=\frac{QK^T}{\sqrt{d_k}}\in\R^{n\times m},\qquad P=\operatorname{softmax}_{\rm row}(S),\qquad O=PV\in\R^{n\times d_v}.

SS contains scores, PP contains nonnegative row-normalized weights, and OO contains output vectors. A row of OO need not sum to one. Its coordinates are values, not probabilities. The square root is a scale on dot-product scores, not a normalization of the final output.

A numerical example before the general architecture#

Take one query and two keys with dk=dv=1d_k=d_v=1, Q=[0]Q=[0], K=(0,0)TK=(0,0)^T, and V=(2,8)TV=(2,8)^T. The scores are (0,0)(0,0), so softmax gives (1/2,1/2)(1/2,1/2). The output is (1/2)2+(1/2)8=5(1/2)2+(1/2)8=5. The computation did not choose one value; it mixed both.

If scores are (log⁡3,0)(\log3,0), exponentials are (3,1)(3,1), weights are (3/4,1/4)(3/4,1/4), and the same values produce 3.53.5. Do not substitute the unnormalized scores directly into the weighted sum. Adding any constant to all scores in one row leaves softmax unchanged because the common exponential factor cancels. Subtract the largest allowed score before exponentiating to reduce overflow:

pj=exp⁡(sj−a)∑k∈Aexp⁡(sk−a),a=max⁡k∈Ask,j∈A.p_j=\frac{\exp(s_j-a)}{\sum_{k\in A}\exp(s_k-a)},\quad a=\max_{k\in A}s_k, \qquad j\in A.

Here AA is the set of allowed columns. Set disallowed weights to zero. If AA is empty, there is no probability distribution to normalize; the reference rejects the row rather than silently returning NaNs or arbitrary zeros.

The scale 1/dk1/\sqrt{d_k} has an intuitive variance motivation: under independent zero-mean unit-variance query/key coordinates with suitable independence between vectors, the dot product has variance dkd_k. Dividing by dk\sqrt{d_k} keeps that variance near one. Learned vectors need not satisfy those assumptions; this is a motivating model, not a theorem that all network scores have unit variance.

Mask before normalization#

In causal self-attention, query position ii may use key positions j≤ij\le i, not future positions. For two zero-score positions with scalar values (2,8)(2,8), the masked weight matrix and output are

P=(101/21/2),O=(25).P=\begin{pmatrix}1&0\\1/2&1/2\end{pmatrix},\qquad O=\begin{pmatrix}2\\5\end{pmatrix}.

The first position cannot see value 8. If you instead normalize the first row over both columns and then zero the future entry, the row becomes (1/2,0)(1/2,0) and outputs 1. The remaining weights no longer sum to one. Renormalization after masking could repair this simple case, but masking before softmax expresses the intended contract directly and avoids accidentally allowing future scores to influence the denominator.

The diagram uses zero-based positions increasing downward and rightward. A useful test changes only a future value or key and checks that an earlier causal output is unchanged. Such tests check noninterference for selected inputs; the indexing argument explains why the implementation should preserve it generally.

Differentiate the mixing operation#

For a single softmax row, direct differentiation of the quotient gives

∂pj∂sk=pj(1j=k−pk).\frac{\partial p_j}{\partial s_k}=p_j(\mathbf1_{j=k}-p_k).

The Jacobian is diag⁡(p)−ppT\operatorname{diag}(p)-pp^T. You rarely need to construct that full matrix. Given upstream sensitivities uj=∂L/∂pju_j=\partial L/\partial p_j,

∂L∂sk=pk(uk−∑jpjuj).\frac{\partial L}{\partial s_k}=p_k\left(u_k-\sum_jp_ju_j\right).

For p=(1/4,3/4)p=(1/4,3/4) and u=(1,3)u=(1,3), the weighted mean is 5/25/2, so the score gradient is (−3/8,3/8)(-3/8,3/8). Its sum is zero. This matches softmax invariance to a common score shift: that direction should have zero first-order effect.

For matrices, let DOD_O be the output sensitivity with the same shape as OO. Then

DV=PTDO,DP=DOVT.D_V=P^TD_O,\qquad D_P=D_OV^T.

Apply the row formula to DPD_P to obtain DSD_S. For the unmasked score product,

DQ=DSK/dk,DK=DSTQ/dk.D_Q=D_SK/\sqrt{d_k},\qquad D_K=D_S^TQ/\sqrt{d_k}.

Masked score coordinates have zero sensitivity under a fixed mask. Check dimensions: DSD_S is nn-by-mm, so DSKD_SK is nn-by-dkd_k. A transposition error may survive square test cases; use unequal query/key counts and unequal dk,dvd_k,d_v in tests. These derivative formulas are taught here; the supplied lab intentionally implements only the forward pass. A backward implementation is an unassigned continuation, not an implied completed feature.

What a transformer adds, and what it does not establish#

An embedding table maps an integer token ID to a learned vector. The lookup is not an ordinal statement that token 200 is twice token 100. Positional information distinguishes sequence order; self-attention without position information has permutation symmetries that may be inappropriate for a sequence task. Learned projections construct queries, keys, and values from hidden states.

Multiple heads use separate projections, perform several attention operations, concatenate outputs, and project again. Residual paths add compatible-shaped representations; normalization and a position-wise feed-forward subnetwork provide further transformations. A decoder additionally enforces causal information flow. Exact normalization placement, positional schemes, and other details vary by architecture; the original paper is one specific design, not a specification of every current model.

Next-token training compares a model’s distribution at one position with a target token using a classification loss. Training on many such examples does not turn a high-probability continuation into established truth. Evaluation needs held-out data, contamination controls, clear task definitions, and failure analysis. Attention weights alone are not a causal explanation of the final answer: value vectors and later transformations can alter the consequence of a weight, and alternative parameterizations may change apparent importance.

Count computation and memory before claiming acceleration#

A straightforward self-attention score array for batch BB, heads HH, and sequence length NN has BHN2BHN^2 entries. At B=2,H=8,N=1024B=2,H=8,N=1024 with four-byte entries, that is 67,108,86467{,}108{,}864 bytes, or 64 MiB. Doubling NN makes this one array 256 MiB. This excludes queries, keys, values, gradients, activations, optimizer state, and temporary buffers. Efficient attention algorithms can avoid materializing some full intermediates; the quadratic storage statement describes the straightforward implementation, not an unavoidable lower bound for every exact implementation.

The dot products and weighted sums require work on the order of N2dk+N2dvN^2d_k+N^2d_v per head in the direct method. During autoregressive inference, a key/value cache reuses earlier projected states rather than recomputing them for every new token. It trades memory for work; it does not remove the need for the new query to interact with the allowed history under ordinary dense attention. This CPU lab is for correctness and dimensions, not GPU throughput or an inference benchmark.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

G14.01CSolution

For QQ of shape 3×23\times2, KK of shape 5×25\times2, and VV of shape 5×45\times4, give the shapes of scores, weights, and outputs. Explain why replacing VV by a 4×54\times5 array violates this contract.

G14.02CSolution

Compute the attention output for zero scores and values (2,8)(2,8), then for scores (log⁡3,0)(\log3,0) with the same values. Explain why the output is not itself a probability distribution.

G14.03TSolution

For two zero-score positions and values (2,8)(2,8), derive the causal outputs. Identify the wrong first output obtained by applying softmax over both positions and only then zeroing the future weight.

G14.04CSolution

With p=(1/4,3/4)p=(1/4,3/4) and upstream u=(1,3)u=(1,3), derive the gradient with respect to the two scores. Verify that the entries sum to zero and interpret that identity.

G14.05TSolution

Calculate the bytes and MiB for one float32 score array at B=2,H=8,N=1024B=2,H=8,N=1024, then at N=2048N=2048. State three excluded memory costs and why this is not a measured GPU-memory requirement for every implementation.

G14.06LSolution

Implement softmax and forward attention in starters/attention.py. Enforce shapes, finite inputs, a fixed causal mask, and rejection of entirely masked rows. Test row sums, unequal query/key counts, extreme scores, and invariance of earlier causal outputs to a changed future value.

Smallest useful hints#

For .01, write the inner dimensions of every matrix product. For .03, the first causal row has one allowed element, so its allowed weight must be one. For .05, MiB uses 2202^{20} bytes, not 10610^6.

Complete solutions: G14#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

G14.01Show solution

Shapes follow from the summation indices

QKTQK^T is (3×2)(2×5)=3×5(3\times2)(2\times5)=3\times5. Row softmax preserves this shape. Multiplication by VV gives (3×5)(5×4)=3×4(3\times5)(5\times4)=3\times4. A 4×54\times5 replacement supplies only four value rows for five keys and fails the inner-dimension requirement. Square-only examples would conceal this category of transpose error.

Back to G14.01
G14.02Show solution

Normalize scores before mixing values

Zero scores give weights (1/2,1/2)(1/2,1/2) and output 5. Scores (log⁡3,0)(\log3,0) give weights (3/4,1/4)(3/4,1/4) and output 3/2+2=7/23/2+2=7/2. The output is a scalar in the value space and can exceed one or be negative if values permit; only the weights have the normalized probability-vector structure. Shifting both scores by the same finite constant gives the same exact weights.

Back to G14.02
G14.03Show solution

Causality constrains the denominator too

The first position sees only value 2 and outputs 2. The second sees both and outputs 5. Normalizing the first row over both positions before zeroing produces (1/2,0)(1/2,0) and output 1, violating the intended normalized mixing rule. A test that changes the future value alone would not catch every denominator bug; also check allowed row sums and the one-allowed-key case.

Back to G14.03
G14.04Show solution

The constant-shift direction has zero sensitivity

The weighted upstream mean is (1/4)1+(3/4)3=5/2(1/4)1+(3/4)3=5/2. Therefore the derivatives are (1/4)(1−5/2)=−3/8(1/4)(1-5/2)=-3/8 and (3/4)(3−5/2)=3/8(3/4)(3-5/2)=3/8. Their sum is zero, as required because adding the same perturbation to both scores leaves softmax unchanged. This check catches some, not all, derivative errors.

Back to G14.04
G14.05Show solution

One tensor is not the entire memory budget

At 1024, 2⋅8⋅10242⋅4=67,108,8642\cdot8\cdot1024^2\cdot4=67{,}108{,}864 bytes, or 64 MiB. At 2048, it is 268,435,456268{,}435{,}456 bytes, or 256 MiB. Gradients, projected states, optimizer buffers, and allocator overhead are examples of excluded costs. An algorithm that streams blocks may not store this entire tensor, and a measured device peak also depends on implementation and allocation behavior. The calculation is a transparent model of one array.

Back to G14.05
G14.06Show solution

A bounded forward reference

The code validates row widths, key/value counts, and finite arithmetic. It normalizes only allowed entries using the maximum allowed score. The causal option requires aligned self-attention lengths rather than silently guessing how unequal sequence positions correspond. Tests compare hand-computed outputs and a future-value change. This is the full reference for the assigned forward lab; it does not claim to train a language model or implement optimized GPU attention.

graduate_code/solutions/attention.pyPython
"""G14: single-head scaled dot-product attention, not a trained transformer."""
import math


def softmax(scores, allowed=None):
    s = list(scores)
    mask = [True]*len(s) if allowed is None else list(allowed)
    if not s or len(mask) != len(s) or any(type(x) is not bool for x in mask) or not any(mask):
        raise ValueError("nonempty row with at least one allowed position required")
    if any(not math.isfinite(x) for x in s):
        raise ValueError("finite pre-mask scores required")
    top = max(v for v,ok in zip(s,mask) if ok)
    e = [math.exp(v-top) if ok else 0. for v,ok in zip(s,mask)]
    total = math.fsum(e)
    return [v/total for v in e]


def attention(Q,K,V,causal=False):
    n,m = len(Q),len(K)
    d = len(Q[0]) if n else 0
    dv = len(V[0]) if V else 0
    if (type(causal) is not bool or not 1 <= n <= 256 or not 1 <= m <= 256
            or not 1 <= d <= 128 or not 1 <= dv <= 128 or len(V) != m):
        raise ValueError("bounded nonempty matching matrices required")
    if causal and n != m:
        raise ValueError("this causal lab requires equal query/key lengths")
    for matrix,width in ((Q,d),(K,d),(V,dv)):
        if any(len(row) != width or any(not math.isfinite(x) for x in row) for row in matrix):
            raise ValueError("ragged or nonfinite matrix")
    out, weights = [],[]
    for i,q in enumerate(Q):
        scores = [math.fsum(a*b for a,b in zip(q,k))/math.sqrt(d) for k in K]
        p = softmax(scores,[j <= i for j in range(m)] if causal else None)
        weights.append(p)
        out.append([math.fsum(p[j]*V[j][v] for j in range(m)) for v in range(dv)])
    return out,weights


if __name__ == "__main__":
    print(attention([[0.],[0.]],[[1.],[2.]],[[2.],[8.]],causal=True))
Back to G14.06

Readiness and continuation#

Explain QQ, KK, VV, scores, weights, and outputs as six distinct objects. Derive a backward shape on unequal lengths and demonstrate a masking counterexample. A next project can add the taught backward formulas and finite-difference tests, then a complete tiny decoder with leakage-free next-token evaluation. That additional project is not counted as completed work here.