Skip to content

KV cache chains: bytes per token, the pool, and what reuse saves

8 chains

Every KV question in a loop starts from one of these. Each chain names its model and part, shows the substitution, and ends with a line that tells you whether the result is plausible. The linked questions carry each chain into a decision.

Bytes per token, GQA

inputs   Llama 3.1 8B: 32 layers, 8 KV heads, head dim 128, BF16 (2 B)
         Llama 3.3 70B: 80 layers, 8 KV heads, head dim 128, BF16
formula  2 (K and V) x layers x KV heads x head dim x bytes
8B       2 x 32 x 8 x 128 x 2 B = 131,072 B = 128 KiB per token
70B      2 x 80 x 8 x 128 x 2 B = 327,680 B = 320 KiB per token
sanity   one 8,192-token 8B sequence = 8,192 x 128 KiB = 1 GiB

The usual slip is multiplying by query heads. Llama 3.1 8B has 32 of them, so that answer is 4x too large and every capacity figure after it is wrong by the same factor.

Drilled in: KV footprint for MHA, GQA and MLA · KV cache bytes and budget

Bytes per token, MLA

inputs   DeepSeek V3: 61 layers, BF16; per layer one 512-wide latent
         and one 64-wide RoPE key
formula  layers x (latent + rope key) x bytes
         61 x (512 + 64) x 2 B = 70,272 B per token
sanity   70,272 / 131,072 = 0.54: a 671B model caches about half
         of what Llama 3.1 8B does per token

The latent replaces both K and V, so there is no factor of 2. Forgetting the 64-wide RoPE key understates the cache by 11%. Under tensor parallelism the latent cannot be split by head, so each rank holds all of it.

Drilled in: DP attention plus EP for MLA models

The pool left for KV

inputs   H100 SXM 80GB: CUDA reports 79.18 GiB = 85.02 GB
         vLLM v0.32.0 gpu_memory_utilization 0.92 (default)
         Llama 3.1 8B BF16 weights 16.06 GB
         activations and other allocations assumed 3.0 GB
budget   0.92 x 85.02 GB = 78.22 GB
pool     (78.22 - 16.06 - 3.0) GB / 131,072 B = 451,355 tokens
sanity   451,355 / 8,192 = 55 sequences of 8K tokens at once

Two errors cost the most here: starting from the 80 on the label instead of what CUDA reports, and mixing GB with GiB, which is a 7% error. The 3.0 GB is an assumption; the engine's startup log reports the real figure, and that is the number to use.

Drilled in: vLLM memory accounting

What block rounding wastes

inputs   vLLM block of 16 tokens (v0.32.0 default on CUDA); 200 running sequences
         the 451,355-token pool above; lengths uniform modulo 16
per seq  at most 15 tokens in the last block, 7.5 on average
total    200 x 7.5 = 1,500 tokens = 0.33% of the pool (197 MB at 128 KiB)
256-token blocks  200 x 127.5 = 25,500 tokens = 5.6% of the pool
sanity   paging made waste small; block size is the knob that brings it back

Paging removes the contiguous reservation, not the partial block. Larger blocks help the kernel and hurt capacity in direct proportion.

Drilled in: Paged KV waste bound

Sliding-window layers

inputs   gpt-oss-120b: 18 global layers and 18 window-128 layers
         8 KV heads x 64 dims x 2 (K, V) x 2 B = 2,048 B per layer per token, BF16
128K     global 18 x 131,072 x 2,048 B = 4.83 GB; window 18 x 128 x 2,048 B = 4.7 MB
total    4.84 GB, against 36 x 131,072 x 2,048 B = 9.66 GB with every layer global
sanity   2.0x, the 1:1 layer ratio; the 128-token window barely appears

The saving is set by how many layers are windowed. Whether the engine actually frees the windowed layers' old blocks is a separate question about its allocator.

Drilled in: Sliding-window hybrid KV at 128K

A shared system prompt

inputs   2,000-token system prompt, 100 concurrent requests, Llama 3.1 8B BF16
         H100 SXM 989 TFLOP/s dense BF16, 50% MFU assumed
memory   unshared 100 x 2,000 x 131,072 B = 26.2 GB; shared once = 262 MB
compute  each hit skips 2 x 8.03e9 x 2,000 = 32 TFLOP = 65 ms of prefill
blocks   2,000 / 16 = 125 full blocks, so the whole prompt is shareable
sanity   a hit saves pool and TTFT together; a 2,007-token prompt
         shares 125 blocks and recomputes 7 tokens

vLLM matches whole blocks only, and a block's key chains its parent's hash, so one changed token near the start of the prompt invalidates every block after it.

Drilled in: Prefix caching and block hashing

Fetch from host memory or recompute

inputs   8,192-token prefix, Llama 3.1 8B BF16; PCIe Gen5 at an assumed 50 GB/s
         recompute 1.49e14 FLOPs (linear plus attention) at 60% of 989 TFLOP/s
bytes    8,192 x 131,072 B = 1.07 GB
fetch    1.07 GB / 50 GB/s = 21.5 ms
compute  1.49e14 / (0.6 x 989e12) = 251 ms
sanity   fetching wins when bytes per token / link bandwidth
         < FLOPs per token / achieved compute

The copy is the part a transfer metric reports. A hit's TTFT also includes the lookup, the scheduling of the load and whatever the engine does before the first step, so time the hit path end to end before deciding where the break-even sits.

Drilled in: KV offload: fetch or recompute · Host-tier hits slower than misses

FP8 KV

inputs   the pool above, KV stored as FP8 E4M3 (1 B) instead of BF16
per token  2 x 32 x 8 x 128 x 1 B = 64 KiB
pool       59.16 GB / 65,536 B = 902,710 tokens, twice the BF16 pool
sanity     the weights did not shrink, so the KV budget is unchanged
           and the token count doubles exactly

The capacity arithmetic is the easy half. vLLM uses a scale of 1.0 when a checkpoint carries no KV scales and warns once at startup, so read the log before trusting the accuracy of a doubled pool.

Drilled in: FP8 KV scales and long-context validation

Next card: Batching and Scheduling Chains