大模型推理基准测试实战:从TTFT到并发吞吐的完整指南

大模型推理基准测试实战:从TTFT到并发吞吐的完整指南
LLM 推理基准测试LLM Inference Benchmarking本质是一个工程动作把一个已经训练好的大语言模型放到指定环境中去跑请求然后量化记录“等多久才能收到第一个字”“后续每个 token 的生成速度”“多个用户同时用会不会互相拖垮”“显存会不会先爆”。这件事和模型本身能不能答对题是两码事。模型能力评测看的是“智力”推理基准测试看的是“运行时表现”而运行时表现恰恰决定了你把这个模型接到 Web、API 或批处理任务中以后体验和成本是否可控。如果你近期在做模型选型或者正打算用 vLLM、LMDeploy、llama.cpp、Ollama 这类推理框架搭一个本地或局域网的推理服务如果你正纠结“手里的显卡到底能不能带得动这个模型”那这篇文章会比较有用。下面不偏重堆配置而是按一次实测流程来拆目的是让你能跑出一份可复现、能比较、敢拿去做技术决策的数据。最值得关注的点就是不是跑出一个数字而是知道这个数字在什么条件下成立什么条件下失效。我最早做推理测试时也犯过不少错。拿不同的输入长度去比速度只看单请求延迟就判断能不能上线最后一跑并发数据完全不可用。后来我把测试当成一套有输入条件、有边界、有验收标准的实验来做问题才清楚很多。1. 先想清楚推理基准测试测的并不是“模型有多聪明”1.1 为什么大模型本身的评测榜单不能替代推理测试现在市面上的大模型榜单很多考数学、考代码、考指令遵循。这些结果能帮你判断模型能力上限但无法告诉你三件部署时最关心的事这个模型在你的 GPU 上启动需要多少显存。一次普通请求要等多久才开始返回。四个人同时用和你一个人用速度差多少。模型能力评测通常用的是固定数据集和离线脚本看重答案质量。推理测试则看重延迟、吞吐、资源占用和稳定性。同一个模型在不同推理框架下的表现可能差很多同一个框架在不同显卡、不同量化方式、不同 KV Cache 设置下也会有很大差异。所以你不光要选模型还要选推理服务方案推理基准测试就是给这个选择提供依据的。1.2 推理阶段要拆成两段看预填充和解码大模型生成回复并不是一次性算完的。输入进来后先要对 prompt 做预填充把用户的问题和已有上下文编码成 KV Cache这个过程会显著影响首 token 延迟。随后模型进入逐 token 自回归生成阶段每生成一个 token 都要读取一次增量上下文也就是解码过程。预填充阶段更吃算力解码阶段更吃显存带宽。这就是为什么经常能看到“首 token 很快但后续生成偏慢”或相反的情况。如果你只用一个总的请求耗时来评价就很难定位瓶颈在哪一段。对交互式应用来说用户感知的是“打完字多久出现内容”和“内容是不是像打字机一样流畅”对离线批处理来说用户可能更关心每小时能处理多少条请求。测试计划在设计阶段就应该把这两类场景分开。2. 开始测试前先把环境和任务形态固定下来这一节看起来基础但很多测试报告不可信恰恰是因为前置条件不齐。这里要求的不是“能不能跑”而是“能不能稳定复现”。2.1 搭建一个能横向比较的最小环境准备好推理基准测试环境时至少要考虑这些变量显卡型号和显存、驱动、CUDA 或 ROCm 版本、Python 版本、推理框架版本、模型权重格式、量化方式、上下文长度。把这些信息记下来而不是只写“我跑了一个 7B 模型”。因为同样叫 7B 模型FP16 全精度和 INT4 量化后的显存占用、生成速度、首 token 速度都会不同。测试开始前可以先写一个环境记录表至少包含显卡型号和显存容量CUDA、PyTorch、推理框架版本模型路径、模型精度或量化格式服务端口、请求协议单条测试使用的 prompt 长度和生成长度如果你在普通办公电脑上做测试CPU 推理也不是不能跑但延迟会明显高于带独立显卡的环境。这种情况下得到的数据更适合验证流程不适合作为生产容量规划依据。2.2 不固定输入长度和输出上限成绩就不可比较推理延迟和两个长度强相关输入 prompt 的 token 数以及输出 max_tokens 上限。输入越长预填充时间越长输出上限越大解码阶段耗时通常越久。很多性能比较踩坑就是因为一边用 1000 token 的输入测另一边用 50 token 的输入测最后把结果放到一起比毫无意义。我一般的做法是先准备三组固定测试文本短 prompt大约几十个 token中等 prompt几百个 token长 prompt如果模型支持长上下文可以再准备一组接近模型上下文上限的输入。这里的关键是每次测试要使用完全相同的一组 prompt而不是每次都随机换问题。为了结果稳定还可以使用替换后的占位数据集保证内容不丢失且长度一致。2.3 交互服务和离线批量任务要分开测交互服务例如聊天机器人、客服问答通常一次一个用户在前台等待看的是首 token 延迟、解码体感、单请求是否超时。离线批量任务例如文档归纳、批量摘要通常不发散给真用户看的是最大吞吐、吞吐和延迟的平衡点。同一个部署方案如果既支持流式接口又支持非流式接口建议两种模式都测。流式接口能改善首 token 体验但调度开销和非流式完全不同。如果不写清楚这一点后续别人复现你的测试很可能复现不出同样的数据。3. 五个核心指标每个指标到底代表什么推理测试不能只汇报一个“每秒生成 token 数”。真实场景里不同角色关注不同指标。下面按常见性整理一下你可以按需要选择。指标英文常用名解决什么问题最关心的场景首 Token 延迟TTFT用户多久能看到第一个字聊天、搜索问答、客服每输出 Token 耗时TPOT每秒或每个 token 的解码速度流式生成、长文本总结端到端延迟End-to-end Latency从发请求到完整结果/流结束的时间非流式接口、单次调用吞吐Throughput系统单位时间能处理的请求或 token批量任务、多用户服务峰值显存Peak VRAM模型 KV Cache 运行时开销的容量占用显卡选型、量化评估3.1 TTFT首 Token 延迟为什么优先看TTFT全称 Time To First Token指从客户端发出请求到收到第一个生成 token 的时间。这段耗时里包含网络传输、排队、预填充和调度。TTFT 不稳定往往表现为“请求发出去了半天没反应突然一下字全出来了”。如果是流式接口第一个 token 的时间会直接影响用户是否觉得服务“卡了”。TTFT 翻倍不一定代表推理框架计算变慢也可能是请求队列长了、模型上下文被拉长了或者服务端在做并发调度。记录 TTFT 时最好同时记录当时的并发数和服务端排队水位。3.2 TPOT生成阶段的真实手速TPOT全称 Time Per Output Token指平均每个输出 token 花费的时间。用 1 除以 TPOT能得到每秒生成 token 数。比如 TPOT 是 50ms对应大约 20 token/s。它在流式对话里影响打字感。如果 TPOT 太大用户会觉得字是一个一个蹦出来的如果太小又可能产生一种“模型直接抽风输出整段”的错觉。需要说明的是这里没有适合所有任务的“标准值”交互体验标准要根据业务场景自己定义。3.3 吞吐并发场景下最容易被低估的指标单个请求快不代表服务能扛住多人同时使用。推理服务通常支持连续批处理或 Paged Attention 这类机制把多个请求合并到一次前向计算里。并发提高后单请求延迟往往上升但单位时间吞吐可能提高。你要找的不是“延迟绝对最低”也不是“吞吐绝对最高”而是业务可接受延迟范围内的最大吞吐。例如某个服务单请求延迟很低但 8 路并发后延迟暴涨超时率上升另一个服务单请求稍慢但 16 路并发下延迟只比单请求高一点点。对于多用户场景第二组通常更合适。这种差异只看单请求测试是看不出来的。3.4 p99 比平均值更能暴露抖动平均值会被大量正常请求拉走掩盖极少数慢请求。对一个在线推理服务来说用户遇到一次长时间的卡顿会产生非常差的体验。理想情况下至少统计 p50、p95、p99。p99 高通常意味着调度、排队或者资源争抢在某段时间集中爆发。如果你看到 p99 远高于 p95不要急着调生成参数先看这段时间是不是有多条长输入同时进入或者显卡温度、功耗是否触发降频。调度层面的日志往往比客户端更有说服力。4. 按一套真实流程跑通从单条请求到并发测试下面这套流程比较通用适合已经在本地跑起一个 OpenAI 风格接口或相似协议的推理服务。如果你的框架没有现成接口也可以在代码里调用模型并手动计时但网络协议、批量调度等差异要额外说明。4.1 先做冒烟测试不管之后要做什么复杂的并发压测先跑一条最小请求。这一条请求的任务是确认服务能启动、模型能加载、接口能返回结果而不是测性能。我建议第一轮使用最小输入比如一句话输出限制也小一些例如 64 个 token。如果这一步就报错后边所有性能数据都不必测。冒烟测试建议使用最简单最不需要模型思考的任务比如“你好”或“11”。这样能尽快把环境问题和模型能力问题分开。4.2 单请求往返延迟的记录方式冒烟通过后可以写一个简单脚本测量端到端延迟。这里的关键不是脚本本身多复杂而是确保每次请求使用相同模型、相同 prompt、相同生成参数。下面是一个使用 Python requests 的示例假设你的服务暴露了类似 /v1/completions 的接口。import time import requests url http://127.0.0.1:8000/v1/completions prompt 请用三句话介绍什么是操作系统。 payload { model: local-model, prompt: prompt, max_tokens: 64, temperature: 0.2, stream: False, } for i in range(10): start time.time() try: resp requests.post(url, jsonpayload, timeout120) elapsed time.time() - start data resp.json() if resp.status_code 200: total_tokens data.get(usage, {}).get(total_tokens, 0) print(fround{i 1}, latency{elapsed:.2f}s, total_tokens{total_tokens}) else: print(fround{i 1}, error, status{resp.status_code}) except Exception as e: print(fround{i 1}, exception{e})这段代码只记录端到端延迟不测量 TTFT。如果你想记录首 token 时间需要开启流式请求并把“收到第一个数据包”的时间单独记录。很多框架自带日志或者结果打印也可以直接从服务端日志里读取 prefill 时间和 decode 时间。4.3 并发测试要从小并发慢慢往上加不要一上来就开 32 并发。先把并发数设成 1、2、4、8每组跑固定数量的请求比如每组 20 到 50 条。每轮加压前先确认上一轮没有报错和超时。异常过多的时候继续加压只会放大噪声。为了提高可复现性可以写一个并发线程池或 asyncio 脚本给每个请求单独记录时间但这会带进客户端开销比较时保持一致即可。import concurrent.futures import time import requests URL http://127.0.0.1:8000/v1/completions PAYLOAD_TEMPLATE { model: local-model, prompt: 请用简短的语言介绍贝叶斯统计的核心思想。, max_tokens: 128, temperature: 0.2, stream: False, } def send_one(_): start time.time() try: resp requests.post(URL, jsonPAYLOAD_TEMPLATE, timeout120) return resp.status_code, time.time() - start except Exception as e: return -1, time.time() - start for concurrency in [1, 2, 4, 8]: latencies [] statuses [] with concurrent.futures.ThreadPoolExecutor(max_workersconcurrency) as pool: for status, elapsed in pool.map(send_one, range(30)): statuses.append(status) latencies.append(elapsed) ok statuses.count(200) avg sum(latencies) / len(latencies) p99 sorted(latencies)[int(len(latencies) * 0.99) - 1] print(fconcurrency{concurrency}, ok{ok}/{len(statuses)}, avg_latency{avg:.2f}s, p99{p99:.2f}s)这段代码需要你根据实际接口自行调整字段。核心思想是同一组输入、同一组模型参数、不同并发梯度记录成功率、平均延迟、分位延迟。只跑一次的数据不算数建议每个并发档位至少跑 3 轮取中间结果。不同框架的批量调度行为不一样不要只看“到达率”。你开的并发数代表服务要同时处理的活跃请求数框架是否排队、是否切批、是否抢占都可能影响结果。4.4 跑测试时不要关掉资源监控推理性能测试最容易犯的错就是只看数字不看环境。测试过程中应该用一个单独终端监控 GPUwatch -n 1 nvidia-smi要注意看四样显存占用、显存温度、功耗、GPU 利用率。如果测试刚开始 GPU 利用率就冲到 100% 但温度跟随上升说明计算负载上来了如果最后几轮速度明显下降而温度很高很可能不是推理框架变慢而是显卡已经触发降频保护。如果显存占用在测试过程中持续增长也不一定是泄漏可能是预留上下文增大或者历史会话保留导致。要看服务端日志里是否真有内存增长而不是只看客户端时间。4.5 把测试条件和结果一次性记录下来建议在本地建一个结果文件每次测试都记录一组完整信息。记录格式可以是这样| 测试编号 | 并发 | 输入token数 | 输出上限 | TTFT均值 | TPOT均值 | 端到端P50 | 端到端P99 | 成功率 | 峰值显存 |每条记录必须对应一组独立条件。不然跑完一个星期后你很可能记不清某一组数据到底是流式接口还是非流式接口跑出来的也不知道是哪种量化格式。5. 数据怎么看什么样的结果算“能用”什么样的算“翻车”拿到测试结果后新手容易盯着平均值。有经验的人会先做几件事按任务类型定标准、看尾部延迟、看并发拐点、检查输出是否正确。5.1 交互任务和批量任务要采用两套验收标准交互任务比如聊天机器人可以自己先定义一条体验基线首 token 一两秒内返回是不是可接受后续生成速度太慢用户会不会等待并发上限之内不要出现请求超时。批量任务不一样比如你要用一个模型批量处理 1000 篇文档用户不会实时等待。此时单请求延迟并不重要重要的是总耗时、吞吐和失败重试成本。如果一次批量任务需要 10 个小时哪怕单请求慢一点只要吞吐稳定、断点可恢复也仍然可以用。5.2 先找“翻车点”而不是只看最优点很多测试曲线长这样并发 1 到 4延迟缓慢上涨并发 8延迟明显上涨并发 16延迟暴涨出现大量超时。这个从平缓到陡峭的转折点就是你服务的“软上限”。实际使用中不要让服务长期运行在陡峭区间。如果 16 并发已经是拐点最好按 8 到 12 并发规划容量。还有一个容易被忽略的信号并发升高后成功率开始下降。即使延迟看起来不高失败请求对用户体验的破坏往往大于慢请求。5.3 对比方案时要保证唯一变量对比框架或者对比量化方案很容易踩的一个坑就是每次请求输出长度完全不同。有的模型同样输出 100 个 token结果实际生成了 180 个因为重复惩罚、停止符或者采样参数不同。为了避免这种误差对比时要注意使用相同数量和内容的 prompt。固定温度、seed、top_p、频率惩罚、存在惩罚等采样参数。固定 max_tokens。对比实际生成的 token 数如果差异过大要说明原因不要只看耗时。例如在对比 FP16 和 INT4 时如果两者回答的 token 数量不同单看每秒 token 数并不能直接说明量化后更慢或更快。还要检查生成质量是否下降。推理性能测试如果以牺牲正确输出为代价那数字再漂亮也没有实际参考价值。5.4 量化位宽和精度会影响输出内容不一定只有“速度差异”量化可以降低显存占用有时还能提升吞吐尤其在显存成为瓶颈的时候。但量化后输出可能和原始模型不完全一致。同一个 promptFP16 版本输出 100 个 tokenINT8 可能输出 96 个INT4 可能输出 115 个。这种差异不是框架 bug而是模型精度变化导致的采样分布变化。所以在对比精度方案时除了看速度最好再抽查几组输出的语义完整性。基准测试应该同时包含“结果是否正确”这一项哪怕它只是人工抽查。否则你测出来的可能只是“输出乱得更快”。6. 容易踩的坑和一套可复用的排查顺序最后这部分是经验总结。下面的问题在各类推理测试中都可能遇到建议遇到异常数据时按这个顺序排查而不是直接认定是框架不稳定。6.1 数值离奇先检查环境和日志不要先改模型参数有一次我测一个模型速度突然掉了很多当时以为是服务端崩了。后来发现是另一台机器同时跑了一个挖矿式的占显存进程资源被抢走了。推理测试过程中任何后台任务都可能污染数据。排查优先级我建议这样排先看客户端有没有报 4xx、5xx、超时或连接错误。再看服务端日志检查报错时间点、排队长度、是否 OOM。用 nvidia-smi 看显存、温度、功耗和利用率是否正常。确认测试期间没有其他进程占用同一个 GPU。最后再怀疑模型参数和框架配置。这种顺序能帮你少走不少弯路。很多时候问题不在模型能力而在输入格式、请求路径或资源争抢。6.2 统计口径不一致是误差的主要来源流式接口和非流式接口的耗时定义完全不同。流式接口的端到端时间指连接到最后一个 chunk 结束非流式接口则要等服务端把完整回复计算完再返回。把这两类数据放进同一张表里比较会得出误导性结论。另外客户端计时包含网络 RTT服务器端计时不包含。如果服务部署在远程服务器上网络抖动会被算进延迟。为了做更精细的对比最好在服务端日志里读取 prefill 和 decode 的耗时再结合客户端计时综合判断。6.3 一次测试结果只能代表“当时那一刻”推理性能受很多不稳定因素影响显卡温度、其他用户请求、CUDA 图的复用、批处理器是否完成预热、磁盘缓存是否命中。第一次测试时服务端还没有完全预热后续请求可能因为动态缓存命中或算子优化而变快。所以建议正式测试前先多发几轮请求让服务预热等显存和批处理状态稳定后再开始记录。如果测了半天只跑一轮数据可信度有限。正式记录时也最好采用多轮取统计值的方式而不是记录第一次成功请求的数据。6.4 不要把一个压测脚本复用到底而不看语义如果测的是数学、代码、结构化 JSON 输出不要只记录“请求成功”。你应该看返回内容是否满足任务要求。推理框架可以做到“接口正常返回 200但输出终止符没生成完或者 JSON 结构截断了”。出现截断不一定是性能问题可能是 max_tokens 设置太小输出被提前切断也可能是采样参数里 stop token 设置不当。这种情况下生成的 token 数看起来不低但业务根本无法使用。性能数据必须结合结果有效性一起看。写在最后推理基准测试是给部署决策用的不是用来刷数据的做一次完整的 LLM Inference Benchmarking其实是在回答一个部署问题这个模型放在这套服务环境中能以什么延迟、什么吞吐、什么资源成本对外服务。这个结论需要环境信息、输入信息、输出约束、并发模型和验收标准一起支撑才具备可复现性。如果你只是做技术学习可以先在单机单卡上用一个小模型跑通流程默认参数通常够用。如果把测试结果用于生产容量规划就要把日志、服务端统计、输出 token 数量、显存峰值、失败重试、量化格式全部记录下来。长期使用某一套推理服务时建议周期性复测因为依赖版本升级、驱动更换、数据长度分布变化都可能导致之前的结论失效。我个人更建议先把单条请求和单用户场景跑稳再去压并发。因为并发压测不仅能反映框架能力还能暴露调度、队列、显存管理、失败重试等一系列问题。只有把这些边界搞清楚一张基准测试表才真正具备做决策的价值。

最新新闻

日新闻

周新闻

月新闻