Skip to content

渐进式教程:三版跑起来

本页速览 把 v1→v2→v3 的部署优化拆成六版(v0 baseline → v1 KV cache → v2 continuous batching → v3 INT4 → v4 speculative decoding → v5 Triton + autoscale),每一版只加一项优化,给出 diff-style 代码与对应基准数据,便于逐项学习。

渐进式教程:三版跑起来

一次加一项优化,是学推理优化最快的路径——每一步都能看见数字变化、能定位收益来源、能回退对比。本文把"从零部署一个推理服务"拆成六版,每版只动一处。

从零部署一个推理服务 给出的是 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_attnxformers 的优化 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. 基准

指标v0v1v1 相对 v0
TTFT (ms)850600-29%
TPOT (ms/tok)4028-30%
端到端 (s)11.17.8-30%
显存峰值 (GB)14.613.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. 基准

指标v1v2 单请求v2 32 并发
TTFT (ms)600200250
TPOT (ms/tok)282022
端到端 (s)7.85.36.0
显存峰值 (GB)13.51836
吞吐 (tok/s)36501100

单请求提升不夸张(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 8000

2. 基准

指标v2v3v3 相对 v2
权重显存 (GB)144-71%
单请求 TPOT (ms/tok)2014-30%
32 并发吞吐 (tok/s)11001700+55%
64 并发吞吐 (tok/s)720(开始排队)2100+190%
模型质量 (MMLU)45.344.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 8000

2. 基准

指标v3v4 单请求v4 16 并发v4 64 并发
单请求 TPOT (ms/tok)146712
单请求端到端 (s)3.61.61.83.1
16 并发吞吐 (tok/s)7007002200-
64 并发吞吐 (tok/s)2100--1800(反而降)
显存峰值 (GB)2236(多 draft 模型)3636

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 上限202020
集群 QPS 上限2060200
集群 p99 延迟 (s)1.81.82.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)模型质量
v0baseline85040≈ 2514.645.3
v1+FlashAttention260028≈ 3613.545.3
v2+PagedAttention + 连续批2002011003645.3
v3+AWQ INT41801417002244.8
v4+EAGLE 投机解码15062200(16 并发)3644.8
v5+Triton + autoscale20022集群级水平扩展22×N44.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,是最稳的学习路径:

  1. v0 + v1:半天内完成,掌握 transformers + FlashAttention 的基本盘
  2. v2:花一天时间上 vLLM,理解 PagedAttention 与 continuous batching——这是 批处理与请求调度 的核心
  3. v3:花半天跑 AWQ,理解 模型量化基础 的 trade-off
  4. v4:花半天跑 EAGLE,理解 投机解码 的 batch 依赖
  5. v5:花两三天上 Triton + K8s,理解 模型服务化与编排

跳过 v2 直接上 v3 的反模式

见过团队一上来就量化 + 投机解码,跳过 vLLM——结果显存预算乱算、KV pool 配错、性能不及格。v2 的 PagedAttention 是 v3/v4 的前置基础:没有 KV cache 的统一管理,量化省下的显存也用不到 KV pool 上。按顺序上,每一步收益都能复现。

九、延伸阅读

参考资料