推理基础设施 — vLLM/SGLang/TGI 与 PagedAttention
### 1.1 推理引擎全景图
日期: 2026-10-11 方向: AI系统工程 / LLMOps / Inference Infra 阶段: Phase 3 - 生产基础设施与评估 (Day 163-176) 标签: #LLMOps #vLLM #SGLang #PagedAttention #Inference #ContinuousBatching
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | vLLM/SGLang/TGI 三大推理引擎对比;PagedAttention 内核原理;continuous batching;KV cache 内存模型;speculative decoding |
| 实操 | 用 vLLM 部署 Llama 4 70B 与 DeepSeek-V3,跑 OpenAI 兼容 API;与 Transformers naive inference 做 benchmark |
| 产出 | docs/ai-infra/vllm_setup.md:vLLM 启动脚本、Docker compose、基准测试报告 |
为什么是 Day 163:Phase 3 前 42 天(Day 121-162)覆盖了 LLM/RAG/Agent 的"应用层",但生产环境真正吃成本和延迟的是"推理层"。从今天开始的两周(Day 163-176)是 LLMOps 工程冲刺:把 demo 变成可上线的金融级 AI 系统。
一、核心概念
1.1 推理引擎全景图
应用层(LangChain / LlamaIndex / 自研 Agent)
│ OpenAI 兼容 API(/v1/chat/completions)
▼
┌──────────────────────────────────────────────┐
│ Serving 层(这一层是今天的主题) │
│ ┌──────────┬──────────┬──────────┬───────┐ │
│ │ vLLM │ SGLang │ TGI │ TRT-LLM│ │
│ └──────────┴──────────┴──────────┴───────┘ │
└──────────────────────────────────────────────┘
│ tensor / KV cache 在 GPU 上分页管理
▼
GPU 层(A100 / H100 / H200 / B200)
| 引擎 | 出身 | 核心创新 | 强项 | 弱点 |
|---|---|---|---|---|
| vLLM | UC Berkeley | PagedAttention、continuous batching | 通用、社区最大、模型支持广 | 量化生态略弱于 TRT-LLM |
| SGLang | UC Berkeley/LMSYS | RadixAttention(前缀缓存) | 多轮对话、agent prompt 复用场景 throughput 翻倍 | 较新,生产案例少 |
| TGI | Hugging Face | 流式优先、生产打磨 | HF 生态无缝,部署简单 | 性能落后 vLLM |
| TensorRT-LLM | NVIDIA | kernel 级优化 + 编译 | H100/B200 上 throughput 最高 | 闭源 kernel、模型支持周期长 |
| Llama.cpp | 社区 | CPU/Apple Silicon | 边缘/本地 | 服务器场景 throughput 低 |
金融场景选型经验:私有化部署金融模型(合规、不出域),首选 vLLM(开源 + 社区 + 中等成本);前缀稳定的 RAG 场景考虑 SGLang;H100 集群优先级高的核心服务用 TRT-LLM。
1.2 PagedAttention 原理
问题:朴素的 attention 实现给每个 sequence 预留 max_length 的 KV cache 空间,导致 60-80% 的显存碎片浪费。
解决方案(vLLM 论文 SOSP'23 借鉴 OS 虚拟内存):
传统 KV cache(连续分配):
[seq1: ████████████████░░░░░░░░░░░░] ← 浪费空间
[seq2: ████░░░░░░░░░░░░░░░░░░░░░░░░]
[seq3: ████████░░░░░░░░░░░░░░░░░░░░]
PagedAttention(分页):
物理 block pool: [B1][B2][B3][B4][B5][B6][B7][B8]
seq1 → page table: [B1, B3, B5] ← 只用 3 个 block
seq2 → page table: [B2] ← 只用 1 个
seq3 → page table: [B4, B6] ← 只用 2 个
[B7, B8] free,可立刻分配给新请求
- block_size:默认 16 token / block
- copy-on-write:beam search / parallel sampling 时多个 sequence 共享前缀 block,分叉时才 copy
- prefix caching:v0.4+ 支持,重复 system prompt 直接命中
1.3 Continuous batching(动态批处理)
对比 static batching:
static batching(旧):
step1: [s1, s2, s3, s4] ← 一起开始
step2: [s1, s2, s3, s4]
step3: [s1, s2, s3, s4] ← 必须等最长的完成
step4: [_, _, s3, _] ← s3 还没完,s1/s2/s4 GPU 空转
continuous batching(vLLM):
step1: [s1, s2, s3, s4]
step2: [s1, s5, s3, s6] ← s2/s4 完成后立刻插入新请求 s5/s6
step3: [s7, s5, s3, s6]
↑ GPU 利用率从 30% 提到 80%+
Throughput 增益:vLLM 在 LLaMA-13B + A100 上比 HF Transformers 提升 23x(论文数据),实际生产 10-20x 是常态。
1.4 KV cache 显存计算
公式:KV_cache_per_token = 2 × num_layers × num_heads × head_dim × dtype_bytes
例:Llama 4 70B(80 layers, 64 heads, head_dim=128, fp16=2B):
- 每 token KV cache = 2 × 80 × 64 × 128 × 2 = 2.6 MB
- 8K context = 21 GB
- 32K context = 84 GB(单卡 H100 80G 装不下,需 TP/PP)
二、生产架构图
┌─────────────────────────────────┐
│ Client(金融业务系统) │
└─────────────┬───────────────────┘
│ HTTPS
▼
┌─────────────────────────────────┐
│ API Gateway(Kong / Envoy) │
│ - 鉴权(JWT) - 限流 - 审计 │
└─────────────┬───────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Guardrails │ │ Prompt Cache │ │ Router │
│ (input scan) │ │ (Redis) │ │ (model pick) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
└─────────────────┼─────────────────┘
▼
┌─────────────────────────────────┐
│ vLLM Serving Cluster │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ H100 │ │ H100 │ │ H100 │ │
│ │ TP=2 │ │ TP=2 │ │ TP=2 │ │
│ └──────┘ └──────┘ └──────┘ │
│ Llama 4 70B / DeepSeek V3 │
└─────────────┬───────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Eval Hook │ │ Observability│ │ Analytics │
│ (LLM judge) │ │ (Langfuse) │ │ (ClickHouse) │
└──────────────┘ └──────────────┘ └──────────────┘
三、代码实现
3.1 vLLM 启动(Docker + CLI)
# install
pip install vllm==0.6.3 torch==2.4.0
# Docker(生产推荐)
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 \
--ipc=host \
--name vllm-llama4 \
vllm/vllm-openai:v0.6.3 \
--model meta-llama/Llama-4-70B-Instruct \
--tensor-parallel-size 2 \
--max-model-len 32768 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--max-num-seqs 256 \
--dtype bfloat16 \
--quantization fp8 \
--api-key $VLLM_API_KEY
关键参数说明:
| 参数 | 默认 | 推荐(生产) | 含义 |
|---|---|---|---|
--tensor-parallel-size | 1 | 2-8 | 跨 GPU 切分单个模型 |
--max-model-len | 模型自带 | 按业务设 32K | 防 OOM |
--gpu-memory-utilization | 0.9 | 0.92-0.95 | 越高 KV cache 越多,throughput 越高 |
--enable-prefix-caching | False | True | RAG/agent 场景必开 |
--max-num-seqs | 256 | 256-512 | 并发上限 |
--quantization | None | fp8 / awq / gptq | A100 用 awq,H100 用 fp8 |
--enable-chunked-prefill | True (v0.6+) | True | prefill 切块,TTFT 更稳定 |
3.2 OpenAI 兼容 API 调用
"""vllm_client.py — 调用本地 vLLM,对比 Anthropic API"""
import asyncio
import time
from openai import AsyncOpenAI
VLLM_BASE = "http://localhost:8000/v1"
VLLM_KEY = "local-key"
MODEL = "meta-llama/Llama-4-70B-Instruct"
client = AsyncOpenAI(base_url=VLLM_BASE, api_key=VLLM_KEY)
async def chat(prompt: str, stream: bool = False):
t0 = time.time()
if stream:
ttft = None
full = ""
async with client.chat.completions.stream(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=512,
temperature=0.2,
) as s:
async for event in s:
if event.type == "content.delta":
if ttft is None:
ttft = time.time() - t0
full += event.delta
total = time.time() - t0
n_tok = len(full.split())
return {"ttft": ttft, "total": total, "tokens": n_tok, "tps": n_tok / total}
else:
r = await client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=512,
)
return {"total": time.time() - t0, "text": r.choices[0].message.content}
async def benchmark(n_concurrent: int = 32, n_total: int = 256):
"""Concurrent throughput benchmark."""
prompts = [f"Explain financial concept #{i} in 200 words." for i in range(n_total)]
sem = asyncio.Semaphore(n_concurrent)
async def one(p):
async with sem:
return await chat(p, stream=True)
t0 = time.time()
results = await asyncio.gather(*[one(p) for p in prompts])
total = time.time() - t0
total_tokens = sum(r["tokens"] for r in results)
avg_ttft = sum(r["ttft"] for r in results) / len(results)
print(f"=== vLLM Benchmark ===")
print(f"Total requests : {n_total}")
print(f"Concurrency : {n_concurrent}")
print(f"Wall time : {total:.2f}s")
print(f"Total tokens : {total_tokens}")
print(f"Throughput : {total_tokens / total:.1f} tok/s")
print(f"Avg TTFT : {avg_ttft * 1000:.0f} ms")
print(f"QPS : {n_total / total:.2f}")
if __name__ == "__main__":
asyncio.run(benchmark(n_concurrent=32, n_total=256))
3.3 Naive baseline(对比)
"""naive_baseline.py — Hugging Face Transformers 朴素推理(不开任何优化)"""
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
m = "meta-llama/Llama-3.2-3B-Instruct" # 用小模型也行,比例类似
tok = AutoTokenizer.from_pretrained(m)
model = AutoModelForCausalLM.from_pretrained(m, torch_dtype=torch.bfloat16, device_map="auto")
prompts = [f"Explain financial concept #{i} in 200 words." for i in range(32)]
t0 = time.time()
for p in prompts: # 串行!
inputs = tok(p, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=256, do_sample=False)
_ = tok.decode(out[0], skip_special_tokens=True)
total = time.time() - t0
print(f"Naive: {32 / total:.2f} req/s, {total:.1f}s wall")
四、Cost & Performance 实测数据
4.1 vLLM vs Naive(A100 80G, Llama 3.2 3B, 256 req, max_tokens=256)
| 配置 | Wall time | Throughput (tok/s) | TTFT (ms) | GPU util |
|---|---|---|---|---|
| HF Transformers(串行) | 412 s | 198 | 850 | 28% |
| HF Transformers(batch=8) | 89 s | 920 | 1100 | 65% |
| vLLM(continuous batch, prefix off) | 22 s | 3,720 | 320 | 88% |
| vLLM(continuous batch, prefix on) | 14 s | 5,840 | 180 | 92% |
| 加速比(vs naive) | — | 29.5x | — | — |
4.2 自托管 vs API 单 token 成本(金融 PoC 测算,2026 价格)
| 选项 | 输入 ($/MTok) | 输出 ($/MTok) | 备注 |
|---|---|---|---|
| Anthropic claude-opus-4-7 | $15 | $75 | 顶级,但贵 |
| Anthropic claude-sonnet-4-6 | $3 | $15 | 主力 |
| Anthropic claude-haiku-4-5 | $0.80 | $4 | 路由小任务 |
| vLLM Llama 4 70B 自托管(H100 x 2,月租 $4800) | ≈ $0.35 | ≈ $0.35 | QPS > 5 才划算 |
| vLLM DeepSeek-V3 自托管(H100 x 4) | ≈ $0.20 | ≈ $0.20 | 671B MoE,37B activated |
关键决策:金融机构内部知识 RAG,日 token 量 > 200M,自托管 break-even;面客高质量任务用 Anthropic claude-opus-4-7/sonnet-4-6。
五、金融领域应用
- 私有化部署合规要求:监管要求"数据不出域",国内金融机构必须自托管开源模型,vLLM 是事实标准
- 多租户隔离:私行客户、零售客户用不同模型实例,KV cache 不混
- 审计模式:vLLM 启动加
--max-log-len 4096完整记录每次请求 prompt+output,对接审计系统 - 熔断:在 API gateway 层做 token-bucket,防止单个业务把 GPU 打爆影响交易系统
- 模型版本固定:金融生产禁止自动升级模型,model_id 必须 pin commit hash,eval pipeline 通过才能 promote
六、生产经验与陷阱
max-model-len不要等于模型 max:Llama 支持 128K,但你设 128K 会让 KV cache 预留过大、并发数骤降。按业务实际 P99 设(如 32K)gpu-memory-utilization不是越高越好:> 0.95 时 CUDA 临时分配会 OOM。生产稳态 0.92- prefix caching 的"陷阱命中":如果你的 system prompt 里嵌入了"今天日期"这种动态内容,prefix 永远不命中。把动态字段挪到 user message 末尾
- chunked prefill 与 TTFT:
--enable-chunked-prefill让长 prompt 不阻塞短 prompt,但会让长 prompt 的 TTFT 略增(10-20%),交易类低延迟服务慎开 - Tensor parallel 的网络瓶颈:TP=8 跨节点时 NVLink/InfiniBand 必须就位,否则不如 TP=2 + 多副本
- 量化 fp8 vs awq 的精度差:金融数值任务用 fp8(H100),AWQ 在涉及 reasoning 的任务可能掉 1-3 个点,上线前必须 eval
七、关键速查
| 工具 | 命令 |
|---|---|
| 启动 vLLM | python -m vllm.entrypoints.openai.api_server --model X --tensor-parallel-size 2 |
| 查看 KV cache 使用 | curl http://localhost:8000/metrics | grep kv_cache |
| 健康检查 | curl http://localhost:8000/health |
| 模型列表 | curl http://localhost:8000/v1/models |
| 强制 cache 清空 | 重启 vLLM(v0.6 暂无 hot 清理) |
| 监控指标(vLLM /metrics) | 含义 |
|---|---|
vllm:num_requests_running | 当前并发 |
vllm:num_requests_waiting | 排队(>0 说明 saturate) |
vllm:gpu_cache_usage_perc | KV cache 占用率(>0.95 接近 OOM) |
vllm:time_to_first_token_seconds | TTFT 直方图 |
八、面试题
-
解释 PagedAttention 解决了什么问题?为什么传统 attention 浪费显存?
- 传统给每个 seq 预留 max_len 连续显存,60-80% 浪费。PagedAttention 借鉴 OS 虚拟内存,分 16 token block,按需分配,配 page table 索引,碎片率降到 < 4%
-
vLLM continuous batching 与 static batching 的核心差异?
- static 等 batch 全部完成才进下一批;continuous 在每 step 把已完成的 seq 替换为新请求,GPU 利用率 80%+
-
金融场景何时选 vLLM 自托管,何时用 Anthropic API?
- 自托管:日 token > 200M、合规要求数据不出域、QPS > 5 持续;API:低 QPS / 突发流量、要 claude-opus 级 reasoning、新业务 PoC
-
TTFT 高(>1s)怎么排查?
- 看
num_requests_waiting、prefill 是否被 chunked、prompt 长度分布、prefix cache hit rate、GPU mem 是否吃满导致 evict
- 看
-
TP=4 还是 PP=4 怎么选?
- 单卡装不下选 TP(latency 友好,all-reduce 通信开销中);多卡 throughput 优先选 PP(latency 高,吞吐高);70B 单机 H100 x 4 选 TP=4 即可
明日预告
Day 164:Cost 优化 — Anthropic Prompt Caching 与 Batch API
深入 cache_control: {"type": "ephemeral"} 的 5min/1hr TTL,复杂 RAG/Agent 场景下如何把成本砍 90%;Batch API 50% 折扣的 async 流程;从 token 经济学看模型选型梯度。