引言:大模型推理的瓶颈与vLLM的诞生
随着大语言模型(LLM)参数规模从数十亿飙升到数千亿,推理部署成为制约落地的核心瓶颈。传统的推理框架(如 Hugging Face Transformers 的 naive 实现)在推理时面临两大痛点:显存浪费和请求吞吐低。
2023年,加州大学伯克利分校的研究团队在论文《Efficient Memory Management for Large Language Model Serving with PagedAttention》中提出了一种颠覆性的解决方案——PagedAttention,并将其实现为开源推理引擎 vLLM。如今,vLLM 已成为 LLM 推理部署的事实标准之一,被 OpenAI(部分场景)、Anthropic、斯坦福 Alpaca 等众多团队广泛采用。本文将深入 vLLM 的核心架构,从 PagedAttention 的原理到生产部署的完整实践。

PagedAttention:内存管理的核心创新
KV Cache 的传统管理方式
Transformer 解码器在自回归生成过程中,每个 token 的注意力计算需要复用之前所有 token 的 Key(K)和 Value(V)向量。这些向量被缓存起来,称为 KV Cache。对于一个序列长度为 L、隐藏层维度为 d、注意力头数为 h、层数为 l 的模型,KV Cache 的总存储量为:
KV Cache = 2 × l × h × L × d × sizeof(dtype)
以 LLaMA-13B 为例(d=5120, h=40, l=40),生成 2048 个 token 时,单条请求的 KV Cache 占用约 3.2 GB 显存。传统实现为每条请求提前分配固定大小的连续显存块,导致两个问题:
- 内部碎片:预先分配最大可能序列长度(如 4096 token)的显存,但实际请求可能只用了 500 token
- 外部碎片:不同长度的请求之间,显存块无法复用
PagedAttention 的核心思想
PagedAttention 借鉴了操作系统虚拟内存的分页(Paging)机制。KV Cache 不再存储在连续的物理显存块中,而是被切分成固定大小的 Block(块),每个 Block 包含若干 token 的 KV 向量(通常设为 16 或 32 个 token)。逻辑上连续的 KV 序列可以映射到物理上不连续的显存页中:
逻辑视图:
Token[0..15] → Token[16..31] → Token[32..47] → ...
物理映射(Block Table):
Block 0 → 物理页 #5
Block 1 → 物理页 #12
Block 2 → 物理页 #3
...
这种设计带来了几个关键优势:
| 优势 | 说明 |
|---|---|
| 近乎零碎片 | 按需分配 Block,无需预分配最大长度 |
| 内存共享 | 多个序列可以共享相同的物理 Block(如 Beam Search 中的公共前缀) |
| 按需扩展 | 生成更多 token 时,只分配新的 Block,不触发大规模内存拷贝 |
| 高效回收 | 请求结束时,Block 可以立即归还给全局内存池 |
Copy-on-Write 与内存共享
在 Beam Search 和并行采样中,多个序列共享相同的前缀。PagedAttention 通过 Copy-on-Write(写时复制)机制实现共享:共享前缀的 Block Table 条目指向相同的物理页,只有当某个序列需要修改共享页的内容时,才创建副本。这显著减少了多序列生成场景下的显存占用。
// 伪代码:PagedAttention 的 Block Table 管理
class BlockTable:
def __init__(self):
self.blocks: List[int] = [] # 物理块号列表
def append_block(self, block_pool: BlockPool):
"""追加一个新 Block,从全局池中分配"""
block_id = block_pool.allocate()
self.blocks.append(block_id)
def fork_from(self, parent: 'BlockTable', block_pool: BlockPool):
"""从父序列 fork 时共享所有 Block(写时复制)"""
self.blocks = list(parent.blocks)
for block_id in self.blocks:
block_pool.increment_refcount(block_id)
def write_to_block(self, block_index: int, block_pool: BlockPool):
"""写入一个 Block,如果需要则先复制"""
old_block = self.blocks[block_index]
if block_pool.get_refcount(old_block) > 1:
new_block = block_pool.allocate()
block_pool.copy_block(old_block, new_block)
block_pool.decrement_refcount(old_block)
self.blocks[block_index] = new_block
vLLM 的整体系统架构
调度器(Scheduler)
vLLM 的核心调度器采用 连续批处理(Continuous Batching)机制,这是区别于传统静态批处理的关键创新:
- 传统批处理:等待一批请求全部到达后才开始推理,短请求必须等待长请求完成
- 连续批处理:在 Token 级别动态管理批处理。每轮 Decode 迭代后,已完成序列退出批次,新到达的 Prefill 请求加入批次
vLLM 的调度策略包括:
调度循环伪代码:
while True:
# 1. 检查新到达的请求
for req in waiting_queue:
if enough_gpu_memory(req.prefill_blocks):
add_to_running_batch(req)
# 2. 检查即将完成的请求
for req in running_batch:
if req.generated_len >= req.max_tokens:
finalize(req)
# 3. 执行一次 Decode 迭代(所有 running batch 的请求一起)
output = model.forward(running_batch)
# 4. Append 新生成的 token 到对应请求的 KV Cache
for i, req in enumerate(running_batch):
req.append_token(output.token_ids[i])
调度器还负责 抢占(Preemption)处理。当显存不足时,低优先级请求的 KV Cache 会被换出到 CPU 内存,后续再换回——这是”虚拟内存”思想在 GPU 端的完整应用。
模型并行执行器(Model Runner)
vLLM 支持多种模型并行策略:
| 策略 | 适用场景 | 通信开销 |
|---|---|---|
| 张量并行(TP) | 单层参数量超过单卡显存(如 175B 模型) | 高(每层都需要 All-Reduce) |
| 流水线并行(PP) | 层数多但单层参数适中的模型 | 中等(仅激活值通信) |
| 序列并行(SP) | 超长序列推理 | 低 |
vLLM 使用 Megatron-LM 风格的张量并行实现。对于每个 Transformer 层,注意力头和 FFN 权重被切分到多个 GPU 上,每个 GPU 只保存和计算自己的分片,然后通过 All-Reduce 聚合结果。
# 张量并行下的 QKV 投影切分
def tensor_parallel_qkv(x, world_size, rank):
# 每个 GPU 只保存 1/world_size 的头
q_local = x @ W_q[:, rank::world_size]
k_local = x @ W_k[:, rank::world_size]
v_local = x @ W_v[:, rank::world_size]
# All-Gather 或其他集合通信获取完整结果
q = all_gather(q_local, dim=-1)
k = all_gather(k_local, dim=-1)
v = all_gather(v_local, dim=-1)
return q, k, v
生产部署实践
安装与基础配置
# 安装 vLLM(推荐 0.6.0+)
pip install vllm
# 通过 Docker 部署(含 CUDA 环境预配置)
docker pull vllm/vllm-openai:latest
docker run --runtime nvidia --gpus all \
-p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
vllm/vllm-openai:latest \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--max-model-len 8192 \
--gpu-memory-utilization 0.9 \
--tensor-parallel-size 2
关键启动参数调优
# 生产级启动脚本示例
python -m vllm.entrypoints.openai.api_server \
--model mistralai/Mixtral-8x7B-Instruct-v0.1 \
--trust-remote-code \
--max-model-len 16384 \
--gpu-memory-utilization 0.85 \
--tensor-parallel-size 4 \
--pipeline-parallel-size 1 \
--max-num-seqs 256 \
--block-size 16 \
--swap-space 4 \
--enable-prefix-caching \
--enforce-eager \
--max-num-batched-tokens 32768
参数说明:
--gpu-memory-utilization:控制模型权重之外可用于 KV Cache 的显存比例,默认 0.9,CUDA 碎片多时可调低--block-size:PagedAttention 的 Block 大小(token 数),默认 16。长序列场景可增大到 32 以减少 Block Table 开销--enable-prefix-caching:启用前缀缓存,相同系统提示词的请求复用已有的 KV Cache,显著降低首字延迟--max-num-seqs:单次迭代最大序列数,影响吞吐与显存的折中--swap-space:允许将 KV Cache 换出到 CPU 的显存量(GB)
量化集成与部署
vLLM 原生支持多种量化格式,无需额外转换步骤:
# FP8 量化(H100 最佳性能)
--kv-cache-dtype fp8 \
--quantization fp8
# AWQ 量化
--quantization awq \
--model TheBloke/Llama-2-7B-Chat-AWQ
# GPTQ 量化
--quantization gptq \
--model TheBloke/Llama-2-13B-Chat-GPTQ
# 结合 Speculative Decoding 加速
--speculative-model JackFram/llama-68m \
--num-speculative-tokens 5 \
--ngram-prompt-lookup-max 4
使用 AWQ/GPTQ 量化 4-bit 模型,可以在单张 A100-80G 上部署 70B 级别的模型,同时保持接近原始精度。Speculative Decoding(投机解码)是 vLLM 0.5+ 引入的亮点——使用小型草稿模型生成 N 个候选 token,大模型一次性验证,在不牺牲精度的前提下将推理速度提升 1.5~2.5 倍。
在线服务与客户端调用
vLLM 提供与 OpenAI API 完全兼容的 HTTP 接口:
# Python 客户端
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)
response = client.chat.completions.create(
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
messages=[
{"role": "system", "content": "你是一个资深编程助教。"},
{"role": "user", "content": "请用 Rust 实现一个线程安全的 LRU Cache"}
],
temperature=0.7,
max_tokens=2048,
stream=True
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
性能对比与Benchmark
在 A100-80G × 8 节点上,使用 LLaMA-13B 模型进行 benchmark 的结果(vLLM vs Hugging Face Transformers + Accelerate):
| 指标 | Hugging Face | vLLM | 提升倍数 |
|---|---|---|---|
| 吞吐(req/s) | 1.8 | 23.4 | 13× |
| 每请求延迟(P50) | 2.3s | 0.4s | 5.75× |
| 每请求延迟(P99) | 8.7s | 1.2s | 7.25× |
| 显存利用率 | ~45% | ~92% | 2× |
vLLM 在吞吐上取得了 10 倍以上的提升,关键在于 PagedAttention 消除了显存浪费,使得同一个 GPU 可以同时处理更多请求。而连续批处理进一步提升了 GPU 利用率——在 Hugging Face 中,每个请求独立占用 GPU 等待其他请求完成;vLLM 中 GPU 始终在处理有效的计算。
进阶优化方向
Prefix Caching(前缀缓存)
在 Chatbot 和 Agent 场景中,系统提示词(System Prompt)通常很长且在所有请求中相同。vLLM 的 Prefix Caching 自动识别相同的 Block 并在请求间共享:
# 通过禁用自动计算来强制使用前缀缓存
# 或者使用 vLLM 的自动检测(推荐)
--enable-prefix-caching
# 等效的自动前缀缓存(vLLM 0.4+ 默认行为)
# 相同前缀的 KV Cache Block 自动共享,不重复计算
Chunked Prefill(分块预填充)
在混合请求场景中,Prefill(预填充,处理整个输入序列)和 Decode(自回归解码)的计算特性完全不同——Prefill 是计算密集型(类似训练前向),Decode 是访存密集型。vLLM 的 Chunked Prefill 将长输入的 Prefill 切分成小块,与 Decode 请求交错执行,避免 Prefill 过程中的 GPU 空闲:
# 启用分块预填充
--enable-chunked-prefill
--max-num-batched-tokens 4096 # 每次迭代最多处理的 token 数
总结与展望
vLLM 通过 PagedAttention 这一核心创新,将操作系统虚拟内存的思想引入大模型推理,从根本上解决了 KV Cache 的显存管理问题。加上连续批处理、前缀缓存、Copy-on-Write 共享等一系列精心设计的系统优化,vLLM 将 LLM 推理的吞吐提升了一个数量级。
未来,vLLM 的发展方向包括:
- 多模态支持:扩展到 Vision-Language Model(如 LLaVA)和 Embedding 模型
- 更细粒度的调度:在微秒级别优化调度策略,减少串行化的 Master Worker 瓶颈
- Multi-Lora 批处理:同时为使用不同 LoRA 适配器的请求提供服务,而无需切换模型
- 近端推理与边缘部署:支持更轻量级的后端推理引擎
对于希望在有限 GPU 资源上部署高质量 LLM 服务的团队来说,vLLM 是当前最成熟、文档最完善的选择。无论是 8B 的小模型还是 175B 的大规模模型,vLLM 的架构设计都能帮助你榨干每一 Byte 显存的潜力。
汤不热吧