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)。

关键认知:KV Cache并非固定大小,它随并发序列数和生成长度动态增长。传统推理框架为每个请求预分配最大长度的缓存,导致大量浪费;而vLLM将KV Cache视为可动态调度的“显存页”。
GPU内存层次与KV Cache分配示意(A100 80GB) 寄存器文件 每个SM 256KB · 线程私有 · 带宽最高 共享内存 每个SM 192KB · 可编程 · 线程块内共享 L2 缓存 40MB · 所有SM共享 · 缓存全局内存访问 HBM 全局显存(80GB) 带宽 2TB/s · 模型权重 + KV Cache 驻留区 模型权重 70B FP16 ≈ 140GB(需多卡) 40GB(单卡示例) 固定占用 · 推理期间不变 KV Cache(动态区域) 可用 ≈ 80×0.9 − 40 = 32GB 按需分配 · 随序列增长 传统框架:预分配最大长度 → 浪费 加载到SM 逐层访问 gpu_memory_utilization=0.9 控制权重+KV Cache总占比,剩余10%留给CUDA context与激活值

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)时,多个候选序列共享相同前缀,但传统方式为每个序列独立分配缓存,前缀部分被重复存储。
典型后果:当并发请求数增加时,显存碎片化导致可用连续空间不足,系统不得不降低批大小或直接报OOM(Out of Memory)。即使显存总量足够,碎片化也会让有效利用率大幅下降。

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)机制极大节省了显存。

量化收益:若前缀长度为512 token,单token KV Cache为512KB(7B模型),则前缀共享可节省 512×512KB×(3−1) = 512MB 显存。在长提示词、高并发场景下,这一节省可达数GB。

3.6 误区与复盘

误区1:认为KV Cache只与序列长度有关。
实际上,KV Cache还与模型层数、注意力头数、头维度、精度直接相关。层数×头数×头维度越大,单token KV Cache越大。例如,70B模型(80层,64头,头维128)单token KV Cache约2.5MB,是7B模型的5倍。
误区2:忽略block_size对碎片的影响。
block_size过小(如4)会减少内部碎片但增加块表开销和索引计算成本;block_size过大(如64)则内部碎片增多。vLLM默认16是权衡后的选择,实际调优时应结合平均生成长度测试。
复盘要点:PagedAttention并非消除所有碎片,而是将碎片控制在单块粒度内。最后一个块的平均浪费为block_size/2个token的KV空间。对于短序列占比高的场景,适当减小block_size可提升显存利用率,但需监控吞吐变化。