Skip to content
AILinkDeepTech
Go back
Reinforcement Learning Medium

The Transformer Architecture: Scaled Dot-Product Attention, Multi-Head Projections, and PyTorch Engine

Abstract

Master the Transformer: scaled dot-product attention, multi-head projections, sinusoidal & RoPE encodings, Pre-LN vs Post-LN stability, and PyTorch engines.

Parallel Sequence Processing and Constant Path Lengths

Prior to the Transformer (Vaswani et al., NeurIPS 2017), sequence modeling relied on Recurrent Neural Networks (LSTM, GRU) and temporal convolutions (WaveNet, ConvS2S). Recurrent architectures enforce sequential dependency constraints: computing hidden state requires all preceding states, creating an unparallelizable temporal bottleneck and vanishing/exploding gradients over long sequence horizons.

The Transformer eliminates recurrence and convolutions entirely, establishing Self-Attention as the sole sequence-mixing primitive. Every token in a sequence interacts directly with every other token in operational path length via parallel matrix multiplications:

This formulation delivers two decisive advantages:

  1. Full Parallelism: Forward and backward passes process sequence length concurrently across GPU Tensor Cores.
  2. Global Constant-Distance Receptive Field: Information propagates between arbitrary token pairs in a single layer, eliminating exponential signal decay.

Sequence Modeling Paradigms Comparison

ParadigmSequential Path LengthCompute Complexity per LayerParallel TrainingReceptive Field ExpansionDominant Modern Architecture
Recurrent (LSTM / GRU)Impossible ( steps)Linear with Steps ()Legacy NLP / Embedded Edge
1D Convolution (ConvS2S)Full ( steps)Logarithmic with DepthAudio Encoders (Wav2Vec2)
Self-Attention (Transformer)Full ( steps)Instant Global ( Layer)LLaMA, GPT-4, DeepSeek, ViT
Linear State-Space (Mamba-2) (Chunked)Full (via Associative Scan)Continuous Hidden StateHybrid Transformers (Jamba)
Linear Attention (RWKV-7)Full (via Parallel Scan)Exponential Decay StateRWKV-v7 / FlashLinear

Mathematical Foundations

flowchart TD INPUT["Input Sequence Tokens X in R^(N x d_model)"] --> EMB["Token Embedding + Positional Encoding (PE)"] EMB --> PRE_NORM["Layer Normalization / RMSNorm(X)"] subgraph MHA_BLOCK["Multi-Head Attention Sub-Layer"] PROJ_Q["Q = X * W_Q in R^(N x d_model)"] PROJ_K["K = X * W_K in R^(N x d_model)"] PROJ_V["V = X * W_V in R^(N x d_model)"] SPLIT["Reshape into h Heads: (B, h, N, d_k)"] SCORES["Attention Scores: S = (Q * K^T) / sqrt(d_k) + M"] SOFTMAX["Softmax over Key Dimension: A = softmax(S)"] CONTEXT["Context Accumulation: Head_out = A * V"] CONCAT["Concat Heads & Linear Projection: Out = Concat(Heads) * W_O"] PROJ_Q --> SPLIT PROJ_K --> SPLIT PROJ_V --> SPLIT SPLIT --> SCORES --> SOFTMAX --> CONTEXT --> CONCAT end PRE_NORM --> PROJ_Q PRE_NORM --> PROJ_K PRE_NORM --> PROJ_V CONCAT --> RES1["Residual Addition: X + Out"] RES1 --> FFN_NORM["Layer Normalization / RMSNorm"] subgraph FFN_BLOCK["Feed-Forward Network (FFN / SwiGLU)"] UP["Up-Projection / Gate Projections"] ACT["Activation: ReLU / SiLU / GeLU"] DOWN["Down-Projection: W_2 / W_down"] UP --> ACT --> DOWN end FFN_NORM --> UP DOWN --> RES2["Residual Addition: Out + FFN_Out"] RES2 --> NEXT["Next Transformer Block / Output Head"]

Figure 1: Standard Transformer Block dataflow featuring Scaled Dot-Product Multi-Head Attention, residual connections, and Feed-Forward projections.

1. Scaled Dot-Product Attention and Variance Normalization

Given query matrix , key matrix , and value matrix :

The Scaling Factor Variance Proof:

Assume the elements of query vector and key vector are independent and identically distributed (i.i.d.) random variables with mean and variance .

The raw dot product evaluates to . Its expectation and variance evaluate to:

For large head dimensions (e.g., ), the standard deviation of raw scores is . Passing large values into the function pushes activations into saturated regions where gradients vanish exponentially:

Dividing by normalizes the variance back to unity:


2. Multi-Head Attention Subspace Decomposition

Rather than computing a single attention function over dimension , Multi-Head Attention (MHA) projects queries, keys, and values into independent low-dimensional representation subspaces ():

where projection parameters are , , , and output projection .

The parameter count for standard MHA is invariant to head count :


3. Position-Wise Feed-Forward Networks (FFN & SwiGLU)

The attention layer acts as a spatial token mixer, while the position-wise Feed-Forward Network (FFN) acts as a per-token channel mixer:

1. Standard ReLU FFN (Vaswani et al.):

2. SwiGLU Gated Feed-Forward Network (Shazeer, 2020; LLaMA, DeepSeek):

where , with intermediate hidden dimension sized to .


4. Residual Connection and Normalization Dynamics

Post-LN (Original Transformer):

In Post-LN, gradients passing through the main residual branch are recursively scaled by , leading to severe gradient vanishing/explosion in deep networks without strict learning rate warmup.

Pre-LN & RMSNorm (Modern LLMs):

In Pre-LN, the identity branch provides an unhindered gradient highway , allowing training of 100+ layer architectures with zero warm-up instability.


5. Positional Encoding Geometry

Because self-attention is permutation-equivariant ( for any permutation matrix ), order information must be injected explicitly.

Sinusoidal Positional Encoding (Vaswani et al.):

By trigonometric identity , any linear shift represents a linear transformation of :

where is a block-diagonal planar rotation matrix.


Production PyTorch Implementation

Below is a complete, modular PyTorch implementation of a Pre-LN Transformer Decoder Engine with Grouped-Query Attention (GQA), SwiGLU activations, and KV-cache support.

Step 1: Pre-LN Multi-Head / Grouped-Query Attention Layer

from __future__ import annotations

import math
import torch
import torch.nn as nn
import torch.nn.functional as F


class MultiHeadAttention(nn.Module):
    """Production Multi-Head Attention supporting GQA and causal masking."""

    def __init__(
        self,
        d_model: int = 4096,
        num_heads: int = 32,
        num_kv_heads: int = 8, # Grouped-Query Attention (GQA)
        head_dim: int = 128,
    ) -> None:
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.num_kv_heads = num_kv_heads
        self.head_dim = head_dim
        self.num_queries_per_kv = num_heads // num_kv_heads
        self.scale = 1.0 / math.sqrt(head_dim)

        self.q_proj = nn.Linear(d_model, num_heads * head_dim, bias=False)
        self.k_proj = nn.Linear(d_model, num_kv_heads * head_dim, bias=False)
        self.v_proj = nn.Linear(d_model, num_kv_heads * head_dim, bias=False)
        self.o_proj = nn.Linear(num_heads * head_dim, d_model, bias=False)

    def forward(
        self,
        x: torch.Tensor,
        kv_cache: tuple[torch.Tensor, torch.Tensor] | None = None,
        is_causal: bool = True,
    ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
        batch_size, seq_len, _ = x.shape

        # 1. Project Q, K, V
        q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(batch_size, seq_len, self.num_kv_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(batch_size, seq_len, self.num_kv_heads, self.head_dim).transpose(1, 2)

        # 2. KV-Cache Update for Autoregressive Decoding
        if kv_cache is not None:
            k_prev, v_prev = kv_cache
            k = torch.cat([k_prev, k], dim=-2)
            v = torch.cat([v_prev, v], dim=-2)
        current_kv = (k, v)

        # 3. GQA Key-Value Head Expansion
        if self.num_queries_per_kv > 1:
            k = k.repeat_interleave(self.num_queries_per_kv, dim=1)
            v = v.repeat_interleave(self.num_queries_per_kv, dim=1)

        # 4. Scaled Dot-Product Attention (Hardware-Fused via PyTorch SDPA)
        causal_mask = is_causal if (seq_len > 1 and kv_cache is None) else False
        out = F.scaled_dot_product_attention(
            q, k, v, scale=self.scale, is_causal=causal_mask
        )

        out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
        return self.o_proj(out), current_kv

Step 2: RMSNorm and SwiGLU Feed-Forward Block

from __future__ import annotations

import torch
import torch.nn as nn
import torch.nn.functional as F
from transformer_attention import MultiHeadAttention


class RMSNorm(nn.Module):
    """Root Mean Square Layer Normalization."""

    def __init__(self, dim: int, eps: float = 1e-6) -> None:
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        variance = x.pow(2).mean(-1, keepdim=True)
        return x * torch.rsqrt(variance + self.eps) * self.weight


class SwiGLUFFN(nn.Module):
    """SwiGLU Feed-Forward Block."""

    def __init__(self, d_model: int, d_ffn: int) -> None:
        super().__init__()
        self.gate_proj = nn.Linear(d_model, d_ffn, bias=False)
        self.up_proj = nn.Linear(d_model, d_ffn, bias=False)
        self.down_proj = nn.Linear(d_ffn, d_model, bias=False)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))


class TransformerBlock(nn.Module):
    """Standard Pre-LN Transformer Decoder Layer."""

    def __init__(self, d_model: int, num_heads: int, num_kv_heads: int, d_ffn: int) -> None:
        super().__init__()
        self.attn_norm = RMSNorm(d_model)
        self.attn = MultiHeadAttention(d_model, num_heads, num_kv_heads)
        self.ffn_norm = RMSNorm(d_model)
        self.ffn = SwiGLUFFN(d_model, d_ffn)

    def forward(
        self,
        x: torch.Tensor,
        kv_cache: tuple[torch.Tensor, torch.Tensor] | None = None,
    ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
        # Attention sub-layer with Pre-LN residual connection
        normed_x = self.attn_norm(x)
        attn_out, next_kv = self.attn(normed_x, kv_cache=kv_cache)
        x = x + attn_out

        # FFN sub-layer with Pre-LN residual connection
        x = x + self.ffn(self.ffn_norm(x))
        return x, next_kv

Step 3: End-to-End Decoder-Only Transformer Model

from __future__ import annotations

import torch
import torch.nn as nn
from transformer_layers import RMSNorm, TransformerBlock


class DecoderOnlyTransformer(nn.Module):
    """Complete Autoregressive Decoder-Only Transformer."""

    def __init__(
        self,
        vocab_size: int = 32000,
        d_model: int = 4096,
        num_layers: int = 32,
        num_heads: int = 32,
        num_kv_heads: int = 8,
        d_ffn: int = 11008,
    ) -> None:
        super().__init__()
        self.tok_embeddings = nn.Embedding(vocab_size, d_model)
        self.layers = nn.ModuleList(
            [TransformerBlock(d_model, num_heads, num_kv_heads, d_ffn) for _ in range(num_layers)]
        )
        self.norm = RMSNorm(d_model)
        self.output = nn.Linear(d_model, vocab_size, bias=False)

        # Tie input embeddings and output projection weights
        self.output.weight = self.tok_embeddings.weight

    def forward(
        self,
        input_ids: torch.Tensor,
        kv_caches: list[tuple[torch.Tensor, torch.Tensor]] | None = None,
    ) -> tuple[torch.Tensor, list[tuple[torch.Tensor, torch.Tensor]]]:
        x = self.tok_embeddings(input_ids)
        new_kv_caches = []

        for i, layer in enumerate(self.layers):
            cache_i = kv_caches[i] if kv_caches is not None else None
            x, next_kv = layer(x, kv_cache=cache_i)
            new_kv_caches.append(next_kv)

        logits = self.output(self.norm(x))
        return logits, new_kv_caches


if __name__ == "__main__":
    model = DecoderOnlyTransformer(vocab_size=1000, d_model=512, num_layers=4, num_heads=8, num_kv_heads=2, d_ffn=1376)
    tokens = torch.randint(0, 1000, (2, 16))
    logits, caches = model(tokens)
    print(f"Logits shape: {logits.shape} (Expected: [2, 16, 1000])")

Empirical Benchmark Evaluation

Architectural comparisons across benchmark model configurations:

ArchitectureModel ParametersLayersSequence LengthContext Path LengthArithmetic IntensityTraining Throughput (H100)
Vaswani Base (2017) (Enc-Dec)Low ()
BERT-Base (2018) (Enc-Only)Moderate
GPT-2 XL (2019) (Dec-Only)High
LLaMA-3 (8B) (GQA + SwiGLU)Very High ()
DeepSeek-V3 (671B) Active (MoE+MLA)Frontier ()

Troubleshooting Common Transformer Implementation Faults

1. Causal Attention Mask Upper-Triangular Off-by-One Bug

  • Symptom: Training loss drops to near-zero within tens of steps, but during autoregressive inference the model outputs repetitive degenerate nonsense.
  • Root Cause: Setting diagonal=1 in torch.triu(..., diagonal=1) allows token to attend to future token , leaking ground-truth labels during teacher-forcing.
  • Remedy: Ensure the causal diagonal allows position to attend to : mask = torch.triu(torch.full((S, S), float('-inf')), diagonal=1).

2. Saturated Softmax and Gradient Collapse

  • Symptom: Attention outputs become one-hot matrices with near-zero gradients on query/key projection layers early in training.
  • Root Cause: Omitting the scaling factor causes inner products to scale proportionally to , blowing up softmax logits.
  • Remedy: Always scale raw scores by prior to mask addition and softmax.

3. Floating-Point Underflow in Half-Precision Softmax

  • Symptom: Training in FP16 causes sudden loss divergence to NaN when sequence lengths exceed .
  • Root Cause: Masking with -1e9 or -inf causes numerical underflow/overflow in 16-bit half-precision exponentials: .
  • Remedy: Compute the softmax reduction in torch.float32 before casting back to torch.bfloat16 or torch.float16, or use PyTorch’s native F.scaled_dot_product_attention.

References

  1. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., & Polosukhin, I. (2017). Attention Is All You Need. Advances in Neural Information Processing Systems (NeurIPS 2017).
  2. Shazeer, N. (2020). GLU Variants Improve Transformer. arXiv:2002.05202.
  3. Zhang, B., & Sennrich, R. (2019). Root Mean Square Layer Normalization. NeurIPS 2019.
  4. Dao, T., Fu, D. Y., Ermon, S., Rudra, A., & Ré, C. (2022). FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. NeurIPS 2022.
  5. Ainslie, J., et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. EMNLP 2023.


Cite this Explanation

@article{ailinkdeeptech2025transformer,
  title={The Transformer Architecture: Scaled Dot-Product Attention, Multi-Head Projections, and PyTorch Engine},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2025},
  url={https://ailinkdeeptech.com/research/transformer}
}

Related Explanations