2023 年,加州大学伯克利分校的 Woosuk Kwon 等人在 OSDI(操作系统领域顶会)上发表了一篇论文,将操作系统的虚拟内存管理思想引入 LLM 推理。
标题是:《Efficient Memory Management for Large Language Model Serving with PagedAttention》
核心创新:像操作系统管理内存一样管理 KV Cache。
这个看似简单的类比,解决了 LLM 推理服务中最大的瓶颈——GPU 显存浪费。基于 PagedAttention 构建的 vLLM 系统,吞吐量达到 HuggingFace Transformers 的 24 倍,比当时最快的商业系统也快 3.5 倍。
vLLM 重新定义了 LLM 推理服务的基础设施。
本文要点
- LLM 推理的显存瓶颈与 KV Cache 分析
- PagedAttention 核心思想:操作系统分页类比
- KV Cache 的 Block 管理与物理内存映射
- Continuous Batching vs Static Batching
- Prefix Caching 共享 System Prompt
- vLLM 系统架构与关键实现
- 性能基准:vLLM vs HuggingFace TGI vs TensorRT-LLM
- 生态扩展:SGLang、LoRA Serve、Multi-Modal
一、LLM 推理的显存瓶颈
1.1 推理阶段概述
LLM 推理分为两个阶段,每个阶段对计算和内存的需求截然不同:
1.2 KV Cache 显存占用分析
KV Cache(键值缓存)是 LLM 推理中最大的显存消费者。它存储了 attention 层中所有先前 token 的 Key 和 Value 向量,避免重复计算。
# KV Cache 显存占用计算def kv_cache_memory( num_layers: int, # 模型层数,如 80 num_heads: int, # 注意力头数,如 64 head_dim: int, # 头维度,如 128 seq_len: int, # 序列长度,如 2048 batch_size: int, # 批大小 dtype_bytes: float = 2, # FP16 每参数 2 字节) -> int: """ KV Cache 显存 = 2 (K+V) × num_layers × num_heads × head_dim × seq_len × batch_size × dtype_bytes """ per_token_per_layer = 2 * num_heads * head_dim * dtype_bytes # K + V total = num_layers * per_token_per_layer * seq_len * batch_size return total
# LLaMA-2 70B 示例# 80 层, 64 头, 128 维, FP16memory = kv_cache_memory( num_layers=80, num_heads=64, head_dim=128, seq_len=2048, batch_size=1)# 约 2.5 GB / 序列
# 当 batch_size = 32, seq_len = 2048 时# KV Cache 约 80 GB —— 超出 A100-80G 全部显存1.3 传统系统的显存浪费
现有 LLM 推理系统对 KV Cache 的管理存在严重的显存浪费:
具体数据:在 LLaMA-2 13B 模型上,传统系统仅能服务约 10 个并发请求,而 GPU 显存利用率不到 30%。
二、PagedAttention:操作系统分页类比
2.1 核心类比
PagedAttention 的灵感来自操作系统的虚拟内存管理:
2.2 Block 结构
PagedAttention 将 KV Cache 分割为固定大小的 Block:
# Block 结构示意class KVBlock: """ 一个 Block 存储固定数量 token 的 KV Cache Block 大小 = block_size × num_heads × head_dim × 2 (K+V) × dtype_size """ block_size: int = 16 # 每个 Block 存储 16 个 token 的 KV # 物理 Block 在 GPU 显存中可以不连续存储
class BlockTable: """ 类似操作系统的页表 记录逻辑 Block 到物理 Block 的映射 """ # 请求的逻辑 Block 0 → 物理 Block 42 # 请求的逻辑 Block 1 → 物理 Block 7 # 请求的逻辑 Block 2 → 物理 Block 123 # 物理位置不必连续! mappings: list[int]2.3 注意力计算中的分页
PagedAttention 需要在注意力计算过程中处理 Block 映射:
def paged_attention( query: Tensor, # [num_heads, head_dim] key_cache: Tensor, # [num_blocks, block_size, num_heads, head_dim] value_cache: Tensor, # [num_blocks, block_size, num_heads, head_dim] block_table: Tensor, # [max_num_blocks_per_seq] 逻辑→物理映射 seq_len: int, block_size: int = 16,): """ PagedAttention: 在分块的 KV Cache 上执行注意力计算
关键:通过 block_table 间接访问物理 KV Block, 实现"逻辑连续但物理不连续"的内存布局 """ num_blocks = (seq_len + block_size - 1) // block_size output = torch.zeros(num_heads, head_dim)
# 累积 softmax 的在线计算(类似 Flash Attention) max_score = float('-inf') sum_exp = 0.0
for block_idx in range(num_blocks): # 通过 block_table 获取物理 Block 编号 physical_block = block_table[block_idx]
# 从物理 Block 中读取 K, V k_block = key_cache[physical_block] # [block_size, num_heads, head_dim] v_block = value_cache[physical_block] # [block_size, num_heads, head_dim]
# 计算注意力分数 scores = torch.matmul(query, k_block.transpose(-1, -2)) / math.sqrt(head_dim)
# 在线 softmax 更新(处理最后一个 Block 的部分填充) valid_tokens = min(block_size, seq_len - block_idx * block_size) scores = scores[:valid_tokens]
block_max = scores.max() new_max = max(max_score, block_max)
# 重缩放之前的累积结果 sum_exp = sum_exp * math.exp(max_score - new_max) new_sum = torch.exp(scores - new_max).sum() output = output * sum_exp * math.exp(max_score - new_max) output += torch.matmul( torch.exp(scores - new_max).unsqueeze(-2), v_block[:valid_tokens] ).squeeze(-2)
sum_exp += new_sum max_score = new_max
output /= sum_exp return output2.4 Block 管理器
vLLM 设计了一个类似操作系统内存管理器的 Block Manager:
三、Continuous Batching(连续批处理)
3.1 Static Batching 的问题
传统批处理方式是”静态”的——一个 batch 中的所有请求必须同时开始、同时结束:
3.2 Continuous Batching 的实现
class ContinuousBatcher: """ 连续批处理器:在每次迭代中动态管理请求队列 """ def __init__(self, model, block_manager, max_batch_size): self.model = model self.block_manager = block_manager self.max_batch_size = max_batch_size self.running_requests = [] # 当前正在处理的请求 self.waiting_queue = [] # 等待队列
def step(self): """执行一步调度""" # 1. 移除已完成的请求,释放 KV Cache Block for req in self.running_requests: if req.is_finished(): self.block_manager.free(req) self.running_requests.remove(req)
# 2. 从等待队列中填入新请求 while (len(self.running_requests) < self.max_batch_size and self.waiting_queue): new_req = self.waiting_queue.pop(0) # 为新请求分配 KV Cache Block if self.block_manager.can_allocate(new_req): self.block_manager.allocate(new_req) self.running_requests.append(new_req)
# 3. 对当前所有 running 请求执行一次前向传播 # 注意:不同请求处于不同阶段(prefill/decode) output = self.model.forward(self.running_requests)
# 4. 每个请求独立处理自己的输出 for req, out in zip(self.running_requests, output): req.append_token(out.next_token)3.3 Iteration-level Scheduling
vLLM 的关键设计是 iteration-level scheduling——在每次解码迭代结束时重新调度:
核心优势:与传统系统(batch 级调度)不同,vLLM 在每次 token 生成的 iteration 粒度上调度,确保 GPU 始终满载。
四、Prefix Caching(前缀缓存)
4.1 System Prompt 共享
在 LLM 应用中,大量请求共享相同的 System Prompt。例如:
- 聊天机器人:所有请求共享相同的系统提示
- RAG 应用:多个请求使用相同的检索上下文
- Agent 场景:工具描述和 Few-shot 示例完全相同
4.2 Copy-on-Write 机制
Prefix Caching 使用类似操作系统的 Copy-on-Write(写时复制) 机制:
class PrefixCacheManager: """ 管理共享前缀的 KV Cache Block 使用 Copy-on-Write 避免数据竞争 """ def __init__(self, block_size=16): self.block_size = block_size self.prefix_blocks = {} # prefix_hash → [physical_block_ids] self.ref_counts = {} # physical_block_id → 引用计数
def get_or_create_prefix(self, prefix_tokens): """获取或创建共享前缀 Block""" prefix_hash = hash(tuple(prefix_tokens))
if prefix_hash in self.prefix_blocks: # 前缀已存在,增加引用计数 blocks = self.prefix_blocks[prefix_hash] for block_id in blocks: self.ref_counts[block_id] += 1 return blocks else: # 首次创建,计算 KV Cache 并存储 blocks = self._compute_and_store(prefix_tokens) self.prefix_blocks[prefix_hash] = blocks for block_id in blocks: self.ref_counts[block_id] = 1 return blocks
def release_prefix(self, prefix_hash): """释放前缀引用,引用计数为 0 时回收""" blocks = self.prefix_blocks[prefix_hash] for block_id in blocks: self.ref_counts[block_id] -= 1 if self.ref_counts[block_id] == 0: self._free_block(block_id)4.3 效果量化
| 场景 | System Prompt 长度 | 并发请求数 | 无缓存 KV 显存 | 有缓存 KV 显存 | 节省比例 ||--------------------|--------------------|------------|----------------|----------------|----------|| ChatGPT 风格 | 500 tokens | 20 | 10.0 GB | 2.0 GB | 80% || RAG 应用 | 2000 tokens | 10 | 20.0 GB | 6.0 GB | 70% || Agent 多工具 | 3000 tokens | 8 | 24.0 GB | 5.0 GB | 79% || 代码助手 | 1000 tokens | 30 | 30.0 GB | 4.0 GB | 87% |五、vLLM 系统架构
5.1 整体架构
5.2 调度策略
当 GPU 显存不足以服务所有请求时,vLLM 采用两种 Preemption 策略:
5.3 CUDA Kernel 优化
PagedAttention 的核心是高效的 CUDA 实现:
# vLLM 的 PagedAttention CUDA Kernel 设计要点"""1. 合并内存访问(Coalesced Memory Access) - Block 内的 KV 数据在 GPU 显存中连续存储 - 线程束(Warp)可以高效地合并读取
2. 减少 Block Table 查找 - Block Table 常驻 GPU 显存 - 查找开销远小于连续内存带来的收益
3. 支持 Tensor Parallelism - KV Cache 按 Head 维度切分到多 GPU - 每个 GPU 只需存储部分 Head 的 KV Cache
4. 变长序列支持 - 同一 Batch 中不同序列长度不同 - 通过 seq_len 数组标识每个序列的有效长度"""六、性能基准
6.1 vLLM vs 其他系统
论文中的主要性能对比:
| 系统 | 模型 | GPU | 吞吐量 (req/s) | vs HF 加速比 ||------------------------|-------------|----------|----------------|--------------|| HuggingFace Transformers| LLaMA-13B | A100 | 2.5 | 1.0x || TGI (HuggingFace) | LLaMA-13B | A100 | 17.0 | 6.8x || vLLM | LLaMA-13B | A100 | 60.0 | 24x || vLLM | LLaMA-70B | 4xA100 | 8.5 | — || TensorRT-LLM | LLaMA-70B | 4xA100 | 7.2 | — |6.2 显存利用率
| 系统 | 有效 KV Cache 比例 | GPU 显存利用率 | 最大并发数 ||---------------|-------------------|---------------|-----------|| HuggingFace | ~20% | ~25% | 10 || TGI | ~55% | ~60% | 28 || vLLM | >95% | >90% | 54 |6.3 PagedAttention 开销
6.4 不同场景性能
| 场景 | 输入长度 | 输出长度 | vLLM 吞吐优势 ||------------------------|---------|---------|---------------|| 短对话 (Chatbot) | 128 | 64 | 24x vs HF || 长文档摘要 | 2048 | 256 | 20x vs HF || 代码生成 | 512 | 512 | 22x vs HF || RAG 检索增强 | 1024 | 128 | 18x vs HF || 共享 System Prompt | 500 | 200 | 30x vs HF |七、生态与扩展
7.1 SGLang
SGLang(Structured Generation Language)是 vLLM 团队后续开发的推理引擎,进一步优化了:
- RadixAttention:基于 Radix Tree 的自动前缀缓存,比手动 Prefix Caching 更灵活
- 编程模型:提供结构化的 LLM 编程接口
- 编译优化:将 LLM 调用编译为高效执行计划
7.2 LoRA Serve
vLLM 支持 LoRA 动态加载,允许在同一个基础模型上同时服务多个 LoRA 适配器:
# vLLM LoRA Serve 示例from vllm import LLM, SamplingParams
llm = LLM( model="meta-llama/Llama-2-7b-hf", enable_lora=True, max_loras=4, # 最多同时加载 4 个 LoRA max_lora_rank=16,)
# 不同的请求可以使用不同的 LoRAoutputs = llm.generate( [ { "prompt": "翻译为英文", "lora_request": LoRARequest("translator", 1, "/path/to/translator"), }, { "prompt": "写代码", "lora_request": LoRARequest("coder", 2, "/path/to/coder"), }, { "prompt": "通用对话", "lora_request": None, # 不使用 LoRA }, ])7.3 Multi-Modal 扩展
vLLM 已扩展支持多模态模型:
- LLaVA:视觉语言模型,支持图片输入
- CLIP + LLM:图文联合推理
- 音频输入:Whisper + LLM 联合部署
7.4 与其他系统对比
| 特性 | vLLM | TensorRT-LLM | HuggingFace TGI | SGLang ||------------------|---------------|--------------|-----------------|---------------|| PagedAttention | 是 原生 | 否 自有方案 | 否 | 是 RadixAttn || Continuous Batch | 是 | 是 | 是 | 是 || Prefix Caching | 是 | 是 | 否 | 是 自动 || Speculative Dec | 是 | 是 | 否 | 是 || LoRA 动态加载 | 是 | 否 | 是 | 是 || 多模态 | 是 | 是 | 是 | 是 || 开源协议 | Apache 2.0 | Apache 2.0 | Apache 2.0 | Apache 2.0 || 易用性 | 高 Python | 中 C++ | 高 Docker | 高 Python || 性能 | 高 | 极高 | 中 | 高 |八、使用指南
8.1 快速启动
# 安装# pip install vllm
from vllm import LLM, SamplingParams
# 基础用法llm = LLM(model="meta-llama/Llama-2-7b-hf")sampling_params = SamplingParams( temperature=0.7, top_p=0.9, max_tokens=256,)outputs = llm.generate(["你好,请介绍一下自己"], sampling_params)for output in outputs: print(output.outputs[0].text)8.2 OpenAI 兼容 API 服务
# 启动 OpenAI 兼容的 API 服务器python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Llama-2-7b-hf \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.9 \ --max-model-len 4096 \ --enable-prefix-caching
# 客户端调用(与 OpenAI API 完全兼容)import openaiclient = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")response = client.chat.completions.create( model="meta-llama/Llama-2-7b-hf", messages=[{"role": "user", "content": "你好"}],)8.3 关键参数调优
| 参数 | 说明 | 推荐值 ||--------------------------|------------------------------|----------------|| gpu-memory-utilization | GPU 显存利用率上限 | 0.85 - 0.95 || max-model-len | 最大序列长度 | 按需设置 || tensor-parallel-size | 张量并行度 | GPU 数量 || block-size | KV Cache Block 大小 | 16(默认) || swap-space | CPU Swap 空间大小(GB) | 4 - 8 || enable-prefix-caching | 是否启用前缀缓存 | True || max-num-seqs | 最大并发序列数 | 64 - 256 |常见问题 FAQ
Q1:PagedAttention 会影响模型精度吗?
A:不会。PagedAttention 只改变了 KV Cache 的内存管理方式,注意力计算本身是数学等价的。分块和间接寻址不引入任何近似或量化。
Q2:block_size 应该怎么选择?
A:默认 16 是经过实验验证的最佳值。过小的 block_size 增加管理开销(更多 Block Table 条目),过大的 block_size 增加内部碎片。16 在延迟和利用率之间取得了很好的平衡。
Q3:vLLM 适合训练场景吗?
A:不适合。vLLM 专门为推理服务优化(Serving),不涉及训练。训练场景应使用 Megatron-LM、DeepSpeed 等框架。
Q4:Swapping 和 Recomputation 应该选哪个?
A:取决于硬件配置。如果 CPU-GPU 之间的 PCIe/CXL 带宽高(如 NVLink 连接的统一内存),Swapping 更好。如果带宽有限但 GPU 计算能力强,Recomputation 更好。vLLM 默认自动选择。
Q5:vLLM 和 TensorRT-LLM 应该选哪个?
A:追求极致性能选 TensorRT-LLM(NVIDIA 专有优化,更快但部署复杂),追求易用性和灵活生态选 vLLM(Python 原生,社区活跃,功能丰富)。许多团队用 vLLM 开发验证后,再迁移到 TensorRT-LLM 上线。
Q6:Prefix Caching 和 RadixAttention 有什么区别?
A:Prefix Caching 需要用户显式标记共享前缀;RadixAttention(SGLang 提出)通过 Radix Tree 自动识别和缓存所有重复前缀,无需手动干预。
Q7:vLLM 支持量化模型吗?
A:支持。vLLM 支持 AWQ、GPTQ 量化模型和 FP8/INT8 推理,可以与 PagedAttention 无缝配合使用。
小结
PagedAttention 将操作系统的经典思想——虚拟内存分页管理——应用到 LLM 推理的 KV Cache 管理,从根本上解决了显存浪费问题。
核心贡献:
vLLM 让 LLM 推理服务化从梦想变为现实。
PagedAttention 的意义远超一篇论文。它建立了 LLM 推理系统的基本范式——用系统思维解决 AI 基础设施问题。这一范式深刻影响了后续的 SGLang、LoRA Serve 等工作,并推动了整个 LLM 推理生态的成熟。
参考资料
- Efficient Memory Management for Large Language Model Serving with PagedAttention - Kwon et al., OSDI 2023
- vLLM GitHub Repository - 伯克利开源推理引擎
- SGLang: Structured Generation Language - vLLM 团队后续工作
- FlashAttention: Fast and Memory-Efficient Exact Attention - PagedAttention 的注意力内核基础
- Orca: A Distributed Serving System for Transformer-Based Generative Models - Continuous Batching 原始论文
- vLLM Official Documentation - 官方使用文档与最佳实践
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






