返回博客列表

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 显存中的实际布局):
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ B0B1B2B3B4B5B6FREEFREE │
│R2-0R1-0R2-1R1-1R2-2R1-2R2-3 │     │     │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

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

每个 Block 存储固定数量 tokenK/V 向量(默认 16 tokensBlock 在物理上可以不连续,通过 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助手..."(共20token2blockRequest 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 blocksRequest 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 tokensReq B────────┤           │  Req B 生成 40 tokens,但必须等 AReq 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

注意事项:

  • readinessProbeinitialDelaySeconds 要足够大——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 延迟。这些原理不会因为框架的更迭而过时。