返回博客列表

vLLM 深度解析:从 PagedAttention 到生产部署

·AI

vLLM 深度解析:从 PagedAttention 到生产部署

如果让我选一个对 LLM 落地影响最大的开源项目,答案是 vLLM。它不是最早的推理框架,却是第一个用一个简洁的核心思想——PagedAttention——把 LLM 推理的吞吐量提升了 2-4 倍的项目,并且迅速成为行业事实标准。

这篇文章不是入门教程,而是一次深度解析。我会从项目背景讲起,逐层拆解 PagedAttention 的内存管理机制、Continuous Batching 的调度策略、内部架构的组件交互,最后到生产部署的实战经验。


一、vLLM 是什么

1.1 项目背景

vLLM 诞生于 UC Berkeley SKY Lab,核心作者是 Woosuk Kwon 等人。2023 年 6 月发布论文 Efficient Memory Management for Large Language Model Serving with PagedAttention,同期开源了 vLLM 项目。

项目解决的核心问题是:LLM 推理时 KV Cache 的内存管理极其低效,导致 GPU 显存大量浪费,吞吐量远低于理论上限。

在 vLLM 之前,主流推理框架(HuggingFace TGI、NVIDIA FasterTransformer)为每个请求预分配一段连续的 KV Cache 内存,长度等于模型的最大序列长度。但实际请求长度是动态的——一个 4096 max_len 的模型,实际请求平均可能只有 512 token。这意味着 60-80% 的 GPU 显存被浪费了。

1.2 定位与生态

vLLM 的定位是 高吞吐量的 LLM 推理和服务引擎。截至 2026 年初:

  • GitHub 50k+ stars,是 LLM 推理领域最活跃的开源项目
  • 支持 100+ 种模型架构(LLaMA、Qwen、Mistral、DeepSeek、Gemma 等)
  • 支持 FP16/BF16/FP8/INT8/INT4/GPTQ/AWQ/GGUF 等多种精度格式
  • 兼容 OpenAI API 格式,可作为 drop-in replacement
  • 被 Anyscale、Databricks、AWS Bedrock 等平台集成

vLLM 不做训练、不做微调,只专注一件事:把模型推理做到极致。


二、核心优势

vLLM 的核心优势可以归纳为三个层面:

2.1 PagedAttention:近乎零浪费的内存管理

传统方案预分配连续内存,导致大量内部碎片和外部碎片。PagedAttention 把 KV Cache 切成固定大小的 block(默认 16 个 token 一个 block),按需分配、非连续存储。

效果:KV Cache 内存利用率从 20-40% 提升到 >95%,同等硬件条件下能服务 2-4 倍的并发请求。

2.2 Continuous Batching:消除等待浪费

传统 static batching 要等一个 batch 内所有请求都生成完毕才能开始下一个 batch,短请求被长请求拖累。Continuous Batching 在每一个生成步骤(iteration level)动态调整 batch 组成——完成的请求立即移出,新请求立即加入。

效果:GPU 利用率从 30-50%(static batching)提升到 80-95%。

2.3 高吞吐低延迟的工程优化

除了两个核心算法创新,vLLM 还做了大量工程优化:

  • Prefix Caching:相同 system prompt 的请求共享 KV Cache,避免重复计算
  • Chunked Prefill:长 prompt 的 prefill 分块进行,不阻塞其他请求的 decode
  • Speculative Decoding:小模型快速草拟,大模型批量验证
  • CUDA Graph:缓存 GPU kernel 调用图,减少 CPU-GPU 间的 launch overhead
  • Flash Attention / FlashInfer 集成:IO-aware 的精确 Attention 实现

三、基础原理详解

3.1 PagedAttention 内存管理机制

传统方式的问题

先看传统 KV Cache 管理的问题:

GPU 显存布局(传统连续分配):
┌────────────────────────────────────────────────┐
│  Request 1 KV Cache    [██████░░░░░░░░░░░░░░]  │  实际用30%,预留100%
│  Request 2 KV Cache    [████████████░░░░░░░░]  │  实际用60%,预留100%
│  Request 3 KV Cache    [██░░░░░░░░░░░░░░░░░░]  │  实际用10%,预留100%
│  ░░░░░░░░░░ 剩余空间不够再分配一个完整的 ░░░░  │  外部碎片
└────────────────────────────────────────────────┘
█ = 已使用    ░ = 已预留但未使用(浪费)

三类浪费:

  • 预留浪费:为每个请求预留 max_seq_len 的空间,实际可能只用一小部分
  • 内部碎片:已分配但未使用的空间
  • 外部碎片:剩余空间因为不连续无法分配给新请求

PagedAttention 的解法

借鉴操作系统虚拟内存的核心思想:逻辑地址连续,物理地址不必连续。

PagedAttention 内存管理:

逻辑视图(每个请求看到的):
Request 1: [Token 0-15] [Token 16-31] [Token 32-40]
Request 2: [Token 0-15] [Token 16-31] [Token 32-47] [Token 48-55]

              │ Block Table 映射 │
              ▼                  ▼

物理视图(GPU 显存中的实际布局):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ B0  │ B1  │ B2  │ B3  │ B4  │ B5  │ B6  │FREE │FREE │
│R2-0 │R1-0 │R2-1 │R1-1 │R2-2 │R1-2 │R2-3 │     │     │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

Block Table:
┌───────────┬────────────────────┐
│ Request 1 │ [B1, B3, B5]       │
│ Request 2 │ [B0, B2, B4, B6]   │
└───────────┴────────────────────┘

每个 Block 存储固定数量 token 的 K/V 向量(默认 16 tokens)
Block 在物理上可以不连续,通过 Block Table 维护逻辑→物理的映射

关键数据结构:

# Block Table 的核心逻辑(简化版)
class BlockTable:
    def __init__(self, block_size: int = 16):
        self.block_size = block_size
        self.table: dict[int, list[int]] = {}  # seq_id → [physical_block_ids]
        self.free_blocks: list[int] = list(range(NUM_GPU_BLOCKS))

    def allocate(self, seq_id: int) -> int:
        """为序列分配一个新的物理 block"""
        if not self.free_blocks:
            raise MemoryError("No free blocks")
        block_id = self.free_blocks.pop(0)
        if seq_id not in self.table:
            self.table[seq_id] = []
        self.table[seq_id].append(block_id)
        return block_id

    def free(self, seq_id: int):
        """释放序列的所有 block"""
        if seq_id in self.table:
            self.free_blocks.extend(self.table.pop(seq_id))

    def get_physical_blocks(self, seq_id: int) -> list[int]:
        """获取序列的物理 block 列表"""
        return self.table.get(seq_id, [])

Prefix Caching 与 Copy-on-Write

PagedAttention 的分页设计还带来了两个重要特性:

Prefix Caching——多个请求共享相同 system prompt 的 KV Cache:

System Prompt: "你是一个有帮助的AI助手..."(共20个token → 2个block)

Request A: system_prompt + "帮我写代码"
Request B: system_prompt + "帮我翻译"
Request C: system_prompt + "帮我总结"

Block Table:
┌───────────┬───────────────────────────────────┐
│ Request A │ [B0*, B1*, B5, B6]                │  B0, B1 是共享的
│ Request B │ [B0*, B1*, B7, B8]                │  prefix blocks
│ Request C │ [B0*, B1*, B9]                    │
└───────────┴───────────────────────────────────┘
* = 共享 block(引用计数 > 1)

Copy-on-Write——共享 block 被修改时才复制,类似 Linux fork 的 COW 机制。在 parallel sampling(同一个 prompt 生成多个候选)场景中,prompt 阶段的 KV Cache 完全共享,只有 decode 阶段各自生成的部分独立存储。

3.2 Continuous Batching 调度策略

Static Batching 的问题

Static Batching(传统):

时间 →
Batch 1: ┌─Req A────────────────────┐  Req A 生成 100 tokens
         │ Req B────────┤           │  Req B 生成 40 tokens,但必须等 A
         │ Req C──┤                 │  Req C 生成 15 tokens,等更久
         └────────────────────────┘
                                    ↑ 整个 batch 完成后才能开始 Batch 2
                        GPU 空闲等待 ↑

问题:短请求被长请求拖累,GPU 计算单元大量空闲

Continuous Batching(Iteration-Level Scheduling)

Continuous Batching(vLLM):

时间 → (每一列是一次 forward pass)
Step:  1   2   3   4   5   6   7   8   9   10  11  12
Req A: █   █   █   █   █   █   █   █   █   █   █   █   → 完成
Req B: █   █   █   █   █   ✓                            → Step 5 完成
Req C: █   █   ✓                                         → Step 2 完成
Req D:             █   █   █   █   █   █   ✓             → Step 3 加入
Req E:                     █   █   █   █   █   █   █     → Step 5 加入
Req F:                                 █   █   █   █     → Step 8 加入

✓ = 请求完成,slot 立即释放
█ = 正在生成

每一步都把 batch 填满,完成的请求立即移出,新请求立即加入

vLLM 的调度器在每一次 forward pass 前都会重新评估 batch 组成:

# Scheduler 核心逻辑(简化)
class Scheduler:
    def schedule(self) -> SchedulerOutput:
        # 1. 已在 running 中的请求继续 decode
        running_seqs = self.running.copy()

        # 2. 尝试加入 waiting 队列中的新请求(prefill)
        while self.waiting:
            seq = self.waiting[0]
            # 检查是否有足够的 KV Cache blocks
            if not self.block_manager.can_allocate(seq):
                break  # 显存不足,停止加入
            # 检查是否超过 max_num_batched_tokens
            if self._exceeds_token_budget(seq, running_seqs):
                break
            self.waiting.pop(0)
            self.block_manager.allocate(seq)
            running_seqs.append(seq)

        # 3. 如果显存仍然不足,preempt 优先级低的请求
        while not self.block_manager.can_append_slots(running_seqs):
            victim = running_seqs.pop()  # 最后加入的优先被抢占
            self._preempt(victim)        # swap 到 CPU 或 recompute

        self.running = running_seqs
        return SchedulerOutput(scheduled_seqs=running_seqs)

Preemption 策略

当显存不足时,vLLM 有两种抢占策略:

  • Swap:把被抢占请求的 KV Cache 从 GPU 交换到 CPU 内存,后续恢复时再换回来
  • Recompute:直接丢弃 KV Cache,恢复时重新计算 prefill

Swap 适合 GPU-CPU 带宽充裕的场景,Recompute 适合请求较短的场景。

3.3 Tensor Parallel 多卡推理

大模型单卡放不下时,vLLM 使用 Tensor Parallelism 把模型切分到多块 GPU:

Tensor Parallelism(TP=4,以 Attention 层为例):

输入 X (batch, seq_len, d_model)
       │
       │ 广播到 4 块 GPU
       ▼
┌──────────┬──────────┬──────────┬──────────┐
│  GPU 0   │  GPU 1   │  GPU 2   │  GPU 3   │
│          │          │          │          │
│ W_Q[:,:1024]  W_Q[:,1024:2048]  ...       │  Q/K/V 权重按列切分
│ W_K[:,:1024]  W_K[:,1024:2048]  ...       │
│ W_V[:,:1024]  W_V[:,1024:2048]  ...       │
│          │          │          │          │
│ 局部Attn │ 局部Attn │ 局部Attn │ 局部Attn │  各自独立计算
│          │          │          │          │
│ O[:,:1024]│ O[:,1024:2048]│    ...        │  W_O 按行切分
└────┬─────┴────┬─────┴────┬─────┴────┬─────┘
     │          │          │          │
     └──────────┴──────┬───┴──────────┘
                       │ AllReduce (NCCL)
                       ▼
              最终输出 (batch, seq_len, d_model)

每个 decode step 需要一次 AllReduce 同步(每个 Transformer 层两次:Attention 后一次,FFN 后一次)。NCCL 在 NVLink 互联的 GPU 间延迟很低(~10μs),但跨节点(PCIe/InfiniBand)会成为瓶颈。

vLLM 底层使用 Ray Actor 来管理多 GPU Worker:

# vLLM 内部的多卡初始化(简化)
# 每个 GPU 是一个 Ray Actor
workers = []
for rank in range(tensor_parallel_size):
    worker = Worker.remote(
        model_config=model_config,
        parallel_config=parallel_config,
        rank=rank,
    )
    workers.append(worker)

# 每一步推理:所有 worker 并行执行,通过 NCCL 同步
futures = [w.execute_model.remote(input_metadata) for w in workers]
outputs = ray.get(futures)

四、架构设计

4.1 核心组件

┌────────────────────────────────────────────────────┐
│                   API Server                        │
│          (OpenAI-compatible HTTP Server)            │
└──────────────────────┬─────────────────────────────┘
                       │ 请求
                       ▼
┌────────────────────────────────────────────────────┐
│                   LLMEngine                         │
│  ┌─────────────┐  ┌────────────┐  ┌─────────────┐ │
│  │  Tokenizer  │  │  Scheduler │  │ BlockManager│ │
│  └─────────────┘  └─────┬──────┘  └──────┬──────┘ │
│                         │ 调度决策         │ 内存管理│
│                         ▼                 ▼        │
│  ┌──────────────────────────────────────────────┐  │
│  │              Model Executor                   │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐     │  │
│  │  │ Worker 0 │ │ Worker 1 │ │ Worker N │     │  │
│  │  │(GPU 0)   │ │(GPU 1)   │ │(GPU N)   │     │  │
│  │  │┌────────┐│ │┌────────┐│ │┌────────┐│     │  │
│  │  ││ModelRun││ ││ModelRun││ ││ModelRun││     │  │
│  │  │└────────┘│ │└────────┘│ │└────────┘│     │  │
│  │  └──────────┘ └──────────┘ └──────────┘     │  │
│  └──────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────┘

4.2 各组件职责

API Server:接收 HTTP 请求,兼容 OpenAI /v1/completions 和 /v1/chat/completions 格式。内部用 asyncio 实现非阻塞 IO,支持 SSE 流式输出。

LLMEngine:核心引擎,协调所有组件。主循环:

# LLMEngine 的主循环(简化)
class LLMEngine:
    def step(self) -> list[RequestOutput]:
        # 1. Scheduler 决定本轮执行哪些请求
        scheduler_output = self.scheduler.schedule()

        # 2. 准备模型输入(token ids, positions, block tables)
        execute_input = self.prepare_input(scheduler_output)

        # 3. 发送给 Worker 执行 forward pass
        output = self.model_executor.execute_model(execute_input)

        # 4. 采样:从 logits 中选择下一个 token
        sampled = self.sampler.sample(output.logits, sampling_params)

        # 5. 更新状态:追加 token、分配新 block、检查停止条件
        return self.process_outputs(sampled, scheduler_output)

Scheduler:调度器,每一步决定:

  • 哪些 waiting 请求可以开始 prefill
  • 哪些 running 请求继续 decode
  • 哪些请求需要被 preempt(抢占)

调度优先级:FCFS(先到先服务),但 prefill 和 decode 会被区分对待——Chunked Prefill 允许将长 prompt 的 prefill 分块与 decode 混合执行。

BlockManager:KV Cache 的内存管理器。维护 Block Table,决策 block 的分配、释放、swap 和共享。

ModelExecutor:封装 Worker 的管理。单卡时直接执行,多卡时通过 Ray Actor 编排。

Worker:每块 GPU 上运行的实际计算单元。持有模型权重的分片。

ModelRunner:Worker 内部的模型执行器。负责:

  • 构建 attention metadata(block table 信息传递给 kernel)
  • 调用 model forward
  • 管理 CUDA Graph capture/replay

4.3 一次请求的完整生命周期

用户发送请求: "解释量子力学"
       │
       ▼
① API Server 接收,tokenize → [token_ids]
       │
       ▼
② 加入 Scheduler.waiting 队列
       │
       ▼
③ Scheduler.schedule():
   - 检查 BlockManager 有足够 free blocks
   - 分配初始 blocks
   - 将请求从 waiting → running
   - 标记为 prefill 阶段
       │
       ▼
④ Prefill(并行计算所有输入 token 的 KV):
   - 所有输入 token 一次性送入模型
   - 计算 KV Cache 并存入分配的 blocks
   - 从最后一个位置的 logits 采样得到第一个输出 token
   - TTFT(Time to First Token)在此确定
       │
       ▼
⑤ Decode 循环(逐 token 生成):
   ┌─────────────────────────────────────┐
   │ - Scheduler 将请求保持在 running     │
   │ - 新 token 送入模型(只算 1 个 token)│
   │ - 从 KV Cache 读取历史 K/V           │
   │ - 计算 attention,得到 logits        │
   │ - 采样得到下一个 token               │
   │ - 如果当前 block 满了,分配新 block   │
   │ - 检查停止条件(EOS/max_tokens)     │
   └─────────────┬───────────────────────┘
                 │ 每一步流式返回一个 token
                 │
                 ▼ 直到满足停止条件
       │
       ▼
⑥ 完成:
   - 释放所有 blocks 回 free pool
   - 从 running 中移除
   - 返回完整结果

五、与其他推理框架对比

5.1 对比一览

特性 vLLM TGI SGLang TensorRT-LLM
开发方 UC Berkeley Hugging Face UC Berkeley NVIDIA
核心优势 PagedAttention、高吞吐 HF 生态集成 RadixAttention、结构化生成 极致延迟、硬件优化
内存管理 PagedAttention 连续分配 → 后来引入 Paged RadixAttention(前缀树) 预分配
调度 Continuous Batching Continuous Batching Continuous Batching In-flight Batching
量化支持 GPTQ/AWQ/FP8/INT8 GPTQ/AWQ/bitsandbytes GPTQ/AWQ/FP8 FP8/INT8/INT4(最全)
多卡 Tensor Parallel (Ray) Tensor Parallel Tensor/Data Parallel Tensor/Pipeline Parallel
API 兼容 OpenAI 格式 自定义 + OpenAI OpenAI 格式 Triton + OpenAI
部署复杂度 低 低 低 高(需要 build engine)
开源协议 Apache 2.0 Apache 2.0 Apache 2.0 Apache 2.0

5.2 TGI (Text Generation Inference)

Hugging Face 出品,优势是与 HF 生态(模型库、Tokenizer)的深度集成。早期 TGI 不支持 PagedAttention,在吞吐量上落后于 vLLM。后来也引入了 Paged KV Cache,差距缩小。

适合场景:已经深度使用 HF 生态,需要快速部署,对吞吐量要求不是极致的场景。

5.3 SGLang

同样来自 UC Berkeley(与 vLLM 团队关系密切),核心创新是 RadixAttention——用前缀树(Radix Tree)管理 KV Cache,对多轮对话和共享前缀场景做了深度优化。

SGLang 还提供了一个独特的结构化生成 DSL,可以约束 LLM 的输出格式(正则表达式、JSON Schema),在 Agent 工具调用等场景非常实用。

# SGLang 的结构化生成
@sgl.function
def extract_info(s, text):
    s += "Extract structured info from: " + text + "\n"
    s += "Name: " + sgl.gen("name", stop="\n")
    s += "Age: " + sgl.gen("age", regex=r"\d{1,3}")
    s += "City: " + sgl.gen("city", stop="\n")

适合场景:多轮对话密集型应用、需要结构化输出的 Agent 场景。

5.4 TensorRT-LLM

NVIDIA 官方推理方案,底层基于 TensorRT 做 kernel fusion 和图优化,能压榨出硬件的最后一点性能。

优势:

  • 延迟最低(kernel 级别的深度优化)
  • FP8 量化支持最成熟(NVIDIA GPU 原生支持)
  • 对 NVIDIA 新硬件(H100/B200)第一时间适配

劣势:

  • 需要先把模型 build 成 TensorRT engine,流程复杂
  • 模型支持速度慢于 vLLM(新模型适配周期长)
  • 灵活性低,定制难度大

适合场景:追求极致延迟、使用 NVIDIA 最新硬件、有专业 Infra 团队的生产环境。

5.5 选型建议

你的场景是什么?
├── 需要最快上线、模型种类多 → vLLM
├── 深度依赖 HF 生态 → TGI
├── 多轮对话 + 结构化输出 → SGLang
├── 极致延迟 + NVIDIA 硬件 + 有 Infra 团队 → TensorRT-LLM
└── 不确定 → 先用 vLLM,后续按需切换

大多数场景下 vLLM 是默认选择——模型支持最广、社区最活跃、部署最简单、性能足够好。只有在特定场景下的特定瓶颈,才需要考虑切换。


六、生产部署实践

6.1 API Server 部署

最简单的部署方式——直接启动 OpenAI 兼容的 API Server:

# 基础启动
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3-8B-Instruct \
    --host 0.0.0.0 \
    --port 8000

# 生产配置
python -m vllm.entrypoints.openai.api_server \
    --model /models/Llama-3-70B-Instruct \
    --tensor-parallel-size 4 \
    --max-model-len 8192 \
    --gpu-memory-utilization 0.92 \
    --max-num-seqs 256 \
    --enable-prefix-caching \
    --enable-chunked-prefill \
    --disable-log-requests \
    --uvicorn-log-level warning

关键参数说明:

参数 作用 生产建议
--gpu-memory-utilization KV Cache 可用的显存比例 0.90-0.95(留一点 buffer)
--max-model-len 最大序列长度 按实际业务设置,不要用默认的模型最大值
--max-num-seqs 最大并发序列数 根据压测结果调整
--enable-prefix-caching 启用前缀缓存 有 system prompt 的场景必开
--enable-chunked-prefill 分块 prefill 长 prompt 场景开启
--quantization 量化方式 gptq/awq/fp8 按需选择
--enforce-eager 禁用 CUDA Graph 调试时开启,生产关闭

6.2 Docker 部署

# Dockerfile
FROM vllm/vllm-openai:latest

# 如果模型已下载到本地
VOLUME /models

ENTRYPOINT ["python", "-m", "vllm.entrypoints.openai.api_server"]
CMD ["--model", "/models/Llama-3-8B-Instruct", \
     "--host", "0.0.0.0", \
     "--port", "8000", \
     "--gpu-memory-utilization", "0.92"]
# 运行
docker run -d \
    --gpus all \
    --name vllm-server \
    -p 8000:8000 \
    -v /data/models:/models \
    -e VLLM_WORKER_MULTIPROC_METHOD=spawn \
    vllm/vllm-openai:latest \
    --model /models/Llama-3-8B-Instruct \
    --host 0.0.0.0 \
    --port 8000

6.3 Kubernetes 部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama3
  labels:
    app: vllm-llama3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-llama3
  template:
    metadata:
      labels:
        app: vllm-llama3
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        args:
          - "--model"
          - "/models/Llama-3-70B-Instruct"
          - "--tensor-parallel-size"
          - "4"
          - "--host"
          - "0.0.0.0"
          - "--port"
          - "8000"
          - "--gpu-memory-utilization"
          - "0.92"
          - "--max-model-len"
          - "8192"
          - "--enable-prefix-caching"
        ports:
        - containerPort: 8000
        resources:
          limits:
            nvidia.com/gpu: "4"
            cpu: "32"
            memory: "128Gi"
          requests:
            nvidia.com/gpu: "4"
            cpu: "16"
            memory: "64Gi"
        volumeMounts:
        - name: model-storage
          mountPath: /models
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 180
          periodSeconds: 30
      volumes:
      - name: model-storage
        persistentVolumeClaim:
          claimName: model-pvc
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-llama3
spec:
  selector:
    app: vllm-llama3
  ports:
  - port: 8000
    targetPort: 8000
  type: ClusterIP

注意事项:

  • readinessProbe 的 initialDelaySeconds 要足够大——70B 模型加载可能需要 2-3 分钟
  • 多卡推理(TP>1)时 Pod 内的 GPU 必须在同一节点上(不能跨节点 TP)
  • 设置 tolerations 确保 Pod 调度到 GPU 节点

6.4 监控与告警

vLLM 内置 Prometheus metrics 端点:

# 访问 /metrics 获取指标
curl http://localhost:8000/metrics

关键监控指标:

指标 含义 告警阈值建议
vllm:num_requests_running 当前运行中的请求数 接近 max_num_seqs 时告警
vllm:num_requests_waiting 等待队列长度 > 0 持续超过 30s
vllm:gpu_cache_usage_perc KV Cache 使用率 > 95%
vllm:avg_generation_throughput_toks_per_s 生成吞吐量 低于基线 20%
vllm:e2e_request_latency_seconds 端到端请求延迟 P99 超过 SLA
vllm:time_to_first_token_seconds 首 token 延迟 P99 > 2s

Grafana Dashboard 配置示例:

# prometheus.yml
scrape_configs:
  - job_name: 'vllm'
    scrape_interval: 5s
    static_configs:
      - targets: ['vllm-llama3:8000']
    metrics_path: /metrics

6.5 压测与调优

部署后必须做压测来确定最优配置:

# 用 locust 做压测
from locust import HttpUser, task, between

class VLLMUser(HttpUser):
    wait_time = between(0.1, 0.5)

    @task
    def chat_completion(self):
        self.client.post("/v1/chat/completions", json={
            "model": "Llama-3-70B-Instruct",
            "messages": [
                {"role": "system", "content": "你是一个有帮助的AI助手。"},
                {"role": "user", "content": "解释一下什么是 PagedAttention?"},
            ],
            "max_tokens": 256,
            "temperature": 0.7,
        })

调优思路:

  1. 先跑基准——默认参数下的 QPS、P50/P99 延迟
  2. 逐步提高 max-num-seqs,观察吞吐量和延迟的 trade-off
  3. 开启 --enable-prefix-caching,对比有无 prefix cache 的效果
  4. 如果 TTFT 过高,开启 --enable-chunked-prefill
  5. 如果显存不足,考虑量化(--quantization awq)或降低 --max-model-len

七、总结

vLLM 的成功不是偶然。它在正确的时间点(LLM 推理从实验走向生产)解决了正确的问题(KV Cache 内存管理),并用工程上的持续迭代(Continuous Batching、Prefix Caching、Chunked Prefill、Speculative Decoding)不断巩固优势。

对于 AI 工程师,理解 vLLM 的价值不只是会用一个框架,而是理解了 LLM 推理系统设计中最核心的 trade-off:内存效率 vs 计算效率 vs 延迟。这些原理不会因为框架的更迭而过时。