2. vLLM核心架构解析:模型加载机制、PagedAttention原理、KV Cache管理

好,咱们直接进入正题。这一章我会把vLLM最核心的三个模块拆开来讲。说白了,你理解了这三个东西,vLLM在你眼里就不再是个黑盒了。我个人习惯是,先搞懂原理再动手调优,这样踩坑的时候心里有数。

2.1 模型加载机制:从磁盘到GPU的旅程

模型加载,听起来就是把文件读进来对吧?但实际没那么简单。我刚开始部署vLLM时,就遇到过加载一个70B模型把内存撑爆的情况。嗯,这里要注意,vLLM的加载机制其实分三步走。

2.1.1 权重格式与分片

vLLM支持多种权重格式,比如常见的FP16、BF16,还有量化后的INT4、INT8。你想想看,一个70B的模型,FP16下光权重就140GB。单卡显存根本放不下,所以必须分片。

核心原则:vLLM采用张量并行(Tensor Parallelism)来切分模型。每个GPU只加载自己负责的那部分权重。

举个例子,你用4张A100部署LLaMA-70B:

# 启动命令示例
python -m vllm.entrypoints.openai.api_server \
    --model /path/to/llama-70b \
    --tensor-parallel-size 4 \
    --dtype bfloat16

这里--tensor-parallel-size 4就是告诉vLLM:把模型切成4份,每张卡一份。我在项目中遇到过,如果显存刚好卡在边界,可以配合--max-model-len适当降低序列长度来省显存。

2.1.2 加载流程

vLLM的加载流程大致如下:

  1. 元数据解析:读取config.json,确定模型结构、层数、头数等
  2. 权重映射:根据张量并行策略,计算每张卡需要哪些权重
  3. 分片加载:每个进程只加载自己的那部分,避免重复
  4. 显存分配:预分配KV Cache空间(这个后面细说)

避坑指南:我曾经遇到过加载时OOM,原因是--max-model-len设得太大,导致KV Cache预分配把显存吃光了。解决办法是先用小序列长度测试,再逐步调大。

2.2 PagedAttention原理:灵感来自操作系统

PagedAttention是vLLM的杀手锏。你想想看,传统的Attention实现里,KV Cache是一整块连续内存。但问题是,请求的长度不一样,有的长有的短,连续分配很容易产生碎片。

vLLM团队怎么解决的?他们借鉴了操作系统的分页内存管理。说白了,就是把KV Cache切成固定大小的“页”(Page),按需分配。这和虚拟内存的思路一模一样。

2.2.1 核心数据结构

PagedAttention引入了几个关键概念:

概念 说明
逻辑块(Logical Block) 每个请求的KV Cache被分成逻辑块,每个块固定大小(比如16个token)
物理块(Physical Block) GPU显存中实际分配的块,大小和逻辑块一致
块表(Block Table) 记录逻辑块到物理块的映射关系

为什么这样做?好处很明显:

  • 消除碎片:物理块可以分散在显存各处,不要求连续
  • 按需分配:请求来了才分配物理块,不像传统做法一次性预留全部空间
  • 共享内存:多个请求可以共享同一个物理块(比如beam search场景)

我的经验:在实际部署中,块大小(block size)是个关键参数。默认是16,但如果你处理的都是短文本(比如对话场景),可以试试8,能减少内部碎片。反之,长文本场景用32更高效。

2.2.2 注意力计算

PagedAttention的计算过程和标准Attention不太一样。它需要根据块表去“拼凑”出完整的KV序列。具体来说:

# 伪代码示意
def paged_attention(query, block_table, physical_kv_cache):
    # 遍历每个逻辑块
    for logical_block_id in range(num_blocks):
        # 查表找到物理块地址
        physical_block_id = block_table[logical_block_id]
        # 从物理缓存中读取KV
        k, v = physical_kv_cache[physical_block_id]
        # 计算当前块的注意力
        attn_block = attention(query, k, v)
        # 累加结果
        attn_output += attn_block
    return attn_output

嗯,这里要注意,实际实现中vLLM用了高度优化的CUDA kernel,不会真的逐块循环。但原理就是这么回事。

2.3 KV Cache管理:显存就是命根子

KV Cache管理,说白了就是怎么用好显存。在LLM推理中,KV Cache占的显存往往比模型权重还大。我见过不少团队,模型能加载进去,但跑几个请求就OOM了,问题就出在Cache管理上。

2.3.1 预分配 vs 动态分配

vLLM默认采用预分配策略。启动时根据--max-model-len--gpu-memory-utilization算出KV Cache的总容量,一次性分配好。这样做的好处是:

  • 避免运行时分配开销:推理过程中不会因为分配显存而卡顿
  • 内存确定性:能提前知道最多支持多少个并发请求

但代价也很明显:如果--max-model-len设得太大,大量显存会被闲置。

调优建议:我个人习惯把--gpu-memory-utilization设为0.9到0.95,留出10%给模型权重和临时变量。如果业务场景的序列长度波动很大,可以试试0.85,更稳妥。

2.3.2 调度策略

vLLM的调度器负责决定:哪个请求的KV Cache该被换出?哪个该保留?

它用的是类似LRU的策略,但做了优化:

  1. 请求优先级:正在生成的请求优先保留
  2. 块级换出:以块为单位换出,不是整个请求
  3. 写回机制:换出的块可以写回CPU内存(如果配置了)

我曾经踩过的坑:有次我把--swap-space设得很大,以为能靠CPU内存来扩展容量。结果发现CPU-GPU之间的传输带宽成了瓶颈,吞吐量反而下降了。后来我干脆关掉了swap,只靠GPU显存,效果反而更好。

2.3.3 显存监控

生产环境中,我建议你至少监控这几个指标:

指标 说明 告警阈值
gpu_cache_usage_perc KV Cache使用率 > 90%
num_requests_running 正在处理的请求数 根据模型容量设定
num_requests_waiting 排队等待的请求数 > 100

这些指标vLLM的Prometheus exporter都会暴露,直接接入Grafana就行。

2.4 核心架构总览

为了让你更直观地理解这三个模块的关系,我画了张图:

vLLM核心架构总览 模型加载 权重分片加载 张量并行分配 元数据解析 PagedAttention 逻辑块/物理块 块表映射 非连续内存访问 KV Cache管理 预分配策略 LRU调度换出 显存利用率监控 权重 缓存 三者协同工作流程 请求到达 → 模型加载权重 → PagedAttention计算 → KV Cache动态管理 高吞吐推理输出

从这张图你能看到,三个模块不是孤立的。模型加载决定了权重怎么放,PagedAttention决定了计算怎么快,KV Cache管理决定了显存怎么省。三者配合好了,才能实现vLLM宣称的“接近零碎片”和“高吞吐”。

一句话总结:模型加载是基础,PagedAttention是核心创新,KV Cache管理是工程落地关键。三者缺一不可。

好了,这一章的内容就到这里。理解了这个架构,你再去调优vLLM,就知道该从哪里下手了。


专注资料整理