Transformers & Self-Attention Mechanisms
The Transformer architecture powers modern Large Language Models (LLMs), Vision Transformers (ViT), and multimodal AI. By replacing sequential recurrence with Self-Attention, Transformers achieve $O(1)$ path lengths between distant tokens and massive GPU parallelization. In this chapter, master the attention mathematics, modern FlashAttention integration, and build a complete decoder block.
1. Scaled Dot-Product Attention Mechanics
Given an input sequence mapped to Query (Q), Key (K), and Value (V) matrices of dimension d_k, the attention weights represent the similarity of each token to every other token:
Measures compatibility between token queries and key representations. Produces an (N × N) pairwise attention matrix.
As d_k grows large, dot products grow large in magnitude, pushing softmax into regions with vanishing gradients. Dividing by √d_k preserves stable variance.
Softmax normalizes attention rows into a probability distribution summing to 1.0, which computes a convex combination of values $V$.
2. Accelerated Attention: F.scaled_dot_product_attention
Standard naive self-attention materializes the full $N \times N$ attention matrix in GPU High-Bandwidth Memory (HBM), consuming $O(N^2)$ memory and creating an I/O memory bottleneck. PyTorch 2.0+ integrates FlashAttention-2 directly intotorch.nn.functional.scaled_dot_product_attention (SDPA):
import torch
import torch.nn.functional as F
# Batch=4, Heads=8, Sequence Length=1024, Head Dimension=64
B, H, S, D = 4, 8, 1024, 64
q = torch.randn(B, H, S, D, device="cuda", dtype=torch.float16)
k = torch.randn(B, H, S, D, device="cuda", dtype=torch.float16)
v = torch.randn(B, H, S, D, device="cuda", dtype=torch.float16)
# Hardware-accelerated FlashAttention-2 with fused CUDA kernels
# is_causal=True automatically constructs and applies the autoregressive triangular mask!
with torch.backends.cuda.sdp_kernel(enable_flash=True, enable_math=False):
output = F.scaled_dot_product_attention(q, k, v, is_causal=True)
print(f"SDPA Output Shape: {output.shape}") # torch.Size([4, 8, 1024, 64])Why is FlashAttention 3x-5x Faster?
FlashAttention tiles the attention computation into SRAM blocks on the GPU streaming multiprocessor, computing Softmax online without ever writing the massive intermediate $N \times N$ attention matrix to main HBM memory!
3. Building a Multi-Head Attention Layer from Scratch
Here is a pure PyTorch implementation of Multi-Head Attention showing projection splits, head permutations, and causal masking:
import torch
import torch.nn as nn
import math
class CausalSelfAttention(nn.Module):
def __init__(self, embed_dim: int, num_heads: int):
super().__init__()
assert embed_dim % num_heads == 0, "embed_dim must be divisible by num_heads"
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
# Key, Query, Value projected in a single efficient matrix
self.qkv_proj = nn.Linear(embed_dim, 3 * embed_dim, bias=False)
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
B, S, C = x.shape # Batch, Sequence Length, Channels (Embedding Dim)
# 1. Project Q, K, V in one GEMM: [B, S, 3 * C]
qkv = self.qkv_proj(x)
q, k, v = qkv.chunk(3, dim=-1)
# 2. Reshape into [B, Num_Heads, S, Head_Dim]
q = q.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
# 3. Fast Causal Scaled Dot-Product Attention
attn_out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
# 4. Concatenate heads back: [B, S, C]
attn_out = attn_out.transpose(1, 2).contiguous().view(B, S, C)
# 5. Output projection
return self.out_proj(attn_out)4. Assembling a Modern GPT Transformer Block
Modern architectures (LLaMA, GPT-4, Mistral) employ Pre-LayerNorm (normalizing inputs before attention and MLP layers) for vastly superior training stability compared to original Post-LN Transformers:
class TransformerBlock(nn.Module):
"""
Standard Pre-LN Autoregressive Transformer Decoder Block.
"""
def __init__(self, embed_dim: int, num_heads: int, mlp_ratio: int = 4):
super().__init__()
self.ln1 = nn.LayerNorm(embed_dim)
self.attn = CausalSelfAttention(embed_dim, num_heads)
self.ln2 = nn.LayerNorm(embed_dim)
# Position-wise Feedforward Network with GELU activation
self.mlp = nn.Sequential(
nn.Linear(embed_dim, mlp_ratio * embed_dim),
nn.GELU(),
nn.Linear(mlp_ratio * embed_dim, embed_dim)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Pre-LN Residual Attention Stream
x = x + self.attn(self.ln1(x))
# Pre-LN Residual MLP Stream
x = x + self.mlp(self.ln2(x))
return x
# Instantiate & Verify Block
block = TransformerBlock(embed_dim=256, num_heads=4)
sample_tokens = torch.randn(2, 64, 256) # Batch=2, Seq=64, Dim=256
block_output = block(sample_tokens)
print(f"Transformer Block Output Shape: {block_output.shape}") # [2, 64, 256]Model Checkpointing & Serialization
Continue to the next hands-on deep learning guide in this PyTorch series.
Explore Related Tools
Hand-picked utilities and calculators related to this tool.
