深蓝黑背景封面。左侧是一次流式请求的时间轴解剖图:灰色箭头标注「请求发出」,其后依次排列 t1 到 t5 五个 token 方块;青色括号从请求发出横跨到 t1,标注「TTFT:排队 + prefill」;紫色括号标在相邻两个 token 之间,标注「TPOT:逐 token 间隔」;下方灰色大括号横跨整段,标注「端到端延迟 = TTFT + TPOT ×(输出 token 数 − 1)」。再下方一张深色面板写「吞吐 = 输出 token 总数 ÷ 总耗时(tokens/s)」,并注明「并发压测时看聚合吞吐定容量」「TTFT / TPOT 定体感,报数要带 P50 / P99 分位」。右侧大标题「首 token 延迟和吞吐,推理性能到底怎么衡量」,副标题「把『快』拆成三个可测指标:TTFT、TPOT、吞吐」,小注「压测先统一指标口径,再谈 SLA 与容量规划」。左上角标注「面试经典 · 推理性能」,右上角圆角胶囊写「Day 09 · 12-06」。 图 1:本文封面,一次流式请求的时间轴解剖与三个核心指标。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「如何衡量大模型的推理速度」这道题,考的不是让你背几个缩写,而是你能不能把「快」这个模糊的词拆成可测量、可对比、可写进 SLA 的指标。模型服务上线前定容量、定体验目标,第一步都是把指标口径统一。

一句话结论:把一次请求的时间轴切开——首 token 延迟看 TTFT,流式体感看 TPOT,服务能力看吞吐,三者各有各的口径,不能混报。

先切时间轴:prefill 与 decode 是两种活。

大模型生成一条回复分两个阶段。prefill 阶段把全部输入 token 一次性并行过一遍前向,产出第一个输出 token;decode 阶段进入自回归循环,每一步只产一个 token,串行推进直到结束。两个阶段对硬件的压力完全不同:prefill 是算力密集,一次大矩阵运算;decode 是显存带宽密集,每步都要把整个 KV cache 读一遍。正因为两阶段性质不同,「这条请求花了 5 秒」是没有信息量的说法——必须分段计量。

三个指标各自的定义与分工。

TTFT(Time To First Token):从请求发出到收到第一个输出 token 的时间,等于排队时间加 prefill 时间。它决定用户按下回车后的「首响体感」,对实时交互场景最重要;批量离线任务反而不在乎它。敏感因素是排队深度与输入长度,优化方向是 prefix 缓存和削峰排队。

TPOT(Time Per Output Token):相邻两个输出 token 的平均间隔,近似等于单步 decode 耗时。它决定流式输出的打字机效果顺不顺——TPOT 是 50 毫秒,用户看到的就是每秒约 20 个 token 的稳定吐出。敏感因素是显存带宽与 batch 大小,优化方向是 KV cache 管理与连续批处理。端到端延迟近似等于 TTFT 加 TPOT 乘(输出 token 数减一),由输出长度主导。

吞吐(throughput):全部请求的输出 token 总数除以墙钟时间,单位 tokens/s。它是容量规划与成本核算的核心数:同样一张卡,吞吐越高,单 token 成本越低。

三个指标互相拉扯:并发实验看张力。

吞吐和延迟不是同向指标——提高并发能摊薄每步 decode 的开销、推高吞吐,但单步变慢、TPOT 变差。用一段模拟把这个张力量化(Node.js 22.15.0,node metrics.js):简化一台服务器,prefill 按输入长度计、单步 decode 随并发放大,扫并发梯度:

function simulate(concurrency, reqsPerCon) {
  const inputLen = 512, outputLen = 100;
  const prefillPerToken = 0.02;          // ms / 输入 token(简化模型)
  const decodeStepBase = 8;              // ms,单请求时单步 decode
  const decodeStep = (c) => decodeStepBase * (1 + (c - 1) * 0.12); // 并发放大单步
  const prefillMs = inputLen * prefillPerToken;
  const tpots = []; let t = 0, totalOut = 0;
  for (let r = 0; r < reqsPerCon * concurrency; r++) {
    const tpot = decodeStep(concurrency);
    tpots.push(tpot);
    t += prefillMs + tpot * (outputLen - 1);
    totalOut += outputLen;
  }
  const wallMs = t / concurrency;        // 并发分摊墙钟
  const tpotP50 = tpots.slice().sort((a, b) => a - b)[Math.floor(tpots.length / 2)];
  return { tpotP50, throughput: totalOut / (wallMs / 1000) };
}
for (const c of [1, 4, 16, 64]) {
  const r = simulate(c, 8);
  console.log(`并发 ${String(c).padStart(2)}: TPOT(P50)=${r.tpotP50.toFixed(2)}ms  吞吐=${r.throughput.toFixed(0)} tokens/s`);
}

实测输出:

并发  1: TPOT(P50)=8.00ms   吞吐=125 tokens/s
并发  4: TPOT(P50)=10.88ms  吞吐=368 tokens/s
并发 16: TPOT(P50)=22.40ms  吞吐=718 tokens/s
并发 64: TPOT(P50)=68.48ms  吞吐=943 tokens/s

并发从 1 提到 64,吞吐涨了约 7.5 倍,但单 token 间隔从 8 毫秒劣化到 68 毫秒——打字机效果肉眼可见地变卡。这条曲线先陡后平:算力或显存打满后吞吐见顶,再加并发只会纯劣化延迟。容量规划要找的就是拐点前的甜点区。

压测时怎么采集才不算数造假。

三条纪律。第一,扫并发梯度而不是单点压测:只报一个并发下的数字,看不出甜点区在哪。第二,报分位数而不是平均值:TTFT、TPOT 都要带 P50/P99,平均值会被长尾请求掩盖。vLLM 官方的 vllm bench serve 压测工具就内置了这套口径,默认输出 TTFT、TPOT、ITL(token 间延迟)与端到端延迟的分位数,并支持按指标设 SLO 阈值统计达标率(goodput)。第三,控制输入输出长度:吞吐对输出长度敏感,对比两次压测必须先确认数据集的输入输出分布一致,否则数字不可比。

深蓝黑背景概念图。顶部小字「概念图 · 推理性能指标」,主标题「先把一次请求的时间轴切开,再谈三个指标」。第一块面板是分段时间轴:左侧青色大块标注「prefill 阶段:全部输入 token 并行一次前向」,右侧六个紫色小块标注「decode 阶段:每步只产 1 个 token,串行」,块内标 t1 至 t6;下方青色括号写「TTFT = 排队 + prefill」,紫色括号写「TPOT」,右端小字写「端到端 ≈ TTFT + TPOT ×(n−1)」。中间三张指标卡并排:TTFT 卡写「首 token 延迟」「用户体感:响应快不快」「敏感因素:排队深度、输入长度」「优化方向:prefix 缓存、削峰排队」;TPOT 卡写「相邻 token 间隔」「决定流式打字机顺不顺」「近似 = 单步 decode 耗时」「敏感因素:显存带宽、batch 大小」「优化方向:KV cache、连续批处理」;吞吐卡写「= 全部请求输出 token ÷ 墙钟时间」「随并发先升后平,算力显存打满见顶」「容量规划与成本核算的核心数」「压测要扫并发梯度找拐点」。底部总结条写「TTFT 看首响、TPOT 看流式、吞吐看容量;报告指标必须带并发数与 P50 / P99 分位,否则不可比」,最下方小字补充吞吐与延迟互为约束、压测找拐点前的甜点区。 图 2:两阶段时间轴、三指标分工与压测纪律。

面试现场的答题结构。

先给结论「快不是一个数,是三个指标」,然后切时间轴讲 prefill 与 decode 的性质差异,顺势给出 TTFT、TPOT、端到端、吞吐四个定义与各自的用户含义。接着讲指标间的张力:吞吐与延迟互为约束,并发实验说明甜点区的存在。追问「怎么压测」时答三条纪律:扫并发梯度、报 P50/P99 分位、固定输入输出长度分布。最后落到工程:TTFT 定首响 SLA、TPOT 定流式体验、吞吐定容量与成本,报数必须带并发数与分位口径。你们团队的模型服务 SLA 是按哪个指标定的?压测时踩过「平均延迟很好看、P99 崩了」的坑吗?欢迎聊聊。