File size: 14,296 Bytes
256c9c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# Knowledge: Transformer Components

## Purpose
Canonical correct implementations of transformer building blocks that papers frequently reference but don't re-explain. When a paper says "standard transformer encoder," this file tells you what that means and what mistakes to avoid.

---

## Multi-Head Attention

### Canonical implementation

```python
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model: int, n_heads: int, dropout: float = 0.0,
                 bias: bool = True):
        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  # head dimension

        self.W_q = nn.Linear(d_model, d_model, bias=bias)
        self.W_k = nn.Linear(d_model, d_model, bias=bias)
        self.W_v = nn.Linear(d_model, d_model, bias=bias)
        self.W_o = nn.Linear(d_model, d_model, bias=bias)
        self.dropout = nn.Dropout(dropout)

    def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor,
                mask: Optional[torch.Tensor] = None) -> torch.Tensor:
        batch_size = query.size(0)

        # Project and reshape: (batch, seq, d_model) -> (batch, n_heads, seq, d_k)
        q = self.W_q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        k = self.W_k(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        v = self.W_v(value).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

        # Scaled dot-product attention
        # (batch, n_heads, seq_q, d_k) @ (batch, n_heads, d_k, seq_k) -> (batch, n_heads, seq_q, seq_k)
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))

        attn_weights = F.softmax(scores, dim=-1)  # (batch, n_heads, seq_q, seq_k)
        attn_weights = self.dropout(attn_weights)

        # (batch, n_heads, seq_q, seq_k) @ (batch, n_heads, seq_k, d_k) -> (batch, n_heads, seq_q, d_k)
        context = torch.matmul(attn_weights, v)

        # Reshape back: (batch, n_heads, seq_q, d_k) -> (batch, seq_q, d_model)
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        return self.W_o(context)  # (batch, seq_q, d_model)
```

### Common mistakes

1. **Scaling by sqrt(d_model) instead of sqrt(d_k)**
   - The scale factor is `sqrt(d_k)` where `d_k = d_model / n_heads`
   - NOT `sqrt(d_model)`. This is the single most common mistake.
   - Vaswani et al. Β§3.2.1: "We suspect that for large values of d_k, the dot products grow large in magnitude"

2. **Wrong mask convention**
   - Additive mask: add a large negative number (e.g., -1e9 or -inf) to scores BEFORE softmax
   - Multiplicative mask: multiply attention weights by 0/1 AFTER softmax
   - Both are valid, but papers rarely specify which. Additive is more common and numerically cleaner.
   - Using -inf is cleaner than -1e9 (avoids non-zero attention for -1e9 with float16)

3. **Forgetting `.contiguous()` after transpose**
   - After `transpose(1, 2)`, the tensor may not be contiguous
   - `.view()` requires a contiguous tensor
   - This will crash, not silently fail β€” but it's a common "why doesn't my code run" bug

4. **Causal mask shape**
   - Should be `(1, 1, seq_len, seq_len)` for broadcasting with `(batch, n_heads, seq_len, seq_len)` scores
   - Mask where `mask[i][j] = 1` if position `j` is allowed for position `i`
   - Upper triangular = disallowed, not lower triangular (common mistake)

### Causal masking

```python
def create_causal_mask(seq_len: int, device: torch.device) -> torch.Tensor:
    """Creates a causal (autoregressive) attention mask.
    Returns a (1, 1, seq_len, seq_len) boolean tensor where True = attend, False = mask.
    """
    mask = torch.tril(torch.ones(seq_len, seq_len, device=device, dtype=torch.bool))
    return mask.unsqueeze(0).unsqueeze(0)  # (1, 1, seq_len, seq_len)
```

---

## Positional Encodings

### Sinusoidal (Vaswani et al., 2017)

```python
class SinusoidalPositionalEncoding(nn.Module):
    """Fixed sinusoidal positional encoding from 'Attention Is All You Need'.

    PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
    PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
    """
    def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.0):
        super().__init__()
        self.dropout = nn.Dropout(dropout)

        pe = torch.zeros(max_len, d_model)  # (max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()  # (max_len, 1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
        )  # (d_model/2,)

        pe[:, 0::2] = torch.sin(position * div_term)  # even indices
        pe[:, 1::2] = torch.cos(position * div_term)  # odd indices
        pe = pe.unsqueeze(0)  # (1, max_len, d_model)
        self.register_buffer('pe', pe)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x: (batch, seq_len, d_model)
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)
```

### Common mistakes with sinusoidal PE:
- Using `arange(0, d_model)` instead of `arange(0, d_model, 2)` for div_term
- Off-by-one in position indexing (should start at 0)
- Forgetting to `register_buffer` (so it's not a parameter but moves with the model to GPU)

### Learned positional embeddings

```python
class LearnedPositionalEmbedding(nn.Module):
    def __init__(self, max_len: int, d_model: int):
        super().__init__()
        self.embedding = nn.Embedding(max_len, d_model)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x: (batch, seq_len, d_model)
        positions = torch.arange(x.size(1), device=x.device)  # (seq_len,)
        return x + self.embedding(positions)  # broadcast over batch
```

### Rotary Position Embedding (RoPE) β€” Su et al., 2021

```python
class RotaryPositionalEmbedding(nn.Module):
    """RoPE: Enhanced Transformer with Rotary Position Embedding.

    Applied to each head individually within the attention computation,
    AFTER the Q and K projections but BEFORE the dot product.
    """
    def __init__(self, d_head: int, max_len: int = 8192, base: float = 10000.0):
        super().__init__()
        inv_freq = 1.0 / (base ** (torch.arange(0, d_head, 2).float() / d_head))
        self.register_buffer('inv_freq', inv_freq)
        self.max_len = max_len

    def forward(self, x: torch.Tensor, seq_len: int) -> Tuple[torch.Tensor, torch.Tensor]:
        t = torch.arange(seq_len, device=x.device).float()
        freqs = torch.outer(t, self.inv_freq)  # (seq_len, d_head/2)
        emb = torch.cat([freqs, freqs], dim=-1)  # (seq_len, d_head)
        return emb.cos(), emb.sin()


def apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
    """Apply RoPE to query or key tensor.
    x: (batch, n_heads, seq_len, d_head)
    """
    d_half = x.shape[-1] // 2
    x1, x2 = x[..., :d_half], x[..., d_half:]
    return torch.cat([
        x1 * cos[..., :d_half] - x2 * sin[..., :d_half],
        x2 * cos[..., d_half:] + x1 * sin[..., d_half:]
    ], dim=-1)
```

### Key difference: RoPE is applied to Q and K individually, NOT summed onto embeddings like sinusoidal PE.

### ALiBi (Press et al., 2022)

ALiBi doesn't use positional embeddings at all. Instead, it adds a linear bias to the attention scores:

```python
def get_alibi_slopes(n_heads: int) -> torch.Tensor:
    """Compute ALiBi slopes for each head.
    Head i gets slope 2^(-8i/n_heads) for i = 1, ..., n_heads
    """
    ratio = 2 ** (-8 / n_heads)
    slopes = torch.tensor([ratio ** i for i in range(1, n_heads + 1)])
    return slopes  # (n_heads,)

def apply_alibi(scores: torch.Tensor, slopes: torch.Tensor) -> torch.Tensor:
    """Apply ALiBi bias to attention scores.
    scores: (batch, n_heads, seq_q, seq_k)
    slopes: (n_heads,)
    """
    seq_q, seq_k = scores.size(-2), scores.size(-1)
    # Position difference: relative distance between query and key positions
    positions = torch.arange(seq_k, device=scores.device).unsqueeze(0) - \
                torch.arange(seq_q, device=scores.device).unsqueeze(1)  # (seq_q, seq_k)
    bias = slopes.unsqueeze(-1).unsqueeze(-1) * positions.unsqueeze(0)  # (n_heads, seq_q, seq_k)
    return scores + bias.unsqueeze(0)  # broadcast over batch
```

---

## Layer Normalization

### Pre-norm vs Post-norm β€” THIS MATTERS ENORMOUSLY

**Post-norm (original Transformer):**
```python
# Post-norm: normalize AFTER the residual addition
x = self.norm(x + self.sublayer(x))
```

**Pre-norm (GPT-2, most modern transformers):**
```python
# Pre-norm: normalize BEFORE the sublayer, residual OUTSIDE the norm
x = x + self.sublayer(self.norm(x))
```

**Why it matters:**
- Post-norm requires learning rate warmup and careful initialization
- Pre-norm is much more stable to train at scale
- They produce different quality models β€” not interchangeable
- Many papers show post-norm in figures but use pre-norm in experiments β€” ALWAYS CHECK

### RMSNorm (Zhang & Sennrich, 2019)

```python
class RMSNorm(nn.Module):
    """Root Mean Square Layer Normalization.
    Used in LLaMA, T5. Simpler than LayerNorm (no centering, no bias).
    """
    def __init__(self, d_model: int, eps: float = 1e-6):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(d_model))
        self.eps = eps

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        rms = torch.sqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + self.eps)
        return x / rms * self.weight
```

---

## Feed-Forward Network

### Standard (Vaswani et al.)
```python
class FeedForward(nn.Module):
    """Two-layer feed-forward network with expansion factor.
    FFN(x) = W_2 * activation(W_1 * x + b_1) + b_2
    """
    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.0,
                 activation: str = "relu"):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

        if activation == "relu":
            self.activation = nn.ReLU()
        elif activation == "gelu":
            self.activation = nn.GELU()
        elif activation == "silu":
            self.activation = nn.SiLU()
        else:
            raise ValueError(f"Unknown activation: {activation}")

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # (batch, seq, d_model) -> (batch, seq, d_ff) -> (batch, seq, d_model)
        return self.linear2(self.dropout(self.activation(self.linear1(x))))
```

### SwiGLU (Shazeer, 2020) β€” used in LLaMA, PaLM
```python
class SwiGLU(nn.Module):
    """Gated feed-forward with SiLU activation.
    SwiGLU(x) = (SiLU(W_1 * x) βŠ™ W_3 * x) * W_2
    Note: uses 3 weight matrices, not 2. This changes parameter count.
    """
    def __init__(self, d_model: int, d_ff: int):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff, bias=False)
        self.w2 = nn.Linear(d_ff, d_model, bias=False)
        self.w3 = nn.Linear(d_model, d_ff, bias=False)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.w2(F.silu(self.w1(x)) * self.w3(x))
```

---

## Embedding with Weight Tying

```python
class TransformerEmbedding(nn.Module):
    """Token + positional embedding with optional weight tying to output projection.

    Weight tying (Press & Wolf, 2017): The embedding matrix and the output
    projection matrix are the SAME tensor. This reduces parameters and often
    improves performance. Many papers do this without mentioning it explicitly.
    """
    def __init__(self, vocab_size: int, d_model: int, max_len: int,
                 dropout: float = 0.0, scale: bool = True):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, d_model)
        self.pos_emb = LearnedPositionalEmbedding(max_len, d_model)
        self.dropout = nn.Dropout(dropout)
        self.scale = math.sqrt(d_model) if scale else 1.0
        # Vaswani et al. Β§3.4: "we multiply those weights by sqrt(d_model)"

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x: (batch, seq_len) of token IDs
        tok = self.token_emb(x) * self.scale  # (batch, seq_len, d_model)
        return self.dropout(self.pos_emb(tok))
```

**Weight tying note:** If the paper ties embedding and output weights, the output projection is `F.linear(x, model.embedding.token_emb.weight)` β€” not a separate `nn.Linear`. Many papers do this without stating it. Check the parameter count in the paper against your model β€” if yours is higher, weight tying might be missing.

---

## Complete Transformer Block

### Post-norm variant (original)
```python
class TransformerBlockPostNorm(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, n_heads, dropout)
        self.ff = FeedForward(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, mask=None):
        x = self.norm1(x + self.dropout1(self.attn(x, x, x, mask)))
        x = self.norm2(x + self.dropout2(self.ff(x)))
        return x
```

### Pre-norm variant (modern standard)
```python
class TransformerBlockPreNorm(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, n_heads, dropout)
        self.ff = FeedForward(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, mask=None):
        x = x + self.dropout1(self.attn(self.norm1(x), self.norm1(x), self.norm1(x), mask))
        x = x + self.dropout2(self.ff(self.norm2(x)))
        return x
```

**Key difference:** In pre-norm, LayerNorm is applied BEFORE each sublayer. The residual connection adds the UN-normalized input. This is more stable for training deep models.