vLLM 架构总览

vLLM 由 UC Berkeley 团队开发,核心创新是 PagedAttention——一种受操作系统虚拟内存启发的注意力机制。其架构层次:

┌─────────────────────────────────────────────┐
│           OpenAI Compatible API             │
│         (Streaming / Function Calling)      │
├─────────────────────────────────────────────┤
│          Request Scheduler                  │
│    (Continuous Batching + Priority Queue)   │
├──────────────┬──────────────────────────────┤
│  PagedAttention│   ParallelWorker           │
│  KV Cache Mgr  │   (Tensor Pipeline Parallel)│
├──────────────┴──────────────────────────────┤
│           GPU Workers (CUDA)                │
│   FlashAttention / FlashInfer Backend       │
└─────────────────────────────────────────────┘

PagedAttention 原理

核心问题

传统 LLM 推理中,KV Cache 按最大序列长度预分配连续显存。这导致:

  • 内部碎片:实际序列 < 最大序列,浪费 60-80% 显存
  • 外部碎片:频繁分配/释放产生碎片
  • 无法共享:相同前缀的请求各自维护 KV Cache

PagedAttention 方案

借鉴 OS 的分页机制,将 KV Cache 分割为固定大小的 block(通常每 block 存 16 个 token 的 KV):

逻辑视图 (Token Sequence):
  [BOS] [Hello] [world] [How] [are] [you] [?]
   │     │       │       │      │      │     │
物理块映射 (Block Table):       ┌──────┐
  Block 0 → GPU 0x1000  ←───── │ B0   │ [BOS Hello world How]
  Block 1 → GPU 0x2000  ←───── │ B1   │ [are you ? _ _ _ ...]
                                └──────┘

关键优势:

指标 传统 Attention PagedAttention
KV Cache 利用率 20-40% 95%+
最大并发序列 受限于预分配 受限于总显存
前缀共享 ✅ 自动
内存碎片 严重 几乎无

代码层面理解

# vLLM 内部 KV Cache 管理(简化示意)
class PagedAttentionKVCache:
    def __init__(self, num_blocks, block_size=16, num_heads=32, head_size=128):
        self.block_size = block_size
        # 预分配所有 block 的显存池
        self.kv_pool = torch.empty(
            num_blocks, block_size, 2, num_heads, head_size,
            device='cuda', dtype=torch.float16
        )
        self.free_blocks = list(range(num_blocks))
        self.block_tables = {}  # seq_id → [block_indices]

    def allocate(self, seq_id, num_tokens):
        blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
        self.block_tables[seq_id] = []
        for _ in range(blocks_needed):
            if not self.free_blocks:
                raise OOMError("KV Cache exhausted")
            block_idx = self.free_blocks.pop(0)
            self.block_tables[seq_id].append(block_idx)

    def append_kv(self, seq_id, keys, values):
        # 写入对应物理块,无需连续
        table = self.block_tables[seq_id]
        last_block = table[-1]
        # ... write to physical block

Continuous Batching

静态 Batching 的问题

时间 →  T0   T1   T2   T3   T4   T5
Req A: [gen] [gen] [gen] [done] ----  ----
Req B: [gen] [gen] [gen] [gen] [gen] [done]
Req C: [gen] [gen] [done] ----  ----  ----
       ↑ GPU 在 A、C 完成后空等 B

Continuous Batching 机制

时间 →  T0   T1   T2   T3   T4   T5
Req A: [gen] [gen] [gen] [done]
Req B: [gen] [gen] [gen] [gen] [gen] [done]
Req C: [gen] [gen] [done]
Req D:       [gen] [gen] [gen] [done]       ← A 完成后立即插入
Req E:                  [gen] [gen] [done]  ← C 完成后插入
       ↑ GPU 持续满载
# vLLM Scheduler 核心逻辑(简化)
class Scheduler:
    def __init__(self, max_num_seqs=256, max_tokens_per_batch=8192):
        self.waiting: list[SeqGroup] = []
        self.running: list[SeqGroup] = []
        self.max_num_seqs = max_num_seqs

    def schedule(self) -> ScheduledBatch:
        scheduled = []
        total_tokens = 0

        # 1. 优先推进 running 序列(decode step)
        for seq in self.running:
            if total_tokens + 1 <= self.max_tokens_per_batch:
                scheduled.append(seq)
                total_tokens += 1

        # 2. 空位填充 waiting 序列(prefill step)
        while self.waiting and len(scheduled) < self.max_num_seqs:
            seq = self.waiting.pop(0)
            if total_tokens + seq.prompt_len <= self.max_tokens_per_batch:
                scheduled.append(seq)
                total_tokens += seq.prompt_len
            else:
                self.waiting.insert(0, seq)
                break

        # 3. 移除已完成序列,腾出位置
        self.running = [s for s in scheduled if not s.is_finished()]
        return ScheduledBatch(scheduled)

量化支持

vLLM 支持多种量化方案,各有取舍:

方案 精度损失 显存节省 推理加速 支持情况
GPTQ 4x 1.5-2x ✅ 成熟
AWQ 4x 1.5-2x ✅ 成熟
GGUF 低-中 2-8x 1.2-1.8x ⚠️ 实验性
FP8 (NVIDIA) 极低 2x 1.5-2x ✅ H100/L40S
BitsAndBytes 2-4x 无加速 ⚠️ 纯兼容

GPTQ 部署

from vllm import LLM, SamplingParams

# 直接加载 GPTQ 量化模型
llm = LLM(
    model="TheBloke/Llama-2-13B-GPTQ",
    quantization="gptq",
    dtype="float16",
    gpu_memory_utilization=0.9,
    max_model_len=4096,
    enforce_eager=False,  # 使用 CUDA Graph 加速
)

sampling = SamplingParams(temperature=0.7, max_tokens=512)
outputs = llm.generate(["Explain PagedAttention in 3 sentences."], sampling)

AWQ 部署

# 启动 OpenAI 兼容 API 服务器
python -m vllm.entrypoints.openai.api_server \
  --model TheBloke/Mistral-7B-Instruct-v0.2-AWQ \
  --quantization awq \
  --dtype float16 \
  --gpu-memory-utilization 0.9 \
  --max-model-len 8192 \
  --port 8000 \
  --enable-prefix-caching \
  --enforce-eager

FP8 部署(H100/L40S)

# FP8 精度,H100 上最佳吞吐
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --quantization fp8 \
  --kv-cache-dtype fp8 \
  --gpu-memory-utilization 0.92 \
  --tensor-parallel-size 4

分布式推理

Tensor Parallelism

# 4 卡张量并行(推荐用于 70B+ 模型)
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --gpu-memory-utilization 0.9 \
  --max-model-len 32768

# 2 节点 × 4 卡(需要 MPI 或 Ray)
# 节点 0
VLLM_HOST_IP=10.0.0.1 python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 8 \
  --pipeline-parallel-size 1 \
  --distributed-executor-backend ray

Pipeline Parallelism

# 张量并行 + 流水线并行混合
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 2 \
  --gpu-memory-utilization 0.9
并行策略 适用场景 通信开销 推荐配置
TP only 单节点多卡 高(AllReduce) 70B → TP=4
PP only 多节点 低(P2P) 超大模型
TP + PP 超大模型 中等 405B → TP=8, PP=2

性能对比

吞吐量基准(Llama 3.1 8B,A100 40G)

引擎 并发数 吞吐 (tok/s) P99 延迟 (ms) 显存效率
vLLM 0.6 64 4200 350 96%
TGI 2.3 64 3100 520 78%
Ollama 0.5 1 85 12 60%
llama.cpp 1 72 14 N/A

Prefix Caching 加速

# 启用前缀缓存(vLLM 0.4+)
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --enable-prefix-caching \
  --gpu-memory-utilization 0.9
场景 无 Prefix Cache 有 Prefix Cache 加速比
系统提示 2K + 用户 100 tok 180ms 45ms 4x
多轮对话(10 轮) 1200ms 280ms 4.3x
RAG(相同文档检索) 800ms 120ms 6.7x

生产部署配置

Docker Compose

version: "3.9"
services:
  vllm:
    image: vllm/vllm-openai:latest
    container_name: vllm
    runtime: nvidia
    environment:
      - NVIDIA_VISIBLE_DEVICES=0,1,2,3
      - HF_TOKEN=${HF_TOKEN}
    volumes:
      - ./models:/root/.cache/huggingface
    ports:
      - "8000:8000"
    command:
      - --model=meta-llama/Llama-3.1-70B-Instruct
      - --tensor-parallel-size=4
      - --gpu-memory-utilization=0.9
      - --max-model-len=32768
      - --enable-prefix-caching
      - --uvicorn-log-level=info
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 5
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

关键启动参数

参数 默认值 说明
--gpu-memory-utilization 0.9 显存使用上限
--max-model-len 模型默认 最大上下文长度
--max-num-seqs 256 最大并发序列数
--enforce-eager False 关闭 CUDA Graph
--enable-prefix-caching False 前缀缓存
--swap-space 4 (GB) CPU 侧 swap 空间
--block-size 16 KV Cache block 大小

监控与调优

# 使用 vLLM 内置 Prometheus 指标
# /metrics 端点自动暴露

关键指标:

指标 含义 告警阈值
vllm:num_requests_running 正在处理的请求数 > max_num_seqs × 0.9
vllm:num_requests_waiting 等待队列长度 > 0 持续 5min
vllm:gpu_cache_usage_perc KV Cache 使用率 > 0.95
vllm:time_to_first_token_seconds TTFT > 500ms
vllm:time_per_output_token_seconds TPOT > 50ms

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。