3、核心概念:LLM推理、批处理、KV Cache、PagedAttention
要真正理解 vLLM 为什么能成为生产环境中最受欢迎的大模型推理框架之一,必须先厘清四个环环相扣的核心概念:LLM 推理的基本过程、批处理如何提升吞吐、KV Cache 为什么是性能的关键瓶颈,以及 PagedAttention 如何用操作系统的虚拟内存思想破解这一瓶颈。本节将逐一拆解,并最终用一张综合示意图把这些概念串联起来。
3.1 LLM 推理:从 Token 到 Token 的自回归循环
大语言模型的推理本质是一个自回归(Autoregressive)过程:模型每次只生成一个 token(词元),然后将新生成的 token 拼接到输入序列末尾,再作为下一次预测的输入,如此循环,直到遇到终止符或达到最大长度。这个过程可以拆分为两个阶段:
- 预填充阶段(Prefill):处理用户输入的完整 prompt,并行计算所有输入 token 的注意力权重和中间状态,生成第一个输出 token。此阶段计算密集,GPU 利用率高。
- 解码阶段(Decode):逐 token 生成后续输出。每步只处理一个新增 token,但需要读取之前所有 token 的 KV Cache。此阶段访存密集,GPU 算力往往闲置。
一个简单的生成循环伪代码如下:
def generate(prompt, model, max_tokens):
tokens = tokenize(prompt)
kv_cache = None
for _ in range(max_tokens):
logits, kv_cache = model.forward(tokens, kv_cache)
next_token = sample(logits[-1])
if next_token == EOS:
break
tokens = [next_token] # 解码阶段只输入新 token
return detokenize(tokens)
理解这两个阶段的差异至关重要:Prefill 是计算瓶颈,Decode 是访存瓶颈。vLLM 的许多优化都围绕如何让这两个阶段更高效地协同工作。
3.2 批处理:连续批处理与动态调度
单条请求的 Decode 阶段 GPU 利用率极低,因为每次只计算一个 token 的矩阵乘法,大量显存带宽被浪费。最直接的解决办法是批处理(Batching)——同时处理多条请求,让 GPU 的并行计算能力被充分填充。
传统静态批处理(Static Batching)存在明显缺陷:必须等批次中最慢的请求全部生成完毕,才能释放资源,导致其他请求空等。vLLM 采用连续批处理(Continuous Batching),也叫动态批处理,其核心思想是:
- 每个请求独立推进,完成即退出批次,新请求随时加入。
- GPU 每一轮迭代都重新组合当前所有活跃请求,形成新的批次。
- 通过调度器(Scheduler)管理请求的生命周期,最大化 GPU 利用率。
下图展示了静态批处理与连续批处理的差异:
3.3 KV Cache:为什么需要它?
在自回归解码过程中,每生成一个新 token,都需要计算它与之前所有 token 的注意力分数。如果不做缓存,每一步都要重新计算之前所有 token 的 Key 和 Value 矩阵,计算量随序列长度呈平方级增长,完全不可接受。
KV Cache 的核心思想是:在 Prefill 阶段计算出每个 token 的 Key 和 Value 后,将其缓存在显存中;后续 Decode 阶段只需计算新 token 的 Key 和 Value,然后从缓存中读取历史 KV 进行注意力计算。这样,每一步的计算量从 O(n²) 降为 O(n),极大加速推理。
但 KV Cache 带来了新的问题——显存管理。每个请求的 KV Cache 大小取决于序列长度,而序列长度是动态变化的。传统做法是预先分配一块连续显存,按最大可能长度预留,这导致:
- 内部碎片:预留空间大于实际使用,浪费显存。
- 外部碎片:不同请求的缓存块大小不一,难以紧凑排列。
- 无法共享:即使多个请求共享相同的前缀(如系统提示词),也无法复用 KV Cache。
下表对比了不同显存管理方式的效率:
| 管理方式 | 显存利用率 | 碎片问题 | 前缀共享 | 实现复杂度 |
|---|---|---|---|---|
| 静态预分配 | 60%~70% | 严重 | 不支持 | 低 |
| 动态按需分配 | 80%~90% | 中等 | 不支持 | 中 |
| PagedAttention | 96%~100% | 几乎为零 | 支持 | 较高 |
3.4 PagedAttention:向操作系统借智慧
PagedAttention 的灵感直接来自操作系统的虚拟内存分页机制。在操作系统中,进程的虚拟地址空间被划分为固定大小的页(Page),物理内存也被划分为同样大小的页框,通过页表建立映射。这样,进程可以拥有不连续的物理内存,且按需换入换出。
vLLM 将这一思想移植到 KV Cache 管理中:
- 固定大小块(Block):将 KV Cache 划分为固定大小的块,每个块可存储固定数量 token 的 KV 数据(例如 16 个 token)。
- 块表(Block Table):每个请求维护一张块表,记录其逻辑块到物理块的映射关系,类似于操作系统的页表。
- 按需分配:请求需要更多 KV 空间时,动态分配新的物理块,无需预先预留连续空间。
- 前缀共享:多个请求若共享相同的前缀 token,可以共享对应的物理块,通过引用计数管理生命周期。
PagedAttention 的注意力计算也做了相应调整:传统注意力需要 KV 在内存中连续排列,而 PagedAttention 通过块表间接寻址,在计算时按块读取 KV 数据,虽然增加了少量寻址开销,但换来了接近 100% 的显存利用率。
核心收益:在相同显存条件下,PagedAttention 可以支持更大的并发请求数(吞吐量提升 2~4 倍),或者支持更长的上下文长度。这也是 vLLM 在长文本场景下表现优异的关键原因。
3.5 综合案例:一次完整的 vLLM 推理流程
假设有两个用户同时向 vLLM 服务发送请求:
- 请求 1:
"请解释什么是量子纠缠"(前缀与请求 2 共享"请解释") - 请求 2:
"请解释什么是黑洞"
vLLM 的处理流程如下:
- 调度器接收两个请求,将它们加入等待队列。
- Prefill 阶段:调度器将两个请求组成一个批次,并行执行 Prefill,计算各自的 KV Cache。由于前缀
"请解释"相同,vLLM 检测到共享前缀,让两个请求共享这部分物理块。 - Decode 阶段:两个请求各自独立生成 token。请求 1 生成
"量子",请求 2 生成"黑洞"。每步生成后,新 token 的 KV 被写入新分配的物理块。 - 动态调度:请求 1 先生成完毕,立即释放其独占的物理块(共享块保留),请求 2 继续生成。此时新请求 3 到达,调度器立即将请求 3 加入批次。
- 完成:所有请求完成后,共享块引用计数归零,全部释放。
整个过程中,显存利用率始终保持在 95% 以上,且没有请求被无谓阻塞。
3.6 常见误区与复盘
误区一:KV Cache 越大越好?
并非如此。KV Cache 占用显存,而显存是有限资源。增大 KV Cache 意味着减少可用显存,可能降低并发能力。vLLM 提供了 --max-model-len 和 --gpu-memory-utilization 参数,需要根据实际负载平衡 KV Cache 大小与并发数。
误区二:PagedAttention 完全消除了显存碎片?
PagedAttention 将碎片率降到极低,但并非零。块内仍可能存在少量内部碎片(最后一个块未填满),且块表本身也占用少量显存。不过相比传统方式,这些开销已可忽略不计。
复盘要点:理解 LLM 推理的 Prefill/Decode 两阶段特性,是理解 vLLM 一切优化的基础。连续批处理解决的是 GPU 利用率问题,KV Cache 解决的是重复计算问题,而 PagedAttention 解决的是显存管理问题。三者环环相扣,共同构成了 vLLM 高性能推理的核心引擎。