vLLM 深度解析:从 PagedAttention 到生产部署
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,
})
调优思路:
- 先跑基准——默认参数下的 QPS、P50/P99 延迟
- 逐步提高
max-num-seqs,观察吞吐量和延迟的 trade-off - 开启
--enable-prefix-caching,对比有无 prefix cache 的效果 - 如果 TTFT 过高,开启
--enable-chunked-prefill - 如果显存不足,考虑量化(
--quantization awq)或降低--max-model-len
七、总结
vLLM 的成功不是偶然。它在正确的时间点(LLM 推理从实验走向生产)解决了正确的问题(KV Cache 内存管理),并用工程上的持续迭代(Continuous Batching、Prefix Caching、Chunked Prefill、Speculative Decoding)不断巩固优势。
对于 AI 工程师,理解 vLLM 的价值不只是会用一个框架,而是理解了 LLM 推理系统设计中最核心的 trade-off:内存效率 vs 计算效率 vs 延迟。这些原理不会因为框架的更迭而过时。