显存不够用?vLLM 把 KV 缓存拆成了 16 个词一页

同样一张显卡,同样一个模型,换个推理引擎,能同时服务的用户数可以差出好几倍。差距不在模型,而在显存怎么管、请求怎么排。开发者 Aleksa Gordić 近日发布了一篇 vLLM 源码级拆解长文,以 2025 年 8 月 9 日的 42172ad 提交为准,完整梳理了这套高吞吐推理引擎的内部结构。

vLLM 由加州大学伯克利分校团队于 2023 年开源,采用 Apache 2.0 协议,目前已是开源大模型私有化部署事实上的主流选择,被大量云平台和推理服务商集成。它的核心思路,是把操作系统管内存的老办法搬到了 GPU 上。

把显存当成分页内存来管

传统推理框架给每个请求预留一整段连续显存,按最大可能长度分配。一个请求只生成了 100 个词,却按 4096 个词的规格占着位置,浪费极其可观。

vLLM 的做法是分页。KV 缓存(模型生成时缓存的键值向量,避免重复计算)被切成固定大小的块,默认每块存 16 个 token。每个请求维护一张块表,把逻辑上连续的缓存映射到物理上零散的显存页。单块占用的字节数按「2(键和值)× 块大小 × KV 注意力头数 × 头维度 × 数据类型字节数」计算,块池规模常常达到数十万块。

调度器的核心数据结构就是这个空闲块队列。每来一批新词,allocate_slots 先算需要几块——比如预填充阶段有 17 个新词,就是向上取整 17÷16 等于 2 块;池子不够时,引擎会抢占低优先级请求,把它们已占用的块归还池中。

预填充和解码,是两种完全不同的活

引擎面对的负载分两类:预填充是对整段提示词做一次前向计算,属于计算密集型;解码只处理最新一个词,但仍要把整个模型权重和缓存读一遍,属于显存带宽密集型。

老版本 V0 引擎一个步骤内只能二选一,新版 V1 调度器可以把两类请求混在同一步里跑。调度顺序上,已在运行队列中的解码请求优先,剩余的词预算再分给等待队列里的预填充请求。调度策略支持先来先服务和按优先级两种。

前向计算时,所有序列被拍平拼接成一条「超级序列」,靠位置索引和注意力掩码保证各自只看自己的内容。这样就不需要右侧补齐,也就天然支持连续批处理——完成一个请求立刻释放资源补进新请求,GPU 不必等最慢的那条跑完。

四个让长请求不拖后腿的设计

分块预填充:把超长提示词的预填充切成小段,避免一个长请求独占一个引擎步骤,把其他请求的延迟全部拉高。参数上表现为给 long_prefill_token_threshold 设一个正整数。

前缀缓存:多轮对话中系统提示词和历史消息是重复的,相同前缀的缓存块可以在请求之间共享,不必重算。

引导解码:用语法约束的有限状态机限制采样范围,让输出严格符合指定格式。

投机解码与 PD 分离:前者用小模型先猜若干个词再由大模型批量验证,后者把预填充和解码拆到不同资源上执行。

启动时那几秒都在干什么

引擎构造阶段,工作进程会依次完成三件事:分配 CUDA 设备并按 gpu_memory_utilization(例如设为 0.8 即占用八成显存)核对余量、加载模型权重并切换到推理模式、初始化 KV 缓存。

最后一步会先跑一次空转前向计算,对显存拍个快照,据此反推能塞下多少个缓存块。除非显式指定 --enforce-eager,引擎还会针对若干预热批量大小捕获 CUDA Graph,把整串 GPU 操作录成一张有向无环图,后续直接回放,省下逐个启动内核的开销。

这些机制叠加起来,才是「同一张卡多扛几倍请求」的真正来源。对于正在做私有化部署、纠结要不要加卡的团队,先把引擎参数调明白,往往比直接采购更划算。

来源:Aleksa Gordić 技术博客《Inside vLLM: Anatomy of a High-Throughput LLM Inference System》、vLLM 开源项目