1. 概述
为什么LLM推理性能测试至关重要
在我处产品测试工作中,随着大语言模型在智能客服、代码辅助、文档分析等业务场景的深度集成,LLM推理性能直接影响用户体验和系统可用性。与传统Web应用不同,LLM推理具有流式输出、Token级延迟、显存密集型等独特特征,传统的QPS/响应时间指标体系已不足以全面评估其性能表现。
以金融行业典型场景为例:一个智能投研助手需要实时分析多份财报并生成摘要,如果首Token延迟超过2秒,分析师的使用意愿将急剧下降;如果并发处理能力不足,高峰时段的服务质量将无法保障。因此,建立科学的LLM推理性能测试体系是我处AI质量保障的核心任务之一。
LLM推理性能测试 vs 传统Web应用性能测试
| 对比维度 | 传统Web应用 | LLM推理 |
|---|---|---|
| 核心指标 | QPS、平均响应时间、错误率 | TTFT、TPOT、tokens/s、端到端延迟 |
| 响应模式 | 一次性返回完整结果 | 流式逐Token生成(SSE) |
| 资源瓶颈 | CPU、内存、网络I/O | GPU显存、显存带宽、计算单元利用率 |
| 负载特征 | 请求独立、无状态 | 请求具有KV Cache状态、显存驻留 |
| 延迟构成 | 网络+业务处理+数据库 | Prompt编码 + Token生成(自回归) |
| 压测工具 | JMeter、Locust、Gatling | JMeter + SSE插件、vLLM benchmark、自研脚本 |
| 容量评估 | 基于QPS的线性扩展 | 基于GPU显存和并发数的非线性扩展 |
2. 核心性能指标
LLM推理性能评估需要从用户感知延迟和系统吞吐效率两个维度综合考量。以下是我处性能测试中重点关注的五大核心指标:
| 指标 | 英文缩写 | 定义 | 业务阈值参考 | 测量方法 |
|---|---|---|---|---|
| 首Token延迟 | TTFT | 从发送请求到收到第一个Token的时间间隔,包含Prompt编码和首个Token生成 | < 500ms(实时对话) < 2s(文档分析) |
记录SSE流中首个data事件的时间戳 |
| 每Token延迟 | TPOT | 后续每个Token生成的平均间隔时间(不含首Token) | < 50ms(保证流畅感) | (末Token时间 - 首Token时间)/(总Token数 - 1) |
| 吞吐量 | Throughput | 单位时间内系统处理的Token总量(包括输入和输出) | 根据硬件和模型差异评估 | 总Token数 / 总耗时(含并发叠加) |
| 并发能力 | Concurrency | 系统在满足SLO前提下能同时处理的请求数 | 不低于预估峰值的1.5倍 | 逐步增加并发数直至TTFT/SLO超标 |
| 端到端响应时间 | E2E Latency | 用户从发起请求到接收完整响应的总时间 | < 10s(常规问答) | TTFT + TPOT × 输出Token数 |
3. 测试方法
3.1 流式响应测试(SSE)
LLM推理的主流交互方式是基于Server-Sent Events(SSE)的流式输出。测试时需要精确捕获和记录每个Token到达的时间戳,这比传统HTTP请求-响应模式的测试更复杂。在我处测试实践中,我们基于JMeter开发了专门的SSE流式响应采样器,能够:
- 逐Token打点:记录每个Token到达的精确时间,计算TTFT和TPOT
- 流完整性校验:验证SSE流是否完整闭合、无中断
- 内容质量关联:同步采集输出内容进行质量评估
// SSE流式响应解析核心逻辑(JMeter JSR223 采样器示例)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
def conn = new URL("${endpoint}/v1/chat/completions").openConnection()
conn.setRequestMethod("POST")
conn.setRequestProperty("Authorization", "Bearer ${apiKey}")
conn.setRequestProperty("Content-Type", "application/json")
conn.setDoOutput(true)
// 写入请求体
def body = '''
{
"model": "${model}",
"messages": [{"role": "user", "content": "${prompt}"}],
"stream": true,
"max_tokens": 2048
}'''
conn.outputStream.write(body.getBytes("UTF-8"))
// 读取SSE流并记录时间
def reader = new BufferedReader(new InputStreamReader(conn.inputStream))
long startTime = System.currentTimeMillis()
long firstTokenTime = 0
int tokenCount = 0
String line
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ")) {
def data = line.substring(6)
if (data == "[DONE]") break
if (firstTokenTime == 0) {
firstTokenTime = System.currentTimeMillis()
}
tokenCount++
}
}
long endTime = System.currentTimeMillis()
long ttft = firstTokenTime > 0 ? firstTokenTime - startTime : -1
double tpot = tokenCount > 1 ? (endTime - firstTokenTime) / (tokenCount - 1) : 0
// 写入JMeter变量
vars.put("ttft_ms", String.valueOf(ttft))
vars.put("tpot_ms", String.valueOf(Math.round(tpot)))
vars.put("total_tokens", String.valueOf(tokenCount))
vars.put("e2e_ms", String.valueOf(endTime - startTime))
log.info("TTFT=${ttft}ms, TPOT=${Math.round(tpot)}ms, Tokens=${tokenCount}")
3.2 批量推理测试
批量推理(Batch Inference)适用于离线场景,如批量文档摘要、数据标注等。测试要点包括:
- 吞吐量最大化:验证批量模式下的tokens/s是否达到理论峰值
- 显存利用率:监控GPU显存使用率是否稳定在高位
- 批大小调优:找到延迟和吞吐量的最佳平衡点
3.3 并发压力测试
模拟多用户同时访问的场景,评估系统在并发压力下的性能退化曲线。在我处实践中,采用阶梯式加压策略:
- 从1并发开始,每30秒增加5个并发线程
- 持续监控TTFT P50/P95/P99的变化趋势
- 当P95 TTFT超过SLO阈值时,记录此时的并发数为系统容量上限
- 继续加压至服务极限,观察队列机制是否正常工作
3.4 持续负载测试
长时间保持一定并发量运行(通常≥2小时),检验系统在持续负载下的稳定性。重点关注:
- KV Cache内存泄漏:显存是否随时间持续增长
- 性能衰减:TTFT/TPOT是否有逐渐恶化趋势
- 异常恢复:间歇性故障恢复后性能是否回归基线
4. 影响因素分析
LLM推理性能受多种因素的综合影响,测试时需要系统性地进行变量控制和对比分析:
| 影响因素 | 影响维度 | 典型规律 | 测试建议 |
|---|---|---|---|
| 模型大小 | TPOT、显存占用 | 7B→70B,TPOT约增加3-5倍,显存用量约增加8-10倍 | 建立不同模型大小的性能基线,辅助模型选型决策 |
| 量化精度 | 吞吐量、显存占用、精度损失 | FP16→INT8吞吐提升~2x,显存减半;INT4吞吐再提升~1.5x,但精度损失需评估 | 对每种量化精度独立测试,关联精度评测结果 |
| 输入长度 | TTFT、显存占用 | 输入Token数翻倍,TTFT约增加40%-80% | 按短/中/长文本设计测试用例组(如256/1024/4096 tokens) |
| 输出长度 | 端到端延迟、TPOT | 输出Token数超KV Cache窗口时TPOT骤升 | 设置不同max_tokens参数进行对比测试 |
| GPU配置 | 整体性能 | H800相较A100,推理吞吐提升~2-3x(同精度下) | 记录硬件配置详情,作为性能报告必备附件 |
| 推理引擎 | 并发能力、吞吐量 | vLLM的Continuous Batching显著优于静态批处理;TensorRT-LLM在特定硬件上有额外优化 | 在同等条件下对比不同引擎的性能表现 |
5. 测试工具
5.1 JMeter + 自建SSE插件
我处性能测试团队在Apache JMeter基础上进行了深度定制,开发了适配LLM推理测试的专用组件:
- SSE采样器:支持流式响应解析和Token级计时
- 动态Prompt参数化:从CSV数据集中读取不同长度/类型的Prompt,实现真实场景复现
- 多维度结果聚合:自动生成TTFT/TPOT/吞吐量统计报告
- GPU资源监控集成:通过nvidia-smi采集GPU利用率和显存数据,与性能指标关联分析
# JMeter非GUI模式命令行执行示例
# 适用于CI/CD流水线集成
jmeter -n -t llm_perf_test.jmx \
-Jendpoint=http://gpu-server:8000 \
-Jmodel=qwen2-72b-int8 \
-Jconcurrency=10 \
-Jduration=300 \
-Jprompt_file=prompts_dataset.csv \
-l results/llm_perf_$(date +%Y%m%d_%H%M%S).jtl \
-e -o reports/llm_perf_$(date +%Y%m%d_%H%M%S)/
5.2 vLLM性能基准工具
vLLM官方提供的benchmark_serving.py是业界广泛使用的LLM推理基准测试工具。其优势在于:
- 直接调用OpenAI兼容API,无需额外适配
- 内置多种负载模式(固定并发数、Poisson请求分布)
- 自动统计TTFT、TPOT、吞吐量等核心指标
# vLLM benchmark 典型命令
python -m vllm.entrypoints.openai.api_server \
--model /models/qwen2-72b-int8 \
--max-model-len 8192 \
--gpu-memory-utilization 0.9
# 在另一个终端执行基准测试
python benchmarks/benchmark_serving.py \
--backend vllm \
--model qwen2-72b-int8 \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 512 \
--request-rate 8 \
--num-prompts 500 \
--result-dir ./benchmark_results/
5.3 OpenAI API兼容测试
大多数LLM推理框架(vLLM、TGI、Ollama等)均提供OpenAI兼容的API端点。这使我们可以用统一的接口进行跨引擎对比测试。我处封装了一套轻量级的Python测试脚本,支持:
- 统一的API调用接口,屏蔽不同引擎的差异
- 可配置的负载参数(并发数、请求间隔、测试时长)
- 自动生成Markdown格式的性能对比报告
6. 实战演练
任务1:使用JMeter对LLM推理服务进行延迟基准测试
目标:掌握JMeter SSE采样器的配置和使用方法,建立LLM推理性能测试基线。
环境要求:
- 已部署的LLM推理服务(如vLLM提供的OpenAI兼容API)
- JMeter 5.6+ 及自建SSE插件
- 测试数据集:包含短文本(256 tokens)、中文本(1024 tokens)、长文本(4096 tokens)的Prompt文件
步骤要点:
- 创建JMeter测试计划,添加Thread Group(线程数=1,循环=10)
- 配置SSE采样器:填入API endpoint、model名称、stream=true
- 通过CSV Data Set Config读取测试Prompt数据集
- 添加JSR223后置处理器,解析SSE流并计算TTFT/TPOT
- 添加Aggregate Report和Summary Report监听器
- 执行测试,记录三类文本长度的TTFT/TPOT基线数据
预期产出:一份包含P50/P95/P99统计的延迟基线报告,作为后续性能调优的对比基准。
任务2:对比不同量化精度下的推理性能
目标:量化评估FP16、INT8、INT4三种精度在推理吞吐、延迟和显存消耗上的差异,为部署选型提供数据支撑。
测试矩阵:
| 模型 | 量化精度 | 并发数 | 输入长度 | 输出长度 | 关注指标 |
|---|---|---|---|---|---|
| Qwen2-72B | FP16 | 1 / 4 / 8 | 1024 | 512 | 基线参考 |
| Qwen2-72B | INT8 | 1 / 4 / 8 | 1024 | 512 | 吞吐提升倍数 |
| Qwen2-72B | INT4 | 1 / 4 / 8 | 1024 | 512 | 极限吞吐+精度损失 |
关键操作:
- 分别部署FP16/INT8/INT4版本的推理服务,确保其他配置一致
- 使用JMeter或vLLM benchmark工具执行相同测试用例
- 同步采集GPU利用率(nvidia-smi dmon)和显存占用数据
- 绘制「并发数 vs 吞吐量」和「并发数 vs P95 TTFT」双维度对比图
- 关联精度评测数据,计算「性能提升 / 精度损失」的ROI比值
任务3:并发压力测试——探寻系统容量上限
目标:通过阶梯式加压找到当前部署方案的最大并发承载能力,验证排队和限流机制的有效性。
压测方案:
- 初始并发数:1,每60秒增加5个并发线程
- 终止条件:P95 TTFT > 3秒 或 错误率 > 1%
- 每个阶梯使用混合Prompt(短:中:长 = 3:4:3)模拟真实流量分布
分析维度:
- 容量拐点识别:找到TTFT开始急剧恶化的并发数阈值
- 排队行为验证:当请求数超过处理能力时,服务端是否正确返回429或排队等待
- 资源瓶颈定位:通过GPU监控数据判断瓶颈是显存容量、显存带宽还是计算单元
- 恢复能力测试:在压力达到峰值后迅速降载,观察性能是否能在1分钟内回归基线
📋 案例研究:银行智能客服的LLM推理性能测试
背景:某大型商业银行计划将智能客服系统升级为基于大语言模型的方案,需支持500并发用户,端到端响应时间 < 3秒。在正式上线前,需要进行全面的推理性能测试以验证系统能力。
测试过程
- 使用 JMeter 对LLM API进行阶梯式并发测试(Stepped Ramp-Up)
- 并发从 50 逐步增加到 500,每阶梯持续5分钟
- 监控指标:TTFT(首Token时间)、TPOT(每Token时间)、端到端延迟、错误率
- 对比了 7B 和 70B 两种参数规模模型的性能差异
- 测试环境:4×A100-80GB GPU,vLLM推理框架,FP16精度
测试结果
| 并发数 | 7B TTFT (ms) | 7B TPOT (ms) | 7B E2E (s) | 70B TTFT (ms) | 70B TPOT (ms) | 70B E2E (s) | 错误率 |
|---|---|---|---|---|---|---|---|
| 50 | 180 | 22 | 0.82 | 420 | 35 | 1.45 | 0.00% |
| 100 | 215 | 26 | 1.05 | 560 | 42 | 1.92 | 0.00% |
| 200 | 310 | 32 | 1.58 | 1040 | 55 | 2.76 | 0.02% |
| 300 | 520 | 45 | 2.35 | 1820 | 78 | 4.12 | 0.15% |
| 400 | 780 | 58 | 3.20 | 2850 | 110 | 6.80 | 0.48% |
| 500 | 1100 | 75 | 4.50 | — | — | — | 3.20% |
关键发现:
- 🟡 300并发时系统达到性能拐点,7B模型的TTFT从310ms(200并发)急剧攀升至520ms
- 🔴 70B模型在200并发时TTFT已超过1秒阈值(1040ms),不适合高并发场景
- 🟢 7B模型可支撑500并发,但TTFT达到1.1s且E2E超4.5s,超出3s目标
- ❌ 70B模型在500并发时3.2%错误率,基本不可用(表中标记"—")
- 找拐点比测极值更重要:性能测试的目标不是简单测出"最大并发数",而是找到TTFT突然恶化的拐点(本例为300并发),这才是系统的实际可用容量上限。
- 模型大小与并发能力需权衡:70B模型回答质量更优,但并发能力远弱于7B。在延迟敏感的场景下,小模型往往是更务实的选择。
- 混合部署策略:建议采用两级路由——简单高频问题(余额查询、密码重置等)由7B小模型处理,复杂长尾问题由70B大模型异步处理,兼顾性能与质量。