vLLM核心机制
大约 5 分钟
vLLM是加州伯克利开源的LLM推理框架,核心目标是最大化推理吞吐量、降低延迟, 其核心优化机制为: PagedAttention、Prefix Cache、Continuous Batching。
1. PagedAttention: 分页注意力
在Transformer解码时,每个token的生成,都需要和之前所有token的KV做注意力计算,如果不缓存,每次都需要重新计算整个序列,效率极低。KV Cache通过存储所有历史token的键值对,实现了空间换时间,最终复杂度从
传统方法
传统的缓存方法,通常为每个推理请求分配一块连续的显存空间来存储KV缓存。在多用户并发请求的场景下,会引发以下问题:
- 碎片化严重:不同请求的上下文长度不同,请求到达和完成时间不一致,导致连续显存的频繁分配与释放,产生大量碎片。
- 预分配冗余:为应对可能出现的超长上下文,系统会为个请求预留更多缓存空间,很可能用不上,导致显存浪费。
PagedAttention
vLLM借鉴操作系统的分页思路,来管理KV Cache。
- 分块:将KV Cache划分为固定大小的块(Block),每个块包含固定数量token的键值。
- 按需分配:序列的KV Cache由若干个非连续的物理块组成,通过一个逻辑块表来映射。
- 优势
- 非连续存储:blocks 可分散在显存任意位置;
- 按需分配:prefill/decode 时动态申请新 block;
- 高内存利用率:可达 90%+(传统方法通常 < 20%);
- 支持共享:多个请求可引用同一 block(只读)。
| 因素 | Block Size较小 | Block Size较大 |
|---|---|---|
| 内存碎片 | 较多 | 较少 |
| 缓存效率 | 粒度细,利于共享短前缀,但对长前缀来说,管理开销更大 | 粒度粗。共享长前缀效率高,但短前缀可能浪费空间 |
| 吞吐量 | 可能因调度开销增加而降低吞吐量 | 通常利于提高吞吐量 |
vLLM为每个请求维护一个块表(block table),用于记录逻辑 token 序列中各段K/V缓存与物理显存块之间的映射关系
2. Prefix Cache: 前缀缓存优化
核心机制
不仅单个请求内会有KV复用,跨请求之间也存在KV复用,如:
- 同一个系统提示词,处理不同用户的请求
- 同一个用户,携带历史记录进行对话
- 缓存触发条件
- 启动时指定参数:
enable-prefix-caching - block已填满(prefill完成或者decode写满)
- 缓存粒度
- 以
block为单位 - 每个
block根据以下信息生成hash keyblock内部的Token IDS: 内容敏感- 对应的Position IDS: 位置敏感
- 模型层标识(多层共享时): 同一个token,每层的KV矩阵不同
调度策略
- 释放策略(引用计数法):每个cached block 维护
ref_count:
- +1: 被某个请求引用(decode/prefill 使用)
- +1: 被缓存系统自身持有(即使无活跃请求)
- 当
ref_count == 0时,会触发物理显存的释放 - 当显存不足时,使用LRU规则驱逐
cache_block.ref_count -= 1
- 复用逻辑(Partial Match)
- 新请求的 prompt 从头开始逐 block 匹配;
- 只要连续前缀匹配,就复用对应 blocks;
- 一旦出现不匹配(如不同 token),后续部分重新计算;
- 支持任意长度前缀复用(哪怕只共享 1 个 block)。
3. Continuous Batching: 连续批处理
在 LLM 推理中,每个请求包含两阶段:
- Prefill(预填充):一次性处理整个 prompt(并行计算,快)
- Decode(解码):逐 token 生成(自回归,慢)
传统静态批处理
- 所有请求必须同时开始、同时结束;
- 但不同请求的 prompt 长度和生成长度差异巨大;
- 结果:短请求必须等待长请求完成 → GPU 利用率极低(大量时间空转)。
Continuous Batching
核心机制
Continuous Batching是一种动态、异步的批处理策略:
- 随时加入新请求(只要显存允许);
- 每个请求的prefill 和 decode 分开调度;
- 已完成的请求立即退出 batch,释放资源;
- 新 token 生成阶段(decode)可与其他请求的 prefill 混合执行。
vLLM按照如下步骤实现连续批处理:
- 讲每个请求的
Prefill和Decode任务分离 - 使用调度器(Scheduler)动态组建batch,
Prefill和Decode混合在一个batch里,就叫做chunked prefill
while has_free_gpu_memory():
if new_requests_in_queue():
add_prefill_request() # 加入 prefill
if decode_requests_waiting():
add_decode_request() # 加入 decode
- 请求生命周期管理
- 请求进入 → 分配 blocks → 执行 prefill → 进入 decode 队列;
- 每次 decode 生成 1 个 token;
- 生成完毕(EOS 或 max_len)→ 释放 blocks → 从 batch 移除;
- 新请求可随时插入,老请求可随时退出。
为什么Prefill和Decode可以在一个Batch里
虽然Prefill和Decode看起来不同,但它们的底层计算本质上是相似的:
- 都使用相同的attention机制
- 都需要矩阵乘法和attention计算
- 都可以分解为更小的计算单元
vLLM中,将长序列的Prefill分解成多个chunk,Prefill和Decode都使用了causal mask,每个chunk的计算模式类似于Decode步骤,以此实现混合调度。
- prefill: 直接取
1 - max_length - decode: 其余位置用
pad填充
# 内部表示
batch_tokens = [
# Prefill chunk: 64个token
[t1, t2, ..., t64],
# Decode 1: 1个token + 49个填充(为了对齐)
[t1, pad, pad, ..., pad],
# Decode 2: 1个token + 49个填充
[t1, pad, pad, ..., pad],
# ... 更多decode
]
注意
使用了chunked prefill之后,由于自回归的特点,后一个 chunk 的 attention 计算依赖前一个 chunk 产生的 KV cache。因此prefill的推理方式,从完全并行,变成了chunk内并行,chunk间串行。