Skip to content

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.pyBenchmarker 类,接受 engine,发送随机请求,收集 BenchResult
run.py运行 benchmark 并打印统计报告;无模型时用模拟数据展示格式

BenchResult 数据结构

python
@dataclass
class BenchResult:
    ttft_ms: float     # 首 Token 延迟(毫秒)
    tpot_ms: float     # 每 Token 延迟(毫秒)
    output_len: int    # 本次请求生成的 token 数
    throughput: float  # 全局吞吐量(tokens/s,所有请求共用同一个值)

bench.py 中的近似说明

当前实现对 TTFT/TPOT 的拆分做了简化:

python
ttft_ms = total_ms * 0.2          # 用总时间的 20% 近似 prefill
tpot_ms = (total_ms - ttft_ms) / (output_len - 1)  # 剩余时间均摊

真实的 prefill/decode 时间拆分需要 engine 在生成第一个 token 时打一个时间戳。目前 RealModelEnginegenerate() 接口是一次性返回所有 token,没有暴露 streaming 接口,所以用近似值代替。如果要精确测量 TTFT,需要 engine 支持逐 token 回调或 streaming 生成。


运行

bash
python run.py
# 无模型时使用模拟数据展示报告格式

有模型时(需要 step19_real_modelRealModelEngine):

bash
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 接口,让推理能力可以被其他程序、其他语言、其他机器通过网络调用。