Bonus 3:LLM 推理优化全景手册
推理才是 LLM 应用真正的成本中心。训练是一次性的,推理是 7×24 每个请求都触发;产品体感 TTFT 200ms 还是 2000ms,决定用户留不留。裸跑 Hugging Face Transformers model.generate() 在 A800 上单流约 30 tok/s;vLLM / SGLang 这类专用引擎可以做到 500+ tok/s,显存利用率 95%+。
整篇围绕「同一张卡,怎么把 LLM 推理压到工程基线」展开:框架选型、量化、KV cache、speculative decoding,外加一组实测 benchmark 兜底。训练优化决定能不能做出来,推理优化决定养不养得起。
| 维度 | 训练 | 推理 |
|---|---|---|
| 频率 | 一次性 / 偶发 | 每个请求都触发 |
| 成本 | 主要是工程师工资 + 一次性 GPU | 每月 7×24 GPU 账单 |
| 体感 | 用户无感 | TTFT 200ms vs 2000ms 直接决定体验 |
| 瓶颈 | 算力(FLOPs) | 显存(KV cache)+ 带宽(memory-bound) |
1. 主流推理框架全景
1.1 八大引擎对比
| 引擎 | 出品方 | 核心技术 | 长板 | 短板 |
|---|---|---|---|---|
| vLLM | UC Berkeley | PagedAttention + Continuous Batching | 生态最大,易上手,功能全 | 极端 latency 不如专精引擎 |
| SGLang | LMSYS / Berkeley | RadixAttention(自动 prefix cache)+ 协程调度 | prompt 复用场景吞吐 vLLM 1.5-2× | 生态较新,功能在追赶 |
| TensorRT-LLM | NVIDIA | 编译期 graph 优化 + CUDA kernel 融合 + INT4/FP8 | 绝对极致 latency,NVIDIA 自家硬件最优 | 闭源 kernel,Triton 部署复杂 |
| TGI | Hugging Face | continuous batching + flash attention | 与 HF Hub 无缝 | 性能落后 vLLM 一截 |
| LMDeploy | OpenMMLab | TurboMind kernel + AWQ + W4A16 | 国产模型(Qwen/InternLM)适配最好 | 多模态支持一般 |
| DeepSpeed-MII | Microsoft | DeepSpeed inference engine | 与 DeepSpeed 训练栈一体 | 体验不如 vLLM |
| Llama.cpp | ggerganov | GGUF + CPU/Apple Silicon | 端侧 / CPU 推理王者 | GPU 性能远不及 vLLM |
| Ollama | 基于 llama.cpp | 一行命令,本地 model 管理 | 个人电脑零门槛 | 不适合 serving |
1.2 选型决策树
追求极致 latency, 全 NVIDIA + 愿意搞 Triton → TensorRT-LLM
通用 LLM serving, 中文 + 国产模型为主 → LMDeploy
通用 LLM serving, 国际生态 → vLLM(默认)
prompt 大量复用 (RAG / agent) → SGLang
端侧 / 笔记本 / CPU → llama.cpp / Ollama
HF Hub 强绑定 → TGI
实战默认 vLLM 起步,把指标榨干后再考虑切 SGLang / TensorRT-LLM。
2. 实测:vLLM Concurrency Benchmark
测试环境:A800-SXM4-40GB · vLLM 0.6.5 · Qwen2.5-3B-Instruct(BF16)· max_model_len=2048 · gpu-memory-utilization=0.85。
2.1 测试代码
固定 8 条不同 prompts,流式 stream=True,max_tokens=128,按并发档位采集每请求 TTFT + TPOT:
import asyncio, time
from openai import AsyncOpenAI
client = AsyncOpenAI(base_url="http://10.43.165.182:8000/v1", api_key="not-needed")
async def one_call(prompt, max_tokens=128):
t0 = time.perf_counter(); first_t = None; n_out = 0
stream = await client.chat.completions.create(
model="qwen2.5-3b",
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens, temperature=0.3, stream=True,
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
if first_t is None: first_t = time.perf_counter()
n_out += 1
t_end = time.perf_counter()
return {
"ttft": (first_t - t0) * 1000,
"tpot": (t_end - first_t) * 1000 / max(n_out - 1, 1),
"total_ms": (t_end - t0) * 1000, "n_out": n_out,
}
2.2 实测结果
| 并发 | n_req | wall(s) | req/s | tok/s | TTFT p50 | TTFT p95 | TPOT p50 | TPOT p95 |
|---|---|---|---|---|---|---|---|---|
| 1 | 8 | 20.11 | 0.40 | 46.8 | 45.0 ms | 388.5 ms | 20.7 ms | 21.4 ms |
| 2 | 8 | 10.99 | 0.73 | 84.2 | 45.0 ms | 59.0 ms | 21.5 ms | 22.3 ms |
| 4 | 8 | 5.67 | 1.41 | 164.4 | 54.3 ms | 77.7 ms | 22.0 ms | 23.6 ms |
| 8 | 16 | 5.90 | 2.71 | 315.9 | 84.7 ms | 96.3 ms | 22.7 ms | 23.7 ms |
| 16 | 32 | 6.34 | 5.05 | 588.5 | 120.6 ms | 152.7 ms | 24.3 ms | 25.6 ms |
2.3 数据解读 —— Continuous Batching 在表演
| 指标 | conc=1 → conc=16 | 解释 |
|---|---|---|
| 吞吐 (tok/s) | 46.8 → 588.5(12.6×) | Continuous Batching 把多请求塞到一次 forward |
| TPOT p50 | 20.7 → 24.3 ms(+17%) | 几乎线性没出现 —— PagedAttention 把 batch 内 attention 算并行 |
| TTFT p50 | 45 → 120.6 ms(+170%) | prefill 阶段 batched,但每个请求要排队进 batch |
TPOT 不变是 vLLM 的「魔法」:传统 batched inference,batch=16 时单 token 生成时间应当接近线性增长(每步矩阵更大,显存带宽更紧)。但 PagedAttention 把 KV cache 分页,attention 计算在 batch 维度真正并行化,token 生成时间几乎独立于 batch size。
TTFT 上涨快不是 vLLM 的锅,是 prefill 本身计算密度高(整个 prompt 一次性进 attention),batch 越大 prefill 越慢。生产里要么(1)--enable-chunked-prefill 让 prefill 跟 decode 混合调度,要么(2)用 SGLang 的 prefix caching 把重复 system prompt 一次性命中,降低 prefill 量。
2.4 TCO 推算
- 单 A800 40GB 跑 Qwen2.5-3B(~6 GB BF16),conc=16 吞吐 588 tok/s
- 假设单请求 200 tokens 输出 → ~0.34s 输出 → 单卡稳态 ~2.9 req/s ≈ 250K req/day(24×7)
- 月成本按 A800 公有云 ¥15/h 估:¥10,800 → 每 request 约 ¥0.0014
这条数线是后面所有量化 / 蒸馏 / spec decoding 优化的对比基线。每砍掉 30% 单 request 成本,年化就是六位数。
3. 量化技术全景
3.1 主流量化方法
| 方法 | bit | 算法 | 适用阶段 | 工具 | 评价 |
|---|---|---|---|---|---|
| FP16/BF16 | 16 | 直接降精度 | 推理(默认) | 任意框架 | baseline,无损 |
| FP8(E4M3 / E5M2) | 8 | Hopper / Ada 原生支持 | 推理 | TensorRT-LLM, vLLM | H100/H200 必选,接近 BF16 质量,2× 吞吐 |
| AWQ | 4 | Activation-aware Weight Quant | PTQ | AutoAWQ, vLLM, LMDeploy | 中文 LLM 主流,损失 <1% |
| GPTQ | 4 | Hessian 近似 + per-group quant | PTQ | auto-gptq, vLLM | 较老,大多被 AWQ 替代 |
| GGUF Q4_K_M | 4 | k-means + 分块 | PTQ | llama.cpp | CPU/端侧首选 |
| SmoothQuant | 8 (W8A8) | 平滑 outliers 后均匀量化 | PTQ | TensorRT-LLM, vLLM | activation 也量化,极致显存 |
| W4A16 (Marlin) | W4A16 | Mixed precision GEMM kernel | PTQ | vLLM (awq_marlin), LMDeploy | vLLM 上 AWQ 的「加速版」 |
| INT4 (QAT) | 4 | Quantization Aware Training | 训练时 | bitsandbytes / NF4 | 训练时量化,推理直接用 |
| HQQ | 1-8 | 半二次量化 | PTQ | HQQ lib | 实验性,极低 bit |
3.2 量化方式选择
H100/H200 + 极致吞吐 → FP8 (TensorRT-LLM)
A100/A800/L40 + 通用 LLM → AWQ + Marlin (vLLM / LMDeploy)
RTX 3090/4090 个人推理 → AWQ 4bit
端侧 / Apple Silicon / CPU → GGUF Q4_K_M (llama.cpp)
显存极紧 W8A8 都量化 → SmoothQuant
训练就要量化感知 → QLoRA (NF4)
3.3 AWQ vs BF16
业内公认数据(vLLM 官方 benchmark + Qwen 团队论文):
| 模型 | 精度 | 显存 | 单请求 latency | 吞吐(16 并发) | MMLU 损失 |
|---|---|---|---|---|---|
| Qwen2.5-7B | BF16 | ~14 GB | 1.0× | 1.0×(baseline) | 0 |
| Qwen2.5-7B | AWQ-W4A16 (Marlin) | ~4.5 GB | 0.7×(更快) | 1.3× | <1% |
| Qwen2.5-7B | GPTQ-W4A16 | ~4.5 GB | 0.8× | 1.1× | 1-2% |
| Qwen2.5-7B | FP8 (H100) | ~7 GB | 0.6× | 1.5× | 0.5% |
核心结论:A800 上 AWQ 把同尺寸模型显存压到 1/3,latency 反而降低(memory-bound 场景下 4bit 加载更快),质量损失 <1%。生产 LLM serving 默认就上 AWQ。
4. Speculative Decoding 全景
4.1 核心思想
慢路(target model, 7B): 输出 1 token / step
快路(draft model, 0.5B): 快速猜 N 个 token
然后 target 一次 forward 验证 N 个猜测 → 接受多少算多少
如果 draft 猜得准,N step 的工作量被压缩到 1 step,latency 直接 2-3×。
4.2 三种 speculative 方法
| 方法 | draft 来源 | 适用 | 加速 |
|---|---|---|---|
| Draft model | 小模型(同系列,如 Qwen 7B + Qwen 0.5B) | 通用,经典 | 1.5-2.5× |
| n-gram | 历史输出的 n-gram 统计 | 重复模式多的输出(代码、表格、JSON) | 1.5-3× |
| EAGLE / Medusa | 在 target model 上加 head | latency 关键,有训练资源 | 2-4× |
| Lookahead Decoding | 自动并行解码多 token | 不需要 draft 模型 | 1.5-2× |
| DeepSeek MTP | Multi-Token Prediction(训练时设计) | DeepSeek V3 / R1 系列原生 | 1.8× |
4.3 vLLM 开启 speculative decoding
# n-gram(零额外模型,最简,先试这个)
vllm serve Qwen/Qwen2.5-7B-Instruct \
--speculative-model "[ngram]" --ngram-prompt-lookup-max 4 --num-speculative-tokens 5
# draft model(经典)
vllm serve Qwen/Qwen2.5-7B-Instruct \
--speculative-model Qwen/Qwen2.5-0.5B-Instruct --num-speculative-tokens 5
# eagle(需要 EAGLE checkpoint)
vllm serve meta-llama/Llama-3.1-70B \
--speculative-model yuhuili/EAGLE-LLaMA3.1-Instruct-70B --num-speculative-tokens 5
4.4 何时开 / 何时关
| 场景 | 是否启用 |
|---|---|
| 代码生成 / JSON / 表格输出 | 强烈建议(n-gram 命中率高) |
| 多语言混合输出 | 通常 OK |
| 极致 latency 关键应用 | 必须 |
| 高并发(conc > 32) | 视情况 —— 高并发下 batched 已经很满,spec 加速边际效益小 |
| draft model 跟 target 体量差太大(<1B 对 70B) | 接受率低,反而拖慢 |
工程权衡:speculative decoding 不是「白送」的加速 —— 验证失败的 step 浪费两次 forward。代码/JSON 这种模式可预测的输出才划算,自由对话场景接受率低于 60% 就会负优化。
5. KV Cache 优化
KV cache 是 LLM serving 的「显存吞噬兽」。对 7B 模型,seq_len=2048 时单 sequence KV cache ≈ 1.5 GB。
5.1 PagedAttention(vLLM 原创)
把 KV cache 切成固定大小 block(默认 16 token / block),仿 OS 虚拟内存管理:
| 传统连续分配 | PagedAttention |
|---|---|
每 seq 预分配 max_seq_len KV cache | 按需分配 block |
| 显存碎片 60-80% | 显存碎片 <4% |
| 显存利用率 ~30% | 显存利用率 ~95% |
效果:同样显存能塞下 3-4× 并发请求(本次 conc=16 比 conc=1 吞吐 12.6× 一半归功于此)。
5.2 Prefix Caching
system prompt 通常几百 tokens 且大量请求复用。Prefix Caching 把 system prompt 的 KV cache 算一次,后续请求复用 —— vllm serve ... --enable-prefix-caching。RAG/Agent 场景 system prompt 占比 80% 时,TTFT 降 60-90%。
SGLang 的 RadixAttention 是 prefix caching 的「自动版」—— 基于 radix tree 自动识别共享前缀,不需要手动声明 cache。这就是 SGLang 在 RAG/Agent 场景能比 vLLM 快 1.5-2× 的原因。
5.3 KV Cache Offload / Quantization
显存装不下时把不活跃 sequence 的 KV cache swap 到 CPU memory —— vllm serve ... --swap-space 16(16 GB CPU swap)。代价是 swap 时 latency 抖动几百 ms,生产只用做峰值保护。
--kv-cache-dtype fp8_e5m2 让 KV cache 显存减半,质量损失 <1%。H100/H200 上几乎是免费午餐。
6. 端到端选型决策表
6.1 按场景选
| 场景 | 引擎 | 量化 | 其他优化 |
|---|---|---|---|
| 生产中文对话(A800) | vLLM 或 LMDeploy | AWQ-W4A16 (Marlin) | prefix caching, chunked prefill |
| 极致 latency(H100) | TensorRT-LLM | FP8 | FP8 KV cache, EAGLE spec decoding |
| RAG/Agent 频繁复用 prompt | SGLang | AWQ | RadixAttention(自动 prefix cache) |
| 代码补全 / Copilot 类 | vLLM | BF16 | n-gram speculative decoding |
| 个人电脑 / MacBook | Ollama (llama.cpp) | GGUF Q4_K_M | metal/CUDA |
| 端侧 / Edge / 手机 | mlc-llm / executorch | INT4 | NPU 加速 |
6.2 综合优化清单
- 基础选 vLLM 起步(生态最大,坑最少)
- 必开 prefix caching(
--enable-prefix-caching)+ chunked prefill(--enable-chunked-prefill) - 必开 AWQ 量化(Qwen / Llama / InternLM 都有官方 AWQ 版)
- 试 speculative decoding(
[ngram]零成本,先试再说);H 系列卡试 FP8 KV cache --max-num-seqs256-1024、--max-num-batched-tokens4096-8192(按显存和 prompt 长度调)- 加 Prometheus + Grafana 监控(
vllm_*metrics 全套),HPA 基于vllm_num_requests_waiting
7. 一句话总结
PagedAttention 解决显存碎片 · Continuous Batching 解决调度并行 · AWQ 量化解决模型尺寸 · Speculative Decoding 解决 latency 上限 · Prefix Caching 解决重复 prompt。
这五项叠起来,单 A800 把 Qwen2.5-3B 推到 588 tok/s @ TPOT 24ms,这就是当下 LLM serving 的工程基线。
面试常见题
Q1:PagedAttention 到底解决了什么问题?
传统连续分配的 KV cache 每个 sequence 一上来就按 max_seq_len 预留显存(比如 2048 tokens 全留),实际只用 200 个 token 就结束的请求,剩下 1800 个 token 显存全是碎片,整卡显存利用率 ~30%。
PagedAttention 把 KV cache 切成固定大小 block(默认 16 token / block),仿 OS 虚拟内存做按需分配 + indirection 表。显存碎片从 60-80% 砍到 <4%,利用率拉到 95%。直接结果:同样显存能塞下 3-4× 的并发请求。
深问:为什么不直接动态分配? —— attention kernel 要求 KV cache 物理连续才能高效访问,PagedAttention 在 CUDA kernel 里加了一层「block table」做间接寻址,付出极小 kernel 复杂度换来巨大显存收益。
Q2:Continuous Batching vs Static Batching 区别在哪?
Static batching 固定一批 N 个请求,全部跑完才开始下一批 —— 短请求被长请求拖死,GPU 利用率 30-40%。
Continuous batching(vLLM 默认)在每个 forward step 边界检查谁结束了、谁可以加入,新请求随时「插队」进当前 batch。短请求一结束就出,长请求继续跑,GPU 利用率 90%+。本质是「在 token 维度做调度」而不是「在 request 维度做调度」。这是 vLLM 单卡吞吐能从 30 tok/s 拉到 500+ tok/s 的核心机制之一。
Q3:TTFT 和 TPOT 是什么?怎么影响 SLO 设计?
- TTFT(Time To First Token):用户按下 Enter 到看到第一个字的延迟,决定「响应感」。受 prefill 阶段(整个 prompt 一次性过 attention)影响最大。
- TPOT(Time Per Output Token):每个后续 token 的平均生成时间,决定「打字速度」。受 decode 阶段 + batch size 影响。
- Throughput(tok/s):单位时间总产 token 数,决定「单卡能养多少用户」。
生产 SLO 一般:TTFT p50 < 300ms / p99 < 1s(聊天体感临界点);TPOT p50 < 50ms ≈ 20 tok/s(比人类阅读速度快才不卡顿);单卡 throughput > 500 tok/s @ A800(经济性下限)。
三个指标互相打架:开 chunked prefill 会让 TTFT 抖动小但峰值 throughput 略降;开大 batch 会拉 throughput 但 TPOT 变差。按业务排优先级 —— 对话 → TTFT,批处理 → throughput,代码补全 → TPOT。
Q4:Speculative Decoding 的工程权衡有哪些?
收益:draft 猜对的 step 等于「N step 工作量压成 1 step」,理论上限 N×。代价四条:
- 验证失败的 step 等于白跑 —— draft forward 一遍 + target 验证一遍,两边都白干
- draft 跟 target 体量差太大接受率低 —— 0.5B 对 70B 接受率可能 <40%,反而拖慢
- 高并发下边际收益小 —— batched decode 本来就把 GPU 喂饱了
- 代码 / JSON / 表格场景接受率高(n-gram 60%+),自由对话低(30-40%)
落地原则:先用 [ngram] 零成本试,看接受率指标(vllm_spec_decode_num_accepted_tokens_total);接受率 >50% 才考虑上 draft model 或 EAGLE。
Q5:怎么做 LLM 推理的 TCO 优化?
按性价比从高到低:
- 量化(AWQ / FP8) —— 显存减 1/3 到 1/2,吞吐 +30-50%,质量损失 <1%。性价比最高,默认动作
- Prefix caching —— RAG/Agent 场景 system prompt 复用,TTFT -60%,几乎零成本
- Speculative decoding —— 代码 / JSON 场景 latency -50%,自由对话场景可能负优化
- 模型蒸馏(7B → 3B 同任务) —— 成本 -50% 起步,但要训练投入 + 评测验证质量
- Spot GPU / 低优先级实例 —— 公有云成本 -60-70%,要做好被抢占时的 graceful degradation
成本模型粗算:baseline(A800 跑 Qwen2.5-3B BF16)每 request ¥0.0014。加 AWQ 吞吐 +30% → ¥0.0011;再加 prefix caching(RAG prompt 复用 80%)→ ¥0.0005;再切 Spot GPU → ¥0.0002。一年下来差几十万。
下一步
推理优化的几条主线(量化、batching、KV cache、speculative)都铺完后,下一篇 Bonus 4 转向长上下文工程:100K+ context 怎么不爆显存、attention 复杂度怎么从 O(n²) 砍到 O(n log n)、RoPE / YaRN / Ring Attention 这些扩展技巧的工程取舍。