DeepConcepts

LLM internals / inference / attention / architecture

Grouped-Query Attention

The misconception

That GQA is a compute optimisation — fewer heads, less math, a smaller and slightly dumber model. Every one of those is wrong. Query heads are untouched, the attention FLOPs of a decode step are identical at 32 kv heads and at 1 because the shared keys are broadcast to every query head rather than skipped, and the parameter saving is about 1% of the model. The entire win is bytes of KV cache per token, which is why it shows up as concurrent sequences and not as a faster single stream. The second half of the misconception is that fewer kv heads is monotonically better: below the tensor-parallel degree the serving engine replicates the kv heads across ranks, so aggregate cache stops falling entirely, and the quality cost of the last step from 8 heads to 1 is the one step that actually hurt in the paper.

13 min

Grouped-query attention does not remove any query heads, does not make the attention arithmetic smaller, and does not make the model meaningfully smaller. It removes copies of the keys and values. That is a memory decision, and it is worth four to eight times as many concurrent users on the same card.

Some vocabulary first, because this area is thick with three-letter names for the same dial. A transformer layer projects each token into a query, a key and a value, several times over in parallel — each parallel copy is a head. In multi-head attention (MHA), the original 2017 arrangement, every query head gets its own key head and its own value head. In multi-query attention (MQA), proposed by Shazeer in 2019, all the query heads share one key head and one value head. Grouped-query attention (GQA), from Ainslie et al. in 2023, is the setting in between: the query heads are divided into groups, and each group shares one key head and one value head. Every model config you will read states this as num_attention_heads and num_key_value_heads. They are the two ends and the middle of a single dial.

Only the second number is charged to memory, because only keys and values are kept between tokens — that stored tensor is the KV cache, and it costs:

bytes per token = 2 (K and V) × layers × kv_heads × head_dim × bytes_per_element

Query heads are absent from that formula. A query is used once, in the step that created it, and then thrown away. So halving the key/value heads halves the cache, exactly, with no effect on how many queries are computed.

Below is one card running one model. The dial marked kv heads sweeps from multi-head attention on the right to multi-query attention on the left. Move it and watch three numbers that most people expect to move together come apart: the cache, the parameters, and the arithmetic.

Model — shipped configs, verbatim
GPU — dense BF16, one node

The kv heads dial only stops on divisors of this model's query-head count, because a group has to be a whole number of query heads. Its leftmost stop is multi-query attention (1 kv head) and its rightmost stop is multi-head attention (kv heads = query heads). Everything else about the model is held fixed.

sequences resident
kv cache / token / gpu
decode tokens / sec
attention flops / seq / step
attention flops per byte
parameters
Who shares what — one layer, all ranks

Each box is one key/value head that must be stored and re-read on every token. The small cells inside it are the query heads it serves, and they cost nothing to keep. A magenta box is a replica: the same key/value head duplicated onto another rank because there were fewer kv heads than GPUs. It occupies memory and bandwidth without holding anything new.

This setting against both ends of the dial

Same model, same GPU, same context length; only num_key_value_heads differs. Bars are scaled to the largest value in each row.

Layer counts, head counts, head_dim, vocabulary and feed-forward widths are read from each model's published config.json; the bandwidth and dense BF16 tensor-core rates are NVIDIA's published figures — 989 TFLOP/s for Hopper, which is the 1,979 on the datasheet halved, because that one is the with-sparsity number. Timings are illustrative, not benchmarked: 80% of peak bandwidth, 55% of peak dense FLOP/s, every resident sequence held at full context, all-reduce cost and kernel launch overhead ignored. The byte counts and FLOP counts are exact; the milliseconds are a sketch.

Start at the defaults — Qwen3-8B, H100, 8k tokens of context — and drag kv heads from 8 up to 32, which is what this model would have been before 2023. The cache goes from 144 KiB per token to 576 KiB, and the number of sequences the card can hold falls from 49 to 11. Throughput falls with it, 1,738 tokens per second to 413. Now look at what did not change: attention still does 4.83 GFLOP per sequence per step at every stop on the dial, because the shared key head is broadcast to all four of its query heads rather than skipped. The parameter count moves by about a tenth, 8.19B to 9.10B. A 10% change in one number and a 4× change in the other, from the same dial.

Three numbers that people expect to move together

The reason GQA is misread is that "fewer heads" sounds like "smaller model, less work, worse quality" — one lever with three consequences. It is not. The dial you just moved separates them.

The cache is linear in kv heads. Every kv head you remove removes its bytes from every token of every resident sequence, forever. Qwen3-8B at 8k context: 576 KiB per token at 32 kv heads, 144 KiB at 8, 18 KiB at 1. That term is multiplied by context length and by concurrency, so it is the term that decides how many users fit.

The parameters barely move. Only k_proj and v_proj get narrower. On Qwen3-8B that is 9.10B parameters at multi-head down to 8.19B at 8 kv heads — 10% — and only 8.19B to 7.93B for the whole rest of the journey to multi-query. Nobody adopts GQA to save parameters.

The arithmetic does not move at all. This is the part that surprises people. A shared key head is not computed once and used once; it is read once and broadcast to every query head in its group, and each of those query heads still does a full dot product against it. The readout proves it: 4.83 GFLOP per sequence per step on Qwen3-8B at 8k context, at every stop on the dial. Some kernels literally materialise the repeat — PyTorch's reference path calls repeat_interleave on the key and value tensors before the matmul; a GQA-aware kernel keeps the shared head in registers instead. Either way the FLOP count is the same and the byte count is not.

That is the whole mechanism, and it has a compact statement. The attention part of a decode step reads 2 × layers × kv_heads × head_dim × 2 bytes per token of context and does 4 × layers × q_heads × head_dim FLOPs on it. Divide:

attention FLOPs per byte of cache = q_heads ÷ kv_heads = the group size

The readout labelled "attention flops per byte" is that number, and it is the reason this lesson exists. Arithmetic intensity — FLOPs performed per byte moved from memory — is what decides whether a kernel waits on the memory system or on the tensor cores. An H100 can do about 295 FLOPs in the time it takes to fetch one byte from HBM (High Bandwidth Memory, the DRAM stacked next to the GPU die). Multi-head attention during decode achieves 1.0. It is not close, it is not marginal: the tensor cores are idle for 99.7% of that kernel. GQA with 8 groups gets 4.0, multi-query gets 32. Still bandwidth-bound, but each byte now does eight or thirty-two times as much useful work, and the cache it has to fetch is that many times smaller.

Why nobody ships one kv head

If the cache is linear in kv heads and the arithmetic is free, the dial should obviously go to 1. Almost no current model does. There are two reasons, and the simulation makes the second one visible.

Quality. Ainslie et al. uptrained a T5-XXL checkpoint into each variant using 5% of the original pre-training compute — the key and value projection matrices of each group are mean-pooled into one, which they found works better than picking one head or starting from random. Their measured result across summarisation, translation and question answering: multi-head scored 47.2 with 1.51 s per sample of inference, multi-query scored 46.6 at 0.24 s, and GQA with 8 groups scored 47.1 at 0.28 s. Read those two rows against each other. Almost the entire speed win of multi-query is already there at 8 groups, and almost the entire quality loss is in the last step from 8 groups to 1. Shazeer's original multi-query paper reported "only minor quality degradation"; the GQA paper adds that multi-query "can lead to training instability during fine-tuning, in particular combined with long input tasks". Those are measured results from the papers, not something this simulation models.

Tensor parallelism, which is the boundary you can reach here. Set tensor parallel size to 8 and then sweep the kv dial from 8 down to 1. Nothing happens. The cache per token stays at 18 KiB per rank, the resident sequence count stays put, throughput does not move. The head diagram shows why: the boxes turn magenta and start repeating themselves.

Tensor parallelism splits each weight matrix across GPUs by head, so a rank owns some query heads and the kv heads that go with them. A rank cannot own a third of a kv head. When there are fewer kv heads than ranks, the engine gives every rank its own full copy — literally, in vLLM's LlamaAttention:

if self.total_num_kv_heads >= tp_size:
    # Number of KV heads is greater than TP size, so we partition
    # the KV heads across multiple tensor parallel GPUs.
    assert self.total_num_kv_heads % tp_size == 0
else:
    # Number of KV heads is less than TP size, so we replicate
    # the KV heads across multiple tensor parallel GPUs.
    assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)

max(1, …) is the floor. Eight ranks each holding one replica of a single kv head store exactly as many kv bytes in total as eight distinct kv heads would have, so a multi-query model on eight GPUs has the aggregate cache footprint of a GQA-8 model — and the quality of a multi-query model. This is stated plainly in the GQA paper: "standard sharding for large models replicates the single key and value head by the number of model partitions; GQA removes the waste from such partitioning." Eight groups is not a coincidence, and neither is the fact that Qwen3-8B, Qwen3-32B, Qwen2.5-72B and Mistral-7B all ship exactly 8. It is the largest tensor-parallel degree people commonly deploy on a single node.

Real multi-query models do exist and their configs say so out loud: Falcon-7B sets multi_query: true with num_attention_heads: 71, so 71 query heads share one key and one value head. Seventy-one is prime, incidentally, which means that model has no grouped middle ground available at all — the only whole-number options are 1 and 71.

The other place the dial stops paying. Set context to 512 tokens and leave everything else alone. At 8 kv heads the pool would hold 791 sequences and the scheduler cap of 256 binds first: memory is no longer the constraint, so removing kv heads adds no concurrency. It still speeds up the step — 13.3 ms to 7.6 ms, because there is less cache to re-read — and at one kv head the decode step on this model finally becomes compute-bound, which is the only regime in this entire lesson where buying a GPU with more FLOPs would help. Short contexts and big batches are exactly where GQA matters least.

Checking it on a real system

Read it off the config before you benchmark anything. In config.json:

"num_attention_heads": 32,
"num_key_value_heads": 8,
"head_dim": 128,
"num_hidden_layers": 36

If num_key_value_heads is absent, the model is multi-head and the value equals num_attention_heads. If head_dim is absent, it is hidden_size ÷ num_attention_heads — but check rather than assume, because Qwen3-32B states 128 explicitly while that division gives 80. Then the cache per token is 2 × num_hidden_layers × num_key_value_heads × head_dim × dtype_bytes: for the config above, 2 × 36 × 8 × 128 × 2 = 147,456 bytes, 144 KiB per token, 1.13 GiB for an 8k-token request. Use kv heads. Using query heads is the single most common arithmetic error in capacity planning, and it overstates the cache by the group size — a factor of four here, eight on a 64-head model.

Then check what your serving engine actually built. vLLM prints its pool at boot as one line:

GPU KV cache size: 1,048,576 tokens, Maximum concurrency
for 32,768 tokens per request: 32.00x

Divide that token count by your real p95 context length to get your true concurrency ceiling — vLLM's own division uses the model's maximum context, which is usually far longer than what you serve. If the number is a factor of two to eight below what you expected, compare num_key_value_heads against your --tensor-parallel-size first. Any deployment where kv heads is less than the TP degree is paying for replicas: 8 kv heads on 16 ranks stores twice the cache the config implies. The fix is fewer ranks, or tensor-parallel 8 combined with pipeline or data parallelism for the rest, not a scheduler knob.

To confirm on the hardware that you are still bandwidth-bound after all this — you almost certainly are — sample DCGM's DCGM_FI_PROF_DRAM_ACTIVE against DCGM_FI_PROF_PIPE_TENSOR_ACTIVE during steady-state generation. High DRAM activity with tensor-pipe activity in the low single digits is the signature, and it is what "attention flops per byte = 4" looks like from the outside.

One last trap worth naming, because it costs people a week: you cannot convert an existing multi-head checkpoint to GQA by dropping heads at load time. The mean-pooled projections have to be uptrained, which is what the 5% of pre-training compute in the paper buys. GQA is an architecture decision made before training, not a serving flag. The serving-time levers on the same bytes are KV cache quantization, which halves them again, and paged attention, which stops you wasting the ones you have. What is not a lever on them is FlashAttention: it removes the N × N score matrix from HBM, not the cache.

You serve Qwen2.5-72B (64 query heads, 8 kv heads) on eight H200s with tensor-parallel size 8. A colleague proposes retraining with 4 kv heads to halve the KV cache and double concurrency. What actually happens to the aggregate cache per token?

Next: the general form of the bandwidth-versus-arithmetic argument this lesson keeps leaning on, arithmetic intensity; what the cache costs and why decode re-reads all of it, the KV cache; and the other thing people believe shrinks it, FlashAttention. The head-splitting arrangement GQA modifies is multi-head attention, and the step it performs is scaled dot-product attention.

Why this concept is on the site

Topics are chosen from places engineers visibly get stuck, and the sources are kept with the lesson so the claim is checkable.