Date: 21/07/2026
The Transformer Architecture: Encoder–Decoder Mechanisms
Preface
The Transformer, introduced by Vaswani et al. (2017), replaced recurrence and convolution with a purely attention-based mechanism for sequence transduction. Its significance is not merely empirical (parallelizability, superior long-range dependency modeling) but architectural: it recasts sequence modeling as a problem of learned, content-addressable routing between positions, expressed entirely as compositions of matrix multiplications, softmax normalizations, and pointwise nonlinearities.
This treatise is written for a reader who already possesses fluency in linear algebra, multivariable calculus, and the standard vocabulary of deep learning (gradients, backpropagation, normalization layers, embeddings). The goal is not to motivate why attention is useful — that is assumed — but to establish the architecture with textbook-grade rigor: precise definitions, explicit tensor-dimension bookkeeping at every transformation, and a direct correspondence between the mathematics and a working PyTorch implementation.
Four parts follow. Part 1 establishes the atomic operation — Scaled Dot-Product Attention — and its generalization to Multi-Head Attention. Part 2 assembles the Encoder from residual sub-layers. Part 3 assembles the Decoder, introducing causal masking and cross-attention as the mechanism by which decoder states query encoder memory. Part 4 places a complete, minimal PyTorch implementation into correspondence with the preceding mathematics, block by block.
This revision additionally closes three gaps that a first pass through the architecture typically leaves open. First, the informal observation that large "hurts gradients" is replaced with a closed-form derivative of the softmax saturation effect, including a worked numerical example at the paper's own hyperparameters. Second, every code listing in Part 4 is followed by a line-by-line correspondence to the equation it implements, with particular attention to the view / transpose / contiguous sequence that is the most common source of silent bugs in hand-written multi-head attention. Third, boxed Design Intuition passages — grounded in the original paper and in the standard pedagogical literature on this architecture — explain why the mechanism is shaped the way it is, not merely that it is shaped that way.
Table of Contents
- Conventions and Notation
- Part 1 — The Core Mechanism: Scaled Dot-Product & Multi-Head Attention
- Part 2 — The Encoder Architecture
- Part 3 — The Decoder Architecture & The Interaction Layer
- Part 4 — Minimal, Functional PyTorch Implementation
- Appendix A — Equation-to-Implementation Cross-Reference
Conventions and Notation
| Symbol | Meaning |
|---|---|
| batch size | |
| source sequence length (encoder) | |
| target sequence length (decoder), or generic sequence length when unambiguous | |
| model dimension, | |
| number of attention heads | |
| per-head key/query dimension, | |
| per-head value dimension (in this treatise ) | |
| inner dimension of the position-wise feed-forward network | |
| a single-sequence matrix of -dimensional row vectors, one per position | |
| query, key, value matrices | |
| learned linear projections producing | |
| learned output projection recombining attention heads | |
| elementwise (Hadamard) product | |
| tensor shape annotation, e.g. |
Throughout, tensors carry an explicit leading batch dimension in the implementation, but the mathematical definitions in Parts 1–3 are stated for a single sequence (batch size elided) unless batching materially changes the operation (as with masking). Where code and mathematics are cross-referenced, both conventions are shown side by side so the correspondence is unambiguous.
Remark (row-vector / right-multiplication convention). Every equation in Parts 1–3 treats a single token's feature vector as a row vector, and a linear map with weight acts on the right: for , . This is the convention Vaswani et al. (2017) use, and it is why stacks row vectors and applies the same projection to every row simultaneously. PyTorch's nn.Linear(d_in, d_out), by contrast, internally stores its weight as a tensor and computes — i.e. linear.weight is the transpose of the mathematical used throughout this treatise. This changes nothing about the computed function; it is flagged once here so that no confusion arises later when cross-referencing shapes in Part 4 against .weight.shape.
Part 1 — The Core Mechanism: Scaled Dot-Product & Multi-Head Attention
1.1 Scaled Dot-Product Attention
Definition (Scaled Dot-Product Attention).
Let , , be query, key, and value matrices, where is the number of query positions and is the number of key/value positions. Attention is defined as
where is applied row-wise over the last axis (the key axis, of size ), following Vaswani et al. (2017), Section 3.2.1.
Structural Walkthrough.
Decompose the computation into three stages:
- Compatibility scoring. . Entry measures the (unnormalized) affinity between query position and key position as an inner product in .
- Scaling and normalization. , a row-stochastic matrix: each row sums to 1 and defines a categorical distribution over key positions, conditioned on query .
- Aggregation. , a convex combination of value vectors weighted by the attention distribution.
This score → normalize → aggregate structure is why , , are named as they are: the mechanism is a continuous, differentiable relaxation of a dictionary lookup, replacing exact key-matching with a similarity score and hard selection with a weighted blend. §3.2 develops this framing fully once cross-attention gives it a concrete external "table" to query.
An optional mask (or, in the batched case, broadcastable to ) may be applied before the softmax. Rather than the multiplicative shorthand — an abuse of notation, since is not defined — the masked logits are properly given as a piecewise sum:
which forces for masked pairs, since , while leaving unmasked entries untouched (adding ). Masking is central to Part 3, where Eq. (3.2) is exactly this definition specialized to equal to the lower-triangular causal matrix of Eq. (3.1).
Implementation Note. In the accompanying code, ScaledDotProductAttention.forward computes exactly via torch.matmul(Q, K_t), divides by math.sqrt(d_k) (where d_k = Q.size(-1)), applies the mask via masked_fill, and finally applies F.softmax(scores, dim=-1) — normalizing over the last axis, which is precisely the key axis , matching Equation (1.1). masked_fill overwrites masked entries with rather than adding to them, which is why the ill-defined reading of the mask never arises in practice: the implementation always realizes the piecewise form of Eq. (1.2), never the multiplicative shorthand. Separately, F.softmax internally subtracts the row-wise maximum from scores before exponentiating; since for any constant (the factor cancels between numerator and denominator of every ), this is a numerically-stabilizing shift that changes nothing about the mathematical result of Eq. (1.1)–(1.2).
1.2 Derivation of the Scaling Factor
Claim. If the components of are i.i.d. random variables with mean and variance , then , and the scaling factor restores unit variance to the dot product.
Derivation. Write . Since are independent of each other and mutually independent across , with and :
By linearity of variance under independence,
Hence , and grows in magnitude as increases, even though each term has fixed unit variance. Dividing by ,
restores the pre-softmax logits to unit variance regardless of .
Why this matters (gradient argument). Vaswani et al. (2017, §3.2.1, note 4) attribute this to softmax saturation: they conjecture that as grows, the increasing magnitude of unscaled dot products pushes the softmax's operating point toward its flat tails, where derivatives with respect to the logits become minuscule. In the original paper this is a footnote-length heuristic. The remainder of this section promotes it to a derivation: an exact closed form for the softmax Jacobian, an exact identity for how sharply that Jacobian collapses as a row saturates, and a proof — not an assertion — that this collapse is driven by the growth already established in Eq. (1.3).
Definition (softmax Jacobian). Let be a single row of pre-softmax logits (one query's compatibility scores against keys), and let , i.e. with . Then
Proof. By the quotient rule, . Since and ,
is symmetric ( and both are), and every column of sums to zero: , since . This is not incidental — perturbing one logit can only redistribute probability mass among the outputs, never change its total, so the induced changes across all outputs must cancel exactly.
Claim (saturation collapses the entire Jacobian, not just one entry). The diagonal entries are maximized at (value ) and vanish as or ; the off-diagonal entries () vanish whenever either or . Summing the diagonal gives an exact identity, not merely an asymptotic bound:
using . Since is minimized at the uniform distribution () and increases monotonically toward as approaches a one-hot vector, shrinks to exactly precisely as attention saturates onto a single key. This certifies that the entire Jacobian collapses, not one coordinate of it: once a single key dominates a row, every partial derivative of every attention weight with respect to every logit in that row is simultaneously near zero, and no gradient signal reaches or through that row.
Claim (the collapse is driven by ). It remains to connect saturation back to itself. Consider a query and two independent, identically-distributed competing keys (components i.i.d., mean , variance , as in the Claim above), and let , be the two dot products competing for a row's softmax mass. Conditioned on , each of is a linear combination of independent unit-variance terms, so ; since , and are independent given , so . Because for every , the law of total variance gives
Notice that sharing the query between the two competing keys does not change this result — it is exactly , the value one would obtain by naively treating as independent dot products of variance each, even though they are not unconditionally independent.
Writing for the unscaled logit gap, its typical magnitude is — growing without bound as grows. In the two-key case, , the derivative of the logistic sigmoid , which decays as for large . Substituting the typical unscaled gap gives a gradient magnitude on the order of — exponential decay in . After the correction, the same gap becomes
a constant, independent of . This is the precise mechanism by which the scaling factor in Eq. (1.1) prevents saturation: it does not simply shrink the logits, it holds the typical competitive gap between keys at a fixed scale regardless of head width, so the softmax's operating point never drifts toward its zero-gradient tails no matter how large is chosen.
Worked example (, the paper's own value). Substituting Vaswani et al.'s hyperparameters (, ):
| typical gap | ||
|---|---|---|
| unscaled | ||
| scaled by |
— roughly a 13,000-fold difference in gradient magnitude at this single representative pair of competing keys, and the gap widens further as grows: the unscaled column decays exponentially in , while the scaled column stays fixed at forever. This is the fully quantitative form of the paper's qualitative footnote.
1.3 Multi-Head Attention
Single-head attention computes one attention distribution per query, averaging value vectors along a single subspace. Multi-Head Attention (MHA) instead projects into distinct learned subspaces, applies Scaled Dot-Product Attention independently ("in parallel") within each, and recombines the results.
Definition (Multi-Head Attention). Given input matrices (query/key/value length may differ; here presented with matched for notational economy), and per-head projections , for :
where and, per Vaswani et al. (2017, §3.2.2), , so that and the concatenated heads occupy the same dimensionality as the original model space.
Mathematical Transformation — dimension tracking (single sequence, no batch):
Implementation Note — the batched, "combined-then-split" formulation. Rather than instantiating separate small projection matrices , the standard implementation (and the code below) uses single, full-width linear maps and reshapes the result into heads. This is mathematically equivalent to Equation (1.5)–(1.6); §4.3's walkthrough proves the equivalence explicitly via block-partitioning of , rather than merely asserting it as the original draft did.
With batch dimension restored, the tensor-dimension trace through MultiHeadAttention.forward is:
split heads — view(B, T, H, d_k) then transpose(1, 2):
scaled dot-product attention per head (Part 1.1, batched over and simultaneously):
merge heads — transpose(1, 2) then contiguous().view(B, T_q, H \cdot d_k):
output projection :
This final shape is identical to the input query shape, which is precisely what permits attention sub-layers to be embedded inside a residual block (Part 2.2).
Design Intuition — why multiple heads? A single attention head produces one convex-combination geometry per query: because softmax yields one peaked-or-diffuse distribution per row, one head can represent only one "mode" of relevance at a time. independent heads, each operating in its own learned -dimensional subspace, let the model attend to different representation subspaces at different positions simultaneously — for instance, one head specializing in short-range syntactic dependency (subject–verb agreement between nearby tokens) while another specializes in long-range coreference (resolving a pronoun to an antecedent many positions earlier). Vaswani et al. (2017, §3.2.2) substantiate this kind of specialization empirically via attention-map visualizations, and the same visual style — color-coded attention weights overlaid directly on a sentence — was later popularized in accessible expositions of the architecture (notably Alammar's illustrated walkthroughs) as the standard way to build intuition for what an individual head has learned. Mechanically, restricting each head to dimensions rather than giving every head the full -dimensional space is what forces this specialization: an ensemble of narrow, independent views costs the same total parameter budget as one wide view (), so the benefit of multiple heads is architectural diversity of representation, not additional capacity.
Part 2 — The Encoder Architecture
Definition (Encoder Stack). The encoder is a stack of identical layers, each layer composed of exactly two sub-layers: multi-head self-attention followed by a position-wise feed-forward network, each wrapped in a residual connection and layer normalization. Given a source sequence embedded and positionally encoded into , the encoder computes:
with the final encoder output (the "memory," consumed by cross-attention in the decoder) being .
2.1 Sub-layer 1: Multi-Head Self-Attention
In self-attention, the same tensor serves as query, key, and value:
Every encoder position attends to every other encoder position (subject only to a padding mask, never a causal mask — the encoder has full bidirectional visibility, in contrast to the decoder of Part 3). Each row of is thereby recontextualized as a learned mixture of all , , weighted by learned content-based compatibility.
Design Intuition — "self" describes the source, not the values. Passing the same tensor x three times does not mean numerically. Each of applies a different learned projection to the same input, so in general even though all three are derived from . "Self"-attention means every position attends back into the same sequence it came from — the "table" being queried is the layer's own input — as opposed to cross-attention (§3.2), where the table being queried is a different sequence entirely.
Implementation Note. In TransformerEncoderLayer.forward, this is self.self_attn(x, x, x, src_mask), where src_mask (shape , from make_src_mask) masks out padding positions only — it broadcasts over both the head axis and the query-position axis , since padding validity does not depend on the querying position.
2.2 Residual Connections and Layer Normalization
Definition (Layer Normalization). For a single position's feature vector (one row of ), let
denote its mean and variance, computed across the feature axis — independently for every position and every sequence in the batch, never mixing statistics across positions or across the batch axis (the defining contrast with Batch Normalization, which instead normalizes each feature channel across the batch). Then
where are learned per-channel scale and shift parameters (initialized to and respectively) and is a small constant preventing division by zero. Applied to , LayerNorm acts identically and independently on each of the rows — a property that matters for masking: a padding position's normalized output depends only on its own (meaningless) values, never leaking information to or from real tokens elsewhere in the sequence.
Definition (Residual Sub-layer). Following He et al.'s residual formulation as adapted by Vaswani et al. (2017, §3.1), each sub-layer (self-attention or FFN) is wrapped as:
This is the Post-LN convention used by the original paper and by the accompanying code: normalization is applied after the residual sum. The alternative, Pre-LN, normalizes the sub-layer's input before it is transformed:
Structural Walkthrough — Post-LN vs. Pre-LN.
| Property | Post-LN (2.2) | Pre-LN (2.3) |
|---|---|---|
| Normalization placement | after residual addition | before sub-layer input |
| Residual stream | normalized at every layer boundary; bounded but partially attenuated | unnormalized, additive accumulation across all layers |
| Gradient at initialization | can vanish/explode in deep stacks without warmup | more stable; identity-like path from output to any layer's input |
| Training stability | typically requires learning-rate warmup (as used in the original Transformer) | more robust to omitting warmup, common in later large-scale variants |
| Fidelity to Vaswani et al. (2017) | exact match | a widely-adopted post-publication modification |
Mathematical intuition. Under Post-LN, the identity path is not preserved verbatim into the next layer because is applied to as a whole, which can distort gradient magnitude as depth grows — layer normalization renormalizes the combined signal, so the residual's variance-stabilizing benefit is only partial. Under Pre-LN, the sum preserves exactly as an additive term untouched by normalization, giving a term that is exactly the identity matrix plus a Jacobian contribution from the sub-layer — a cleaner path for gradient backpropagation through many stacked layers.
Remark (theoretical grounding). The qualitative claims in the table above — that Post-LN's effective gradient scale is depth-dependent while Pre-LN's is comparatively stable — were later made rigorous by follow-up theoretical work analyzing expected gradient norms at initialization as a function of layer count (Xiong et al., 2020, "On Layer Normalization in the Transformer Architecture"). That analysis is not reproduced here, since it is not required to implement Eq. (2.2)–(2.3), but it is worth knowing that the intuition sketched above is the subject of a dedicated theoretical treatment, and that Pre-LN's improved stability is precisely why it became the more common choice in large-scale Transformer variants developed after this architecture.
Implementation Note. The provided code implements Post-LN exactly as in Equation (2.2):
pythonattn_out, _ = self.self_attn(x, x, x, src_mask) x = self.norm1(x + self.dropout1(attn_out))attn_out, _ = self.self_attn(x, x, x, src_mask) x = self.norm1(x + self.dropout1(attn_out))
Observe the order of operations: dropout → + x (residual) → norm1. This is Post-LN, matching Vaswani et al. (2017) precisely, and identically structured for the second sub-layer:
pythonffn_out = self.ffn(x) x = self.norm2(x + self.dropout2(ffn_out))ffn_out = self.ffn(x) x = self.norm2(x + self.dropout2(ffn_out))
2.3 Sub-layer 2: Position-wise Feed-Forward Networks
Definition (Position-wise FFN). Applied identically and independently to each position (hence "position-wise" — no mixing across the sequence axis occurs within this sub-layer):
with , , and (Vaswani et al. use against , a ratio of 4).
Mathematical Transformation — dimension tracking:
Structural Walkthrough. Equation (2.4) can equivalently be read as two pointwise (position-wise) convolutions with a ReLU nonlinearity between them — a per-token 2-layer MLP applied with shared weights across all positions. Where the self-attention sub-layer performs cross-position mixing (information exchange across positions), the FFN sub-layer performs purely local, per-position nonlinear feature transformation, expanding into a higher-dimensional space and projecting back to . The two sub-layers are thus complementary: attention routes information between positions, FFN transforms information at each position independently.
Design Intuition — why does attention need an FFN at all? Self-attention (Eq. 1.1) is, structurally, an affine operation in for any fixed attention matrix : is linear in , and although depends non-linearly on through the softmax, the value pathway through which content actually flows contains no nonlinearity of its own. Stacking self-attention sub-layers with nothing between them would compose a sequence of context-dependent linear remixings — powerful for routing information, but with strictly limited capacity to transform it. The FFN sub-layer is where the architecture's nonlinear feature computation actually happens: attention decides where information comes from; the FFN decides what to do with it once it has arrived.
Implementation Note. PositionwiseFeedForward.forward implements Equation (2.4) exactly: linear1 () expands , F.relu applies the nonlinearity, dropout regularizes the hidden activation, and linear2 () projects back to . Per the Remark following the Notation table, linear1.weight is stored as — the transpose of the mathematical used in Eq. (2.4) — and linear2.weight is stored as , the transpose of .
2.4 Positional Encoding
Because self-attention (Eq. 1.1) is permutation-equivariant — it contains no operation sensitive to the ordering of rows in — the model has no intrinsic notion of sequence order. Positional information must be injected explicitly.
Definition (Sinusoidal Positional Encoding). For position and dimension index , Vaswani et al. (2017, §3.5) define:
Token embeddings are combined with positional encodings additively (not concatenated):
where the embedding scaling (also specified in §3.4 of the paper) equalizes the relative magnitude of the (typically variance-normalized) embedding vectors against the fixed-magnitude positional encodings, which lie in by construction.
Mathematical Transformation — the geometric argument for relative-position linearity. A key property motivating the sinusoidal form is that, for any fixed offset , is expressible as a linear function of . This follows from the angle-addition identities:
where is the fixed angular frequency of dimension pair . Because and are constants once is fixed, the pair is obtained from by a fixed rotation matrix
independent of itself. Vaswani et al. (2017, §3.5) conjecture that this structure may let the model learn to attend by relative position directly, since a fixed offset corresponds to a fixed linear — in fact orthogonal — transformation of the encoding: the matrix in Eq. (2.8) has determinant and satisfies , so it is a genuine rotation (angle- and length-preserving), not merely some linear map. This orthogonality is what makes the conjecture plausible in the first place — a rotation applied consistently to both a query and a key leaves their inner product unchanged, so a learned linear transformation of or could in principle isolate the relative-offset component of that inner product from the absolute-position component.
Remark (a later architectural descendant). The rotation structure of Eq. (2.8) was, several years after this architecture, generalized into a purely multiplicative mechanism — Rotary Position Embedding (RoPE) — which applies an analogous rotation directly to and before the dot product, rather than adding a fixed vector to the token embedding beforehand. That mechanism is outside the scope of the architecture treated in this document, but it is worth flagging that the rotational-invariance property derived above was not a dead end: it directly motivated later position-encoding designs built around the same idea.
Implementation Note. PositionalEncoding.__init__ precomputes a buffer pe of shape via torch.arange for position indices and an exponentiated div_term implementing — an exact match to the exponent structure of Equations (2.5)–(2.6), computed in log-space for numerical stability. pe[:, 0::2] and pe[:, 1::2] fill even and odd dimension slots with and respectively. In forward, x + self.pe[:, :T, :] implements the additive combination of Equation (2.7) (with the scaling applied earlier, in Encoder.forward, via self.embed(src) * math.sqrt(self.d_model)), and the result is broadcast over the batch dimension since pe carries a singleton leading axis.
2.5 The Full Encoder Stack
Structural Block Diagram — one Encoder layer:

Structural Block Diagram — the full Encoder ( layers):

Implementation Note. This diagram corresponds exactly to Encoder.forward: embedding lookup and scaling, positional encoding addition (with dropout, folded into PositionalEncoding), an nn.ModuleList of N = n_layers stacked TransformerEncoderLayer instances applied sequentially, and a final self.norm(x) — an additional normalization applied to the output of the entire stack, standard in implementations though not always drawn as a distinct step in illustrative diagrams of the original paper. Note that nn.ModuleList does not share weights across layers: each of the TransformerEncoderLayer instances has its own independently initialized and trained parameters — "stack of identical layers" describes identical architecture, not identical weights.
Part 3 — The Decoder Architecture & The Interaction Layer
Definition (Decoder Stack). The decoder is a stack of identical layers, each composed of three sub-layers (one more than the encoder): masked multi-head self-attention, encoder–decoder cross-attention, and a position-wise feed-forward network — each again wrapped in a residual connection and Post-LN, exactly as in Equation (2.2).
3.1 Masked Multi-Head Self-Attention
The decoder must remain autoregressive: when generating output position , the model must not have access to ground-truth tokens at positions , or the training objective becomes trivial (the model could "cheat" by copying the future token it is meant to predict) and inference-time behavior (where future tokens are simply unavailable) would diverge from training-time behavior.
Definition (Causal / Look-Ahead Mask). Define as the lower-triangular matrix:
so that query position is permitted to attend only to key positions . Combined with Equation (1.2), the masked self-attention logits become:
so that after , exactly for all — future positions receive zero attention weight and thus contribute nothing to .
Design Intuition — why causal masking is mathematically mandatory, not merely helpful. The necessity of Eq. (3.1) is a matter of the correctness of the training objective, not empirical convenience. An autoregressive model is defined by the chain-rule factorization of the target sequence's joint distribution,
which is a valid factorization of any joint distribution regardless of architecture, but is a usable generative procedure only if each factor can be evaluated from strictly less information than itself — otherwise, at sampling time, when genuinely do not yet exist, the factor is simply not computable.
During training, however, the decoder processes the entire target sequence in a single parallel forward pass (teacher forcing), for efficiency. If self-attention at position were left unmasked, then by Eq. (1.1) the hidden state feeding the prediction of would be a function of for every , including — and each is, at the very first layer, a direct learned linear function of the embedding of the ground-truth token . The model would be asked to predict while 's own embedding sits, undisguised, among its inputs. The function "attend to position with weight ; let and approximately preserve the embedding; let the final projection invert the (near-injective) embedding map" is trivially reachable by gradient descent and drives training loss toward zero without the network ever discovering any genuine predictive structure in the data. This is not a degraded solution the model might stumble into — it is the global minimizer of an unmasked training objective, and it is exactly the function gradient descent has every incentive to find first, since it requires no compositional reasoning about context at all.
The mask closes this loophole structurally, not statistically: setting for every (Eq. 3.2) removes every computational path from to the hidden state used to predict , at every layer, simultaneously — a property of the computation graph itself, true regardless of what values the learned weights take. This is the precise sense in which causal masking is mandatory rather than merely helpful: without it, the network is not training to approximate at all, but to solve an unrelated and trivial copying task, and the resulting weights would be meaningless at inference time — not just less accurate, but structurally unusable, since the information they learned to depend on (future tokens) does not exist yet when generation actually happens.
Structural Walkthrough — combining causal and padding masks. In the batched setting, the decoder must respect both the causal constraint (Eq. 3.1) and any padding mask for variable-length sequences within a batch. The combined mask is the elementwise logical AND:
Implementation Note. Transformer.make_tgt_mask constructs exactly this composite: pad_mask has shape (identical in structure to the encoder's src_mask), causal_mask = torch.tril(torch.ones((T,T))) produces the lower-triangular matrix of Equation (3.1) with shape , reshaped to , and the two are combined via & with broadcasting to produce tgt_mask of shape — note this differs from the encoder's mask shape precisely because causal masking is query-position-dependent (each query position sees a different set of valid keys), whereas padding masking alone is not. §4.7's walkthrough derives this broadcast axis-by-axis.
3.2 Encoder–Decoder Cross-Attention
Cross-attention is the mechanism by which decoder representations are informed by the source sequence. It is structurally identical to Scaled Dot-Product / Multi-Head Attention (Part 1), but with queries drawn from the decoder and keys/values drawn from the encoder's output memory:
Mathematical Transformation — dimension tracking. Query length (decoder) and key/value length (encoder) may differ, since source and target sequences are generally of unequal length:
The masking applied here (memory_mask, structurally identical to src_mask, shape ) masks only source-side padding — there is no causal constraint on cross-attention, since the entire encoder memory is legitimately visible regardless of target position (the encoder was never autoregressive to begin with).
Design Intuition — cross-attention as a differentiable dictionary lookup. It is useful to think of cross-attention (Eq. 3.4) as a continuous, differentiable relaxation of a hash-table lookup. A classical lookup takes a query and an index of stored pairs, and returns the value associated with whichever key exactly matches the query — a discrete, non-differentiable selection. Attention relaxes this along two axes at once: exact equality becomes a continuous compatibility score (), and hard selection-of-one becomes a weighted average over all entries (), with the weights themselves learned end-to-end via the softmax normalization, so the entire retrieval operation participates in backpropagation.
The "table" being queried in Eq. (3.4) is the encoder's output memory: keys and values are computed once — via applied to Memory — and then reused, unchanged, at every decoder layer and every generation step, exactly like repeatedly querying a fixed, precomputed database, while the query vector changes at each decoding position, reflecting what the decoder has generated so far. This fixed-table / varying-query asymmetry is the architectural signature that distinguishes cross-attention from self-attention at the level of the computation graph, independent of the already-noted historical lineage connecting attention to Bahdanau- and Luong-style alignment mechanisms for recurrent sequence-to-sequence models.
Structural Walkthrough — interpretation. Cross-attention answers the question: "given decoder state (having already attended causally to target positions ), which source positions are most relevant to predicting the next token?" Each row of the resulting attention matrix constitutes a soft, differentiable alignment between target position and every source position — the direct architectural descendant of the Bahdanau/Luong attention mechanisms that preceded the Transformer, now generalized to a multi-head, non-recurrent form.
3.3 The Full Decoder Stack
Structural Block Diagram — one Decoder layer:

Structural Block Diagram — the full Decoder ( layers):

Implementation Note. This matches TransformerDecoderLayer.forward (three residual sub-layers, norm1/norm2/norm3 applied in Post-LN order exactly as in Part 2.2) and Decoder.forward (embedding, scaling, positional encoding, sequential layer stack each receiving the same memory tensor, final LayerNorm).
3.4 Linear and Softmax Projection Layer
Definition (Generator / Output Projection). The decoder's final hidden states are projected into the target vocabulary space by a learned linear map (with bias), followed by a softmax to yield a next-token probability distribution at every position:
Vaswani et al. (2017, §3.4) additionally note that the same weight matrix may optionally be tied between the input embedding layers and this pre-softmax linear transformation (weight tying), scaled appropriately — a parameter-efficiency detail orthogonal to the core architecture and not required for a minimal implementation.
Design Intuition — why weight tying is a sensible inductive bias. The embedding layer maps a discrete token to a vector; the generator maps a vector back to a distribution over discrete tokens. These are, informally, inverse operations, and tying to the (transposed) embedding table encodes exactly this symmetry: a hidden state that is close, in embedding space, to the vector for some token should score highly under the generator — which is precisely what a shared weight matrix enforces by construction, rather than something a separately-initialized has to learn from scratch.
Mathematical Transformation — dimension tracking:
Implementation Note. The provided code defines self.generator = nn.Linear(d_model, tgt_vocab_size) and applies it as the last operation of Transformer.forward: logits = self.generator(dec_out). The softmax of Equation (3.6) is deliberately not applied inside the model in this implementation — a standard practice, since training uses F.cross_entropy (or nn.CrossEntropyLoss), which internally combines log_softmax and negative log-likelihood in a single, numerically stable operation. The explicit softmax of Eq. (3.6) is applied only at inference time, when converting logits to a probability distribution for sampling or beam search.
Part 4 — Minimal, Functional PyTorch Implementation
The following sections present the full reference implementation, decomposed to match Parts 1–3 exactly. Tensor shapes are annotated inline as in the source, using the convention of the Notation section. Each listing is followed by a Line-by-Line Code Walkthrough that maps every consequential line — every view, transpose, matmul, and broadcast — back to a specific equation from Parts 1–3, so that no line of code is left as an unexplained black box.
Here is the complete code: github
4.1 Positional Encoding (§2.4)
Implements Equations (2.5)–(2.7).
pythonimport math import torch import torch.nn as nn import torch.nn.functional as F class PositionalEncoding(nn.Module): """Sinusoidal positional encodings, added to token embeddings.""" def __init__(self, d_model, max_len=5000, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) pe = torch.zeros(max_len, d_model) # [max_len, D] position = torch.arange(0, max_len, dtype=torch.float).unsqueeze( 1 ) # [max_len, 1] div_term = torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) # [D/2] pe[:, 0::2] = torch.sin(position * div_term) # [max_len, D/2] -> even dims pe[:, 1::2] = torch.cos(position * div_term) # [max_len, D/2] -> odd dims pe = pe.unsqueeze(0) # [max_len, D] -> [1, max_len, D] self.register_buffer("pe", pe) def forward(self, x): # x: [B, T, D] T = x.size(1) x = ( x + self.pe[:, :T, :] ) # [B, T, D] + [1, T, D] -> [B, T, D] (broadcast over batch) return self.dropout(x) # [B, T, D]import math import torch import torch.nn as nn import torch.nn.functional as F class PositionalEncoding(nn.Module): """Sinusoidal positional encodings, added to token embeddings.""" def __init__(self, d_model, max_len=5000, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) pe = torch.zeros(max_len, d_model) # [max_len, D] position = torch.arange(0, max_len, dtype=torch.float).unsqueeze( 1 ) # [max_len, 1] div_term = torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) # [D/2] pe[:, 0::2] = torch.sin(position * div_term) # [max_len, D/2] -> even dims pe[:, 1::2] = torch.cos(position * div_term) # [max_len, D/2] -> odd dims pe = pe.unsqueeze(0) # [max_len, D] -> [1, max_len, D] self.register_buffer("pe", pe) def forward(self, x): # x: [B, T, D] T = x.size(1) x = ( x + self.pe[:, :T, :] ) # [B, T, D] + [1, T, D] -> [B, T, D] (broadcast over batch) return self.dropout(x) # [B, T, D]
Line-by-Line Code Walkthrough.
pe = torch.zeros(max_len, d_model)— allocates the encoding table once, up front. Because is a fixed, non-learned function of position, it can be computed a single time for every position up tomax_lenand then sliced per forward call, rather than recomputed on every batch.position = torch.arange(0, max_len, ...).unsqueeze(1)— the literal column vector of values from Eq. (2.5)–(2.6), shape ; the trailing singleton axis exists purely to set up broadcasting againstdiv_termin the next step.div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))— this line is the frequency term . Since , andtorch.arange(0, d_model, 2)produces exactly the values for (i.e. it already is "", not ""), the expression computes — matching the exponent of Eq. (2.5)–(2.6) term for term. Computing this in log-space (expof alog) rather than directly as10000 ** (-2*i/d_model)avoids forming a very large intermediate power and is the standard numerically-stable idiom for quantities spanning many orders of magnitude. Shape: .pe[:, 0::2] = torch.sin(position * div_term)—position * div_termbroadcasts , computing for every pair simultaneously (an outer product), then applies elementwise and writes the result into the even-indexed columns. This is the fully vectorized realization of Eq. (2.5) for all positions and all at once — no explicit loop overposoriis needed.pe[:, 1::2] = torch.cos(position * div_term)— identical broadcast, instead of , written into odd-indexed columns: Eq. (2.6), vectorized.pe = pe.unsqueeze(0)— , inserting a batch axis so this single precomputed table can broadcast against any batch size duringforward.self.register_buffer("pe", pe)— registerspeas a tensor that moves with the module across devices (.to(device),.cuda(), etc.) and is saved instate_dict(), but is explicitly excluded from.parameters()and therefore from gradient-based optimization. This line is the implementation-level statement that is a fixed function of position, not a learned quantity — consistent with the paper's choice of the sinusoidal (as opposed to a learned-embedding) variant.x = x + self.pe[:, :T, :]— slices the precomputed table down to the sequence length actually present in this batch, then broadcast-adds against : broadcasts against because a size-1 axis matches any size under NumPy/PyTorch broadcasting rules. This line is Eq. (2.7), (with the scaling already applied upstream, inEncoder.forward/Decoder.forward, before this module ever seesx).return self.dropout(x)— dropout is applied to the sum of embedding and positional encoding, matching the paper's placement, before the result enters the first encoder/decoder layer.
4.2 Scaled Dot-Product Attention (§1.1–1.2)
Implements Equations (1.1)–(1.2).
pythonclass ScaledDotProductAttention(nn.Module): """Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V""" def __init__(self, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) def forward(self, Q, K, V, mask=None): # Q: [B, H, T_q, d_k] # K: [B, H, T_k, d_k] # V: [B, H, T_k, d_k] d_k = Q.size(-1) K_t = K.transpose(-2, -1) # [B, H, T_k, d_k] -> [B, H, d_k, T_k] scores = torch.matmul(Q, K_t) / math.sqrt( d_k ) # [B,H,T_q,d_k] @ [B,H,d_k,T_k] -> [B, H, T_q, T_k] if mask is not None: # mask: [B, 1, T_q, T_k] (1 broadcasts over head dim H) scores = scores.masked_fill(mask == 0, float("-inf")) # [B, H, T_q, T_k] attn = F.softmax(scores, dim=-1) # [B, H, T_q, T_k], softmax over key dim T_k attn = self.dropout(attn) # [B, H, T_q, T_k] context = torch.matmul( attn, V ) # [B,H,T_q,T_k] @ [B,H,T_k,d_k] -> [B, H, T_q, d_k] return context, attnclass ScaledDotProductAttention(nn.Module): """Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V""" def __init__(self, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) def forward(self, Q, K, V, mask=None): # Q: [B, H, T_q, d_k] # K: [B, H, T_k, d_k] # V: [B, H, T_k, d_k] d_k = Q.size(-1) K_t = K.transpose(-2, -1) # [B, H, T_k, d_k] -> [B, H, d_k, T_k] scores = torch.matmul(Q, K_t) / math.sqrt( d_k ) # [B,H,T_q,d_k] @ [B,H,d_k,T_k] -> [B, H, T_q, T_k] if mask is not None: # mask: [B, 1, T_q, T_k] (1 broadcasts over head dim H) scores = scores.masked_fill(mask == 0, float("-inf")) # [B, H, T_q, T_k] attn = F.softmax(scores, dim=-1) # [B, H, T_q, T_k], softmax over key dim T_k attn = self.dropout(attn) # [B, H, T_q, T_k] context = torch.matmul( attn, V ) # [B,H,T_q,T_k] @ [B,H,T_k,d_k] -> [B, H, T_q, d_k] return context, attn
Line-by-Line Code Walkthrough.
d_k = Q.size(-1)— reads directly off the tensor's last axis rather than taking it as a hardcoded constant, confirming architecturally that is, by definition, "whatever the last axis of currently is" — consistent with the Notation table's definition after the head split of §4.3.K_t = K.transpose(-2, -1)— : forms independently within each slice. Only the last two axes are swapped; and are left untouched, because must be transposed within each independent attention instance, never across batch or head boundaries.scores = torch.matmul(Q, K_t) / math.sqrt(d_k)—torch.matmulon 4-D tensors treats every leading axis () as a batch dimension and performs an ordinary 2-D matrix multiply on the trailing two: , independently and simultaneously for all slices. This single line computes every instance of (Eq. 1.1, stage 1) at once;/ math.sqrt(d_k)then divides every entry by the same scalar, implementing the scaling of Eq. (1.1)/(1.4) as one fused elementwise operation. Note the resulting rank-4 shape is the batched instantiation of the rank-2 mathematics in Eq. (1.1) — exactly the "batch dimension elided" convention flagged in the Notation section, made concrete.scores = scores.masked_fill(mask == 0, float("-inf"))— implements Eq. (1.2) via a boolean-select overwrite rather than literal addition;mask == 0selects exactly the positions where the mask's value is (disallowed), matching Eq. (1.2)'s case split. As already noted in §1.1, this sidesteps the ill-defined arithmetic that the multiplicative shorthand would otherwise invite.attn = F.softmax(scores, dim=-1)—dim=-1is the axis (the last axis ofscores, shape ). This is precisely what makes row-stochastic per query: softmax normalizes over keys, independently for every triple — the vectorized realization of the Structural Walkthrough's stage 2 in §1.1.attn = self.dropout(attn)— dropout applied directly to the post-softmax attention weights; not present in Eq. (1.1) itself, but a standard Transformer-specific regularizer. Note this technically breaks the exact row-sum-to-1 property during training, since a dropped weight is not renormalized — an accepted implementation detail, not a bug.context = torch.matmul(attn, V)— : stage 3 (aggregation) of Eq. (1.1), , again batched over .return context, attn— returns both the aggregated output and the raw attention weights; the latter is unused in the minimal model's forward pass but is standard to expose for the kind of attention-map visualization referenced in §1.3's Design Intuition box.
4.3 Multi-Head Attention (§1.3)
Implements Equations (1.5)–(1.6) via the combined-projection formulation.
pythonclass MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout=0.1): super().__init__() assert d_model % n_heads == 0, "d_model must be divisible by n_heads" self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_heads self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model) self.W_o = nn.Linear(d_model, d_model) self.attention = ScaledDotProductAttention(dropout) def _split_heads(self, x, B, T): # x: [B, T, D] -> [B, T, H, d_k] -> [B, H, T, d_k] x = x.view(B, T, self.n_heads, self.d_k) return x.transpose(1, 2) def forward(self, query, key, value, mask=None): # query: [B, T_q, D] key/value: [B, T_k, D] B, T_q, _ = query.shape T_k = key.size(1) Q = self.W_q(query) # [B, T_q, D] -> [B, T_q, D] K = self.W_k(key) # [B, T_k, D] -> [B, T_k, D] V = self.W_v(value) # [B, T_k, D] -> [B, T_k, D] Q = self._split_heads(Q, B, T_q) # [B, T_q, D] -> [B, H, T_q, d_k] K = self._split_heads(K, B, T_k) # [B, T_k, D] -> [B, H, T_k, d_k] V = self._split_heads(V, B, T_k) # [B, T_k, D] -> [B, H, T_k, d_k] context, attn = self.attention(Q, K, V, mask) # context: [B, H, T_q, d_k] context = context.transpose( 1, 2 ).contiguous() # [B, H, T_q, d_k] -> [B, T_q, H, d_k] context = context.view( B, T_q, self.n_heads * self.d_k ) # [B, T_q, H, d_k] -> [B, T_q, D] output = self.W_o(context) # [B, T_q, D] -> [B, T_q, D] return output, attnclass MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout=0.1): super().__init__() assert d_model % n_heads == 0, "d_model must be divisible by n_heads" self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_heads self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model) self.W_o = nn.Linear(d_model, d_model) self.attention = ScaledDotProductAttention(dropout) def _split_heads(self, x, B, T): # x: [B, T, D] -> [B, T, H, d_k] -> [B, H, T, d_k] x = x.view(B, T, self.n_heads, self.d_k) return x.transpose(1, 2) def forward(self, query, key, value, mask=None): # query: [B, T_q, D] key/value: [B, T_k, D] B, T_q, _ = query.shape T_k = key.size(1) Q = self.W_q(query) # [B, T_q, D] -> [B, T_q, D] K = self.W_k(key) # [B, T_k, D] -> [B, T_k, D] V = self.W_v(value) # [B, T_k, D] -> [B, T_k, D] Q = self._split_heads(Q, B, T_q) # [B, T_q, D] -> [B, H, T_q, d_k] K = self._split_heads(K, B, T_k) # [B, T_k, D] -> [B, H, T_k, d_k] V = self._split_heads(V, B, T_k) # [B, T_k, D] -> [B, H, T_k, d_k] context, attn = self.attention(Q, K, V, mask) # context: [B, H, T_q, d_k] context = context.transpose( 1, 2 ).contiguous() # [B, H, T_q, d_k] -> [B, T_q, H, d_k] context = context.view( B, T_q, self.n_heads * self.d_k ) # [B, T_q, H, d_k] -> [B, T_q, D] output = self.W_o(context) # [B, T_q, D] -> [B, T_q, D] return output, attn
Line-by-Line Code Walkthrough.
self.W_q = nn.Linear(d_model, d_model)(andW_k,W_v,W_o) — these are the "combined-then-split" projections flagged in §1.3's Implementation Note, using a single matrix in place of separate matrices. The two are not merely claimed equivalent — they are provably so: partition into contiguous column blocks, , each . Matrix multiplication distributes over this horizontal blocking of the right-hand factor:
so computing the full projection and then slicing the result into contiguous chunks gives exactly the same numbers as slicing the weight matrix into separate per-head matrices first and projecting with each independently. This is the proof, not merely the assertion, that the code below realizes Eq. (1.5)–(1.6).
self.d_k = d_model // n_heads— implements the Notation table's ._split_heads:x.view(B, T, self.n_heads, self.d_k)— reinterprets the trailing axis of size as two axes with .viewis a zero-copy operation: it reinterprets strides over the same underlying memory without moving any data, which is only legal when the tensor is contiguous — true here, sinceself.W_q(query)is the direct output of a freshnn.Linearcall and has not yet been transposed. Because the split follows row-major (C-contiguous) order, the first elements of each -vector become head , the next become head , and so on — i.e. head occupies coordinates of the full projected vector, exactly the coordinate range that block produces in the equivalence proof above..transpose(1, 2)— , moving the head axis before the sequence axis. This is the exact tensor-shape prerequisite for §4.2's batched matmuls, which treat jointly as "batch" and as the matrix being multiplied — the concrete implementation of §1.3's batched dimension-tracking diagram.context, attn = self.attention(Q, K, V, mask)— delegates to the module from §4.2, invoked identically regardless of whether this call site is encoder self-attention, decoder masked self-attention, or cross-attention. The class is polymorphic over all three uses precisely because Eq. (1.5)–(1.6) do not distinguish these cases structurally; only the arguments — which tensors are passed as query/key/value, and which mask — differ, which is the architectural basis for §3.2's characterization of cross-attention as "structurally identical" to self-attention.context.transpose(1, 2).contiguous()— the inverse of_split_heads's reordering: . Here.contiguous()is not optional.transposereturns a view that only changes strides, not the physical memory layout, and the.view(...)call on the very next line requires contiguous memory to reinterpret shape — unliketranspose,viewcannot operate on an arbitrary strided tensor. This is the single most common source of a silentRuntimeError(or, with a more permissive reshape, silently wrong results) in hand-written multi-head attention, and it is worth contrasting directly with the firstviewcall in this same method:self.W_q(query)is fresh, contiguousnn.Linearoutput, so_split_heads'sviewneeds no.contiguous()call, while this secondview— coming immediately after atranspose— does. Same operation, different prerequisite, for exactly this reason.context.view(B, T_q, self.n_heads * self.d_k)— merges the last two axes , again in row-major order, so head 's values occupy the first coordinates of the merged vector, head 's the next , and so on. This is the literal matrix realization of in Eq. (1.6): PyTorch'sview-based merge and the paper'sConcat(·)notation denote the same operation, one expressed in tensor-reshape language and the other in matrix-concatenation language.output = self.W_o(context)— the final of Eq. (1.6).
4.4 Position-wise Feed-Forward Network (§2.3)
Implements Equation (2.4).
pythonclass PositionwiseFeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout=0.1): super().__init__() self.linear1 = nn.Linear(d_model, d_ff) self.linear2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): # x: [B, T, D] x = self.linear1(x) # [B, T, D] -> [B, T, d_ff] x = F.relu(x) # [B, T, d_ff] x = self.dropout(x) # [B, T, d_ff] x = self.linear2(x) # [B, T, d_ff] -> [B, T, D] return xclass PositionwiseFeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout=0.1): super().__init__() self.linear1 = nn.Linear(d_model, d_ff) self.linear2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): # x: [B, T, D] x = self.linear1(x) # [B, T, D] -> [B, T, d_ff] x = F.relu(x) # [B, T, d_ff] x = self.dropout(x) # [B, T, d_ff] x = self.linear2(x) # [B, T, d_ff] -> [B, T, D] return x
Line-by-Line Code Walkthrough.
self.linear1 = nn.Linear(d_model, d_ff)— stores its weight as a tensor (PyTorch's[out_features, in_features]convention), the transpose of the mathematical in Eq. (2.4), per the Remark following the Notation table.linear1(x)internally computesx @ linear1.weight.T + linear1.bias, which is exactly oncelinear1.weight.Tis identified with .x = self.linear1(x)— , the expansion half of Eq. (2.4).x = F.relu(x)— elementwise , applied independently to every one of the scalar activations — the nonlinearity in Eq. (2.4).x = self.dropout(x)— regularizes the expanded hidden representation; not part of Eq. (2.4) itself, but standard practice, applied here (in the wide -dimensional space) rather than after the projection back to .x = self.linear2(x)— , the projection half of Eq. (2.4);linear2.weightis stored as , the transpose of .
4.5 The Encoder Layer and Encoder Stack (§2.1, 2.2, 2.5)
Implements the Post-LN residual composition of Eq. (2.2), assembled from §4.3 and §4.4.
pythonclass TransformerEncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(dropout) self.dropout2 = nn.Dropout(dropout) def forward(self, x, src_mask=None): # x: [B, S, D] attn_out, _ = self.self_attn(x, x, x, src_mask) # [B, S, D] -> [B, S, D] x = self.norm1(x + self.dropout1(attn_out)) # residual + LayerNorm: [B, S, D] ffn_out = self.ffn(x) # [B, S, D] -> [B, S, D] x = self.norm2(x + self.dropout2(ffn_out)) # residual + LayerNorm: [B, S, D] return x class Encoder(nn.Module): def __init__( self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout=0.1 ): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) self.pos_enc = PositionalEncoding(d_model, max_len, dropout) self.layers = nn.ModuleList( [ TransformerEncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ] ) self.norm = nn.LayerNorm(d_model) def forward(self, src, src_mask=None): # src: [B, S] (integer token ids) x = self.embed(src) * math.sqrt( self.d_model ) # [B, S] -> [B, S, D], scaled per Vaswani et al. x = self.pos_enc(x) # [B, S, D] -> [B, S, D] for layer in self.layers: x = layer(x, src_mask) # [B, S, D] -> [B, S, D] return self.norm(x) # [B, S, D] final encoder memoryclass TransformerEncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(dropout) self.dropout2 = nn.Dropout(dropout) def forward(self, x, src_mask=None): # x: [B, S, D] attn_out, _ = self.self_attn(x, x, x, src_mask) # [B, S, D] -> [B, S, D] x = self.norm1(x + self.dropout1(attn_out)) # residual + LayerNorm: [B, S, D] ffn_out = self.ffn(x) # [B, S, D] -> [B, S, D] x = self.norm2(x + self.dropout2(ffn_out)) # residual + LayerNorm: [B, S, D] return x class Encoder(nn.Module): def __init__( self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout=0.1 ): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) self.pos_enc = PositionalEncoding(d_model, max_len, dropout) self.layers = nn.ModuleList( [ TransformerEncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ] ) self.norm = nn.LayerNorm(d_model) def forward(self, src, src_mask=None): # src: [B, S] (integer token ids) x = self.embed(src) * math.sqrt( self.d_model ) # [B, S] -> [B, S, D], scaled per Vaswani et al. x = self.pos_enc(x) # [B, S, D] -> [B, S, D] for layer in self.layers: x = layer(x, src_mask) # [B, S, D] -> [B, S, D] return self.norm(x) # [B, S, D] final encoder memory
Line-by-Line Code Walkthrough.
attn_out, _ = self.self_attn(x, x, x, src_mask)— the literal code-level instantiation of Eq. (2.1), : the same tensorxis passed for all three of query/key/value. As already flagged in §2.1's Design Intuition, this does not mean numerically — insideMultiHeadAttention.forward,W_q(x),W_k(x), andW_v(x)each apply a different learned projection to the same input, so the three resulting matrices differ in general even though they share a source tensor.x = self.norm1(x + self.dropout1(attn_out))— Post-LN, Eq. (2.2), with the order of operationsdropout(sub-layer output) → + residual x → LayerNormmatching the equation exactly.ffn_out = self.ffn(x)/x = self.norm2(x + self.dropout2(ffn_out))— the second Post-LN sub-layer, structurally identical to the first.self.embed(src) * math.sqrt(self.d_model)—nn.Embeddingperforms a differentiable lookup, mathematically equivalent to for an embedding matrix , though implemented as direct row-indexing rather than an explicit one-hot matrix multiply for efficiency. Multiplying bymath.sqrt(self.d_model)is the literal factor of Eq. (2.7).x = self.pos_enc(x)— delegates to §4.1, adding and applying dropout, as already walked through there.for layer in self.layers: x = layer(x, src_mask)— the literal realization of for . As already noted in §2.5's Implementation Note,nn.ModuleListdoes not tie weights across iterations — each of the layer objects owns independent parameters, so "stack of identical layers" is an architectural, not a weight-sharing, statement (in contrast to, say, a recurrent network applying the same weights at every step).return self.norm(x)— the stack-level final LayerNorm, matching the block diagram's terminal step beforeMemoryis handed to the decoder.
4.6 The Decoder Layer and Decoder Stack (§3.1–3.3)
Implements the three-sub-layer residual composition, including cross-attention (Eq. 3.4).
pythonclass TransformerDecoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout) self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.norm3 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(dropout) self.dropout2 = nn.Dropout(dropout) self.dropout3 = nn.Dropout(dropout) def forward(self, x, memory, tgt_mask=None, memory_mask=None): # x: [B, T, D] (decoder input / previous layer output) # memory: [B, S, D] (encoder output) self_attn_out, _ = self.self_attn(x, x, x, tgt_mask) # [B, T, D] -> [B, T, D] x = self.norm1( x + self.dropout1(self_attn_out) ) # residual + LayerNorm: [B, T, D] cross_attn_out, _ = self.cross_attn(x, memory, memory, memory_mask) # Q: [B, T, D] (from decoder), K/V: [B, S, D] (from encoder) -> output: [B, T, D] x = self.norm2( x + self.dropout2(cross_attn_out) ) # residual + LayerNorm: [B, T, D] ffn_out = self.ffn(x) # [B, T, D] -> [B, T, D] x = self.norm3(x + self.dropout3(ffn_out)) # residual + LayerNorm: [B, T, D] return x class Decoder(nn.Module): def __init__( self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout=0.1 ): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) self.pos_enc = PositionalEncoding(d_model, max_len, dropout) self.layers = nn.ModuleList( [ TransformerDecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ] ) self.norm = nn.LayerNorm(d_model) def forward(self, tgt, memory, tgt_mask=None, memory_mask=None): # tgt: [B, T] (integer token ids), memory: [B, S, D] (encoder output) x = self.embed(tgt) * math.sqrt(self.d_model) # [B, T] -> [B, T, D] x = self.pos_enc(x) # [B, T, D] -> [B, T, D] for layer in self.layers: x = layer(x, memory, tgt_mask, memory_mask) # [B, T, D] -> [B, T, D] return self.norm(x) # [B, T, D] final decoder hidden statesclass TransformerDecoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout) self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.norm3 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(dropout) self.dropout2 = nn.Dropout(dropout) self.dropout3 = nn.Dropout(dropout) def forward(self, x, memory, tgt_mask=None, memory_mask=None): # x: [B, T, D] (decoder input / previous layer output) # memory: [B, S, D] (encoder output) self_attn_out, _ = self.self_attn(x, x, x, tgt_mask) # [B, T, D] -> [B, T, D] x = self.norm1( x + self.dropout1(self_attn_out) ) # residual + LayerNorm: [B, T, D] cross_attn_out, _ = self.cross_attn(x, memory, memory, memory_mask) # Q: [B, T, D] (from decoder), K/V: [B, S, D] (from encoder) -> output: [B, T, D] x = self.norm2( x + self.dropout2(cross_attn_out) ) # residual + LayerNorm: [B, T, D] ffn_out = self.ffn(x) # [B, T, D] -> [B, T, D] x = self.norm3(x + self.dropout3(ffn_out)) # residual + LayerNorm: [B, T, D] return x class Decoder(nn.Module): def __init__( self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout=0.1 ): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) self.pos_enc = PositionalEncoding(d_model, max_len, dropout) self.layers = nn.ModuleList( [ TransformerDecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ] ) self.norm = nn.LayerNorm(d_model) def forward(self, tgt, memory, tgt_mask=None, memory_mask=None): # tgt: [B, T] (integer token ids), memory: [B, S, D] (encoder output) x = self.embed(tgt) * math.sqrt(self.d_model) # [B, T] -> [B, T, D] x = self.pos_enc(x) # [B, T, D] -> [B, T, D] for layer in self.layers: x = layer(x, memory, tgt_mask, memory_mask) # [B, T, D] -> [B, T, D] return self.norm(x) # [B, T, D] final decoder hidden states
Line-by-Line Code Walkthrough.
self.self_attn = MultiHeadAttention(...)andself.cross_attn = MultiHeadAttention(...)— two separate instances of the same class from §4.3, with independently initialized and trained parameters; nothing is shared between them beyond the class definition.self_attn_out, _ = self.self_attn(x, x, x, tgt_mask)— masked self-attention, usingtgt_mask(the composite causal-and-padding mask of Eq. 3.3), structured exactly like the encoder's self-attention call, but with a query-position-dependent mask this time.x = self.norm1(x + self.dropout1(self_attn_out))— the first of the decoder layer's three Post-LN sub-layers.cross_attn_out, _ = self.cross_attn(x, memory, memory, memory_mask)— this is the onlyMultiHeadAttentioncall site in the entire model where the three positional arguments are not all the same tensor:query=x(the decoder's own, causally-processed state) whilekey=value=memory(the encoder's output). This is the code-level realization of Eq. (3.4), , and the single structural fact that makes cross-attention "cross" rather than "self." InsideMultiHeadAttention.forward,T_q = query.shape[1]is read fromx(length ) whileT_k = key.size(1)is read independently frommemory(length ) — confirming the implementation correctly handles with no hardcoded assumption that source and target share a length, exactly as the dimension-tracking in §3.2 requires.x = self.norm2(x + self.dropout2(cross_attn_out))— the second Post-LN sub-layer, wrapping cross-attention exactly as the first wrapped self-attention.ffn_out = self.ffn(x)/x = self.norm3(...)— the third sub-layer, identical in structure to the encoder's FFN sub-layer.Decoder.forward:for layer in self.layers: x = layer(x, memory, tgt_mask, memory_mask)— note thatmemoryis passed to every decoder layer unchanged: the encoder runs once, up front, and the sameMemorytensor is reused as the key/value source at every one of the decoder layers — the code-level expression of the "fixed table, varying query" asymmetry from §3.2's Design Intuition.
4.7 The Full Transformer: Masking, Assembly, and Output Projection (§3.4)
Assembles §4.5 and §4.6 and implements Eq. (3.1), (3.3), and (3.5).
pythonclass Transformer(nn.Module): def __init__( self, src_vocab_size, tgt_vocab_size, d_model=512, n_heads=8, d_ff=2048, n_layers=6, max_len=5000, dropout=0.1, pad_idx=0, ): super().__init__() self.pad_idx = pad_idx self.encoder = Encoder( src_vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout ) self.decoder = Decoder( tgt_vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout ) self.generator = nn.Linear( d_model, tgt_vocab_size ) # final projection to vocab logits for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) def make_src_mask(self, src): # src: [B, S] src_mask = (src != self.pad_idx).unsqueeze(1).unsqueeze(2) # [B, S] -> [B, 1, S] -> [B, 1, 1, S]; broadcasts over heads H and query positions T_q return src_mask def make_tgt_mask(self, tgt): # tgt: [B, T] B, T = tgt.shape pad_mask = ( (tgt != self.pad_idx).unsqueeze(1).unsqueeze(2) ) # [B, T] -> [B, 1, 1, T] causal_mask = torch.tril( torch.ones((T, T), device=tgt.device) ).bool() # [T, T], lower-triangular causal_mask = causal_mask.unsqueeze(0).unsqueeze(0) # [T, T] -> [1, 1, T, T] tgt_mask = pad_mask & causal_mask # [B,1,1,T] & [1,1,T,T] -> [B, 1, T, T] return tgt_mask def forward(self, src, tgt): # src: [B, S] source token ids # tgt: [B, T] target token ids (shifted-right during training) src_mask = self.make_src_mask(src) # [B, 1, 1, S] tgt_mask = self.make_tgt_mask(tgt) # [B, 1, T, T] memory = self.encoder(src, src_mask) # [B, S] -> [B, S, D] dec_out = self.decoder( tgt, memory, tgt_mask, src_mask ) # [B, T], [B, S, D] -> [B, T, D] logits = self.generator(dec_out) # [B, T, D] -> [B, T, V_tgt] return logitsclass Transformer(nn.Module): def __init__( self, src_vocab_size, tgt_vocab_size, d_model=512, n_heads=8, d_ff=2048, n_layers=6, max_len=5000, dropout=0.1, pad_idx=0, ): super().__init__() self.pad_idx = pad_idx self.encoder = Encoder( src_vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout ) self.decoder = Decoder( tgt_vocab_size, d_model, n_heads, d_ff, n_layers, max_len, dropout ) self.generator = nn.Linear( d_model, tgt_vocab_size ) # final projection to vocab logits for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) def make_src_mask(self, src): # src: [B, S] src_mask = (src != self.pad_idx).unsqueeze(1).unsqueeze(2) # [B, S] -> [B, 1, S] -> [B, 1, 1, S]; broadcasts over heads H and query positions T_q return src_mask def make_tgt_mask(self, tgt): # tgt: [B, T] B, T = tgt.shape pad_mask = ( (tgt != self.pad_idx).unsqueeze(1).unsqueeze(2) ) # [B, T] -> [B, 1, 1, T] causal_mask = torch.tril( torch.ones((T, T), device=tgt.device) ).bool() # [T, T], lower-triangular causal_mask = causal_mask.unsqueeze(0).unsqueeze(0) # [T, T] -> [1, 1, T, T] tgt_mask = pad_mask & causal_mask # [B,1,1,T] & [1,1,T,T] -> [B, 1, T, T] return tgt_mask def forward(self, src, tgt): # src: [B, S] source token ids # tgt: [B, T] target token ids (shifted-right during training) src_mask = self.make_src_mask(src) # [B, 1, 1, S] tgt_mask = self.make_tgt_mask(tgt) # [B, 1, T, T] memory = self.encoder(src, src_mask) # [B, S] -> [B, S, D] dec_out = self.decoder( tgt, memory, tgt_mask, src_mask ) # [B, T], [B, S, D] -> [B, T, D] logits = self.generator(dec_out) # [B, T, D] -> [B, T, V_tgt] return logits
Line-by-Line Code Walkthrough.
make_src_mask:(src != self.pad_idx)— an elementwise boolean comparison, ..unsqueeze(1).unsqueeze(2)— , inserting two singleton axes. These axes are not arbitrary:scoresinsideScaledDotProductAttentionhas shape , and the mask must broadcast against it. The inserted axis at position broadcasts against — a padding key is invalid regardless of which head is asking — and the inserted axis at position broadcasts against — a padding key is invalid regardless of which query position is asking. The final axis, of size , matches directly, with no broadcasting needed. The shape is thus dictated by which ofscores's four axes must vary freely and which must apply uniformly, not an arbitrary convention.make_tgt_mask:causal_mask = torch.tril(torch.ones((T, T), ...)).bool()—torch.ones((T,T))is an all-ones matrix;torch.tril(·)zeroes every entry strictly above the main diagonal, leaving s exactly where — the literal constructive definition of in Eq. (3.1), including the diagonal (, "attend to self," which Eq. (3.1)'s explicitly permits).device=tgt.deviceensures the mask is materialized on the same device astgt, avoiding a silent CPU/GPU mismatch.pad_mask & causal_mask— broadcasting two masks of different rank against each other. Tracing every axis explicitly, right-aligned:
| Axis | pad_mask | causal_mask | Result |
|---|---|---|---|
| 0 (batch) | |||
| 1 (head) | |||
| 2 (query pos.) | |||
| 3 (key pos.) |
giving tgt_mask of shape — matching the earlier claim, now derived rather than merely asserted. This shape genuinely differs from the encoder's , and for a specific reason: causal masking is query-position-dependent (each row of the mask permits a different set of keys), so the query-position axis can no longer broadcast as a singleton the way it does for padding-only masking.
forward:dec_out = self.decoder(tgt, memory, tgt_mask, src_mask)— note thatsrc_mask, not a separately constructedmemory_mask, is passed as the fourth argument. This is correct, not an oversight: cross-attention's key/value sequence is the encoder's source sequence, so the padding mask governing which source positions are attendable is identical to the mask already computed for encoder self-attention — no independent "memory mask" needs to be built.for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p)— applies Xavier/Glorot-uniform initialization, with , to every parameter tensor of rank (weight matrices), explicitly excluding rank-1 tensors (biases, and LayerNorm's , which keep their own default initializations of and ). Xavier initialization is chosen to approximately preserve activation variance across the initial forward pass — the same variance-preservation goal that motivates the attention scaling of §1.2. Taken together, the two techniques mean the architecture is engineered, at every stage from initialization through the attention mechanism itself, around one unifying principle: keep signal variance roughly constant as depth increases, so neither activations nor gradients silently vanish or explode before training has a chance to shape them.
4.8 Structural Verification
The __main__ block instantiates the full model at the canonical hyperparameters of Vaswani et al. (2017) — , (hence ), , layers per stack — and verifies output shape:
pythonif __name__ == "__main__": torch.manual_seed(0) SRC_VOCAB, TGT_VOCAB = 5000, 5000 B, S, T = 2, 10, 12 # batch size, source length, target length model = Transformer( src_vocab_size=SRC_VOCAB, tgt_vocab_size=TGT_VOCAB, d_model=512, n_heads=8, d_ff=2048, n_layers=6, max_len=100, dropout=0.1, pad_idx=0, ) src = torch.randint(1, SRC_VOCAB, (B, S)) # [B, S] tgt = torch.randint(1, TGT_VOCAB, (B, T)) # [B, T] logits = model(src, tgt) # [B, T, V_tgt] print(logits.shape) # torch.Size([2, 12, 5000])if __name__ == "__main__": torch.manual_seed(0) SRC_VOCAB, TGT_VOCAB = 5000, 5000 B, S, T = 2, 10, 12 # batch size, source length, target length model = Transformer( src_vocab_size=SRC_VOCAB, tgt_vocab_size=TGT_VOCAB, d_model=512, n_heads=8, d_ff=2048, n_layers=6, max_len=100, dropout=0.1, pad_idx=0, ) src = torch.randint(1, SRC_VOCAB, (B, S)) # [B, S] tgt = torch.randint(1, TGT_VOCAB, (B, T)) # [B, T] logits = model(src, tgt) # [B, T, V_tgt] print(logits.shape) # torch.Size([2, 12, 5000])
End-to-End Shape Trace. Every dimension-tracking claim made symbolically throughout Parts 1–4 is verified below by hand-executing the model at these concrete numbers (, , , , , , , ). This is deliberately exhaustive: every stage a tensor passes through is listed, so that no transformation in the entire model is left unverified.
Encoder, per layer (×6, identical shapes each iteration):
| Stage | Operation | Shape |
|---|---|---|
| input | — | |
| self-attn projections | each | |
| split heads | view + transpose | each |
| scores | ||
| softmax + context | ||
| merge heads, | transpose + view, then | |
| residual + norm1 | — | |
| FFN | ||
| residual + norm2 | — |
After 6 layers and the stack-final LayerNorm: .
Decoder, per layer (×6, identical shapes each iteration):
| Stage | Operation | Shape |
|---|---|---|
| input | — | |
| masked self-attn | split heads | each |
| self-attn scores | , tgt_mask broadcasts | |
| self-attn context, merge, | — | |
| residual + norm1 | — | |
| cross-attn (from ) | split heads | |
| cross-attn (from memory) | split heads | each |
| cross-attn scores | , memory_mask broadcasts | |
| cross-attn context, merge, | — | |
| residual + norm2 | — | |
| FFN | ||
| residual + norm3 | — |
The bolded entry is the concrete payoff of §3.2's dimension tracking: cross-attention scores are genuinely non-square (), unlike every self-attention score matrix in the model, which is always square (, since query and key come from the same sequence there). After 6 layers and the stack-final LayerNorm: .
Final projection: — matching the printed torch.Size([2, 12, 5000]) exactly, and confirming, at the level of concrete numbers rather than symbols, that Eq. (3.5)'s output shape is realized correctly end to end.
Appendix A — Equation-to-Implementation Cross-Reference
| Equation | Description | Implementation |
|---|---|---|
| (1.1) | Scaled dot-product attention | ScaledDotProductAttention.forward, §4.2 |
| (1.2) / (3.2) | Additive masking | masked_fill call, §4.2 |
| (1.3)–(1.4) | Variance growth / restoration | realized implicitly by / math.sqrt(d_k), §4.2 |
| (1.4a)–(1.4c) | Softmax Jacobian and saturation analysis | mathematical result motivating the scaling in §4.2; no direct code counterpart |
| (1.5)–(1.6) | Multi-head attention | MultiHeadAttention.forward, §4.3 |
| (2.1) | Encoder self-attention | self.self_attn(x, x, x, src_mask), §4.5 |
| (2.2) | Post-LN residual sub-layer | norm(x + dropout(...)) pattern, §4.5 / §4.6 |
| (2.4) | Position-wise FFN | PositionwiseFeedForward.forward, §4.4 |
| (2.5)–(2.7) | Sinusoidal positional encoding | PositionalEncoding, §4.1 |
| (3.1) / (3.3) | Causal + padding mask | make_tgt_mask, §4.7 |
| (3.4) | Cross-attention | self.cross_attn(x, memory, memory, memory_mask), §4.6 |
| (3.5)–(3.6) | Output projection | self.generator, §4.7 |