Qwen Architecture › Core Architectural Components

Michael BrenndoerferUpdated August 7, 202520 min read
Language AI HandbookAll chapters
Part 29 of 67Modern Decoder Models
Chapter 212Qwen Architecture › Core Architectural Components
Reading section 4 of 11 · Core Architectural Components · 20 minSection 4 of 11 · 20 min

Book contents

Section 4 of 11 · 20 min read

Qwen Architecture › Core Architectural ComponentsOne section · Sections in this chapter4 / 11 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

This section builds your understanding of Qwen's key innovations step by step. We'll start with the attention mechanism, where Qwen makes its most impactful efficiency choice, then explore the feed-forward network and positional encoding. For each component, we'll first build intuition about the problem being solved, then examine the mathematical formulation, and finally see it in code.

Grouped-Query Attention

To understand why Qwen uses Grouped-Query Attention (GQA), we first need to understand the memory bottleneck in transformer inference.

The main bottleneck is KV cache growth.

During autoregressive generation, transformers must store the key (K) and value (V) vectors for all previously generated tokens. This "KV cache" allows the model to attend to its full history without recomputing everything at each step. For a model with LL layers, hh attention heads, head dimension dhd_h, and sequence length nn, the cache requires:

KV Cache Memory=2×L×h×n×dh×bytes_per_element\text{KV Cache Memory} = 2 \times L \times h \times n \times d_h \times \text{bytes\_per\_element}

For Qwen-72B generating a 4096-token sequence in FP16, this exceeds 40GB just for the cache. The insight behind GQA is that not every query head needs its own unique key-value representation, so we can share KV heads across groups of query heads to sharply reduce this memory burden.

Out[4]:
Visualization
Line plot showing KV cache memory in GB versus sequence length for MHA (1:1), GQA 4:1, and GQA 8:1 configurations.
KV cache memory grows linearly with sequence length. GQA with higher ratios (8:1 in Qwen-72B) sharply reduces memory requirements, letting longer context windows within the same memory budget.

The solution: sharing key-value heads

While LLaMA-7B uses standard multi-head attention (MHA), Qwen employs Grouped-Query Attention (GQA) across all model sizes. GQA strikes a balance between the quality of MHA and the efficiency of Multi-Query Attention (MQA).

Out[5]:
Visualization
Three diagrams comparing MHA, MQA, and GQA attention patterns showing how keys and values are shared across query heads.
Comparison of attention mechanisms: Multi-Head Attention uses separate K,V heads per query head, Multi-Query shares one K,V across all queries, and Grouped-Query Attention groups query heads to share K,V heads. Qwen uses GQA for all model sizes.

In standard multi-head attention, each query head has its own dedicated key and value heads. GQA groups multiple query heads together to share a single set of key-value heads. This reduces memory requirements while preserving most of the representational capacity.

Understanding the attention variants

Before diving into the formulas, let's build intuition about the three attention variants:

  1. Multi-Head Attention (MHA): The original design. Each query head gets its own private key-value pair. Maximum expressiveness, but maximum memory cost. Think of it as giving each employee their own filing cabinet.

  2. Multi-Query Attention (MQA): The extreme efficiency approach. All query heads share a single key-value pair. Minimal memory, but information gets compressed. Like having everyone share one filing cabinet.

  3. Grouped-Query Attention (GQA): The balanced solution. Small groups of query heads share key-value pairs. Qwen uses this because it retains most of MHA's quality while achieving most of MQA's efficiency gains.

The mathematical formulation

Now we can understand the formal definition. The overall GQA computation concatenates the outputs from all attention heads and projects them back to the model dimension:

GQA(Q,K,V)=Concat(head1,…,headh)WO\text{GQA}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O

where:

  • Q,K,VQ, K, V: the QKV tensors derived from the input hidden states
  • headi\text{head}_i: the output of the ii-th attention head, capturing what information token ii should attend to
  • hh: the total number of query heads (e.g., 32 for Qwen-7B, 64 for Qwen-72B)
  • WOW^O: the output projection matrix of shape (h⋅dhead,dmodel)(h \cdot d_{\text{head}}, d_{\text{model}}) that recombines head outputs
  • Concat\text{Concat}: concatenation along the head dimension, stacking all head outputs together

Each individual head computes attention using its query projection but shares key-value projections with other heads in its group. This is where the efficiency gain comes from:

headi=Attention(QiWiQ,Kg(i)Wg(i)K,Vg(i)Wg(i)V)\text{head}_i = \text{Attention}(Q_i W^Q_i, K_{g(i)} W^K_{g(i)}, V_{g(i)} W^V_{g(i)})

where:

  • QiQ_i: the query input for head ii (unique to each head)
  • WiQW^Q_i: the query projection matrix for head ii (unique to each head)
  • g(i)g(i): a grouping function that maps query head ii to its corresponding KV group index
  • Kg(i),Vg(i)K_{g(i)}, V_{g(i)}: the key and value inputs for the group containing head ii (shared within group)
  • Wg(i)K,Wg(i)VW^K_{g(i)}, W^V_{g(i)}: the shared key and value projection matrices for that group

How grouping works in practice

The grouping function g(i)g(i) determines which query heads share KV heads. For a model with HQH_Q query heads and HKVH_{KV} key-value heads:

g(i)=⌊i⋅HKVHQ⌋g(i) = \left\lfloor \frac{i \cdot H_{KV}}{H_Q} \right\rfloor

Let's trace through two concrete examples:

  • Qwen-7B: 32 query heads, 32 KV heads (ratio 1:1). Here g(i)=ig(i) = i, so each query head has its own KV head. This is equivalent to standard MHA, the safe choice for a smaller model.

  • Qwen-72B: 64 query heads, 8 KV heads (ratio 8:1). Every 8 consecutive query heads share the same KV head. Query heads 0-7 share KV head 0, heads 8-15 share KV head 1, and so on. This gives an 8× reduction in KV cache memory.

Why this works

The intuition for why grouping works is that different query heads often look for similar patterns in the keys and values. By forcing them to share, we impose a useful regularization. Empirically, researchers found that models with GQA ratios up to 8:1 show minimal quality degradation while achieving substantial memory and speed improvements.

Summary of advantages

  • Reduced KV cache memory: Fewer KV heads means smaller cache during generation (8× reduction for Qwen-72B)
  • Faster inference: Less memory bandwidth needed to load KV cache, directly improving tokens/second
  • Maintained quality: Grouping preserves most of MHA's representational capacity through shared but still diverse key-value representations

Attention with Bias

Having understood how Qwen organizes its attention heads, we now turn to a subtler design choice: whether to include bias terms in the attention projections.

The design question

When projecting hidden states to QKV tensors, we have a choice:

Without bias:Q=XWQWith bias:Q=XWQ+bQ\text{Without bias:} \quad Q = XW^Q \qquad \text{With bias:} \quad Q = XW^Q + b^Q

LLaMA follows the trend of removing all bias terms, arguing that the weight matrices alone have sufficient capacity and that removing bias simplifies the model. Qwen takes the opposite stance, adding bias to the QKV projections.

Why bias might help

The intuition is subtle but important. Bias terms give a learned "default" activation that doesn't depend on the input. In attention, this means:

  • Queries can have a baseline direction they search in, independent of the current token
  • Keys can have a baseline "signature" that makes certain patterns more or less likely to match
  • Values can contribute baseline information even before considering specific content

Think of it like this: without bias, attention patterns must emerge entirely from token-token interactions. With bias, the model can learn that "queries from this head should generally attend more to earlier positions" or "values should include this baseline information regardless of content."

The parameter cost

This flexibility comes at minimal cost. For Qwen-7B:

Bias parameters per layer=3×dmodel=3×4096=12,288\text{Bias parameters per layer} = 3 \times d_{\text{model}} = 3 \times 4096 = 12{,}288

Compared to the total attention parameters per layer (~67M), this is about 0.02%. Qwen's researchers found this negligible overhead gives measurable quality improvements, particularly in tasks requiring fine-grained attention patterns.

Implementation

Unlike LLaMA which removes all bias terms from linear projections, Qwen adds bias to the QKV projections:

In[6]:
Code
import torch
import torch.nn.functional as F
from torch import nn


class QwenAttention(nn.Module):
    """Qwen attention with QKV bias and optional GQA"""

    def __init__(
        self,
        hidden_size: int,
        num_attention_heads: int,
        num_kv_heads: int = None,
    ):
        super().__init__()
        self.hidden_size = hidden_size
        self.num_heads = num_attention_heads
        self.num_kv_heads = num_kv_heads or num_attention_heads
        self.head_dim = hidden_size // num_attention_heads
        self.num_kv_groups = self.num_heads // self.num_kv_heads

        # Qwen uses bias=True for Q, K, V projections
        self.q_proj = nn.Linear(
            hidden_size, self.num_heads * self.head_dim, bias=True
        )
        self.k_proj = nn.Linear(
            hidden_size, self.num_kv_heads * self.head_dim, bias=True
        )
        self.v_proj = nn.Linear(
            hidden_size, self.num_kv_heads * self.head_dim, bias=True
        )
        self.o_proj = nn.Linear(
            self.num_heads * self.head_dim, hidden_size, bias=False
        )

    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        batch_size, seq_len, _ = hidden_states.shape

        # Project to Q, K, V with bias
        query = self.q_proj(hidden_states)
        key = self.k_proj(hidden_states)
        value = self.v_proj(hidden_states)

        # Reshape for attention
        query = query.view(batch_size, seq_len, self.num_heads, self.head_dim)
        key = key.view(batch_size, seq_len, self.num_kv_heads, self.head_dim)
        value = value.view(
            batch_size, seq_len, self.num_kv_heads, self.head_dim
        )

        # Transpose for batch matrix multiplication
        query = query.transpose(1, 2)  # (batch, num_heads, seq_len, head_dim)
        key = key.transpose(1, 2)
        value = value.transpose(1, 2)

        # Expand KV for GQA if needed
        if self.num_kv_groups > 1:
            key = key.repeat_interleave(self.num_kv_groups, dim=1)
            value = value.repeat_interleave(self.num_kv_groups, dim=1)

        # Scaled dot-product attention
        scale = self.head_dim**-0.5
        attn_weights = torch.matmul(query, key.transpose(-2, -1)) * scale

        # Causal mask
        causal_mask = torch.triu(
            torch.ones(seq_len, seq_len, device=hidden_states.device)
            * float("-inf"),
            diagonal=1,
        )
        attn_weights = attn_weights + causal_mask

        attn_weights = F.softmax(attn_weights, dim=-1)
        attn_output = torch.matmul(attn_weights, value)

        # Reshape and project output
        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.view(batch_size, seq_len, -1)

        return self.o_proj(attn_output)


# Test the attention module
hidden_size = 4096
num_heads = 32
num_kv_heads = 32  # Qwen-7B uses 1:1 ratio

attention = QwenAttention(hidden_size, num_heads, num_kv_heads)
x = torch.randn(2, 64, hidden_size)
output = attention(x)
Out[7]:
Console
Qwen Attention Module:
  Input shape: torch.Size([2, 64, 4096])
  Output shape: torch.Size([2, 64, 4096])

Attention configuration:
  Hidden size: 4096
  Query heads: 32
  KV heads: 32
  Head dimension: 128

Parameter breakdown:
  Total attention parameters: 67,121,152
  Bias parameters (Q, K, V): 12,288
  Bias as % of attention: 0.02%

The output shape matches the input shape, confirming the attention module correctly processes the sequence while preserving dimensions. The bias parameters add only 0.02% overhead to the attention layer, but Qwen's researchers found this small addition improves attention pattern learning. The 128-dimensional heads (4096 / 32) give sufficient capacity for complex attention patterns. While LLaMA removes biases for simplicity and to reduce parameters, Qwen found that the small parameter increase (less than 0.1% of total) gives real quality improvements.

SwiGLU Feed-Forward Network

After attention aggregates information across positions, the feed-forward network (FFN) processes each position independently, adding nonlinearity and increasing the model's representational capacity. Here we explore why Qwen chose SwiGLU over simpler alternatives.

The evolution of feed-forward design

The original Transformer used a simple two-layer FFN with ReLU activation:

FFN(x)=ReLU(xW1+b1)W2+b2\text{FFN}(x) = \text{ReLU}(xW_1 + b_1)W_2 + b_2

This works, but researchers discovered that gated variants perform better. The key insight: instead of applying a single nonlinearity, use one transformation to decide what information to pass through (the gate) and another to decide which information is available (the values).

The gating mechanism

SwiGLU introduces element-wise multiplication between two parallel branches:

SwiGLU(x)=Swish(xWgate)⏟what to keep⊙(xWup)⏟candidate values\text{SwiGLU}(x) = \underbrace{\text{Swish}(xW_{\text{gate}})}_{\text{what to keep}} \odot \underbrace{(xW_{\text{up}})}_{\text{candidate values}}

Let's unpack each part:

  1. Gate branch (xWgatexW_{\text{gate}}): Projects the input to an intermediate dimension, then applies Swish activation. The Swish output ranges from near-zero (suppress) to unbounded positive (amplify).

  2. Value branch (xWupxW_{\text{up}}): Projects the input to the same intermediate dimension. These are the "candidate" values that might pass through.

  3. Element-wise product (⊙\odot): For each element, the gate controls how much of the corresponding value passes through. Near-zero gate means that value is suppressed; large positive gate means that value is amplified.

The Swish activation

Why Swish rather than ReLU for the gate? Swish gives smooth, non-monotonic gating:

Swish(z)=z⋅σ(z)=z1+e−z\text{Swish}(z) = z \cdot \sigma(z) = \frac{z}{1 + e^{-z}}

Key properties that make Swish effective:

  • Smooth gradients: Unlike ReLU's hard cutoff at zero, Swish transitions gradually, improving gradient flow during training
  • Non-monotonic: For small negative inputs, Swish outputs small negative values before approaching zero. This allows the gate to preserve subtle negative signals
  • Self-gating: The σ(z)\sigma(z) term acts as a soft switch on the zz term itself
Out[8]:
Visualization
Line plot comparing ReLU (hard cutoff at zero) with Swish (smooth curve with small negative dip) activation functions.
Comparison of ReLU and Swish activation functions. Swish gives smooth gradients and exhibits non-monotonic behavior for small negative inputs, letting subtle signals to pass before suppression. The minimum occurs around z = -1.28.

The complete MLP

After the gated transformation, we project back to the model dimension:

MLP(x)=SwiGLU(x)Wdown\text{MLP}(x) = \text{SwiGLU}(x) W_{\text{down}}

where WdownW_{\text{down}} has shape (dff,dmodel)(d_{\text{ff}}, d_{\text{model}}). The intermediate dimension dffd_{\text{ff}} is typically ≈2.7×dmodel\approx 2.7 \times d_{\text{model}} rather than the standard 4×4\times. This adjustment accounts for SwiGLU having three projection matrices instead of two, keeping the total parameter count similar to a standard FFN.

Implementation

Like LLaMA, Qwen uses the SwiGLU activation function in its feed-forward layers:

In[9]:
Code
class QwenMLP(nn.Module):
    """Qwen MLP with SwiGLU activation"""

    def __init__(self, hidden_size: int, intermediate_size: int):
        super().__init__()
        self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
        self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
        self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # SwiGLU: Swish(gate) * up, then down projection
        return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))


# Qwen-7B dimensions
hidden_size = 4096
intermediate_size = 11008  # ~2.7x hidden_size

mlp = QwenMLP(hidden_size, intermediate_size)
x = torch.randn(2, 64, hidden_size)
output = mlp(x)
Out[10]:
Console
Qwen MLP (SwiGLU):
  Input shape: torch.Size([2, 64, 4096])
  Output shape: torch.Size([2, 64, 4096])

Dimensions:
  Hidden size: 4096
  Intermediate size: 11008
  Expansion ratio: 2.69x

Parameter breakdown:
  Gate projection: 45,088,768
  Up projection: 45,088,768
  Down projection: 45,088,768
  Total MLP parameters: 135,266,304

Analyzing the output

The MLP parameters dominate each transformer layer, accounting for roughly two-thirds of per-layer parameters. The 2.69x expansion ratio (rather than the standard 4x) compensates for SwiGLU's three projections instead of two, keeping total parameter count comparable to standard FFN layers while giving the gating mechanism's quality benefits.

Why gating matters

The key insight is that not all intermediate features are equally useful for every input. In a standard FFN, every intermediate neuron contributes to every output (modulated only by ReLU killing negative values). With SwiGLU, the gate branch learns when each intermediate feature should contribute, creating input-dependent computation paths. This is a form of learned sparsity that improves model quality without increasing inference cost.

RoPE: Rotary Positional Embeddings

Transformers process sequences in parallel, treating each position identically. This is computationally efficient but creates a problem: how does the model know that token 5 comes before token 10? The answer lies in positional encoding, and Qwen uses Rotary Position Embeddings (RoPE), the same approach as LLaMA.

The positional encoding challenge

Early transformers used absolute positional embeddings: learn a vector for position 0, another for position 1, and so on. This has two problems:

  1. Length limitation: The model can only handle positions it saw during training
  2. No relative reasoning: Positions 10-15 look completely different from positions 100-105, even though the relationship (5 tokens apart) is identical

What we really want is a way to encode position such that attention scores depend on relative distance between tokens, not their absolute positions.

The rotation insight

RoPE reaches this by encoding position as a rotation in embedding space. The key insight is that when two rotations are composed, the result depends only on their angular difference:

Rm−1⋅Rn=Rn−mR_m^{-1} \cdot R_n = R_{n-m}

If we rotate queries by their position and keys by their position, then when we compute attention (which involves a dot product between queries and keys), the rotation effect depends only on the relative distance.

How rotation encodes position

For each position mm, we construct a rotation matrix RmR_m that rotates pairs of dimensions in the embedding space. The rotation angle depends on both the position and the dimension pair:

θi=m⋅1100002i/d\theta_i = m \cdot \frac{1}{10000^{2i/d}}

where ii indexes the dimension pair (there are d/2d/2 pairs for dimension dd) and mm is the position. Lower-frequency rotations (larger ii) encode longer-range position information; higher-frequency rotations (smaller ii) encode fine-grained local position.

Out[11]:
Visualization
Heatmap showing RoPE rotation angles across sequence positions (x-axis) and dimension pairs (y-axis), with color representing the cosine of the rotation angle.
RoPE rotation patterns across positions and dimension pairs. Lower dimensions (top) rotate rapidly for fine-grained local position encoding, while higher dimensions (bottom) rotate slowly for long-range position information. The periodic patterns let relative position encoding.

The relative position property

The key property emerges when we compute attention scores. Given a query at position mm and a key at position nn:

(Rmq)⊤(Rnk)=q⊤Rm⊤Rnk=q⊤Rn−mk(R_m q)^\top (R_n k) = q^\top R_m^\top R_n k = q^\top R_{n-m} k

The attention score depends only on Rn−mR_{n-m}, the rotation corresponding to the relative distance (n−m)(n-m). This means:

  • Tokens 5 positions apart always have the same relative encoding, regardless of absolute position
  • The model can potentially generalize to longer sequences than it was trained on
  • Nearby tokens have similar rotations; distant tokens have dissimilar rotations

Implementation

Qwen uses the same RoPE implementation as LLaMA for positional encoding, which encodes position through rotation of query and key vectors:

In[12]:
Code
def precompute_freqs_cis(dim: int, max_seq_len: int, theta: float = 10000.0):
    """Precompute rotary embedding frequencies"""
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: dim // 2].float() / dim))
    t = torch.arange(max_seq_len, device=freqs.device)
    freqs = torch.outer(t, freqs).float()
    return torch.polar(torch.ones_like(freqs), freqs)


def apply_rotary_emb(
    xq: torch.Tensor, xk: torch.Tensor, freqs_cis: torch.Tensor
):
    """Apply rotary embeddings to query and key tensors"""
    xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
    xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))

    freqs_cis = freqs_cis[: xq_.shape[1], :].unsqueeze(0).unsqueeze(2)

    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)

    return xq_out.type_as(xq), xk_out.type_as(xk)


# Demonstrate RoPE
head_dim = 128
max_len = 2048
freqs_cis = precompute_freqs_cis(head_dim, max_len)

# Sample Q and K
batch, seq_len, n_heads = 2, 64, 32
q = torch.randn(batch, seq_len, n_heads, head_dim)
k = torch.randn(batch, seq_len, n_heads, head_dim)

q_rope, k_rope = apply_rotary_emb(q, k, freqs_cis)
Out[13]:
Console
RoPE (Rotary Positional Embeddings):
  Head dimension: 128
  Max sequence length: 2048

Before RoPE:
  Q shape: torch.Size([2, 64, 32, 128])
  K shape: torch.Size([2, 64, 32, 128])

After RoPE:
  Q shape: torch.Size([2, 64, 32, 128])
  K shape: torch.Size([2, 64, 32, 128])

RoPE encodes relative position through rotation,
letting length extrapolation beyond training length.

Understanding the code

The implementation uses complex number arithmetic as an efficient way to apply 2D rotations. The precompute_freqs_cis function generates the rotation angles for each position and dimension pair, stored as complex numbers (cosine + i·sine). The apply_rotary_emb function then applies these rotations by treating pairs of real dimensions as complex numbers and multiplying.

This approach is mathematically equivalent to applying rotation matrices but is much more efficient on modern hardware. The key insight remains: after rotation, attention scores between queries and keys naturally encode relative position, letting the model to reason about token relationships regardless of their absolute positions in the sequence.