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 ? _ _ _ ...]
                                └──────┘

关键优势:

指标传统 AttentionPagedAttention
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 支持多种量化方案,各有取舍:

方案精度损失显存节省推理加速支持情况
GPTQ4x1.5-2x✅ 成熟
AWQ4x1.5-2x✅ 成熟
GGUF低-中2-8x1.2-1.8x⚠️ 实验性
FP8 (NVIDIA)极低2x1.5-2x✅ H100/L40S
BitsAndBytes2-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.664420035096%
TGI 2.364310052078%
Ollama 0.51851260%
llama.cpp17214N/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 tok180ms45ms4x
多轮对话(10 轮)1200ms280ms4.3x
RAG(相同文档检索)800ms120ms6.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-utilization0.9显存使用上限
--max-model-len模型默认最大上下文长度
--max-num-seqs256最大并发序列数
--enforce-eagerFalse关闭 CUDA Graph
--enable-prefix-cachingFalse前缀缓存
--swap-space4 (GB)CPU 侧 swap 空间
--block-size16KV Cache block 大小

监控与调优

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

关键指标:

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

加入讨论

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

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