LLM推理性能测评:从核心指标到压测脚本的工程实践指南
2026/9/6 2:51:30 网站建设 项目流程

最近很多人问我:同样是跑一个大模型,为什么别人汇报的“推理性能”那么亮眼,自己一上线就卡成 PPT?延迟数字看着都挺低,放到生产环境里怎么完全对不上?

这不是模型不行,也不是 GPU 不够,而是大多数人对LLM Inference Benchmarking的理解还停留在“找个脚本跑一下,看个 token 数”的阶段。

做语言模型评测(Evaluation)和做推理性能评测(Benchmarking)是两回事。前者关心“回答得对不对”,后者关心“回答得多快、多稳、多省”。两者都需要测,但方法论完全不同。本文要讲的是后者:如何科学地评测一个 LLM 推理系统的性能

这篇文章会带你走通从“概念 → 指标 → 测试脚本 → 结果分析 → 生产环境风险”的完整链路。如果你正在做模型部署、网关开发、推理框架选型,或者要给业务方一个可信的容量评估报告,这篇文章值得收藏。

1. 为什么 LLM 推理 Benchmarking 这么容易被误读

先回答一个核心问题:为什么同样是 70B 模型,有的团队说吞吐能到几千 token/s,有的团队说首字延迟要两秒?两个数字都对,只是测的不是同一个东西。

很多团队在宣传某个推理框架时,喜欢报一个“极端吞吐数字”。这个数字通常是在长 prompt、允许高延迟、大 batch 的条件下压出来的,并不代表真实用户体验。

反过来,在移动端或网页聊天场景,真正的决定因素是“用户从按下回车到看到第一个字的时间”。这个指标叫TTFT(Time To First Token),它和吞吐的关系是矛盾的:为了压低首字延迟,你可能不敢把请求攒太多再一起做 batch;而为了把吞吐做高,你可能要牺牲单用户的首字体验。

这就引出推理 Benchmarking 的第一个原则:不要问“这个推理服务有多快”,要问“在某个人群、某个请求分布、某个成本约束下,它能不能达标”

实际中最常见的误解是:

  • 直接用单并发测试结果估计生产容量。
  • 不区分 prefilling(预填充)和 decoding(解码)阶段的耗时。
  • 只测平均延迟,不测尾延迟。
  • 没有预热模型,拿缓存冷启动的数据充数。
  • 压测时间太短,没有观察显存碎片和内存增长。

这些坑后面都会一一展开。

2. LLM 推理的基本流程与 Benchmarking 的关系

要测准,先得搞清楚一个请求在推理系统里到底经历了什么。

2.1 推理全过程拆解

一次 LLM 请求可以被分成两个阶段:

Prefilling(预填充)阶段。用户输入的 prompt 被编码成 token,模型需要把这些 token 并行地“读进去”,层层计算并生成 Key/Value 缓存(KV Cache)。这个阶段是计算密集型的,GPU 利用率往往可以拉得很高。它的耗时大致和 prompt 长度成正比。

Decoding(解码)阶段。模型每生成一个 token,就把这个 token 放进序列,再次读取 KV Cache,预测下一个 token。这个阶段是访存密集型的,瓶颈往往不是算力,而是显存带宽。

有意思的是,实测一个 7B 模型时,Decoding 阶段单个 token 的耗时可能远大于你按 Prefilling 阶段的总 token 数除以同时长算出来的均值。如果你用“总 token 数 / 总时间”去推单 token 时延,会有偏差。

2.2 动态 Batching 让情况变得更复杂

现代推理服务不会一次只处理一个请求。多个请求会被Continuous Batching(连续批处理)或类似机制拼在一起。

当一个请求处于 Decoding 阶段、正在慢慢吐词时,另一个新请求的 Prefilling 阶段可以插进来,共享一颗 GPU。这种方式大幅提高了吞吐,但也让“某个请求的延迟”不再是独立变量,而是取决于此刻 GPU 上还有多少并发请求。

因此,在 Benchmarking 时必须明确并发模型、请求到达模式、输入输出长度分布。否则,任何延迟数字都只是在特定环境下的一次快照。

3. LLM 推理性能评测的核心指标

评测一个 LLM 推理服务,不能只测一个指标。这里列出一组最常用的指标,以及它们各自在什么场景下最重要。

3.1 TTFT(Time To First Token)

定义:从请求发起到服务端返回第一个 token 的耗时。聊天场景的“字出来得快不快”,主要看它。用户通常希望 TTFT 在 1 秒以内,这也是学术界和工业界最常见的 SLA 分位线。

3.2 TPOT(Time Per Output Token)

定义:解码阶段每输出一个 token 的平均耗时。计算方式可以是用生成阶段的墙钟时间除以生成的 token 数量。用户感知的“打字速度”就是由 TPOT 决定的。

3.3 ITL(Inter-Token Latency)

定义:两个相邻输出 token 之间的时间间隔。TPOT 是一个平均值,ITL 能看出输出是否均匀。如果某段时间 GPU 在忙别的大请求,ITL 可能突然飙升,表现为模型的字“卡一下才继续出来”。

3.4 Throughput(吞吐)

定义:服务在单位时间内能处理的请求数或生成的 token 数。常用的有:

  • requests/s
  • output tokens/s

Batch 推理和在线交互场景对吞吐的定义不完全一致,建议报告时注明是“总输出 token / 总墙钟时间”。

3.5 并发与 QPS(Query Per Second)

在固定并发数下测试,可以得到不同 QPS 时的延迟曲线。注意:QPS 提升到一定程度后,队列会排队,延迟会非线性上涨。评测时最好能找到那个“服务还能稳定运行”的拐点。

3.6 分位延迟

只看 p50 是不够的,尾延迟更重要。在生产环境,核心指标建议看 p95、p99,聊天场景甚至要看 p99.9。p99 延迟很高但 p50 很低的系统,说明存在明显的长尾波动,可能是热点请求或批调度不均。

指标关心问题典型影响场景
TTFT用户多久看到第一个字在线对话、语音交互
TPOT生成的整体速度是否流畅写作助手、翻译、总结
ITL输出过程是否卡顿流式输出、代码补全
Throughput单位时间能服务多少请求离线批处理、容量规划
p99 尾延迟最差体验是否能接受企业级 SLA、网关超时
显存占用能否稳定运行不 OOM长上下文、高并发服务

4. LLM 推理 Benchmarking 的常见方法框架

很多人第一次做推理压测,第一反应是“用 curl 发一个请求,看时间”。这不是不对,而是不够系统。

下面梳理一套可复用的评测流程,这套流程不会绑死某个供应商,而是适配绝大多数 OpenAI 兼容协议的推理服务。

4.1 评测步骤总览

  1. 确定评测目标:你是想看容量上限,还是想验证一个用户路径的体验。
  2. 校准输入负载:使用与真实场景匹配的 prompt 长度、输出长度和并发数。
  3. 准备标准数据集或合成请求。
  4. 预热服务。
  5. 运行多轮测试,延长观测时间。
  6. 记录原始日志并计算指标。
  7. 分析长尾和异常,输出结论。

4.2 需要注意的干扰因素

  • GPU 频率波动和散热:相邻测试之间要留冷却时间,否则结果会受到影响。
  • 显存缓存:服务端如果开了 prefix caching,会显著降低重复 prompt 的耗时。这可能导致结果不可比。如果目标是评测“系统能力”,建议在测试中混入足够多的 prompt 前缀;如果是评测“真实场景”,可以把缓存命中率作为变量记录。
  • 日志输出:打印每步的 debug 日志会消耗 CPU 并掩盖真实请求开销。
  • 网络延迟:如果并发压测的客户端和服务端在同一台机器,网络开销会被低估。建议将压测客户端部署在与服务端低延迟但稳定的网络环境中。

5. 环境准备与最小实验设计

这里给出一套可以照着操作的最小方案。假设你手上有一张支持量化运行的 GPU(如果没有,可以用满足最低显存需求的云主机 + CPU 推理来体验流程,指标趋势仍然有参考价值)。

5.1 组件说明

组件作用
推理服务端加载模型并暴露 HTTP API
压测客户端模拟并发请求,收集响应
数据集定义请求的 prompt 与期望输出长度

如果你要快速跑通整个 Benchmarking 流程,我推荐用以下技术栈之一:

  • vLLM 或 SGLang 作为 OpenAI 兼容的推理服务
  • 自写 Python 多线程脚本作为压测客户端
  • 用一个包含 200~2000 条 prompt 的测试集作为负载

5.2 模型与硬件说明

本文不限定具体模型名称,运行命令中出现的模型路径需要替换为你的本地模型路径或你有权通过 API 访问的模型标识。

如果你的 GPU 显存在 24GB 以下,建议优先尝试 7B~14B 的量化模型来跑通流程;如果显存充足,则用 70B 以上模型观察 Prefill/Decode 的差异。推理框架的版本请以官方最新稳定版为准。

5.3 启动一个最小推理服务

这里以 vLLM 为例。vLLM 暴露的 API 兼容 OpenAI 格式,便于后面用统一脚本压测。

先安装依赖:

pip install vllm

启动服务(需要按实际模型路径替换):

python -m vllm.entrypoints.openai.api_server \ --model /path/to/your-model \ --served-model-name test-model \ --gpu-memory-utilization 0.90 \ --max-model-len 8192 \ --port 8000

启动成功后,服务会打印监听的地址。我们可以先用一个请求验证服务正常:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "test-model", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}], "max_tokens": 128, "stream": false }'

返回结果里有usage.completion_tokens,说明服务端可以正常生成内容。

6. 用 Python 实现一个可复用的推理性能测试脚本

这里给出一个用标准库 + requests 实现的压测客户端。它足够简洁,不会引入太重的学习成本,同时又覆盖了 TTFT、TPOT、吞吐和分位延迟的计算。

6.1 脚本功能说明

脚本会做几件事:

  1. 从文件中读取若干 prompt。
  2. 使用线程池模拟并发请求。
  3. 每个请求记录请求开始时间、首 token 时间、首个 token 结束时间、完整完成时间。
  4. 计算 TTFT、TPOT、总耗时。
  5. 汇总输出 p50 / p95 / p99 和吞吐。

6.2 完整代码示例

# 文件路径:bench_llm.py import json import time import threading import statistics from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from typing import List, Optional import requests API_URL = "http://127.0.0.1:8000/v1/chat/completions" MODEL_NAME = "test-model" MAX_TOKENS = 256 CONCURRENCY = 8 REQUESTS_PER_WORKER = 10 PROMPT_FILE = "prompts.jsonl" @dataclass class RequestResult: ttft: Optional[float] = None # 首 token 延迟 completion_time: Optional[float] = None # 总完成时间 output_tokens: int = 0 total_tokens: int = 0 error: Optional[str] = None def load_prompts(path: str, limit: int = 500) -> List[str]: prompts = [] with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue obj = json.loads(line) prompts.append(obj.get("prompt", "")) if len(prompts) >= limit: break if not prompts: prompts = ["请用中文写一段关于人工智能发展的简介。"] return prompts def send_one_request(prompt: str) -> RequestResult: result = RequestResult() payload = { "model": MODEL_NAME, "messages": [{"role": "user", "content": prompt}], "max_tokens": MAX_TOKENS, "stream": True, } start = time.perf_counter() try: with requests.post(API_URL, json=payload, stream=True, timeout=120) as resp: resp.raise_for_status() first_token_time = None accumulated = "" for raw_line in resp.iter_lines(decode_unicode=True): if not raw_line: continue if raw_line.startswith("data:"): data_str = raw_line[len("data:"):].strip() else: data_str = raw_line if data_str == "[DONE]": break try: chunk = json.loads(data_str) except json.JSONDecodeError: continue choices = chunk.get("choices") or [] if not choices: continue delta = choices[0].get("delta") or {} text_piece = delta.get("content") or "" usage = chunk.get("usage") if text_piece and first_token_time is None: first_token_time = time.perf_counter() if usage: result.output_tokens = usage.get("completion_tokens", 0) result.total_tokens = usage.get("total_tokens", 0) end = time.perf_counter() if first_token_time is not None: result.ttft = first_token_time - start result.completion_time = end - start except Exception as exc: result.error = str(exc) return result def run_benchmark() -> List[RequestResult]: prompts = load_prompts(PROMPT_FILE) tasks = [] for worker in range(CONCURRENCY): for _ in range(REQUESTS_PER_WORKER): tasks.append(prompts[(worker + len(prompts)) % len(prompts)]) results: List[RequestResult] = [] lock = threading.Lock() def worker_task(prompt: str): r = send_one_request(prompt) with lock: results.append(r) with ThreadPoolExecutor(max_workers=CONCURRENCY) as executor: futures = [executor.submit(worker_task, p) for p in tasks] for f in futures: f.result() # 等待所有线程结束 return results def _percentile(values: List[float], percent: float) -> Optional[float]: if not values: return None sorted_values = sorted(values) idx = max(0, int(len(sorted_values) * percent / 100) - 1) return sorted_values[idx] def print_report(results: List[RequestResult]): ttft_values = [r.ttft for r in results if r.ttft is not None] total_time_values = [r.completion_time for r in results if r.completion_time is not None] total_output_tokens = sum(r.output_tokens for r in results) wall_time_start = time.perf_counter() time.sleep(0.001) # 这里不用做太多时间统计,主要展示汇总结果 print("========== LLM Inference Benchmark Report ==========") print(f"Total requests: {len(results)}") print(f"Error requests: {len([r for r in results if r.error])}") print(f"Total output tokens: {total_output_tokens}") print() if ttft_values: print(f"TTFT p50 : {_percentile(ttft_values, 50):.3f} s") print(f"TTFT p95 : {_percentile(ttft_values, 95):.3f} s") print(f"TTFT p99 : {_percentile(ttft_values, 99):.3f} s") if total_time_values: print(f"Total time p50: {_percentile(total_time_values, 50):.3f} s") print(f"Total time p95: {_percentile(total_time_values, 95):.3f} s") print("=====================================================") if __name__ == "__main__": results = run_benchmark() print_report(results)

需要说明,脚本中的 usage 信息并不是所有推理服务都会在流式响应里带最后一条 usage 数据。如果服务端不返回 usage,你可以改用字符级统计或按 tokenizer 估算。这段代码更重要的价值在于给你展示了如何记录首 token 时间和完整时间。

6.3 准备测试数据

可以在prompts.jsonl中准备一批中文文本,每行是一条 JSON:

{"prompt": "请写一篇关于量子计算的科普短文,不少于200字。"} {"prompt": "解释一下什么是反向传播算法,并给出一个教学案例。"} {"prompt": "帮我列出云计算和边缘计算的区别,使用表格回答。"}

在生成测试数据时,记得控制前几个测试固定为短 prompt、短输出,以便快速验证链路;后续再加长 prompt。

6.4 运行压测

python bench_llm.py

运行结束后,脚本会输出 TTFT 的 p50 / p95 / p99,以及总完成时间。如果 p95 远远高于 p50,说明并发调度有不稳定因素,值得继续排查。

7. 如何分析测试结果:不是所有延迟高都怪模型

拿到结果后,第一反应不要太快。以下是我建议的分析顺序。

7.1 先看错误率

如果错误率超过 0,说明系统在目标并发下已经不稳定了,后面的指标参考价值有限。优先排查超时、连接拒绝、显存不足等错误。

7.2 看 TTFT 是否符合用户体验预期

如果 TTFT 的 p95 超过 2 秒,聊天类业务的体感就会非常差。此时要关注:

  • 请求是否在排队。
  • 服务端最大并发数是否设置得太低。
  • Prefill 阶段是否过于耗时。

7.3 看 TPOT / ITL 是否均匀

如果总完成时间正常,但 ITL 有显著波动,说明解码阶段被长请求的 Prefill 挤占了。部分推理服务允许开启chunked prefill,把长 prompt 的预填充拆碎,穿插到解码流程中,即“分块预填充”。那样可以降低 ITL 波动,但会轻微增加响应整体的处理耗时。

7.4 对比不同并发下的吞吐曲线

建议将并发数从 1、2、4、8、16、32 依次加压。记录每个并发下的 Throughput 和延迟。你会发现:

  • 并发太低时:显存可能没满,吞吐没有拉满。
  • 并发逐步提高:延迟缓慢增长,吞吐持续上升。
  • 并发超过某个阈值:延迟快速上涨,吞吐增长变缓甚至下降。

这个“拐点”对容量规划非常关键。如果生产环境要求 p99 延迟低于某个阈值,就不应该把并发压到理论最高吞吐点。

8. LLM 推理 Benchmarking 常见误区与排查方法

误区可能后果排查与解决
只在单并发下测延迟严重低估生产环境的排队开销至少按 1/8/16/32 多组并发测试
请求长度不一致平均延迟被长请求带偏,难以横向对比按输入输出长度分段统计
不预热直接压测第一次请求需要加载 CUDA kernel 和权重,结果异常高先发 10~50 个请求预热,观察稳定后重新测试
测试时间太短长时间运行后可能出现显存碎片、OOM、变慢至少持续 5~10 分钟,观察趋势
忽略客户端网络开销延迟被高估,主要是网络等待将客户端部署在靠近服务端的可靠网络,或做本地回环测试
混淆 Prefill 和 Decode 优化只看总耗时,难以定位瓶颈分开记录 prefill 与 decode 耗时
使用完全重复 promptprefix cache 让结果失真准备多样化的 prompt,并注明是否命中缓存
依赖默认的全局温度与采样参数不同采样路径耗时不同固定 seed 和 sampling 参数,保证可复现

真实项目里最容易被忽略的是“预热不足”。有团队曾把预热不足时的 TTFT 当成服务真实水平,结果在压测报告里写了一个比正常值高几倍的数字,导致容量评估出现偏差。

9. LLM 推理评测结果与生产容量规划的对接

Benchmarking 的最终目的是服务上线前的容量规划。这里给一个简单的换算思路。

假设你有以下测量值:

  • 单请求平均输出长度:300 token
  • 单并发下 TPOT:20 ms/token,即 50 token/s
  • 目标吞吐:500 并发用户中,可能有 50 个同时在线请求

你可以估算:

  • 单个请求平均耗时约为 300 * 20ms = 6 秒。
  • 如果一个用户平均每 30 秒发一次请求,单用户稳态负载约为总时间占比的 20%。
  • 50 个活跃用户同时对系统产生请求的概率需要用排队论估算,但通常取一个合理的并发因子。

在容量规划中,最忌讳的是“用户数乘以最差情况延迟”的粗暴估算。正确做法是建立一个小规模真实负载压测平台,利用采集到的 p95 延迟和吞吐曲线反推网关限流阈值。

如果团队还没有建设压力测试平台,建议第一步先做两件事:

  1. 用结构化日志记录每次请求的 prompt 长度、输出长度、TTFT、TPOT。
  2. 在网关层暴露这些指标到监控,便于后续做容量预测。

10. 面向真实场景的 LLM 推理评测实践建议

10.1 不同场景应该使用不同侧重点

场景不同,Benchmarking 的指标权重完全不同:

  • 智能客服:TTFT 要低,同时要有足够大的并发吞吐支持。
  • 离线数据处理:吞吐优先,延迟不敏感,允许较大 batch。
  • 代码补全:ITL 很重要,因为用户正在打字,模型给出的补全需要跟上节奏。
  • Agent 工具调用:总延迟和函数调用准确性更关键,因为可能涉及多轮工具调用。

必要时应该在评测报告里把指标按场景拆开,而不是只给一张总表。

10.2 固定采样与随机种子

采样策略会显著影响生成 token 数量。同一个 prompt,如果 temperature 很高,模型可能输出更长或更短的内容,导致总耗时不可比。评测时建议固定temperature=0、固定seed,并且固定max_tokens

10.3 prompt 长度分布要尽量贴近真实

不要只测“短 prompt 长输出”或“长 prompt 短输出”。真实业务里的用户输入长度通常呈偏态分布:

  • 客服场景 prompt 包含用户问题 + 历史上下文,可能很长。
  • 闲聊场景 prompt 很短,但输出长度随机。
  • RAG 场景中,文档检索结果和指令模板拼接后,prompt 可能达到 2000~6000 token。

你可以从线上日志里抽样统计 prompt 长度分布,然后按比例构造压测请求。

10.4 保护生产集群与数据

压测时如果目标服务连接的是包含用户数据的生产环境,一定要确认请求不会写入实际业务库,也不会触发发送短信、邮件等外部动作。评测 LLM 推理性能还需要注意:

  • 使用最小权限账号启动服务。
  • 在测试环境或隔离的 GPU 资源组中执行。
  • 如果必须对生产服务压测,先和平台与运维团队确认容量水位,建议从低并发开始逐步加压。

11. LLM 推理性能评测的主流工具与配套组件

如果你不想重复造轮子,可以了解下面几类工具:

11.1 推理服务框架自带压测能力

  • vLLM 社区有一些 benchmark 脚本,通常在仓库的benchmarks目录下,用于测试吞吐和延迟。
  • SGLang 同样提供官方 benchmark 脚本。
  • TensorRT-LLM 部署后可用inflight_batcher_benchmark执行基准测试。

这类工具与框架本身深度绑定,适合评估框架内部不同参数的效果。

11.2 通用 API 压测工具

如果你已经通过网关对外提供服务,API 层压测更适合使用通用工具。这类工具能更真实地还原网络链路。

  • k6:支持 TypeScript 编写压测脚本,可以较方便地模拟流式响应。
  • Locust:Python 生态,写请求逻辑更自由,容易和自定义指标采集集成。
  • wrk / ghz:偏 HTTP/gRPC 层压测,适合攻坚持久层能力。

11.3 可观测性工具

指标的可靠性最终取决于可观测性系统。建议接入 Prometheus + Grafana,对推理服务的关键指标做实时监控。要注意区分“业务指标”和“系统指标”。

系统指标,如 GPU 利用率、显存使用、显存带宽、GPU 温度,适合通过 NVIDIA 官方的dcgmnvidia-smi采集。

业务指标,如 TTFT、TPOT、吞吐、p99,适合在推理服务端或网关层通过日志埋点导出。

11.4 引入 Profiling 定位阶段瓶颈

如果发现性能不符合预期,只靠黑盒压测仍然不够。你还需要结合 profiler 观察 GPU kernel 的耗时占比,判断瓶颈究竟是:

  • 显存带宽不足(解码阶段常见)。
  • 计算核心未打满(prefill 阶段受 padding 和注意力机制影响)。
  • CPU 预处理慢(prompt 过长时 tokenizer 和 batch 组包开销可能被忽视)。
  • 调度线程不足(并发请求多时,处理不足会导致排队)。

NVIDIA 提供 CUDA 事件计时和 profiling 工具,可以用来对照。

12. 一次完整评测的示例输出与解读

下面是一份虚构但结构完整的示例报告片段,方便你理解如何给团队讲清楚结果。

并发数QPSTTFT p95TPOT 均值输出吞吐 tokens/sp99 总延迟
10.30.45s45ms226.8s
41.20.62s52ms987.4s
82.10.91s63ms1748.5s
163.01.85s88ms21012.3s
323.14.20s142ms22519.0s

从这张表里可以读出几个信息:

  1. 并发从 1 涨到 8,吞吐涨得很明显,TTFT 仍在 1 秒以内,这个区间属于“健康区”。
  2. 并发到 16 时,吞吐在持续提升,但 TTFT 已经逼近两秒,说明开始有明显的排队等待。
  3. 并发到 32 时,QPS 几乎不涨,但延迟继续恶化,这是典型的“过载拐点”。

对于在线聊天业务,这张表直接告诉你:并发最好不要超过 8~12;对于离线批处理任务,你可以继续用 32 并发压榨吞吐,只要不超时即可。

换句话说,同一套系统,服务于不同类型业务时,应该采取不同的并发与限流策略。这也是为什么“单点最优”指标没有意义,必须结合业务诉求来解读。

13. 跨框架对比时需要注意的细节

团队做技术选型时,经常要对 vLLM、SGLang、TensorRT-LLM、llama.cpp 等框架做横向 Benchmarking。跨框架对比比单独评测一个系统更复杂,因为要保证公平性。

需要注意这些点:

  • 使用同一模型权重文件和同一精度(FP16 / BF16 / INT8 / INT4)。
  • 固定相同的max_model_lengpu_memory_utilization。显存分配策略不同,可能会影响后续 batching 的上限。
  • 设置相同的采样参数和max_tokens
  • 尽量使用同一版本的 CUDA、PyTorch 和 GPU 驱动。推理框架对底层运行时的依赖差异显著。
  • 预热时间和总测试时长需要一致。
  • 需要确认是否开启 prefix caching,或明确标注缓存命中情况。

跨框架对比不可能做到绝对公平,但至少要让所有被测对象运行在同一个目标环境下,并把可调参数调整到各自较为合理的生产配置。否则“谁跑得快”只能反映“谁的默认参数更好”。

14. 生产环境长期评测与常态监控建议

性能评测不是上线前做一次就结束了。模型在迭代、框架在升级、GPU 驱动在变化,线上流量分布也在变化。推理性能的可观测性应当成为平台工程的一部分。

建议把以下几项纳入常态化监控:

14.1 业务延迟分层监控

在网关层记录“客户端到网关”“网关到推理服务”“推理服务响应首字”“完成全部输出”几个阶段的时间,这样可以快速定位延迟来自网络链路、排队还是模型生成。

14.2 限流与排队指标

在推理服务入口记录排队请求数。如果队尾持续增长,说明上游 QPS 已经超过服务容量,仅看平均延迟可能被整体抬高。

14.3 长尾分布跟踪

延迟监控不仅要有平均值,还要保留 p95、p99 甚至 p99.9。任何短时间抖动都会在长尾上反映出来。

14.4 请求内容分布监控

由于 LLM 推理耗时和输入输出长度强相关,建议对 prompt token 数与 completion token 数做分桶统计,比如 0~128、128~1024、1024~4096。当某个桶的延迟突然变高时,能够更准确地定位问题。

14.5 版本变更验收流程

每次升级推理框架版本或修改服务参数,都应该重新跑一次既有的基准测试集,形成对比报告。没有经过基准测试的版本,尽量不直接发布到生产。

15. LLM 推理优化的下一步方向

如果你已经能完成标准 Benchmarking,下一步可以从这些方向继续深入。

15.1 Speculative Decoding(推测解码)

通过小模型先草拟多个 token,再交给大模型一次性验证,可以降低解码阶段的耗时。但这种优化对 benchmark 的影响比较微妙:单请求延迟可能下降,但吞吐不一定线性上升。压测时也需要评估 CPU 侧额外开销。

15.2 Prefix Caching(前缀缓存)

如果业务中存在大量共享系统提示词或历史文档片段,缓存这些前缀的 KV 可以大幅降低 TTFT。基准测试时如果开启该功能,必须设计带共享前缀的请求集,否则测不出预期收益。

15.3 量化与 KV Cache 压缩

使用 FP8 / INT8 / INT4 量化可以把模型压缩到更小,部分场景能让显存容纳更多并发。但量化后的精度损失需要结合任务效果判断。压缩 KV Cache 同理,都需要引入“模型质量和推理性能”的综合评估,否则容易片面追求速度。

15.4 多机多卡分布式推理

当单卡放不下模型时,需要考虑张量并行或流水线并行,并行度提升会带来通信开销。Benchmarking 时必须记录卡数和并行策略,否则无法归因到底层是通信瓶颈还是计算瓶颈。

15.5 与 RAG/Agent 链路结合

真实业务往往不是简单的一来一回,而是多次工具调用和多轮检索。评测范围如果只到单次 LLM 请求,可能低估系统整体延迟。更完整的评测应该包含:

  • 请求是否经过检索。
  • 提示词构建是否耗时。
  • 工具调用是否触发了外部服务的额外等待。
  • 多轮对话历史是否需要重算。

16. 给不同角色的最终建议

如果你是算法工程师,重点研究模型与推理框架的组合收益,但不要只报一个好看的数字。建议把评测脚本和测试数据版本化管理,方便后续对比。

如果你是后端工程师,重点把延迟和吞吐接口标准化,把指标接入告警,让性能问题在上线前就暴露。

如果你是技术管理者,不要追求“最大化吞吐”这一个目标。先明确业务对延迟的底线,再反推并发上限和基础设施成本。评测结论最好包含“在什么条件下,系统能服务多少用户,成本是多少”,这样才具备决策价值。

17. 小结与直接可执行的下一步

LLM Inference Benchmarking 不是跑一个脚本、拿几个指标就结束的“一次性动作”,而是从测试设计、指标定义、并发模型、生产对接、长期监控串起来的一套工程能力。

这篇文章覆盖的核心链路是:

  • 理解 LLM 推理的 Prefill/Decode 两阶段以及动态批处理对指标的影响。
  • 掌握 TTFT、TPOT、ITL、吞吐、分位延迟等指标的含义。
  • 知道如何用 OpenAI 兼容协议的服务配合自写脚本做最小压测。
  • 能够判断测试结果是否可靠,避开缓存、预热、请求分布等坑。
  • 能够把单次压测结果转化成容量规划和业务监控的依据。

如果你现在正准备开始一次推理性能评测,建议按下面的顺序行动:

  1. 选定一个推理框架并在隔离环境启动模型服务。
  2. 先跑 20 个单并发请求,确认链路通、指标能取到。
  3. 再按 1/4/8/16/32 五档并发跑完整流程。
  4. 记录每组结果的 TTFT、TPOT、p99 和吞吐。
  5. 把产出整理成一份“不同并发下延迟和吞吐对照表”,用于后续与业务方对齐容量预期。

推理性能评测的真正难点不在遥不可及的底层代码,而在于你是否能把“模型生成策略、推理服务、请求负载、并发模型”这几个变量控制住,然后诚实回答一个问题:这套系统在业务真正需要的并发下,体验还是不是达标的。只要你的评测流程是可控、可复现、可解释的,你就已经比大多数只会在群里晒吞吐数字的团队靠谱了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询