推理性能指标与瓶颈定位
把训练好的模型搬上线,和把它训练出来是两件目标不同的事:训练追求 Loss 收敛,推理追求在延迟与成本的约束下尽可能快、尽可能多地生成 Token。后一句里的每个词都对应一个指标,而这些指标的轻重关系由一条硬件层面的判据决定。
这一篇建立的是后面所有优化技术的坐标系 —— 每一项技术(PagedAttention、Continuous Batching、量化、投机解码)都是在这个坐标系里的某个位置动刀。
延迟指标:用户等多久
| 指标 | 定义 | 由什么决定 |
|---|---|---|
| TTFT(Time To First Token) | 从提交请求到收到第一个 Token | 排队时间 + Prefill 时间 + 网络延迟;Prompt 越长 TTFT 越大 |
| TPOT(Time Per Output Token) | 生成阶段平均每个输出 Token 的耗时 | Decode 阶段每步耗时 |
| ITL(Inter-Token Latency) | 相邻输出 Token 的间隔 | 与 TPOT 同指一件事 |
| E2E | 提交到收完的总时间 |
TPOT 与 ITL 在多数语境下是同义词,差别只在是否把首 Token 那段算进去。NVIDIA GenAI-Perf 的口径是排除:
E2E 那个公式看着平凡,但它决定优化方向。代入两个场景(设 TTFT = 500 ms、TPOT = 40 ms):
| 场景 | 计算 | TTFT 占比 |
|---|---|---|
| 短输出(20 Token) | 约 40% —— 首字慢就先炸 | |
| 长输出(800 Token) | 约 1.5% —— 优化 TPOT 才是正事 |
同一套系统,业务形态不同,优化重心差得极远。「先测清自己的输入输出长度分布,再谈优化」这条纪律,正是这个公式的直接推论。
吞吐指标,与「平均值骗人」
吞吐有两种口径,混用会得出相反结论:
| 口径 | 含义 | 用来看什么 |
|---|---|---|
| Token/s(系统) | 全系统每秒生成的总 Token | 集群产能与单位成本 |
| Token/s(单用户) | 单用户感受到的出字速度 | 约等于 |
| RPS | 每秒成功完成的请求数 | 并发承载能力 |
系统吞吐随并发上升而增长,直到 GPU 饱和后趋平甚至回落。吞吐和延迟是一对矛盾:把更多请求塞进一个 Batch 提高系统吞吐,但每个请求的排队与计算时间变长,单用户延迟变差。
平均延迟是最容易骗人的指标 —— 少数慢得离谱的请求会被平均值藏起来。生产环境看尾延迟:P50 / P95 / P99。
Goodput(有效吞吐) 比 Raw QPS 更贴近真实价值:它只统计满足 SLO 的那部分请求的吞吐(例如「TTFT < 1 s 且 TPOT < 50 ms」)。一个系统 QPS 很高但大部分请求超时,Goodput 就很低。
混淆的代价
「Raw QPS 涨了」和「用户体验变好了」是两件事。把 Goodput 作为优化目标,是因为它是唯一一个同时包含延迟约束的吞吐指标。
瓶颈定位:为什么 Decode 打不满算力
这是整个推理优化的第一性原理。
算术强度
GPU 干活要同时做两件事:从显存(HBM)搬数据,和用计算单元算数据。衡量一个任务偏计算还是偏访存:
直白说就是每从显存搬 1 个字节,能顺带做多少次计算。
Roofline 与平衡点
Roofline 模型把它画成一张图:横轴算术强度,纵轴实际可达算力。图上有一条「屋顶线」——左半段是被显存带宽压住的斜坡(Memory Bound 区),右半段是被峰值算力压住的水平线(Compute Bound 区),两段的交点就是这块卡的平衡点。
平衡点 = 峰值算力 ÷ 显存带宽。以 NVIDIA H100 SXM 为例,BF16 稠密算力约 990 TFLOPS(厂商标称的 1979 TFLOPS 是开启结构化稀疏后的数值,稠密约为一半),HBM3 带宽 3.35 TB/s:
在这块卡上,一个任务每搬 1 字节得配上约 295 次浮点运算才刚好把算力和带宽同时喂饱。
不同 GPU 的平衡点都在同一量级(几百 FLOP/Byte)。带宽升级往往比算力升级更值钱,就是因为绝大多数 Decode 负载死死卡在斜坡左侧的带宽墙上。
把 Prefill 和 Decode 放上去
| 阶段 | 算术强度 | 落在哪 |
|---|---|---|
| Prefill | 高 —— 权重搬一次,batch 内所有 Token 共享,却要做海量矩阵乘 | Compute Bound 区 |
| Decode | 极低 —— 每步只处理 1 个 Token,却要把整个模型的权重搬一遍 | Memory Bound 区 |
Decode 的算术强度可以手算。对一个权重量为
对比平衡点 295,低了两个数量级 —— 理论上算力利用率不到 1%。
把
Decode 慢在搬不过来,不在算不过来。 GPU 的计算单元大部分时间在空等权重从显存加载。
这一句话直接推出两个结论:
- Batch 能大幅提吞吐而几乎不增延迟 —— 权重只搬一次服务整个 Batch,把闲置算力利用起来。
- Weight-only 量化对 Decode 加速明显 —— 瓶颈在搬权重,FP16 压到 INT4 等于要搬的字节数减到 1/4。
两种负载落在同一张 Roofline 的两端:
实际可达算力
▲
990 ┤ ┌────────────────────── 算力上限(Compute Bound 区)
TFLOPS│ ╱ ● Prefill:权重搬一次,batch 内所有 Token 共享
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱ ↑ 斜坡 = 带宽上限(Memory Bound 区)
│ ╱
│ ╱
│ ╱
│ ╱
│╱ ● Decode:每步只处理 1 个 Token,却要把整个模型的权重搬一遍
└──┬───────────────┬───────────────────────▶ 算术强度
1 295(平衡点 = 990 TFLOPS ÷ 3.35 TB/s)
↑
低了两个数量级 ⇒ 理论上算力利用率不到 1%
⇒ Decode 慢在「搬不过来」,不在「算不过来」
⇒ 两个直接推论:Batch 能大幅提吞吐而几乎不增延迟;
Weight-only 量化对 Decode 加速明显(要搬的字节数减到 1/4)全链路逐环节
一个请求从进到出走这一条链,每个环节的计算特性和潜在瓶颈都不同:
Tokenize → Prefill → Decode Loop → Sampling → Detokenize
↑______|| 环节 | 做什么 | 计算特性 | 潜在瓶颈 |
|---|---|---|---|
| Tokenize | 文本切 Token ID | CPU | 长文本或高并发时 CPU 成瓶颈 |
| Prefill | 并行处理全部 Prompt Token,产出首 Token 与初始 KV Cache | GPU,Compute Bound | Prompt 长时拉高 TTFT |
| Decode Loop | 逐 Token 生成,每步读全量 KV、追加新 KV | GPU,Memory Bound | 显存带宽,决定 TPOT |
| Sampling | 按 sampling 参数从分布取下一个 Token | GPU,轻量 | 复杂采样或约束解码略增开销 |
| Detokenize | Token ID 拼回文本,流式返回 | CPU | 与主循环争抢 CPU |
Tokenize / Detokenize 这两个 CPU 环节最容易被忽略。GPU 优化到位后它们反而成为新瓶颈 —— 这是 vLLM V1 用多进程把它们与 GPU 核心循环重叠起来的原因。约束解码(结构化输出)也会把开销加在 Sampling 上。
一个请求从进到走(标出每个环节跑在谁身上):
Tokenize ──▶ Prefill ──▶ Decode Loop ──▶ Sampling ──▶ Detokenize
[CPU] [GPU] [GPU] [GPU] [CPU]
│ │ ▲ │ │ │
│ │ └─┘ │ │
│ │ 逐 Token 循环 │ │
│ ▼ │ │
│ Compute Bound │ │
│ (产出首 Token + 初始 KV Cache) │ │
│ │ │
└─ 长文本 / 高并发时 CPU 成瓶颈 ──────────┴─ 与主循环争抢 CPU ─┘
⇒ 两个 CPU 环节最容易被忽略:GPU 优化到位后它们反而成为新瓶颈
(vLLM V1 用多进程把它们与 GPU 核心循环重叠起来)按症状选技术
优化该先定位症状,再挑手段 —— 把最新的技术都开上解决不了问题:
| 症状 | 该看哪些技术 |
|---|---|
| TTFT 过高 | Chunked Prefill、Prefix Cache(命中可直接砍掉 Prefill 计算)、GEMM 优化、调小 max_num_batched_tokens 之外的排队因素 |
| TPOT 过高 | FlashAttention 类后端、CUDA Graph、Batching、Speculative Decoding、量化(Weight-only) |
| 显存不够 / 并发上不去 | PagedAttention、KV Cache 量化、量化权重、张量并行 |
| 尾延迟失控 | P/D 解耦、SLO 感知调度、限制批次规模 |
技术叠加不等于效果叠加。 已知的冲突有两处:投机解码与量化叠加会放大精度风险;投机解码与 Continuous Batching 叠加会抬高调度复杂度(一步内不同请求推进的 Token 数不再统一)。
推荐的优化顺序:
先解决 OOM → 再攻 TTFT → 再攻 TPOT/吞吐 → 最后处理尾延迟理由是这个顺序里前一步会改变后一步的边界。显存不够时谈 TPOT 没有意义——你连请求都塞不进去。
压测与回归门禁
优化效果必须量化验证,而「只报一次平均耗时」是没有意义的。一套可复现的推理压测至少要固定:
| 类别 | 必须记录 |
|---|---|
| 环境 | GPU 型号与数量、驱动、CUDA 版本、引擎版本、模型版本与精度 |
| 负载 | 输入/输出长度分布(不是平均值)、并发数、到达率 |
| 指标 | TTFT(P50/P95/P99)、TPOT(P50/P95/P99)、吞吐、显存峰值、GPU 利用率 |
| 配置 | 全部引擎参数,尤其 max_num_batched_tokens、gpu_memory_utilization、并行度 |
工具侧:vLLM 自带 vllm bench serve / vllm bench latency / vllm bench throughput;跨引擎对比可用 GenAI-Perf、Triton Perf Analyzer。下钻用 torch.profiler(算子级)、Nsight Systems(全链路)、Nsight Compute(Kernel 级)。
性能回归门禁:把「TPOT P95 退化超过 5% 则阻断合并」这类规则写进 CI,退化时用 git bisect 配合 Nsight 对比定位。
**负载敏感性**:Continuous Batching 让延迟指标对负载高度敏感。低负载时批次小、延迟低;高负载时批次变大、排队变长,P95/P99 会明显上升。**容量规划必须在目标并发下压测,空载数据会给出过于乐观的结论。**
P/D 解耦的动机
前面那张 Prefill/Decode 对照表藏着一个调度问题:两个阶段的瓶颈在相反的两侧(一个算力受限、一个带宽受限),放在同一批里会互相干扰 —— 长 Prefill 会把同批 Decode 的 TPOT 拉出尖刺。
由此推出服务系统的两个设计:
- 必须把多个并发 Decode 批在一起,把「一次性读权重」的开销摊到多个请求上。这是 batch size 在 Decode 阶段比在 Prefill 阶段重要得多的原因。
- 大部署会把两个阶段分到不同硬件上(disaggregated prefill/decode)—— 因为瓶颈在两边,混在一起调度会互相等待。
第 2 条也就是 PD 解耦架构的出发点。它在源材料里只有大纲(见 00-AI Infra 专栏导览 的待建清单),本库暂不展开。
这条和 18-Context Engineering 里
cold prefill / resume prefill / short decode的分解是同一件事的两种拆法:那里按成本结构拆,这里按硬件瓶颈拆,指的是同两个阶段。
相关
- 10-KV Cache 与推理优化 —— KV Cache 的显存账本与注意力布局
- 03-推理调度:Continuous Batching 与 Chunked Prefill —— 利用 Memory Bound 结论的调度层实现
- 18-Context Engineering —— 从成本角度对同一组阶段的另一种拆法
- 08-模型选型 —— TTFT/TPOT 与 Prompt Cache 如何进入选型决策
参考
- https://caomaolufei.github.io/AIInfraGuide/guides/模块四-推理优化/第1章-llm推理基础/11-llm推理基础
- https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html
- https://www.nvidia.com/en-us/data-center/h100/
- https://dl.acm.org/doi/10.1145/1498765.1498785
- https://caomaolufei.github.io/AIInfraGuide/guides/模块四-推理优化/第11章-推理优化选型与端到端实战
YJ