3、性能基准测试:如何设计基准测试、关键性能指标(吞吐量、延迟、显存)、测试工具与脚本编写
聊到 Batch 推理优化,有个问题我经常被问到:「你怎么知道你的优化有效?」
嗯,这就要靠基准测试了。没有基准,你所有的调优都是盲人摸象。我在项目中见过太多团队,花了两周调参数,结果上线后发现吞吐量反而下降了——就是因为没有一套靠谱的测试流程。
这一章,我们就来聊聊怎么设计一个能打、能复现、能说服人的基准测试。
3.1 基准测试的核心设计原则
说白了,基准测试就三个目标:可复现、可对比、可解释。
- 可复现:你今天的测试结果,明天还能跑出来。环境、数据、模型版本都得锁死。
- 可对比:不同 Batch 大小、不同精度、不同硬件,你得能公平地比。
- 可解释:性能好了或差了,你得知道为什么。不能只给个数字。
我个人习惯:每次测试前,先跑一个「基线」——通常是 Batch Size=1、FP32、不做任何优化。然后所有优化方案都跟这个基线比。这样你心里就有底了。
3.2 关键性能指标(KPI)
你想想看,做推理优化,我们到底在优化什么?三个维度:吞吐量、延迟、显存。这三者经常互相打架。
3.2.1 吞吐量(Throughput)
单位时间内能处理多少个请求。通常用 requests/s 或 tokens/s 表示。
- 对于 LLM,我更关注 输出 tokens/s,因为生成阶段才是瓶颈。
- 对于 CV 模型,关注 images/s。
我曾经在一个项目中,把 Batch Size 从 1 调到 32,吞吐量提升了 8 倍。但代价是延迟暴涨——这个后面会讲。
3.2.2 延迟(Latency)
单个请求从发起到返回的时间。注意,这里要区分 P50、P95、P99。
- P50:一半请求比这个快,适合看整体表现。
- P99:99% 的请求比这个快,适合看尾延迟。在线服务尤其关注这个。
我建议你至少记录 P50 和 P99。因为有时候平均延迟好看,但 P99 可能已经炸了。
3.2.3 显存(Memory)
GPU 显存是硬约束。Batch 越大,显存占用越高。一旦 OOM,一切归零。
- 记录 峰值显存(peak memory)和 稳态显存。
- 注意:显存占用不是线性的。模型权重、KV Cache、中间激活,各有各的规律。
避坑指南:我曾经在测试时只看了吞吐量,没注意显存。结果上线后,用户并发一上来,直接 OOM 崩溃。从那以后,我每次测试都会把显存监控加上。
3.3 测试工具与脚本编写
工具方面,我推荐几个常用的:
- PyTorch Profiler:自带,适合看算子级性能。
- NVIDIA Nsight Systems:系统级 profiling,能看到 GPU 利用率、数据搬运等。
- 自定义脚本:最灵活,适合做批量测试。
下面是我自己写的一个基准测试脚本模板,你可以直接拿去改:
import torch
import time
import numpy as np
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16).cuda()
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 测试参数
batch_sizes = [1, 4, 8, 16, 32]
input_length = 128
output_length = 64
num_warmup = 10
num_runs = 50
results = []
for bs in batch_sizes:
# 构造输入
inputs = tokenizer(["Hello, how are you?"] * bs, return_tensors="pt", padding=True, truncation=True, max_length=input_length)
inputs = {k: v.cuda() for k, v in inputs.items()}
# Warmup
for _ in range(num_warmup):
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=output_length)
# 正式测试
torch.cuda.synchronize()
start_time = time.time()
latencies = []
for _ in range(num_runs):
torch.cuda.synchronize()
t0 = time.time()
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=output_length)
torch.cuda.synchronize()
t1 = time.time()
latencies.append(t1 - t0)
# 统计
latencies = np.array(latencies)
throughput = bs * output_length / latencies.mean() # tokens/s
p50 = np.percentile(latencies, 50)
p99 = np.percentile(latencies, 99)
# 显存
memory = torch.cuda.max_memory_allocated() / 1024**3 # GB
results.append({
"batch_size": bs,
"throughput_tokens_per_s": round(throughput, 2),
"latency_p50_ms": round(p50 * 1000, 2),
"latency_p99_ms": round(p99 * 1000, 2),
"peak_memory_gb": round(memory, 2)
})
torch.cuda.reset_peak_memory_stats()
# 打印结果
import pandas as pd
df = pd.DataFrame(results)
print(df.to_string(index=False))
注意:上面的脚本假设你用的是 HuggingFace 的 generate 函数。如果你用 vLLM 或 TensorRT-LLM,接口会不同,但测试逻辑是一样的。
3.4 知识体系图:基准测试的核心逻辑
下面这张图,是我自己总结的基准测试流程。你照着这个走,基本不会漏掉什么。
3.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 不要只测一次:GPU 频率、系统负载都会波动。至少跑 50 次,取统计值。
- Warmup 不能省:第一次推理通常很慢(CUDA 初始化、缓存未命中)。我一般 warmup 10-20 次。
- 注意输入长度:LLM 的延迟跟输入+输出长度强相关。测试时一定要固定长度,或者记录长度分布。
- 显存要清零:每次测试后,记得
torch.cuda.reset_peak_memory_stats(),否则峰值会累加。
我的小习惯:每次测试完,我会把结果存成一个 CSV 文件,带上时间戳和 Git commit hash。这样以后回看,还能知道当时跑的是哪个版本的代码。
好了,基准测试这块就聊这么多。你拿着上面的脚本和流程,应该能跑出一份靠谱的测试报告了。记住:没有基准,就没有优化。