LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnInference & Production ScaleMulti-Query & Grouped-Query Attention
🚀HardInference Optimization

Multi-Query & Grouped-Query Attention

Compare MHA, MQA, and GQA architectures, calculate their KV cache footprint, and reason about memory-limited serving tradeoffs.

35 min read
Learning path
Step 137 of 196 in the full curriculum
Inference: TTFT, TPS & KV CacheKV Cache & PagedAttention

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The last chapter sized a key-value (KV) cache and showed why decode often waits on memory traffic, not FLOPs. One choice in that formula belongs to the architecture: how many Key and Value heads each token adds to the cache.

Hold eight query heads fixed. Under standard MHA, one new token writes eight K/V pairs. GQA with two KV heads writes two pairs, and MQA writes one. All three still form eight queries; only the amount of persistent K/V state changes.

That smaller state can admit more live requests and reduce bytes read during decode. Sharing also makes several queries use the same K/V representation, so quality can move with the head count. We can calculate the memory win first, then test whether a model and serving stack preserve the quality and latency we need. Paging, scheduling, and quantization address the remaining budget.

In decoder-side self-attention, standard multi-head attention (MHA) gives each query head its own Key and Value projections.[1]Reference 1Attention Is All You Need.https://arxiv.org/abs/1706.03762 Incremental decode keeps those K/V vectors for every earlier token. As prompts grow and more users share a device, that per-head state can reach hundreds of gigabytes. GQA and MQA shrink the dynamic part by sharing cached K and V across query heads.

What do MQA and GQA change compared with standard multi-head attention?

Answer

They keep multiple query heads but reduce the number of cached key/value heads. MQA shares one KV head across all queries, while GQA shares KV heads within groups of query heads.

Why KV cache memory becomes the bottleneck

On each decode step, a model projects the new token and keeps its Key and Value vectors so later steps don't recompute them. Each new token adds one more row to the cache. In standard MHA, every query head owns a K/V row, so the bill grows with head count as well as token count.

Count the stored numbers first

Start with one layer, two attention heads, and four values per head. Count K vectors first, then V vectors, for one token:

ComponentCountNumbers stored
Key vectors2 heads2 x 4 = 8
Value vectors2 heads2 x 4 = 8
Total per token16

Now multiply by eight requests and 2,048 tokens: 16 x 2,048 x 8 = 262,144 numbers for one layer. Across 80 layers, that becomes 20,971,520 numbers before we count bytes. The arithmetic is easy; keeping that state in GPU memory while High Bandwidth Memory (HBM) reads it on every decode step is the serving constraint.

count-kv-elements.py
1def kv_elements(layers: int, tokens: int, batch: int, kv_heads: int, head_dim: int) -> int: 2 return 2 * layers * tokens * batch * kv_heads * head_dim 3 4tiny_one_layer = kv_elements(layers=1, tokens=2048, batch=8, kv_heads=2, head_dim=4) 5scaled_layers = kv_elements(layers=80, tokens=2048, batch=8, kv_heads=2, head_dim=4) 6print("tiny one-layer elements:", tiny_one_layer) 7print("with 80 layers:", scaled_layers)
Output
1tiny one-layer elements: 262144 2with 80 layers: 20971520

For the tiny 2-head example, how much KV cache does one layer need for 8 concurrent requests at 2,048 tokens each?

Answer

Each token stores 16 numbers. The one-layer cache is 16 x 2,048 x 8 = 262,144 numbers. A real model then multiplies again by layers and bytes per number.

Side-by-side MHA, GQA, and MQA head maps: eight query heads stay fixed while KV heads shrink from eight to two to one, showing which query heads share each cached K/V pair.
Eight query heads stay fixed. MHA stores eight K/V pairs, GQA stores two, and MQA stores one. The cache shrinks with KV-head count, not with the number of questions the model can still ask.

In standard MHA, each of the hhh attention heads maintains its own Key (KKK) and Value (VVV) projections. The attention computation for a single layer uses softmax over scaled query-key scores:

Attention(Q,K,V)=softmax ⁣(QK⊤dk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)VAttention(Q,K,V)=softmax(dk​​QK⊤​)V

Where Q∈Rh×n×dkQ \in \mathbb{R}^{h \times n \times d_k}Q∈Rh×n×dk​, K∈Rh×n×dkK \in \mathbb{R}^{h \times n \times d_k}K∈Rh×n×dk​, and V∈Rh×n×dkV \in \mathbb{R}^{h \times n \times d_k}V∈Rh×n×dk​ for a sequence of length nnn. Each head learns a different attention pattern by attending to different aspects of the input. The KV cache stores KKK and VVV for all previously computed tokens so the model doesn't have to recompute them on each decoding step.

KV cache memory with standard MHA

For standard Multi-Head Attention (MHA) with hhh heads of dimension dkd_kdk​, attach a byte count to that picture. The leading 2 stores one Key and one Value tensor. hhh counts heads, dkd_kdk​ counts values inside each head, and bytes per element comes from the storage format: 2 bytes for FP16 (16-bit floating point), 1 byte for INT8 (8-bit integer), and so on.

Those factors give one token in one layer. Add layers, sequence positions, and requests for a full serving estimate:

KV cache per token per layer=2×h×dk×bytes per element\text{KV cache per token per layer} = 2 \times h \times d_k \times \text{bytes per element}KV cache per token per layer=2×h×dk​×bytes per element

Total KV cache=2×L×S×B×h×dk×bytes per element\text{Total KV cache} = 2 \times L \times S \times B \times h \times d_k \times \text{bytes per element}Total KV cache=2×L×S×B×h×dk​×bytes per element

Where LLL is layers, SSS is sequence length, and BBB is batch size.

Common trap: 2×h×dk×bytes2 \times h \times d_k \times \text{bytes}2×h×dk​×bytes is only the cache for one token in one layer. For a full serving estimate, you still need to multiply by layers, sequence length, and batch size.

Concrete example (72B-style decoder dimensions)

Put concrete numbers into the formula. Qwen2.5-72B[2]Reference 2Qwen2.5 Technical Reporthttps://arxiv.org/abs/2412.15115 has 80 layers, 64 query heads, and a head dimension of 128. Its published architecture uses GQA-8, so the calculation below is a counterfactual MHA cache with the same dimensions:

ParameterValue
dmodeld_{\text{model}}dmodel​8192
hhh (heads)64
dkd_kdk​128
Layers80
Sequence length4096
Batch size32

KV cache=2×80×4096×32×64×128×2 bytes≈344 GB\text{KV cache} = 2 \times 80 \times 4096 \times 32 \times 64 \times 128 \times 2 \text{ bytes} \approx \mathbf{344 \text{ GB}}KV cache=2×80×4096×32×64×128×2 bytes≈344 GB

(Equivalent formulation using dmodel=h×dk=8192d_{\text{model}} = h \times d_k = 8192dmodel​=h×dk​=8192: 2×80×4096×32×8192×22 \times 80 \times 4096 \times 32 \times 8192 \times 22×80×4096×32×8192×2 bytes.)

The first 222 counts K and V, 808080 counts layers, 409640964096 counts tokens per request, and 323232 counts requests. The remaining 64×12864 \times 12864×128 is the per-head width, and the final 222 is FP16 bytes per element.

At roughly 344 GB, this cache is more than twice the model's ~144 GB of FP16 weights. For long-context or high-concurrency decode, moving K/V through HBM can dominate incremental attention.[3]Reference 3Efficiently Scaling Transformer Inference.https://arxiv.org/abs/2211.05102

The same total of 131,072 token positions can arrive as 32 requests at 4K or one request at 128K. The next figure keeps the dimensions and total positions fixed while changing that workload shape, so full MHA still lands near 344 GB.

Bar chart of one 128K request on a 72B-style decoder: full MHA KV cache is 344 GB, one H100 has 80 GB of HBM, GQA-8 cache is 43 GB, and MQA cache is 5.4 GB. Query heads stay at 64; only stored KV heads change. Bars are cache bytes only, not model weights.
At 128K, the stored KV-head count decides whether one request's cache fits on an 80 GB GPU at all. GQA-8 lands under that HBM line; full MHA doesn't. Weights, runtime buffers, and extra requests still sit on top of these bars.

That is the difference between an MHA cache that doesn't fit on one 80 GB GPU and a GQA-8 cache around 43 GB before weights, runtime buffers, and fragmentation. Neither configuration is a deployment decision by itself: weights, quantization, hardware layout, scheduler policy, and measured traffic still set the budget.

compare-attention-kv-footprints.py
1def kv_cache_gb(kv_heads: int, tokens: int = 4096, batch: int = 32) -> float: 2 bytes_used = 2 * 80 * tokens * batch * kv_heads * 128 * 2 3 return bytes_used / 1e9 4 5for name, kv_heads in (("MHA", 64), ("GQA-8", 8), ("MQA", 1)): 6 print(f"{name}: {kv_cache_gb(kv_heads):.2f} GB")
Output
1MHA: 343.60 GB 2GQA-8: 42.95 GB 3MQA: 5.37 GB

What is the most common KV-cache sizing mistake?

Answer

Using the per-token-per-layer formula as if it were the full cache. A full serving estimate must multiply by layers, sequence length, batch size, KV heads, head dimension, K/V tensors, and bytes per element.


Multi-Query Attention (MQA)

One shared K/V pair

MQA takes the extreme cut. All hhh query projections remain, but one Key projection and one Value projection serve every query head.[4]Reference 4Fast Transformer Decoding: One Write-Head is All You Need.https://arxiv.org/abs/1911.02150 Storage therefore drops by the full query-head count. The cost is that every query now reads from the same K/V subspace.

In the head-sharing figure above, that's the rightmost column: eight queries, one cached K/V pair.

How much memory MQA saves

Apply the same count to the tiny example: 2 heads, dimension 4, one token. MHA stores 16 numbers. MQA stores one Key and one Value head, so 2×1×4=82 \times 1 \times 4 = 82×1×4=8 numbers. The reduction is 2x for 2 heads and 64x for 64 heads.

The MHA formula's hhh becomes 111:

MQA KV cache per token per layer=2×1×dk×bytes per element\text{MQA KV cache per token per layer} = 2 \times 1 \times d_k \times \text{bytes per element}MQA KV cache per token per layer=2×1×dk​×bytes per element

Where 111 means a single shared KV head, so cache per token per layer scales with dkd_kdk​ rather than with h⋅dkh \cdot d_kh⋅dk​.

MethodKV headsCache per token per layer (FP16)Savings
MHA642×64×128×2=32 KiB2 \times 64 \times 128 \times 2 = 32\text{ KiB}2×64×128×2=32 KiB1x
MQA12×1×128×2=512 B2 \times 1 \times 128 \times 2 = 512\text{ B}2×1×128×2=512 B64x

For the same 72B-style dimensions with batch=32 and seq=4096, MHA uses 344 GB of KV cache. MQA cuts that by 64x to ~5.4 GB.

Your 64-head serving model runs out of memory under long chats. You swap from MHA to MQA. What single term in the KV-cache formula changed, and why does that produce a 64x reduction?

Answer

The KV-head count dropped from 64 to 1. MHA stores K and V for all 64 heads, while MQA stores one shared K head and one shared V head, so the cache shrinks by the same 64x factor.

Performance vs. quality tradeoffs in MQA

The byte win has a representational price. Query heads can still ask different questions, but all of them read one shared K/V subspace. Tasks that need several distinct relation or value views may lose quality.

Common mistake: Treating MQA quality loss as either zero or catastrophic. Real impact depends on model size and task. Shazeer's original paper motivates MQA as a serving optimization, and later GQA work shows why many larger models want more than one KV head.[4]Reference 4Fast Transformer Decoding: One Write-Head is All You Need.https://arxiv.org/abs/1911.02150[5]Reference 5GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245

That failure mode gives us a useful test for the next architecture. If latency improves while relation-heavy evals regress, query diversity survived but K/V diversity did not. GQA restores some K/V subspaces without returning to one pair per query head.

After an MQA migration, latency is great but relation-heavy evals regress. What bottleneck did you probably introduce?

Answer

All query heads still ask different questions, but they now read from one shared K/V representation. That single shared subspace can bottleneck tasks that need several distinct attention patterns.

A minimal MQA decode step

Keep projection details out of the first decode sketch. Q has one vector per query head; K and V are single shared sequences. Each query scores the same cached K and blends the shared V, so the cache holds one K sequence and one V sequence rather than hhh of each.

mqa-decode-step.py
1import math 2 3def dot(a: list[float], b: list[float]) -> float: 4 return sum(x * y for x, y in zip(a, b, strict=True)) 5 6def softmax(xs: list[float]) -> list[float]: 7 peak = max(xs) 8 exps = [math.exp(x - peak) for x in xs] 9 total = sum(exps) 10 return [e / total for e in exps] 11 12def mqa_decode_step( 13 query_heads: list[list[float]], 14 keys: list[list[float]], 15 values: list[list[float]], 16) -> list[list[float]]: 17 """Attend h query heads over one shared K/V sequence.""" 18 scale = math.sqrt(len(query_heads[0])) 19 outputs: list[list[float]] = [] 20 for query in query_heads: 21 weights = softmax([dot(query, key) / scale for key in keys]) 22 outputs.append([ 23 sum(weight * value[dim] for weight, value in zip(weights, values, strict=True)) 24 for dim in range(len(values[0])) 25 ]) 26 return outputs 27 28# Two query heads, one shared cache of two tokens, d_k = 2. 29q_heads = [[1.0, 0.0], [0.0, 1.0]] 30shared_k = [[1.0, 0.0], [0.0, 1.0]] 31shared_v = [[1.0, 0.0], [0.0, 1.0]] 32out = mqa_decode_step(q_heads, shared_k, shared_v) 33assert len(out) == 2 34assert len(out[0]) == 2 35print("head0 output:", [round(x, 3) for x in out[0]]) 36print("head1 output:", [round(x, 3) for x in out[1]]) 37print("shared KV tokens:", len(shared_k))
Output
1head0 output: [0.67, 0.33] 2head1 output: [0.33, 0.67] 3shared KV tokens: 2

Why can the two query heads produce different outputs if they share one K/V cache?

Answer

They still have different query vectors, so they put different softmax weights on the same cached keys. Sharing K/V changes stored state, not the fact that each head can ask a different question.


Grouped-Query Attention (GQA)

Groups of queries, fewer K/V heads

MQA saves the most memory, but one K/V pair can be too narrow for some workloads. GQA keeps several groups: query heads within a group share one Key and one Value, while different groups keep different K/V pairs. That sits between MQA's one pair for everyone and MHA's one pair per query head.

GQA[5]Reference 5GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245 uses ggg KV heads where 1<g<h1 < g < h1<g<h:

hg=queries per group\frac{h}{g} = \text{queries per group}gh​=queries per group

Each group of h/gh/gh/g queries shares one K and one V. Cached KKK and VVV therefore have shape g×n×dkg \times n \times d_kg×n×dk​, not h×n×dkh \times n \times d_kh×n×dk​. With h=8h=8h=8 and g=2g=2g=2, four query heads read each KV pair. Mistral 7B uses the same 32-query, 8-KV shape, so its cache is 4x smaller than MHA's while retaining more than MQA's single shared subspace.[6]Reference 6Mistral 7B.https://arxiv.org/abs/2310.06825

map-query-heads-to-kv-groups.py
1def kv_group_for_query(query_head: int, query_heads: int, kv_heads: int) -> int: 2 assert query_heads % kv_heads == 0 3 return query_head // (query_heads // kv_heads) 4 5assignments = [kv_group_for_query(head, query_heads=8, kv_heads=2) for head in range(8)] 6print("query to KV group:", assignments) 7print("cache reduction:", 8 // 2, "x")
Output
1query to KV group: [0, 0, 0, 0, 1, 1, 1, 1] 2cache reduction: 4 x

GQA in practice

Published dense layouts make the ratios concrete. They are reference configurations, not a 2026 shortlist. Llama 2 used GQA at 34B and 70B.[7]Reference 7Llama 2: Open Foundation and Fine-Tuned Chat Models.https://arxiv.org/abs/2307.09288 Llama 3 kept 8 KV heads across its released dense sizes: 8B has 32 query heads, 70B has 64, and 405B has 128.[8]Reference 8The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783

ModelQuery heads (hhh)KV heads (ggg)Ratio
Qwen2.5-72B[2]Reference 2Qwen2.5 Technical Reporthttps://arxiv.org/abs/2412.151156488:1
Llama 3 70B[8]Reference 8The Llama 3 Herd of Models.https://arxiv.org/abs/2407.217836488:1
Llama 3 8B[8]Reference 8The Llama 3 Herd of Models.https://arxiv.org/abs/2407.217833284:1
Llama 3 405B[8]Reference 8The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783128816:1
Llama 2 70B[7]Reference 7Llama 2: Open Foundation and Fine-Tuned Chat Models.https://arxiv.org/abs/2307.092886488:1
Mistral 7B[6]Reference 6Mistral 7B.https://arxiv.org/abs/2310.068253284:1
Gemma 2 9B[9]Reference 9Gemma 2: Improving Open Language Models at a Practical Sizehttps://storage.googleapis.com/deepmind-media/gemma/gemma-2-report.pdf1682:1

🔬 Research insight: Gemma 2 chose a 2:1 query-to-KV ratio on its 9B model (16 query heads, 8 KV heads) after ablations kept downstream scores close to MHA while improving inference speed.[9]Reference 9Gemma 2: Improving Open Language Models at a Practical Sizehttps://storage.googleapis.com/deepmind-media/gemma/gemma-2-report.pdf

Head ratios meet another constraint when you shard a model. Tensor parallelism splits work across accelerators, so a simple head-sharded plan needs query heads to divide evenly by the tensor-parallel (TP) degree.

vLLM divides KV heads while possible. When TP exceeds the KV-head count, it replicates KV ownership so every shard has a KV head.[10]Reference 10vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionhttps://github.com/vllm-project/vllm Other runtimes and newer layouts can shard along the sequence dimension too. Treat the helper below as a mental model for divisibility and replication, not as a universal runtime contract.

check-vllm-kv-head-sharding.py
1def vllm_sharding_plan(query_heads: int, kv_heads: int, tensor_parallel: int) -> str: 2 if query_heads % tensor_parallel != 0: 3 return "invalid: query heads must divide evenly across TP shards" 4 if tensor_parallel <= kv_heads: 5 return f"even: {kv_heads // tensor_parallel} KV heads per shard" 6 if tensor_parallel % kv_heads != 0: 7 return "replicated: runtime-specific KV ownership" 8 replicas_per_kv_head = tensor_parallel // kv_heads 9 return f"replicated: each KV head appears on {replicas_per_kv_head} shards" 10 11print("TP=4:", vllm_sharding_plan(query_heads=64, kv_heads=8, tensor_parallel=4)) 12print("TP=16:", vllm_sharding_plan(query_heads=64, kv_heads=8, tensor_parallel=16))
Output
1TP=4: even: 2 KV heads per shard 2TP=16: replicated: each KV head appears on 2 shards

A teammate says "32 query heads and 8 KV groups means 8x savings." What is correct, and why?

Answer

It's 4x savings, not 8x. The factor comes from num_query_heads / num_key_value_heads = 32 / 8 = 4, so each KV head is shared by four query heads.

Why can a 64-query-head, 8-KV-head model lose part of its cache saving at TP=16 in vLLM?

Answer

The 64 query heads still divide cleanly across 16 shards, but there are only 8 KV heads. vLLM gives every shard at least one KV head, so each KV head is replicated on two shards. The model still uses GQA, but the physical serving layout duplicates some cached state.

Memory comparison (72B model, seq=4096)

These are per-request KV cache sizes at the given sequence length:

MethodKV groupsKV cache per requestQuality posture
MHA64~10.7 GBReference architecture
GQA-88~1.34 GBValidate converted or trained model on task evals
MQA1~0.17 GBStrongest sharing constraint; evaluate carefully

In Ainslie et al., intermediate group counts recovered much of MHA quality while retaining MQA-like inference benefits after uptraining.[5]Reference 5GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245 The correct group count for another model remains an architecture and evaluation choice, not a guarantee inherited from that experiment.

You are serving a large support model and need more concurrency without hurting answer quality too much. Why is GQA often safer than pure MQA?

Answer

GQA keeps several KV subspaces instead of collapsing everything into one shared pair. That preserves more representational diversity while still delivering most of the KV-cache savings.

Adoption notes

ArchitectureTypical usageDesign rationale
MHAOlder decoder designs, or deployments where KV memory is less constrainedMaximum per-head flexibility, highest KV-cache cost
MQAServing-first deployments that need the smallest possible KV cacheAggressive memory reduction with the strongest sharing constraint
GQADefault in many current open decoders, including Llama 3 at 8B/70B/405BIntermediate KV-head count; evaluate quality and serving together

Choosing a ratio

MQA targets serving efficiency: shared KV heads cut cache size and reduce memory traffic during incremental decode.[4]Reference 4Fast Transformer Decoding: One Write-Head is All You Need.https://arxiv.org/abs/1911.02150[3]Reference 3Efficiently Scaling Transformer Inference.https://arxiv.org/abs/2211.05102 GQA keeps several subspaces, but its ratio remains model-specific. Llama 3 kept 8 KV heads even at 405B, so the query-to-KV ratio grew with width.[8]Reference 8The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783

There isn't a universal best group count. Model size, quality target, and serving stack decide whether 2:1, 4:1, 8:1, or 16:1 fits the workload.[5]Reference 5GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245


Converting MHA to GQA via uptraining

A team inheriting an MHA checkpoint doesn't have to restart pretraining. It can reduce the K/V heads, initialize the new projections by pooling old ones, and then let the model adapt through additional language-model training. This process is called uptraining.

The pooled checkpoint is an initialization, not an exact preservation of the old attention computation. Plan to validate quality and serving behavior after adaptation.

The conversion has two parts:

  1. Mean-pool KV heads: Group existing Key and Value heads into partitions (for example, 64 heads into 8 groups of 8), then average each group's KKK and VVV projection matrices. The averages retain signal from the old heads as a useful starting point, but they don't reproduce the old attention computation.
  2. Fine-tune (uptrain): Continue the standard next-token objective for a small fraction of the original pretraining budget. The model can then adapt its internal representations to the merged projections.
Diagram showing MHA checkpoint, Mean-pool K/V groups, Uptrain ~5% budget, and Quality + serving eval.
MHA checkpoint, Mean-pool K/V groups, Uptrain ~5% budget, and Quality + serving eval.
Eight rows of eight MHA K/V heads, each row one color, mean-pooled into eight GQA heads of matching color. Consecutive heads 0-7 become KV0, heads 8-15 become KV1, and so on. The pooled heads are an initialization, not the original attention computation.
Keep published head order. Each colored row is one group: eight consecutive MHA heads average into one GQA head. That pooled tensor is only the starting point for uptraining.

The original GQA paper[5]Reference 5GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.https://arxiv.org/abs/2305.13245 found quality close to MHA after using 5% of the original pretraining compute in T5.1.1 encoder-decoder experiments. That evidence has boundaries: it didn't compare decoder-only models or a same-size GQA model trained from scratch.

A new model can bake its KV-head pattern into the architecture from day one. Uptraining is for an inherited MHA checkpoint where restarting pretraining isn't practical.

mean-pool-kv-heads-for-uptraining.py
1def mean_pool_heads(heads: list[list[float]], group_size: int) -> list[list[float]]: 2 assert len(heads) % group_size == 0 3 pooled: list[list[float]] = [] 4 for start in range(0, len(heads), group_size): 5 group = heads[start : start + group_size] 6 pooled.append([ 7 sum(values) / len(group) 8 for values in zip(*group) 9 ]) 10 return pooled 11 12mha_k_heads = [[1.0, 3.0], [3.0, 5.0], [10.0, 12.0], [14.0, 16.0]] 13print("GQA initial K heads:", mean_pool_heads(mha_k_heads, group_size=2))
Output
1GQA initial K heads: [[2.0, 4.0], [12.0, 14.0]]

How do you initialize GQA from an existing MHA checkpoint?

Answer

Group the old K/V heads and mean-pool their projection weights into fewer KV heads. Then uptrain the converted model so it adapts to the reduced K/V capacity.


Beyond GQA: Multi-Head Latent Attention (MLA)

GQA reduces the number of distinct KV heads stored in the cache. Multi-Head Latent Attention (MLA) takes a different route: it reduces the dimensionality stored for each position through a learned low-rank latent representation.

How MLA works (high-level)

Instead of caching full per-head Key and Value content vectors for every token, MLA follows this path:

  1. Input hidden states are projected into a compact latent vector of dimension dcd_cdc​.

  2. Only this compact latent state (plus a small amount of decoupled positional information) is written into the KV cache.

  3. The architecture defines learned up-projections from that latent state. In an optimized inference implementation, those projection matrices can be absorbed into the query and output paths so decode doesn't materialize full per-head cached K/V again.[11]Reference 11DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Modelhttps://arxiv.org/abs/2405.04434

DeepSeek-V2 also separates a positional RoPE component from compressed content so the cache can retain the required position-dependent term without preventing content compression.[11]Reference 11DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Modelhttps://arxiv.org/abs/2405.04434

DeepSeek-V2 reports a 93.3% reduction in deployed KV-cache memory footprint compared with DeepSeek 67B.[11]Reference 11DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Modelhttps://arxiv.org/abs/2405.04434 That deployed comparison also includes KV-cache quantization, so don't treat 93.3% as an MLA-only head-count ratio. MLA's architectural payload depends on the chosen latent width dcd_cdc​, positional component, and execution path. DeepSeek-V3 keeps the same cache contract: a 512-wide content latent plus a 64-wide RoPE key.[12]Reference 12DeepSeek-V3 Technical Report.https://arxiv.org/abs/2412.19437

Same-axis bar chart of cached values per token per layer: GQA-8 stores 2048 values from eight 128-d key heads and eight 128-d value heads, while DeepSeek-V2 MLA stores 576 values from a 512-d content latent plus a 64-d RoPE key. This is an architecture payload comparison, not DeepSeek-V2 deployed savings versus DeepSeek 67B.
Count stored values, not marketing ratios. GQA-8 keeps eight full 128-d K/V pairs (2048 values). DeepSeek-V2 MLA stores 512 + 64 = 576 values, the same width as 2.25 GQA heads of dimension 128. The paper's 93.3% figure is a different, deployed comparison against DeepSeek 67B.

You need long-context serving on a runtime that already supports GQA kernels but not MLA-specific execution. Why is GQA operationally simpler, even if MLA compresses harder on paper?

Answer

GQA keeps a conventional K/V-head contract that existing grouped-attention kernels and sharding layouts support. MLA can compress harder, but the model and runtime must implement its latent-cache attention path correctly.

Trade-offs and adoption

AspectGQAMLA
MechanismFewer KV heads (g<hg < hg<h)Low-rank latent compression + up-projection
Kernel compatibilityFits GQA-aware attention kernelsNeeds an MLA-aware latent-cache implementation
Compression measureKV-head ratio gives exact cache-factor comparisonCached latent width and positional component determine savings
Published examplesLlama 3 70B, Qwen2.5-72B, Mistral 7B, Gemma 2 9BDeepSeek-V2, DeepSeek-V3

Runtime support decides whether the smaller representation helps. GQA slots into grouped-attention implementations and familiar tensor-parallel strategies. MLA changes the cache contract, so it needs a compatible execution path.

Choose GQA when that path is mature; consider MLA when its model-specific kernels and cache handling are ready.

DeepSeek-V2 documents a 512-dimensional compressed K/V latent and a 64-dimensional decoupled key component. That means MLA stores 576 values per layer and token before storage-precision choices. For a shape comparison, an illustrative GQA layout with 128-dimensional heads stores 2×g×1282 \times g \times 1282×g×128 values, so MLA's 576-value payload sits at the same width as 2.25 GQA heads. This is an architecture-level payload comparison, not a claim that unrelated models will have the same latency or quality.[11]Reference 11DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Modelhttps://arxiv.org/abs/2405.04434

compare-cached-state-widths.py
1def gqa_cached_values_per_token(kv_heads: int, head_dim: int) -> int: 2 return 2 * kv_heads * head_dim 3 4gqa_8_values = gqa_cached_values_per_token(kv_heads=8, head_dim=128) 5mla_example_values = 512 + 64 # compact content latent plus positional component 6equivalent_gqa_heads = mla_example_values / (2 * 128) 7print("GQA-8 cached values:", gqa_8_values) 8print("MLA-style cached values:", mla_example_values) 9print("MLA-style width in GQA-head units:", equivalent_gqa_heads)
Output
1GQA-8 cached values: 2048 2MLA-style cached values: 576 3MLA-style width in GQA-head units: 2.25

Your stack already ships MLA kernels and cache pressure is still dominant after other optimizations. When does MLA become worth extra complexity?

Answer

MLA becomes worth it when the runtime already supports its fused kernels and the main remaining limiter is KV-cache footprint. In that setting, extra compression can buy more concurrency than GQA alone.


Serving impact

Concurrency impact (72B-style dimensions, 4K context)

Reducing KV cache doesn't guarantee equal throughput, but it raises the number of requests that fit under a fixed cache budget. Using the same 80-layer, 64-query-head, 128-dim-head example and FP16 cache:

Three 8-by-8 grids for the same KV-cache memory budget. Each filled cell is one admitted request: MHA fits 1, GQA-8 fits 8, and MQA fits 64. The grids show a KV-limited concurrency ceiling, not measured tokens per second.
Same KV-cache budget, counted as live requests. MHA admits 1, GQA-8 admits 8, and MQA admits 64. Filled cells are a memory-limited ceiling; kernels, weights, prefill, and scheduler policy decide realized throughput.
ConfigKV Cache per RequestRelative Memory-Limited Concurrency CeilingRelative KV bytes / decode step (same sss)
MHA~10.7 GB1x1x
GQA-8~1.34 GB~8x~1/8
MQA~0.17 GB~64x~1/64

These ratios are capacity math, not measured throughput. If KV state is what limits batch size, they approximate the memory-limited ceiling.

Fewer KV heads also mean less HBM traffic per decode step while attention reads the growing cache, which can improve inter-token latency when attention bandwidth binds. That gain isn't automatic TPS: weight streaming, kernels, prefill mix, and the scheduler can dominate. Benchmark the workload before turning a cache ratio into a performance claim.[3]Reference 3Efficiently Scaling Transformer Inference.https://arxiv.org/abs/2211.05102

Long-context impact

Longer sequences make the same ratio more expensive in absolute bytes. For the same 72B-style dimensions and FP16 cache, per-request state grows linearly with sequence length:

Sequence LengthMHA KV Cache (approx.)GQA-8 KV Cache (approx.)Savings
4K~10.7 GB~1.34 GB8x
32K~85.9 GB~10.7 GB8x
128K~343.6 GB~42.9 GB8x
1M~2.75 TB~343.6 GB8x

These are derived cache sizes, not claims that a model or runtime supports every listed context. At 128K, full MHA uses roughly 344 GB of KV state per request in this example, before weights, allocator fragmentation, or extra concurrency. GQA-8 drops that cache to about 43 GB, which is still expensive.

Head sharing is one part of the end-to-end memory budget, not the budget itself.

budget-long-context-cache.py
1def kv_cache_gb(kv_heads: int, tokens: int) -> float: 2 return 2 * 80 * tokens * kv_heads * 128 * 2 / 1e9 3 4mha_128k = kv_cache_gb(kv_heads=64, tokens=131_072) 5gqa_128k = kv_cache_gb(kv_heads=8, tokens=131_072) 6print(f"MHA 128K cache: {mha_128k:.1f} GB") 7print(f"GQA-8 128K cache: {gqa_128k:.1f} GB") 8print("GQA cache plus 144 GB weights fits in 4x80 GB raw:", gqa_128k + 144 <= 320)
Output
1MHA 128K cache: 343.6 GB 2GQA-8 128K cache: 42.9 GB 3GQA cache plus 144 GB weights fits in 4x80 GB raw: True

Common mistake: Treating long-context support as an architectural context-window claim rather than a serving budget. A long support history, codebase prompt, or retrieved document bundle can consume the same KV allocation. Calculate active-token memory before promising concurrency.

You are targeting 128K context on an 80 GB GPU. Why is GQA-8 alone still not enough, and what else usually joins it?

Answer

GQA-8 cuts the example from about 344 GB to about 43 GB per request, but 43 GB is still too large once weights, batching, fragmentation, and other requests are included. Long-context systems still need paging, quantized KV cache, scheduling, or retrieval.


How serving engines handle GQA head expansion

The serving kernel still has to map the smaller KV-head cache onto the larger query-head set. An index expresses that mapping without copying K/V. Production kernels, including FlashAttention's current flash_attn_with_kvcache path and FlashInfer[13]Reference 13FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.https://arxiv.org/abs/2501.01005, keep the grouping without materializing repeated K/V.

gqa-head-index.py
1def kv_index_for_query(query_head: int, query_heads: int, kv_heads: int) -> int: 2 assert query_heads % kv_heads == 0 3 return query_head // (query_heads // kv_heads) 4 5def gqa_scores( 6 queries: list[list[float]], 7 kv_keys: list[list[float]], 8) -> list[float]: 9 """One decode token: each query head dots with its group's cached key.""" 10 assert len(queries) % len(kv_keys) == 0 11 scores: list[float] = [] 12 for i, query in enumerate(queries): 13 kv_key = kv_keys[kv_index_for_query(i, len(queries), len(kv_keys))] 14 scores.append(sum(q * k for q, k in zip(query, kv_key, strict=True))) 15 return scores 16 17queries = [[1.0, 0.0] for _ in range(4)] + [[0.0, 1.0] for _ in range(4)] 18kv_keys = [[1.0, 0.0], [0.0, 1.0]] 19scores = gqa_scores(queries, kv_keys) 20assert scores == [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] 21print("query to KV index:", [kv_index_for_query(i, 8, 2) for i in range(8)]) 22print("per-head scores:", scores)
Output
1query to KV index: [0, 0, 0, 0, 1, 1, 1, 1] 2per-head scores: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]

The index preserves the grouping while the smaller cache stays in place. The next chapter explains the serving-side cache layout; FlashAttention[14]Reference 14FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.https://arxiv.org/abs/2307.08691 gets its own kernel-level chapter after prefix caching.

Production tip: Model size alone isn't enough for serving. num_kv_heads, context length, and KV-cache dtype often dominate concurrency. GQA-8 raises the memory-limited ceiling by roughly 8x versus same-dimension MHA in this arithmetic example, but realized cost per query still depends on batching, quantization, and kernels.

Why should a serving engine avoid materializing repeat_interleave for GQA?

Answer

Materializing repeated K/V heads recreates the larger MHA-shaped tensor and wastes memory. Native kernels compute grouped attention directly from the smaller KV-head cache.

KV cache update correctness

Once head grouping is correct, many GQA failures come from updating the cache one token at a time. The attention formula can be right while a cursor or position write is wrong.

GQA decode step with eight query heads and two KV heads: the new token still projects eight queries, the cache writes only KV0 and KV1, then query heads 0-3 read KV0 and heads 4-7 read KV1. Incremental logits should match a full-prefix recompute.
Write the narrow KV-head slice, then let every query head read its group. If cached decode logits drift from a full-prefix recompute, the bug is usually cursor, mask, or position IDs, not the attention formula.

Trace one decode step in order:

  1. Project only the new token into Q, K, and V.
  2. Apply RoPE or another positional transform using the absolute position of that token.
  3. Append the new K/V slice at the next cache position.
  4. Run attention with Q from the new token against all cached K/V positions.
  5. Track cache length per request, because different requests finish at different times.

The cache shape should use KV heads, not query heads:

text
1K_cache: (batch, n_kv_heads, max_seq, d_k) 2V_cache: (batch, n_kv_heads, max_seq, d_k)

In an MHA model, n_kv_heads == n_heads. MQA sets n_kv_heads == 1. GQA sits between them with 1 < n_kv_heads < n_heads.

enforce-cache-append-cursor.py
1def append_kv(cache: list[str], cursor: int, token_value: str) -> int: 2 if cursor != len(cache): 3 raise ValueError("cursor would overwrite or skip cache state") 4 cache.append(token_value) 5 return cursor + 1 6 7cache: list[str] = [] 8cursor = append_kv(cache, cursor=0, token_value="token-0-kv") 9cursor = append_kv(cache, cursor=cursor, token_value="token-1-kv") 10print("cache length:", len(cache)) 11try: 12 append_kv(cache, cursor=1, token_value="stale-write") 13except ValueError as exc: 14 print("blocked:", exc)
Output
1cache length: 2 2blocked: cursor would overwrite or skip cache state

Common cache-update bugs look like this:

SymptomLikely bugCheck
answer quality degrades after a few tokensoverwrote position t - 1 instead of appending at tprint cache length after every decode step
works at batch size 1, fails under batchingused one global cache length for all requestsstore per-request positions
GQA memory is still hugeallocated cache with query heads instead of KV headsassert K_cache.size(1) == n_kv_heads
long-context output becomes incoherentapplied RoPE with local chunk position instead of absolute positionlog the position id used for each appended token

That's why cache tests should compare a cached decode path against a full-prefix recompute path on the same tiny prompt. The next-token logits (raw scores before probabilities) should match closely. If they don't, the bug is usually position IDs, mask shape, or cache append order.

Cached decode starts drifting after token 200, while full-prefix recompute still looks correct. Which invariants do you inspect first?

Answer

First, verify the cache shape is (batch, n_kv_heads, max_seq, d_k), not query heads. Then compare cached decode logits against a full-prefix recompute on a tiny prompt, because late-token drift usually comes from wrong position IDs, bad mask shape, or incorrect append order.


Common pitfalls

The head ratio answers only one question: how much K/V state the architecture creates. The mistakes below happen when that answer gets mistaken for a quality result, a training speedup, or a complete deployment budget.

"Model weights are the same thing as KV cache"

  • Symptom: A quantized model fits on paper, then the server still runs out of memory under long context or high concurrency.

  • Cause: Weights are static model parameters. The KV cache is dynamic per-request state. GQA mainly shrinks dynamic K/V activations, not the feed-forward layers or the full weight tensor.

  • Fix: Keep weights and KV cache in separate budgets. Add activation buffers, allocator slack, scheduler state, and fragmentation before admitting requests.

"GQA speeds up training"

  • Symptom: A training benchmark changes little after switching from MHA to GQA.

  • Cause: Training often has a different bottleneck from incremental decoding. GQA reduces decode cache state, while training processes whole sequences and may remain dominated by large attention and feed-forward computations.

  • Fix: Measure decode throughput (tokens per second during autoregressive generation), not training throughput. GQA helps when each new token repeatedly loads cached K/V, not when training computes a full sequence in parallel.

"MQA always destroys quality"

  • Symptom: MQA gets rejected even for a small, latency-sensitive model whose task eval shows no visible quality change.

  • Cause: MQA imposes the strongest sharing constraint, but impact depends on model size, task, and training recipe. Treating it as always catastrophic is as wrong as treating it as free.

  • Fix: Compare task evals and serving metrics on your workload. Choose GQA when you need more representational room without returning to MHA's cache cost.

"I saved 8x on KV cache, so my serving cost dropped 8x"

  • Symptom: A large KV-cache reduction produces only a small latency or cost improvement.

  • Cause: KV cache is one piece of the puzzle. Model weights, feed-forward network (FFN) compute, attention arithmetic, scheduler overhead, and interconnect traffic still matter. If your batch was previously limited by compute rather than memory, shrinking the cache won't move the needle as much.

  • Fix: Profile end-to-end latency at realistic batch sizes. Use NVIDIA Nsight Systems or vLLM metrics to identify a memory-bound versus compute-bound workload before attributing a gain to GQA.

"repeat_interleave exploded my memory during GQA training"

  • Symptom: A training loop runs out of memory after expanding KV heads with repeat_interleave.

  • Cause: repeat_interleave materializes a larger tensor in memory. For 32 query heads and 8 KV groups, that temporarily creates a 4x larger K/V tensor before the matmul.

  • Fix: Use FlashAttention or FlashInfer, which handle GQA without materializing the expanded tensor. A custom kernel should broadcast or fuse the grouping instead of creating the expanded tensor.

"I can regroup heads any way I want during GQA conversion"

  • Symptom: A converted checkpoint loads, but quality drops or tensor-parallel shards disagree during rollout.

  • Cause: Head grouping isn't arbitrary. Query and KV heads often follow a fixed ordering in the weight matrices, and tensor-parallel sharding can interleave or chunk that order in specific ways. Regrouping without respecting the original layout silently changes which heads share a K/V projection.

  • Fix: Preserve the model's published head ordering. Run a tiny parity test and shard-level smoke test before broader evals. Don't assume head 0-7 is always the intended first group.

"Architecture-level KV savings always survive tensor parallelism"

  • Symptom: Tensor-parallel serving uses more memory than the head-ratio estimate predicts.

  • Cause: Physical cache layout is runtime-specific. In vLLM, query heads must divide evenly across TP shards, and KV heads are replicated when TP degree exceeds KV-head count so each shard owns at least one.

  • Fix: Check model config and runtime sharding rules before choosing TP degree. Benchmark the physical layout you intend to deploy.[10]Reference 10vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttentionhttps://github.com/vllm-project/vllm


Validate a GQA conversion before rollout

Before rollout, keep the evidence chain short. Preserve published head ordering, convert the checkpoint, and compare cached decoding with full-prefix recomputation on a tiny fixture.

Then compare held-out quality with the original MHA checkpoint and measure physical KV allocation at the intended TP degree and context length. For any throughput or latency claim, record hardware, runtime version, workload, precision, and exact baseline.

Promote only when quality stays inside budget and the measured deployment gains memory headroom or decode throughput without cache drift.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A decoder keeps 32 query heads but caches 8 key heads and 8 value heads per layer. Which architecture and sharing pattern does this describe?

Correct answer: GQA, where each KV head is shared by a group of 4 query heads.

GQA keeps multiple query heads but uses an intermediate number of KV heads. With 32 query heads and 8 KV heads, each KV head serves 32 / 8 = 4 query heads. MHA would use 32 KV heads, while MQA would use only one shared KV head.

2.A decoder has 40 layers, 32 query heads, 8 KV heads, head dimension 128, FP16 KV-cache elements, batch size 8, and sequence length 2,048. Approximately how large is the KV cache, and how does it compare with same-size MHA?

Correct answer: About 2.7 GB, which is 4x smaller than MHA because 32 query heads share 8 KV heads.

The full GQA cache is 2 x 40 x 2,048 x 8 x 8 x 128 x 2 bytes = 2,684,354,560 bytes, about 2.7 GB using decimal gigabytes (1e9), matching the article's cache formula. Same-size MHA would use 32 KV heads instead of 8, so it would be 32/8 = 4x larger, about 10.7 GB.

3.A 64-query-head model moves from MHA to GQA-8, and its per-request KV cache drops by about 8x. In production, end-to-end tokens per second improves only slightly. Which explanation is most plausible?

Correct answer: KV savings raise the memory-limited concurrency ceiling, but throughput can still be limited by weights, FFN compute, prefill, scheduling, kernels, or interconnect traffic.

Shrinking the KV cache removes one important memory bottleneck, especially for long context or high concurrency. It doesn't remove model weights, feed-forward compute, prefill cost, scheduler overhead, kernel effects, or interconnect traffic. The cache ratio is a ceiling for memory-limited admission, not a guaranteed wall-clock speedup.

4.Cached GQA decode matches full-prefix recompute for short prompts but starts drifting after a few hundred tokens. Which checks are most directly tied to the cache-update invariant?

Correct answer: Verify K_cache and V_cache use shape (batch, n_kv_heads, max_seq, d_k), append at each request's own cursor, use absolute position IDs, and compare cached logits against full-prefix recompute.

GQA cache correctness depends on writing the smaller KV-head state at the next position, tracking positions per request, and applying positional transforms with the token's absolute position. A tiny parity test against full-prefix recompute catches wrong position IDs, bad masks, incorrect append order, or allocating the cache with query heads instead of KV heads.

5.After an MQA migration, latency is great but relation-heavy evals regress. What bottleneck did you probably introduce?

Correct answer: All query heads still ask different questions, but they now read from one shared K/V representation. That single shared subspace can bottleneck tasks that need several distinct attention patterns.

MQA preserves multiple query heads, so the model can still form different queries. The constraint is on the K/V side: every query head attends through the same shared key and value representation. That can reduce representational diversity for tasks that benefit from several distinct attention subspaces.

6.An older MHA checkpoint has 64 K/V heads. You want to convert it to a GQA model with 8 K/V heads without training from scratch. What plan matches the uptraining recipe?

Correct answer: Group the old K and V heads into 8 partitions, mean-pool the K/V projection weights within each group, then continue language-model training and validate quality and serving behavior.

GQA uptraining initializes fewer K/V heads by averaging the old K and V projection weights within each group. That initialization doesn't exactly preserve the original MHA computation, so continued next-token training lets the model adapt. The converted model still needs quality and serving validation.

7.Your runtime has mature GQA kernels but no MLA-specific latent-cache path. Cache pressure matters, but you need a near-term deployment. Why is GQA operationally safer than MLA in this setting?

Correct answer: GQA keeps a conventional K/V-head cache that grouped-attention kernels can serve, while MLA changes the cached representation and needs model and runtime support for the latent path.

GQA reduces the number of full K/V heads but keeps the familiar K/V-cache contract. MLA stores a compact latent representation plus positional information and depends on an MLA-aware inference path that avoids materializing full cached K/V again. Without that runtime support, MLA's paper compression isn't a deployment shortcut.

8.A model has 64 query heads and 8 KV heads. In vLLM, tensor parallel degree 16 is selected. What physical-cache effect should you expect?

Correct answer: The 64 query heads split evenly, but only 8 KV heads exist, so each KV head is replicated on two shards.

In the described vLLM rule, query heads must divide evenly across the tensor-parallel degree, and 64 divides by 16. But TP=16 exceeds the 8 KV heads, so the runtime replicates KV ownership so every shard has KV state; each KV head appears on 16/8 = 2 shards.

9.A reference GQA implementation uses repeat_interleave to expand 8 KV heads to 32 query heads before attention. Why should a production decode kernel avoid that materialization?

Correct answer: It recreates the larger MHA-shaped K/V tensors in memory; a native grouped kernel reads the smaller KV cache directly.

repeat_interleave is useful for explaining the shape mapping, but materializing repeated K/V heads throws away much of GQA's memory advantage. GQA-aware kernels compute grouped attention directly from the smaller KV-head cache instead of building an expanded MHA-shaped tensor.

10.With 80 layers, 64 query heads, head dimension 128, FP16 cache, and one 128K-token request, full MHA needs about 344 GB of KV cache. GQA-8 cuts that to about 43 GB. What conclusion should drive deployment planning on an 80 GB GPU?

Correct answer: GQA-8 is necessary but not sufficient; weights, buffers, fragmentation, batching, and scheduling still need a full memory budget.

GQA-8 greatly reduces the cache, but 43 GB is cache only. A real deployment also needs memory for model weights, runtime buffers, allocator slack, fragmentation, scheduler state, and any additional active requests. Long-context serving often combines head sharing with paging, scheduling, retrieval, or KV-cache quantization.

10 questions remaining.

Next Step
Continue to KV Cache & PagedAttention

MQA and GQA set how many KV bytes each token needs. The next chapter is about packing those bytes: paging, fragmentation, and how many variable-length requests can share one GPU without wasting the savings you just calculated.

PreviousInference: TTFT, TPS & KV Cache
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Attention Is All You Need.

Vaswani, A., et al. · 2017

https://arxiv.org/abs/1706.03762

Qwen2.5 Technical Report

Qwen Team · 2024

https://arxiv.org/abs/2412.15115

Efficiently Scaling Transformer Inference.

Pope, R., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2211.05102

Fast Transformer Decoding: One Write-Head is All You Need.

Shazeer, N. · 2019 · arXiv preprint

https://arxiv.org/abs/1911.02150

GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints.

Ainslie, J., et al. · 2023 · EMNLP 2023

https://arxiv.org/abs/2305.13245

Mistral 7B.

Jiang, A. Q., et al. · 2023

https://arxiv.org/abs/2310.06825

Llama 2: Open Foundation and Fine-Tuned Chat Models.

Touvron, H., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2307.09288

The Llama 3 Herd of Models.

Dubey, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2407.21783

Gemma 2: Improving Open Language Models at a Practical Size

Gemma Team, Google DeepMind · 2024

https://storage.googleapis.com/deepmind-media/gemma/gemma-2-report.pdf

vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention

vLLM Team · 2024

https://github.com/vllm-project/vllm

DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model

DeepSeek-AI · 2024

https://arxiv.org/abs/2405.04434

DeepSeek-V3 Technical Report.

DeepSeek-AI · 2024 · arXiv preprint

https://arxiv.org/abs/2412.19437

FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving.

Ye, Z., et al. · 2025

https://arxiv.org/abs/2501.01005

FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.

Dao, T. · 2023 · ICLR 2024

https://arxiv.org/abs/2307.08691

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.