第 45 章
vLLM 高并发推理服务
第45章 vLLM 高并发推理服务
导语
Ollama 解决了"能不能跑"的问题,vLLM 解决的是"能不能撑住"的问题。当你的 Hermes Agent 需要同时服务 50 个、500 个甚至 5000 个并发请求时,Ollama 会让你的用户排队到天荒地老,而 vLLM 的 PagedAttention 机制可以将吞吐量提升 10-30 倍。本章从原理到实战,带你掌握 vLLM 的高并发推理服务。
45.1 vLLM 安装与 GPU 环境配置
系统要求
| 组件 | 最低版本 | 推荐版本 |
|---|---|---|
| CUDA | 11.8 | 12.1+ |
| Python | 3.8 | 3.10-3.11 |
| GPU 架构 | Ampere(30系) | Hopper(H100) |
| 驱动 | 520.xx | 550.xx+ |
安装步骤
# 1. 创建虚拟环境
python3.11 -m venv venv-vllm
source venv-vllm/bin/activate
# 2. 安装 vLLM(自动匹配 CUDA 版本)
pip install vllm
# CUDA 12.1 专用版本(更快)
pip install vllm --extra-index-url https://download.pytorch.org/whl/cu121
# 3. 验证安装
python -c "import vllm; print(vllm.__version__)"
python -c "from vllm import LLM; print('GPU:', LLM._get_and_verify_max_len.__module__)"
# 4. 安装辅助工具
pip install httpx rich typer # 测试和监控工具
# 5. 验证 GPU
python -c "
import torch
print(f'CUDA 可用: {torch.cuda.is_available()}')
print(f'GPU 数量: {torch.cuda.device_count()}')
for i in range(torch.cuda.device_count()):
props = torch.cuda.get_device_properties(i)
print(f' GPU {i}: {props.name}, {props.total_memory // 1024**3} GB')
"
Docker 方式安装(推荐生产环境)
# 拉取官方镜像
docker pull vllm/vllm-openai:latest
# 带 GPU 支持运行
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model NousResearch/Hermes-3-70B \
--dtype bfloat16 \
--tensor-parallel-size 2 \
--max-model-len 65536
45.2 启动 Hermes 4 推理服务的完整命令
基础启动命令(单卡)
# 使用 HuggingFace Hub 模型(自动下载)
python -m vllm.entrypoints.openai.api_server \
--model NousResearch/Hermes-3-Llama-3.1-70B \
--dtype bfloat16 \
--max-model-len 65536 \
--gpu-memory-utilization 0.90 \
--port 8000 \
--host 0.0.0.0
# 使用本地模型目录
python -m vllm.entrypoints.openai.api_server \
--model /models/hermes-4-70b \
--dtype bfloat16 \
--max-model-len 65536 \
--gpu-memory-utilization 0.92 \
--port 8000
生产环境完整启动命令
#!/bin/bash
# start_vllm_hermes.sh
set -e
MODEL_PATH="${MODEL_PATH:-NousResearch/Hermes-3-Llama-3.1-70B}"
GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader | wc -l)
echo "检测到 ${GPU_COUNT} 块 GPU"
python -m vllm.entrypoints.openai.api_server \
--model "$MODEL_PATH" \
\
`# 精度和量化` \
--dtype bfloat16 \
--quantization None \
\
`# 上下文和内存` \
--max-model-len 65536 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 256 \ # 最大并发序列数
--max-num-batched-tokens 65536 \ # 每批最大 token 数
\
`# 并行配置(多卡)` \
--tensor-parallel-size "$GPU_COUNT" \
\
`# 调度配置` \
--scheduler-delay-factor 0.1 \ # 批处理等待时间(秒)
--use-v2-block-manager \ # 使用 v2 块管理器
\
`# API 配置` \
--host 0.0.0.0 \
--port 8000 \
--api-key "${VLLM_API_KEY:-}" \ # 可选 API Key
\
`# 日志` \
--log-level info \
2>&1 | tee /var/log/vllm-hermes.log
GGUF 模型加载(vLLM 0.5+ 支持)
# vLLM 0.5+ 支持直接加载 GGUF 格式
python -m vllm.entrypoints.openai.api_server \
--model /models/hermes-4-70b-q4_k_m.gguf \
--tokenizer NousResearch/Hermes-3-Llama-3.1-70B \
--quantization gguf \
--dtype float16 \
--max-model-len 32768 \
--port 8000
45.3 PagedAttention 原理
传统 KV Cache 的问题
在传统推理框架中,KV Cache 需要预先分配连续的内存块:
传统方法(预分配):
序列 A: [████████████████████░░░░░░░░░░░░] 预留 2048 tokens,实际用 800
↑ 浪费了 60% 的显存
序列 B: [████████████████████████████░░░░] 预留 2048 tokens,实际用 1400
序列 C: 等待...(虽然 GPU 有空余碎片显存,但无法分配)
碎片化问题:已分配但未使用的内存(内部碎片)+ 无法分配的小块空闲内存(外部碎片)= 浪费 30-60% 显存。
PagedAttention:虚拟分页管理
PagedAttention 借鉴了操作系统的虚拟内存分页思想:
PagedAttention 内存模型:
物理 KV Block(固定大小,如 16 tokens/块):
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ B6 │ B7 │ 物理内存块
└────┴────┴────┴────┴────┴────┴────┴────┘
逻辑 KV 视图(每个序列按需申请块):
序列 A: [B0] → [B3] → [B7] (3 块,非连续)
序列 B: [B1] → [B2] → [B4] → [B6] (4 块,非连续)
序列 C: [B5] (1 块,按需分配)
核心优势:
- 零内部碎片:按 Block 精确分配,最后一块有少量碎片
- 零外部碎片:非连续的物理块通过页表映射为连续逻辑视图
- 内存共享:多个请求共享相同 Prompt 的 KV Cache(Prefix Caching)
PagedAttention 吞吐量提升原理
# 吞吐量对比的直觉解释
def throughput_comparison():
"""
传统方法 vs PagedAttention 的内存利用率对比
"""
gpu_vram_gb = 80 # A100 80GB
model_size_gb = 40 # Hermes 70B Q4
available_for_kv = gpu_vram_gb - model_size_gb # 40 GB
# 传统方法:预分配 max_len
# 每个序列预分配 65536 tokens 的 KV Cache
# FP16: 65536 * 0.25 GB/K = 16.4 GB 每序列
traditional_seq_size_gb = 65536 / 1024 * 0.25 # ~16 GB
traditional_max_concurrent = int(available_for_kv / traditional_seq_size_gb)
# PagedAttention:按实际使用分配
# 平均序列长度通常 1000-3000 tokens
avg_actual_seq_len = 2048 # tokens
paged_seq_size_gb = avg_actual_seq_len / 1024 * 0.25 # ~0.5 GB
paged_max_concurrent = int(available_for_kv / paged_seq_size_gb)
print(f"传统方法最大并发: {traditional_max_concurrent} 序列")
print(f"PagedAttention 最大并发: {paged_max_concurrent} 序列")
print(f"吞吐量提升: {paged_max_concurrent / traditional_max_concurrent:.1f}x")
# 传统: 2 序列,PagedAttention: ~80 序列 → 40x 提升
throughput_comparison()
Prefix Caching(前缀共享)
vLLM 的 Prefix Caching 让多个请求共享相同前缀的 KV Cache:
# 共享系统提示词的场景
SYSTEM_PROMPT = "你是 Hermes,一个专业的 AI 助手..." * 100 # 假设很长的系统提示
# 请求 1: system_prompt + user_query_1
# 请求 2: system_prompt + user_query_2
# 请求 3: system_prompt + user_query_3
# 传统方法:每个请求都重新计算 system_prompt 的 KV Cache(×3 计算量)
# Prefix Caching:system_prompt 只计算一次,三个请求共享
启用方式:
python -m vllm.entrypoints.openai.api_server \
--model NousResearch/Hermes-3-Llama-3.1-70B \
--enable-prefix-caching \ # 启用前缀缓存
--max-model-len 65536
45.4 吞吐量 vs 延迟权衡配置
核心权衡矩阵
| 配置策略 | 吞吐量 | 延迟(P50) | 延迟(P99) | 适用场景 |
|---|---|---|---|---|
| 吞吐量优先 | 极高 | 高 | 极高 | 批量 API、异步处理 |
| 延迟优先 | 低 | 极低 | 低 | 交互式应用、IDE 插件 |
| 平衡模式 | 高 | 中 | 中高 | 通用 API 服务 |
吞吐量优先配置
# 最大化吞吐量配置
python -m vllm.entrypoints.openai.api_server \
--model NousResearch/Hermes-3-Llama-3.1-70B \
--max-num-seqs 512 \ # 允许大量并发序列
--max-num-batched-tokens 131072 \ # 大批次处理
--scheduler-delay-factor 0.5 \ # 等待更多请求聚合(延迟换吞吐)
--enable-prefix-caching \
--gpu-memory-utilization 0.95 # 激进的内存利用
延迟优先配置
# 最小化延迟配置
python -m vllm.entrypoints.openai.api_server \
--model NousResearch/Hermes-3-Llama-3.1-70B \
--max-num-seqs 32 \ # 限制并发,减少排队
--max-num-batched-tokens 8192 \ # 小批次,快速响应
--scheduler-delay-factor 0.0 \ # 不等待,立即处理
--gpu-memory-utilization 0.85 # 留余量给 KV Cache
动态批处理调优
# vllm_tuning.py
"""
自动寻找吞吐量和延迟的最优平衡点。
"""
import asyncio
import time
import httpx
import statistics
async def send_request(client: httpx.AsyncClient, prompt: str) -> tuple[float, float]:
"""返回 (首token延迟, 总时间)"""
start = time.time()
first_token_time = None
async with client.stream(
"POST",
"http://localhost:8000/v1/completions",
json={
"model": "NousResearch/Hermes-3-Llama-3.1-70B",
"prompt": prompt,
"max_tokens": 200,
"stream": True,
"temperature": 0.1
},
headers={"Authorization": "Bearer your-api-key"}
) as response:
async for line in response.aiter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
if first_token_time is None:
first_token_time = time.time() - start
total_time = time.time() - start
return first_token_time or total_time, total_time
async def benchmark_vllm(
num_requests: int = 50,
concurrency: int = 10,
prompt: str = "请写一段关于人工智能的200字介绍:"
) -> dict:
"""并发压测 vLLM 服务"""
async with httpx.AsyncClient(timeout=180) as client:
# 预热
await send_request(client, "Hello")
# 正式测试
start = time.time()
semaphore = asyncio.Semaphore(concurrency)
async def bounded_request():
async with semaphore:
return await send_request(client, prompt)
tasks = [bounded_request() for _ in range(num_requests)]
results = await asyncio.gather(*tasks, return_exceptions=True)
elapsed = time.time() - start
# 过滤错误
valid = [(ttft, total) for r in results if isinstance(r, tuple) for ttft, total in [r]]
ttfts = [r[0] for r in valid]
totals = [r[1] for r in valid]
return {
"total_requests": num_requests,
"successful": len(valid),
"concurrency": concurrency,
"elapsed_seconds": round(elapsed, 2),
"throughput_rps": round(len(valid) / elapsed, 2),
"ttft_p50_ms": round(statistics.median(ttfts) * 1000, 0),
"ttft_p99_ms": round(sorted(ttfts)[int(len(ttfts) * 0.99)] * 1000, 0),
"total_p50_ms": round(statistics.median(totals) * 1000, 0),
"total_p99_ms": round(sorted(totals)[int(len(totals) * 0.99)] * 1000, 0),
}
# 运行测试
async def main():
print("=== vLLM 性能基准测试 ===\n")
for concurrency in [1, 5, 10, 20, 50]:
result = await benchmark_vllm(num_requests=concurrency * 5, concurrency=concurrency)
print(f"并发={concurrency:3d}: "
f"吞吐={result['throughput_rps']:5.1f} RPS, "
f"TTFT P50={result['ttft_p50_ms']:5.0f}ms, "
f"TTFT P99={result['ttft_p99_ms']:6.0f}ms")
asyncio.run(main())
45.5 并发压测(wrk / ab)
使用 wrk 压测
# 安装 wrk
apt-get install wrk # Ubuntu
brew install wrk # macOS
# 创建 wrk Lua 脚本(用于 POST 请求)
cat > vllm_wrk.lua << 'EOF'
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.headers["Authorization"] = "Bearer your-api-key"
wrk.body = [[{
"model": "NousResearch/Hermes-3-Llama-3.1-70B",
"prompt": "用一句话解释量子计算的核心原理。",
"max_tokens": 100,
"temperature": 0.1
}]]
EOF
# 运行压测
# 持续 30 秒,10 个线程,100 个并发连接
wrk -t 10 -c 100 -d 30s \
-s vllm_wrk.lua \
http://localhost:8000/v1/completions
# 典型输出:
# Running 30s test @ http://localhost:8000/v1/completions
# 10 threads and 100 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 2.45s 890.23ms 8.12s 78.34%
# Req/Sec 8.20 4.55 23.00 65.12%
# 2456 requests in 30.05s, 892.15KB read
# Requests/sec: 81.74
# Transfer/sec: 29.69KB
使用 ab 压测
# 创建请求体文件
cat > request_body.json << 'EOF'
{
"model": "NousResearch/Hermes-3-Llama-3.1-70B",
"prompt": "请写一个 Python 排序函数。",
"max_tokens": 200,
"temperature": 0.1
}
EOF
# 100 个请求,10 个并发
ab -n 100 -c 10 \
-p request_body.json \
-T "application/json" \
-H "Authorization: Bearer your-api-key" \
http://localhost:8000/v1/completions
监控 GPU 使用率
# 实时监控(每秒刷新)
watch -n 1 nvidia-smi
# 详细监控(所有指标)
nvidia-smi dmon -s pumcet -d 1
# 字段说明:
# p = power usage u = GPU utilization
# m = memory util c = SM clock
# e = temperature t = PCIe throughput
# Python 监控脚本
python3 << 'EOF'
import subprocess
import time
while True:
result = subprocess.run(
["nvidia-smi", "--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
"--format=csv,noheader,nounits"],
capture_output=True, text=True
)
for i, line in enumerate(result.stdout.strip().split("\n")):
util, mem_used, mem_total, temp = line.split(", ")
print(f"GPU {i}: 利用率={util}%, 显存={mem_used}/{mem_total}MB, 温度={temp}°C")
print()
time.sleep(2)
EOF
45.6 与 Hermes Agent 的集成
OpenAI 兼容接口集成
vLLM 提供 OpenAI 兼容 API,可直接替换 OpenAI 客户端:
# vllm_hermes_agent.py
from openai import AsyncOpenAI
import asyncio
# 直接使用 OpenAI SDK 连接 vLLM
client = AsyncOpenAI(
base_url="http://localhost:8000/v1",
api_key="your-api-key-or-dummy" # vLLM 可配置为不验证
)
async def chat_with_hermes_vllm(
messages: list[dict],
max_tokens: int = 2048,
temperature: float = 0.1
) -> str:
"""通过 vLLM OpenAI 接口调用 Hermes"""
response = await client.chat.completions.create(
model="NousResearch/Hermes-3-Llama-3.1-70B",
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
stream=False
)
return response.choices[0].message.content
async def streaming_chat(messages: list[dict]) -> str:
"""流式输出版本"""
full_content = ""
stream = await client.chat.completions.create(
model="NousResearch/Hermes-3-Llama-3.1-70B",
messages=messages,
max_tokens=2048,
temperature=0.1,
stream=True
)
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)
full_content += delta.content
print()
return full_content
# Hermes Agent 配置(切换后端只需修改 base_url)
HERMES_VLLM_CONFIG = {
"provider": "openai_compatible",
"base_url": "http://localhost:8000/v1",
"model": "NousResearch/Hermes-3-Llama-3.1-70B",
"api_key": "your-api-key",
"max_tokens": 4096,
"temperature": 0.1,
"context_window": 65536
}
负载均衡多实例
# nginx.conf — vLLM 多实例负载均衡
upstream vllm_hermes {
# 轮询(默认)
server 127.0.0.1:8000 weight=1;
server 127.0.0.1:8001 weight=1;
server 127.0.0.1:8002 weight=1;
# 或使用 least_conn(最少连接数,更适合长请求)
# least_conn;
keepalive 32; # 保持长连接
}
server {
listen 80;
location /v1/ {
proxy_pass http://vllm_hermes;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
# 流式输出不缓冲
proxy_buffering off;
proxy_cache off;
# 长超时(LLM 推理需要)
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
}
}
本章小结
vLLM 是生产环境 Hermes 部署的首选推理引擎:
| 特性 | vLLM 优势 |
|---|---|
| 并发吞吐 | PagedAttention 实现近无碎片内存,吞吐比传统方法高 10-30x |
| API 兼容性 | OpenAI 兼容,切换成本几乎为零 |
| 功能丰富 | Prefix Caching、量化、多卡并行开箱即用 |
| 监控 | 内置 Prometheus 指标,完善的可观测性 |
关键配置三要素:--max-num-seqs(并发上限)、--scheduler-delay-factor(批处理等待)、--gpu-memory-utilization(内存利用率)。
思考题
-
PagedAttention 中 Block 大小的选择(默认 16 tokens/块)对性能有什么影响?Block 太小或太大各有什么问题?
-
vLLM 的 Prefix Caching 在 Hermes Agent 场景中最能发挥价值的是哪种情况?请给出一个具体的业务场景,并估算缓存命中率。
-
在"吞吐量优先"和"延迟优先"两种配置下,当 GPU 利用率达到 95% 时,服务会出现什么行为差异?如何设计熔断机制来保护服务质量?