Step 18: Benchmark — 推理性能评测
为什么需要 Benchmark?
做完推理系统的优化之后,面对一个核心问题:优化到底有没有效果?效果多大?
"感觉快了"不够。不同优化方向改善的指标不同——有些减少延迟但降低吞吐,有些提升吞吐但让单个用户等更久。没有量化,就无法判断优先级,也无法定位瓶颈。
Benchmark 工具的作用是:
- 用统一的指标语言描述系统行为
- 在改动前后各跑一次,对比数字
- 找到瓶颈:问题出在 prefill 还是 decode?
本节实现一个轻量级 benchmark 工具,测量三个核心指标:TTFT、TPOT、吞吐量。
三个核心指标的业务含义
理解这三个指标,最好从用户视角出发:用户打开一个 AI 对话框,输入一段话,按下发送——从这一刻起,他的体验由两件事决定。
TTFT(Time To First Token)首 Token 延迟
从请求发出到第一个字出现在屏幕上的时间。
用户按下发送
│
▼
[prefill 阶段] ←── 处理整个 prompt
│
▼ ── TTFT ──▶ 第一个字出现 ✓
│
[decode 阶段] ←── 逐 token 生成TTFT 决定用户是否感到"系统卡住了"。如果等了 3 秒还没有任何反应,用户会怀疑请求是否成功。
影响 TTFT 的主要因素:
- prompt 长度:prefill 要处理每一个输入 token,prompt 越长,prefill 越慢
- 批处理积压:请求进入时如果队列里有其他请求正在 prefill,需要等待
- 模型大小:大模型的注意力计算更慢
TPOT(Time Per Output Token)每 Token 延迟
第一个 token 出现之后,后续每个 token 平均需要多久。
第一个字 → 第二个字 → 第三个字 → ... → 最后一个字
←TPOT→ ←TPOT→ ←TPOT→TPOT 决定用户看到的"流式输出速度"。人类阅读速度大约是每秒读几个字,如果 TPOT 太大,用户会看到文字一顿一顿地出现。
影响 TPOT 的主要因素:
- 当前批大小:decode 阶段同时处理的请求越多,每个 token 的生成越慢(但整体吞吐越高)
- KV Cache 大小:每次 decode 都要读取之前所有 token 的 KV Cache,输出越长读取越慢
- 内存带宽:decode 是内存带宽密集型操作,显存带宽是瓶颈
吞吐量(Throughput)
单位时间内系统生成的 output token 总数,单位 tokens/s。
时间轴:
[请求A decode][请求A decode]
[请求B decode][请求B decode][请求B decode]
[请求C decode][请求C decode]
吞吐量 = 所有请求的 output token 总数 / 总耗时吞吐量衡量系统的整体生产力,直接对应服务成本:同样一块 GPU,吞吐量越高,能服务的用户越多,每次对话的成本越低。
连续批处理(Continuous Batching)正是为了提升吞吐量而设计——让 GPU 始终处于忙碌状态,而不是等一个请求完全结束再处理下一个。
三角权衡:不能同时最优
这三个指标不能同时最优,存在内在的权衡关系:
低延迟(TTFT & TPOT)
/\
/ \
/ \
/ × \
/________\
小批量 高吞吐
(快响应, (大批量,
低吞吐) 慢响应)TPOT 与吞吐量的权衡最典型:
- 大批量:许多请求同时 decode,GPU 利用率高,吞吐量大;但每个 token 要分摊给所有请求,每个用户等待更久(TPOT 升高)
- 小批量:响应快(TPOT 低),但 GPU 大部分时间在等待,吞吐量低
TTFT 与吞吐量也有冲突:
- 为了凑大批量,新来的请求需要等待其他请求的 prefill 结束,排队时间增加 TTFT
- 为了最小化 TTFT,请求一到就立刻处理,但频繁打断正在进行的 decode 批次
实际系统的选择:
不同场景对三个指标的优先级不同:
- 交互式对话:TTFT 优先(用户不能等太久才看到第一个字)
- 批量处理任务(摘要、翻译):吞吐量优先(没有实时交互)
- 实时语音/代码补全:TPOT 优先(输出速度要跟上用户阅读/思考速度)
为什么用 Poisson 到达过程模拟真实负载
bench.py 目前使用顺序请求(一个完成再发下一个),这是串行负载模型,不能反映真实情况。
真实的 LLM 服务中,请求是并发随机到达的:
串行模型(不真实):
请求1 ──── 请求2 ──── 请求3 ────▶ 时间轴
(前一个结束才发下一个)
Poisson 到达(更真实):
请求1 ────────────
请求2 ──────────────────
请求3 ────────
请求4 ─────────▶ 时间轴
(随机时刻到达,可能同时存在多个请求)Poisson 过程是描述"独立随机事件以恒定平均速率发生"的数学模型。用户的请求彼此独立,且到达速率在短时间内大致恒定,符合 Poisson 过程的假设。
使用 Poisson 到达的好处:
- 能测出排队延迟:高负载下新请求需要等待系统处理能力空闲
- 能找到系统容量上限:当到达速率超过处理速率,队列无限增长,延迟急剧恶化
- 能模拟负载突发:Poisson 过程允许短时间内多个请求同时到达
当前 bench.py 的实现是教学简化版,使用随机输入长度的顺序请求来演示指标计算框架,不模拟并发。
如何用 Benchmark 结果指导优化
Benchmark 不只是跑一遍数字,而是要读懂数字在说什么:
瓶颈在 prefill?
信号:TTFT 很高,TPOT 正常
TTFT: ████████████████ (很长)
TPOT: ██ (正常)原因:prefill 计算量随 prompt 长度平方增长(注意力的计算复杂度)。长 prompt 或高并发时 prefill 成为瓶颈。
优化方向:
- Chunked Prefill:将长 prompt 分块处理,避免单次 prefill 占用太长时间
- 减少 prompt 长度(提示词工程)
- 增大 prefill 的优先级调度权重
瓶颈在 decode?
信号:TTFT 正常,TPOT 高,吞吐量低
TTFT: ██ (正常)
TPOT: ████████████████ (很长)原因:decode 是内存带宽密集型——每步都要从显存读取整个模型权重和所有 KV Cache。显存带宽不够时,GPU 算力大量空闲等待数据。
优化方向:
- 增大批大小,让每次内存读取服务更多请求(分摊带宽成本)
- 量化(Quantization):减少权重大小,减少显存读取量
- Speculative Decoding:用小模型预测多个 token,大模型一次验证
吞吐量低但延迟也不高?
信号:TTFT 和 TPOT 都不高,但 tokens/s 数字小
原因:GPU 利用率低,批大小太小,或请求到达太稀疏。
优化方向:
- 检查连续批处理是否正常工作(Step 05 的内容)
- 增大最大批大小限制
- 考虑请求调度策略
文件说明
| 文件 | 功能 |
|---|---|
bench.py | Benchmarker 类,接受 engine,发送随机请求,收集 BenchResult |
run.py | 运行 benchmark 并打印统计报告;无模型时用模拟数据展示格式 |
BenchResult 数据结构
@dataclass
class BenchResult:
ttft_ms: float # 首 Token 延迟(毫秒)
tpot_ms: float # 每 Token 延迟(毫秒)
output_len: int # 本次请求生成的 token 数
throughput: float # 全局吞吐量(tokens/s,所有请求共用同一个值)bench.py 中的近似说明
当前实现对 TTFT/TPOT 的拆分做了简化:
ttft_ms = total_ms * 0.2 # 用总时间的 20% 近似 prefill
tpot_ms = (total_ms - ttft_ms) / (output_len - 1) # 剩余时间均摊真实的 prefill/decode 时间拆分需要 engine 在生成第一个 token 时打一个时间戳。目前 RealModelEngine 的 generate() 接口是一次性返回所有 token,没有暴露 streaming 接口,所以用近似值代替。如果要精确测量 TTFT,需要 engine 支持逐 token 回调或 streaming 生成。
运行
python run.py
# 无模型时使用模拟数据展示报告格式有模型时(需要 step19_real_model 的 RealModelEngine):
QWEN3_MODEL_PATH=~/huggingface/Qwen3-0.6B python run.py预期输出格式:
============================================================
mini-vllm-tutorial Benchmark 工具
============================================================
请求数: 3
首 Token 延迟 (TTFT):
平均: 146.4ms
P50: 145.2ms
每 Token 延迟 (TPOT):
平均: 8.3ms
总吞吐量: 412 tok/s
✅ step18_benchmark 通过下一步
本节的 benchmark 工具建立了性能基线。下一步(HTTP Serve:OpenAI 兼容推理服务)将把引擎封装成 HTTP 推理服务:实现 OpenAI 兼容的 API 接口,让推理能力可以被其他程序、其他语言、其他机器通过网络调用。