2.2 PagedAttention 与 KV Cache 分页管理 如果说连续批处理解决了"GPU 别空转",那么 PagedAttention 解决的是另一个更要命的问题:显存被 KV Cache 撑爆,导致并发上不去。这一节是整本教程的硬核,请慢读——因为它直接决定了你能同时服务多少个用户,也决定了你每张卡能"塞"下多大的并发。我见过太多团队因为不懂这一节,把 80GB 的卡只跑出 20 个并发,而懂的人同样一张卡能跑到 200 个。 2.2.1 为什么 KV Cache 是显存杀手 Transformer 在生成每个 token 时,需要用到之前所有 token 的"键值"(Key/Value)向量,这就是 KV Cache。
如果说连续批处理解决了"GPU 别空转",那么 PagedAttention 解决的是另一个更要命的问题:显存被 KV Cache 撑爆,导致并发上不去。这一节是整本教程的硬核,请慢读——因为它直接决定了你能同时服务多少个用户,也决定了你每张卡能"塞"下多大的并发。我见过太多团队因为不懂这一节,把 80GB 的卡只跑出 20 个并发,而懂的人同样一张卡能跑到 200 个。
Transformer 在生成每个 token 时,需要用到之前所有 token 的"键值"(Key/Value)向量,这就是 KV Cache。它有两个特点让人头疼:
max_model_len)。以 DeepSeek V4 这类大模型为例,单层的 KV 维度(隐藏维 × 层数 × 2(K,V) × 精度字节数)乘上上下文长度,再乘并发数,很快就能吃掉几十 GB 显存。传统做法是为每个请求预先分配"最大可能长度"的连续显存块——这带来两个灾难:内部碎片(实际只用了一半长度,另一半空着)和外部碎片(零散的空闲块拼不出一个连续大块)。在长上下文 + 高并发下,这两个碎片能让你的有效显存利用率掉到 40% 以下,等于白白浪费一大半卡。
vLLM 借用了操作系统的虚拟内存分页思路,这是它工程上最巧妙的一笔:
这样,显存利用率从"预留最大值"变成"用多少占多少",碎片几乎为零。实测中 PagedAttention 能把 KV Cache 显存浪费降到很低(官方早期数据接近把有效利用率翻倍以上),意味着同样的显卡能多服务近乎一倍的并发。这不是理论上的数字,而是我压测时反复验证过的:同一张卡、同一个模型,开 PagedAttention 后并发上限从约 90 提到约 180,几乎翻倍。
假设块大小是 16 token。一个长度为 35 的序列,逻辑上分 3 块:块0(token 0-15)、块1(16-31)、块2(32-34)。块表把它们映射到物理块 7、3、9(物理块可以是显存里任意不连续的地址)。当序列继续生成到第 36 个 token,块2 满,调度器从空闲池拿一个新物理块(如 12)挂到块表末尾。整个过程不需要搬迁已有数据,也没有"预留到 2048"的浪费。
关键点:因为映射是逻辑到物理的间接层,多个序列还能共享同一个物理块——这就是前缀缓存(prefix caching)的基础。如果 100 个用户都用了相同的系统提示词(system prompt),那 100 个序列的逻辑前缀可以指向同一组物理块,KV 只存一份。对带固定 system prompt 的 DeepSeek V4 对话服务,这是巨大的显存和 prefill 双重节省。我强烈建议所有对话类服务都开启前缀缓存,它几乎是免费的午餐:不改任何业务逻辑,只因"大家开头都一样"就省下大量重复计算和显存。
DeepSeek V4 的 KV Cache 体量和上下文长度都很大,PagedAttention 的价值被放大,这也是为什么部署 MoE 必用 vLLM 这类分页方案:
但也要注意代价:块表是间接寻址,注意力计算时 GPU 要按块拼接读取,带来一点额外的访存与 kernel 复杂度。vLLM 的 PagedAttention kernel 专门针对这种"非连续块"做了融合优化,所以这块开销在主流硬件上可控,通常值得为换来翻倍的并发而付出。不要因为"听说有开销"就回避它——实测开销远小于收益。
你调参时这几个值直接作用于分页管理,务必理解每个的含义:
gpu_memory_utilization:vLLM 用它估算"把多少比例的显存留给 KV 池"。设太低,KV 池小,并发上不去;设太高(如 0.95+),可能挤占模型权重或引发 OOM。经验值常从 0.85~0.90 起步,结合实测调。block_size:每块 token 数,常见 16。一般不必改,除非你的 kernel 或序列长度分布特殊(极短请求可试更大块减少块表开销)。max_model_len:限制单请求最大上下文,直接决定 KV 池上限需求。设置时应匹配 DeepSeek V4 的真实上下文能力并留余量,设得过大等于白白预留显存。一个常见踩坑:有人把 gpu_memory_utilization 设到 0.95,结果偶尔 OOM 崩溃——因为权重占用会随 batch 浮动。稳妥做法是先 0.85,压测稳定后再小幅上调,且始终保证 max_model_len × 并发估算 不超物理显存。我一般留 10% 的安全垫,宁可并发少一点也不让服务半夜崩。
读完本节,你应该能回答三问:
光说"利用率翻倍"太虚,给你一个可手算的估算框架。假设单卡 80GB,DeepSeek V4 权重占约 40GB(量化后),剩下 40GB 给 KV。设 gpu_memory_utilization=0.9,KV 池约 36GB。单个请求的 KV 占用 ≈ 层数 × 2 × 隐藏维 × 块精度字节 × 上下文长度。对典型的几十层、隐藏维几千的量级,粗略算得每千 token 上下文约几百 MB 量级(具体数值请结合模型 config 算,这里只给方法)。那么在平均上下文 4K token 时,单卡约能支撑的并发 ≈ 36GB / 单请求 KV。代入你的真实数字,就能反推出该设多大的 max_num_seqs,而不是拍脑袋。
这个估算的意义在于:它把"并发上不去"从一个玄学问题变成了可拆解的算术题。当你发现实际并发远小于估算值,八成是 max_num_seqs 或 max_model_len 卡住了,而不是卡不够。
前缀缓存几乎是免费的午餐,但有几个坑必须知道:
上线前用这几条快速体检:
gpu_memory_utilization 是否留了 10% 安全垫,避免 OOM 半夜崩?max_model_len 是否匹配真实上下文需求,没设得过大白白预留显存?前面说了分页省显存、从而撑起更大并发。还有一层间接收益很容易被忽略:因为 KV 池利用率高,你能把 max_num_seqs 设得更大,而更大的运行批次又摊薄了 MoE 的权重搬运(呼应 2.1 的访存逻辑),进一步抬高吞吐。也就是说,PagedAttention 不仅直接省显存,还通过放宽调度预算间接提升了 GPU 效率。这解释了为什么"同样是 vLLM,懂不懂分页调参,吞吐差一倍"——它不是孤立的一块,而是牵动整条链路的支点。
如果估算框架告诉你并发已到 KV 池上限,下一步怎么选?我的建议优先级是:先开前缀缓存(几乎免费,常能再挤出 20%~40% 容量);再适度上调 gpu_memory_utilization(在安全垫内);最后才考虑加卡或用更省的量化。加卡是最贵的,且多卡会引入张量并行的通信开销,未必线性扩容。这个取舍顺序,是分页管理成熟后的自然延伸。
单独看 PagedAttention 省显存,单独看连续批处理填 GPU,二者结合才产生倍增效应,这里点破它,帮你建立系统观:分页让 KV 池能撑更大并发,于是调度器可以把 max_num_seqs 设更大、批次更满;批次更满又摊薄了 MoE 的权重搬运(2.1 的访存逻辑),GPU 效率再升。这解释了为什么"同样跑 vLLM,懂协同调参和只改单点,吞吐差一倍"。优化时不要孤立调一个模块,而要顺着"显存→批次→带宽"的传导链一起拧。
收尾前澄清几个关于 KV Cache 的误区,避免你调参时走偏:
把这三条记住,2.2 就算真正过关。
最后补一句工程体力活建议:把 KV 池占用、块分配失败次数、前缀缓存命中率这三条做成实时看板,每次上线发版后盯 10 分钟。多数分页相关的隐性退化(比如某次改动让 system 前缀不一致、缓存命中率从 90% 掉到 10%)都能在这 10 分钟里暴露,比你等用户投诉早半天发现。监控不是负担,是分页管理成熟度的标志。
一句话带走:PagedAttention 用"分页 + 块表"把 KV Cache 从显存黑洞变成了可精细计量的资源,是高并发能成立的前提。 下一节我们看调度器如何在这块资源上拼装批次。如果你只能记住本节一句话,就记住"显存利用率翻倍等于并发翻倍"——这是 PagedAttention 最朴素也最有力的结论。