3、内存管理原理:GPU内存层次、KV Cache机制、PagedAttention如何减少内存碎片与浪费
3.1 GPU内存层次与显存预算
要理解vLLM的内存管理,首先需要建立GPU内存的层次化视图。以NVIDIA A100/H100 80GB为例,显存并非单一均匀的存储池,而是由多个具有不同带宽和容量的层级构成:
- 寄存器文件(Register File):每个SM内部,总计约256KB,带宽最高,但仅能由当前线程访问。
- 共享内存(Shared Memory):每个SM约192KB,可编程控制,用于线程块内数据交换,延迟极低。
- L2缓存:A100为40MB,H100为50MB,所有SM共享,用于缓存全局内存访问。
- HBM全局显存(Global Memory):80GB,带宽约2TB/s,是KV Cache和模型权重的主要驻留地。
在推理场景中,模型权重(如70B模型以FP16加载约140GB,需多卡)和KV Cache是显存的两大消费者。vLLM的显存管理器将可用显存划分为两部分:gpu_memory_utilization(默认0.9)控制用于模型权重和KV Cache的总比例,剩余部分留给CUDA context和激活值。例如,在80GB GPU上,若模型权重占40GB,则KV Cache可用约32GB(80×0.9−40)。
3.2 KV Cache机制:为什么需要它?
Transformer解码器在生成第t个token时,需要计算注意力权重:
Attention(Q, K, V) = softmax(Q·K^T / √d_k) · V
其中Q来自当前token,而K和V来自所有历史token。若不缓存,每生成一个新token都要重新计算之前所有token的K和V,时间复杂度从O(n)退化为O(n²),极大浪费算力。KV Cache正是将历史token的K、V矩阵缓存下来,避免重复计算。
KV Cache的大小计算如下:
KV Cache大小 = 2(K和V) × 层数 × 注意力头数 × 头维度 × 序列长度 × 精度字节数
示例:7B模型(32层,32头,头维128,FP16)
单token KV Cache = 2 × 32 × 32 × 128 × 2字节 = 512KB
生成1024个token时,单序列KV Cache = 512KB × 1024 = 512MB
这意味着一个7B模型在生成长序列时,KV Cache可能占据比模型权重更大的显存空间。若同时服务多个用户,KV Cache总量会迅速膨胀。
3.3 传统内存管理的问题:碎片化与浪费
在vLLM出现之前,主流推理框架(如FasterTransformer、原始HuggingFace实现)采用静态缓存分配策略:
- 预分配最大长度:为每个请求分配
max_seq_len的连续显存空间,即使实际生成远未达到该长度,空间也无法释放给其他请求。 - 内部碎片:请求实际只用了分配空间的30%,剩余70%被锁定,无法被其他请求使用。
- 外部碎片:不同请求的缓存块大小不一,频繁分配释放后,显存出现大量不连续空洞,新的大块请求可能因找不到连续空间而失败。
- 无法共享:并行采样(如beam search)时,多个候选序列共享相同前缀,但传统方式为每个序列独立分配缓存,前缀部分被重复存储。
3.4 PagedAttention:操作系统的分页思想
vLLM的核心创新是将操作系统的虚拟内存分页思想引入KV Cache管理。传统KV Cache要求连续物理显存,而PagedAttention将KV Cache划分为固定大小的块(Block),每个块存储固定数量token的K/V向量(默认block_size=16)。
每个序列维护一张块表(Block Table),记录逻辑块到物理块的映射。逻辑上连续的KV Cache在物理显存中可以不连续,甚至分散在不同区域。注意力计算时,通过块表索引到对应的物理块,分块读取K/V进行attention计算。
# vLLM中KV Cache块表示意
class KVBlockTable:
def __init__(self):
self.logical_to_physical = {} # 逻辑块号 → 物理块号
self.ref_count = {} # 物理块引用计数
def allocate_block(self):
# 从显存分配器获取空闲物理块
physical_block = allocator.get_free_block()
return physical_block
PagedAttention带来的核心收益:
| 问题 | 传统静态分配 | PagedAttention |
|---|---|---|
| 内部碎片 | 预分配最大长度,实际使用率低 | 按需分配块,最后一个块可能不满(平均浪费 |
| 外部碎片 | 连续分配导致显存空洞 | 物理块不连续,无外部碎片 |
| 前缀共享 | 不支持,重复存储 | 块级引用计数,多序列共享相同前缀块 |
| 内存利用率 | 通常40%~60% | 可达90%以上 |
3.5 案例:并行采样中的前缀共享
假设用户输入提示词“中国的首都是”,使用n=3的并行采样生成3个不同续写结果。三个序列共享完整的前缀“中国的首都是”,其K/V计算结果完全相同。
在传统框架中,三个序列各自分配独立缓存,前缀部分被存储3次。而vLLM中,前缀token的KV Cache只存储一次,三个序列的块表都指向同一组物理块,仅当序列开始分叉(生成不同token)时才分配新块。这种写时复制(Copy-on-Write)机制极大节省了显存。
3.6 误区与复盘
实际上,KV Cache还与模型层数、注意力头数、头维度、精度直接相关。层数×头数×头维度越大,单token KV Cache越大。例如,70B模型(80层,64头,头维128)单token KV Cache约2.5MB,是7B模型的5倍。
block_size过小(如4)会减少内部碎片但增加块表开销和索引计算成本;block_size过大(如64)则内部碎片增多。vLLM默认16是权衡后的选择,实际调优时应结合平均生成长度测试。