4、vLLM内存架构总览:GPU显存布局、CPU内存角色、内存池设计
聊到vLLM的长序列推理,内存管理绝对是绕不开的核心。我刚开始接触这个项目时,第一反应就是:这玩意儿到底怎么在有限的显存里塞下那么长的上下文?
说白了,vLLM能跑长序列,靠的不是魔法,而是一套精心设计的内存架构。今天我就带你从全局视角,把GPU显存怎么布局、CPU内存扮演什么角色、以及那个神奇的内存池到底怎么运作,一次性讲清楚。
核心观点:vLLM的内存架构,本质上是在「显存不够用」和「CPU内存太慢」之间找平衡。它用内存池做缓冲,用PagedAttention做精细化管理,最终实现了长序列推理的可行性。
4.1 GPU显存布局:每一块都要精打细算
先说说GPU显存。你想想看,一张A100 80G的卡,看着挺大,但跑起长序列来,分分钟就爆了。vLLM是怎么分配这些显存的?
我个人习惯把GPU显存分成三大块来看:
- 模型权重区:存放Transformer的每一层参数。这块是固定的,模型加载完就占住了。
- KV Cache区:这是vLLM重点优化的地方。每个请求的Key和Value缓存都放这儿,长序列推理时,这块是最大的消耗者。
- 临时计算区:包括激活值、梯度(训练时)、以及各种中间结果。推理时这块相对小一些。
我在项目中遇到过一个问题:有同事把模型权重和KV Cache混在一起管理,结果显存碎片化严重,跑着跑着就OOM了。vLLM的做法是——严格分区,互不干扰。
我的经验:KV Cache区的大小,直接决定了你能支持的最大序列长度。vLLM默认会预留约80%的显存给KV Cache(除去模型权重后),这个比例你可以通过--gpu-memory-utilization参数调整。
4.2 CPU内存角色:不是备胎,是战略缓冲
很多人觉得CPU内存就是「显存放不下时拿来凑合用的」。其实不然。在vLLM里,CPU内存扮演着三个关键角色:
- Swap空间:当显存里的KV Cache满了,就把不常用的块换到CPU内存里。等需要时再换回来。
- 预加载缓冲区:新请求的初始Prompt可以先放在CPU内存里做预处理,等GPU有空了再送过去。
- 元数据存储:每个Block的映射关系、引用计数、状态标记,这些轻量级数据放在CPU内存里更划算。
嗯,这里要注意:CPU内存和GPU显存之间的传输是有代价的。我曾经踩过一个坑——频繁地在CPU和GPU之间交换数据,结果推理延迟反而增加了。后来我学乖了,只在真正需要时才触发Swap。
避坑指南:我曾经把CPU内存设得太大,结果系统开始用磁盘Swap,性能直接崩了。建议CPU内存分配不要超过物理内存的50%,留点余量给操作系统和其他进程。
4.3 内存池设计:vLLM的精髓所在
终于聊到内存池了。这是vLLM最让我拍案叫绝的设计之一。它解决了一个核心问题:如何避免显存碎片化?
传统做法是:每个请求来了,动态分配一块连续显存。请求走了,释放掉。时间一长,显存就变成「蜂窝煤」了——到处都是小碎片。
vLLM的做法是:提前申请一大块连续显存,切成固定大小的Block(默认16个token一个Block)。每个请求按需领取Block,用完了归还。这就是内存池的核心思想。
我画了一张图,帮你理解这个流程:
内存池的核心数据结构,我简单列一下:
| 组件 | 作用 | 存储位置 |
|---|---|---|
| Block Table | 记录每个Block的分配状态(空闲/已用) | CPU内存 |
| Block映射表 | 逻辑Block ID → 物理Block地址 | CPU内存 |
| 引用计数器 | 每个Block被多少个请求共享 | CPU内存 |
| 空闲Block链表 | 所有可用的空闲Block列表 | CPU内存 |
| 物理Block数据 | 实际的KV Cache数据 | GPU显存 |
你可能会问:为什么元数据都放CPU内存?因为元数据访问频率高,但数据量小。放在CPU内存里,既不影响GPU的计算速度,又能快速查询。这是vLLM的一个巧妙设计。
一个小技巧:你可以通过--block-size参数调整每个Block的大小。默认是16个token。如果你的序列特别长(比如10万token),可以试试32或64,能减少Block数量,降低管理开销。但别太大,否则内部碎片会增多。
4.4 内存池的工作流程
理解了各个组件,我们来看看它们怎么协同工作。我拿一个实际请求来举例:
- 请求到达:新请求的Prompt先进入CPU内存的预加载缓冲区,做分词和预处理。
- 分配Block:vLLM从空闲Block链表中取出足够的Block,分配给这个请求。每个Block对应一段连续的KV Cache空间。
- 写入数据:GPU计算时,把Key和Value写入对应的物理Block地址。
- 共享与复用:如果多个请求共享相同的前缀(比如系统Prompt),它们的Block映射表会指向同一个物理Block。引用计数器+1。
- 换出与换入:显存不够时,把引用计数为0的Block换出到CPU Swap空间。需要时再换回来。
- 释放Block:请求完成后,归还所有Block到空闲链表。引用计数器清零。
整个过程,说白了就是「借了还,还了借」。但因为有内存池的精细管理,碎片化问题被大大缓解了。
注意:我曾经在生产环境遇到过一个问题——某个请求占用了大量Block,但一直不释放,导致其他请求饿死。后来发现是引用计数器没正确递减。所以,引用计数的正确性是内存池稳定运行的命门,一定要做好单元测试。
4.5 总结一下
vLLM的内存架构,其实就三句话:
- GPU显存:分三区,KV Cache用内存池管理,避免碎片。
- CPU内存:做Swap缓冲、预加载、存元数据,是GPU的得力助手。
- 内存池:固定Block大小,引用计数共享,换入换出按需进行。
这套设计,让vLLM能在有限的显存里,跑出远超传统方案的长序列推理。下一节,我会深入PagedAttention的细节,看看它怎么和内存池配合,实现真正的「无限上下文」。