尧图精选

大模型压测中TTFT指标获取全流程:概念、工具与排障实践

🕒 发布时间:2026/10/2 3:07:49 📁 来源:尧图网络
前两天在群里看到有人问压测大模型的时候TTFT这个指标到底怎么拿底下回答挺多但大部分是概念性的真到了动手阶段很多人还是不知道该从哪里下手。我自己第一次搭vLLM服务做压测时也踩过类似的坑以为响应时间减掉请求时间就是TTFT后来发现测出来的数据根本对不上。这篇文章就围绕大模型压测里的TTFT获取这件事把概念、工具、脚本、排障全部过一遍适合正在做模型服务部署、性能测试、SRE的同学参考。1. 先把TTFT这个指标掰开揉碎1.1 TTFT、TPOT、首字节三个概念别混了TTFT全称 Time to First Token指的是从客户端发出请求到收到模型返回的第一个有效 token 之间的时间。注意这里的关键词是“第一个有效 token”不是“第一个字节”。这两个概念在大模型服务里差别很大因为绝大多数推理框架都是流式返回HTTP 响应头、SSE 数据帧可能先到但真正业务上的第一个生成内容可能要再等一会儿。如果你直接把“收到响应头的时间”当成 TTFT测出来的值会偏小而且偏多少完全看服务端实现。传统接口压测只要看 response time请求出去、完整响应回来就算一次。大模型不能只看这个。用户感知到的“反应快不快”很大程度上取决于屏幕上第一个字什么时候出现。所以 TTFT 经常被当成大模型服务的核心体验指标之一。和它一起出现的通常还有 TPOTTime Per Output Token平均每个输出 token 的时间和总响应时间。三者组合起来才能判断一个服务到底是“开口慢”还是“全程拖沓”。指标含义主要受影响因素常见误区TTFT首个 token 到达的延迟prefill、排队、网络拿首字节时间当首 token 时间TPOT平均每个输出 token 耗时decode 速度、显存带宽、并发忽略 TTFT 单独看 TPOT总响应时间请求发出到完整响应返回TTFT TPOT * 输出长度 网络掩盖首 token 慢的问题1.2 为什么压测大模型时必须单独看 TTFT大模型推理和传统接口最大的区别在于一次请求里包含了两个完全不同的计算阶段prefill 和 decode。prefill 阶段要把整段 prompt 一次性处理掉计算量大耗时也大decode 阶段才是逐字生成每生成一个 token 都要做一次自回归计算。TTFT 主要由 prefill 耗时决定后续输出速度则由 decode 耗时决定。prompt 越长、并发越高prefill 排队越严重TTFT 就越高。这个特性决定了 TTFT 特别能反映系统在高并发下的排队压力。同一个模型服务低并发时 TTFT 可能只有几百毫秒并发一旦上来prefill 请求会在推理引擎里排队TTFT 会迅速恶化。如果只看完整响应时间你只能感觉到“变慢了”但看不出慢在哪一段。把 TTFT 和 TPOT 分开看能快速区分是首 token 慢还是整体生成速度不行。后续如果要做容量评估、扩缩容、SLA 制定TTFT 的 p95 和 p99 比平均值更值得盯。1.3 TTFT 的时间构成拆解一次 HTTP 请求到返回首个 token时间可以拆成四段客户端发送请求到服务端的网络传输服务端接收请求后的排队和调度prompt 的 prefill 计算生成第一个 token 并通过网络返回。第一段和最后一段是网络开销中间两段是服务端开销。客户端测到的 TTFT 天然包含网络时延和客户端处理时延所以它是“用户视角”的指标服务端 metrics 里的 TTFT 是“推理引擎视角”的指标不含网络。两边对不上是正常的关键是你需要知道自己到底要测哪个视角。2. 压测前先确认服务和工具链路2.1 先验证你的模型服务是否真的支持流式输出OpenAI 兼容接口普遍支持在请求体里加stream: true返回格式是 SSE一行一行把 token 推过来。但有些框架、网关、代理服务器不一定透传流式响应可能等到完整结果才一次性返回。这种情况下你能拿到的只有“完整响应时间”拿不到真正的 TTFT。所以压测前第一步先用 curl 手动验证一次。curl -N http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen/Qwen2.5-7B-Instruct, messages: [{role: user, content: 你好}], stream: true, max_tokens: 32 }如果服务正常你会看到类似这样的连续输出data: {id:chatcmpl-xxx,choices:[{index:0,delta:{content:你},finish_reason:null}]} data: {id:chatcmpl-xxx,choices:[{index:0,delta:{content:好},finish_reason:null}]} data: [DONE]如果看不到这种逐行data:输出而是等到很久之后一次性返回一段完整 JSON那就要排查服务端配置或网关缓冲策略。有些框架默认设置了缓冲需要手动关闭有些网关为了压缩会攒一批再发也会破坏流式效果。2.2 压测工具选型别让工具限制了指标采集工具优点缺点适合场景JMeter社区成熟能直接出报告默认采样器测的是完整响应测 TTFT 要写脚本公司已有 JMeter 流程想快速接入wrk / vegeta高并发能力强对流式长连接支持弱结果只有聚合耗时短连接压测不适合排查 TTFTLocustPython 可编程分布式方便需要自己写客户端逻辑需要复杂场景编排时Python requests / aiohttp最灵活SSE 解析可控并发能力不如专业压测工具自己写脚本精确采集 TTFT我的建议是如果只是临时验证Python 脚本最快如果要长期做容量回归可以自己写一个简单的并发压测脚本把 TTFT、TPOT、总耗时都存下来后续还能接到监控平台。JMeter 也能做但你需要接受 JSR223 采样器的复杂度。2.3 服务端自带的指标端口别浪费很多推理框架自带 Prometheus metrics。以 vLLM 为例启动服务后通常可以在同一个端口访问/metrics里面直接有vllm:time_to_first_token_seconds相关的 histogram 指标。压测前先确认 metrics 端点能访问能省不少事。curl -s http://127.0.0.1:8000/metrics | grep time_to_first_token如果看到类似下面的输出说明服务端已经在记录 TTFT# HELP vllm:time_to_first_token_seconds Distribution of time to first token in seconds. # TYPE vllm:time_to_first_token_seconds histogram vllm:time_to_first_token_seconds_bucket{model_name...,le0.1} 10 vllm:time_to_first_token_seconds_sum{model_name...} 3.5 vllm:time_to_first_token_seconds_count{model_name...} 100有了这些指标你就不需要依赖客户端解析 SSE压测过程中用 PromQL 直接查分位数就行。后面第三节我会专门讲怎么对拍客户端和服务端的数据。3. 三种实测 TTFT 的完整方法3.1 方法一Python 脚本从 SSE 流里掐表这是最通用、最直接的方法。原理很简单发起请求前用perf_counter记录起始时间然后逐行读取响应流遇到第一个包含非空 content 字段的 chunk 时记录当前时间差值就是 TTFT。import requests import time import json url http://127.0.0.1:8000/v1/chat/completions payload { model: qwen/Qwen2.5-7B-Instruct, messages: [{role: user, content: 写一段关于压测的短文。}], stream: True, max_tokens: 512, } start time.perf_counter() ttft_ms None with requests.post(url, jsonpayload, streamTrue, timeout300) as resp: for line in resp.iter_lines(): if not line: continue line line.decode(utf-8) if not line.startswith(data:): continue data line[5:].strip() if data [DONE]: break if ttft_ms is None: obj json.loads(data) delta obj[choices][0].get(delta, {}) content delta.get(content) if content: ttft_ms (time.perf_counter() - start) * 1000 print(ffirst token content: {content}) if ttft_ms is None: print(未捕获到有效首 token请检查服务端流式输出) else: print(fTTFT: {ttft_ms:.2f} ms)这里有几个地方容易忽略。第一必须判断delta.content非空因为很多服务返回的第一个 chunk 可能只有role信息content 为空直接把它当成首 token 会虚低。第二requests的iter_lines只有在流式响应时会按行返回如果服务端把整个响应缓存到最后一次性返回这个脚本只会读到一次大块数据TTFT 等于总耗时一眼就能看出服务端没有真正流式。3.2 方法二JMeter 压测时用 JSR223 采样器算 TTFT如果团队已经统一用 JMeter可以在 JSR223 采样器里写 Groovy 脚本。核心思路和 Python 一样发 HTTP 请求读流式响应记录首个非空 content chunk 的时间然后把这个时间写到采样结果里。JMeter 自带的“响应时间”字段记录的是完整响应回来后的时间不能直接当 TTFT 用。下面是一个最小可用的 Groovy 示例假设你已经把请求 URL 和 payload 放到了 JMeter 变量里import java.net.HttpURLConnection import java.net.URL import java.io.BufferedReader import java.io.InputStreamReader import groovy.json.JsonSlurper def url new URL(vars.get(api_url)) def conn (HttpURLConnection) url.openConnection() conn.setRequestMethod(POST) conn.setRequestProperty(Content-Type, application/json) conn.setDoOutput(true) conn.setUseCaches(false) conn.setReadTimeout(300000) conn.getOutputStream().write(vars.get(payload).getBytes(UTF-8)) long start System.nanoTime() def ttftMs -1 BufferedReader reader new BufferedReader(new InputStreamReader(conn.getInputStream(), UTF-8)) String line while ((line reader.readLine()) ! null) { if (!line.startsWith(data:)) { continue } def data line.substring(5).trim() if (data [DONE]) { break } def obj new JsonSlurper().parseText(data) def delta obj?.choices[0]?.delta if (delta delta.content) { ttftMs (System.nanoTime() - start) / 1_000_000 break } } reader.close() conn.disconnect() if (ttftMs 0) { SampleResult.setSuccessful(false) SampleResult.setResponseData(未捕获到有效首 token, UTF-8) } else { SampleResult.setSuccessful(true) SampleResult.setResponseData(TTFT ttftMs ms, UTF-8) }需要注意几点。第一这个写法是同步阻塞的并发线程数上去之后JMeter 自身的线程会占用不少资源压测机规格要留足。第二JsonSlurper对每个 chunk 做 JSON 解析在高频场景下会有一定开销但对 TTFT 计算影响不大因为解析发生在时间戳记录之后。第三如果你跑在旧版本 JMeter 和 Java 8 上HttpURLConnection是兼容的不用引入额外依赖。3.3 方法三直接读 vLLM 服务端 Metrics顺便校准如果你用的是 vLLM服务端已经把 TTFT 统计好了没必要非从客户端解析 SSE。压测过程中可以直接用 Prometheus 查询。最简单的均值计算rate(vllm:time_to_first_token_seconds_sum[5m]) / rate(vllm:time_to_first_token_seconds_count[5m])要看 p95 的话需要用 histogram 分位数函数histogram_quantile( 0.95, sum(rate(vllm:time_to_first_token_seconds_bucket[5m])) by (le) )服务端指标的好处是干净不包含客户端网络时延和客户端所在机器的调度抖动。缺点是它反映的是“服务端认为用户等了多久”和真实用户体验之间有一个网络偏差。如果你要评价线上用户体验以客户端观测为主如果你要定位服务端瓶颈以服务端指标为主。3.4 客户端和服务端指标对不上怎么办我一般会做一次“对拍”同一个请求客户端脚本记录 TTFT同时看服务端 histogram 的增量。两者差多少大概就是网络时延和代理或网关引入的时延。如果差异在一两百毫秒内属于正常范围如果差异到了秒级就要怀疑网关缓冲、代理缓存、连接复用等问题。还有一个容易被忽略的地方时间单位。客户端用毫秒服务端 metrics 通常显示秒。很多人对着数字怎么都想不通结果是把 0.85 秒读成了 0.85 毫秒。做对拍前先统一单位。4. 完整压测实操过程从脚本到结果4.1 搭建最小可测环境压测环境不一定要很复杂。我在本机测试时通常用一个 vLLM 服务加载一个 7B 级别的模型显存足够跑起来就行。请求走 OpenAI 兼容接口固定 prompt 和 max_tokens。这里最关键的是把输入长度和输出长度固定住否则每次请求的 prefill 和 decode 量不一样TTFT 就没有可比性。正式压测前先用 curl 或者单次 Python 请求验证服务可用。然后做一次 warmup时间不用太长30 秒到 1 分钟就够了。不做 warmup 的话首次请求可能要加载 CUDA kernel、初始化显存池TTFT 会异常偏大影响整个压测样本。4.2 设计压测场景压测大模型和压测普通接口不一样不能只堆并发。我习惯按下面这张表来控制变量场景并发prompt token 数max_tokens持续时间A 基线15005123 分钟B 中等并发165005123 分钟C 高并发325005123 分钟D 长输入1620005123 分钟对比 A 和 B/C能看到并发对 TTFT 的影响对比 B 和 D能看到 prompt 长度对 TTFT 的影响。如果还想看输出长度的影响可以把 max_tokens 改成 2048 再跑一轮。每轮之间建议间隔几分钟让服务端队列和 GPU 状态回到稳定水平。4.3 用 Python 脚本跑一轮并发压测单线程脚本只能验证功能真要压测还是得并发。我这里给一个简化版的多线程压测脚本足够跑出 TTFT 的分位数import concurrent.futures import requests import time import json import statistics url http://127.0.0.1:8000/v1/chat/completions payload_template { model: qwen/Qwen2.5-7B-Instruct, messages: [{role: user, content: 你好请介绍一下自己。}], stream: True, max_tokens: 256, } def single_request(idx): start time.perf_counter() ttft_ms None try: with requests.post(url, jsonpayload_template, streamTrue, timeout300) as resp: for line in resp.iter_lines(): if not line: continue line line.decode(utf-8) if not line.startswith(data:): continue data line[5:].strip() if data [DONE]: break if ttft_ms is None: obj json.loads(data) delta obj[choices][0].get(delta, {}) if delta.get(content): ttft_ms (time.perf_counter() - start) * 1000 except Exception as e: return None return ttft_ms with concurrent.futures.ThreadPoolExecutor(max_workers16) as executor: futures [executor.submit(single_request, i) for i in range(100)] ttfts [f.result() for f in futures if f.result() is not None] ttfts.sort() if ttfts: print(样本量:, len(ttfts)) print(平均 TTFT:, statistics.mean(ttfts), ms) print(p50:, ttfts[len(ttfts) // 2], ms) print(p95:, ttfts[int(len(ttfts) * 0.95) - 1], ms) print(p99:, ttfts[int(len(ttfts) * 0.99) - 1], ms)实际压测时我不会把样本量写成固定 100而是压固定时长比如持续 3 分钟最后用所有样本计算分位数。另外多线程脚本跑在高并发下要小心客户端本身成为瓶颈max_workers不要盲目开太大。4.4 解读结果先看分位数再看趋势下面是一组我在本地环境跑出来的示例数据只做参考不代表任何固定结论。低并发时 TTFT 很稳定基本在几百毫秒并发提升后平均值和 p95 都明显上涨。场景平均 TTFTp50p95p99A 基线420ms400ms500ms560msB 中等并发1100ms980ms1900ms2600msC 高并发2300ms2100ms3800ms5000msD 长输入2400ms2200ms4100ms5600ms从这个表可以明显看出并发和输入长度对 TTFT 的影响都很大。如果只看平均值B 场景好像还能接受但 p99 已经到了 2.6 秒对交互式应用来说已经算明显卡顿。所以大模型压测报告里一定要带分位数不能只写平均值。4.5 顺手把 TPOT 也算出来只测 TTFT 还不够至少要把 TPOT 一起算。简单理解一次流式请求的总耗时约等于 TTFT 加上生成剩余 token 的时间。如果已知总耗时和第一个 token 出现的时间并且知道总共生成了多少 token就能估算 TPOTTPOT ≈ (总响应时间 - TTFT) / (总输出 token 数 - 1)在 Python 脚本里记录第一个 token 的时间之后继续循环读取直到[DONE]拿到完整响应耗时和输出内容长度就能算出 TPOT。TTFT 描述“开口速度”TPOT 描述“输出节奏”两者结合才是完整的大模型服务体验画像。5. 常见问题、坑与排查速查5.1 第一个 SSE 包不是 token而是 roleOpenAI 兼容接口在流式返回时有些服务会先推一个只带delta.role的 chunkcontent 为空。如果脚本判断条件写得太宽松一见到data:就记时间TTFT 就会偏小。正确做法是必须判断delta.content存在且非空。我见过有人压测出来的 TTFT 比服务端指标低了一百多毫秒最后定位就是这个问题。5.2 JMeter 采样器耗时不等于 TTFT用 JMeter 的人最容易犯的错是直接看聚合报告里的 Average 响应时间。JMeter 默认采样器的“响应时间”是完整响应回来后的时间里面包含全部输出 token 的生成时间。如果你想看 TTFT必须用 JSR223 采样器自己记录或者把服务端 metrics 拉出来看。沿用默认报告只会让你得到一个大模型“总响应时间”而不是首 token 延迟。5.3 并发一高TTFT 就翻倍这是大模型服务非常典型的特征。prefill 阶段占用大量算力并发升高后请求会排队。如果你的压测目标是验证单请求体验那就用低并发跑多轮如果你的压测目标是找系统上限那就用阶梯并发慢慢加压观察 TTFT 的 p95 曲线什么时候开始拐弯。看到 TTFT 翻倍不必惊慌先确认是不是触达了算力瓶颈。5.4 首轮 warmup 缺失导致数据失真很多人刚部署完服务顺手就开压测。第一批请求往往会把 CUDA context、显存池、KV cache 分配流程全部走一遍TTFT 高出正常值好几倍。把这些脏样本混进结果里平均值会被明显抬高。我现在的习惯是先发少量请求预热 30 秒以上再开始正式采样如果压测平台支持丢弃前 N 个样本直接丢弃最省事。5.5 连接复用和网络代理干扰如果把 TCP 握手和 TLS 握手时间也算进 TTFT那测出来的其实是“建连时间 TTFT”。压测客户端最好用requests.Session或者连接池复用连接并且在脚本里对首次建连单独记录。如果有网络代理或七层网关SSE 流式模式可能被缓冲导致第一个 token 迟迟到不了客户端。排查时可以用同一份请求分别打直连服务和打代理服务对比 TTFT 的差异。5.6 排查速查表现象可能原因排查方向TTFT 整体偏高prefill 慢、资源不足看 GPU 利用率、降低并发、缩短 promptTTFT 持续上升请求排队、显存碎片看服务端 queue 指标、监控显存TTFT 忽高忽低其他任务抢占、网络抖动独占 GPU、检查服务端日志客户端 TTFT 比服务端指标低第一个 chunk 不是有效 token校验 delta.content 是否非空客户端 TTFT 比服务端指标高很多网络时延、代理缓冲抓包看首个 data 包到达时间以上这些坑我基本都踩过一遍。TTFT 这个指标本身不难理解难的是在压测工具、流式协议、服务端指标之间找到一条稳定、可重复的采集路径。我个人现在做压测的固定动作是先开服务端 metrics再用 Python 脚本做客户端观测两边对拍一次确认偏差最后用固定场景阶梯并发跑完整报告。这套流程跑顺之后基本不会再出现“数据看起来靠谱但说不出怎么来的”情况。如果你正在搭大模型服务的压测体系建议先把 TTFT 的采集链路固定下来再谈性能优化否则后面每次排查问题都可能要回到“这个指标到底准不准”的原始争论里。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →