๐Ÿงฎ Autoregressive Transformer: Encoder + Decoder

Every matrix multiplication, formula, and manual calculation behind a from-scratch NumPy Transformer
Transformers & Attention Intermediate
Contents

    This notebook implements the original Transformer (Vaswani et al., 2017, Attention Is All You Need) in pure NumPy โ€” the full encoderโ€“decoder architecture, with every intermediate matrix printed and every formula written out.

    The task here is autoregressive next-token prediction:

    • the encoder reads a prefix of the sentence (9 tokens);
    • the decoder predicts the continuation of that same sentence (6 tokens), one token at a time, each prediction conditioned on the prefix and on everything it has already generated.

    This is the "prefix-LM" arrangement: same architecture as translation, different task. Nothing about the encoder, the decoder, or the cross-attention changes โ€” only what the two sequences contain.

    The chain is carried all the way to the end:

    tokens โ†’ IDs โ†’ embeddings โ†’ positional encoding โ†’ encoder (MHA + FFN) โ†’ decoder (masked MHA + cross-attention + FFN) โ†’ linear projection โ†’ logits โ†’ softmax โ†’ next token


    The Autoregressive Factorization

    "Autoregressive" means the model factorizes the probability of the whole continuation into a product of next-token probabilities, each conditioned only on what came before:

    $$P(y_1, y_2, \dots, y_n \mid x) \;=\; \prod_{t=1}^{n} P(y_t \mid y_1, \dots, y_{t-1},\, x)$$

    Two architectural components enforce the "only on what came before" part, and both are implemented below:

    1. Shift right โ€” at position $t$ the decoder is fed $y_{t-1}$, never $y_t$.
    2. Causal mask โ€” position $t$ cannot attend to positions $> t$.

    The conditioning on $x$ (the prefix) enters through cross-attention, which is the one place the encoder and decoder touch:

    $$Q \text{ from the \textbf{decoder}}, \qquad K, V \text{ from the \textbf{encoder}}$$

    Honest caveat, stated once: the weights here are random, not trained. Every number is computed correctly, but the predicted token is meaningless. The purpose is the mechanics of the forward pass, not linguistic quality.

    NumPy: 2.4.4
    Display helpers ready: show_matmul, show_elementwise, show_masked, show_weights_masked, expand.

    1. Task & Hyperparameters

    The sentence is split into two parts:

    content length fed to
    prefix $x$ the cat sat on the mat and was looking 9 encoder
    continuation $y$ at a tiny mouse running by 6 decoder (autoregressively)

    The decoder must produce at a tiny mouse running by <eos>, one token at a time.

    The two lengths are deliberately different (9 vs 6). This matters: it forces the cross-attention matrix to be rectangular ($n_{tgt} \times n_{src}$), which is easy to get wrong when both lengths are equal, and it makes the shapes unambiguous when reading the printouts.

    Note that both halves share one vocabulary โ€” they are two parts of the same English sentence. In the translation version of this architecture the two halves would be two different languages; the machinery is identical either way.

    Symbol Meaning Value
    $n_{src}$ prefix length 9
    $n_{tgt}$ decoder input length 7 (<sos> + 6 words)
    $d_{model}$ embedding / model width 8
    $h$ number of attention heads 2
    $d_k = d_v = d_{model}/h$ width of one head 4
    $d_{ff}$ inner width of the FFN 32 ($=4 d_{model}$)
    $V$ vocabulary size 500
    Full sentence (15 tokens): ['the', 'cat', 'sat', 'on', 'the', 'mat', 'and', 'was', 'looking', 'at', 'a', 'tiny', 'mouse', 'running', 'by']
    
    PREFIX       -> encoder (9 tokens): ['the', 'cat', 'sat', 'on', 'the', 'mat', 'and', 'was', 'looking']
    CONTINUATION -> decoder (6 tokens): ['at', 'a', 'tiny', 'mouse', 'running', 'by']
    
    d_model=8, h=2, d_k=4, d_ff=32, V=500

    2. Vocabulary & Token IDs

    As requested, token IDs are random integers in [50, 500) โ€” this mimics a real tokenizer, where the ID is an arbitrary index into a vocabulary, carrying no meaning by itself.

    Three IDs are reserved below 50, as is standard:

    • <pad> = 0 โ€” padding (not used here, sequences are not batched)
    • <sos> = 1 โ€” start of sequence, fed to the decoder at position 0
    • <eos> = 2 โ€” end of sequence, what the decoder must predict last

    Prefix and continuation share one vocabulary of size $V = 500$ โ€” they are halves of the same sentence. Note that the repeated word "the" receives the same ID at both of its positions โ€” an embedding table is a lookup by ID, so identical words start from identical vectors. What makes them differ later is positional encoding and context.

    Vocabulary: 17 entries used out of V=500
    
    PREFIX token -> ID  (encoder)
      the       -> 83
      cat       -> 168
      sat       -> 458
      on        -> 328
      the       -> 83
      mat       -> 421
      and       -> 185
      was       -> 276
      looking   -> 274
    
    CONTINUATION token -> ID  (decoder)
      at        -> 127
      a         -> 340
      tiny      -> 68
      mouse     -> 57
      running   -> 412
      by        -> 322
    
    Note: 'the' appears at positions 0 and 4, both with ID 83.

    3. Weight Initialization

    The old notebook drew every weight from np.random.rand, i.e. $\mathcal{U}(0,1)$: all entries positive, mean 0.5.

    For $y = xW$ with $W \in \mathbb{R}^{8\times 8}$ and all entries positive, each output is a sum of 8 positive products, so $\mathbb{E}[y] \approx 8 \cdot 0.5 \cdot \mathbb{E}[x] = 4\,\mathbb{E}[x]$. The signal is multiplied by ~4 at every matrix product, and it is never re-centred because there is no LayerNorm. Chaining โ‰ˆ 8 such products gives $4^8 \approx 65\,000$ โ€” exactly the order of magnitude seen in the old output (โ‰ˆ 80 000). Worse, since all rows grow in the same direction, they become nearly parallel and the row-to-row differences vanish: the tokens lose their identity.

    Xavier/Glorot initialization fixes the scale:

    $$W_{ij} \sim \mathcal{U}(-a, a), \qquad a = \sqrt{\frac{6}{fan_{in} + fan_{out}}}$$

    which keeps $\mathrm{Var}(y) \approx \mathrm{Var}(x)$ and $\mathbb{E}[y] = 0$.

    After 8 chained matmuls, no LayerNorm:
      U(0,1) init  -> mean |value| =   33138.98   row-to-row std = 9033.2708
      Xavier init  -> mean |value| =     0.2531   row-to-row std = 0.1784
    
    The U(0,1) rows have exploded AND collapsed onto each other.

    4. Embeddings

    The embedding table is $E \in \mathbb{R}^{V \times d_{model}} = \mathbb{R}^{500 \times 8}$. Looking up token ID $t$ means taking row $t$: this is exactly a one-hot vector times $E$,

    $$\mathrm{emb}(t) = \mathbf{e}_t^\top E, \qquad \mathbf{e}_t \in \{0,1\}^{V}$$

    but implemented as indexing, since the matrix product would be wasteful.

    Scaling by $\sqrt{d_{model}}$. The paper multiplies the embeddings by $\sqrt{d_{model}}$ before adding the positional encoding (Section 3.4). The reason is scale matching: positional encodings are sines and cosines with values in $[-1, 1]$, while embeddings initialized with variance $1/d_{model}$ have entries around $\pm 0.35$. Without the scaling, the positional signal would dominate the semantic signal. Here $\sqrt{8} \approx 2.83$.

    Embedding table E shape: (500, 8)
    
    src_emb = E[src_ids] * sqrt(8) = E[src_ids] * 2.8284
    Prefix embeddings shape: (9, 8)
    
      the       (id  83) -> [-0.1   -0.204 -0.878 -0.827 -0.226  0.367  0.914 -0.803]
      cat       (id 168) -> [-0.685  0.791 -1.97   0.893 -1.211  0.731  0.014 -0.954]
      sat       (id 458) -> [ 1.393  0.839  0.873 -0.966 -0.079 -0.3   -1.006  0.36 ]
      on        (id 328) -> [ 1.221 -0.877  1.712 -1.748  0.435  0.475 -0.796  0.424]
      the       (id  83) -> [-0.1   -0.204 -0.878 -0.827 -0.226  0.367  0.914 -0.803]
      mat       (id 421) -> [ 0.234  1.035 -0.836  1.424 -0.092 -0.888  0.998 -0.287]
      and       (id 185) -> [-0.536 -1.707 -1.117  1.236 -0.156 -0.548  0.16   0.502]
      was       (id 276) -> [-0.864  1.279  1.042  0.584 -0.13   0.58  -0.707  0.856]
      looking   (id 274) -> [ 0.234 -0.249  1.201  0.14  -1.967 -1.117 -0.186  0.31 ]
    
    Check that both occurrences of 'the' are identical before PE:
      position 0: [-0.1   -0.204 -0.878 -0.827 -0.226  0.367  0.914 -0.803]
      position 4: [-0.1   -0.204 -0.878 -0.827 -0.226  0.367  0.914 -0.803]
      identical? True

    5. Positional Encoding

    Self-attention is a set operation: it computes a weighted sum over positions, and the weights depend only on content. Nothing in $QK^\top$ knows that position 3 comes after position 2. Order must therefore be injected into the input itself.

    The sinusoidal encoding from the paper, for position $pos$ and dimension index $i$:

    $$PE_{(pos,\,2i)} = \sin\!\left(\frac{pos}{10000^{2i/d_{model}}}\right), \qquad PE_{(pos,\,2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

    With $d_{model} = 8$ there are 4 frequency pairs, $i = 0, 1, 2, 3$:

    $i$ columns wavelength $2\pi \cdot 10000^{2i/8}$
    0 0, 1 $2\pi \approx 6.28$
    1 2, 3 $2\pi \cdot 10 \approx 62.8$
    2 4, 5 $2\pi \cdot 100 \approx 628$
    3 6, 7 $2\pi \cdot 1000 \approx 6283$

    Fast columns encode fine-grained local position; slow columns encode coarse absolute position. The property that makes this choice useful is that $PE_{pos+k}$ is a fixed linear function of $PE_{pos}$ (a 2-D rotation by angle $k\omega_i$ in each frequency pair), so relative offsets are linearly recoverable by the attention weights.

    PE_src shape: (9, 8)
    [[ 0.     1.     0.     1.     0.     1.     0.     1.   ]
     [ 0.842  0.54   0.1    0.995  0.01   1.     0.001  1.   ]
     [ 0.909 -0.416  0.199  0.98   0.02   1.     0.002  1.   ]
     [ 0.141 -0.99   0.295  0.955  0.03   1.     0.003  1.   ]
     [-0.757 -0.654  0.389  0.921  0.04   0.999  0.004  1.   ]
     [-0.959  0.284  0.479  0.878  0.05   0.999  0.005  1.   ]
     [-0.279  0.96   0.565  0.825  0.06   0.998  0.006  1.   ]
     [ 0.657  0.754  0.644  0.765  0.07   0.998  0.007  1.   ]
     [ 0.989 -0.145  0.717  0.697  0.08   0.997  0.008  1.   ]]
    
    Hand check  PE[3,2] = sin(3 / 10000^(2*1/8)) = sin(3 * 0.100000) = 0.295520
    From array  PE[3,2] = 0.295520   ->  match: True

    Encoder Input

    $$X_{enc} = \mathrm{Embedding}(\text{src\_ids}) \cdot \sqrt{d_{model}} + PE$$

    This is the step that was missing in the old notebook. Its effect is visible immediately: the two occurrences of "the" (positions 0 and 4 of the prefix), which had identical embeddings, now have different input vectors.

    X_enc shape: (9, 8)
    
       0 the       [-0.1    0.796 -0.878  0.173 -0.226  1.367  0.914  0.197]
       1 cat       [ 0.157  1.331 -1.87   1.888 -1.201  1.731  0.015  0.046]
       2 sat       [ 2.302  0.423  1.071  0.014 -0.059  0.699 -1.004  1.36 ]
       3 on        [ 1.362 -1.867  2.008 -0.792  0.465  1.475 -0.793  1.424]
       4 the       [-0.857 -0.857 -0.489  0.094 -0.186  1.367  0.918  0.197]
       5 mat       [-0.725  1.319 -0.357  2.302 -0.042  0.111  1.003  0.713]
       6 and       [-0.815 -0.747 -0.552  2.061 -0.096  0.45   0.166  1.502]
       7 was       [-0.207  2.033  1.686  1.348 -0.06   1.577 -0.7    1.856]
       8 looking   [ 1.223 -0.394  1.918  0.837 -1.887 -0.12  -0.178  1.31 ]
    
    'the' at position 0: [-0.1    0.796 -0.878  0.173 -0.226  1.367  0.914  0.197]
    'the' at position 4: [-0.857 -0.857 -0.489  0.094 -0.186  1.367  0.918  0.197]
    identical now? False  <- order information is now present

    Embeddings + Positional Encoding

    $X_{enc} = \text{scaled embeddings} + PE$ is element-wise: three matrices of shape $(9, 8)$, nothing mixes across rows or columns. Both operands are printed side by side below.

    ==============================================================================
      X_enc = emb*sqrt(d) + PE      all three are (9, 8) โ€” element-wise, no mixing
    ==============================================================================
    
           the | 
          emb*sqrt(d):   -0.100    -0.204    -0.878    -0.827    -0.226    +0.367    +0.914    -0.803
                   PE:   +0.000    +1.000    +0.000    +1.000    +0.000    +1.000    +0.000    +1.000
                X_enc:   -0.100    +0.796    -0.878    +0.173    -0.226    +1.367    +0.914    +0.197
    
           cat | 
          emb*sqrt(d):   -0.685    +0.791    -1.970    +0.893    -1.211    +0.731    +0.014    -0.954
                   PE:   +0.841    +0.540    +0.100    +0.995    +0.010    +1.000    +0.001    +1.000
                X_enc:   +0.157    +1.331    -1.870    +1.888    -1.201    +1.731    +0.015    +0.046
    
           sat | 
          emb*sqrt(d):   +1.393    +0.839    +0.873    -0.966    -0.079    -0.300    -1.006    +0.360
                   PE:   +0.909    -0.416    +0.199    +0.980    +0.020    +1.000    +0.002    +1.000
                X_enc:   +2.302    +0.423    +1.071    +0.014    -0.059    +0.699    -1.004    +1.360
    
            on | 
          emb*sqrt(d):   +1.221    -0.877    +1.712    -1.748    +0.435    +0.475    -0.796    +0.424
                   PE:   +0.141    -0.990    +0.296    +0.955    +0.030    +1.000    +0.003    +1.000
                X_enc:   +1.362    -1.867    +2.008    -0.792    +0.465    +1.475    -0.793    +1.424
    
           the | 
          emb*sqrt(d):   -0.100    -0.204    -0.878    -0.827    -0.226    +0.367    +0.914    -0.803
                   PE:   -0.757    -0.654    +0.389    +0.921    +0.040    +0.999    +0.004    +1.000
                X_enc:   -0.857    -0.857    -0.489    +0.094    -0.186    +1.367    +0.918    +0.197
    
           mat | 
          emb*sqrt(d):   +0.234    +1.035    -0.836    +1.424    -0.092    -0.888    +0.998    -0.287
                   PE:   -0.959    +0.284    +0.479    +0.878    +0.050    +0.999    +0.005    +1.000
                X_enc:   -0.725    +1.319    -0.357    +2.302    -0.042    +0.111    +1.003    +0.713
    
           and | 
          emb*sqrt(d):   -0.536    -1.707    -1.117    +1.236    -0.156    -0.548    +0.160    +0.502
                   PE:   -0.279    +0.960    +0.565    +0.825    +0.060    +0.998    +0.006    +1.000
                X_enc:   -0.815    -0.747    -0.552    +2.061    -0.096    +0.450    +0.166    +1.502
    
           was | 
          emb*sqrt(d):   -0.864    +1.279    +1.042    +0.584    -0.130    +0.580    -0.707    +0.856
                   PE:   +0.657    +0.754    +0.644    +0.765    +0.070    +0.998    +0.007    +1.000
                X_enc:   -0.207    +2.033    +1.686    +1.348    -0.060    +1.577    -0.700    +1.856
    
       looking | 
          emb*sqrt(d):   +0.234    -0.249    +1.201    +0.140    -1.967    -1.117    -0.186    +0.310
                   PE:   +0.989    -0.146    +0.717    +0.697    +0.080    +0.997    +0.008    +1.000
                X_enc:   +1.223    -0.394    +1.918    +0.837    -1.887    -0.120    -0.178    +1.310
    
      check element [0,0]: -0.100154 + +0.000000 = -0.100154   MATCH: True

    6. Layer Normalization

    This is the component whose absence caused the 80 000-magnitude output in the old notebook. For each row (each token) independently:

    $$\mu = \frac{1}{d}\sum_{j=1}^{d} x_j, \qquad \sigma^2 = \frac{1}{d}\sum_{j=1}^{d}(x_j - \mu)^2, \qquad \mathrm{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta$$

    Three points worth being precise about:

    1. Per row, not per column. Unlike BatchNorm, no statistic is shared across tokens. Token $i$ is normalized using only its own 8 numbers. This is what makes it valid at inference time with a single sequence.
    2. $\gamma, \beta \in \mathbb{R}^{d_{model}}$ are learned; initialized to $\mathbf{1}$ and $\mathbf{0}$, so LayerNorm starts as pure standardization.
    3. $\epsilon = 10^{-5}$ guards against division by zero.

    The original paper uses post-norm: LayerNorm(x + Sublayer(x)). That is what is implemented below. (Modern implementations often use pre-norm, x + Sublayer(LayerNorm(x)), which trains more stably in deep stacks โ€” but post-norm is the 2017 architecture and this notebook follows it.)

    x        = [-0.1    0.796 -0.878  0.173 -0.226  1.367  0.914  0.197]
    mean     = 0.2803
    variance = 0.4513   (std = 0.6718)
    LN(x)    = [-0.566  0.768 -1.724 -0.16  -0.754  1.618  0.943 -0.124]
               mean = -8.33e-17, std = 1.0000

    7. Scaled Dot-Product Attention

    $$\mathrm{Attention}(Q, K, V) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right) V$$

    Reading it as a sequence of shapes, with $n_q$ queries and $n_{kv}$ keys/values:

    Step Operation Shape
    1 $Q$, $K$, $V$ $(n_q, d_k)$, $(n_{kv}, d_k)$, $(n_{kv}, d_v)$
    2 $S = QK^\top$ $(n_q, n_{kv})$ โ€” one score per (query, key) pair
    3 $S / \sqrt{d_k}$ same
    4 $+ M$ (mask, optional) same
    5 $A = \mathrm{softmax}(\cdot)$ row-wise same, each row sums to 1
    6 $AV$ $(n_q, d_v)$

    Why divide by $\sqrt{d_k}$? If the components of $q$ and $k$ are independent with mean 0 and variance 1, then $q \cdot k = \sum_{j=1}^{d_k} q_j k_j$ has mean 0 and variance $d_k$, i.e. standard deviation $\sqrt{d_k}$. Feeding scores of that magnitude into a softmax pushes it toward a one-hot distribution, where the gradient is nearly zero. Dividing by $\sqrt{d_k}$ restores unit variance and keeps the softmax in its responsive region. Here $\sqrt{d_k} = \sqrt{4} = 2$.

    Softmax, applied to each row $s$ independently:

    $$A_{ij} = \frac{\exp(s_{ij})}{\sum_{j'} \exp(s_{ij'})}$$

    implemented as exp(s - max(s)) for numerical stability โ€” subtracting the row max changes nothing mathematically (the constant cancels) but prevents exp from overflowing.

    Numerical check on a tiny example:
      raw scores q.k    = [[2. 0. 1.]]      (2, 0, 1)
      scaled by sqrt(4) = [[1.  0.  0.5]]
      softmax weights   = [[0.506 0.186 0.307]]  sum = 1.0000
      output = w @ V    = [[5.065 1.863 3.072 0.   ]]
      -> the query matches key 0 most, so V row 0 dominates the output.

    8. Multi-Head Attention

    The old notebook described "multi-head" but computed a single head and omitted the output projection $W^O$. Both are corrected here.

    $$\mathrm{MultiHead}(Q,K,V) = \mathrm{Concat}(\mathrm{head}_1, \dots, \mathrm{head}_h)\,W^O, \qquad \mathrm{head}_i = \mathrm{Attention}(QW_i^Q,\, KW_i^K,\, VW_i^V)$$

    Why several heads? A single softmax row is a probability distribution โ€” it can only express one weighting of the context. Averaging cancels competing patterns. With $h$ heads, each head runs the same operation in its own $d_k$-dimensional subspace, so one head can track syntactic agreement while another tracks a co-reference, and $W^O$ recombines them.

    Implementation. Rather than $h$ separate small matrices, one uses full $d_{model} \times d_{model}$ matrices $W^Q, W^K, W^V$ and reshapes the result โ€” mathematically identical, since a block of columns of $W^Q$ is exactly $W_i^Q$:

    X (n, 8) @ Wq (8, 8) -> Q (n, 8)
                 reshape -> (n, 2, 4)
               transpose -> (2, n, 4)      # (heads, tokens, d_k)
    

    Total parameter count is unchanged: $h \cdot d_k = d_{model}$. Multi-head attention costs the same as single-head, but is strictly more expressive.

    MHA class defined. Each instance owns Wq, Wk, Wv, Wo of shape (8, 8).
    Parameters per attention sub-layer: 4 * 8 * 8 = 256

    9. Encoder: Self-Attention

    "Self"-attention means $Q$, $K$ and $V$ all come from the same input $X_{enc}$. No causal mask is used here. This is the point that most often causes confusion in an autoregressive setup: the prefix is fully known input, not something being generated, so the encoder is free to be bidirectional โ€” token 2 may look at token 7. The autoregressive constraint applies only to the decoder side, where tokens are produced one at a time.

    Below the computation is unrolled, and one score is verified by hand against the definition.

    [encoder self-attn] x_q (9, 8) , x_kv (9, 8)
      Q,K,V per head: (2, 9, 4), (2, 9, 4), (2, 9, 4)
      scores/weights: (2, 9, 9)   concat: (9, 8)   out: (9, 8)
    
    --- Head 0: Q, K, V (9 x 4 each) ---
    Q[head0]:
    [[-0.363  0.43   1.132 -1.267]
     [-1.14  -1.112  0.583 -0.527]
     [-0.04  -0.869  0.345 -0.223]
     [ 1.895  1.278  0.877  0.181]
     [ 0.557  1.335  1.018 -0.361]
     [-1.574  0.05  -0.869 -0.623]
     [-0.513  0.355 -0.385  0.953]
     [ 0.287  0.698 -0.771 -1.189]
     [-0.319 -0.98  -0.91   1.053]]
    
    K[head0]:
    [[-0.642  0.947 -0.654 -0.265]
     [-0.815  1.333 -0.331  0.315]
     [-0.212  0.556  1.075  0.39 ]
     [ 0.833 -0.997  1.658 -0.011]
     [ 0.126 -0.482  0.012 -0.353]
     [ 0.888 -0.151  0.471 -0.344]
     [ 1.368 -1.584  1.217 -0.221]
     [ 1.429 -0.377  0.183  0.313]
     [ 0.161 -1.964  2.485  1.113]]
    
    V[head0]:
    [[-1.375 -1.282  0.058  1.186]
     [-2.807 -1.31  -0.778  1.31 ]
     [ 0.743  0.139 -0.711 -1.884]
     [ 1.641 -0.434  0.404 -0.671]
     [-1.132 -1.533  0.746  2.018]
     [-3.082 -0.455 -1.491  0.839]
     [-2.619 -1.071 -0.747  1.683]
     [-2.389  0.491 -2.011 -0.774]
     [-1.053 -0.132  0.331 -1.287]]
    
    --- Head 0: raw scores Q K^T / sqrt(d_k), shape (9, 9) ---
    [[ 0.117  0.047  0.519  0.579  0.104  0.291  0.24  -0.436  0.25 ]
     [-0.282 -0.457  0.023  0.566  0.292 -0.194  0.514 -0.634  1.432]
     [-0.482 -0.656 -0.095  0.704  0.248  0.167  0.896  0.132  1.156]
     [-0.314 -0.037  0.661  0.878 -0.214  0.921  0.798  1.222  0.087]
     [ 0.168  0.437  0.788  0.412 -0.216  0.449 -0.016  0.183 -0.203]
     [ 0.895  0.72  -0.408 -1.398 -0.007 -0.8   -1.577 -1.311 -1.602]
     [ 0.332  0.66   0.133 -0.715 -0.288 -0.509 -0.972 -0.319 -0.338]
     [ 0.648  0.289 -0.483 -0.862  0.055  0.098 -0.695 -0.183 -2.283]
     [-0.204 -0.207 -0.522 -0.404  0.025 -0.463 -0.112  0.039  0.393]]
    q_1 (head 0, token 'cat') = [-1.14  -1.112  0.583 -0.527]
    k_8 (head 0, token 'looking') = [ 0.161 -1.964  2.485  1.113]
    
    q . k  = (-1.1400)(0.1606) + (-1.1117)(-1.9641) + (0.5834)(2.4849) + (-0.5266)(1.1127)
           = 2.864096
    / sqrt(d_k) = 2.864096 / 2.0 = 1.432048
    
    From the scores matrix: 1.432048   match: True
    
    Softmax of row 1 (head 0), by hand:
    [0.059 0.05  0.08  0.138 0.105 0.065 0.131 0.042 0.329]
    sum = 1.000000
    matches library row: True
    Every row sums to 1: True
    
    Note the full square (no mask): the encoder is bidirectional over the prefix.
    The pattern is diffuse because the weights are random, not trained.

    Self-Attention, Step by Step

    This is the shape question spelled out. Follow the inner dimensions as they cancel:

    # operation shapes result
    1 $X_{enc} W^Q$ $(9,8) @ (8,8)$ $(9,8)$
    2 split into heads reshape $(2,9,4)$
    3 $Q_0 K_0^\top$ $(9,4) @ (4,9)$ $(9,\mathbf{9})$
    4 softmax element-wise per row $(9,9)$
    5 $A V_0$ $(9,9) @ (9,4)$ $(9,\mathbf{4})$
    6 concat heads $(9,4) + (9,4)$ side by side $(9,8)$
    7 $\cdot\, W^O$ $(9,8) @ (8,8)$ $(9,8)$

    Why step 5 returns $(9,4)$ and not $(9,9)$: the $9 \times 9$ matrix is weights, not content. In $AV$ the shared dimension is the 9 keys, and it cancels โ€” $(9,9)@(9,4) \to (9,4)$. Each output row is a weighted average of the 9 rows of $V$, so it has $V$'s width, which is $d_v = 4$. The count of tokens sets the number of rows, never the number of columns.

    The block therefore returns $(9,8)$ โ€” exactly the shape it received. That invariance is what allows 6 blocks to be stacked.

    STEP 1 : project X_enc to Q (K and V are identical in form)
    ==============================================================================
    ==============================================================================
      X_enc (9, 8)   @   Wq (8, 8)   =   Q_full (9, 8)
      shape rule: (9 x 8) @ (8 x 8) -> (9 x 8)   inner dim 8 cancels
      K uses Wk, V uses Wv โ€” same shapes, same arithmetic
    ==============================================================================
    
    X_enc =
              the |   -0.100    +0.796    -0.878    +0.173    -0.226    +1.367    +0.914    +0.197
              cat |   +0.157    +1.331    -1.870    +1.888    -1.201    +1.731    +0.015    +0.046
              sat |   +2.302    +0.423    +1.071    +0.014    -0.059    +0.699    -1.004    +1.360
               on |   +1.362    -1.867    +2.008    -0.792    +0.465    +1.475    -0.793    +1.424
              the |   -0.857    -0.857    -0.489    +0.094    -0.186    +1.367    +0.918    +0.197
              mat |   -0.725    +1.319    -0.357    +2.302    -0.042    +0.111    +1.003    +0.713
              and |   -0.815    -0.747    -0.552    +2.061    -0.096    +0.450    +0.166    +1.502
              was |   -0.207    +2.033    +1.686    +1.348    -0.060    +1.577    -0.700    +1.856
          looking |   +1.223    -0.394    +1.918    +0.837    -1.887    -0.120    -0.178    +1.310
    
    Wq =
         -0.356    -0.524    +0.249    -0.119    +0.514    +0.184    -0.502    +0.549
         -0.296    -0.199    -0.120    -0.550    -0.004    -0.461    -0.121    -0.183
         +0.334    +0.371    -0.424    -0.163    -0.155    -0.427    +0.366    +0.254
         -0.379    -0.163    -0.483    +0.157    -0.464    +0.595    +0.344    -0.134
         +0.096    +0.504    +0.066    -0.399    -0.149    +0.420    +0.177    +0.185
         +0.449    +0.483    +0.514    -0.496    +0.054    +0.295    +0.315    +0.131
         -0.408    +0.364    +0.270    -0.507    -0.044    -0.399    -0.199    -0.590
         -0.122    +0.061    +0.146    +0.200    +0.412    -0.348    -0.465    +0.431
    
    Q_full =
              the |   -0.363    +0.430    +1.132    -1.267    +0.149    -0.034    -0.191    -0.764
              cat |   -1.140    -1.112    +0.583    -0.527    -0.220    +1.320    +0.033    -0.870
              sat |   -0.040    -0.869    +0.345    -0.223    +1.661    -0.113    -1.033    +2.714
               on |   +1.895    +1.278    +0.877    +0.181    +1.397    +0.233    +0.047    +3.064
              the |   +0.557    +1.335    +1.018    -0.361    -0.263    +0.392    +0.511    -0.763
              mat |   -1.574    +0.050    -0.869    -0.623    -1.129    +0.147    +0.361    -1.317
              and |   -0.513    +0.355    -0.385    +0.953    -0.637    +1.160    +0.400    -0.138
              was |   +0.287    +0.698    -0.771    -1.189    -0.113    -0.821    +0.700    +1.169
          looking |   -0.319    -0.980    -0.910    +1.053    +0.767    -1.128    -0.522    +1.422
    
      worked element  Q_full[0,0]  =  row 0 of X_enc  .  column 0 of Wq
          row  0 of X_enc = [-0.1    0.796 -0.878  0.173 -0.227  1.367  0.914  0.197]
          col  0 of Wq = [-0.356 -0.296  0.334 -0.379  0.096  0.449 -0.408 -0.122]
          (-0.1002)*(-0.3560) + (+0.7963)*(-0.2957) + (-0.8780)*(+0.3342) + (+0.1731)*(-0.3792) + 
          (-0.2265)*(+0.0962) + (+1.3674)*(+0.4493) + (+0.9136)*(-0.4081) + (+0.1968)*(-0.1221)
          = -0.363201   |  from array: -0.363201   |  MATCH: True
    
    K_full = X_enc @ Wk -> (9, 8)
    V_full = X_enc @ Wv -> (9, 8)
    STEP 2 : split the 8 columns into 2 heads of 4
    ==============================================================================
    Q_full (9, 8)  ->  reshape(9, 2, 4)  ->  transpose  ->  (2, 9, 4)
    
    head 0 takes columns 0..3, head 1 takes columns 4..7
    
    Q head 0 (9, 4):
    [[-0.363  0.43   1.132 -1.267]
     [-1.14  -1.112  0.583 -0.527]
     [-0.04  -0.869  0.345 -0.223]
     [ 1.895  1.278  0.877  0.181]
     [ 0.557  1.335  1.018 -0.361]
     [-1.574  0.05  -0.869 -0.623]
     [-0.513  0.355 -0.385  0.953]
     [ 0.287  0.698 -0.771 -1.189]
     [-0.319 -0.98  -0.91   1.053]]
    
    Q head 1 (9, 4):
    [[ 0.149 -0.034 -0.191 -0.764]
     [-0.22   1.32   0.033 -0.87 ]
     [ 1.661 -0.113 -1.033  2.714]
     [ 1.397  0.233  0.047  3.064]
     [-0.263  0.392  0.511 -0.763]
     [-1.129  0.147  0.361 -1.317]
     [-0.637  1.16   0.4   -0.138]
     [-0.113 -0.821  0.7    1.169]
     [ 0.767 -1.128 -0.522  1.422]]
    
    matches the column slices of Q_full: True
    STEP 3 : scores = Q0 @ K0^T, THEN divide by sqrt(d_k)
    ==============================================================================
    ==============================================================================
      Q_head0 (9, 4)   @   K_head0^T (4, 9)   =   raw_scores (9, 9)
      shape rule: (9 x 4) @ (4 x 9) -> (9 x 9)   inner dim 4 cancels
      K is TRANSPOSED: (9,4) becomes (4,9) so the inner dim 4 can cancel
    ==============================================================================
    
    Q_head0 =
              the |   -0.363    +0.430    +1.132    -1.267
              cat |   -1.140    -1.112    +0.583    -0.527
              sat |   -0.040    -0.869    +0.345    -0.223
               on |   +1.895    +1.278    +0.877    +0.181
              the |   +0.557    +1.335    +1.018    -0.361
              mat |   -1.574    +0.050    -0.869    -0.623
              and |   -0.513    +0.355    -0.385    +0.953
              was |   +0.287    +0.698    -0.771    -1.189
          looking |   -0.319    -0.980    -0.910    +1.053
    
    K_head0^T =
         -0.642    -0.815    -0.212    +0.833    +0.126    +0.888    +1.368    +1.429    +0.161
         +0.947    +1.333    +0.556    -0.997    -0.482    -0.151    -1.584    -0.377    -1.964
         -0.654    -0.331    +1.075    +1.658    +0.012    +0.471    +1.217    +0.183    +2.485
         -0.265    +0.315    +0.390    -0.011    -0.353    -0.344    -0.221    +0.313    +1.113
    
    raw_scores =
              the |   +0.235    +0.094    +1.038    +1.159    +0.208    +0.582    +0.480    -0.871    +0.500
              cat |   -0.563    -0.913    +0.046    +1.132    +0.584    -0.388    +1.027    -1.269    +2.864
              sat |   -0.964    -1.311    -0.190    +1.408    +0.496    +0.335    +1.791    +0.264    +2.311
               on |   -0.628    -0.073    +1.321    +1.757    -0.429    +1.841    +1.597    +2.444    +0.174
              the |   +0.336    +0.874    +1.576    +0.824    -0.432    +0.897    -0.033    +0.366    -0.405
              mat |   +1.791    +1.441    -0.816    -2.796    -0.014    -1.601    -3.154    -2.623    -3.204
              and |   +0.665    +1.320    +0.265    -1.430    -0.577    -1.019    -1.944    -0.639    -0.675
              was |   +1.297    +0.578    -0.966    -1.724    +0.110    +0.195    -1.390    -0.367    -4.566
          looking |   -0.407    -0.414    -1.043    -0.807    +0.049    -0.926    -0.223    +0.078    +0.785
    
      worked element  raw_scores[0,0]  =  row 0 of Q_head0  .  column 0 of K_head0^T
          row  0 of Q_head0 = [-0.363  0.43   1.132 -1.267]
          col  0 of K_head0^T = [-0.642  0.947 -0.654 -0.265]
          (-0.3632)*(-0.6416) + (+0.4299)*(+0.9465) + (+1.1320)*(-0.6542) + (-1.2669)*(-0.2648)
          = +0.234868   |  from array: +0.234868   |  MATCH: True
    
    then divide every entry by sqrt(d_k) = sqrt(4) = 2:
    [[ 0.117  0.047  0.519  0.579  0.104  0.291  0.24  -0.436  0.25 ]
     [-0.282 -0.457  0.023  0.566  0.292 -0.194  0.514 -0.634  1.432]
     [-0.482 -0.656 -0.095  0.704  0.248  0.167  0.896  0.132  1.156]
     [-0.314 -0.037  0.661  0.878 -0.214  0.921  0.798  1.222  0.087]
     [ 0.168  0.437  0.788  0.412 -0.216  0.449 -0.016  0.183 -0.203]
     [ 0.895  0.72  -0.408 -1.398 -0.007 -0.8   -1.577 -1.311 -1.602]
     [ 0.332  0.66   0.133 -0.715 -0.288 -0.509 -0.972 -0.319 -0.338]
     [ 0.648  0.289 -0.483 -0.862  0.055  0.098 -0.695 -0.183 -2.283]
     [-0.204 -0.207 -0.522 -0.404  0.025 -0.463 -0.112  0.039  0.393]]
    
    matches enc_scores[0]: True
    
    >>> this (9, 9) matrix is WEIGHTS, not output. 9 queries x 9 keys.
    STEP 5 : the multiplication that answers the shape question
    ==============================================================================
    ==============================================================================
      A (weights) (9, 9)   @   V_head0 (9, 4)   =   head0_out (9, 4)
      shape rule: (9 x 9) @ (9 x 4) -> (9 x 4)   inner dim 9 cancels
      the 9 KEYS are the shared dimension and cancel: (9,9)@(9,4) -> (9,4)
    ==============================================================================
    
    A (weights) =
              the |   +0.100    +0.093    +0.149    +0.158    +0.098    +0.119    +0.113    +0.057    +0.114
              cat |   +0.059    +0.050    +0.080    +0.138    +0.105    +0.065    +0.131    +0.042    +0.329
              sat |   +0.046    +0.039    +0.068    +0.152    +0.096    +0.089    +0.184    +0.086    +0.239
               on |   +0.045    +0.060    +0.121    +0.150    +0.050    +0.156    +0.138    +0.211    +0.068
              the |   +0.100    +0.131    +0.186    +0.128    +0.068    +0.133    +0.083    +0.102    +0.069
              mat |   +0.325    +0.273    +0.088    +0.033    +0.132    +0.060    +0.027    +0.036    +0.027
              and |   +0.172    +0.238    +0.140    +0.060    +0.092    +0.074    +0.047    +0.089    +0.088
              was |   +0.243    +0.169    +0.078    +0.054    +0.134    +0.140    +0.063    +0.106    +0.013
          looking |   +0.103    +0.102    +0.075    +0.084    +0.129    +0.079    +0.112    +0.131    +0.186
    
    V_head0 =
         -1.375    -1.282    +0.058    +1.186
         -2.807    -1.310    -0.778    +1.310
         +0.743    +0.139    -0.711    -1.884
         +1.641    -0.434    +0.404    -0.671
         -1.132    -1.533    +0.746    +2.018
         -3.082    -0.455    -1.491    +0.839
         -2.619    -1.071    -0.747    +1.683
         -2.389    +0.491    -2.011    -0.774
         -1.053    -0.132    +0.331    -1.287
    
    head0_out =
              the |   -1.056    -0.609    -0.373    +0.150
              cat |   -1.043    -0.545    -0.128    -0.077
              sat |   -1.195    -0.542    -0.307    +0.081
               on |   -1.373    -0.387    -0.771    +0.019
              the |   -1.177    -0.543    -0.567    +0.075
              mat |   -1.611    -1.020    -0.317    +0.854
              and |   -1.461    -0.731    -0.478    +0.355
              was |   -1.678    -0.833    -0.516    +0.723
          looking |   -1.426    -0.606    -0.399    +0.233
    
      worked element  head0_out[0,0]  =  row 0 of A (weights)  .  column 0 of V_head0
          row  0 of A (weights) = [0.1   0.093 0.149 0.158 0.098 0.118 0.113 0.057 0.114]
          col  0 of V_head0 = [-1.375 -2.807  0.743  1.641 -1.132 -3.082 -2.619 -2.389 -1.053]
          (+0.0996)*(-1.3749) + (+0.0929)*(-2.8072) + (+0.1489)*(+0.7433) + (+0.1581)*(+1.6414) + 
          (+0.0983)*(-1.1323) + (+0.1185)*(-3.0817) + (+0.1126)*(-2.6194) + (+0.0573)*(-2.3888) + 
          (+0.1138)*(-1.0528)
          = -1.055683   |  from array: -1.055683   |  MATCH: True
    
    >>> (9, 9) @ (9, 4) = (9, 4).  The 9x9 collapsed back to width 4.
    >>> Row i of the result is a weighted average of the 9 rows of V,
    >>> so it must have V's width (d_v = 4), not the token count.
    STEPS 6 and 7 : concatenate the heads, then project with W_O
    ==============================================================================
    head0_out (9, 4)   +   head1_out (9, 4)   side by side   ->   concat (9, 8)
    
         token |      ---- head 0 (dims 0-3) ----       |      ---- head 1 (dims 4-7) ----      
    --------------------------------------------------------------------------------------------
           the |   -1.056    -0.609    -0.373    +0.150 |   +0.444    +0.965    +0.219    +0.641
           cat |   -1.043    -0.545    -0.128    -0.077 |   +1.377    +0.827    +0.138    +0.455
           sat |   -1.195    -0.542    -0.307    +0.081 |   -1.675    +1.218    +0.676    +0.960
            on |   -1.373    -0.387    -0.771    +0.019 |   -1.276    +1.107    +0.730    +0.920
           the |   -1.177    -0.543    -0.567    +0.075 |   +1.481    +0.723    +0.177    +0.401
           mat |   -1.611    -1.020    -0.317    +0.854 |   +1.799    +0.757    +0.101    +0.299
           and |   -1.461    -0.731    -0.478    +0.355 |   +1.617    +0.604    +0.223    +0.383
           was |   -1.678    -0.833    -0.516    +0.723 |   +0.761    +0.511    +0.451    +0.556
       looking |   -1.426    -0.606    -0.399    +0.233 |   -1.386    +1.132    +0.539    +0.899
    
    matches concat_e: True
    
    ==============================================================================
      concat (9, 8)   @   W_O (8, 8)   =   attn_out (9, 8)
      shape rule: (9 x 8) @ (8 x 8) -> (9 x 8)   inner dim 8 cancels
      W_O is what MIXES the two heads โ€” without it they stay separate
    ==============================================================================
    
    concat =
              the |   -1.056    -0.609    -0.373    +0.150    +0.444    +0.965    +0.219    +0.641
              cat |   -1.043    -0.545    -0.128    -0.077    +1.377    +0.827    +0.138    +0.455
              sat |   -1.195    -0.542    -0.307    +0.081    -1.675    +1.218    +0.676    +0.960
               on |   -1.373    -0.387    -0.771    +0.019    -1.276    +1.107    +0.730    +0.920
              the |   -1.177    -0.543    -0.567    +0.075    +1.481    +0.723    +0.177    +0.401
              mat |   -1.611    -1.020    -0.317    +0.854    +1.799    +0.757    +0.101    +0.299
              and |   -1.461    -0.731    -0.478    +0.355    +1.617    +0.604    +0.223    +0.383
              was |   -1.678    -0.833    -0.516    +0.723    +0.761    +0.511    +0.451    +0.556
          looking |   -1.426    -0.606    -0.399    +0.233    -1.386    +1.132    +0.539    +0.899
    
    W_O =
         -0.504    -0.106    +0.306    +0.237    +0.601    -0.272    -0.002    -0.209
         -0.073    +0.437    +0.226    +0.468    -0.363    +0.457    -0.539    +0.476
         -0.209    -0.227    +0.015    +0.540    -0.554    -0.181    +0.437    -0.127
         -0.368    -0.092    -0.140    +0.509    +0.321    +0.006    +0.598    -0.043
         -0.541    -0.291    -0.078    -0.420    +0.548    +0.327    -0.284    +0.111
         +0.271    +0.540    +0.467    +0.019    +0.336    +0.076    +0.065    -0.472
         -0.458    +0.036    -0.283    -0.587    +0.273    -0.573    +0.117    +0.048
         +0.574    -0.019    -0.534    +0.063    +0.418    +0.609    -0.085    -0.220
    
    attn_out =
              the |   +0.889    +0.304    -0.476    -0.917    +0.737    +0.560    +0.165    -0.565
              cat |   +0.298    -0.049    -0.437    -1.226    +0.878    +0.768    -0.166    -0.353
              sat |   +2.155    +1.103    -0.508    -0.271    -0.249    -0.125    +0.760    -0.912
               on |   +2.059    +1.127    -0.603    -0.726    +0.005    +0.145    +0.327    -0.632
              the |   +0.268    -0.033    -0.544    -1.488    +1.099    +0.857    -0.295    -0.200
              mat |   -0.004    -0.399    -0.824    -1.378    +1.245    +0.804    +0.451    -0.363
              and |   +0.166    -0.233    -0.781    -1.540    +1.076    +0.832    -0.026    -0.176
              was |   +0.589    -0.076    -1.056    -1.237    +0.755    +0.541    +0.482    -0.268
          looking |   +2.087    +0.973    -0.608    -0.374    -0.197    +0.056    +0.747    -0.810
    
      worked element  attn_out[0,0]  =  row 0 of concat  .  column 0 of W_O
          row  0 of concat = [-1.056 -0.61  -0.373  0.15   0.444  0.965  0.219  0.641]
          col  0 of W_O = [-0.504 -0.073 -0.209 -0.368 -0.541  0.271 -0.458  0.574]
          (-1.0557)*(-0.5044) + (-0.6095)*(-0.0727) + (-0.3735)*(-0.2093) + (+0.1498)*(-0.3685) + 
          (+0.4438)*(-0.5410) + (+0.9654)*(+0.2712) + (+0.2191)*(-0.4579) + (+0.6409)*(+0.5742)
          = +0.889213   |  from array: +0.889213   |  MATCH: True

    Encoder Add & Norm (1)

    $$X' = \mathrm{LayerNorm}\big(X_{enc} + \mathrm{MultiHead}(X_{enc})\big)$$

    The residual + is what lets the original token identity survive an arbitrary number of layers; the LayerNorm is what keeps the magnitude from drifting. Watch the mean and standard deviation before and after.

    attention output   : mean=+0.0581  std=0.8073
    after residual add : mean=+0.4630  std=1.2245
    after LayerNorm    : mean=+0.0000  std=1.0000   <- rescaled
    
    Encoder Add&Norm 1 output (shape (9, 8)):
    [[ 0.411  0.715 -1.678 -1.084  0.14   1.521  0.693 -0.718]
     [ 0.175  0.808 -1.938  0.333 -0.421  1.739 -0.288 -0.408]
     [ 2.439  0.46  -0.19  -0.744 -0.779 -0.183 -0.735 -0.268]
     [ 1.913 -0.932  0.534 -1.464 -0.105  0.682 -0.745  0.116]
     [-0.502 -0.768 -0.893 -1.211  0.82   1.975  0.565  0.014]
     [-1.366  0.494 -1.875  0.498  0.814  0.489  1.096 -0.149]
     [-0.831 -1.17  -1.532  0.369  0.84   1.15  -0.021  1.195]
     [-0.644  1.286 -0.341 -0.976 -0.26   1.483 -1.379  0.832]
     [ 1.965  0.004  0.529 -0.079 -1.908 -0.457 -0.002 -0.052]]

    Add & Norm Operands

    ==============================================================================
      residual = X_enc + attn_out      all three are (9, 8) โ€” element-wise, no mixing
    ==============================================================================
    
           the | 
                X_enc:   -0.100    +0.796    -0.878    +0.173    -0.226    +1.367    +0.914    +0.197
             attn_out:   +0.889    +0.304    -0.476    -0.917    +0.737    +0.560    +0.165    -0.565
             residual:   +0.789    +1.101    -1.354    -0.744    +0.511    +1.927    +1.078    -0.368
    
           cat | 
                X_enc:   +0.157    +1.331    -1.870    +1.888    -1.201    +1.731    +0.015    +0.046
             attn_out:   +0.298    -0.049    -0.437    -1.226    +0.878    +0.768    -0.166    -0.353
             residual:   +0.455    +1.282    -2.307    +0.662    -0.324    +2.499    -0.150    -0.307
    
           sat | 
                X_enc:   +2.302    +0.423    +1.071    +0.014    -0.059    +0.699    -1.004    +1.360
             attn_out:   +2.155    +1.103    -0.508    -0.271    -0.249    -0.125    +0.760    -0.912
             residual:   +4.457    +1.526    +0.563    -0.257    -0.308    +0.575    -0.243    +0.448
    
            on | 
                X_enc:   +1.362    -1.867    +2.008    -0.792    +0.465    +1.475    -0.793    +1.424
             attn_out:   +2.059    +1.127    -0.603    -0.726    +0.005    +0.145    +0.327    -0.632
             residual:   +3.421    -0.739    +1.404    -1.518    +0.470    +1.620    -0.466    +0.793
    
           the | 
                X_enc:   -0.857    -0.857    -0.489    +0.094    -0.186    +1.367    +0.918    +0.197
             attn_out:   +0.268    -0.033    -0.544    -1.488    +1.099    +0.857    -0.295    -0.200
             residual:   -0.589    -0.890    -1.033    -1.394    +0.912    +2.223    +0.623    -0.003
    
           mat | 
                X_enc:   -0.725    +1.319    -0.357    +2.302    -0.042    +0.111    +1.003    +0.713
             attn_out:   -0.004    -0.399    -0.824    -1.378    +1.245    +0.804    +0.451    -0.363
             residual:   -0.729    +0.920    -1.180    +0.924    +1.204    +0.915    +1.454    +0.350
    
           and | 
                X_enc:   -0.815    -0.747    -0.552    +2.061    -0.096    +0.450    +0.166    +1.502
             attn_out:   +0.166    -0.233    -0.781    -1.540    +1.076    +0.832    -0.026    -0.176
             residual:   -0.649    -0.980    -1.333    +0.521    +0.980    +1.282    +0.140    +1.326
    
           was | 
                X_enc:   -0.207    +2.033    +1.686    +1.348    -0.060    +1.577    -0.700    +1.856
             attn_out:   +0.589    -0.076    -1.056    -1.237    +0.755    +0.541    +0.482    -0.268
             residual:   +0.382    +1.958    +0.630    +0.111    +0.696    +2.119    -0.218    +1.587
    
       looking | 
                X_enc:   +1.223    -0.394    +1.918    +0.837    -1.887    -0.120    -0.178    +1.310
             attn_out:   +2.087    +0.973    -0.608    -0.374    -0.197    +0.056    +0.747    -0.810
             residual:   +3.311    +0.579    +1.310    +0.463    -2.084    -0.064    +0.569    +0.500
    
      check element [0,0]: -0.100154 + +0.889213 = +0.789059   MATCH: True
    
    
    Then LayerNorm, row by row (each token uses only its own 8 numbers):
    
         token |      mean       std | before -> after (dim 0..7)
    --------------------------------------------------------------------------------------------
           the |   +0.3675    1.0255 |  +0.79   +1.10   -1.35   -0.74   +0.51   +1.93   +1.08   -0.37
               |                     |  +0.41   +0.71   -1.68   -1.08   +0.14   +1.52   +0.69   -0.72   <- normalized
           cat |   +0.2262    1.3070 |  +0.46   +1.28   -2.31   +0.66   -0.32   +2.50   -0.15   -0.31
               |                     |  +0.18   +0.81   -1.94   +0.33   -0.42   +1.74   -0.29   -0.41   <- normalized
           sat |   +0.8451    1.4812 |  +4.46   +1.53   +0.56   -0.26   -0.31   +0.57   -0.24   +0.45
               |                     |  +2.44   +0.46   -0.19   -0.74   -0.78   -0.18   -0.73   -0.27   <- normalized
            on |   +0.6231    1.4626 |  +3.42   -0.74   +1.40   -1.52   +0.47   +1.62   -0.47   +0.79
               |                     |  +1.91   -0.93   +0.53   -1.46   -0.10   +0.68   -0.74   +0.12   <- normalized
           the |   -0.0187    1.1352 |  -0.59   -0.89   -1.03   -1.39   +0.91   +2.22   +0.62   -0.00
               |                     |  -0.50   -0.77   -0.89   -1.21   +0.82   +1.98   +0.57   +0.01   <- normalized
           mat |   +0.4821    0.8865 |  -0.73   +0.92   -1.18   +0.92   +1.20   +0.92   +1.45   +0.35
               |                     |  -1.37   +0.49   -1.88   +0.50   +0.81   +0.49   +1.10   -0.15   <- normalized
           and |   +0.1609    0.9748 |  -0.65   -0.98   -1.33   +0.52   +0.98   +1.28   +0.14   +1.33
               |                     |  -0.83   -1.17   -1.53   +0.37   +0.84   +1.15   -0.02   +1.20   <- normalized
           was |   +0.9080    0.8164 |  +0.38   +1.96   +0.63   +0.11   +0.70   +2.12   -0.22   +1.59
               |                     |  -0.64   +1.29   -0.34   -0.98   -0.26   +1.48   -1.38   +0.83   <- normalized
       looking |   +0.5729    1.3930 |  +3.31   +0.58   +1.31   +0.46   -2.08   -0.06   +0.57   +0.50
               |                     |  +1.97   +0.00   +0.53   -0.08   -1.91   -0.46   -0.00   -0.05   <- normalized

    10. Encoder: Feed-Forward Network

    $$\mathrm{FFN}(x) = \max(0,\, xW_1 + b_1)\,W_2 + b_2, \qquad W_1 \in \mathbb{R}^{8 \times 32},\; W_2 \in \mathbb{R}^{32 \times 8}$$

    "Position-wise" is the key word: the same $W_1, W_2$ are applied to each of the 15 token vectors independently. There is no mixing across positions here โ€” all cross-token communication happened in the attention sub-layer. Equivalently, this is a 1-D convolution with kernel size 1.

    Why widen to $d_{ff} = 4 d_{model}$ and come back? The expansion plus ReLU is what gives the block its non-linearity and its capacity; attention alone is (up to the softmax) a linear re-weighting of values. In a trained model the FFN holds the majority of the parameters.

    ReLU: $\max(0, z)$ โ€” it zeroes the negative half, and here roughly half the 32 hidden units will be inactive per token.

    input           (9, 8)
    @ W1 + b1    -> (9, 32)
    ReLU         -> (9, 32)   (47.9% of units zeroed)
    @ W2 + b2    -> (9, 8)
    
    FFN output:
    [[-0.356 -0.582  0.497 -0.98  -0.271 -0.415  0.712 -0.682]
     [-0.286 -0.416  0.454 -1.062  0.248 -1.02   0.6   -1.103]
     [ 0.929 -0.895  0.363 -0.304  0.117 -0.872  0.691 -0.045]
     [ 1.508 -0.685 -0.219 -0.446 -0.421 -0.528  0.863  0.345]
     [-0.052 -0.962  0.084 -0.346  0.016 -0.607  0.336 -0.609]
     [-0.388 -0.072  0.468 -1.447 -0.708 -0.897  0.807 -0.654]
     [ 0.321 -0.755  0.363 -0.291 -0.218 -1.309  0.955 -0.376]
     [ 0.28  -0.163 -0.124  0.022  0.478 -0.853  0.423 -0.951]
     [ 0.614 -0.12  -0.11  -0.333  0.078 -0.498  0.419  0.031]]
    
    Hand check, token 0 hidden unit 5:
      x . W1[:,5] + b1 = 0.586878  ->  ReLU = 0.586878
      from arrays: pre-ReLU 0.586878, post-ReLU 0.586878

    FFN Matrices

    The requested view: the $(9,8)$ input printed next to $W_1$ $(8,32)$, so the multiplication is visible rather than implied.

    ==============================================================================
      FFN input (9,8) (9, 8)   @   W1 (8,32) (8, 32)   =   pre-ReLU (9,32) (9, 32)
      shape rule: (9 x 8) @ (8 x 32) -> (9 x 32)   inner dim 8 cancels
      each of the 9 tokens is expanded from 8 to 32 dims, independently
    ==============================================================================
    
    FFN input (9,8) =
              the |   +0.411    +0.715    -1.678    -1.084    +0.140    +1.521    +0.693    -0.718
              cat |   +0.175    +0.808    -1.938    +0.333    -0.421    +1.739    -0.288    -0.408
              sat |   +2.439    +0.460    -0.190    -0.744    -0.779    -0.183    -0.735    -0.268
               on |   +1.913    -0.932    +0.534    -1.464    -0.105    +0.682    -0.745    +0.116
              the |   -0.502    -0.768    -0.893    -1.211    +0.820    +1.975    +0.565    +0.014
              mat |   -1.366    +0.494    -1.875    +0.498    +0.814    +0.489    +1.096    -0.149
              and |   -0.831    -1.170    -1.532    +0.369    +0.840    +1.150    -0.021    +1.195
              was |   -0.644    +1.286    -0.341    -0.976    -0.260    +1.483    -1.379    +0.832
          looking |   +1.965    +0.004    +0.529    -0.079    -1.908    -0.457    -0.002    -0.052
    
    W1 (8,32) =   [showing first 12 of 32 columns]
         +0.315    -0.239    +0.089    -0.073    -0.355    -0.118    -0.182    +0.144    +0.348    -0.180    +0.241    -0.344
         +0.349    -0.090    -0.059    +0.166    +0.080    -0.156    -0.069    -0.219    +0.361    -0.286    -0.099    -0.367
         +0.372    -0.267    -0.212    -0.223    +0.227    -0.243    -0.336    +0.386    -0.368    +0.260    -0.104    +0.265
         -0.129    +0.017    +0.066    -0.381    +0.202    +0.165    -0.104    -0.346    +0.197    +0.353    -0.201    -0.137
         +0.186    -0.320    +0.131    -0.069    -0.023    -0.349    -0.239    +0.225    -0.036    -0.154    -0.090    +0.275
         -0.308    +0.177    -0.233    -0.100    +0.001    +0.175    -0.300    -0.073    -0.012    +0.032    +0.247    -0.254
         +0.309    +0.307    +0.144    +0.278    -0.319    +0.283    -0.139    -0.249    -0.202    -0.064    -0.193    -0.139
         +0.043    +0.021    +0.120    -0.207    +0.282    -0.148    +0.150    -0.375    -0.099    -0.115    +0.034    -0.331
    
    pre-ReLU (9,32) =   [showing first 12 of 32 columns]
              the |   -0.367    +0.688    -0.043    +1.056    -1.113    +0.587    -0.142    -0.352    +0.713    -1.031    +0.626    -0.907
              cat |   -1.148    +0.753    -0.148    +0.287    -0.381    +0.808    +0.087    -1.012    +1.225    -0.464    +0.605    -1.298
              sat |   +0.627    -0.601    -0.015    +0.147    -0.848    -0.365    -0.032    +0.557    +1.142    -0.690    +0.870    -0.934
               on |   +0.210    -0.613    -0.250    -0.148    -0.657    -0.524    -0.370    +1.262    -0.022    -0.383    +1.118    -0.112
              the |   -0.885    +0.668    -0.160    +0.470    -0.523    +0.412    -0.295    +0.065    -0.530    -0.450    +0.597    +0.024
              mat |   -0.688    +0.950    +0.413    +0.641    -0.208    +0.755    +0.276    -1.271    +0.251    -0.369    -0.452    -0.279
              and |   -1.442    +0.672    +0.328    -0.358    +0.255    +0.438    +0.345    -0.920    -0.231    -0.012    +0.253    -0.194
              was |   -0.652    +0.053    -0.603    +0.023    +0.740    -0.365    +0.179    -0.302    +0.361    -0.606    +0.632    -0.739
          looking |   +0.612    -0.085    -0.092    -0.043    -0.566    +0.219    +0.059    +0.138    +0.553    +0.040    +0.492    -0.917
    
      worked element  pre-ReLU (9,32)[0,0]  =  row 0 of FFN input (9,8)  .  column 0 of W1 (8,32)
          row  0 of FFN input (9,8) = [ 0.411  0.715 -1.678 -1.084  0.14   1.521  0.693 -0.718]
          col  0 of W1 (8,32) = [ 0.315  0.349  0.372 -0.129  0.186 -0.308  0.309  0.043]
          (+0.4110)*(+0.3150) + (+0.7148)*(+0.3490) + (-1.6782)*(+0.3724) + (-1.0841)*(-0.1285) + 
          (+0.1398)*(+0.1856) + (+1.5211)*(-0.3084) + (+0.6930)*(+0.3088) + (-0.7175)*(+0.0428)
          = -0.366652   |  from array: -0.366652   |  MATCH: True
    
    
    ReLU zeroes every negative entry โ€” 138 of 288 entries (47.9%):
    
         token |  negatives killed | first 12 units after ReLU
    ----------------------------------------------------------------------------------------
           the |                15 |  0.00   0.69   0.00   1.06   0.00   0.59   0.00   0.00   0.71   0.00   0.63   0.00
           cat |                15 |  0.00   0.75   0.00   0.29   0.00   0.81   0.09   0.00   1.22   0.00   0.61   0.00
           sat |                14 |  0.63   0.00   0.00   0.15   0.00   0.00   0.00   0.56   1.14   0.00   0.87   0.00
            on |                17 |  0.21   0.00   0.00   0.00   0.00   0.00   0.00   1.26   0.00   0.00   1.12   0.00
           the |                16 |  0.00   0.67   0.00   0.47   0.00   0.41   0.00   0.07   0.00   0.00   0.60   0.02
           mat |                18 |  0.00   0.95   0.41   0.64   0.00   0.75   0.28   0.00   0.25   0.00   0.00   0.00
           and |                13 |  0.00   0.67   0.33   0.00   0.26   0.44   0.35   0.00   0.00   0.00   0.25   0.00
           was |                15 |  0.00   0.05   0.00   0.02   0.74   0.00   0.18   0.00   0.36   0.00   0.63   0.00
       looking |                15 |  0.61   0.00   0.00   0.00   0.00   0.22   0.06   0.14   0.55   0.04   0.49   0.00
    ==============================================================================
      post-ReLU (9,32) (9, 32)   @   W2 (32,8) (32, 8)   =   FFN out (9,8) (9, 8)
      shape rule: (9 x 32) @ (32 x 8) -> (9 x 8)   inner dim 32 cancels
      contraction back to d_model: each output element sums 32 products
    ==============================================================================
    
    post-ReLU (9,32) =   [showing first 12 of 32 columns]
              the |   +0.000    +0.688    +0.000    +1.056    +0.000    +0.587    +0.000    +0.000    +0.713    +0.000    +0.626    +0.000
              cat |   +0.000    +0.753    +0.000    +0.287    +0.000    +0.808    +0.087    +0.000    +1.225    +0.000    +0.605    +0.000
              sat |   +0.627    +0.000    +0.000    +0.147    +0.000    +0.000    +0.000    +0.557    +1.142    +0.000    +0.870    +0.000
               on |   +0.210    +0.000    +0.000    +0.000    +0.000    +0.000    +0.000    +1.262    +0.000    +0.000    +1.118    +0.000
              the |   +0.000    +0.668    +0.000    +0.470    +0.000    +0.412    +0.000    +0.065    +0.000    +0.000    +0.597    +0.024
              mat |   +0.000    +0.950    +0.413    +0.641    +0.000    +0.755    +0.276    +0.000    +0.251    +0.000    +0.000    +0.000
              and |   +0.000    +0.672    +0.328    +0.000    +0.255    +0.438    +0.345    +0.000    +0.000    +0.000    +0.253    +0.000
              was |   +0.000    +0.053    +0.000    +0.023    +0.740    +0.000    +0.179    +0.000    +0.361    +0.000    +0.632    +0.000
          looking |   +0.612    +0.000    +0.000    +0.000    +0.000    +0.219    +0.059    +0.138    +0.553    +0.040    +0.492    +0.000
    
    W2 (32,8) =
         -0.311    -0.208    +0.162    +0.251    +0.277    -0.066    -0.318    -0.048
         -0.209    -0.162    +0.211    -0.329    +0.254    -0.056    +0.074    -0.318
         -0.126    -0.318    +0.358    +0.238    -0.031    -0.214    +0.336    -0.120
         -0.263    +0.049    +0.344    -0.384    -0.203    +0.340    -0.255    -0.300
         +0.054    +0.280    -0.018    +0.383    +0.139    -0.330    +0.382    -0.071
         -0.165    +0.045    -0.152    -0.320    -0.386    -0.039    +0.261    +0.056
         +0.387    -0.165    -0.170    -0.264    -0.260    -0.166    +0.227    +0.314
         +0.368    -0.207    +0.295    -0.133    -0.300    +0.287    +0.105    -0.260
         -0.054    -0.165    +0.183    -0.036    +0.379    -0.345    -0.112    -0.014
         +0.002    -0.143    +0.178    +0.290    +0.340    +0.236    +0.272    +0.039
         +0.252    -0.152    -0.079    -0.142    -0.025    -0.166    +0.358    -0.145
         -0.311    +0.021    -0.343    -0.302    -0.217    -0.028    -0.052    +0.232
         +0.185    -0.057    -0.129    -0.142    +0.212    -0.347    -0.377    +0.229
         -0.018    -0.286    -0.205    +0.335    +0.278    +0.162    -0.014    +0.016
         +0.368    +0.361    -0.221    +0.241    -0.235    -0.222    +0.136    -0.074
         +0.334    -0.318    +0.009    +0.324    +0.313    -0.266    -0.103    +0.092
         -0.264    +0.363    +0.321    -0.258    -0.293    -0.321    +0.317    -0.237
         -0.065    -0.040    +0.077    -0.332    -0.212    +0.334    +0.316    +0.218
         -0.372    -0.134    +0.099    -0.174    +0.317    +0.104    -0.332    +0.317
         +0.355    -0.327    +0.025    -0.152    -0.083    -0.248    +0.060    +0.355
         +0.002    -0.323    +0.093    -0.124    +0.276    -0.051    -0.238    -0.239
         +0.208    -0.182    +0.207    -0.228    -0.138    -0.027    +0.343    +0.334
         -0.227    -0.110    -0.272    +0.008    -0.031    +0.310    +0.333    -0.090
         -0.006    +0.307    +0.238    -0.384    -0.139    -0.061    -0.209    -0.233
         -0.358    +0.111    -0.184    -0.347    -0.208    +0.234    +0.272    -0.013
         -0.065    +0.370    -0.232    -0.220    -0.161    -0.253    -0.093    +0.318
         +0.089    -0.068    +0.336    -0.350    -0.224    +0.051    +0.090    -0.357
         -0.075    +0.116    +0.153    +0.107    -0.139    -0.315    -0.056    -0.288
         +0.347    -0.019    -0.142    +0.206    -0.002    -0.147    +0.242    +0.284
         +0.134    -0.351    +0.051    +0.204    -0.282    -0.210    +0.296    -0.372
         +0.196    -0.020    -0.041    -0.382    +0.247    -0.109    +0.032    +0.137
         +0.157    +0.353    +0.024    -0.078    +0.351    +0.110    -0.156    -0.289
    
    FFN out (9,8) =
              the |   -0.356    -0.582    +0.497    -0.980    -0.271    -0.415    +0.712    -0.682
              cat |   -0.286    -0.416    +0.454    -1.062    +0.248    -1.020    +0.600    -1.103
              sat |   +0.929    -0.895    +0.363    -0.304    +0.117    -0.872    +0.691    -0.045
               on |   +1.508    -0.685    -0.219    -0.446    -0.421    -0.528    +0.863    +0.345
              the |   -0.052    -0.962    +0.084    -0.346    +0.016    -0.607    +0.336    -0.609
              mat |   -0.388    -0.072    +0.468    -1.447    -0.708    -0.897    +0.807    -0.654
              and |   +0.321    -0.755    +0.363    -0.291    -0.218    -1.309    +0.955    -0.376
              was |   +0.280    -0.163    -0.124    +0.022    +0.478    -0.853    +0.423    -0.951
          looking |   +0.614    -0.120    -0.110    -0.333    +0.078    -0.498    +0.419    +0.031
    
      worked element  FFN out (9,8)[0,0]  =  row 0 of post-ReLU (9,32)  .  column 0 of W2 (32,8)
          row  0 of post-ReLU (9,32) = [0.    0.688 0.    1.056 0.    0.587 0.    0.    0.713 0.    0.626 0.    0.085 0.    0.    0.62  0.248 0.    0.013 0.14  0.106 0.729 1.216 0.    0.    0.385
     0.111 0.805 0.    0.296 0.    0.   ]
          col  0 of W2 (32,8) = [-0.311 -0.209 -0.126 -0.263  0.054 -0.165  0.387  0.368 -0.054  0.002  0.252 -0.311  0.185 -0.018  0.368  0.334 -0.264 -0.065 -0.372  0.355  0.002  0.208
     -0.227 -0.006 -0.358 -0.065  0.089 -0.075  0.347  0.134  0.196  0.157]
          (+0.0000)*(-0.3110) + (+0.6883)*(-0.2086) + (+0.0000)*(-0.1260) + (+1.0558)*(-0.2626) + 
          (+0.0000)*(+0.0544) + (+0.5869)*(-0.1647) + (+0.0000)*(+0.3867) + (+0.0000)*(+0.3680) + 
          (+0.7128)*(-0.0544) + (+0.0000)*(+0.0024) + (+0.6262)*(+0.2524) + (+0.0000)*(-0.3114) + 
          (+0.0851)*(+0.1855) + (+0.0000)*(-0.0177) + (+0.0000)*(+0.3681) + (+0.6197)*(+0.3343) + 
          (+0.2479)*(-0.2638) + (+0.0000)*(-0.0648) + (+0.0125)*(-0.3723) + (+0.1395)*(+0.3546) + 
          (+0.1062)*(+0.0024) + (+0.7287)*(+0.2082) + (+1.2161)*(-0.2271) + (+0.0000)*(-0.0064) + 
          (+0.0000)*(-0.3580) + (+0.3846)*(-0.0650) + (+0.1105)*(+0.0893) + (+0.8052)*(-0.0751) + 
          (+0.0000)*(+0.3467) + (+0.2959)*(+0.1340) + (+0.0000)*(+0.1958) + (+0.0000)*(+0.1570)
          = -0.355935   |  from array: -0.355935   |  MATCH: True
    
    
    Final Add & Norm of the encoder:
    ==============================================================================
      residual = LN1 out + FFN out      all three are (9, 8) โ€” element-wise, no mixing
    ==============================================================================
    
           the | 
              LN1 out:   +0.411    +0.715    -1.678    -1.084    +0.140    +1.521    +0.693    -0.718
              FFN out:   -0.356    -0.582    +0.497    -0.980    -0.271    -0.415    +0.712    -0.682
             residual:   +0.055    +0.133    -1.181    -2.064    -0.131    +1.106    +1.405    -1.399
    
           cat | 
              LN1 out:   +0.175    +0.808    -1.938    +0.333    -0.421    +1.739    -0.288    -0.408
              FFN out:   -0.286    -0.416    +0.454    -1.062    +0.248    -1.020    +0.600    -1.103
             residual:   -0.111    +0.392    -1.484    -0.729    -0.172    +0.719    +0.311    -1.511
    
           sat | 
              LN1 out:   +2.439    +0.460    -0.190    -0.744    -0.779    -0.183    -0.735    -0.268
              FFN out:   +0.929    -0.895    +0.363    -0.304    +0.117    -0.872    +0.691    -0.045
             residual:   +3.368    -0.435    +0.173    -1.048    -0.661    -1.055    -0.044    -0.313
    
            on | 
              LN1 out:   +1.913    -0.932    +0.534    -1.464    -0.105    +0.682    -0.745    +0.116
              FFN out:   +1.508    -0.685    -0.219    -0.446    -0.421    -0.528    +0.863    +0.345
             residual:   +3.421    -1.617    +0.315    -1.910    -0.525    +0.154    +0.119    +0.461
    
           the | 
              LN1 out:   -0.502    -0.768    -0.893    -1.211    +0.820    +1.975    +0.565    +0.014
              FFN out:   -0.052    -0.962    +0.084    -0.346    +0.016    -0.607    +0.336    -0.609
             residual:   -0.554    -1.730    -0.809    -1.557    +0.836    +1.368    +0.902    -0.595
    
           mat | 
              LN1 out:   -1.366    +0.494    -1.875    +0.498    +0.814    +0.489    +1.096    -0.149
              FFN out:   -0.388    -0.072    +0.468    -1.447    -0.708    -0.897    +0.807    -0.654
             residual:   -1.755    +0.422    -1.407    -0.949    +0.106    -0.408    +1.902    -0.803
    
           and | 
              LN1 out:   -0.831    -1.170    -1.532    +0.369    +0.840    +1.150    -0.021    +1.195
              FFN out:   +0.321    -0.755    +0.363    -0.291    -0.218    -1.309    +0.955    -0.376
             residual:   -0.510    -1.925    -1.169    +0.078    +0.622    -0.160    +0.934    +0.819
    
           was | 
              LN1 out:   -0.644    +1.286    -0.341    -0.976    -0.260    +1.483    -1.379    +0.832
              FFN out:   +0.280    -0.163    -0.124    +0.022    +0.478    -0.853    +0.423    -0.951
             residual:   -0.365    +1.123    -0.465    -0.955    +0.218    +0.630    -0.956    -0.119
    
       looking | 
              LN1 out:   +1.965    +0.004    +0.529    -0.079    -1.908    -0.457    -0.002    -0.052
              FFN out:   +0.614    -0.120    -0.110    -0.333    +0.078    -0.498    +0.419    +0.031
             residual:   +2.579    -0.116    +0.419    -0.412    -1.830    -0.955    +0.417    -0.021
    
      check element [0,0]: +0.411049 + -0.355935 = +0.055114   MATCH: True
    
    After LayerNorm -> encoder_output H (9, 8):
    [[ 0.277  0.346 -0.812 -1.589  0.113  1.203  1.466 -1.004]
     [ 0.268  0.905 -1.47  -0.513  0.191  1.319  0.803 -1.504]
     [ 2.52  -0.324  0.131 -0.782 -0.493 -0.787 -0.031 -0.233]
     [ 2.211 -1.096  0.173 -1.288 -0.379  0.067  0.044  0.268]
     [-0.262 -1.339 -0.496 -1.181  1.01   1.498  1.07  -0.3  ]
     [-1.276  0.717 -0.958 -0.538  0.428 -0.042  2.074 -0.405]
     [-0.368 -1.872 -1.069  0.257  0.835  0.004  1.167  1.045]
     [-0.368  1.791 -0.514 -1.225  0.478  1.076 -1.227 -0.011]
     [ 2.148 -0.105  0.342 -0.353 -1.538 -0.807  0.34  -0.026]]
    
    (the next cell assigns this to `encoder_output`)

    Encoder Add & Norm (2)

    $$\text{encoder\_output} = \mathrm{LayerNorm}\big(X' + \mathrm{FFN}(X')\big)$$

    This $(9 \times 8)$ matrix, call it $H$, is the memory the decoder will read from through cross-attention. It is computed once and reused at every generation step. In a real model, $N = 6$ such blocks are stacked; here one block is enough to see the mechanics.

    ENCODER OUTPUT  H  (shape (9, 8)):
    [[ 0.277  0.346 -0.812 -1.589  0.113  1.203  1.466 -1.004]
     [ 0.268  0.905 -1.47  -0.513  0.191  1.319  0.803 -1.504]
     [ 2.52  -0.324  0.131 -0.782 -0.493 -0.787 -0.031 -0.233]
     [ 2.211 -1.096  0.173 -1.288 -0.379  0.067  0.044  0.268]
     [-0.262 -1.339 -0.496 -1.181  1.01   1.498  1.07  -0.3  ]
     [-1.276  0.717 -0.958 -0.538  0.428 -0.042  2.074 -0.405]
     [-0.368 -1.872 -1.069  0.257  0.835  0.004  1.167  1.045]
     [-0.368  1.791 -0.514 -1.225  0.478  1.076 -1.227 -0.011]
     [ 2.148 -0.105  0.342 -0.353 -1.538 -0.807  0.34  -0.026]]
    
    mean = -0.0000,  std = 1.0000
    per-row std (should all be ~1 after LN): [1. 1. 1. 1. 1. 1. 1. 1. 1.]
    
    Compare with the old notebook, where the equivalent matrix had values around 80 000 and near-identical rows.
    Row-to-row spread here: 0.8410  (tokens remain distinguishable)

    11. Decoder Input: Shift Right

    This is the second bug from the old notebook (X_decoder_input = X). The decoder must not receive the prefix, and it must not receive the continuation unshifted.

    The continuation is fed shifted right by one, with <sos> prepended:

    position $t$ 0 1 2 3 4 5 6
    input (shifted) <sos> at a tiny mouse running by
    label (to predict) at a tiny mouse running by <eos>

    So at every position the decoder predicts the token one step to the right of its own input. This is exactly the autoregressive factorization

    $$P(y_t \mid y_{<t},\, x)$$

    written as a table.

    Teacher forcing. During training the correct previous tokens are supplied rather than the model's own guesses, which lets all 7 positions be computed in a single parallel pass instead of 7 sequential ones. At inference time the true continuation does not exist, so the loop of section 19 feeds back the model's own predictions instead.

    Combined with the causal mask (next section), the invariant is: the prediction at position $t$ depends only on the prefix $x$ and on continuation tokens $0 \dots t$ โ€” never on $t+1$. Without the shift, or without the mask, the model could read the answer and the task would be trivial.

    n_src = 9,  n_tgt = 7
    
    PREFIX (encoder): the cat sat on the mat and was looking
    
    pos  decoder input   id   ->   label (predict)   id
    --------------------------------------------------------------
      0  <sos>           1   ->   at               127
      1  at            127   ->   a                340
      2  a             340   ->   tiny              68
      3  tiny           68   ->   mouse             57
      4  mouse          57   ->   running          412
      5  running       412   ->   by               322
      6  by            322   ->   <eos>              2
    
    X_dec = E[dec_input_ids] * sqrt(d_model) + PE   shape (7, 8)
    [[ 1.382  1.649 -0.167  1.147  1.207  0.183  0.369  0.607]
     [ 2.015  0.91  -0.007  1.443 -1.561 -0.127 -1.193  1.143]
     [ 0.129  0.666 -1.151  0.565  0.054  1.111 -0.177  1.396]
     [ 0.449 -0.174  1.156  0.372 -0.137  1.282 -0.246  2.607]
     [-0.32   0.537  1.339 -0.564 -2.514  1.934 -1.363  0.775]
     [-1.805 -0.301  0.001  1.237  2.399  0.283 -0.412  0.937]
     [-0.123  1.984  0.74  -0.511 -0.352  1.13  -0.449  0.781]]

    12. Decoder: Masked Self-Attention

    Same operation as encoder self-attention, plus a look-ahead mask applied to the scores before the softmax:

    $$M_{ij} = \begin{cases} 0 & j \le i \quad \text{(past and present: allowed)} \\[2pt] -\infty & j > i \quad \text{(future: blocked)} \end{cases}$$

    $$A = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right)$$

    The mechanism: $\exp(-\infty) = 0$, so a blocked entry contributes 0 to both the numerator and the denominator of the softmax. The row still sums to 1 โ€” the probability mass is simply redistributed over the allowed positions.

    In code -1e9 is used rather than -np.inf. This matters: with -inf, a fully-masked row would produce 0/0 = NaN, and 0 * inf appears in some autograd paths. -1e9 is numerically indistinguishable after exp (it underflows to exactly 0 in float64) and is safe.

    Consequences visible below:

    • Row 0 (<sos>) attends only to itself โ†’ weight exactly 1.0, all others 0.
    • Row $t$ has exactly $t+1$ non-zero entries.
    • The weight matrix is lower-triangular.
    Causal mask (7 x 7), True = blocked:
    [[0 1 1 1 1 1 1]
     [0 0 1 1 1 1 1]
     [0 0 0 1 1 1 1]
     [0 0 0 0 1 1 1]
     [0 0 0 0 0 1 1]
     [0 0 0 0 0 0 1]
     [0 0 0 0 0 0 0]]
    
    Allowed positions per row: [1 2 3 4 5 6 7]
    [decoder masked self-attn] x_q (7, 8) , x_kv (7, 8)
      Q,K,V per head: (2, 7, 4), (2, 7, 4), (2, 7, 4)
      scores/weights: (2, 7, 7)   concat: (7, 8)   out: (7, 8)
    
    --- Head 0: scores BEFORE masking (Q K^T / sqrt(d_k)) ---
    [[ 0.5   0.56  0.12  1.41  0.44  0.59  0.41]
     [ 0.97  2.16 -0.63 -0.04  0.29 -1.32  0.03]
     [ 0.03  0.47 -0.49  0.88  0.46  0.09 -0.04]
     [ 1.22  1.08  0.69  1.45  0.28  0.57  0.83]
     [ 0.55  2.19 -0.59 -1.    0.48 -1.91 -0.17]
     [-0.05 -0.91  0.52  2.1   0.36  1.87  0.51]
     [ 0.26  0.75 -0.42 -0.87 -0.22 -1.02 -0.24]]
    
    --- Head 0: scores AFTER masking ---
    masked_scores =   (blocked entries shown as -inf)
            <sos> |   +0.503      -inf      -inf      -inf      -inf      -inf      -inf
               at |   +0.970    +2.161      -inf      -inf      -inf      -inf      -inf
                a |   +0.031    +0.475    -0.491      -inf      -inf      -inf      -inf
             tiny |   +1.220    +1.080    +0.690    +1.452      -inf      -inf      -inf
            mouse |   +0.552    +2.191    -0.586    -1.000    +0.484      -inf      -inf
          running |   -0.052    -0.905    +0.523    +2.104    +0.365    +1.867      -inf
               by |   +0.258    +0.751    -0.416    -0.868    -0.224    -1.020    -0.236
    
    --- Head 0: attention weights after softmax ---
    A =   (blocked entries are exactly 0)
            <sos> |   1.0000         0         0         0         0         0         0
               at |   0.2330    0.7670         0         0         0         0         0
                a |   0.3172    0.4946    0.1882         0         0         0         0
             tiny |   0.2687    0.2338    0.1582    0.3392         0         0         0
            mouse |   0.1313    0.6761    0.0421    0.0278    0.1227         0         0
          running |   0.0496    0.0211    0.0881    0.4282    0.0752    0.3378         0
               by |   0.2009    0.3289    0.1024    0.0652    0.1240    0.0560    0.1226
    
    Row sums (must all be 1): [1. 1. 1. 1. 1. 1. 1.]
    Row 0 = [1. 0. 0. 0. 0. 0. 0.]   <- '<sos>' can only see itself
    Strictly lower-triangular structure: True
    Row 2, unmasked scores over positions 0..2: [ 0.031  0.475 -0.491]
    exp(s - max)                                : [0.641 1.    0.381]
    / sum = 2.0220                              : [0.317 0.495 0.188]
    
    From the weights matrix (first 3 entries): [0.317 0.495 0.188]
    match: True
    remaining entries are exactly zero: True
    The black upper triangle is the mask: no token attends to its future.

    Masked Self-Attention Matrices

    The same seven steps as the encoder, with both operands and the full result matrix printed at each one. Two things differ: every shape is 7 instead of 9, and step 4 applies the causal mask between the scores and the softmax.

    STEP 1 : project X_dec to Q (K and V are identical in form)
    ==============================================================================
    ==============================================================================
      X_dec (7, 8)   @   Wq (8, 8)   =   Q_full (7, 8)
      shape rule: (7 x 8) @ (8 x 8) -> (7 x 8)   inner dim 8 cancels
      K uses Wk, V uses Wv - same shapes, same arithmetic
    ==============================================================================
    
    X_dec =
            <sos> |   +1.382    +1.649    -0.167    +1.147    +1.207    +0.183    +0.369    +0.607
               at |   +2.015    +0.910    -0.007    +1.443    -1.561    -0.127    -1.193    +1.143
                a |   +0.129    +0.666    -1.151    +0.565    +0.054    +1.111    -0.177    +1.396
             tiny |   +0.449    -0.174    +1.156    +0.372    -0.137    +1.282    -0.246    +2.607
            mouse |   -0.320    +0.537    +1.339    -0.564    -2.514    +1.934    -1.363    +0.775
          running |   -1.805    -0.301    +0.001    +1.237    +2.399    +0.283    -0.412    +0.937
               by |   -0.123    +1.984    +0.740    -0.511    -0.352    +1.130    -0.449    +0.781
    
    Wq =
         +0.420    -0.169    +0.197    -0.036    -0.369    +0.125    -0.538    -0.106
         +0.057    -0.078    +0.305    +0.402    +0.332    -0.565    -0.375    +0.045
         +0.533    +0.410    +0.420    -0.241    -0.086    -0.036    -0.420    -0.574
         +0.547    -0.318    -0.318    -0.510    +0.525    +0.098    +0.205    +0.504
         -0.189    +0.149    -0.565    -0.384    +0.406    +0.327    -0.183    -0.151
         +0.041    -0.612    -0.317    -0.357    -0.306    +0.375    -0.270    -0.378
         +0.005    +0.448    -0.317    -0.516    -0.176    +0.303    +0.062    +0.388
         +0.563    +0.358    +0.127    -0.008    -0.563    +0.593    -0.143    -0.426
    
    Q_full =
            <sos> |   +1.335    -0.345    -0.441    -0.656    +0.681    +0.293    -1.390    +0.235
               at |   +2.610    -1.152    +1.655    +0.810    -0.711    -0.363    -1.044    -0.108
                a |   +0.609    -0.977    -0.584    -0.086    -0.503    +0.945    -0.240    -0.129
             tiny |   +2.543    +0.313    +0.482    -0.855    -2.001    +2.057    -1.293    -2.201
            mouse |   +1.285    -1.150    +2.180    +1.163    -1.923    -0.497    -0.963    -2.205
          running |   -0.014    +0.271    -2.035    -1.504    +1.646    +1.387    +0.663    -0.226
               by |   +0.726    -0.334    +1.135    +0.840    -0.477    -0.576    -1.472    -1.461
    
      worked element  Q_full[0,0]  =  row 0 of X_dec  .  column 0 of Wq
          row  0 of X_dec = [ 1.382  1.649 -0.167  1.147  1.206  0.183  0.369  0.607]
          col  0 of Wq = [ 0.419  0.057  0.532  0.547 -0.189  0.041  0.005  0.563]
          (+1.3822)*(+0.4195) + (+1.6487)*(+0.0571) + (-0.1671)*(+0.5325) + (+1.1467)*(+0.5473) + 
          (+1.2065)*(-0.1891) + (+0.1831)*(+0.0411) + (+0.3687)*(+0.0052) + (+0.6067)*(+0.5629)
          = +1.335453   |  from array: +1.335453   |  MATCH: True
    
    K_full = X_dec @ Wk -> (7, 8)
    V_full = X_dec @ Wv -> (7, 8)
    STEP 2 : split the 8 columns into 2 heads of 4
    ==============================================================================
    Q_full (7, 8)  ->  reshape(7, 2, 4)  ->  transpose  ->  (2, 7, 4)
    
    head 0 takes columns 0..3, head 1 takes columns 4..7
    
    Q head 0 (7, 4):
    [[ 1.335 -0.345 -0.441 -0.656]
     [ 2.61  -1.152  1.655  0.81 ]
     [ 0.609 -0.977 -0.584 -0.086]
     [ 2.543  0.313  0.482 -0.855]
     [ 1.285 -1.15   2.18   1.163]
     [-0.014  0.271 -2.035 -1.504]
     [ 0.726 -0.334  1.135  0.84 ]]
    
    Q head 1 (7, 4):
    [[ 0.681  0.293 -1.39   0.235]
     [-0.711 -0.363 -1.044 -0.108]
     [-0.503  0.945 -0.24  -0.129]
     [-2.001  2.057 -1.293 -2.201]
     [-1.923 -0.497 -0.963 -2.205]
     [ 1.646  1.387  0.663 -0.226]
     [-0.477 -0.576 -1.472 -1.461]]
    
    matches the column slices of Q_full: True
    STEP 3 : scores = Q0 @ K0^T, THEN divide by sqrt(d_k)
    ==============================================================================
    ==============================================================================
      Q_head0 (7, 4)   @   K_head0^T (4, 7)   =   raw_scores (7, 7)
      shape rule: (7 x 4) @ (4 x 7) -> (7 x 7)   inner dim 4 cancels
      (7,4) @ (4,7) -> (7,7): 7 queries x 7 keys
    ==============================================================================
    
    Q_head0 =
            <sos> |   +1.335    -0.345    -0.441    -0.656
               at |   +2.610    -1.152    +1.655    +0.810
                a |   +0.609    -0.977    -0.584    -0.086
             tiny |   +2.543    +0.313    +0.482    -0.855
            mouse |   +1.285    -1.150    +2.180    +1.163
          running |   -0.014    +0.271    -2.035    -1.504
               by |   +0.726    -0.334    +1.135    +0.840
    
    K_head0^T =
         +0.669    +0.249    -0.194    +0.637    -0.471    +0.225    +0.134
         +0.121    -1.721    +0.590    -0.852    -1.635    +0.651    +0.002
         +0.471    +1.726    +0.724    -0.595    +0.982    -1.013    +0.458
         -0.553    -1.444    -1.567    -2.151    -2.104    -0.996    -1.296
    
    raw_scores =
            <sos> |   +1.007    +1.113    +0.246    +2.818    +0.882    +1.175    +0.826
               at |   +1.939    +4.322    -1.256    -0.083    +0.577    -2.646    +0.055
                a |   +0.061    +0.950    -0.982    +1.752    +0.917    +0.178    -0.077
             tiny |   +2.439    +2.161    +1.380    +2.905    +0.562    +1.139    +1.669
            mouse |   +1.104    +4.382    -1.172    -2.001    +0.969    -3.825    -0.340
          running |   -0.104    -1.811    +1.046    +4.208    +0.729    +3.734    +1.017
               by |   +0.516    +1.502    -0.832    -1.736    -0.449    -2.041    -0.473
    
      worked element  raw_scores[0,0]  =  row 0 of Q_head0  .  column 0 of K_head0^T
          row  0 of Q_head0 = [ 1.335 -0.345 -0.441 -0.656]
          col  0 of K_head0^T = [ 0.669  0.121  0.471 -0.552]
          (+1.3355)*(+0.6694) + (-0.3451)*(+0.1215) + (-0.4407)*(+0.4709) + (-0.6556)*(-0.5525)
          = +1.006698   |  from array: +1.006698   |  MATCH: True
    
    then divide every entry by sqrt(d_k) = sqrt(4) = 2:
    
            <sos> |   +0.503    +0.557    +0.123    +1.409    +0.441    +0.588    +0.413
               at |   +0.970    +2.161    -0.628    -0.042    +0.288    -1.323    +0.027
                a |   +0.031    +0.475    -0.491    +0.876    +0.458    +0.089    -0.039
             tiny |   +1.220    +1.080    +0.690    +1.452    +0.281    +0.569    +0.835
            mouse |   +0.552    +2.191    -0.586    -1.000    +0.484    -1.913    -0.170
          running |   -0.052    -0.905    +0.523    +2.104    +0.365    +1.867    +0.508
               by |   +0.258    +0.751    -0.416    -0.868    -0.224    -1.020    -0.236
    
    >>> still a full (7, 7) matrix - nothing is blocked yet.
    STEP 4 : apply the causal mask, BEFORE the softmax
    ==============================================================================
    Scaled scores + M.  Entries above the diagonal are set to -inf,
    so exp(-inf) = 0 and a future token can carry no weight.
    
    masked_scores =   (blocked entries shown as -inf)
            <sos> |   +0.503      -inf      -inf      -inf      -inf      -inf      -inf
               at |   +0.970    +2.161      -inf      -inf      -inf      -inf      -inf
                a |   +0.031    +0.475    -0.491      -inf      -inf      -inf      -inf
             tiny |   +1.220    +1.080    +0.690    +1.452      -inf      -inf      -inf
            mouse |   +0.552    +2.191    -0.586    -1.000    +0.484      -inf      -inf
          running |   -0.052    -0.905    +0.523    +2.104    +0.365    +1.867      -inf
               by |   +0.258    +0.751    -0.416    -0.868    -0.224    -1.020    -0.236
    
    (-1e9 is used in code as a stand-in for -inf: exp(-1e9) underflows to
     exactly 0.0 in float64, while avoiding the 0 * inf = NaN edge case.)
    
    
    After the softmax those blocked entries become hard zeros:
    
    A = softmax(masked_scores) =   (blocked entries are exactly 0)
            <sos> |   1.0000         0         0         0         0         0         0
               at |   0.2330    0.7670         0         0         0         0         0
                a |   0.3172    0.4946    0.1882         0         0         0         0
             tiny |   0.2687    0.2338    0.1582    0.3392         0         0         0
            mouse |   0.1313    0.6761    0.0421    0.0278    0.1227         0         0
          running |   0.0496    0.0211    0.0881    0.4282    0.0752    0.3378         0
               by |   0.2009    0.3289    0.1024    0.0652    0.1240    0.0560    0.1226
    
       upper triangle is exactly 0 : True
       every row still sums to 1   : [1. 1. 1. 1. 1. 1. 1.]
       non-zero entries per row    : [1 2 3 4 5 6 7]  <- row t has t+1
    STEP 5 : weights @ V, the masked weighted sum
    ==============================================================================
    ==============================================================================
      A (masked weights) (7, 7)   @   V_head0 (7, 4)   =   head0_out (7, 4)
      shape rule: (7 x 7) @ (7 x 4) -> (7 x 4)   inner dim 7 cancels
      (7,7)@(7,4) -> (7,4). Zero weights mean those V rows contribute nothing
    ==============================================================================
    
    A (masked weights) =
            <sos> |   +1.000    +0.000    +0.000    +0.000    +0.000    +0.000    +0.000
               at |   +0.233    +0.767    +0.000    +0.000    +0.000    +0.000    +0.000
                a |   +0.317    +0.495    +0.188    +0.000    +0.000    +0.000    +0.000
             tiny |   +0.269    +0.234    +0.158    +0.339    +0.000    +0.000    +0.000
            mouse |   +0.131    +0.676    +0.042    +0.028    +0.123    +0.000    +0.000
          running |   +0.050    +0.021    +0.088    +0.428    +0.075    +0.338    +0.000
               by |   +0.201    +0.329    +0.102    +0.065    +0.124    +0.056    +0.123
    
    V_head0 =
         +0.597    -0.274    +1.040    -1.039
         +1.296    +0.654    +1.575    -2.918
         +0.529    -0.749    +1.454    +0.569
         +1.053    -0.130    +2.242    +0.536
         +1.675    +1.939    +0.404    -1.420
         -0.656    -1.042    -0.113    +2.285
         +1.490    +0.772    -0.180    -0.791
    
    head0_out =
            <sos> |   +0.597    -0.274    +1.040    -1.039
               at |   +1.133    +0.438    +1.451    -2.480
                a |   +0.930    +0.096    +1.383    -1.666
             tiny |   +0.904    -0.083    +1.639    -0.690
            mouse |   +1.212    +0.609    +1.375    -2.245
          running |   +0.459    -0.328    +1.165    +0.832
               by |   +1.023    +0.352    +1.044    -1.221
    
      worked element  head0_out[0,0]  =  row 0 of A (masked weights)  .  column 0 of V_head0
          row  0 of A (masked weights) = [1. 0. 0. 0. 0. 0. 0.]
          col  0 of V_head0 = [ 0.597  1.296  0.529  1.053  1.675 -0.656  1.49 ]
          (+1.0000)*(+0.5966) + (+0.0000)*(+1.2962) + (+0.0000)*(+0.5293) + (+0.0000)*(+1.0534) + 
          (+0.0000)*(+1.6746) + (+0.0000)*(-0.6561) + (+0.0000)*(+1.4896)
          = +0.596564   |  from array: +0.596564   |  MATCH: True
    
    >>> row 0 has a single non-zero weight (1.0 on itself), so its output
    >>> equals V row 0 exactly: True
    STEPS 6 and 7 : concatenate the heads, then project with W_O
    ==============================================================================
    dec_head0 (7, 4)   +   dec_head1 (7, 4)   side by side   ->   concat (7, 8)
    
         token |      ---- head 0 (dims 0-3) ----       |      ---- head 1 (dims 4-7) ----      
    --------------------------------------------------------------------------------------------
         <sos> |   +0.597    -0.274    +1.040    -1.039 |   -1.693    +0.565    +0.092    -1.168
            at |   +1.133    +0.438    +1.451    -2.480 |   -1.629    +0.983    -0.221    -0.983
             a |   +0.930    +0.096    +1.383    -1.666 |   -1.266    +0.861    -0.252    -0.567
          tiny |   +0.904    -0.083    +1.639    -0.690 |   -1.002    +0.785    +0.393    -1.023
         mouse |   +1.212    +0.609    +1.375    -2.245 |   -1.065    +0.435    +0.340    -0.832
       running |   +0.459    -0.328    +1.165    +0.832 |   -1.040    +1.642    -0.355    -0.613
            by |   +1.023    +0.352    +1.044    -1.221 |   -0.754    +0.231    +0.862    -0.728
    
    ==============================================================================
      concat (7, 8)   @   W_O (8, 8)   =   dec_attn_out (7, 8)
      shape rule: (7 x 8) @ (8 x 8) -> (7 x 8)   inner dim 8 cancels
      W_O is what MIXES the two heads - this is the sub-layer's final output
    ==============================================================================
    
    concat =
            <sos> |   +0.597    -0.274    +1.040    -1.039    -1.693    +0.565    +0.092    -1.168
               at |   +1.133    +0.438    +1.451    -2.480    -1.629    +0.983    -0.221    -0.983
                a |   +0.930    +0.096    +1.383    -1.666    -1.266    +0.861    -0.252    -0.567
             tiny |   +0.904    -0.083    +1.639    -0.690    -1.002    +0.785    +0.393    -1.023
            mouse |   +1.212    +0.609    +1.375    -2.245    -1.065    +0.435    +0.340    -0.832
          running |   +0.459    -0.328    +1.165    +0.832    -1.040    +1.642    -0.355    -0.613
               by |   +1.023    +0.352    +1.044    -1.221    -0.754    +0.231    +0.862    -0.728
    
    W_O =
         -0.227    +0.099    +0.282    -0.109    +0.158    +0.244    +0.259    -0.549
         +0.441    -0.051    -0.291    -0.316    -0.021    +0.474    -0.149    +0.306
         -0.494    -0.029    +0.602    +0.331    -0.355    +0.002    -0.326    -0.111
         +0.343    +0.332    -0.333    +0.517    +0.113    -0.147    +0.002    +0.129
         +0.073    -0.017    +0.006    -0.396    +0.383    +0.051    -0.452    +0.326
         +0.530    +0.548    -0.160    -0.446    +0.023    -0.474    +0.532    -0.388
         -0.303    -0.114    -0.143    +0.361    -0.054    -0.403    -0.285    +0.011
         -0.432    +0.137    +0.081    +0.263    +0.536    +0.505    +0.546    -0.313
    
    dec_attn_out =
            <sos> |   -0.474    -0.135    +1.012    -0.026    -1.654    -0.811    +0.254    -1.065
               at |   -0.739    -0.320    +1.676    -1.196    -1.742    -0.106    +0.533    -1.575
                a |   -0.739    -0.063    +1.466    -0.658    -1.290    -0.138    +0.563    -1.420
             tiny |   -0.623    +0.079    +1.225    +0.033    -1.450    -0.813    -0.091    -1.099
            mouse |   -1.048    -0.594    +1.548    -0.898    -1.425    +0.099    -0.070    -1.172
          running |   +0.627    +1.178    +0.381    +0.259    -0.911    -1.163    +0.898    -1.162
               by |   -0.891    -0.412    +0.997    -0.193    -1.074    -0.265    -0.311    -0.824
    
      worked element  dec_attn_out[0,0]  =  row 0 of concat  .  column 0 of W_O
          row  0 of concat = [ 0.597 -0.274  1.04  -1.038 -1.694  0.565  0.092 -1.168]
          col  0 of W_O = [-0.227  0.441 -0.494  0.343  0.073  0.53  -0.303 -0.432]
          (+0.5966)*(-0.2273) + (-0.2739)*(+0.4406) + (+1.0401)*(-0.4938) + (-1.0385)*(+0.3430) + 
          (-1.6935)*(+0.0734) + (+0.5651)*(+0.5296) + (+0.0917)*(-0.3028) + (-1.1684)*(-0.4318)
          = -0.474334   |  from array: -0.474334   |  MATCH: True

    Decoder Add & Norm (1)

    $$Y_1 = \mathrm{LayerNorm}\big(X_{dec} + \mathrm{MaskedMultiHead}(X_{dec})\big)$$

    ==============================================================================
      residual = X_dec + attn_out      all three are (7, 8) โ€” element-wise, no mixing
    ==============================================================================
    
         <sos> | 
                X_dec:   +1.382    +1.649    -0.167    +1.147    +1.207    +0.183    +0.369    +0.607
             attn_out:   -0.474    -0.135    +1.012    -0.026    -1.654    -0.811    +0.254    -1.065
             residual:   +0.908    +1.514    +0.845    +1.120    -0.447    -0.628    +0.623    -0.458
    
            at | 
                X_dec:   +2.015    +0.910    -0.007    +1.443    -1.561    -0.127    -1.193    +1.143
             attn_out:   -0.739    -0.320    +1.676    -1.196    -1.742    -0.106    +0.533    -1.575
             residual:   +1.276    +0.590    +1.669    +0.247    -3.303    -0.232    -0.660    -0.432
    
             a | 
                X_dec:   +0.129    +0.666    -1.151    +0.565    +0.054    +1.111    -0.177    +1.396
             attn_out:   -0.739    -0.063    +1.466    -0.658    -1.290    -0.138    +0.563    -1.420
             residual:   -0.609    +0.603    +0.315    -0.093    -1.236    +0.973    +0.386    -0.024
    
          tiny | 
                X_dec:   +0.449    -0.174    +1.156    +0.372    -0.137    +1.282    -0.246    +2.607
             attn_out:   -0.623    +0.079    +1.225    +0.033    -1.450    -0.813    -0.091    -1.099
             residual:   -0.175    -0.096    +2.381    +0.406    -1.587    +0.469    -0.337    +1.509
    
         mouse | 
                X_dec:   -0.320    +0.537    +1.339    -0.564    -2.514    +1.934    -1.363    +0.775
             attn_out:   -1.048    -0.594    +1.548    -0.898    -1.425    +0.099    -0.070    -1.172
             residual:   -1.368    -0.057    +2.887    -1.462    -3.939    +2.033    -1.433    -0.396
    
       running | 
                X_dec:   -1.805    -0.301    +0.001    +1.237    +2.399    +0.283    -0.412    +0.937
             attn_out:   +0.627    +1.178    +0.381    +0.259    -0.911    -1.163    +0.898    -1.162
             residual:   -1.178    +0.877    +0.382    +1.497    +1.489    -0.880    +0.487    -0.225
    
            by | 
                X_dec:   -0.123    +1.984    +0.740    -0.511    -0.352    +1.130    -0.449    +0.781
             attn_out:   -0.891    -0.412    +0.997    -0.193    -1.074    -0.265    -0.311    -0.824
             residual:   -1.014    +1.572    +1.737    -0.704    -1.426    +0.865    -0.760    -0.043
    
      check element [0,0]: +1.382159 + -0.474334 = +0.907825   MATCH: True
    
    
    then LayerNorm, row by row -> Y1 (shape (7, 8)):
    
            <sos> |   +0.613    +1.398    +0.531    +0.889    -1.142    -1.377    +0.244    -1.156
               at |   +0.967    +0.487    +1.242    +0.247    -2.239    -0.089    -0.388    -0.229
                a |   -0.987    +0.858    +0.420    -0.201    -1.941    +1.421    +0.527    -0.097
             tiny |   -0.440    -0.370    +1.826    +0.075    -1.692    +0.131    -0.584    +1.053
            mouse |   -0.447    +0.204    +1.665    -0.494    -1.724    +1.241    -0.480    +0.035
          running |   -1.579    +0.607    +0.081    +1.266    +1.258    -1.261    +0.192    -0.565
               by |   -0.916    +1.356    +1.501    -0.644    -1.277    +0.735    -0.693    -0.063
    
    mean=+0.0000, per-row std=[1. 1. 1. 1. 1. 1. 1.]

    13. Decoder: Cross-Attention

    This is the one place where the encoder and the decoder communicate, and the shapes stop being square:

    $$Q = Y_1 W^Q \quad \text{from the \textbf{decoder}} \qquad K = H W^K, \;\; V = H W^V \quad \text{from the \textbf{encoder output } } H$$

    comes from shape
    $Q$ decoder, $Y_1$ $(7, 8) \rightarrow$ per head $(2, 7, 4)$
    $K$, $V$ encoder output $H$ $(9, 8) \rightarrow$ per head $(2, 9, 4)$
    scores $QK^\top$ $(2, \mathbf{7}, \mathbf{9})$
    output $(7, 8)$

    Read the $7 \times 9$ score matrix as: for each of the 7 decoder positions, how much attention is paid to each of the 9 prefix words. Each row sums to 1 across the 9 prefix tokens. This is the conditioning on $x$ in $P(y_t \mid y_{<t}, x)$ โ€” without this sub-layer the decoder would be a plain language model that had never seen the prefix.

    Two things to be careful about:

    1. No causal mask here. The decoder is allowed to look at the entire prefix at every step. The prefix is given, not generated, so there is no future to hide. The causal constraint lives only in the decoder's self-attention.
    2. $K$ and $V$ are computed once and reused at every generation step โ€” only $Q$ changes, because only the decoder side grows. This is why encoderโ€“decoder models cache the encoder output.
    [cross-attention] x_q (7, 8) , x_kv (9, 8)
      Q,K,V per head: (2, 7, 4), (2, 9, 4), (2, 9, 4)
      scores/weights: (2, 7, 9)   concat: (7, 8)   out: (7, 8)
    
    Q comes from the DECODER : (7, 8)  -> per head (2, 7, 4)
    K,V come from the ENCODER: (9, 8)  -> per head (2, 9, 4)
    Score matrix is rectangular: (2, 7, 9)  (heads, n_tgt, n_src)
    
    --- Head 0: cross-attention weights (7 decoder x 9 prefix) ---
    [[0.144 0.088 0.091 0.091 0.068 0.018 0.002 0.448 0.05 ]
     [0.005 0.004 0.429 0.338 0.004 0.    0.002 0.005 0.212]
     [0.034 0.039 0.222 0.204 0.044 0.029 0.184 0.019 0.225]
     [0.004 0.006 0.387 0.279 0.007 0.001 0.041 0.004 0.271]
     [0.004 0.004 0.362 0.289 0.005 0.001 0.036 0.001 0.298]
     [0.069 0.074 0.001 0.001 0.07  0.43  0.009 0.345 0.001]
     [0.035 0.029 0.269 0.255 0.037 0.015 0.077 0.011 0.272]]
    
    Each row sums to 1 over the 9 prefix tokens: True
    Decoder position 3 ('tiny') attends most to prefix tokens:
       'sat' (pos 2): 0.3868
       'on' (pos 3): 0.2788
       'looking' (pos 8): 0.2714
    
    (With untrained weights this is arbitrary โ€” a trained model would concentrate on the prefix words that actually constrain the next token.)

    Cross-Attention Matrices

    Here the two stacks meet, and the operands come from different places. This is the clearest place to see why shapes work out:

    operation shapes result
    $Q$ from decoder $Y_1$ $(7,8) @ (8,8)$ $(7,8) \to$ head $(7,4)$
    $K$ from encoder $H$ $(9,8) @ (8,8)$ $(9,8) \to$ head $(9,4)$
    $Q_0 K_0^\top$ $(7,4) @ (4,9)$ $(\mathbf{7},\mathbf{9})$
    $A V_0$ $(7,9) @ (9,4)$ $(\mathbf{7},4)$

    The output has 7 rows (one per decoder position) and 4 columns per head โ€” the 9 prefix positions appear only as the summed-over dimension. The decoder gets back exactly its own shape, having absorbed information from a sequence of a different length.

    Q source = dec_norm1        (7, 8)   (decoder)
    K,V source = encoder_output (9, 8)   (encoder)
    
    ==============================================================================
      Q_dec_head0 (7, 4)   @   K_enc_head0^T (4, 9)   =   raw_scores (7, 9)
      shape rule: (7 x 4) @ (4 x 9) -> (7 x 9)   inner dim 4 cancels
      rows indexed by DECODER positions, columns by PREFIX positions
    ==============================================================================
    
    Q_dec_head0 =
            <sos> |   -1.283    +0.863    -0.584    +2.871
               at |   +0.054    -0.599    +1.694    +2.945
                a |   +0.547    -0.652    +0.976    -0.296
             tiny |   +1.658    -0.365    +1.646    +1.304
            mouse |   +1.061    -1.398    +1.859    +0.975
          running |   -0.135    +1.784    -2.773    -0.212
               by |   +0.329    -1.214    +0.509    +0.816
    
    K_enc_head0^T =
         -1.745    -1.226    +0.050    -0.148    -0.856    -0.898    +1.177    -1.513    +0.188
         -0.360    -0.008    -1.309    -1.395    -0.341    -0.161    -1.431    +1.839    -1.672
         -0.470    -0.308    +2.101    +1.958    -0.500    -1.599    +0.232    -0.248    +1.714
         -0.051    -0.234    +1.244    +1.151    -0.186    -1.401    -1.305    +0.230    +0.913
    
    raw_scores =
            <sos> |   +2.059    +1.076    +1.152    +1.148    +0.563    -2.075    -6.626    +4.335    -0.063
               at |   -0.823    -1.272    +8.009    +7.532    -1.236    -6.786    -2.530    -0.925    +6.602
                a |   -1.164    -0.897    +2.564    +2.399    -0.679    -1.533    +2.188    -2.337    +2.596
             tiny |   -3.602    -2.843    +5.643    +4.988    -2.360    -5.890    +1.153    -3.289    +4.935
            mouse |   -2.271    -2.091    +7.002    +6.555    -1.541    -5.066    +2.408    -4.413    +6.614
          running |   +0.907    +1.056    -8.431    -8.142    +0.932    +4.565    -3.078    +4.125    -7.955
               by |   -0.418    -0.741    +3.691    +3.581    -0.273    -2.057    +1.178    -2.669    +3.709
    
      worked element  raw_scores[0,0]  =  row 0 of Q_dec_head0  .  column 0 of K_enc_head0^T
          row  0 of Q_dec_head0 = [-1.283  0.863 -0.584  2.871]
          col  0 of K_enc_head0^T = [-1.745 -0.36  -0.47  -0.051]
          (-1.2834)*(-1.7453) + (+0.8625)*(-0.3597) + (-0.5840)*(-0.4697) + (+2.8708)*(-0.0505)
          = +2.058955   |  from array: +2.058955   |  MATCH: True
    
    column labels are the prefix tokens: ['the', 'cat', 'sat', 'on', 'the', 'mat', 'and', 'was', 'looking']
    ==============================================================================
      A (7x9) (7, 9)   @   V_enc_head0 (9x4) (9, 4)   =   head0_out (7x4) (7, 4)
      shape rule: (7 x 9) @ (9 x 4) -> (7 x 4)   inner dim 9 cancels
      the 9 PREFIX positions cancel โ€” output keeps the decoder's 7 rows
    ==============================================================================
    
    A (7x9) =
            <sos> |   +0.144    +0.088    +0.091    +0.091    +0.068    +0.018    +0.002    +0.448    +0.050
               at |   +0.005    +0.004    +0.429    +0.338    +0.004    +0.000    +0.002    +0.005    +0.212
                a |   +0.034    +0.039    +0.222    +0.204    +0.044    +0.029    +0.184    +0.019    +0.225
             tiny |   +0.004    +0.006    +0.387    +0.279    +0.007    +0.001    +0.041    +0.004    +0.271
            mouse |   +0.004    +0.004    +0.362    +0.289    +0.005    +0.001    +0.036    +0.001    +0.298
          running |   +0.069    +0.074    +0.001    +0.001    +0.070    +0.430    +0.009    +0.345    +0.001
               by |   +0.035    +0.029    +0.269    +0.255    +0.037    +0.015    +0.077    +0.011    +0.272
    
    V_enc_head0 (9x4) =
         +0.972    -0.975    +0.443    +0.039
         +0.085    -1.478    +1.483    -0.670
         -0.539    -0.849    -0.472    +0.348
         +0.392    -0.988    -0.585    +0.547
         +2.163    -1.204    +0.166    +0.024
         +0.894    +0.418    -0.328    -0.174
         +1.643    -0.586    -0.894    -0.251
         +0.186    +1.391    +1.781    -0.570
         -1.243    -0.802    -1.008    +0.702
    
    head0_out (7x4) =
            <sos> |   +0.322    +0.071    +0.850    -0.194
               at |   -0.343    -0.879    -0.598    +0.477
                a |   +0.143    -0.784    -0.510    +0.261
             tiny |   -0.348    -0.859    -0.637    +0.461
            mouse |   -0.376    -0.867    -0.663    +0.481
          running |   +0.688    +0.391    +0.616    -0.318
               by |   -0.125    -0.843    -0.539    +0.378
    
      worked element  head0_out (7x4)[0,0]  =  row 0 of A (7x9)  .  column 0 of V_enc_head0 (9x4)
          row  0 of A (7x9) = [0.144 0.088 0.091 0.091 0.068 0.018 0.002 0.448 0.05 ]
          col  0 of V_enc_head0 (9x4) = [ 0.972  0.085 -0.539  0.392  2.163  0.894  1.643  0.186 -1.243]
          (+0.1437)*(+0.9724) + (+0.0879)*(+0.0852) + (+0.0913)*(-0.5387) + (+0.0911)*(+0.3924) + 
          (+0.0680)*(+2.1627) + (+0.0182)*(+0.8942) + (+0.0019)*(+1.6430) + (+0.4483)*(+0.1859) + 
          (+0.0497)*(-1.2427)
          = +0.321661   |  from array: +0.321661   |  MATCH: True
    
    >>> (7,9) @ (9,4) = (7,4).  Encoder length 9 vanishes into the sum.
    >>> This is how a decoder of length 7 reads a source of length 9.

    Decoder Add & Norm (2)

    $$Y_2 = \mathrm{LayerNorm}\big(Y_1 + \mathrm{CrossAttention}(Y_1, H)\big)$$

    Note the residual adds $Y_1$ โ€” the decoder stream โ€” not the encoder output. The encoder information enters only through the attention output. This keeps the decoder's own representation as the backbone.

    Decoder Add&Norm 2 (shape (7, 8)):
    [[ 0.764  1.392  0.269  0.846 -1.436 -0.636  0.269 -1.467]
     [ 1.128  0.09   1.533  0.505 -1.823 -0.364 -0.328 -0.741]
     [-1.147  0.963  0.417 -0.026 -1.832  1.408  0.438 -0.221]
     [-0.447 -0.56   2.124  0.323 -1.345 -0.258 -0.62   0.783]
     [-0.493 -0.006  2.077 -0.293 -1.413  0.988 -0.514 -0.345]
     [-1.54   1.181 -0.292  1.414  0.74  -0.902  0.25  -0.851]
     [-0.974  1.342  1.715 -0.487 -0.987  0.564 -0.8   -0.373]]

    14. Decoder: Feed-Forward Network

    Identical in form to the encoder FFN, with its own weights:

    $$\mathrm{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2, \qquad \text{decoder\_output} = \mathrm{LayerNorm}\big(Y_2 + \mathrm{FFN}(Y_2)\big)$$

    (7, 8) -> W1 -> (7, 32) -> ReLU -> W2 -> (7, 8)
    (49.6% of hidden units zeroed by ReLU)
    
    DECODER OUTPUT (shape (7, 8)):
    [[ 0.427  1.034  0.123  1.27  -0.264 -0.185 -0.171 -2.233]
     [ 0.851 -0.119  1.272  1.394 -0.955 -0.262 -0.678 -1.503]
     [-1.206  0.158  0.225  0.322 -0.799  2.268 -0.245 -0.722]
     [-0.107 -1.762  1.586  0.781 -0.874  0.396 -0.657  0.636]
     [-0.541 -0.844  1.685  0.095 -0.302  1.61  -0.936 -0.766]
     [-1.514  0.464 -0.833  1.901  0.461  0.283  0.168 -0.93 ]
     [-0.707  0.527  1.426 -0.643  0.119  1.46  -1.444 -0.738]]
    
    mean = +0.0000, std = 1.0000
    Magnitudes stay O(1) through the whole network โ€” contrast with ~80 000 before.

    15. Output Projection

    The decoder produced 7 vectors of dimension 8. To predict tokens they must be projected onto the vocabulary:

    $$\text{logits} = \text{decoder\_output} \cdot W_{out} + b_{out}, \qquad W_{out} \in \mathbb{R}^{d_{model} \times V} = \mathbb{R}^{8 \times 500}$$

    $$(7 \times 8) \cdot (8 \times 500) = (7 \times 500)$$

    Row $t$, column $v$ of the result is the unnormalized score ("logit") that token $v$ is the correct continuation at position $t$.

    Weight tying. The paper shares this matrix with the embedding table: $W_{out} = E^\top$. Two reasons โ€”

    • Parameter economy: $V \times d_{model} = 4000$ parameters saved here, hundreds of millions in a real model.
    • Interpretation: the logit becomes a dot product between the decoder's output vector and the embedding of each candidate token, $\text{logit}_{t,v} = \langle \text{decoder\_output}_t,\; E_v \rangle$. So the model is literally asking "which token's embedding does my prediction vector point at most?" โ€” a nearest-neighbour search in embedding space, measured by inner product.

    This is why keeping the random IDs in [50, 500) is fine: the projection covers all 500 vocabulary slots, and the handful that correspond to real words simply have meaningful embeddings while the rest are unused noise.

    decoder_output (7, 8) @ W_out (8, 500) = logits (7, 500)
    
    Logits for the last position (t = 6), first 12 vocabulary entries:
    [-0.721 -0.798 -0.249 -0.334  0.354  3.275 -0.734  0.531 -0.554  0.007  0.205 -0.104]
    
    range over the whole matrix: [-4.153, 3.611]
    
    Hand check  logit[t=6, v=2 ('<eos>')] = <decoder_output[6], E[2]>
      = (-0.707)(0.010) + (0.527)(0.452) + (1.426)(0.068) + (-0.643)(0.016) + (0.119)(-0.481) + (1.460)(0.264) + (-1.444)(0.228) + (-0.738)(0.765)
      = -0.249453
      from logits matrix: -0.249453   match: True

    Output Projection Matrix

    ==============================================================================
      decoder_output (7,8) (7, 8)   @   W_out = E^T (8,500) (8, 500)   =   logits (7,500) (7, 500)
      shape rule: (7 x 8) @ (8 x 500) -> (7 x 500)   inner dim 8 cancels
      8 collapses, 500 opens up: one score per vocabulary entry per position
    ==============================================================================
    
    decoder_output (7,8) =
            <sos> |   +0.427    +1.034    +0.123    +1.270    -0.264    -0.185    -0.171    -2.233
               at |   +0.851    -0.119    +1.272    +1.394    -0.955    -0.262    -0.678    -1.503
                a |   -1.206    +0.158    +0.225    +0.322    -0.799    +2.268    -0.245    -0.722
             tiny |   -0.107    -1.762    +1.586    +0.781    -0.874    +0.396    -0.657    +0.636
            mouse |   -0.541    -0.844    +1.685    +0.095    -0.302    +1.610    -0.936    -0.766
          running |   -1.514    +0.464    -0.833    +1.901    +0.461    +0.283    +0.168    -0.930
               by |   -0.707    +0.527    +1.426    -0.643    +0.119    +1.460    -1.444    -0.738
    
    W_out = E^T (8,500) =   [showing first 10 of 500 columns]
         +0.206    +0.489    +0.010    -0.109    -0.158    -0.198    +0.156    +0.230    -0.085    +0.201
         -0.127    +0.229    +0.452    +0.077    +0.069    +0.133    -0.178    -0.035    +0.124    +0.158
         +0.209    -0.059    +0.068    +0.088    +0.380    +0.553    -0.361    +0.653    -0.442    +0.227
         +0.392    +0.052    +0.016    +0.558    -0.363    -0.023    +0.250    -0.378    +0.510    +0.470
         +0.290    +0.427    -0.481    -0.034    +0.047    -0.196    +0.086    -0.539    -0.029    +0.069
         +0.179    -0.289    +0.264    +0.099    -0.248    +0.665    -0.199    -0.245    +0.395    +0.251
         +0.377    +0.130    +0.228    +0.215    +0.423    -0.512    -0.453    -0.016    +0.121    -0.032
         +0.413    -0.139    +0.765    +0.066    -0.539    -0.777    +0.308    +0.086    +0.161    +0.509
    
    logits (7,500) =   [showing first 10 of 500 columns]
            <sos> |   -0.617    +0.734    -1.168    +0.559    +0.753    +1.844    -0.441    -0.340    +0.239    -0.322
               at |   -0.198    +0.175    -0.851    +0.549    +0.378    +2.016    -0.144    +0.964    -0.340    +0.221
                a |   -0.312    -1.477    +0.454    +0.493    -0.144    +2.729    -0.849    -0.440    +0.960    +0.137
             tiny |   +0.672    -1.171    +0.183    +0.420    -0.545    +0.923    +0.258    +1.217    -0.308    +0.810
            mouse |   -0.084    -1.162    -0.501    +0.112    +0.236    +3.130    -0.678    +0.687    -0.348    +0.207
          running |   +0.063    -0.220    -0.650    +1.175    -0.212    +0.591    +0.079    -2.028    +1.494    +0.097
               by |   -0.721    -0.798    -0.249    -0.334    +0.354    +3.275    -0.734    +0.531    -0.554    +0.007
    
      worked element  logits (7,500)[0,0]  =  row 0 of decoder_output (7,8)  .  column 0 of W_out = E^T (8,500)
          row  0 of decoder_output (7,8) = [ 0.427  1.034  0.123  1.27  -0.264 -0.185 -0.171 -2.233]
          col  0 of W_out = E^T (8,500) = [ 0.206 -0.127  0.209  0.392  0.29   0.179  0.377  0.413]
          (+0.4266)*(+0.2064) + (+1.0344)*(-0.1270) + (+0.1228)*(+0.2088) + (+1.2699)*(+0.3920) + 
          (-0.2637)*(+0.2901) + (-0.1854)*(+0.1793) + (-0.1713)*(+0.3771) + (-2.2332)*(+0.4134)
          = -0.617476   |  from array: -0.617476   |  MATCH: True
    
    >>> the only step where the width EXPANDS: 8 -> 500.
    >>> 7 * 8 * 500 = 28,000 multiply-adds, more than the rest of the decoder combined.
    
    >>> then softmax turns each of the 7 rows into a distribution over 500 tokens.

    16. Softmax: Next-Token Distribution

    $$P(y_t = v \mid y_{<t},\, x) = \frac{\exp(\text{logit}_{t,v})}{\sum_{v'=1}^{V} \exp(\text{logit}_{t,v'})}$$

    Each of the 7 rows becomes a probability distribution over all 500 tokens, summing to 1. This is the autoregressive factorization made concrete: row $t$ is one factor of the product $\prod_t P(y_t \mid y_{<t}, x)$.

    The prediction of the next token is the last row. Position $t = 6$ has seen the prefix (through cross-attention) plus <sos> at a tiny mouse running by โ€” the complete continuation so far โ€” so its distribution answers "what comes after by?" (the correct answer being <eos>, since the sentence ends there).

    Rows $0 \dots 5$ are predictions too โ€” they are what the loss is computed on during training โ€” but only the last one is used when generating.

    probs shape (7, 500), every row sums to 1: True
    
    Prefix  (encoder): 'the cat sat on the mat and was looking'
    Context (decoder): '<sos> at a tiny mouse running by'
    Question: what token comes next?
    
    rank    id  token            logit   probability
    --------------------------------------------------
       1     5  <unused_5>      3.2750      0.033622
       2   342  <unused_342>    2.9872      0.025214
       3   190  <unused_190>    2.7904      0.020711
       4   487  <unused_487>    2.5049      0.015566
       5   208  <unused_208>    2.4846      0.015253
    
    PREDICTED NEXT TOKEN: id 5 -> '<unused_5>'
    True next token     : id 2 -> '<eos>'
    
    Probability assigned to the true token: 0.000991
    Uniform baseline (1/V)                : 0.002000
    
    With random weights the prediction is essentially chance โ€” as expected. The arithmetic is correct; the model is simply untrained.
    max probability = 0.03362 at id 5
    entropy = 5.6914 nats (maximum possible = log(500) = 6.2146)

    17. Decoding Strategies

    Turning the distribution into a token is a separate decision, not part of the model.

    Greedy โ€” take the mode: $$\hat{v} = \arg\max_v P(v)$$ Deterministic, but tends to produce repetitive text.

    Temperature $T$ โ€” rescale the logits before the softmax: $$P_T(v) = \frac{\exp(\text{logit}_v / T)}{\sum_{v'} \exp(\text{logit}_{v'} / T)}$$ * $T \to 0$: distribution collapses onto the argmax (greedy). * $T = 1$: unchanged. * $T > 1$: flatter, more random.

    Temperature is a monotone transformation of the logits, so it never changes the ranking โ€” only how sharply the mass concentrates. Entropy is the quantity to watch.

    Top-$k$ โ€” keep the $k$ highest logits, set the rest to $-\infty$, renormalize, sample. This truncates the long tail of 495 implausible tokens that collectively can still hold non-trivial probability mass.

    T   entropy (nats)    max prob   argmax id
    ------------------------------------------------
       0.1           0.2585     0.93886           5
       0.5           4.0472     0.19117           5
       1.0           5.6914     0.03362           5
       2.0           6.0906     0.00931           5
       5.0           6.1952     0.00381           5
    
    The argmax is identical for every T โ€” temperature reshapes, never reorders.
    
    Top-5 renormalized distribution:
       id   5 '<unused>' : 0.033622  ->  0.3046
       id 342 '<unused>' : 0.025214  ->  0.2285
       id 190 '<unused>' : 0.020711  ->  0.1877
       id 487 '<unused>' : 0.015566  ->  0.1410
       id 208 '<unused>' : 0.015253  ->  0.1382
       (the other 495 tokens: probability exactly 0)
    
    8 samples from the top-5 distribution: [208 487 342   5   5 487   5 342]

    18. Cross-Entropy Loss

    Prediction and training are two sides of the same forward pass. The loss at position $t$ is the negative log-probability the model assigned to the correct token:

    $$\mathcal{L}_t = -\log P_t(\text{label}_t), \qquad \mathcal{L} = \frac{1}{n_{tgt}} \sum_{t=0}^{n_{tgt}-1} \mathcal{L}_t$$

    Two reference values are useful for sanity-checking any implementation:

    • An untrained model should score $\mathcal{L} \approx \log V = \log 500 \approx 6.21$, i.e. chance level. In practice it lands slightly above $\log V$: the random distribution is not uniform, so it is confidently wrong on some positions, and $-\log p$ punishes that asymmetrically. Anything in the range $6$โ€“$8$ here is normal; a value near $0$ would mean the labels are leaking into the input.
    • Perplexity $= e^{\mathcal{L}}$ is the effective number of tokens the model is choosing between. At initialization it should be $\approx V$.

    If a fresh implementation starts far from $\log V$, something is wrong (usually a mask leak or a label misalignment). All 7 positions contribute โ€” this is why teacher forcing is efficient: one forward pass yields 7 training signals, one per factor of the autoregressive product.

    t  input        label          P(label)    -log P
    ----------------------------------------------------
      0  <sos>        at             0.002379    6.0412
      1  at           a              0.000329    8.0187
      2  a            tiny           0.001004    6.9041
      3  tiny         mouse          0.001992    6.2188
      4  mouse        running        0.000786    7.1490
      5  running      by             0.000530    7.5420
      6  by           <eos>          0.000991    6.9170
    ----------------------------------------------------
    Mean cross-entropy loss : 6.9701
    log(V) = log(500)       : 6.2146   <- untrained reference
    Perplexity  exp(loss)   : 1064.3  (vs V = 500)
    
    The loss is at chance level (slightly above log V, as expected for a random 
    non-uniform distribution). Training would drive it toward 0; backpropagation 
    is out of scope for this notebook.

    19. Autoregressive Generation

    Everything above computed all 7 positions at once, using the true continuation as decoder input (teacher forcing). At inference time the true continuation does not exist, so the decoder must be run repeatedly, feeding back its own predictions. This is the loop that the word "autoregressive" actually names:

    H = Encoder(prefix)                 # computed ONCE
    y = [<sos>]
    repeat:
        logits = Decoder(y, H)          # full re-run over the prefix of y
        next   = argmax(softmax(logits[-1]))
        y.append(next)
    until next == <eos> or len(y) > max_len
    

    Three points that are easy to miss:

    1. The encoder runs once. $H$, and therefore cross-attention's $K$ and $V$, never change. Only the decoder is re-executed. Every generated token is conditioned on the same 9-token prefix.
    2. Step $t$ costs a full decoder pass over $t{+}1$ positions. Positions $0 \dots t-1$ recompute identical values, because the causal mask guarantees they cannot depend on the newly appended token. Real implementations exploit this with a KV cache, storing $K$ and $V$ per position; the naive loop below recomputes them so the mechanism stays visible.
    3. The causal mask is still required even though nothing to the right exists yet โ€” it is what makes point 2 true, and it keeps training and inference behaviour identical.

    The two functions below wrap the exact same weights and operations used above.

    encoder_forward matches encoder_output : True
    decoder_forward matches logits         : True
    PREFIX GIVEN TO ENCODER: the cat sat on the mat and was looking
    
    step  context length  predicted          id         P   entropy
    ------------------------------------------------------------------
       0               1  <unused_479>      479   0.02287    5.7252
       1               2  <unused_479>      479   0.03229    5.6743
       2               3  <unused_314>      314   0.03234    5.6773
       3               4  <unused_314>      314   0.03286    5.6893
       4               5  <unused_314>      314   0.03137    5.6932
       5               6  <unused_314>      314   0.03170    5.6736
       6               7  <unused_314>      314   0.03167    5.6559
       7               8  <unused_314>      314   0.03266    5.6519
    
    GENERATED: <sos> <unused_479> <unused_479> <unused_314> <unused_314> <unused_314> <unused_314> <unused_314> <unused_314>
    TRUE CONTINUATION: <sos> at a tiny mouse running by <eos>
    
    The output is gibberish because the weights are random. What the loop demonstrates is the mechanism: encoder once, decoder per step, each step conditioned on all tokens generated so far.
    T=0.7  ->  <sos> <42> <130> <375> <233> <21> <179> <194> <71>
    T=1.0  ->  <sos> <43> <102> <388> <253> <45> <197> <206> <72>
    T=1.5  ->  <sos> <43> <110> <394> looking <46> <205> <243> <84>
    
    Greedy is deterministic; sampling varies with T and the seed. The forward pass is identical in all cases.

    20. Summary

    Stage Operation Output shape
    Prefix tokens split 9
    Prefix IDs vocabulary lookup $(9,)$
    Prefix embeddings $E[\text{ids}] \cdot \sqrt{d_{model}}$ $(9, 8)$
    $+$ positional encoding $X_{enc}$ $(9, 8)$
    Encoder self-attn (no mask) $Q,K,V$ per head $(2, 9, 4)$
    scores / weights $(2, 9, 9)$
    concat $\cdot W^O$ $(9, 8)$
    Add & Norm $(9, 8)$
    Encoder FFN $8 \to 32 \to 8$ $(9, 8)$
    Add & Norm encoder output $H$ $(9, 8)$
    Decoder input <sos> + continuation, shifted $(7,)$
    $+$ positional encoding $X_{dec}$ $(7, 8)$
    Masked self-attn (causal) scores / weights $(2, 7, 7)$
    Add & Norm $(7, 8)$
    Cross-attn $Q$ from decoder, $K,V$ from $H$ scores $(2, 7, 9)$
    Add & Norm $(7, 8)$
    Decoder FFN $8 \to 32 \to 8$ $(7, 8)$
    Add & Norm decoder output $(7, 8)$
    Output projection $\cdot\, W_{out} = E^\top$ $(7, 500)$
    Softmax per row over vocabulary $(7, 500)$
    Next token argmax of the last row scalar

    Where Autoregression Happens

    Requirement Enforced by Section
    $y_t$ predicted from $y_{<t}$ only shift right (<sos> prepended) 11
    no attention to the future causal mask on decoder self-attention 12
    conditioning on the prefix $x$ cross-attention, $Q$ decoder / $K,V$ encoder 13
    one factor $P(y_t \mid y_{<t}, x)$ per row softmax over the vocabulary 16
    generation = repeated conditioning the loop, decoder re-run per step 19

    Note that the encoder is deliberately not autoregressive: it is bidirectional over the prefix, because the prefix is given input rather than generated output.

    Parameter Count

    Component Count
    Embedding table $E$ (tied with $W_{out}$) $500 \times 8 = 4000$
    Encoder self-attention ($W^Q, W^K, W^V, W^O$) $4 \times 64 = 256$
    Encoder FFN $8{\cdot}32 + 32 + 32{\cdot}8 + 8 = 552$
    Decoder self-attention 256
    Cross-attention 256
    Decoder FFN 552
    LayerNorm ($\gamma, \beta$) $\times 5$ $5 \times 16 = 80$
    Total โ‰ˆ 5952

    What's Left Out

    • Backpropagation. Every weight here is random and stays random; the loss is computed but never minimized.
    • Stacking. The paper uses $N = 6$ encoder and 6 decoder blocks. The block above is one of them; stacking means feeding encoder_output into an identical block with fresh weights.
    • Dropout, learning-rate warmup, label smoothing โ€” training details, not forward-pass mechanics.
    • Padding masks and batching. With a single unpadded sequence, no <pad> mask is needed; a real batch requires masking padded key positions in both self- and cross-attention.
    • KV caching during generation, as noted in section 19.

    ---

    Appendix: Every Number, By Hand

    Everything above showed matrices. This appendix takes one token and follows it through every single arithmetic operation of the network, writing out each multiply-and-add explicitly, and checking each result against the arrays computed earlier.

    Two traces:

    • Trace A โ€” encoder token 1, "cat", from embedding lookup to encoder_output[1].
    • Trace B โ€” decoder position 6, "by", from embedding lookup to the final probability of the next token.

    Every line printed below is verified with np.isclose against the corresponding entry of the matrices computed in the main body. If a line says MATCH: True, the hand calculation and the vectorized code agree exactly.

    Helpers defined. Every step below prints its terms, its result, and a match check.

    Trace A: Encoder Token 'cat'

    A.1 Embedding Lookup and Scaling

    $$\text{emb} = E[\,\text{id}\,] \cdot \sqrt{d_{model}} = E[\,\text{id}\,] \cdot 2.828427$$

    Component by component.

    Token 'cat' at position 1, vocabulary id = 168
    
     dim        E[168]     x sqrt(8)         = emb
    ------------------------------------------------
       0     -0.242053      2.828427     -0.684630
       1      0.279528      2.828427      0.790626
       2     -0.696537      2.828427     -1.970104
       3      0.315581      2.828427      0.892597
       4     -0.428214      2.828427     -1.211172
       5      0.258364      2.828427      0.730764
       6      0.005046      2.828427      0.014273
       7     -0.337269      2.828427     -0.953939
    
    matches src_emb[1]: True

    A.2 Positional Encoding

    $$X_{enc}[1, d] = \text{emb}[d] + PE[1, d]$$

    with, for even $d = 2i$: $PE[1, 2i] = \sin(1 \cdot \omega_i)$ and for odd $d = 2i+1$: $PE[1, 2i+1] = \cos(1 \cdot \omega_i)$, where $\omega_i = 10000^{-2i/8}$.

    dim    i    fn       omega_i     PE[1,d]         emb    X_enc[1,d]
    --------------------------------------------------------------------
       0    0   sin      1.000000    0.841471   -0.684630      0.156841
       1    0   cos      1.000000    0.540302    0.790626      1.330928
       2    1   sin      0.100000    0.099833   -1.970104     -1.870270
       3    1   cos      0.100000    0.995004    0.892597      1.887602
       4    2   sin      0.010000    0.010000   -1.211172     -1.201172
       5    2   cos      0.010000    0.999950    0.730764      1.730714
       6    3   sin      0.001000    0.001000    0.014273      0.015273
       7    3   cos      0.001000    1.000000   -0.953939      0.046060
    
    matches X_enc[1]: True
    x = X_enc[1] = [ 0.157  1.331 -1.87   1.888 -1.201  1.731  0.015  0.046]

    A.3 Projection to Q, K, V

    $$Q[1, c] = \sum_{d=0}^{7} X_{enc}[1, d] \cdot W^Q[d, c]$$

    One output component is a sum of 8 products. Below, component $c = 0$ is expanded in full; the remaining 7 are tabulated.

    Recall the head split: columns 0โ€“3 of $Q$ become head 0, columns 4โ€“7 become head 1.

    Q[1, 0] = x . Wq[:, 0]
          (+0.1568)*(-0.3560) + (+1.3309)*(-0.2957) + (-1.8703)*(+0.3342) + (+1.8876)*(-0.3792) + 
          (-1.2012)*(+0.0962) + (+1.7307)*(+0.4493) + (+0.0153)*(-0.4081) + (+0.0461)*(-0.1221)
          = -1.140044   |  from array: -1.140044   |  MATCH: True
    
    All 8 components of Q[1] (full 8-dim, before head split):
      c        Q[1,c]   head
    ------------------------------
      0     -1.140044   head 0
      1     -1.111687   head 0
      2      0.583413   head 0
      3     -0.526634   head 0
      4     -0.219950   head 1
      5      1.319576   head 1
      6      0.033149   head 1
      7     -0.870463   head 1
    
    head 0 slice = columns 0..3 = [-1.14  -1.112  0.583 -0.527]
    matches Qe[0, 1]      = [-1.14  -1.112  0.583 -0.527]   MATCH: True
    head 1 slice = columns 4..7 = [-0.22   1.32   0.033 -0.87 ]
    matches Qe[1, 1]      = [-0.22   1.32   0.033 -0.87 ]   MATCH: True

    A.4 Attention Scores

    $$s_j = \frac{q_1 \cdot k_j}{\sqrt{d_k}} = \frac{q_1 \cdot k_j}{2}, \qquad j = 0 \dots 8$$

    Nine dot products of 4-dimensional vectors (head 0).

    q_1 (head 0) = [-1.14  -1.112  0.583 -0.527]
    
      j  token             q.k_j    /2 = score         array  match
    --------------------------------------------------------------------
      0  the           -0.563007     -0.281504     -0.281504  True
      1  cat           -0.913154     -0.456577     -0.456577  True
      2  sat            0.045592      0.022796      0.022796  True
      3  on             1.131739      0.565869      0.565869  True
      4  the            0.584410      0.292205      0.292205  True
      5  mat           -0.388204     -0.194102     -0.194102  True
      6  and            1.027013      0.513506      0.513506  True
      7  was           -1.268738     -0.634369     -0.634369  True
      8  looking        2.864096      1.432048      1.432048  True
    
    Expansion of j = 8 ('looking'):
          (-1.1400)*(+0.1606) + (-1.1117)*(-1.9641) + (+0.5834)*(+2.4849) + (-0.5266)*(+1.1127)
          = +2.864096, divided by sqrt(4) = 2 -> +1.432048

    A.5 Softmax

    $$A_{1j} = \frac{e^{s_j - m}}{\sum_{j'} e^{s_{j'} - m}}, \qquad m = \max_j s_j$$

    The shift by $m$ is the numerical-stability trick: it cancels in the ratio, since $e^{s_j - m} / \sum e^{s_{j'} - m} = e^{s_j} / \sum e^{s_{j'}}$.

    row max m = 1.432048   (at position 8, 'looking')
    
      j  token             s_j     s_j - m         exp     /Z = A_1j       array
    ----------------------------------------------------------------------------
      0  the         -0.281504   -1.713551    0.180225      0.059310    0.059310
      1  cat         -0.456577   -1.888625    0.151280      0.049785    0.049785
      2  sat          0.022796   -1.409252    0.244326      0.080405    0.080405
      3  on           0.565869   -0.866179    0.420556      0.138401    0.138401
      4  the          0.292205   -1.139843    0.319869      0.105266    0.105266
      5  mat         -0.194102   -1.626150    0.196685      0.064727    0.064727
      6  and          0.513506   -0.918541    0.399101      0.131340    0.131340
      7  was         -0.634369   -2.066417    0.126639      0.041676    0.041676
      8  looking      1.432048    0.000000    1.000000      0.329090    0.329090
    ----------------------------------------------------------------------------
    denominator Z = 0.1802 + 0.1513 + 0.2443 + 0.4206 + 0.3199 + 0.1967 + 0.3991 + 0.1266 + 1.0000 = 3.038680
    
    sum of weights = 1.0000000000    matches array row: True

    A.6 Attention Output

    $$\text{head}_0[1] = \sum_{j=0}^{8} A_{1j} \, v_j$$

    This is the operation attention actually performs: a convex combination of the 9 value vectors, with the softmax weights as coefficients. Component 0 is expanded term by term.

    Component 0:  sum_j A_1j * V[j, 0]
          (+0.0593)*(-1.3749) + (+0.0498)*(-2.8072) + (+0.0804)*(+0.7433) + (+0.1384)*(+1.6414) + 
          (+0.1053)*(-1.1323) + (+0.0647)*(-3.0817) + (+0.1313)*(-2.6194) + (+0.0417)*(-2.3888) + 
          (+0.3291)*(-1.0528)
          = -1.043069   |  from array: -1.043069   |  MATCH: True
    
    All 4 components of head 0 output for token 1:
       dim 0: -1.043069
       dim 1: -0.544660
       dim 2: -0.127629
       dim 3: -0.076856
    
    head 0 = [-1.043 -0.545 -0.128 -0.077]
    head 1 = [1.377 0.827 0.138 0.455]
    concat = [-1.043 -0.545 -0.128 -0.077  1.377  0.827  0.138  0.455]
    matches concat_e[1]: True
    
    Sanity: weights are non-negative and sum to 1, so every component of the output
    lies between min and max of the corresponding V column:
       V[:,0] range = [-3.0817, +1.6414], output[0] = -1.0431

    A.7 Output Projection

    $$\text{attn\_out}[1, c] = \sum_{d=0}^{7} \text{concat}[1, d] \cdot W^O[d, c]$$

    This is the step the old notebook omitted entirely. Without it the two heads are simply glued together and never mixed.

    attn_out[1, 0] = concat . Wo[:, 0]
          (-1.0431)*(-0.5044) + (-0.5447)*(-0.0727) + (-0.1276)*(-0.2093) + (-0.0769)*(-0.3685) + 
          (+1.3767)*(-0.5410) + (+0.8272)*(+0.2712) + (+0.1382)*(-0.4579) + (+0.4548)*(+0.5742)
          = +0.298177   |  from array: +0.298177   |  MATCH: True
    
    full row: [ 0.298 -0.049 -0.437 -1.226  0.878  0.768 -0.166 -0.353]
    array   : [ 0.298 -0.049 -0.437 -1.226  0.878  0.768 -0.166 -0.353]
    MATCH   : True

    A.8 Residual Addition

    $$r = x + \text{attn\_out}$$

    dim             x    + attn_out           = r
    ----------------------------------------------
       0      0.156841      0.298177      0.455018
       1      1.330928     -0.049324      1.281604
       2     -1.870270     -0.436947     -2.307218
       3      1.887602     -1.225554      0.662048
       4     -1.201172      0.877659     -0.323513
       5      1.730714      0.767867      2.498581
       6      0.015273     -0.165709     -0.150436
       7      0.046060     -0.352609     -0.306549
    
    matches enc_res1[1]: True

    A.9 Layer Normalization

    $$\mu = \frac{1}{8}\sum_{d} r_d, \qquad \sigma^2 = \frac{1}{8}\sum_{d} (r_d - \mu)^2, \qquad \mathrm{LN}(r)_d = \gamma_d \frac{r_d - \mu}{\sqrt{\sigma^2 + 10^{-5}}} + \beta_d$$

    With $\gamma = \mathbf{1}$, $\beta = \mathbf{0}$ this is plain standardization of the 8 numbers of this one token.

    mu  = (+0.4550 + +1.2816 + -2.3072 + +0.6620 + -0.3235 + +2.4986 + -0.1504 + -0.3065) / 8
        = +1.809535 / 8 = +0.226192
    
    var = ((+0.2288)^2 + (+1.0554)^2 + (-2.5334)^2 + (+0.4359)^2 + (-0.5497)^2 + (+2.2724)^2 + (-0.3766)^2 + (-0.5327)^2) / 8
        = +13.665984 / 8 = 1.708248
    denominator sqrt(var + 1e-5) = 1.307003
    
     dim           r_d      r_d - mu         / den         array
    ------------------------------------------------------------
       0      0.455018      0.228826      0.175077      0.175077
       1      1.281604      1.055412      0.807505      0.807505
       2     -2.307218     -2.533410     -1.938334     -1.938334
       3      0.662048      0.435856      0.333477      0.333477
       4     -0.323513     -0.549705     -0.420584     -0.420584
       5      2.498581      2.272390      1.738626      1.738626
       6     -0.150436     -0.376628     -0.288162     -0.288162
       7     -0.306549     -0.532741     -0.407605     -0.407605
    
    MATCH: True
    check: mean of result = +2.78e-17, std = 0.999997

    A.10 Feed-Forward Network

    $$z_u = \sum_{d=0}^{7} \text{LN}[1,d]\, W_1[d,u] + b_{1,u}, \qquad a_u = \max(0, z_u), \qquad \text{ffn}_c = \sum_{u=0}^{31} a_u W_2[u,c] + b_{2,c}$$

    The first layer produces 32 hidden units from 8 inputs; two of them are expanded below (one that survives ReLU, one that is killed). The second layer contracts 32 back to 8 โ€” that sum has 32 terms.

    hidden unit u = 8  (survives)
          (+0.1751)*(+0.3476) + (+0.8075)*(+0.3610) + (-1.9383)*(-0.3681) + (+0.3335)*(+0.1975) + 
          (-0.4206)*(-0.0357) + (+1.7386)*(-0.0116) + (-0.2882)*(-0.2019) + (-0.4076)*(-0.0986)
          + b1[8] = +0.0000
          z_8 = +1.224936   ->   ReLU -> a_8 = +1.224936
          array: pre-ReLU +1.224936, post +1.224936  MATCH: True
    
    hidden unit u = 24  (killed by ReLU)
          (+0.1751)*(+0.0218) + (+0.8075)*(-0.3468) + (-1.9383)*(+0.3620) + (+0.3335)*(-0.3270) + 
          (-0.4206)*(-0.2873) + (+1.7386)*(-0.3211) + (-0.2882)*(-0.1196) + (-0.4076)*(-0.0938)
          + b1[24] = +0.0000
          z_24 = -1.451721   ->   ReLU -> a_24 = +0.000000
          array: pre-ReLU -1.451721, post +0.000000  MATCH: True
    
    ReLU killed 15 of 32 units for this token.
    
    Second layer, output component 0 = sum over 32 hidden units:
          (only the 17 non-zero units contribute; the rest add 0)
          (+0.7532)*(-0.2086) + (+0.2872)*(-0.2626) + (+0.8079)*(-0.1647) + (+0.0870)*(+0.3867) + 
          (+1.2249)*(-0.0544) + (+0.6054)*(+0.2524) + (+0.0026)*(-0.0177) + (+0.7401)*(+0.3343) + 
          (+0.4529)*(-0.2638) + (+0.4463)*(+0.0024) + (+1.1784)*(-0.2271) + (+0.4412)*(-0.0650) + 
          (+0.6174)*(+0.0893) + (+1.2025)*(-0.0751) + (+0.1314)*(+0.1340) + (+0.4955)*(+0.1958) + 
          (+0.3005)*(+0.1570)
          + b2[0] = +0.0000
          = -0.286386   |  from array: -0.286386   |  MATCH: True

    A.11 Final Residual + Norm

    $$H[1] = \mathrm{LayerNorm}\big(\text{LN}[1] + \text{ffn}[1]\big)$$

    End of Trace A: this row is what cross-attention will later read as a key and a value.

    dim          LN1        + ffn         = r2       -> LN2        array
    --------------------------------------------------------------------------
       0     0.175077    -0.286386    -0.111309     0.268271     0.268271
       1     0.807505    -0.415618     0.391887     0.905322     0.905322
       2    -1.938334     0.453973    -1.484362    -1.470026    -1.470026
       3     0.333477    -1.062228    -0.728750    -0.513415    -0.513415
       4    -0.420584     0.248086    -0.172498     0.190806     0.190806
       5     1.738626    -1.019688     0.718937     1.319370     1.319370
       6    -0.288162     0.599531     0.311369     0.803385     0.803385
       7    -0.407605    -1.103366    -1.510971    -1.503713    -1.503713
    
    mu = -0.323212, var = 0.623907
    MATCH encoder_output[1]: True
    
    === TRACE A COMPLETE: 'cat' -> [ 0.268  0.905 -1.47  -0.513  0.191  1.319  0.803 -1.504] ===

    Trace B: Decoder Position 6 ('by')

    This is the position whose output becomes the actual prediction. It carries the whole autoregressive condition: <sos> at a tiny mouse running by on the decoder side, and the 9-token prefix through cross-attention.

    B.1 Masked Self-Attention

    Position 6 is the last row, so all 7 positions are visible โ€” the mask blocks nothing here. (Compare with position 0, shown afterwards, where 6 of the 7 are blocked.)

    Decoder position 6 = 'by'
    q (head 0) = [ 0.726 -0.334  1.135  0.84 ]
    
      j  token        masked?        score         exp      weight       array
    ----------------------------------------------------------------------------
      0  <sos>             no     0.258070    0.610869    0.200936    0.200936
      1  at                no     0.750942    1.000000    0.328935    0.328935
      2  a                 no    -0.415989    0.311321    0.102404    0.102404
      3  tiny              no    -0.867940    0.198120    0.065169    0.065169
      4  mouse             no    -0.224371    0.377074    0.124033    0.124033
      5  running           no    -1.020254    0.170129    0.055961    0.055961
      6  by                no    -0.236296    0.372604    0.122562    0.122562
    ----------------------------------------------------------------------------
    Z = 3.040119,  weights sum = 1.0000000000,  MATCH: True
    
    Contrast, position 0 ('<sos>'): [1. 0. 0. 0. 0. 0. 0.]
       -> 6 of 7 scores were set to -1e9, exp(-1e9 - max) underflows to exactly 0,
       -> so the single visible position takes the whole mass: weight = 1.0

    B.2 Output Projection $W^O$

    $$\text{attn\_out}[6, c] = \sum_{d=0}^{7} \text{concat}[6, d] \cdot W^O[d, c]$$

    The two heads are concatenated into 8 numbers, then mixed by $W^O$. Without this projection the heads would remain two independent 4-dimensional streams.

    head 0 output at position 6 = [ 1.023  0.352  1.044 -1.221]
    head 1 output at position 6 = [-0.754  0.231  0.862 -0.728]
    concat (8 numbers)           = [ 1.023  0.352  1.044 -1.221 -0.754  0.231  0.862 -0.728]
    matches concat_d[6]: True
    
    dec_attn_out[6, 0] = concat . W_O[:, 0]
          (+1.0226)*(-0.2273) + (+0.3517)*(+0.4406) + (+1.0439)*(-0.4938) + (-1.2206)*(+0.3430) + 
          (-0.7539)*(+0.0734) + (+0.2307)*(+0.5296) + (+0.8617)*(-0.3028) + (-0.7277)*(-0.4318)
          = -0.891437   |  from array: -0.891437   |  MATCH: True
    
    full row: [-0.891 -0.412  0.997 -0.193 -1.074 -0.265 -0.311 -0.824]
    array   : [-0.891 -0.412  0.997 -0.193 -1.074 -0.265 -0.311 -0.824]
    MATCH   : True

    B.3 Residual + LayerNorm (Add & Norm 1)

    $$r = X_{dec}[6] + \text{attn\_out}[6], \qquad Y_1[6] = \mathrm{LayerNorm}(r)$$

    Same arithmetic as A.8 and A.9, now on the decoder side.

    dim         X_dec    + attn_out           = r
    ----------------------------------------------
       0     -0.122722     -0.891437     -1.014158
       1      1.983701     -0.411742      1.571959
       2      0.739929      0.997242      1.737171
       3     -0.511390     -0.192861     -0.704251
       4     -0.351859     -1.073738     -1.425598
       5      1.130129     -0.265326      0.864802
       6     -0.448548     -0.311432     -0.759979
       7      0.781329     -0.824303     -0.042975
    
    mu  = (-1.0142 + +1.5720 + +1.7372 + -0.7043 + -1.4256 + +0.8648 + -0.7600 + -0.0430) / 8
        = +0.226970 / 8 = +0.028371
    var = 1.295811,   sqrt(var + 1e-5) = 1.138342
    
     dim      r_d - mu         / den         array
    ------------------------------------------------
       0     -1.042530     -0.915832     -0.915832
       1      1.543587      1.355997      1.355997
       2      1.708799      1.501131      1.501131
       3     -0.732622     -0.643588     -0.643588
       4     -1.453969     -1.277270     -1.277270
       5      0.836431      0.734780      0.734780
       6     -0.788351     -0.692543     -0.692543
       7     -0.071346     -0.062675     -0.062675
    
    MATCH dec_norm1[6]: True

    B.4 Cross-Attention

    $$s_j = \frac{q^{cross}_6 \cdot k^{cross}_j}{2}, \qquad j = 0 \dots 8 \;\; (\textbf{prefix positions})$$

    Note what changes here: $q$ comes from the decoder, but $j$ now indexes the 9 prefix tokens, because $K$ and $V$ were built from the encoder output $H$. This is the only arithmetic in the whole network that mixes the two stacks.

    q_cross (head 0, decoder position 6) = [ 0.329 -1.214  0.509  0.816]
    K_cross rows come from H = encoder_output (9 prefix tokens)
    
      j  prefix token        score         exp      weight       array
    ------------------------------------------------------------------
      0  the           -0.208753    0.127016    0.034525    0.034525
      1  cat           -0.370644    0.108031    0.029364    0.029364
      2  sat            1.845304    0.990657    0.269274    0.269274
      3  on             1.790264    0.937605    0.254854    0.254854
      4  the           -0.136596    0.136520    0.037108    0.037108
      5  mat           -1.028494    0.055956    0.015210    0.015210
      6  and            0.588864    0.282006    0.076653    0.076653
      7  was           -1.334534    0.041204    0.011200    0.011200
      8  looking        1.854691    1.000000    0.271813    0.271813
    ------------------------------------------------------------------
    MATCH: True,  sum = 1.0000000000
    
    Expansion of the largest score, j = 8 ('looking'):
          (+0.3289)*(+0.1877) + (-1.2142)*(-1.6720) + (+0.5089)*(+1.7143) + (+0.8162)*(+0.9130)
          = +3.709381 / 2 = +1.854691
    
    cross-attention head-0 output = sum_j weight_j * V_j = [-0.125 -0.843 -0.539  0.378]
          component 0: (+0.0345)*(+0.9724) + (+0.0294)*(+0.0852) + (+0.2693)*(-0.5387) + (+0.2549)*(+0.3924) + 
          (+0.0371)*(+2.1627) + (+0.0152)*(+0.8942) + (+0.0767)*(+1.6430) + (+0.0112)*(+0.1859) + 
          (+0.2718)*(-1.2427)
          = -0.124896   |  from array: -0.124896   |  MATCH: True

    B.5 Cross-Attention $W^O$ + Add & Norm 2

    $$Y_2[6] = \mathrm{LayerNorm}\big(Y_1[6] + \mathrm{CrossAttention}(Y_1, H)[6]\big)$$

    The cross-attention sub-layer also has its own output projection; both heads are mixed before the residual is added.

    cross-attention concat at position 6 = [-0.125 -0.843 -0.539  0.378 -0.512  0.096  0.056  0.095]
    
    cross_out[6, 0] = concat . W_O[:, 0]
          (-0.1249)*(-0.0181) + (-0.8432)*(-0.1343) + (-0.5387)*(-0.2618) + (+0.3780)*(-0.2699) + 
          (-0.5121)*(+0.0703) + (+0.0958)*(+0.4548) + (+0.0564)*(+0.5068) + (+0.0951)*(+0.3894)
          = +0.227632   |  from array: +0.227632   |  MATCH: True
    
    full row: [ 0.228  0.083  0.281  0.403  0.577 -0.01   0.165 -0.073]
    array   : [ 0.228  0.083  0.281  0.403  0.577 -0.01   0.165 -0.073]
    MATCH   : True
    
    
    Add & Norm 2:
    
     dim           Y1      + cross          = r        -> LN        array
    --------------------------------------------------------------------------
       0    -0.915832     0.227632    -0.688200    -0.974226    -0.974226
       1     1.355997     0.082857     1.438854     1.341625     1.341625
       2     1.501131     0.280904     1.782034     1.715267     1.715267
       3    -0.643588     0.402607    -0.240981    -0.487312    -0.487312
       4    -1.277270     0.577426    -0.699844    -0.986904    -0.986904
       5     0.734780    -0.009865     0.724915     0.564317     0.564317
       6    -0.692543     0.164553    -0.527990    -0.799796    -0.799796
       7    -0.062675    -0.073286    -0.135962    -0.372971    -0.372971
    
    mu = +0.206603, var = 0.843587
    MATCH dec_norm2[6]: True

    B.6 Feed-Forward Network

    $$z_u = \sum_{d=0}^{7} Y_2[6,d]\, W_1[d,u] + b_{1,u}, \qquad a_u = \max(0, z_u), \qquad \text{ffn}_c = \sum_{u=0}^{31} a_u W_2[u,c] + b_{2,c}$$

    Identical in form to A.10, with the decoder's own weights.

    hidden unit u = 8  (survives)
          (-0.9742)*(-0.0613) + (+1.3416)*(+0.2223) + (+1.7153)*(+0.3452) + (-0.4873)*(-0.3863) + 
          (-0.9869)*(-0.1973) + (+0.5643)*(+0.0113) + (-0.7998)*(-0.3303) + (-0.3730)*(-0.3614)
          + b1[8] = +0.0000
          z_8 = +1.738386   ->   ReLU -> a_8 = +1.738386
          array: pre-ReLU +1.738386, post +1.738386  MATCH: True
    
    hidden unit u = 27  (killed by ReLU)
          (-0.9742)*(+0.3458) + (+1.3416)*(-0.1154) + (+1.7153)*(-0.3581) + (-0.4873)*(-0.2504) + 
          (-0.9869)*(+0.3746) + (+0.5643)*(-0.1724) + (-0.7998)*(+0.1468) + (-0.3730)*(+0.0328)
          + b1[27] = +0.0000
          z_27 = -1.580555   ->   ReLU -> a_27 = +0.000000
          array: pre-ReLU -1.580555, post +0.000000  MATCH: True
    
    ReLU killed 19 of 32 units at this position.
    
    Second layer, output component 0 = sum over 32 hidden units:
          (only the 13 non-zero units contribute; the rest add 0)
          (+0.2118)*(-0.3195) + (+1.0305)*(+0.1828) + (+0.4628)*(-0.1684) + (+1.7384)*(-0.2064) + 
          (+0.0673)*(-0.2320) + (+0.0340)*(-0.0034) + (+0.2111)*(-0.3802) + (+0.5934)*(+0.2627) + 
          (+0.8853)*(+0.1140) + (+0.3648)*(-0.2992) + (+0.9836)*(+0.2737) + (+0.6189)*(+0.3108) + 
          (+0.5985)*(-0.1744)
          + b2[0] = +0.0000
          = +0.092907   |  from array: +0.092907   |  MATCH: True

    B.7 Final Residual + Norm $\rightarrow$ decoder_output[6]

    $$\text{decoder\_output}[6] = \mathrm{LayerNorm}\big(Y_2[6] + \text{ffn}[6]\big)$$

    This 8-vector is the one the output projection turns into 500 logits in B.8.

    dim           Y2        + ffn         = r3        -> LN        array
    --------------------------------------------------------------------------
       0    -0.974226     0.092907    -0.881320    -0.706660    -0.706660
       1     1.341625    -1.137906     0.203719     0.527177     0.527177
       2     1.715267    -0.721423     0.993844     1.425657     1.425657
       3    -0.487312    -0.338064    -0.825376    -0.643044    -0.643044
       4    -0.986904     0.831840    -0.155063     0.119193     0.119193
       5     0.564317     0.459811     1.024128     1.460093     1.460093
       6    -0.799796    -0.730027    -1.529823    -1.444096    -1.444096
       7    -0.372971    -0.536190    -0.909161    -0.738319    -0.738319
    
    mu = -0.259882, var = 0.773338
    MATCH decoder_output[6]: True
    
    >>> the full decoder block is now traced by hand for position 6.

    B.8 From Output to Logit

    $$\text{logit}_v = \langle \text{decoder\_output}_6,\; E_v \rangle$$

    Because $W_{out} = E^\top$ (weight tying), a logit is literally the inner product between the decoder's output vector and the embedding of candidate token $v$. Below: the true token <eos>, the argmax token, and the geometry that separates them.

    decoder_output[6] = [-0.707  0.527  1.426 -0.643  0.119  1.46  -1.444 -0.738]   (norm = 2.8284)
    
    --- v = 2 '<eos>'  [TRUE next token] ---
          (-0.7067)*(+0.0102) + (+0.5272)*(+0.4520) + (+1.4257)*(+0.0676) + (-0.6430)*(+0.0164) + 
          (+0.1192)*(-0.4808) + (+1.4601)*(+0.2638) + (-1.4441)*(+0.2282) + (-0.7383)*(+0.7648)
          = -0.249453   |  from array: -0.249453   |  MATCH: True
          cosine with decoder output = -0.0823, |E_v| = 1.0710
    
    --- v = 5 '<unused_5>'  [ARGMAX (model's pick)] ---
          (-0.7067)*(-0.1976) + (+0.5272)*(+0.1334) + (+1.4257)*(+0.5535) + (-0.6430)*(-0.0232) + 
          (+0.1192)*(-0.1963) + (+1.4601)*(+0.6651) + (-1.4441)*(-0.5120) + (-0.7383)*(-0.7774)
          = +3.274961   |  from array: +3.274961   |  MATCH: True
          cosine with decoder output = +0.8852, |E_v| = 1.3081
    
    The logit is large when the embedding is both well-aligned in direction
    and long in magnitude โ€” an inner product, not a pure angle.

    B.9 Softmax Over 500 Tokens

    $$P(v) = \frac{e^{\ell_v - m}}{\sum_{v'=1}^{500} e^{\ell_{v'} - m}}$$

    The denominator is a sum over the entire vocabulary, which is why a large vocabulary makes the output layer the expensive part of a language model. Below, the sum is verified and the top of the ranking is expanded.

    max logit m = 3.274961 (token 5)
    denominator Z = sum of 500 exponentials = 29.742343
    
    rank     v  token                logit         l-m         exp      /Z = P       array
    ------------------------------------------------------------------------------------
       1     5  <unused_5>          3.2750      0.0000    1.000000    0.033622    0.033622
       2   342  <unused_342>        2.9872     -0.2878    0.749925    0.025214    0.025214
       3   190  <unused_190>        2.7904     -0.4845    0.615986    0.020711    0.020711
       4   487  <unused_487>        2.5049     -0.7701    0.462966    0.015566    0.015566
       5   208  <unused_208>        2.4846     -0.7904    0.453665    0.015253    0.015253
       6   424  <unused_424>        2.3796     -0.8954    0.408449    0.013733    0.013733
    ...
     289     2  <eos>              -0.2495     -3.5244    0.029469    0.000991    0.000991   <- TRUE
    ------------------------------------------------------------------------------------
    sum of all 500 probabilities = 1.0000000000
    MATCH with probs[6]: True
    
    True token '<eos>' ranks 289 out of 500 โ€” chance-level, as expected from random weights.

    B.10 Loss at This Position

    $$\mathcal{L}_6 = -\log P(y_6 = \texttt{<eos>}) = -\big(\ell_{\texttt{<eos>}} - m - \log Z\big)$$

    The second form is the log-sum-exp identity โ€” it is how the loss is computed in practice, without ever forming the 500 probabilities explicitly.

    direct   : -log(P) = -log(0.00099081) = 6.916985
    log-sum-exp: -(l_v - m - log Z) = -(-0.249453 - 3.274961 - 3.392572) = 6.916985
    identical: True
    
    reference values:
       log(V) = log(500)          = 6.214608   (uniform guess)
       this position              = 6.916985
       perfect prediction (P = 1) = 0.000000
    
    === TRACE B COMPLETE ===

    Every Operation, Counted

    For the record, the full forward pass of this block on these two sequences:

    Operation Where Multiply-adds
    Encoder $Q,K,V$ projections $9 \times 8 \times 8 \times 3$ 1 728
    Encoder attention scores $2 \times 9 \times 9 \times 4$ 648
    Encoder attention $\times V$ $2 \times 9 \times 9 \times 4$ 648
    Encoder $W^O$ $9 \times 8 \times 8$ 576
    Encoder FFN $9 \times (8{\cdot}32 + 32{\cdot}8)$ 4 608
    Decoder self $Q,K,V$ + $W^O$ $7 \times 8 \times 8 \times 4$ 1 792
    Decoder self attention $2 \times 7 \times 7 \times 4 \times 2$ 784
    Cross $Q$ (decoder) $7 \times 8 \times 8$ 448
    Cross $K, V$ (encoder, 9 rows) $9 \times 8 \times 8 \times 2$ 1 152
    Cross scores + $\times V$ $2 \times 7 \times 9 \times 4 \times 2$ 1 008
    Cross $W^O$ $7 \times 8 \times 8$ 448
    Decoder FFN $7 \times (8{\cdot}32 + 32{\cdot}8)$ 3 584
    Output projection $7 \times 8 \times 500$ 28 000

    Two observations worth carrying forward:

    1. The output projection dominates โ€” 28 000 of roughly 45 000 multiply-adds, purely because $V \gg d_{model}$. In real models with $V \approx 50\,000$ this layer and its softmax are a major cost, which is why techniques like sampled softmax and tied embeddings exist.
    2. Attention is not the expensive part here. At $n = 9$ the quadratic term is negligible; the FFN and the output layer dominate. The $O(n^2)$ cost of attention only takes over at long sequence lengths โ€” which is exactly where the various efficient-attention methods aim.

    References

    1. Attention Is All You Need โ€” Vaswani et al., 2017 (NeurIPS)