mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1798 字
5 分钟
PagedAttention 与 vLLM:LLM 推理服务化
2025-04-01

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 推理分为两个阶段,每个阶段对计算和内存的需求截然不同:

flowchart TB subgraph LLM推理两阶段 A["Prefill 阶段(预填充)"] A --> A1["输入:完整 prompt tokens"] A --> A2["计算:并行处理所有 input tokens"] A --> A3["产出:KV Cache + 第一个 output token"] A --> A4["特点:计算密集型(compute-bound)"] B["Decode 阶段(解码)"] B --> B1["输入:前一步的 output token"] B --> B2["计算:自回归逐 token 生成"] B --> B3["产出:下一个 output token"] B --> B4["特点:内存带宽密集型(memory-bound)"] end C["核心瓶颈"] --> D["Decode 阶段每次前向传播只产生 1 个 token"] D --> E["但需要读取全部模型权重 + 完整 KV Cache"] E --> F["GPU 计算利用率极低(< 10%)"] F --> G["关键优化方向:增大 batch size,提升 GPU 利用率"] G --> H["但增大 batch size 受限于 KV Cache 显存占用"]

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 维, FP16
memory = 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 全部显存
flowchart TB subgraph 不同模型的KV Cache显存占用 direction TB A["LLaMA-2 7B (32 层)"] A --> A1["单序列 2048 tokens: ~0.5 GB"] A --> A2["Batch=64: ~32 GB"] B["LLaMA-2 13B (40 层)"] B --> B1["单序列 2048 tokens: ~1.0 GB"] B --> B2["Batch=32: ~32 GB"] C["LLaMA-2 70B (80 层)"] C --> C1["单序列 2048 tokens: ~2.5 GB"] C --> C2["Batch=32: ~80 GB"] D["关键洞察"] D --> D1["KV Cache 占用远超模型权重"] D --> D2["序列长度线性影响显存"] D --> D3["batch size 线性影响显存"] end

1.3 传统系统的显存浪费#

现有 LLM 推理系统对 KV Cache 的管理存在严重的显存浪费:

flowchart TB subgraph 传统KV Cache管理问题 A["问题一:预分配浪费"] A --> A1["为每个请求预分配最大序列长度的连续内存"] A --> A2["实际生成长度通常远小于最大值"] A --> A3["浪费 60%-80% 的预分配显存"] B["问题二:内存碎片化"] B --> B1["请求长短不一,产生外部碎片"] B --> B2["无法高效复用已释放的内存"] B --> B3["导致 GPU 显存利用率低"] C["问题三:无法共享"] C --> C1["相同 System Prompt 的多个请求"] C --> C2["每个请求独立存储 KV Cache"] C --> C3["重复存储相同内容,显存浪费"] end D["影响"] --> E["实际有效 batch size 远小于理论值"] E --> F["GPU 利用率低,吞吐量受限"]

具体数据:在 LLaMA-2 13B 模型上,传统系统仅能服务约 10 个并发请求,而 GPU 显存利用率不到 30%。


二、PagedAttention:操作系统分页类比#

2.1 核心类比#

PagedAttention 的灵感来自操作系统的虚拟内存管理:

flowchart LR subgraph 操作系统虚拟内存 A["进程的虚拟地址空间"] --> B["分页为固定大小的页(Page)"] B --> C["页表映射到物理帧(Frame)"] C --> D["物理帧可以不连续"] D --> E["按需分配,无需预分配全部"] end subgraph PagedAttention F["请求的逻辑 KV Cache"] --> G["分块为固定大小的 Block"] G --> H["Block Table 映射到物理 Block"] H --> I["物理 Block 可以不连续"] I --> J["按需分配,生成多少分配多少"] end A -.->|类比| F B -.->|类比| G C -.->|类比| H D -.->|类比| I E -.->|类比| J style PagedAttention fill:#e8f5e9

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]
flowchart TB subgraph 传统连续存储 A1["请求 1 KV Cache"] A1 --> A2["连续内存块(预分配 max_seq_len)"] A2 --> A3["实际使用 ████████░░░░░░░░░░░░"] A3 --> A4["浪费空间 ░░░░░░░░░░░░░░░░░░"] B1["请求 2 KV Cache"] B1 --> B2["连续内存块(预分配 max_seq_len)"] B2 --> B3["实际使用 ██████████░░░░░░░░░░"] end subgraph PagedAttention分块存储 C1["请求 1 的 Block Table"] C1 --> C2["逻辑 Block 0 → 物理 Block #5"] C1 --> C3["逻辑 Block 1 → 物理 Block #12"] C1 --> C4["逻辑 Block 2 → 物理 Block #3"] D1["请求 2 的 Block Table"] D1 --> D2["逻辑 Block 0 → 物理 Block #8"] D1 --> D3["逻辑 Block 1 → 物理 Block #21"] end E["物理 GPU 显存"] E --> E1["Block #3: Req1-L2 ████████"] E --> E2["Block #5: Req1-L0 ████████"] E --> E3["Block #8: Req2-L0 ████████"] E --> E4["Block #12: Req1-L1 ████████"] E --> E5["Block #21: Req2-L1 ████████"] style PagedAttention分块存储 fill:#e8f5e9

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 output

2.4 Block 管理器#

vLLM 设计了一个类似操作系统内存管理器的 Block Manager:

flowchart TB subgraph BlockManager A["Free Block Pool(空闲块池)"] A --> A1["GPU 显存中所有可用的物理 Block"] A --> A2["按需分配给新请求"] B["Block Table(块表)"] B --> B1["每个请求维护一张块表"] B --> B2["记录逻辑 Block → 物理 Block 映射"] C["分配策略"] C --> C1["新请求:分配 1 个 Block(预填充后)"] C --> C2["生成过程:当前 Block 填满后分配新 Block"] C --> C3["请求结束:释放所有 Block 回空闲池"] D["交换策略"] D --> D1["GPU 显存不足时,将 Block 换出到 CPU"] D --> D2["需要时换入回 GPU"] D --> D3["类似操作系统的 Swap 机制"] end

三、Continuous Batching(连续批处理)#

3.1 Static Batching 的问题#

传统批处理方式是”静态”的——一个 batch 中的所有请求必须同时开始、同时结束:

flowchart TB subgraph StaticBatching direction TB A["时间步 1-10"] A --> A1["请求 A: ████████████ 生成 10 tokens"] A --> A2["请求 B: ██████░░░░░░ 生成 6 tokens(但等 A 结束)"] A --> A3["请求 C: ████░░░░░░░░ 生成 4 tokens(但等 A 结束)"] B["问题"] B --> B1["短请求必须等长请求完成"] B --> B2["GPU 在等待期间空转"] B --> B3["吞吐量被最慢请求拖累"] end subgraph ContinuousBatching direction TB C["时间步 1-10"] C --> C1["请求 A: ████████████"] C --> C2["请求 B: ██████(6步完成,退出)"] C --> C3["请求 D: ░░░░░░██████(B 完成后加入)"] C --> C4["请求 C: ████(4步完成,退出)"] C --> C5["请求 E: ░░░░████████(C 完成后加入)"] D["优势"] D --> D1["请求完成即退出,不浪费计算"] D --> D2["新请求随时加入,保持 GPU 利用率"] D --> D3["吞吐量大幅提升"] end style ContinuousBatching fill:#e8f5e9

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——在每次解码迭代结束时重新调度:

sequenceDiagram participant Q as 等待队列 participant S as Scheduler participant BM as Block Manager participant GPU as GPU Engine Note over S: 迭代开始 S->>GPU: 收集已完成请求 GPU-->>S: Request B 完成 (EOS) S->>BM: 释放 Request B 的 Blocks BM-->>S: Blocks 已回收 S->>BM: 检查能否接纳新请求 BM-->>S: 有空闲 Blocks,可以 S->>Q: 取出 Request D Q-->>S: Request D (prefill) S->>BM: 为 Request D 分配 Blocks BM-->>S: 分配完成 S->>GPU: 执行前向传播 Note over GPU: [Req A: decode] [Req C: decode] [Req D: prefill] GPU-->>S: 每个请求的下一个 token Note over S: 迭代结束,循环

核心优势:与传统系统(batch 级调度)不同,vLLM 在每次 token 生成的 iteration 粒度上调度,确保 GPU 始终满载。


四、Prefix Caching(前缀缓存)#

4.1 System Prompt 共享#

在 LLM 应用中,大量请求共享相同的 System Prompt。例如:

  • 聊天机器人:所有请求共享相同的系统提示
  • RAG 应用:多个请求使用相同的检索上下文
  • Agent 场景:工具描述和 Few-shot 示例完全相同
flowchart TB subgraph 无PrefixCaching A1["请求 1: System Prompt + User Query 1"] A1 --> A2["KV Cache 1: 包含 System Prompt 的 KV"] B1["请求 2: System Prompt + User Query 2"] B1 --> B2["KV Cache 2: 包含 System Prompt 的 KV"] C1["请求 3: System Prompt + User Query 3"] C1 --> C2["KV Cache 3: 包含 System Prompt 的 KV"] D["问题:System Prompt 的 KV Cache 重复存储 3 份"] end subgraph 有PrefixCaching E["共享 System Prompt KV Cache"] E --> F["物理 Block 7: System Prompt Tokens 1-16"] E --> G["物理 Block 15: System Prompt Tokens 17-32"] H["请求 1 Block Table"] H --> H1["Block 0 → 7(共享)"] H --> H2["Block 1 → 15(共享)"] H --> H3["Block 2 → 42(独占 User Query)"] I["请求 2 Block Table"] I --> I1["Block 0 → 7(共享)"] I --> I2["Block 1 → 15(共享)"] I --> I3["Block 2 → 88(独占 User Query)"] J["优势:System Prompt 的 KV Cache 只存 1 份"] end style 有PrefixCaching fill:#e8f5e9

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 整体架构#

flowchart TB subgraph vLLM系统架构 A["API Frontend"] A --> A1["OpenAI-compatible API"] A --> A2["支持 Streaming / 非流式"] B["Scheduler(调度器)"] B --> B1["Iteration-level 调度"] B --> B2["Preemption 策略"] B --> B3["优先级队列"] C["Block Manager"] C --> C1["Block 分配 / 释放"] C --> C2["Prefix Caching"] C --> C3["Swap In/Out"] D["GPU Worker"] D --> D1["PagedAttention Kernel"] D --> D2["CUDA 核心实现"] D --> D3["Tensor Parallelism"] E["KV Cache Pool"] E --> E1["GPU 显存 Block 池"] E --> E2["CPU Swap Buffer"] end A --> B B --> C B --> D C --> E D --> E

5.2 调度策略#

当 GPU 显存不足以服务所有请求时,vLLM 采用两种 Preemption 策略:

flowchart TB subgraph Preemption策略 A["GPU 显存不足"] B["策略一:Swapping(换出)"] B --> B1["将低优先级请求的 KV Cache 换出到 CPU"] B --> B2["需要时换入回 GPU"] B --> B3["适合:KV Cache 较大、预期能换入"] C["策略二:Recomputation(重计算)"] C --> C1["丢弃低优先级请求的 KV Cache"] C --> C2["需要时重新计算 prefill"] C --> C3["适合:KV Cache 较小、重计算快"] D["决策依据"] D --> D1["KV Cache 大小 vs CPU-GPU 传输带宽"] D --> D2["请求优先级(先到先服务 / FCFS)"] D --> D3["GPU 显存压力程度"] end

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 其他系统#

论文中的主要性能对比:

xychart-beta title "LLM 服务系统吞吐量对比(LLaMA-13B, A100 GPU)" x-axis ["HuggingFace", "TGI", "vLLM"] y-axis "吞吐量 (requests/s)" 0 --> 60 bar [2.5, 17, 60]
| 系统 | 模型 | 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 开销#

flowchart TB subgraph PagedAttention开销分析 A["Block Table 查找"] A --> A1["额外开销:< 1% 计算时间"] A --> A2["Block Table 驻留 GPU 显存,访问极快"] B["非连续内存访问"] B --> B1["理论影响:降低缓存命中率"] B --> B2["实际影响:< 5% 性能损失"] B --> B3["原因:Block 内仍然连续"] C["内存分配/释放"] C --> C1["Block 级别管理,操作 O(1)"] C --> C2["无内存碎片化问题"] D["总体"] D --> D1["额外开销 < 5%"] D --> D2["换来 20-50% 更高的 GPU 利用率"] D --> D3["净收益巨大"] end

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 调用编译为高效执行计划
flowchart TB subgraph SGLang架构 A["用户程序"] --> B["SGLang 编译器"] B --> C["优化后的执行计划"] C --> D["vLLM Runtime"] E["RadixAttention"] E --> E1["自动识别重复前缀"] E --> E2["Radix Tree 索引 KV Cache"] E --> E3["零开销前缀复用"] end

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,
)
# 不同的请求可以使用不同的 LoRA
outputs = 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 openai
client = 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 管理,从根本上解决了显存浪费问题。

核心贡献:

flowchart TB subgraph PagedAttention与vLLM核心总结 A["核心洞察"] --> A1["KV Cache 管理是推理服务的最大瓶颈"] B["关键技术"] --> B1["PagedAttention:分页管理 KV Cache"] B --> B2["Continuous Batching:迭代级调度"] B --> B3["Prefix Caching:共享前缀复用"] C["效果"] --> C1["GPU 显存利用率从 20% 提升到 >90%"] C --> C2["吞吐量比 HuggingFace 快 24 倍"] D["影响"] --> D1["vLLM 成为 LLM 推理服务的事实标准"] D --> D2["SGLang 进一步推进自动前缀缓存"] D --> D3["启发了 TensorRT-LLM 等后续系统设计"] end

vLLM 让 LLM 推理服务化从梦想变为现实。

PagedAttention 的意义远超一篇论文。它建立了 LLM 推理系统的基本范式——用系统思维解决 AI 基础设施问题。这一范式深刻影响了后续的 SGLang、LoRA Serve 等工作,并推动了整个 LLM 推理生态的成熟。


参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

PagedAttention 与 vLLM:LLM 推理服务化
https://blog.souloss.com/posts/machine-learning/llm-paper-history/paged-attention-and-vllm-inference/
作者
Souloss
发布于
2025-04-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时