大语言模型(LLM)推理服务的扩展性瓶颈,往往不在计算吞吐量,而在 GPU 显存。传统推理框架将 KV Cache 以连续张量形式预分配,造成大量内存浪费,导致 batch size 被严重限制。vLLM 通过引入操作系统虚拟内存机制启发的 PagedAttention,将显存利用效率推向新高度,成为当前开源 LLM 推理引擎的事实标准之一。
一、LLM 推理的核心挑战
1.1 Prefill 与 Decode 两阶段特性
LLM 推理包含两个截然不同的计算阶段:
- Prefill(预填充):接收用户完整 prompt,计算所有 token 的注意力,生成第一个输出 token。此阶段计算密集,可充分利用 GPU 算力,但延迟高(与 prompt 长度成正比)。
- Decode(解码):自回归生成后续 token,每次仅处理最新一个 token。此阶段内存带宽受限,计算利用率低。
两个阶段在延迟、计算密度和内存访问模式上差异巨大,对调度系统提出了挑战。
1.2 KV Cache 的显存爆炸
Transformer 每层的每个注意力头都需要缓存 Key 和 Value 矩阵。以 FP16 精度计算,单条请求的 KV Cache 显存占用为:
2 × num_layers × num_heads × head_dim × seq_len × 2 bytes
以 Llama-2-70B 为例(80 层、8 组 GQA、128 head_dim),序列长度 4096 时:
def kv_cache_size(layers, heads, head_dim, seq_len, bytes_per_val=2):
"""KV Cache 显存占用(单位:字节)"""
return 2 * layers * heads * head_dim * seq_len * bytes_per_val
# Llama-2-70B, GQA 8 KV heads per group
size = kv_cache_size(
layers=80,
heads=8, # GQA 后的 KV head 数
head_dim=128,
seq_len=4096
)
print(f"单请求 KV Cache: {size / 1024**3:.2f} GiB")
# 输出: 约 1.25 GiB
若 batch_size = 16,仅 KV Cache 就需 20 GiB,再加上模型权重(140 GiB @ FP16),GPU 显存瞬间告急。
1.3 预分配导致的内存浪费
传统框架如 Hugging Face Transformers 在推理开始前就按照 (max_batch_size, max_seq_len) 分配固定大小的 KV Cache。实际场景中,序列长度差异巨大:一条请求 50 tokens,另一条 4000 tokens,但两者占用同等大小的预分配张量。这种 internal fragmentation(内部碎片) 使得显存利用率极低,batch size 往往由内存而非算力决定上限。
二、PagedAttention:虚拟内存的工程迁移
vLLM 的核心创新 PagedAttention,其思想直接借鉴操作系统虚拟内存管理:将物理内存划分为固定大小的页(page),通过页表实现逻辑地址到物理地址的按需映射。
2.1 核心设计
Block Table(块表):每张请求维护一个块表,记录逻辑 token 位置到物理内存块的映射。块大小固定(默认 16 个 token),物理块在显存池中按需分配,逻辑上连续但物理上不连续。
非连续物理存储:与传统框架要求 KV Cache 为 (batch, seq_len, ...) 的连续张量不同,vLLM 的物理 KV Cache 存储在离散的块中。注意力计算通过块表索引,按块加载 KV 数据,绕开了连续内存的约束。
Copy on Write(写时复制):当多个请求共享同一段 prompt prefix(如 system prompt、RAG context)时,它们共用同一组物理块,引用计数递增。当某条请求生成新 token 需要写入时,再复制一份私有块。这类似于操作系统中 fork() 的 COW 机制。
2.2 对比传统方案
| 机制 | 传统框架 | vLLM PagedAttention |
|---|---|---|
| 存储方式 | 连续预分配张量 | 离散固定大小块 |
| 内存分配 | 静态,按最大序列 | 动态,按需分配 |
| 碎片问题 | 严重内部碎片 | 仅尾部块有碎片 |
| Prefix 共享 | 复制多份 | COW 共享 |
| 块大小 | 不固定(与 seq_len 相关) | 固定 16 tokens(可调) |
三、调度器设计:Requests 的生命周期管理
vLLM 的调度器是另一个工程亮点。它通过细粒度状态机和连续批处理策略,最大化 GPU 利用率。
3.1 请求三态模型
每条请求在生命周期中处于以下状态之一:
- Waiting:刚到达,等待 GPU 资源执行 prefill。
- Running:正在 GPU 上执行 prefill 或 decode。
- Swapped:因显存不足,KV Cache 被换出到 CPU 内存,等待重新调度。
[Waiting] --prefill 完成--> [Running] --decode 生成--> [Done]
^ |
|__ swapped 换入 ____________|__ 显存不足 swapped 换出 __>
3.2 连续批处理(Continuous Batching)
传统 Static Batching(静态批处理) 要求同批次内所有请求同时开始、同时结束。最短请求完成后,其 slot 也无法被新请求复用,直到批次内最长请求结束,造成大量算力空转。
Continuous Batching,又称 In-flight Batching,允许在每次 forward 之间动态增删批次中的请求:
- 每轮 decode 完成后,检查是否有运行中请求已满足停止条件(EOS、max_tokens),将其移出批次。
- 从 Waiting 队列挑选新请求加入批次,执行 prefill,生成首个 token 后立即与当前 decode token 合并为一个统一的 forward batch。
- 通过 Token Budget Scheduling(token 预算调度),限制每轮迭代处理的 token 总数,平衡吞吐与延迟。
3.3 抢占与重计算策略
当新请求涌入导致显存不足时,调度器采取以下策略:
- Preemption(抢占):将部分 Running 请求降级为 Swapped,将其 KV Cache 块换出到 CPU RAM。
- Recomputation(重计算):当换入请求重新调度时,可选择直接恢复 KV Cache,或丢弃已生成部分、重新 prefill 计算。vLLM 默认采用后者,因为 prefill 的计算开销通常远小于显存换入换出的带宽开销。
3.4 新请求如何加入运行批次
vLLM 在每次迭代(iteration-level)调度:
- 计算当前 GPU 剩余显存。
- 尝试为 Waiting 队列头部请求分配物理块执行 prefill。
- 若显存不足,检查是否可通过抢占低优先级请求腾出空间。
- Prefill 和 decode 的请求被合并为混合批次(mixed batch),在一次 CUDA kernel 启动中执行。
这种混合批次策略有效解决了纯 continuous batching 中 prefill 和 decode 无法在一个批次内合并的问题。
四、内存节省的量化分析
4.1 块分配器的碎片分析
PagedAttention 通过固定大小块分配器管理显存。内部碎片仅存在于每个 block 的尾部未用位置:一条 18 token 的请求需要 2 个块(32 token 容量),内部碎片为 14/32 ≈ 43.75%。但由于序列通常较长,碎片被摊薄。同时,显存池中的空闲块可供任何请求复用,消除传统方案中因预分配产生的 external fragmentation。
4.2 引用计数与共享
vLLM 对物理块维护引用计数,支持两类共享场景:
- Beam Search(束搜索):多条 beam 共享原始 prompt 的 KV Cache,仅在分叉时触发 COW。
- Parallel Sampling(并行采样):对同一 prompt 生成多个不同回答,KV Cache 完全共享,直到各采样路径产生差异。
在 beam_width = 4、seq_len = 2048 的场景下,共享可减少 60%-70% 的重复 KV Cache。
4.3 横向对比
| 系统 | 核心机制 | KV Cache 管理 | 适用场景 |
|---|---|---|---|
| vLLM | PagedAttention | 分页+动态分配+共享 | 通用高吞吐服务 |
| DeepSpeed-Inference | DeepSpeed-MII | 连续张量,静态分配 | 大模型单机多卡 |
| TensorRT-LLM | In-flight Batching | Paged KV Cache(受 vLLM 启发) | NVIDIA GPU 极致性能 |
| Text-Generation-Inference (TGI) | FlashAttention + Continuous | 分页缓存 | Hugging Face 生态 |
TensorRT-LLM 在 NVIDIA 硬件上通过 kernel 融合和编译优化可达更高 throughput,但闭源且 Nvidia-only。TGI 在功能易用性上表现突出。vLLM 以开源、通用、社区活跃著称,是大多数团队自托管 LLM 的首选起点。
五、vLLM 高级特性解析
5.1 Prefix Caching(前缀缓存)
vLLM 在 v0.4.0 之后引入 Prefix Caching,自动将 GPU 上已计算的 prompt KV Cache 保留在显存池中。当新请求的 prompt 前缀与缓存匹配时,直接复用物理块,跳过重复 prefill。
这对 RAG、Agent 等多轮对话场景价值巨大:系统 prompt 和检索文档的 KV Cache 只需计算一次,后续请求延迟从秒级降至毫秒级。
5.2 投机解码(Speculative Decoding)
vLLM 支持集成 Medusa 和 EAGLE 等投机解码方案:
- 使用小型 draft model(或模型自身的早期层)快速生成候选 token 序列。
- 主模型以 single forward 验证整个候选序列,接受合法前缀并回拒绝位点。
- 在生成延迟敏感场景中,可将 throughput 提升 1.5-2.5 倍。
# vLLM 启动时启用投机解码示例
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-2-7b",
speculative_model="[ngram]", # 或使用独立 draft model
num_speculative_tokens=5,
)
sampling_params = SamplingParams(temperature=0.8)
outputs = llm.generate("The future of AI is", sampling_params)
5.3 流水线并行与张量并行
- Tensor Parallelism(TP):将单层的注意力头和 FFN 切分到多张 GPU,适用于单节点内高通信带宽场景。推荐每张卡承载一个
tp_size分片,如 8xA100 80GB 运行 70B 模型。 - Pipeline Parallelism(PP):将模型按层切分到不同 GPU,每卡存放部分层。TP 与 PP 可组合使用,如
tp=4, pp=2在 8 卡节点上运行 170B+ 模型。
5.4 Chunked Prefill(分块 prefill)
长 prompt 的 prefill 阶段会阻塞同批次 decode 请求,造成抖动。Chunked Prefill 将长序列 prefill 拆分为多个 chunk,与 decode step 交错执行,平滑延迟分布,提升 P99 稳定性。
六、生产部署实践
6.1 安装与启动
pip install vllm
# 单卡启动 OpenAI 兼容 API 服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--tensor-parallel-size 1 \
--max-model-len 8192
6.2 多卡与多节点配置
# 单机 4 卡张量并行
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--tensor-parallel-size 4 \
--pipeline-parallel-size 1 \
--gpu-memory-utilization 0.90
# 多节点(需配置 ray)
ray start --head
# 在其他节点加入 cluster 后
python -m vllm.entrypoints.openai.api_server \
--model ... \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2
6.3 压测与调优
vLLM 自带 benchmark 工具:
python benchmarks/benchmark_throughput.py \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--input-len 1024 \
--output-len 256 \
--num-prompts 1000 \
--tensor-parallel-size 1
关键调优参数:
--gpu-memory-utilization:建议 0.85-0.95,预留空间给 CUDA kernel 工作区和显存碎片。--max-num-seqs:控制最大并发请求数,防止调度器过度乐观导致 OOM。--enable-prefix-caching:开启前缀缓存,RAG/Agent 场景必开。--enable-chunked-prefill:高并发在线服务推荐开启。
6.4 CPU Offloading 兜底
对于超长上下文或超大模型,可启用 CPU offloading 将部分 KV Cache 卸载到主机内存甚至 NVMe SSD,以吞吐换容量:
python -m vllm.entrypoints.openai.api_server \
--model ... \
--cpu-offload-gb 32
七、局限性与替代方案
7.1 vLLM 的适用边界
- 极短序列不友好:PagedAttention 的分页和调度开销在 seq_len < 64 时可能抵消收益,此时 overhead 占比高。
- 非 NVIDIA 硬件支持有限:核心 kernel 以 CUDA 为主,AMD ROCm 和 Intel XPU 支持仍在追赶。
- 复杂控制流:对于需要细粒度干预 logits、动态修改 attention mask 的场景,vLLM 的抽象层可能成为障碍。
7.2 主要替代方案
- SGLang:由 LMSYS 开发,提出 RadixAttention,将 prefix 缓存扩展到更细粒度的 Radix Tree 结构,在多轮对话和嵌套调用场景下共享效率更高。生态较新但增长迅速。
- TensorRT-LLM:NVIDIA 官方推理引擎,通过
torch.compile级别的 kernel 优化在 A100/H100 上达到极致性能。缺点是闭源生态和硬件锁定。 - llama.cpp:GGUF 格式 + CPU/混合量化,适合消费级硬件或边缘部署。在 RAM 受限场景中无可替代,但吞吐远低于 GPU 方案。
- TGI(Text-Generation-Inference):Hugging Face 出品,与 Transformers 生态深度集成,适合快速原型和小规模服务。
结语
vLLM 的成功在于将操作系统领域成熟的虚拟内存思想引入 LLM 推理,从根本上解决了 KV Cache 管理的显存效率问题。连续批处理和迭代级调度进一步优化了 GPU 利用率,使其成为自托管大模型服务的坚实基础。在实际选型中,应结合序列长度分布、硬件约束和团队技术栈,在 vLLM、TensorRT-LLM、SGLang 之间做出权衡。对于绝大多数需要平衡性能、通用性和开源可控性的团队,vLLM 仍是当前的最优起点。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。