KV 缓存与吞吐上限¶
基线:vLLM 0.26.0 · 经 Context7 核实(ADR-0004)
模块: Part 0 · 基础 · 考察课程: KV 缓存
问:在单张 24 GB GPU 上服务一个 7B 模型,为什么限制并发请求数的通常是 KV 缓存——而非模型权重或纯算力?¶
直接答案¶
权重是固定、一次性的显存成本;KV 缓存是随 batch × sequence_length 增长的按序列成本。权重加载后,剩余 VRAM 是一个固定预算,每个并发请求按其上下文长度成比例地吃掉它。于是并发被「剩余显存能塞下多少条序列的 KV 缓存」卡住——这是一个显存带宽与容量问题,不是算力问题。加之 decode 本身也是 memory-bound(每步为极少计算从 HBM 重读整个缓存),所以你会在撞上 FLOPs 墙之前,先撞上显存墙。
深入原理¶
- 算术。 KV bytes \(= 2 \times L \times n_{\text{kv}} \times d_h \times b_{\text{dtype}} \times S \times B\)。对
Qwen2.5-7B-Instruct(28 层、4 KV 头、head_dim 128、BF16)就是 56 KiB/token——即一条 8192-token 序列约 0.44 GiB,满 32k 上下文约 1.75 GiB。 - 为什么不是权重? ~14 GiB(BF16)或 ~5–6 GiB(AWQ 4-bit)——只付一次,不随负载增长。
- 为什么不是算力? Decode 每步只做一个 token 的运算,却重读整个 KV 缓存。算术强度低 → 带宽受限。加 FLOPs 余量没用;加 KV 容量(或带宽)才有用。
- 对优化的含义。 一切提吞吐的手段本质都是「塞下/服务更多 KV」:量化权重(腾 VRAM 给 KV)、
kv_cache_dtype=fp8(KV 字节减半)、PagedAttention(消除碎片)、continuous batching(把 KV 预算填满)、prefix caching(避免重复 KV)。
代码¶
租 GPU 之前先把上限推出来:
# Qwen2.5-7B-Instruct 单 token KV(BF16):2 * 28 * 4 * 128 * 2 = 57344 字节 = 56 KiB
kib_per_token = 2 * 28 * 4 * 128 * 2 / 1024 # 56.0
free_gib = 24 - 14 # 权重 ~14 GiB -> ~10 GiB 给 KV
seq_len = 8192
gib_per_seq = kib_per_token * seq_len / (1024 ** 2) # ~0.44 GiB
max_concurrent = int(free_gib / gib_per_seq) # ~22(示例,忽略激活)
print(kib_per_token, round(gib_per_seq, 2), max_concurrent)
面试官追问¶
- 「怎么把这个并发数翻三倍?」 → 缩小权重(AWQ/GPTQ,腾 VRAM)、缩小 KV(
kv_cache_dtype=fp8;GQA 已经帮了 7×)、或把max_model_len压到工作负载真实的上下文。 - 「PagedAttention 在哪一环?」 → 它消除连续 KV 分配的碎片浪费,让更多剩余 VRAM 变成可用 KV——抬高的是有效容量,而非公式里的原始值。
- 「为什么把
gpu_memory_utilization拉到 0.99 很危险?」 → 并发负载下激活会突增;余量太紧会在生产里 OOM,哪怕启动时没事。 - 「Prefill vs decode 哪个 memory-bound?」 → decode(memory-bound);prefill 一次处理很多 token,通常 compute-bound。