Transformer Deep Dive

In 2017, Google published the paper "Attention Is All You Need", introducing the Transformer architecture. No one expected that this paper would completely change the AI field.

Today's GPT, Claude, Gemini, Llama... almost all mainstream large language models are essentially variants of the Transformer.

Understand the Transformer, and you understand 90% of modern LLMs.

In this article, we will dive into every component of the Transformer: self-attention, multi-head attention, positional encoding, feed-forward networks, layer normalization... covering not only "what" but also "why".

This is one of the most technically in-depth modules. We will use code to demonstrate the core computations, ensuring that you not only understand the formulas but can also implement them yourself.


Transformer Prequel: Limitations of RNN

Before the Transformer, sequence tasks (such as translation and text generation) were mainly handled by RNNs (Recurrent Neural Networks) and their variants LSTM and GRU.

How RNN Works

The core idea of RNN is "process one by one": the input sequence enters the network word by word, and the output at each step contains information from all previous words.

Example

# ============================================
# Simplified RNN forward propagation demonstration
# ============================================

import math


class SimpleRNN:
    """Simplified RNN implementation to demonstrate the principle"""

    def __init__(self, input_size: int, hidden_size: int):
        """Initialize RNN parameters"""
        import random
        random.seed(42)  # Set random seed for reproducibility

        # Weights from input to hidden layer
        self.Wx = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(input_size)]
        # Weights from hidden layer to hidden layer
        self.Wh = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(hidden_size)]
        # Bias
        self.b = [0.0 for _ in range(hidden_size)]

    def step(self, x: list, h_prev: list) -> list:
        """Single-step RNN: takes input x and previous hidden state h_prev, outputs new hidden state"""
        hidden_size = len(h_prev)
        h_new = [0.0 for _ in range(hidden_size)]

        # h_new = tanh(Wx·x + Wh·h_prev + b)
        for i in range(hidden_size):
            # Compute Wx·x
            wx_sum = sum(x[j] * self.Wx[j][i] for j in range(len(x)))
            # Compute Wh·h_prev
            wh_sum = sum(h_prev[j] * self.Wh[j][i] for j in range(hidden_size))
            # Add bias, pass through tanh
            h_new[i] = math.tanh(wx_sum + wh_sum + self.b[i])

        return h_new

    def forward(self, sequence: list) -> list:
        """Process the full sequence"""
        hidden_size = len(self.b)
        h = [0.0 for _ in range(hidden_size)]  # Initial hidden state
        hidden_states = []

        for x in sequence:
            h = self.step(x, h)
            hidden_states.append(h)

        return hidden_states


# Test: demonstrate with a simple vector sequence
input_size = 4
hidden_size = 3

rnn = SimpleRNN(input_size, hidden_size)

# Assume the input sequence is 4 words, each represented by a 4-dimensional vector
sequence = [
    [1.0, 0.0, 0.0, 0.0],  # Word 1
    [0.0, 1.0, 0.0, 0.0],  # Word 2
    [0.0, 0.0, 1.0, 0.0],  # Word 3
    [0.0, 0.0, 0.0, 1.0],  # Word 4
]

hidden_states = rnn.forward(sequence)

print("Example Simple RNN Demo")
print("=" * 40)
for i, h in enumerate(hidden_states):
    print(f"Hidden state at step {i+1}: {[f'{v:.4f}' for v in h]}")
# Output:
# Example Simple RNN Demo
# ========================================
# Hidden state at step 1: ['0.0204', '-0.0434', '0.0556']
# Hidden state at step 2: ['-0.0061', '-0.0529', '0.0775']
# Hidden state at step 3: ['0.0641', '-0.0409', '0.0655']
# Hidden state at step 4: ['0.0143', '-0.0805', '0.0277']

RNN seems reasonable, but it has three fatal problems:

Problem 1: Vanishing Gradients, Difficulty Remembering Long-Range Dependencies

RNN is a "chain" structure; the gradient at each step must backpropagate to the first step. After repeated multiplications, the gradient decays exponentially, becoming almost 0.

For example, in the sentence "I went to Paris in 2010, ... that was my favorite city" — "city" needs to refer to "Paris", but there are too many words in between, so RNNs struggle to learn such long-range dependencies.

LSTM and GRU alleviate this problem, but do not completely solve it.

Problem 2: Cannot Compute in Parallel, Slow Training

An RNN must wait for step t-1 to finish before it can compute step t. This means:

  • Even with 100 GPUs, you can only compute one word at a time
  • The longer the sequence, the longer the training time
  • Difficult to scale to very large datasets

Problem 3: Earlier Information Is Easily "Overwritten"

The hidden state of an RNN is updated step by step, and later information keeps overwriting earlier information. Important information at the beginning of a sentence may be largely "diluted" by the end.

The emergence of Transformer solved all three problems at once.

FeatureRNN/LSTMTransformer
Computation methodSerial, step by stepParallel, compute all at once
Long-range dependenciesWeak, gradient vanishingStrong, direct connection at any distance
Positional informationNaturally sequentialRequires positional encoding
Training speedSlowFast (parallelizable)

Self-Attention Mechanism

Self-attention is the core of Transformer. Its idea is simple:Each word must "communicate" with all words in the sentence, see who is important to itself, and then do a weighted sum by importance.

First look at the overall architecture diagram:

Intuitive Understanding of Q, K, V

Self-attention uses three vectors to describe each word:

  • Q (Query)——"Who am I looking for?"—— which word this word wants to attend to
  • K (Key)——"Who am I?"—— this word's identity
  • V (Value)——"What do I have?"—— the actual content of this word

The computation process is:

  1. Each word uses its Q to "match" with the K of all words, obtaining attention scores
  2. Use Softmax to normalize the scores so they sum to 1
  3. Use the normalized scores to weight and sum the V of all words

The diagram makes it clearer:

自注意力 QKV 计算流程图

Attention Score Calculation Formula

The complete formula is:

Attention(Q, K, V) = softmax( Q·Kᵀ / √dₖ ) · V

Let's implement it step by step in pure Python:

Example

# ============================================
# Pure Python implementation of Self-Attention
# ============================================

import math


def softmax(x: list) -> list:
    """Compute Softmax: turn a set of numbers into a probability distribution summing to 1"""
    # Subtract the maximum value to prevent numerical overflow
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """Matrix multiplication: A is m×n, B is n×p, output is m×p"""
    m = len(A)
    n = len(B)
    p = len(B[0])

    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """Matrix transpose"""
    return list(map(list, zip(*matrix)))


class SelfAttention:
    """Pure Python implementation of self-attention layer"""

    def __init__(self, d_model: int, d_k: int):
        """
Initialize self-attention layer
d_model: input/output dimension
d_k: dimension of Q/K (usually d_k = d_model / num_heads)
        """

        import random
        random.seed(42)  # example: fixed random seed

        self.d_model = d_model
        self.d_k = d_k

        # Initialize the projection matrices for Q, K, V
        # In practice, these are trained
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]

    def forward(self, X: list) -> list:
        """
Self-attention forward propagation
X: input sequence, shape [seq_len, d_model]
Returns: output sequence, shape [seq_len, d_k]
        """

        seq_len = len(X)

        # Step 1: Compute Q, K, V
        # Q = X·Wq, K = X·Wk, V = X·Wv
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        print(" Q's shape:", len(Q), "×", len(Q[0]))
        print(" K's shape:", len(K), "×", len(K[0]))
        print(" V's shape:", len(V), "×", len(V[0]))

        # Step 2: Compute Attention Scores Q·Kᵀ
        K_transposed = transpose(K)
        scores = matrix_multiply(Q, K_transposed)

        print("\nRaw attention scores:")
        for i, row in enumerate(scores):
            print(fRow {i}: {[f'{v:.4f}' for v in row]})

        # Step 3: Scaling: divide by √d_k
        # This is to prevent Softmax from entering the gradient saturation region.
        scale_factor = math.sqrt(self.d_k)
        scores_scaled = [[v / scale_factor for v in row] for row in scores]

        print("\nScaled attention scores:)
        for i, row in enumerate(scores_scaled):
            print(fRow {i}: {[f'{v:.4f}' for v in row]})

        # Step 4: Softmax normalization
        attention_weights = []
        for row in scores_scaled:
            attention_weights.append(softmax(row))

        print("\nAttention weights (after Softmax): ")
        for i, row in enumerate(attention_weights):
            print(fRow {i}: {[f'{v:.4f}' for v in row]})

        # Step 5: Weighted sum V
        output = matrix_multiply(attention_weights, V)

        return output, attention_weights


# ============================================
# Demo: run self-attention with a simple example
# ============================================

# Suppose we have a 4-word sentence, each word represented by an 8-dimensional vector
# For demonstration, we use one-hot + small random noise
d_model = 8
d_k = 6

# Input sequence: 4 words, each 8-dimensional
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Word 0
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Word 1
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Word 2
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],  # Word 3
]

print("=" * 50)
print("Example Self-Attention Demo")
print("=" * 50)
print(f"Input sequence length: {len(X)}")
print(f"Input dimension: {len(X)
print()

attn = SelfAttention(d_model=d_model, d_k=d_k)
output, attention_weights = attn.forward(X)

print("\n" + "=" * 50)
print("Self-attention output:")
print("=" * 50)
for i, row in enumerate(output):
    print(f"Output of word {i}: {[f'{v:.4f}' for v in row]}")

Run this code and you will see the complete computation process: Q/K/V generation, attention score calculation, scaling, Softmax, and finally the weighted output.

Why Divide by √dₖ?

This is an important detail. Assuming each element of Q and K is a random variable with mean 0 and variance 1, the variance of their dot product is dₖ.

If dₖ is large (e.g., 512), the dot product values become very large, causing Softmax to enter the "saturation region"—the gradient is nearly 0, and training cannot proceed.

After dividing by √dₖ, the variance returns to 1, and the Softmax gradient becomes healthy again.

Scaled dot-product attention is one of the key techniques for stable Transformer training.


Multi-Head Attention

Single-head self-attention is powerful, but it has one problem: each word can only "attend" once. What if we want a word to attend to multiple different aspects at the same time?

For example, in the sentence "In the zoo, the tiger is chasing the rabbit":

  • "Tiger" might need one head to attend to "zoo" (location)
  • Another head to attend to "rabbit" (object of action)
  • The third head focuses on "chasing" (action).

Multi-head attention splits Q/K/V into multiple "heads", each learning a different attention pattern, and finally concatenates them.

Steps to Compute Multi-Head Attention

  1. Split Q/K/V into h parts along the last dimension (h is the number of heads).
  2. Each head independently performs self-attention
  3. Concatenate the outputs of all heads
  4. Pass through a linear projection layer to get the final output

The formula is:

MultiHead(Q, K, V) = Concat(head₁, head₂, ..., headₕ) · Wₒ
  其中 headᵢ = Attention(Q·Wqᵢ, K·Wkᵢ, V·Wvᵢ)

Example

# ============================================
# Pure Python implementation of Multi-Head Attention
# ============================================

import math
import random
random.seed(42)  # example: fixed random seed


def softmax(x: list) -> list:
    """Compute Softmax"""
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """Matrix multiplication"""
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """Matrix transpose"""
    return list(map(list, zip(*matrix)))


class MultiHeadAttention:
    """Pure Python implementation of the multi-head attention layer"""

    def __init__(self, d_model: int, num_heads: int):
        """
Initialize multi-head attention
d_model: model dimension (must be divisible by num_heads)
num_heads: number of attention heads
        """

        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # dimension of each head

        # initialize projection matrices
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wo = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def split_heads(self, X: list) -> list:
        """
Split the last dimension into num_heads heads
Input shape: [seq_len, d_model]
Output shape: [num_heads, seq_len, d_k]
        """

        seq_len = len(X)
        # Reshape: [seq_len, num_heads, d_k]
        reshaped = []
        for row in X:
            head_rows = []
            for h in range(self.num_heads):
                start = h * self.d_k
                end = start + self.d_k
                head_rows.append(row[start:end])
            reshaped.append(head_rows)
        # Transpose: [num_heads, seq_len, d_k]
        result = []
        for h in range(self.num_heads):
            head_data = [reshaped[i][h] for i in range(seq_len)]
            result.append(head_data)
        return result

    def combine_heads(self, heads: list) -> list:
        """
Concatenate the heads back together
Input shape: [num_heads, seq_len, d_k]
Output shape: [seq_len, d_model]
        """

        seq_len = len(heads[0])
        result = []
        for i in range(seq_len):
            combined = []
            for h in range(self.num_heads):
                combined.extend(heads[h][i])
            result.append(combined)
        return result

    def attention_single_head(self, Q: list, K: list, V: list) -> list:
        """Single-head self-attention"""
        # Q·K^T
        scores = matrix_multiply(Q, transpose(K))
        # Scaling
        scale = math.sqrt(self.d_k)
        scores_scaled = [[v / scale for v in row] for row in scores]
        # Softmax
        weights = [softmax(row) for row in scores_scaled]
        # Weighted sum of V
        output = matrix_multiply(weights, V)
        return output, weights

    def forward(self, X: list) -> list:
        """Multi-head attention forward pass"""
        seq_len = len(X)

        # Step 1: Compute Q, K, V
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        # Step 2: Split into multiple heads
        Q_heads = self.split_heads(Q)
        K_heads = self.split_heads(K)
        V_heads = self.split_heads(V)

        # Step 3: Each head performs self-attention independently
        output_heads = []
        all_weights = []
        for h in range(self.num_heads):
            out, weights = self.attention_single_head(
                Q_heads[h], K_heads[h], V_heads[h]
            )
            output_heads.append(out)
            all_weights.append(weights)

        # Step 4: Concatenate all heads
        combined = self.combine_heads(output_heads)

        # Step 5: Final linear projection
        output = matrix_multiply(combined, self.Wo)

        return output, all_weights


# ============================================
# Demonstrate multi-head attention
# ============================================

d_model = 12
num_heads = 3  # 12 / 3 = 4 dimensions per head

# Input sequence: 4 tokens, each with 12 dimensions
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]

print("=" * 50)
print("Example Multi-Head Attention Demo")
print("=" * 50)
print(f"d_model = {d_model}, num_heads = {num_heads}")
print(f"Dimension per head: d_k = {d_model // num_heads}")
print()

mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
output, all_weights = mha.forward(X)

print("\n"Attention weights for each head:")
for h in range(num_heads):
    print(f"\n"Head {h}:")
    for i, row in enumerate(all_weights[h]):
        print(f" Position {i}: {[f'{v:.4f}' for v in row]}")

print("\n" + "=" * 50)
print("Multi-head attention final output shape:", len(output), "×", len(output[0]))
print("=" * 50)
for i, row in enumerate(output):
    print(f"Position {i}: {[f'{v:.4f}' for v in row[:6]]}...")

The power of multi-head attention lies in:Different heads automatically learn different types of attention patterns.

For example, in translation tasks:

  • Some heads focus on syntactic relationships (subject-verb-object)
  • Some heads focus on coreference (which noun a pronoun refers to)
  • Some heads focus on local neighboring words

These patterns are not manually designed; they are automatically learned by the model during training.


Positional Encoding

Self-attention has a problem: it is a "bag of words" model—it does not care about word order. "I love you" and "You love me" may look the same to self-attention.

But order is crucial to language. We need to "inject" positional information into the model.

Sinusoidal Positional Encoding

The original Transformer uses sinusoidal positional encoding:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Where:

  • pos is the position (0, 1, 2, ...)
  • i is the dimension index
  • d_model is the model dimension

This design is ingenious:

  • Each position has a unique encoding
  • Relative positions can be represented by linear combinations (sin(a+b) = sin a cos b + cos a sin b)
  • It can extrapolate to longer sequences than those seen during training

Example

# ============================================
# Implement sinusoidal positional encoding
# ============================================

import math


def sinusoidal_positional_encoding(seq_len: int, d_model: int) -> list:
    """
Compute sinusoidal positional encoding
Returns: positional encoding matrix of shape [seq_len, d_model]
    """

    pe = [[0.0 for _ in range(d_model)] for _ in range(seq_len)]

    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            # Use sin for even dimensions
            div_term = math.pow(10000, 2 * i / d_model)
            pe[pos][i] = math.sin(pos / div_term)

            # Use cos for odd dimensions (if the next dimension exists)
            if i + 1 < d_model:
                pe[pos][i + 1] = math.cos(pos / div_term)

    return pe


# Demo: generate positional encoding
seq_len = 10
d_model = 8

pe = sinusoidal_positional_encoding(seq_len, d_model)

print("=" * 50)
print("Example Sinusoidal Positional Encoding Demo")
print("=" * 50)
print(f"Sequence length: {seq_len}, Dimension: {d_model}")
print()
for pos in range(seq_len):
    print(f"Position {pos:2d}: {[f'{v:.4f}' for v in pe[pos]]}")

# Examine the relationship of relative positions
print("\n" + "=" * 50)
print("Verify the linear relationship of relative positions")
print("=" * 50)
print("Encoding difference between position 0 and position 1:")
diff = [pe[1][i] - pe[0][i] for i in range(d_model)]
print([f'{v:.4f}' for v in diff])

RoPE: Rotary Positional Encoding

Although sinusoidal positional encoding is classic, better solutions emerged later. The current mainstream large models (such as LLaMA) useRoPE(Rotary Positional Embedding)。

The idea of RoPE is:Rotate position information into Query and Key.

Specifically, rotate each pair of dimensions of Q and K:

[ q₁, q₂, q₃, q₄, ... ]
  ↓ 旋转
[ q₁·cos mθ - q₂·sin mθ,
  q₁·sin mθ + q₂·cos mθ,
  q₃·cos mθ' - q₄·sin mθ',
  q₃·sin mθ' + q₄·cos mθ',
  ... ]

Advantages of RoPE:

  • Naturally contains relative position information
  • Good extrapolation (works for lengths beyond training)
  • Usually performs better than sinusoidal positional encoding

ALiBi: Decaying Attention by Distance

Another simple and effective approach isALiBi(Attention with Linear Biases)。

It does not add positional encoding, but directly adds a bias to the attention scores: the farther apart, the lower the score.

The advantage of ALiBi is its simplicity, and it is especially friendly to length extrapolation.

Positional Encoding MethodsPrincipleAdvantagesRepresentative Models
Sinusoidal positional encodingsin/cos functionsExtrapolatable, simple computationOriginal Transformer, BERT
Learnable positional encodingDirectly train an embeddingStrong expressive powerGPT-1/2
RoPERotate Q/KRelative positions, good performanceLLaMA, PaLM
ALiBiAdd bias to attention scoresSimple, good for long sequencesMPT, BLOOM

Feed-Forward Network (FFN) Layer

In each Transformer layer, after attention, there is also a feed-forward network.

Structure of FFN

The FFN of the original Transformer is:

FFN(x) = max(0, x·W₁ + b₁) · W₂ + b₂

That is: linear layer → ReLU activation → linear layer.

Usually the intermediate dimension is larger than the input dimension (e.g., d_model=512, intermediate dimension=2048).

ReLU vs SwiGLU

The original used ReLU, but now the mainstream isSwiGLU:

SwiGLU(x) = (x·W₁ ⊗ sigmoid(x·V₁)) · W₂

where ⊗ is element-wise multiplication.

SwiGLU performs better than ReLU on many tasks; LLaMA, PaLM, etc., all use it.

Why is FFN important?

  • Attention is "aggregating information", FFN is "processing information"
  • FFN has a large number of parameters, and is the main place where the model "memorizes" knowledge
  • Without FFN, the expressive capability of Transformer would be greatly reduced

Example

# ============================================
# Implement FFN: ReLU version and SwiGLU version
# ============================================

import math
import random
random.seed(42)  # example


def relu(x: float) -> float:
    """ReLU activation function: max(0, x)"""
    return max(0.0, x)


def sigmoid(x: float) -> float:
    """Sigmoid activation function"""
    if x < -50:  # Prevent numerical overflow
        return 0.0
    return 1.0 / (1.0 + math.exp(-x))


def linear(x: list, W: list, b: list) -> list:
    """Linear layer: y = x·W + b"""
    output_size = len(b)
    output = [0.0 for _ in range(output_size)]

    for i in range(output_size):
        sum_wx = sum(x[j] * W[j][i] for j in range(len(x)))
        output[i] = sum_wx + b[i]

    return output


class FFNReLU:
    """ReLU version FFN"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # Randomly initialize parameters (obtained by training in practice)
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.b1 = [0.0 for _ in range(d_ff)]

        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]
        self.b2 = [0.0 for _ in range(d_model)]

    def forward(self, x: list) -> list:
        """Forward pass"""
        # First layer: linear + ReLU
        hidden = linear(x, self.W1, self.b1)
        hidden = [relu(v) for v in hidden]

        # Second layer: linear
        output = linear(hidden, self.W2, self.b2)
        return output


class FFNSwiGLU:
    """SwiGLU version FFN (a more modern choice)"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # SwiGLU needs two linear layers for gating, plus an output layer
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.V1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]

    def forward(self, x: list) -> list:
        """Forward pass: SwiGLU"""
        # Compute two linear transformations
        x1 = linear(x, self.W1, [0.0 for _ in range(self.d_ff)])
        x2 = linear(x, self.V1, [0.0 for _ in range(self.d_ff)])

        # SwiGLU:x1 * sigmoid(x2)
        hidden = [a * sigmoid(b) for a, b in zip(x1, x2)]

        # Output layer
        output = linear(hidden, self.W2, [0.0 for _ in range(self.d_model)])
        return output


# Demonstrate FFN
d_model = 8
d_ff = 16

# Test input
x = [0.1, -0.2, 0.3, -0.4, 0.5, -0.6, 0.7, -0.8]

print("=" * 50)
print("Example FFN demo")
print("=" * 50)
print(f"Input: {[f'{v:.4f}' for v in x]}")

ffn_relu = FFNReLU(d_model, d_ff)
output_relu = ffn_relu.forward(x)
print(f"\n"ReLU FFN output: {[f'{v:.4f}' for v in output_relu]}")

ffn_swiglu = FFNSwiGLU(d_model, d_ff)
output_swiglu = ffn_swiglu.forward(x)
print(f"SwiGLU FFN output: {[f'{v:.4f}' for v in output_swiglu]}")

Layer Normalization (Layer Norm)

There is another important component in Transformer: Layer Normalization.

What Layer Normalization Does

Layer normalization normalizes the features of each sample to mean 0 and variance 1:

LN(x) = γ ⊗ ((x - μ) / √σ²) + β

where:

  • μ is the mean of x, σ² is the variance of x
  • γ (scale) and β (shift) are learnable parameters
  • ⊗ is element-wise multiplication

Example

# ============================================
# Implement Layer Normalization
# ============================================

import math


def layer_norm(x: list, gamma: list, beta: list, eps: float = 1e-6) -> list:
    """
Layer Normalization
x: input vector
gamma: scaling parameter
beta: offset parameter
eps: small constant to prevent division by zero
    """

    # Compute mean
    mean = sum(x) / len(x)

    # Compute variance
    var = sum((v - mean) ** 2 for v in x) / len(x)

    # Normalization + scale and shift
    normalized = []
    for xi, gi, bi in zip(x, gamma, beta):
        normalized.append(gi * (xi - mean) / math.sqrt(var + eps) + bi)

    return normalized


# Demonstrate layer normalization
d_model = 6

# Input vector
x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]

# gamma initialized to 1, beta initialized to 0
gamma = [1.0 for _ in range(d_model)]
beta = [0.0 for _ in range(d_model)]

print("=" * 50)
print("Example Layer Normalization Demo")
print("=" * 50)
print(f"Input: {[f'{v:.4f}' for v in x]}")

output = layer_norm(x, gamma, beta)
print(f"Output: {[f'{v:.4f}' for v in output]}")

# Verify mean and variance
mean_out = sum(output) / len(output)
var_out = sum((v - mean_out) ** 2 for v in output) / len(output)
print(f"\n"Output mean: {mean_out:.4f}")
print(f"Output variance: {var_out:.4f}")

Pre-Norm vs Post-Norm

Layer normalization has two placement methods:

  • Post-Norm: sublayer → residual → normalization (original Transformer)
  • Pre-Norm: normalization → sublayer → residual (now mainstream)

Pre-Norm training is more stable, and almost all large models use it now.

RMS Norm

RMS NormIt is a simplified version of layer normalization, only calculating root mean square, without subtracting the mean:

RMS(x) = sqrt(mean(x²))
RMSNorm(x) = γ ⊗ (x / RMS(x))

RMS Norm is faster to compute, with similar results; models like LLaMA use it.


Encoder-Decoder vs Decoder-Only Architecture

The original Transformer is an Encoder-Decoder architecture, but most large language models today are Decoder-Only.

Encoder-Decoder: Used for Tasks Like Translation

Encoder-Decoder architecture:

  • Encoder: reads the input sequence (e.g., source language sentence) and generates a context representation
  • Decoder: generates the target sequence (e.g., translation result) based on the Encoder's output

There are two types of attention in the Decoder:

  1. Masked self-attention: can only look at previous words (cannot see the future)
  2. Cross-attention: to attend to the Encoder's output

Suitable tasks: machine translation, speech recognition, summarization (requires "rewriting" input into output).

Decoder-Only: GPT Series, Used for Generation

The Decoder-Only architecture has only a Decoder, no Encoder. Its characteristics:

  • Only masked self-attention (causal attention)
  • Autoregressive generation: outputs one word at a time
  • Suitable for the pretraining + fine-tuning paradigm

Current GPT, LLaMA, Claude, Gemini... are almost all Decoder-Only.

Encoder-Only: BERT Series, Used for Understanding

The Encoder-Only architecture has only an Encoder. Its characteristics:

  • Bidirectional attention: each word can see all words
  • Suitable for understanding tasks: classification, named entity recognition, extractive question answering
  • Pretrained with "Masked Language Model" (MLM)
ArchitectureComponentAttention typeRepresentative modelApplicable tasks
Encoder-DecoderEncoder + DecoderBidirectional + masked + crossT5, BARTTranslation, summarization
Decoder-OnlyDecoderMasked (causal)GPT, LLaMA, ClaudeText generation
Encoder-OnlyEncoderBidirectionalBERT, RoBERTaClassification, NER

Why did Decoder-Only become mainstream? Because it is simple, and under large-scale pretraining, its generation ability is extremely strong. "One size fits all" actually works better than "specialized design".


Flash Attention: Making Attention Computation Fast and Memory-Efficient

The time and memory complexity of standard self-attention are both O(n²); when the sequence gets long (e.g., n=8192), it is slow and GPU memory is insufficient.

Flash AttentionIt is a key optimization; its emergence in 2022 made long-sequence training feasible.

Memory Bottleneck of Standard Attention

Standard self-attention needs to save intermediate results (attention weight matrices) for backpropagation. For n=8192, this is 8192×8192 ≈ 67 million elements, which alone takes up tens of GB of GPU memory.

Optimization Principles of Flash Attention

Core idea of Flash Attention:

  1. Block-wise computation: Split Q/K/V into small blocks and compute block by block
  2. Online Softmax: Don't save the full attention matrix; use mathematical tricks to recompute it during backpropagation
  3. Leverage SRAM: Put the blocks into the GPU's high-speed cache (SRAM) to reduce HBM access

The result:

  • Memory complexity drops from O(n²) to O(n)
  • 2-4x faster
  • Identical precision (mathematically equivalent)

Almost all large model training now uses Flash Attention or its variants (Flash Attention v2, Memory-Efficient Attention, etc.).


KV Cache: Principles of Inference Acceleration

Once training optimization is done, inference (generation) also needs optimization.KV CacheIt is the key to inference acceleration.

Problem: Repetitive Computation in Autoregressive Generation

During autoregressive generation, every time a new token is generated, the K and V of all previous tokens must be recomputed — this is very wasteful.

For example, when generating the 100th token, the K/V of the first 99 tokens has already been computed and does not need to be recomputed.

How KV Cache Works

The idea of KV Cache is simple:

  • Cache the K and V of all previous positions
  • When generating a new token, only compute the new token's K/V and append it to the cache
  • Use the complete K/V for attention

This way, computation per step drops from O(n²) to O(n), greatly improving generation speed.

Example

# ============================================
# Demonstrate the principle of KV Cache
# ============================================

import math
import random
random.seed(42)


def softmax(x):
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matmul(A, B):
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix):
    return list(map(list, zip(*matrix)))


class SimpleAttentionWithKVCache:
    """Simple self-attention with KV Cache"""

    def __init__(self, d_model: int):
        self.d_model = d_model
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def forward(self, x_new: list, kv_cache: tuple = None) -> tuple:
        """
One-step generation with KV Cache
x_new: current new input (single position)
kv_cache: previous K and V, (K_cache, V_cache)
Returns: (output, new kv_cache)
        """

        # Compute Q, K, V for the current position
        q = matmul([x_new], self.Wq)
        k = matmul([x_new], self.Wk)
        v = matmul([x_new], self.Wv)

        # Update KV Cache
        if kv_cache is None:
            K_full = k
            V_full = v
        else:
            K_cache, V_cache = kv_cache
            K_full = K_cache + k
            V_full = V_cache + v

        # Attention: only use the current position's Q
        scores = matmul(q, transpose(K_full))
        scale = math.sqrt(self.d_model)
        scores_scaled = [[v / scale for v in row] for row in scores]
        weights = [softmax(row) for row in scores_scaled]
        output = matmul(weights, V_full)

        return output[0], (K_full, V_full)


# Demonstrate the KV Cache generation process
d_model = 4

# Simulate 4 steps of generation
inputs = [
    [1.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 1.0],
]

attn = SimpleAttentionWithKVCache(d_model)

print("=" * 50)
print("Example KV Cache demonstration")
print("=" * 50)

kv_cache = None
for step, x in enumerate(inputs):
    output, kv_cache = attn.forward(x, kv_cache)
    K_cache, V_cache = kv_cache
    print(f"\n"Step {step}:")
    print(f" KV Cache size: {len(K_cache)} positions")
    print(f" Output: {[f'{v:.4f}' for v in output]}")

print("\n" + "=" * 50)
print("Note: each step only computes the K/V of the current position,")
print("then appends it to the Cache, no need to recompute the previous ones!")
print("=" * 50)

KV Cache has a small cost: it requires extra GPU memory to store K and V. For a 7B model with a 4k context, KV Cache takes about 1-2GB of GPU memory — this is worth it.

There are also more advanced optimizations:

  • Multi-Query Attention: Multiple heads share K/V
  • Grouped Query Attention: Grouped sharing of K/V

These further reduce memory usage and improve inference speed.


Summary: Complete Transformer Pipeline

Let's put all the components together and summarize the complete Transformer flow.

One Decoder-Only Transformer Layer

x → LayerNorm → Masked Multi-Head Attention → 残差连接 →
    → LayerNorm → FFN → 残差连接 → 输出

Complete Decoder-Only Model

  1. Input token IDs → word embedding
  2. Add positional encoding
  3. Pass through N Transformer layers
  4. Final LayerNorm
  5. Linear layer mapping to vocabulary size
  6. Softmax to get the probability of the next token

Key Hyperparameters

HyperparameterMeaningGPT-2 (124M)LLaMA-7BGPT-3 (175B)
d_modelModel dimension768409612288
num_headsNumber of attention heads123296
num_layersNumber of layers123296
d_ffFFN intermediate dimension30721100849152
context_lenContext length10242048/40962048

Code Implementation Guide

If you want to implement a simplified Transformer from scratch, the steps are:

  1. Implement layer normalization
  2. Implement self-attention (with masking)
  3. Implement multi-head attention
  4. Implement FFN
  5. Implement Transformer layer
  6. Implement positional encoding
  7. Assemble the complete model
  8. Implement the training loop

In practice, we use frameworks like PyTorch rather than pure Python. The pure Python implementation here is for demonstrating the principles and helping you understand what each step does.

The best way to understand Transformer is to implement it yourself. Even a simplified version will give you a fresh perspective on many details.

Other extensions