vLLM核心架构:PagedAttention原理、KV Cache管理、连续批处理机制

聊到vLLM,绕不开的就是它的三大核心支柱。说实话,我第一次看到PagedAttention那篇论文时,心里就一个想法——这玩意儿早该有人做了。传统Transformer推理时,显存浪费得让人心疼。今天我就把这几个核心机制掰开揉碎了讲给你听。

一、PagedAttention:虚拟内存的灵感

先问个问题:为什么大模型推理这么吃显存?

答案在于KV Cache。每次生成一个token,模型都要把Key和Value缓存下来。假设模型有32层,hidden size是4096,序列长度2048,精度是FP16——算下来一个请求就要吃掉大约1GB显存。这还只是单条请求。

传统做法是预分配一块连续显存。但问题是,你根本不知道最终序列有多长。我见过不少项目,为了省事直接按最大长度分配,结果显存利用率不到40%。

PagedAttention的思路很简单——借鉴操作系统的虚拟内存管理。把KV Cache切成固定大小的“页”(Page),每个页可以独立分配、释放、甚至共享。

核心思想:逻辑上连续的KV Cache,在物理显存中可以是离散的页。通过页表来映射逻辑块到物理块。

我画了一张图,帮你理解这个映射关系:

逻辑KV Cache(连续) Token 1 Token 2 Token 3 Token 4 Token 5 Token 6 页表(映射关系) 逻辑页 0 → 物理页 3 逻辑页 1 → 物理页 1 逻辑页 2 → 物理页 5 物理显存(离散) 页 0 页 1 页 2 页 3 页 4 页 5 ← 页1(已分配) ← 页3(已分配) ← 页5(已分配) 逻辑页0→物理页3,逻辑页1→物理页1,逻辑页2→物理页5 物理页0、2、4空闲,可分配给其他请求

你看,逻辑上连续的6个Token,在物理显存里可能散落在不同位置。页表就是那个“翻译官”。

我的经验:页大小设置很关键。页太小,页表开销大;页太大,内部碎片多。我个人习惯设成16个token一页,在大多数场景下效果不错。

二、KV Cache管理:从浪费到极致复用

有了PagedAttention,KV Cache的管理就灵活多了。vLLM实现了几个关键策略:

  • 按需分配:不再预分配整块显存,而是生成一个token就分配一页。你想想看,如果请求只生成了50个token,传统方式可能占了2048的坑,现在只占50个token的页。
  • 页共享:这是最妙的部分。多个请求如果共享相同的prefix(比如系统提示词),它们的KV Cache页可以共用。我在项目中遇到过,一个对话系统有2000字的系统提示,共享后显存直接省了60%。
  • 写时复制(Copy-on-Write):当某个请求要修改共享页时,才真正复制一份。这跟操作系统的fork机制一模一样。

来看一个具体的显存对比:

指标 传统方式 vLLM PagedAttention
显存分配方式 预分配最大长度 按需分配页
内部碎片 高(可达60%) 低(<5%)
请求间共享 不支持 支持prefix共享
显存利用率 40%-50% 90%以上
支持的最大并发 受限于预分配 提升2-4倍

注意:页共享虽然省显存,但会增加调度复杂度。我曾经在生产环境遇到过一个问题——共享页被意外释放,导致多个请求同时出错。后来加了引用计数才解决。嗯,这里要提醒你,共享页的生命周期管理一定要做好。

三、连续批处理:让GPU一刻不停

传统批处理有个大问题——等。等所有请求都生成完了,才一起返回。如果某个请求生成长文本,其他短请求就得干等着。

vLLM的连续批处理(Continuous Batching)彻底改变了这个局面。说白了就是:谁准备好了谁先跑,谁跑完了谁先走

具体流程是这样的:

  1. 调度器维护一个请求队列,每个请求记录当前生成了多少token
  2. 每次迭代,调度器从队列中选出一批请求,组成一个batch
  3. batch里的请求一起做prefill(预填充)和decode(解码)
  4. 某个请求生成完了,立即从batch中移除,释放显存
  5. 新请求可以随时加入下一个batch

你想想看,这跟操作系统的抢占式调度是不是很像?

关键优化点:连续批处理的核心是“动态batch”。每次迭代的batch大小和组成都可能不同。调度器需要平衡两个目标——最大化GPU利用率和最小化请求延迟。

我分享一个实际调优经验。刚开始用vLLM时,我把max_num_seqs设得很大,想着一次处理越多越好。结果发现延迟反而上去了。为什么?因为GPU的显存带宽是有限的,batch太大,每个请求分到的带宽就少了,生成速度反而变慢。

后来我总结了一个经验公式:

# 根据显存带宽估算最优batch大小
def estimate_optimal_batch(gpu_memory_gb, model_size_gb, kv_cache_per_token):
    available_memory = gpu_memory_gb * 0.9  # 留10%余量
    model_memory = model_size_gb
    kv_memory_per_request = max_seq_len * kv_cache_per_token
    
    max_batch_by_memory = (available_memory - model_memory) / kv_memory_per_request
    # 还要考虑计算带宽
    # 一般建议 batch_size = 4~8 起步,逐步调优
    return min(max_batch_by_memory, 8)  # 保守起见先设8

我的建议:刚开始用vLLM时,先别急着调参。默认配置在大多数场景下已经比传统方案好很多。等跑熟了,再根据你的业务场景——是延迟敏感还是吞吐优先——来微调max_num_seqs和max_model_len。

四、三者如何协同工作

这三个机制不是孤立的。它们环环相扣:

  • PagedAttention提供了灵活的显存管理基础
  • KV Cache管理利用页机制实现了高效分配和共享
  • 连续批处理则依赖前两者,才能做到动态调度和即时释放

举个例子你就明白了。假设有3个请求同时进来:

  • 请求A:系统提示词+用户问题,预计生成200token
  • 请求B:系统提示词+用户问题,预计生成50token
  • 请求C:纯用户问题,预计生成100token

传统方式下,A、B、C各自预分配2048token的显存。vLLM怎么做?A和B共享系统提示词的KV Cache页,C单独分配。连续批处理让B先跑完释放资源,A和C继续。整个过程显存占用不到传统方式的1/3。

说实话,我第一次在生产环境部署vLLM时,看到显存利用率从45%飙升到92%,心里还是挺震撼的。这不仅仅是省成本的问题——同样的硬件,你能服务更多用户,这才是真正的价值。


公众号:蓝海资料掘金营,微信deep3321