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,在物理显存中可以是离散的页。通过页表来映射逻辑块到物理块。
我画了一张图,帮你理解这个映射关系:
你看,逻辑上连续的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)彻底改变了这个局面。说白了就是:谁准备好了谁先跑,谁跑完了谁先走。
具体流程是这样的:
- 调度器维护一个请求队列,每个请求记录当前生成了多少token
- 每次迭代,调度器从队列中选出一批请求,组成一个batch
- batch里的请求一起做prefill(预填充)和decode(解码)
- 某个请求生成完了,立即从batch中移除,释放显存
- 新请求可以随时加入下一个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