欢迎光临
我们一直在努力

详解vLLM推理引擎架构:PagedAttention原理与LLM高效部署实践

引言:大模型推理的瓶颈与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 的原理到生产部署的完整实践。

vLLM architecture diagram

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%

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 显存的潜力。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 详解vLLM推理引擎架构:PagedAttention原理与LLM高效部署实践
分享到: 更多 (0)