C.W.K.
Stream
Lesson 13 of 13 · published

GQA and MQA: Shrinking the KV Cache

~12 min · gqa, mqa, memory

Level 0Token
0 XP0/94 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

The KV cache is the dragon of long-context inference. Standard MHA stores one K and V per query head, scaling linearly with both context length and head count. Grouped-Query Attention (GQA) and its extreme cousin Multi-Query Attention (MQA) attack this directly by sharing K and V across multiple Q heads.

The shapes

VariantQ headsKV headsKV cache reductionUsed by
MHAHH1× (baseline)BERT, GPT-3, GPT-2
GQAHG < HH/G ×Llama 3, Mistral, Gemma 3, Mixtral
MQAH1H ×Early PaLM, Falcon

Llama 3.3 70B has 64 query heads and 8 KV heads — a GQA group size of 8. Each group of 8 query heads shares one K and one V projection. The KV cache shrinks by 8x. Quality stays close to full MHA because GQA has more KV diversity than MQA but most of the cache savings.

Why this is now the default

Empirically, going from MHA to GQA with 8 KV heads loses very little quality but saves enormous memory at long context. MQA is even more aggressive but shows quality degradation in some tasks, so the field settled on GQA as the default. You can also uptrain an MHA checkpoint into GQA: average the KV heads within each group and continue training for ~5% of the original pretraining compute. Llama 2 70B-chat → GQA was done this way.

Code

GQA in PyTorch (Llama-style)·python
class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, n_q_heads, n_kv_heads):
        super().__init__()
        assert n_q_heads % n_kv_heads == 0
        self.n_q_heads, self.n_kv_heads = n_q_heads, n_kv_heads
        self.d_head = d_model // n_q_heads
        self.W_q = nn.Linear(d_model, n_q_heads * self.d_head, bias=False)
        self.W_k = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)
        self.W_v = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)
        self.W_o = nn.Linear(d_model, d_model, bias=False)
        self.repeat = n_q_heads // n_kv_heads
    def forward(self, x, mask=None):
        B, L, _ = x.shape
        Q = self.W_q(x).view(B, L, self.n_q_heads, self.d_head)
        K = self.W_k(x).view(B, L, self.n_kv_heads, self.d_head)
        V = self.W_v(x).view(B, L, self.n_kv_heads, self.d_head)
        # Repeat KV heads to match query heads
        K = K.repeat_interleave(self.repeat, dim=2)
        V = V.repeat_interleave(self.repeat, dim=2)
        Q, K, V = Q.transpose(1,2), K.transpose(1,2), V.transpose(1,2)
        out = F.scaled_dot_product_attention(Q, K, V, attn_mask=mask)
        return self.W_o(out.transpose(1,2).contiguous().view(B, L, -1))

External links

Exercise

Implement GroupedQueryAttention. Compare its forward-pass memory usage against a same-shape MHA on a (B=4, n=8K) input. Repeat at n=32K. The KV-cache savings should be obvious. Then estimate what 128K context would cost with each.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.