外观
渐进式教程:三版跑起来
一次加一项优化,是学推理优化最快的路径——每一步都能看见数字变化、能定位收益来源、能回退对比。本文把"从零部署一个推理服务"拆成六版,每版只动一处。
从零部署一个推理服务 给出的是 v1 → v2 → v3 的"大版"路线——每跨一版都引入一组优化,性能跃迁明显,但也带来理解盲区:你看到 v2 比 v1 快 30 倍,但不知道这 30 倍里 PagedAttention 贡献多少、continuous batching 贡献多少、FlashAttention 贡献多少。本文把同一模型(Llama-2-7B-Chat)的优化路径再拆细,每一版只加一项,给 diff-style 代码与基准数据,让你看清每一项优化的真实价值。
六版的地图:
v0 baseline HuggingFace transformers + bf16 + naive generate
│
▼
v1 + KV cache 开启 use_cache=True(其实默认就是开的,这里专门讲清楚)
│
▼
v2 + continuous batching vLLM 替换 transformers,引入 PagedAttention
│
▼
v3 + INT4 quantization AWQ 量化,权重 14GB → 4GB
│
▼
v4 + speculative decoding EAGLE-3 投机解码,小 batch 延迟减半
│
▼
v5 + Triton + autoscale 统一编排层、水平扩展、监控告警每一版对照基准是上一版——这是 推理基准测试实践 的"控制变量法"。
本文与 build-your-own 的关系
- 想看完整端到端项目、目录结构、上线流程:读 从零部署一个推理服务
- 想逐项理解每个优化的贡献与配置:读本文
- 两者用同一模型、同一硬件、同一测试方法,数字可以互相对照
一、v0:baseline
v0 的目标是建立"什么优化都没做"的对照基线。所有后续收益都从这里算起。
1. 代码
python
# v0_baseline.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "meta-llama/Llama-2-7b-chat-hf"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(
MODEL,
torch_dtype=torch.bfloat16,
device_map="auto",
)
model.eval()
def generate(prompt, max_new_tokens=256):
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.inference_mode():
out = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False, # greedy,便于复现
pad_token_id=tokenizer.eos_token_id,
)
return tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(generate("用三句话解释 PagedAttention。"))2. 基准
A100 40GB,单请求,prompt 50 tokens / output 256 tokens:
| 指标 | v0 数值 |
|---|---|
| TTFT (ms) | 850 |
| TPOT (ms/tok) | 40 |
| 端到端 (s) | 11.1 |
| 显存峰值 (GB) | 14.6 |
| 吞吐 (tok/s) | 25 |
这就是基线。下面每一版都和它比。
v0 故意没用的事
v0 故意没用 vLLM、没用 FlashAttention、没用 PagedAttention。HuggingFace transformers 默认 use_cache=True,所以 v0 实际上已经有 KV cache 复用——这一点要记牢,否则 v1 的"开 KV cache"会让你迷惑:明明默认就开了,怎么还能"开"?
答:v1 讲的是 use_cache 在不同实现里的差异——transformers 的 KV cache 是 list-of-tensor,每层独立、布局不连续;vLLM 的是 PagedAttention 的 paged 布局。这才是 v1 的真问题。
二、v1:KV cache 优化与 FlashAttention
v1 的目标:把 KV cache 的实现换成 FlashAttention2 + PagedAttention 的"前身"——flash_attn 与 xformers 的优化 attention kernel。这个版本不上 vLLM,只换 attention kernel。
1. 代码 diff
python
# v1_kv_flash.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "meta-llama/Llama-2-7b-chat-hf"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(
MODEL,
torch_dtype=torch.bfloat16,
device_map="auto",
attn_implementation="flash_attention_2", # ← v1 的核心改动
)
model.eval()attn_implementation="flash_attention_2" 是 transformers 4.36+ 引入的接口,把 attention 计算从 PyTorch naive 实现切换到 FlashAttention2 kernel。详见 算子融合与自定义核。
2. 基准
| 指标 | v0 | v1 | v1 相对 v0 |
|---|---|---|---|
| TTFT (ms) | 850 | 600 | -29% |
| TPOT (ms/tok) | 40 | 28 | -30% |
| 端到端 (s) | 11.1 | 7.8 | -30% |
| 显存峰值 (GB) | 14.6 | 13.5 | -8% |
单换 attention kernel 就拿到 30% 加速——这是 LLM 推理 memory-bound 本质的体现:换一个访存更小的 kernel,性能立刻提升。详见 显存层次与带宽墙 与 Roofline 模型与算力分析。
为什么显存也降了
FlashAttention2 把 attention 中间矩阵([batch, head, seq, seq])的存储从 HBM 里"溶解"到 SRAM,不需要在 HBM 里开 O(seq²) 的临时空间——显存峰值随之下降。这是 算子融合与自定义核 中"算子融合省访存"的典型例子。
三、v2:continuous batching(vLLM 替换)
v1 还在 transformers 框架里——generate 接口本质是"batch=1 串行处理"。v2 用 vLLM 替换整个推理栈,引入 PagedAttention 与 continuous batching。
1. 代码 diff
bash
# v2_vllm.sh
vllm serve meta-llama/Llama-2-7b-chat-hf \
--dtype bfloat16 \
--max-model-len 4096 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.9 \
--port 8000客户端从 Python 直接调用变成 HTTP:
python
# v2_client.py
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
resp = client.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
prompt="用三句话解释 PagedAttention。",
max_tokens=256,
temperature=0,
)
print(resp.choices[0].text)2. 基准
| 指标 | v1 | v2 单请求 | v2 32 并发 |
|---|---|---|---|
| TTFT (ms) | 600 | 200 | 250 |
| TPOT (ms/tok) | 28 | 20 | 22 |
| 端到端 (s) | 7.8 | 5.3 | 6.0 |
| 显存峰值 (GB) | 13.5 | 18 | 36 |
| 吞吐 (tok/s) | 36 | 50 | 1100 |
单请求提升不夸张(1.4×)——因为 vLLM 的 PagedAttention 单请求场景优势不大。真正爆发在并发:32 并发下吞吐 1100 tok/s,相当于 v1 单请求的 30 倍。这是 批处理与请求调度 的核心收益。
显存为什么涨这么多
v2 显存从 13.5 GB 涨到 36 GB——因为 vLLM 把 90% 显存都开成 KV pool,准备接更多并发。单请求测试时这 18 GB 的 KV pool 大半是闲置的,但有了它才能在并发来时零延迟接入新请求。这是 throughput-oriented 的设计哲学,与 v1 的 latency-oriented 完全不同。详见 部署设计原则 的"延迟优先 vs 吞吐优先先选一个"。
四、v3:INT4 量化
v2 已经把吞吐拉满,但显存吃紧——单张 A100 40GB 跑 7B 模型 KV pool 占了 22 GB,并发上限被显存卡死。v3 用 AWQ INT4 量化把权重从 14 GB 压到 4 GB,腾出的 10 GB 全给 KV cache,能接更多并发。
1. 代码 diff
bash
# v3_awq.sh
vllm serve ./Llama-2-7b-chat-hf-awq \
--quantization awq \
--dtype float16 \
--max-model-len 4096 \
--max-num-seqs 384 \ # ← 提高并发上限,因为显存腾出来了
--gpu-memory-utilization 0.9 \
--port 80002. 基准
| 指标 | v2 | v3 | v3 相对 v2 |
|---|---|---|---|
| 权重显存 (GB) | 14 | 4 | -71% |
| 单请求 TPOT (ms/tok) | 20 | 14 | -30% |
| 32 并发吞吐 (tok/s) | 1100 | 1700 | +55% |
| 64 并发吞吐 (tok/s) | 720(开始排队) | 2100 | +190% |
| 模型质量 (MMLU) | 45.3 | 44.8 | -0.5 |
v3 在低并发下提升不显著(30%),高并发场景下性能优势放大——因为量化后访存减少,KV pool 更大,能接的并发更多。这是 memory-bound 工作负载的典型特征。
为什么 TPOT 也降了
v2 在 32 并发时 TPOT 反而比单请求高(22 vs 20)——因为 KV cache 显存不够,部分请求的 KV 被换出/重算。v3 量化后腾出显存,KV pool 充裕,TPOT 自然下来。这是 调参与性能调优 中"gpu_memory_utilization 与 max_num_seqs 的平衡"的典型案例。
五、v4:投机解码
v3 的吞吐已经很强,但单请求延迟还卡在 14 ms/tok。对于"一个人在等结果"的场景(聊天、代码补全),吞吐不重要,TPOT 才是命门。v4 引入 EAGLE-3 投机解码,用小模型猜 token、大模型一次验证,把 TPOT 砍半。
1. 代码 diff
bash
# v4_speculative.sh
vllm serve ./Llama-2-7b-chat-hf-awq \
--quantization awq \
--speculative-model "lmsys/Falcon-EAGLE-3-7B" \
--num-speculative-tokens 5 \
--speculative-draft-tensor-parallel-size 1 \
--max-model-len 4096 \
--max-num-seqs 16 \ # ← 故意调小,见下方解释
--port 80002. 基准
| 指标 | v3 | v4 单请求 | v4 16 并发 | v4 64 并发 |
|---|---|---|---|---|
| 单请求 TPOT (ms/tok) | 14 | 6 | 7 | 12 |
| 单请求端到端 (s) | 3.6 | 1.6 | 1.8 | 3.1 |
| 16 并发吞吐 (tok/s) | 700 | 700 | 2200 | - |
| 64 并发吞吐 (tok/s) | 2100 | - | - | 1800(反而降) |
| 显存峰值 (GB) | 22 | 36(多 draft 模型) | 36 | 36 |
v4 在小 batch 下 TPOT 直接砍半(14 → 6),吞吐同步翻倍。但大 batch(64 并发)时性能反而退化——因为大 batch 下大模型本身已被算力喂满,投机解码徒增显存与计算。这是 投机解码与 Medusa/EAGLE 的核心权衡。
投机解码的黄金区间
v4 在并发 ≤ 16 时收益最大,并发 32 是盈亏平衡点,超过 64 是反向收益。生产部署时要么用路由把单请求/低并发流量导到 v4,要么用动态 batch 上限保护 v4——不能让 64 并发灌进 v4 实例。详见 调参与性能调优 与 常见陷阱与反模式。
六、v5:Triton + autoscale
v0–v4 都是"单实例跑得快",但生产环境还差最后一公里:水平扩展、多模型共存、灰度发布、监控告警。v5 用 Triton Inference Server 做统一编排层,外加 K8s HPA 做 autoscale。
1. 架构 diff
┌──────────────────────────────────┐
│ Load Balancer (LB) │
└──────────────┬───────────────────┘
▼
┌──────────────────────────────────┐
│ K8s HPA (基于 QPS/延迟扩缩) │
└──────────────┬───────────────────┘
▼
┌─────────────┬─────────────┼─────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
Triton-1 Triton-2 Triton-3 Triton-4 Triton-N
(vLLM+AWQ (vLLM+AWQ (vLLM+AWQ (vLLM+AWQ (...)
+EAGLE) +EAGLE) +EAGLE) +EAGLE)
│ │ │ │
└─────────────┴─────────────┴─────────────┘
│
▼
/metrics (Prometheus)2. 配置 diff
model_repository/llama2-7b-awq/config.pbtxt:
name: "llama2-7b-awq"
backend: "vllm" # vLLM 0.6+ 原生 Triton backend
max_batch_size: 16
dynamic_batching {
preferred_batch_size: [ 4, 8, 16 ]
max_queue_delay_microseconds: 50000
}
instance_group [
{ kind: KIND_AUTO, count: 1, name: "vllm" }
]
parameters {
key: "model"
value: { string_value: "/models/llama2-7b-chat-hf-awq" }
}
parameters {
key: "quantization"
value: { string_value: "awq" }
}K8s HPA(基于 QPS 与 p99 延迟扩缩):
yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: triton-vllm-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: triton-vllm
minReplicas: 2
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: vllm_requests_per_second
target:
type: AverageValue
averageValue: "20" # 每实例 20 QPS 触发扩容
- type: Pods
pods:
metric:
name: vllm_p99_latency_ms
target:
type: AverageValue
averageValue: "3000" # p99 超 3s 触发扩容3. 基准
| 指标 | v4 单实例 | v5 三实例 | v5 十实例 |
|---|---|---|---|
| 单实例 QPS 上限 | 20 | 20 | 20 |
| 集群 QPS 上限 | 20 | 60 | 200 |
| 集群 p99 延迟 (s) | 1.8 | 1.8 | 2.1(略升) |
| 单实例故障影响 | 全站宕 | 部分请求 5xx | 无感(流量重试) |
v5 的收益不在"单实例更快",而在水平可扩展、故障可恢复。这是从"跑得快的模型"到"可运维的服务"的质变,详见 模型服务化与编排 与 Triton 推理服务。
HPA 阈值怎么设
- 单实例 QPS 上限:先压测一次得到(本文是 20),HPA 触发线设为上限的 70%(14 QPS 触发扩容)
- p99 延迟阈值:业务 SLA 的 60%(业务 SLA 5s → HPA 3s 触发扩容)
- 扩缩容冷却时间:默认 5 分钟太长,推理服务建议 1–2 分钟——请求峰值来得快走得也快
详见 调参与性能调优 的"autoscale 调参"小节。
七、六版收益全景
把六版的指标汇总到一张表(A100 40GB,Llama-2-7B-Chat,单请求 prompt 50 / output 256,并发 32):
| 版本 | 关键优化 | TTFT (ms) | TPOT (ms) | 32 并发吞吐 (tok/s) | 显存 (GB) | 模型质量 |
|---|---|---|---|---|---|---|
| v0 | baseline | 850 | 40 | ≈ 25 | 14.6 | 45.3 |
| v1 | +FlashAttention2 | 600 | 28 | ≈ 36 | 13.5 | 45.3 |
| v2 | +PagedAttention + 连续批 | 200 | 20 | 1100 | 36 | 45.3 |
| v3 | +AWQ INT4 | 180 | 14 | 1700 | 22 | 44.8 |
| v4 | +EAGLE 投机解码 | 150 | 6 | 2200(16 并发) | 36 | 44.8 |
| v5 | +Triton + autoscale | 200 | 22 | 集群级水平扩展 | 22×N | 44.8 |
每个版本相对上一版的增量收益:
v0 → v1: FlashAttention2 +30% (换 kernel,最小代价)
v1 → v2: PagedAttention+CB +30× (架构跃迁,单 batch → 连续批)
v2 → v3: AWQ INT4 +55% (量化压缩,省显存换并发)
v3 → v4: EAGLE 投机解码 +95% (小 batch 才有效,大 batch 反退化)
v4 → v5: Triton + autoscale 无单实例收益,但 QPS 可水平扩展到 10×八、动手顺序建议
按本文顺序从 v0 跑到 v5,是最稳的学习路径:
- v0 + v1:半天内完成,掌握 transformers + FlashAttention 的基本盘
- v2:花一天时间上 vLLM,理解 PagedAttention 与 continuous batching——这是 批处理与请求调度 的核心
- v3:花半天跑 AWQ,理解 模型量化基础 的 trade-off
- v4:花半天跑 EAGLE,理解 投机解码 的 batch 依赖
- v5:花两三天上 Triton + K8s,理解 模型服务化与编排
跳过 v2 直接上 v3 的反模式
见过团队一上来就量化 + 投机解码,跳过 vLLM——结果显存预算乱算、KV pool 配错、性能不及格。v2 的 PagedAttention 是 v3/v4 的前置基础:没有 KV cache 的统一管理,量化省下的显存也用不到 KV pool 上。按顺序上,每一步收益都能复现。
九、延伸阅读
- 从零部署一个推理服务 —— 本文的"大版"对照,每版跨一组优化
- 推理基准测试实践 —— 本文基准数据的方法论
- 调参与性能调优 —— v2–v5 各参数的完整调参清单
- 部署设计原则 —— 跨版本的 12 条纪律
- 常见陷阱与反模式 —— 每一版都可能踩到的坑
- vLLM 与 PagedAttention —— v2 的引擎深入
- 投机解码与 Medusa/EAGLE —— v4 的引擎深入
- Triton 推理服务 —— v5 的编排层深入
- 模型量化基础 —— v3 的原理
- 批处理与请求调度 —— v2 的核心机制
参考资料
- vLLM Documentation: Quickstart —— v2 启动 CLI
- vLLM Documentation: Quantization —— v3 AWQ 配置
- vLLM Documentation: Speculative Decoding —— v4 EAGLE 配置
- HuggingFace Transformers: FlashAttention2 —— v1 attn_implementation 参数
- AutoAWQ: Quickstart —— v3 量化脚本
- Triton Inference Server: vLLM Backend —— v5 Triton 配置
- Kubernetes HPA Documentation —— v5 autoscale
- EAGLE-3: Speculative Decoding —— v4 draft model