
在 LLM 应用从原型走向生产环境时最容易被低估的工作就是 LLM 推理基准测试LLM Inference Benchmarking。这里的基准测试不是让模型做几道题也不是用几个开源评测集看答题得分而是把模型部署到推理框架后用真实请求测量“服务能不能扛得住、快不快、稳不稳”。推理耗时、吞吐量、排队时间、失败率、并发上升后的退化曲线每一项都直接影响成本、用户体验和后端容量规划。这篇文章会围绕一个完整思路展开先明确要测哪些指标再搭一个可复现的测试环境然后用流式和非流式请求完成单次延迟测量接着做并发热度负载最后处理一批容易被测错的细节。你不一定需要 GPU 服务器才能读完全文但落地基准测试时最好有一张可用的推理卡或者一个已部署的云端推理端点。1. 先把 LLM 推理基准测试的目标说清楚1.1 为什么“模型能力”和“推理性能”不能混在一起很多刚接触 LLM 项目的开发者会把两个方向搞混一个是评测模型回答质量另一个是评测系统推理速度。前者属于模型能力评估常见做法是让模型做数学题、代码题、阅读题再和人写好的标准答案比较分数后者属于系统性能评估关心的是请求到达服务后模型多久吐出第一个 token、生成完整个回答需要多长时间、一小时内能完成多少个请求。两种评测对工具和数据的要求完全不同。能力评测选择的是任务样本模型答完还要做打分逻辑推理基准测试选择的是提示词长度、输出长度、并发数、请求格式甚至要控制是否流式返回。在项目规划阶段就把二者分开后面设计实验时才不会出现“准确率高所以性能一定好”的错误判断。推理基准测试的核心目标是验证部署形态。同一套模型权重放在 PyTorch 原生推理和放在 vLLM、TGI、SGLang 这类专用推理框架中性能差异可能很大同一套推理框架开启连续批处理、并行采样、前缀缓存、PD 分离后结果又不一样。基准测试真正衡量的是“当前部署方案在当前硬件条件下对真实请求模式的处理边界”。1.2 推理基准测试要看哪些核心指标在 Web 后端做压力测试时一般只需要关注 QPS、平均响应时间、错误率但 LLM 推理还不够。因为一次 LLM 请求要经历两个阶段预填充阶段读取完整 prompt逐 token 生成阶段按顺序输出内容。客户端看到“响应时间”之前还包含网络等待、服务排队、预填充计算和时间过长的时间段必须把一次请求拆成多个观测点。下面这张表是推理基准测试里最常见的指标也是搭建压测脚本前必须记清楚的术语。指标英文缩写含义常见用途首 token 延迟TTFT, Time To First Token从请求发出到收到第一个内容 token 的时间衡量用户“开始看到回答”的速度单 token 间隔TPOT, Time Per Output Token生成每个输出 token 的平均耗时反映解码阶段快慢端到端延迟End-to-End Latency从请求发出到完整响应结束的耗时衡量单次请求完整体验成功请求吞吐Requests Per Second每秒完成的成功请求数衡量服务整体请求容量输出 token 吞吐Tokens Per Second每秒生成并返回的输出 token 数量衡量推理框架的生成能力错误率Error Rate超时、断连、非 200 状态码的请求占比评估服务稳定性并发用户数Concurrency同一时刻在途的请求数量模拟不同用户规模从服务端视角看TTFT 更接近“算力资源是否充足”因为输入 prompt 越长预填充阶段计算越多TTFT 越高从客户端视角看TPOT 更接近“字出现得流不流畅”。两个指标相互独立一个服务可能出现 TTFT 很低但 TPOT 很高的情况也可能出现 TTFT 很高但一旦开始生成就非常快的情况。只记录总耗时不能满足生产需要因为模型输出是逐字产生的用户在一个长回答开始前必须等待第一个 token这个等待体验完全不同于完整响应时间。日志里如果只有“平均响应 1500 毫秒”就无法知道用户是等了 1400 毫秒看到首个字还是立刻看到返回后一字一句慢慢等了 1400 毫秒。1.3 为什么不能只追求“每秒输出 token 多”LLM 服务有一个和普通 Web 服务很不一样的特点高吞吐往往以牺牲单请求延迟为代价。推理服务一次可以处理很多并发请求但 GPU 显存和算力有限连续批处理会把多个请求的生成任务排在一起做批量矩阵计算。请求越多单次推理的吞吐总量越高但具体到单个请求排队和等待时间都会被拉长。因此压测报告如果不能同时给出“并发数、P50 延迟、P99 延迟、吞吐量”四个数据就很难判断结果是否健康。只看吞吐量会发现并发升高后每秒 token 数上去了但用户侧的 TTFT 可能已经高到不可用。反过来只保持极低并发会发现延迟表现很好但部署资源大量浪费。制定性能目标时应面向业务场景。例如实时对话机器人可能需要“P50 TTFT 小于 800 毫秒P95 完整回答不超过 15 秒”离线批处理则完全不在乎单个回答慢只要单位时间内处理完足够多的文档就行。因此在开始压测前建议先写下至少一条可验证的 SLO比如“在 300 并发下P95 完整响应时间小于 20 秒成功率不低于 99.5%”。后续所有测试都围绕这个目标设计而不是零散地跑几个 curl 看数字。2. 先搭一个可复现的测试环境2.1 硬件、软件和模型条件要写清楚推理基准测试最大的敌人是“不可复现”。换一张 GPU、换一个 CUDA 版本、换一个推理框架 commit结果都会变同一 GPU 上如果开着其他训练任务压测结果也会被污染。因此在正式测之前先创建一份环境说明固定下面这些信息GPU 型号、显存大小、GPU 数量以及是否开启 MIG 或 多实例 GPU。推理框架名称和 commit 或版本号。CUDA、PyTorch、Python 版本。模型名称或权重路径以及权重精度如 FP16、BF16、INT8、INT4。推理服务启动参数如最大输入长度、最大输出长度、GPU 显存利用率、批处理开关。服务端和压测客户端是否在同一台物理机上。如果测试报告里没有这些信息只写着“使用 Llama 模型吞吐量 300 tokens/s”这个数字无法用于任何决策。同样的模型在不同框架、不同显存策略下的速度可能差几倍。2.2 使用一个最小可用的推理服务本文所有示例都把推理服务抽象成 OpenAI 兼容接口请求格式为/v1/chat/completions。这样做的原因是很多主流推理框架都提供 OpenAI 协议兼容层压测脚本可以通用不需要反复适配各家参数。假设你已经有一个模型权重使用 vLLM 启动服务时命令大致如下export CUDA_VISIBLE_DEVICES0 vllm serve /models/your-model-name \ --host 127.0.0.1 \ --port 8000 \ --max-model-len 4096 \ --gpu-memory-utilization 0.9如果你是学习环境可以先用一个小模型快速跑通流程例如能从模型仓库直接下载的 instruction 模型。把/models/your-model-name换成你实际可访问的模型路径即可生产环境还要根据模型卡的大小、显存容量和服务并发需求调整--max-model-len和--gpu-memory-utilization。启动完成后先做一个最简单的连通性检查curl http://127.0.0.1:8000/v1/models这个命令会返回模型列表。只有能看到模型信息后面客户端的 OpenAIClient 才能找到正确的model参数。如果你不打算使用 vLLM而选择 TGI、SGLang、openai 官方服务或云厂商端点压测的概念仍然相同。只需要修改base_url、model名称并确认服务方是否支持流式返回。不要为了实现一个指标跳过流式响应测试因为真实产品很少能接受“等完整内容全部生成完再吐给用户”的体验。2.3 输入工作负载要比“一段话跑 100 次”更接近真实压测请求的提示词不能只是固定的一句话。真实业务可能包含聊天多轮历史、用户提问、知识库片段、工具调用结果输入长度从几十 token 到几千 token 都会出现。不同输入长度直接影响预填充阶段的计算量和排队时间所以基准测试需要至少准备几组不同长度的 prompt。这里给出一份最低限度的输入设计清单短提示词约 50 token 以内模拟简单问答。中长提示词约 500 token 到 1000 token模拟带上下文历史的任务。长提示词接近模型最大长度的 60% 到 80%模拟长文档处理。混合提示词把短、中、长按业务比例混在同一个并发测试中。每个输入还要固定目标输出长度。比较公平的做法是把max_tokens设置成一个固定值比如 128、512、1024让所有请求拥有相同的“预算上限”。实际生成的 token 数会因为 stop 规则、回答长度不同而有差异但至少避免了一个请求输出 10 个 token、另一个请求输出 2000 个 token 导致的不可比场景。如果你有历史日志可以直接从线上请求日志中抽取 prompt 和 completion token 长度分布用真实分布构造测试数据。这是最贴近业务的方式没有日志时也可以用通用公开问答数据集中的前若干条作为负载来源但要注意清洗敏感字段。2.4 压测客户端与被测服务的关系在开发环境中压测脚本和服务可以运行在同一台机器上方便调试。但正式记录性能数据时客户端最好与被测服务分开。原因很简单客户端本身也要消耗 CPU、内存和网络连接如果同一台 GPU 机器既要跑模型推理又要开几百个线程发送请求、记录时间GPU 任务会受到影响。实际场景中服务端和客户端至少应该位于同一个局域网内避免公网带宽和跨区域网络波动对延迟造成干扰。如果只能在同一台机器上测试建议把客户端线程数控制在较低范围并把“同一台机器”写入测试报告作为环境限制。3. 用单次请求测量第一手延迟数据3.1 安装客户端依赖并编写基础请求函数为了减少协议处理代码这里使用openaiPython SDK 调用兼容接口。你需要先安装pip install openai然后编写一个通用请求函数。函数接收client、model、prompt、max_tokens支持流式和非流式两种模式。import time from openai import OpenAI def non_stream_chat(client, model, prompt, max_tokens): start time.perf_counter() response client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0, max_tokensmax_tokens, streamFalse, ) elapsed time.perf_counter() - start usage response.usage return { elapsed: elapsed, prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens, succeed: True, finish_reason: response.choices[0].finish_reason, }这里把temperature0是为了让回答内容尽量稳定减少输出内容差异对时间的干扰。注意temperature0并不代表服务在极低采样温度下保证每次输出完全相同但用于性能测试已经足够。3.2 用流式响应测量 TTFT非流式接口适合测量完整端到端耗时却拿不到首 token 延迟。首 token 延迟必须通过流式 API 观察客户端在收到第一个带内容文本的 chunk 时记录时间戳用该时间戳减去请求发出时间。下面这段流式请求代码的思路与真实 OpenAI SDK 一致不过在具体业务中要按你使用的协议版本做微调import time from collections import Counter def stream_stats(client, model, prompt, max_tokens): start time.perf_counter() stream client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0, max_tokensmax_tokens, streamTrue, ) first_content_at None first_content_latency None content_chunks [] for chunk in stream: if not chunk.choices: continue delta chunk.choices[0].delta if first_content_at is None and delta is not None and delta.content: first_content_at time.perf_counter() first_content_latency first_content_at - start if delta is not None and delta.content: content_chunks.append(delta.content) end time.perf_counter() total_latency end - start return { ttft: first_content_latency, total_latency: total_latency, content_chunks: len(content_chunks), }从代码中可以看到first_content_latency记录的是“第一个非空内容 chunk 到达”的时间而不是开始收到 SSE 事件的时间。很多服务在流式响应最开始会先推一个空 choices 或 role 信息如果只记录首个网络包TTFT 会偏小不够真实。需要强调的是content_chunks数量并不一定等于模型实际生成的 token 数。服务端可能把多个 token 合并到一个 chunk 里返回也可能一个 chunk 里只包含半个字符。要得到准确的 token 数量要么记录整个内容后用 tokenizer 编码统计要么使用服务端返回的 usage 信息。3.3 计算端到端输出速度非流式请求得到response.usage后可以很容易计算“端到端输出 token 速度”output_tokens result[completion_tokens] latency result[elapsed] print(fend_to_end_latency_ms: {latency * 1000:.2f}) print(foutput_tokens: {output_tokens}) print(ftokens_per_sec: {output_tokens / latency:.2f})这个tokens_per_sec是从发送请求到收到完整响应的平均生成速度包含首 token 排队等待时间因此不等于服务端真实的解码速度。解释结果时尽量把它称为“客户端视角端到端吞吐”不要直接叫“推理速度”。如果想观测 TPOT可以用流式响应统计“从首个内容 chunk 到生成结束之间的时长”再除以生成 token 数。但这只是近似值因为生成 token 数并不等于 chunk 数。更精细的做法是在流式响应开启服务端的 usage 统计拿到准确生成数后再计算。3.4 单次测量的合理性检查单次请求的数据会受到很多噪声影响不能直接当作结论。至少需要做以下三点检查首次请求是否执行过 CUDA graph、图编译或显存初始化如果第一次调用明显更慢说明没有预热。多次请求结果是否稳定例如跑 5 次后TTFT 和总延迟是否在合理范围内抖动。服务端日志是否有排队或显存不足警告如果服务在单请求场景已经出现排队说明配置需要调整。建议写一个循环函数对同一 prompt 跑 N 轮输出 min、max、avg、P50、P95而不是只保留最后一次结果。例如def run_repeat(client, model, prompt, max_tokens, n5): samples [] for _ in range(n): samples.append(non_stream_chat(client, model, prompt, max_tokens)) time.sleep(0.5) return samples循环之间加入小间隔是为了让上一轮请求的资源释放干净避免服务端因高并发任务还未结束而影响下一轮结果。下面的表可以作为单请求测试记录模板轮次TTFT ms端到端 ms输入 token输出 token端到端 token/s备注112018008012871.1首次请求可能含预热29016008012880.0预热后38815708012881.5稳定在项目里每一轮环境变更如更换量化方式、调整显存利用率、升级推理框架版本都应该单独保存一批类似表格的结果。4. 用并发请求做吞吐量和排队测试4.1 并发压测脚本需要哪几个要素单请求测试可以验证“服务本身是否正常工作”但不能回答“服务能不能支撑业务高峰”。为了制造压力压测脚本需要同时发出大量请求并统计服务端在压力下的表现。一个最小并发压测脚本至少包含几个要素并发数同一时刻最多有多少个正在处理的请求。总请求数或总持续时间固定请求数适合做短时容量测试固定持续时间适合观察稳定运行状态。请求间隔是否需要每 100ms 新增一个请求还是创建后立即全部发送。超时时间请求超过多少毫秒就放弃避免单个请求永久占用连接。错误记录记录超时、连接断开、HTTP 状态码异常等。结果汇总计算每分钟完成数、平均延迟、P50、P95、P99、最大延迟。4.2 使用 ThreadPoolExecutor 实现简单的并发负载下面代码实现了一个固定持续时间的并发压测入口。它通过并发数控制“同时在途”的请求数量并维护一个计数器在指定时间窗内持续提交请求。import argparse import statistics import threading import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI def build_client(base_url, api_keyEMPTY): return OpenAI(base_urlbase_url, api_keyapi_key) def normal_request(client, model, prompt, max_tokens, timeout60): start time.perf_counter() try: response client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0, max_tokensmax_tokens, timeouttimeout, streamFalse, ) elapsed time.perf_counter() - start usage response.usage return { ok: True, latency: elapsed, completion_tokens: usage.completion_tokens, prompt_tokens: usage.prompt_tokens, } except Exception as exc: elapsed time.perf_counter() - start return {ok: False, latency: elapsed, error: str(exc)} def run_load(base_url, model, prompt, max_tokens, duration, concurrency): client build_client(base_url) results [] lock threading.Lock() submitted 0 deadline time.monotonic() duration def worker_task(index): return normal_request(client, model, prompt, max_tokens) with ThreadPoolExecutor(max_workersconcurrency) as executor: futures {} while time.monotonic() deadline: while len(futures) concurrency and time.monotonic() deadline: idx submitted submitted 1 fut executor.submit(worker_task, idx) futures[fut] idx done [] for fut in list(futures.keys()): if fut.done(): results.append(fut.result()) done.append(fut) for fut in done: futures.pop(fut) time.sleep(0.01) for fut in futures: try: results.append(fut.result()) except Exception as exc: results.append({ok: False, latency: None, error: str(exc)}) return results这段代码没有处理“提交后立刻等待所有请求完成”的细节真实项目最好改成固定请求数的版本或在循环结束后调用executor.shutdown(waitTrue)。其核心思路是让线程池始终保持最多concurrency个请求在途并在时间窗口内持续发起新任务。调用方式可以这样def main(): args argparse.Namespace( base_urlhttp://127.0.0.1:8000/v1, modelyour-model-name, prompt请用三句话介绍 Redis。, max_tokens128, duration60, concurrency16, ) results run_load( args.base_url, args.model, args.prompt, args.max_tokens, args.duration, args.concurrency, ) ok_items [r for r in results if r.get(ok)] fail_items [r for r in results if not r.get(ok)] latencies sorted(r[latency] for r in ok_items) if latencies: total len(latencies) p50 latencies[total // 2] if total % 2 else (latencies[total // 2 - 1] latencies[total // 2]) / 2 p95 latencies[int(total * 0.95) - 1] p99 latencies[int(total * 0.99) - 1] print(fok: {len(ok_items)} fail: {len(fail_items)}) print(favg_latency_ms: {sum(latencies) / total * 1000:.2f}) print(fp50_ms: {p50 * 1000:.2f}) print(fp95_ms: {p95 * 1000:.2f}) print(fp99_ms: {p99 * 1000:.2f}) print(frequests_per_sec: {total / args.duration:.2f}) else: print(all requests failed) if __name__ __main__: main()这个 starter模板足够演示思路。生产环境建议把参数改成命令行参数或配置文件而不是写死在脚本里。4.3 并发数从低到高逐步加压不要一上来就压 128 并发。比较稳妥的压测方式是逐步递增先用 1 并发跑一个基线接着测 4、8、16、32、64观察每个并发档位的变化。每档测试结束后先记录结果再等几秒让服务恢复再升下一档。不同并发档位下可能看到这样的趋势并发数请求成功率P50 延迟P99 延迟每秒 token 数每秒请求数现象1100%低低低低单体延迟最好8100%增长增长提升提升资源利用率上升32100%明显增长明显增长继续提升继续提升批处理吞吐高峰64出现超时P99 超过 SLO很高可能不再提升可能下滑达到服务瓶颈并发提升后每秒请求数和每秒 token 数不一定会一直上升。超出显存和算力能力后服务端排队越来越严重单请求延迟增长越来越快甚至出现连接被服务端断开或超时。这样一组数据能帮你找到系统的“最佳吞吐点”和“压垮点”。4.4 保存完整结果而不只保存平均值很多人在压测结束后只记录了一个平均值之后想回看某个 P99 异常时找不到原始数据。推荐的保存格式是 JSON Lines 或 CSV每个请求都记录时间戳、耗时、是否成功、错误信息、输入 token 数、输出 token 数然后写一个独立的统计脚本做分位数计算。下面是一份 JSONL 记录的示例{request_time: 2025-06-01T12:00:01Z, concurrency: 8, ok: true, latency_ms: 842.1, prompt_tokens: 80, completion_tokens: 96} {request_time: 2025-06-01T12:00:01Z, concurrency: 8, ok: true, latency_ms: 1120.3, prompt_tokens: 80, completion_tokens: 128} {request_time: 2025-06-01T12:00:02Z, concurrency: 8, ok: false, latency_ms: 30000, prompt_tokens: null, completion_tokens: null, error: timeout}原始结果保留下来后即使测试结束后发现某个结论有误也可以重新统计不必重新跑一遍压测。5. 常见测量陷阱与排查路径5.1 测试结果不稳定或忽高忽低现象同样参数连续跑 10 次TTFT 和吞吐量波动很大有时第一次慢得离谱有时后续请求也突然变慢。可能原因之一是模型没有预热。推理框架在首次请求时会完成显存分配、CUDA graph 编译、模型权重加载等工作通常在第一个请求中体现为明显偏高的延迟。解决方式是压测前先发送几轮真实请求等延迟稳定后再记录数据。预热请求不纳入统计。原因之二可能是 GPU 处于节流状态。GPU 温度过高或功耗限制会影响训练和推理下的核心频率。此时需要用nvidia-smi查看设备温度、功耗、利用率和显存使用情况。若温度接近限制压测数据会整体偏慢且难以复现。原因之三是并发度太大导致服务排队。如果你先用 32 并发压出了漂亮结果下一次直接开到 64P99 可能上升好几倍。这不是工具出错而是容量边界变化。需要按 4.3 节做阶梯式压测而不是单点加压。检查路径nvidia-smi主要看温度、功耗、GPU 利用率。然后看服务端日志是否有排队、OOM 或 worker 重启记录。5.2 客户端测出的延迟包含很多排队时间有时你会看到请求平均延迟很高但进入服务端日志后发现实际计算时间并不高。原因很可能是客户端在发送时段创建了大量线程所有线程同时连接同一服务服务端内部排队或将请求发送到同一个端口但客户端所在机器网络栈出现瓶颈。排查方法是同时收集服务端日志中的请求到达时间和结束时间计算出服务端处理耗时再与客户端测到的耗时对比。如果差距很大说明延迟的主要部分在网络传输或在客户端侧等待连接建立。另一种做法是单独测服务端curl小请求的延迟。如果服务端能快速返回小请求但压测脚本中的大请求很慢则要检查是否因为请求体过大、模型输入过长或线程池数量不足导致资源竞争。5.3 TTFT 总是测不准现象流式响应确实是最快路径但纪录下来的第一个 chunk 到达时间包含了客户端网络往返时间和客户端事件循环处理延迟。原因包括SDK 内部先做了连接池初始化导致第一个请求额外耗时。首个 SSE chunk 没有内容只是角色信息或空 choices被误认为首个 token。压测机器本身高负载客户端线程不能及时解析 socket 数据。服务端为了稳定返回可能会把多个内容 token 合并成一个 chunk导致真实 TTFT 小于客户端看到的时间。要减少误差可以先在正式记录前建立连接并发送一个预热请求预热完成后关闭连接或复用连接。判断首个 token 时优先判断 delta 中是否存在非空 content而不是扫描整个 chunk 对象是否为空。如果不确定服务端 chunk 结构可以在压测前先打印前几个 chunk 的字段结构确认后再写正式统计逻辑。5.4 使用不同长度 prompt 混测时输出混乱在一次压测中混合多个不同长度的 prompt 虽然贴近真实业务但会让“为什么延迟升高”变得很难判断。如果短 prompt 请求排在长 prompt 请求后面它的 TTFT 会明显变长这并不代表短 prompt 处理变慢而是因为在服务端队列里被长 prompt 任务阻塞了。因此做根因分析时要把输入长度分成不同的 case。先分别跑“短 prompt 并发”“长 prompt 并发”再跑“混合比例并发”。如果混合场景的 P99 明显高于两类独立场景的 P99说明长请求正在大量占用资源比如通过新增来动态加长排队延迟。5.5 错误和超时也要算进整体结果统计吞吐量时不能只数成功的请求。如果一个压测时段发送了 5000 个请求其中 100 个超时服务依然可能显示每秒 token 数很高但用户侧已经有 2% 请求失败。因此每个报告都必须包含成功请求数、失败请求数、错误原因分类以及错误率。错误分类建议如下错误类型常见原因处理建议timeout服务端生成太慢超过客户端设置超时检查并发数、模型长度、显存和批处理配置connection error服务重启、端口断开、连接池耗尽检查服务日志和系统连接数429 限流服务端配置了 QPS 或 token 限流调整限流阈值或放慢压测发送速率401 鉴权失败API Key 不正确或过期检查客户端环境变量模型返回空内容安全过滤或输出被截断检查服务端日志和内容过滤策略6. 最佳实践与长稳运行建议6.1 不要把压测只看成跑一次脚本LLM 推理基准测试不是一次性交付物而是每次部署变更前的质量门禁。模型量化从 FP16 改成 INT8、推理框架升级小版本、GPU 从 A100 换到 H100、显存利用率从 0.8 改为 0.9这些变更都需要重新跑一遍同等工作负载。建议把基准测试脚本、prompt 数据集、环境版本记录一起提交到代码仓库中形成一份“性能回归套件”。每次上线前至少跑一次单请求基线和 16 并发短时负载比较关键指标是否出现明显回退。如果回退幅度超过 10%需要定位是框架参数变化、模型变化还是硬件环境变化造成的。6.2 离线吞吐与在线延迟要分开看待实际项目中容易混淆两类测试一类是离线吞吐测试输入一批固定 prompt让推理框架按最大效率批量生成 token计算每秒生成 token 总数另一类是在线服务压测通过 HTTP 请求模拟用户逐条发起的交互。离线吞吐往往作为硬件能力的参考指标它能反映推理框架在最好的批处理状态下能跑多快。但用户不可能等一个批处理积累到足够大的请求才发出自己的问题在线服务必须在请求到达后立刻进入服务流程。因此离线数字可以作为容量规划的上限参考而不是承诺给用户的响应时间。做技术方案时最好分别给出一套静态离线数据和一套模拟在线流量数据。6.3 长期运行测试比几分钟压测更容易暴露问题短时压测只适合快速定位配置错误和服务是否能启动却很难发现显存泄漏、连接不释放、日志累积、死循环、服务端孤儿请求等问题。一个靠谱的部署验证流程应该在短时压测通过后再跑至少 30 分钟到数小时的长期负载。长稳测试里要额外观察GPU 显存占用是否持续上涨最终 OOM 重启。客户端连接池中的 TCP 连接数量是否不断增长。服务端错误日志是否出现相同的告警重复刷屏。吞吐量是否随着时间的推移逐步下降。内存占用是否因为缓存策略不断增加。每 10 分钟记录一次系统快照包括 GPU 利用率、显存、CPU、内存、请求量、错误率用表格或时序图对比趋势会比只看最终汇总更容易定位异常。6.4 压测结束后的发布前检查清单在把测试结果用于发布决策之前按下面这份清单逐一检查可以减少各种“数字很高上线就崩”的误判。压测用的模型权重精度与生产发布包一致。压测用的推理框架版本与实际部署版本一致。压测用的 GPU 型号和数量与生产目标一致或已知差异。压测 prompt 长度覆盖业务常见长度而不只是短文本。压测 max_tokens 能覆盖真实回答长度而不只是聊天短回答。完成过至少一次预热首次请求的额外开销未计入采样。记录过至少三个并发档位的结果而不是只有一个最高并发。保存了成功率和错误分类而不是只保留平均延迟。检查过服务端日志中是否有 OOM、CUDA 错误、连接重置。至少跑过一次 30 分钟以上的长稳测试。6.5 下一步扩展方向如果要把 LLM 推理基准测试做深可以从“用固定 token 数压测”升级到“用真实业务负载压测”引入多轮对话、缓存命中、批量离线任务等场景。还可以结合推理服务暴露的 Prometheus 指标例如每请求排队时长、每次解码耗时、每个 batch 的平均大小、显存利用率将客户端观测和服务端观测放在同一张图上分析。更进一步需要根据基准测试结果反推动成本模型。同样的模型在两个并发档位下吞吐不同每个请求对应 GPU 成本也不同找到成本和延迟的平衡点才是最终目标。这个目标不是说跑一次脚本就能完成的而是需要把压测能力沉淀成项目里可持续运行的工具和报告让每次模型和服务变更都建立在可比较的数据之上。第一次做基准测试时不必追求完美。先固定好环境、跑通脚本、记录下所有可观测指标哪怕最初的结果波动很大也比“凭感觉认为某个推理框架更快”可靠得多。等到报告结构和测试流程稳定后再逐步加入多轮对话、真实日志分布和长稳验证就能逐步形成一套适合自己业务的 LLM 推理性能评估体系。