Grouped Query Attention › Mathematical Formulation

Michael BrenndoerferUpdated August 6, 202512 min read
Language AI HandbookAll chapters
Part 29 of 67Modern Decoder Models
Chapter 209Grouped Query Attention › Mathematical Formulation
Reading section 4 of 14 · Mathematical Formulation · 12 minSection 4 of 14 · 12 min

Book contents

Section 4 of 14 · 12 min read

Grouped Query Attention › Mathematical FormulationOne section · Sections in this chapter4 / 14 ↓
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

Let's formalize GQA mathematically. The core idea is straightforward: instead of each query head having its own key-value projections (as in MHA) or all query heads sharing one key-value projection (as in MQA), GQA divides query heads into groups where each group shares a single key-value projection.

Given an input sequence X∈RT×dX \in \mathbb{R}^{T \times d}, GQA is parameterized by:

  • hh: number of query heads (determines query diversity)
  • gg: number of key-value groups, where gg divides hh evenly (controls memory vs. capacity trade-off)
  • dk=d/hd_k = d / h: dimension per head (same as standard multi-head attention)

The ratio h/gh/g determines how many query heads share each key-value head. For example, with h=32h = 32 query heads and g=8g = 8 KV groups, each group of 4 query heads shares one key-value representation.

Query Projections

Query projections work exactly as in standard multi-head attention. Each query head ii has its own learned projection matrix that turns the input into a query representation:

Qi=XWiQfor i∈{1,…,h}Q_i = X W^Q_i \quad \text{for } i \in \{1, \ldots, h\}

where:

  • Qi∈RT×dkQ_i \in \mathbb{R}^{T \times d_k}: the query matrix for head ii, containing a dkd_k-dimensional query vector for each of the TT positions
  • X∈RT×dX \in \mathbb{R}^{T \times d}: the input sequence with TT tokens, each represented as a dd-dimensional vector
  • WiQ∈Rd×dkW^Q_i \in \mathbb{R}^{d \times d_k}: the learned query projection matrix for head ii, transforming from model dimension to head dimension

Having hh separate query projections preserves the model's ability to ask hh different "questions" of the input. Each query head can still specialize in detecting different patterns, maintaining expressive power.

Key-Value Projections (The Key Difference)

Here is where GQA diverges from MHA. Instead of hh key-value projections, we have only gg projections that are shared among groups of query heads:

Kj=XWjK,Vj=XWjVfor j∈{1,…,g}K_j = X W^K_j, \quad V_j = X W^V_j \quad \text{for } j \in \{1, \ldots, g\}

where:

  • Kj∈RT×dkK_j \in \mathbb{R}^{T \times d_k}: the key matrix for group jj, containing key vectors for all TT positions
  • Vj∈RT×dkV_j \in \mathbb{R}^{T \times d_k}: the value matrix for group jj, containing value vectors for all TT positions
  • WjK∈Rd×dkW^K_j \in \mathbb{R}^{d \times d_k}: the learned key projection matrix for group jj
  • WjV∈Rd×dkW^V_j \in \mathbb{R}^{d \times d_k}: the learned value projection matrix for group jj

With only gg key-value projections instead of hh, the KV cache during inference stores gg key-value pairs rather than hh, reducing memory by a factor of h/gh/g.

Grouping Function

To connect query heads to their shared key-value representations, we define a grouping function that maps each query head index to its corresponding key-value group:

group(i)=⌊(i−1)⋅gh⌋+1\text{group}(i) = \left\lfloor \frac{(i - 1) \cdot g}{h} \right\rfloor + 1

where:

  • ii: the query head index, ranging from 1 to hh
  • gg: the total number of key-value groups
  • hh: the total number of query heads
  • ⌊⋅⌋\lfloor \cdot \rfloor: the floor function (rounds down to nearest integer)

This function partitions the hh query heads into gg groups of size h/gh/g each. For example, with h=8h = 8 query heads and g=2g = 2 groups:

  • Query heads 1, 2, 3, 4 map to group 1 (using K1K_1, V1V_1)
  • Query heads 5, 6, 7, 8 map to group 2 (using K2K_2, V2V_2)

Attention Computation

Each query head computes attention using its own query but the shared key-value from its group:

headi=softmax(QiKgroup(i)Tdk)Vgroup(i)\text{head}_i = \text{softmax}\left(\frac{Q_i K_{\text{group}(i)}^T}{\sqrt{d_k}}\right) V_{\text{group}(i)}

where:

  • headi∈RT×dk\text{head}_i \in \mathbb{R}^{T \times d_k}: the output of attention head ii
  • Qi∈RT×dkQ_i \in \mathbb{R}^{T \times d_k}: the query matrix for head ii (unique to this head)
  • Kgroup(i)∈RT×dkK_{\text{group}(i)} \in \mathbb{R}^{T \times d_k}: the key matrix shared by all heads in group group(i)\text{group}(i)
  • Vgroup(i)∈RT×dkV_{\text{group}(i)} \in \mathbb{R}^{T \times d_k}: the value matrix shared by all heads in group group(i)\text{group}(i)
  • QiKgroup(i)T∈RT×TQ_i K_{\text{group}(i)}^T \in \mathbb{R}^{T \times T}: the attention score matrix
  • dk\sqrt{d_k}: scaling factor to prevent dot products from growing too large

The key insight is that different query heads in the same group can still learn different attention patterns. Even though heads 1-4 all use K1K_1 and V1V_1, their different query projections Q1,Q2,Q3,Q4Q_1, Q_2, Q_3, Q_4 produce different attention weights over the same key-value pairs.

Output Combination

Finally, all head outputs are concatenated and projected back to the model dimension:

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

where:

  • Concat(head1,…,headh)∈RT×(h⋅dk)\text{Concat}(\text{head}_1, \ldots, \text{head}_h) \in \mathbb{R}^{T \times (h \cdot d_k)}: concatenation of all hh head outputs along the feature dimension
  • WO∈R(h⋅dk)×dW^O \in \mathbb{R}^{(h \cdot d_k) \times d}: the output projection matrix
  • GQA(X)∈RT×d\text{GQA}(X) \in \mathbb{R}^{T \times d}: the final output, matching the input dimension

Since h⋅dk=dh \cdot d_k = d, the concatenated heads have dimension dd, and WOW^O projects back to dimension dd, preserving the input shape for residual connections.

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


class GroupedQueryAttention(nn.Module):
    """Grouped Query Attention implementation."""

    def __init__(
        self,
        d_model: int,
        num_heads: int,
        num_kv_heads: int,
        dropout: float = 0.0,
    ):
        super().__init__()
        assert d_model % num_heads == 0, (
            "d_model must be divisible by num_heads"
        )
        assert num_heads % num_kv_heads == 0, (
            "num_heads must be divisible by num_kv_heads"
        )

        self.d_model = d_model
        self.num_heads = num_heads
        self.num_kv_heads = num_kv_heads
        self.num_queries_per_kv = num_heads // num_kv_heads
        self.head_dim = d_model // num_heads

        # Query projection: full size for all query heads
        self.w_q = nn.Linear(d_model, num_heads * self.head_dim, bias=False)

        # Key and value projections: reduced size for KV heads only
        self.w_k = nn.Linear(d_model, num_kv_heads * self.head_dim, bias=False)
        self.w_v = nn.Linear(d_model, num_kv_heads * self.head_dim, bias=False)

        # Output projection
        self.w_o = nn.Linear(d_model, d_model, bias=False)

        self.dropout = nn.Dropout(dropout)
        self.scale = self.head_dim**-0.5

    def forward(
        self,
        x: torch.Tensor,
        mask: torch.Tensor = None,
        kv_cache: tuple = None,
    ) -> tuple:
        batch_size, seq_len, _ = x.shape

        # Compute queries: (batch, seq, num_heads, head_dim)
        q = self.w_q(x).view(batch_size, seq_len, self.num_heads, self.head_dim)

        # Compute keys and values: (batch, seq, num_kv_heads, head_dim)
        k = self.w_k(x).view(
            batch_size, seq_len, self.num_kv_heads, self.head_dim
        )
        v = self.w_v(x).view(
            batch_size, seq_len, self.num_kv_heads, self.head_dim
        )

        # Handle KV cache for incremental decoding
        if kv_cache is not None:
            k_cache, v_cache = kv_cache
            k = torch.cat([k_cache, k], dim=1)
            v = torch.cat([v_cache, v], dim=1)

        new_kv_cache = (k, v)
        kv_seq_len = k.size(1)

        # Reshape for attention computation
        # q: (batch, num_heads, seq, head_dim)
        q = q.transpose(1, 2)
        # k, v: (batch, num_kv_heads, kv_seq, head_dim)
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)

        # Expand k, v to match query heads by repeating
        # (batch, num_kv_heads, kv_seq, head_dim) -> (batch, num_heads, kv_seq, head_dim)
        k = k.repeat_interleave(self.num_queries_per_kv, dim=1)
        v = v.repeat_interleave(self.num_queries_per_kv, dim=1)

        # Compute attention scores: (batch, num_heads, seq, kv_seq)
        attn_scores = torch.matmul(q, k.transpose(-2, -1)) * self.scale

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

        attn_weights = F.softmax(attn_scores, dim=-1)
        attn_weights = self.dropout(attn_weights)

        # Apply attention to values: (batch, num_heads, seq, head_dim)
        attn_output = torch.matmul(attn_weights, v)

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

        return self.w_o(attn_output), new_kv_cache
Out[7]:
Console
Grouped Query Attention Test
=======================================================

Configuration:
  d_model: 512
  Query heads: 8
  KV heads: 2
  Queries per KV group: 4

Shapes:
  Input: (2, 16, 512)
  Output: (2, 16, 512)
  KV cache K: (2, 16, 2, 64)
  KV cache V: (2, 16, 2, 64)

Parameter counts:
  GQA: 655,360
  MHA (equivalent): 1,048,576
  MQA (equivalent): 589,824
Out[8]:
Visualization
Stacked bar chart showing parameter counts for MHA, GQA, and MQA, broken down by projection type.
Parameter count comparison between attention variants. GQA reduces parameters compared to MHA by using fewer K,V projections, while retaining more capacity than MQA. The query and output projections remain the same size across all variants.

The key implementation detail is the repeat_interleave operation that expands the key-value tensors to match the number of query heads. This creates the grouping structure: consecutive query heads share the same key-value entries. The KV cache stores only the reduced number of key-value heads. This provides the memory savings.

Out[9]:
Visualization
Heatmap of causal attention weights for query head Q1, sharing K and V with all heads in the group.
Query head Q1 attention pattern.
Heatmap of causal attention weights for query head Q2, sharing K and V with all heads in the group.
Query head Q2 attention pattern.
Out[10]:
Visualization
Heatmap of causal attention weights for query head Q3, sharing K and V with all heads in the group.
Query head Q3 attention pattern.
Heatmap of causal attention weights for query head Q4, sharing K and V with all heads in the group.
Query head Q4 attention pattern.

The attention heatmaps above reveal a important property of GQA: even though all four query heads share the same key and value tensors (they're all in Group 1), each head produces distinct attention patterns. Head Q1 might focus heavily on recent tokens, Q2 on the first token, Q3 on positions 2-3, and Q4 shows yet another pattern. This diversity emerges because each query head has its own learned projection. The queries are different even when the keys are identical.

The attention heatmaps reveal a important property of GQA: even though all four query heads share the same key and value tensors (they're all in Group 1), each head produces distinct attention patterns. Head Q1 might focus heavily on recent tokens, Q2 on the first token, Q3 on positions 2-3, and Q4 shows yet another pattern. This diversity emerges because each query head has its own learned projection. The queries are different even when the keys are identical.