DeepConcepts

LLM internals / inference / serving / distributed

Tensor Parallelism Does Not Divide Your KV Cache by N

The misconception

That --tensor-parallel-size N divides everything by N, so eight GPUs give eight times the weights capacity, eight times the KV cache room and eight times the throughput. Three parts of that are wrong. Grouped-query attention leaves most models with 8 key-value heads, and vLLM computes heads per GPU as max(1, total_num_kv_heads // tensor_parallel_size), so past N = 8 it replicates instead of splitting: on Qwen3-32B at 32k context, going from 8 GPUs to 16 takes concurrent sequences from 60 to 64, a 1.07x return on twice the hardware. Capacity is per-GPU and not summed, because every GPU holds a head-shard of every resident sequence rather than its own set of sequences. And the two all-reduces per layer do not shrink with N — their volume per GPU rises toward twice the tensor — so on a PCIe machine an 8,192-token prefill chunk at N = 32 spends 81% of its time in communication.

14 min

A 32-billion-parameter model does not fit on one card with enough room left to serve anybody, so you split it across eight. The mental model that comes with that decision is division: eight GPUs, one eighth of everything each, eight times the room. Two of the three things you care about do divide like that. The one you are actually short of stops dividing partway through, and on most current models it stops at exactly eight.

The vocabulary first. Tensor parallelism — Megatron-LM calls it intra-layer model parallelism — cuts every weight matrix in a layer into N pieces and gives one piece to each GPU, so all N run the same layer at the same time on different slices. Attention is cut by head: with 64 query heads and N = 8, each GPU owns 8 of them. The feed-forward network is cut by column and then by row. Because each GPU ends up with a partial sum of the layer's output, the pieces have to be added back together, and that addition is an all-reduce: a collective in which every GPU ends up holding the sum of all N contributions. There are exactly two per decoder layer. And GQA, grouped-query attention, is the now-standard design in which many query heads share one key-value head — Qwen3-32B has 64 query heads and 8 key-value heads — which is what the GQA lesson is about and what makes the KV cache small enough to serve from.

Below is Qwen3-32B on H100 80GB cards, sharded N ways. It is set at --tensor-parallel-size 8, the obvious choice for a single eight-GPU node. Read the hero number — how many 32k-token conversations fit at once — then take the tensor-parallel size to 16.

Interconnect between the GPUs

num_key_value_heads is the one architectural what-if here: Qwen3-32B ships with 8, and 64 would make it a full multi-head model. Everything else is that model as published. The fixed cost per all-reduce is the part of a collective that does not depend on message size — launch, synchronisation, the slowest rank — and it is worth sweeping because it is the term that decides whether small-batch decoding cares.

sequences that fit at once
vs. the next size down
KV heads per GPU
cache bytes / token / GPU
weights per GPU
output tokens / second
decode step · all-reduce
8k prefill · all-reduce
Sequences that fit, at every tensor-parallel size

this doubling of the GPU count bought more than 1.2x the capacity. it did not — the KV cache has stopped sharding and you are paying for GPUs that add nothing but weights room. Sizes vLLM refuses outright are left blank; the log says which assertion fires.

One 8,192-token prefill chunk, per tensor-parallel size

Every lane is the same work on a different number of GPUs, drawn on one wall-clock axis. is arithmetic, which halves each time you double N. is the 128 all-reduces that arithmetic requires, which does not.

Qwen3-32B from its published config.json: 64 layers, hidden size 5,120, 64 query heads, 8 key-value heads, head dimension 128, feed-forward 25,600, and an untied 151,936-token vocabulary, giving 32.76 billion parameters. Hardware from NVIDIA's own H100 table: 80 GB, 3.35 TB/s, 989 dense BF16 TFLOP/s, NVLink 900 GB/s, PCIe Gen5 128 GB/s. Weights are BF16; the pool is --gpu-memory-utilization 0.92 of the card minus the weights, with no allowance for activation workspace, so the capacities are an upper bound. All-reduce is costed as a ring: 2(N-1)/N times the tensor, at 75% of the link's peak, plus the fixed cost you set. The ratios are the lesson; the absolute milliseconds are a sketch.

At --tensor-parallel-size 8 the model holds 60 conversations of 32k tokens at once. Each GPU carries 8.19 GB of weights and 1 key-value head, so 32,768 bytes of cache per token.

Double the GPU count to 16. Capacity goes to 64. Four more conversations — 1.07x — for twice the hardware. Look at why: KV heads per GPU still reads 1, and cache bytes per token per GPU still reads 32,768. Nothing about the cache changed. The only thing the extra eight cards bought was weights room, 8.19 GB per GPU down to 4.18 GB, and weights were never what you were short of. Go to 32 and it is 1.03x.

Now find the cause. Take num_key_value_heads to 64, so every query head has its own key-value head and there is no grouping at all. The same 8-to-16 step now goes from 7 sequences to 16 — 2.29x — and 16 to 32 gives 2.06x. Sharding by head works perfectly right up to the point where there are no more heads to give out, and grouped-query attention is a design whose entire purpose is to leave you with as few as possible. vLLM does not error when it runs out. It replicates: every GPU past the eighth is handed a copy of the same key-value head.

The second half of the bill is in the lower panel. Select PCIe Gen5, 128 GB/s and put the tensor-parallel size back to 8. The prefill chunk goes from 225.7 ms on NVLink to 393.6 ms, and the magenta share of the lane goes from 12.6% to 49.9%. Half the machine's time is now spent adding numbers up. At 32 GPUs on PCIe it is 81.1%, and the chunk takes 268.0 ms — slower than eight NVLink GPUs managed.

What a tensor-parallel layer actually does

Megatron-LM's contribution was noticing that a transformer layer can be split so that only two collectives per layer are needed, and that this can be done "with the insertion of a few communication operations in native PyTorch". The trick is the pairing of two sharding directions.

A column-parallel linear splits the weight matrix by output column: A = [A_1, ..., A_p], so GPU i computes Y_i = X·A_i from the full input and holds a slice of the output. A row-parallel linear splits by input row and takes an input that is already sliced, so GPU i computes a partial sum over its slice and the partial sums must be added. Put a column-parallel layer immediately before a row-parallel one and the slice produced by the first is exactly the slice the second wants — no communication between them.

That is what every decoder layer is built from. In vLLM's Qwen3 implementation the QKV projection is a QKVParallelLinear (column-parallel, sharded by head) and the output projection o_proj is a RowParallelLinear; in the feed-forward network gate_up_proj is a MergedColumnParallelLinear and down_proj is a RowParallelLinear. Both take reduce_results=True, the default. Two row-parallel layers, two all-reduces, per layer, in the forward pass. On a 64-layer model that is 128 all-reduces in the decoder stack on every forward pass — the vocabulary embedding adds one more — whether the pass is prefilling 8,192 tokens or decoding one token for one request.

That last clause is the whole cost structure. The tensor being reduced has shape [tokens, hidden], so its size scales with the number of tokens in the pass. In a ring all-reduce each rank ships 2(N-1)/N of the tensor, which rises from 1.0 at N = 2 towards 2.0 and never falls. The count of collectives, meanwhile, depends only on the layer count. Decoding with a batch of one moves almost no bytes and still pays 128 fixed costs; prefilling a chunk pays almost no fixed cost and moves a great deal of data. Move the fixed cost per all-reduce slider with sequences decoding at 1 and you can watch the first regime; leave it at zero and change the interconnect to watch the second.

Where the sharding stops

Query heads shard perfectly because there are 64 of them and you will never have 64 GPUs in one tensor-parallel group. Key-value heads do not, and vLLM's rule for them is four lines with a comment that gives the whole game away:

# If tensor parallelism is used, we divide the number of KV heads by
# the tensor parallel size. We will replicate the KV heads in the
# case where the number of KV heads is smaller than the tensor
# parallel size so each GPU has at least one KV head.
return max(1, total_num_kv_heads // parallel_config.tensor_parallel_size)

max(1, ...). Past tensor_parallel_size == num_key_value_heads the expression is pinned at 1 and every further GPU you add receives a full copy of the same key-value head. The cache is not smaller; it is duplicated. On Qwen3-32B that boundary is at 8, which is also the most common node size, which is why almost nobody meets this until they try to go past one node.

The second thing to be clear about is that capacity is per GPU and not summed. Every GPU in a tensor-parallel group holds a head-shard of every resident sequence — that is what makes it tensor parallelism rather than data parallelism. So the number of sequences the group can serve is the number one GPU's pool can hold, not N times it. The pool grows for two separate reasons as N rises: the weights shrink, freeing bytes, and the cache per token shrinks, so each byte holds more. Only the second of those is worth much, and it is the one that stops.

Third, the set of legal values for N is small and it is model-dependent. vLLM asserts three things at model construction: query heads must divide by N; key-value heads must divide by N, or if there are fewer than N, N must be a multiple of them; and the feed-forward width must divide by N. Drag the tensor-parallel slider onto 3 or 6 and the readouts say refused, with the assertion that fires. This is not a hypothetical: there is a vLLM pull request written by someone who wanted a 70B model on three A100s precisely because it did not fit on two, and it required implementing uneven weight sharding to do it.

A note on what this means for architecture choices you do not control. Multi-head latent attention, the DeepSeek design, sidesteps the floor by compressing the cache into a single latent vector rather than into heads — vLLM's get_num_kv_heads short-circuits to return 1 for it, because "when using MLA during decode it becomes MQA". Multi-query attention, one key-value head, has the floor from the start: set num_key_value_heads to 1 and max(1, 1 // N) is 1 at every N, so the cache per token never shards at all. The seats still climb — 8, 38, 53, 60, 64, 66 across TP 1, 2, 4, 8, 16 and 32 — but every one of those tokens came from weights getting smaller, not cache. The bars turn magenta at TP 8, where the step is 1.13x.

The boundary: when adding GPUs is the wrong purchase

The simulation makes it easy to find the point where more GPUs stop being the answer, and it is worth naming what the alternatives buy instead.

Quantizing the cache attacks the number tensor parallelism has stopped attacking. At the floor you hold 32,768 bytes per token per GPU because you have one key-value head in BF16, the 16-bit brain-float format the weights use; an FP8 (8-bit float) cache halves that on every GPU at once, which is a 2x on capacity where the sixteenth GPU was a 1.07x. That is the trade in what quantization actually changes, and it is the right lever on the right side of the floor.

Data parallelism — running two independent tensor-parallel groups of 8 rather than one group of 16 — does multiply capacity by 2, because the two groups hold different sequences. It costs you the ability to serve any single request faster, and it needs the weights twice. When the sixteenth GPU buys 7% as a tensor-parallel rank and 100% as a second replica, that arithmetic is not close.

Nothing at all is often correct. Put the controls back to the ones the lesson opened with — NVLink 900 GB/s, num_key_value_heads 8, context 32,768 — then set sequences decoding to 256 and read output tokens per second across the sizes: the step from TP 4 to TP 8 is worth 2.28x, and the step from 8 to 16 is worth 1.06x. If the fleet is at TP 8 and saturated, the next machine should be another machine. The interconnect matters less than you would guess here: the same sweep on PCIe Gen5 gives 2.22x and 1.06x, because at a batch of 256 the step is bandwidth-bound on weights and cache, not on the link.

There is one case that runs the other way, and it is the reason nobody should read this lesson as "do not use tensor parallelism". Set sequences decoding to 1 — a single interactive request, latency being the only thing that matters — with the interconnect still on NVLink 900 GB/s and num_key_value_heads still 8. Decode is bandwidth-bound, so splitting the weights over N cards splits the bytes each card must read, and the step time falls from 4.10 ms at TP 8 to 2.60 ms at TP 16 to 1.86 ms at TP 32. Communication climbs from 15.7% of the step to 34.7%, and it still wins. Tensor parallelism is a latency technique that happens to also free memory, and the memory is the part that runs out first. Why splitting bytes helps here at all is a roofline question.

Checking it on a real system

Four numbers, in the order you should look at them.

One: your key-value head count. It is in the model's config.json as num_key_value_heads, and it is the largest tensor-parallel size from which you will get a full memory return. Qwen3-32B and Qwen3-8B both ship 8, which is why the floor and the standard node size are the same number. Read yours rather than assuming it. If your --tensor-parallel-size already equals it, more ranks will not give you more context or more concurrency.

Two: what vLLM says the pool is. The engine prints its own answer at startup, and it is the ground truth that beats any calculation:

INFO ... GPU KV cache size: 1,996,143 tokens, Maximum concurrency for 32,768 tokens per request: 60.92x

One line, not two: vLLM emits both fields from a single logger.info_once. The token count is the real pool after weights and workspace. The second field divides it by max_model_len, so it answers "at full context", not "at the length my requests actually use". The values shown are this lesson's model at --tensor-parallel-size 8, not a capture from a real server.

Run it at your current tensor-parallel size, then at twice that, and compare the two lines. If the token count barely moves, you have found the floor empirically in about four minutes, and no further reasoning is required.

Three: your topology. nvidia-smi topo -m prints a matrix of how each GPU pair is connected. NV# means that many NVLink connections; PIX, PXB, PHB and SYS are all PCIe paths of decreasing quality, and SYS means the traffic crosses the CPU sockets. A tensor-parallel group whose matrix contains SYS is the 128 GB/s case in the simulation or worse. Check this before you tune anything, because it decides whether the magenta half of the lane is 12% or 50%.

Four: the collective's share of the step. Set NCCL_DEBUG=INFO once at startup — NCCL is NVIDIA's Collective Communications Library, the code that actually runs the all-reduce — to confirm which transport was selected — the log names the path per rank pair, and a group you believed was on NVLink silently falling back to shared memory or the network is a common and expensive surprise. For the ongoing measurement, compare median inter-token latency at your tensor-parallel size against the same model at half that size on the same hardware: if halving the group makes per-token latency worse by much less than 2x, communication is eating the difference.

Two configuration notes worth carrying. Tensor parallelism multiplies your exposure to a single slow rank, because 128 all-reduces per forward pass are 128 synchronisation points; one throttling GPU sets the pace for all of them. And the collective implementation is not fixed — vLLM has shipped changes to its default all-reduce path that broke tensor parallelism for particular models, so a version bump that changes throughput without changing your config is worth checking against the release notes before it is worth profiling.

Finally, the interaction that catches people who have already read the rest of this corpus: a larger pool is not by itself more throughput. It raises the ceiling that the scheduler's admission policy runs into, and if --max-num-seqs was already the binding constraint rather than memory, doubling the pool changes nothing you can measure. Check which of the two is binding before buying either.

You serve a 70B model with 8 key-value heads on one node at --tensor-parallel-size 8, and the KV cache is full. A second identical node arrives. Which deployment serves the most concurrent sequences?

Next: the architecture that put the floor there, grouped-query attention; the memory it is a floor on, the KV cache; the other way to buy pool space, quantization; and the allocator that hands the pool out block by block, PagedAttention.

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.