跳到主要内容

推理性能:加载、首字延迟、解码与吞吐

“每秒生成多少 token”只能描述推理体验的一部分。一个系统可能生成很快,却在读取长提示词、排队或加载模型时让用户等很久。比较运行时之前,先固定问题:是一个人在本机交互,还是多人同时请求;关心第一句话出现,还是整批任务尽快完成。

一次请求有几段时间​

一次请求从发出到结束,要依次经过几段等待。模型还没载入时,要先从磁盘读文件、分配内存、把权重传到显卡,有的运行时还要编译或准备内核,这一段叫冷启动。收到文本后,运行时先把它切成 token(tokenization);服务端排队、网络和客户端缓冲,也会加进用户感受到的等待。

模型真正干活的部分分成两段。第一段叫 prefill:把整段提示词一次读完,算出后面生成要用的状态。第二段叫 decode:在普通的自回归生成里,一个接一个地追加输出 token。在下一节的时间线算例里,请求发出 0.8 秒后才出现第一个 token,这 0.8 秒主要花在 prefill;之后每 0.02 秒出一个 token,就是 decode 的节奏。

指标测量范围容易混淆的地方
冷启动时间启动或加载到可服务文件系统缓存可能已热
TTFT(首 token 延迟)请求发出到首个输出 token有的统计包含排队、网络,有的不包含
TPOT(每个输出 token 的平均耗时)/ token 间隔首 token 后的生成节奏流式网络块不一定一块一个 token
端到端延迟请求发出到完整响应结束包含输入处理与最后收尾
吞吐一段时间内完成的请求或输出 token多用户总吞吐不能当作单用户速度

vLLM 的服务基准接口分别提供首 token、每输出 token、token 间隔和端到端延迟等口径,并支持百分位数。使用其他工具也应明确相同的起止点,尤其注明是否包含客户端排队。

推测解码可在一轮验证中提交多个 token;计时应包含起草、验证与状态刷新。

一个时间线算例​

假设请求在 0 秒发出,0.8 秒收到首 token,2.8 秒收到第 101 个也是最后一个 token。忽略额外收尾时,TTFT 为 0.8 秒,首 token 后的平均间隔是 (2.8 - 0.8) / (101 - 1) = 0.02 秒,相当于这段 decode 每秒 50 token。若把输出数除以整个请求时间,则是 101 ÷ 2.8,约 36.1 token/s。这两个数字各有定义,不能混着比较。

真实流式接口可能把多个 token 放在一个数据块里,按数据块计数会高估或低估速度。应使用模型 tokenizer 对完整输出计数,或采用服务端可靠的 token 用量,并保留测量口径。

逐字生成时,每个 token 都要把全部权重读一遍​

decode 的时间花在哪里,决定了批处理、量化、推测解码和 KV 缓存这几种手段为什么有用。答案是:模型每生成一个 token,都要把全部权重从显存读一遍,而读的时间远比算的时间长。

先看为什么要读全部权重。生成下一个 token 时,刚生成的那个 token 的向量要依次穿过每一层,每一层都拿自己的权重矩阵去乘它。所以每个权重都会被用到一次;这个 token 算完,下一个 token 又要把所有权重重新读一遍。

再看读和算哪个慢。设想一个 80 亿参数的模型,比如 Llama 3 8B,每个参数用 BF16 存,占 2 字节,权重一共 8×10⁹ × 2 = 16 GB。把它放在 NVIDIA L40S 上,这块卡的标称参数是显存带宽每秒 864 GB、BF16 张量算力每秒 362 万亿次运算:

  1. 读一遍权重:16 ÷ 864 ≈ 0.0185 秒,也就是 18.5 毫秒。
  2. 算一个 token:每个参数做一次乘法、一次加法,共 16×10⁹ 次运算;按每秒 362×10¹² 次算,只要约 0.04 毫秒。

读比算慢四百多倍,所以一个人逐字生成时,速度几乎完全由“权重有多大”和“显存读得多快”决定。就算计算不花时间,这个模型在这块卡上最快也只能每秒出 1 ÷ 0.0185 ≈ 54 个 token。上面时间线里每 0.02 秒一个 token,也就是每秒 50 个,正落在这个量级上。

换到自己的机器上,还是同一个除法。苹果标称 M5 Pro 的内存带宽是每秒 307 GB。Llama 3.1 8B 量化成 Q4_K_M 后约 4.9 GB(见量化笔记),上限约 307 ÷ 4.9 ≈ 63 token/s。

混合专家模型(MoE)是例外。它的每个 token 只经过路由选中的几个专家,所以每步读的字节按激活参数算,比全部权重少得多。可是没被选中的专家也得放在内存里,容量仍按总参数算,见混合专家。

几种常见的提速和省内存手段,都是在“每个 token 读一遍权重”这一步上做文章:

手段它在这一步上做了什么详见
批处理权重读一遍,给多个用户各算一个 token下一节
量化权重变小,每个 token 少读一些字节模型量化
推测解码权重读一遍,同时验证小模型猜出来的一串 token推测解码
KV 缓存每步除了读权重,还要读之前所有 token 的缓存;上下文长、并发多时,这部分流量会追上权重注意力变体与 KV 缓存

prefill 不受这个限制。提示词里的几千个 token 是一起算的,权重读一遍就能用在所有这些 token 上,这时算力才是瓶颈。所以长提示词主要拖慢首字,长输出主要拖慢 decode。

为什么批处理会改变结果​

批处理用的正是上一节那一步:权重读一遍,可以同时给多个用户各算一个 token。还用同一个模型和同一块卡来算,每个用户的上下文都是 4K 个 token。按 Llama 3 8B 的形状,每个 token 的 KV 缓存占 128 KiB,4K 个 token 就是 512 MiB,约 0.54 GB(算法见 KV 缓存笔记):

同时解码的人数每步要读的数据每步耗时每人每秒得到的 token总吞吐
116 + 0.54 ≈ 16.5 GB16.5 ÷ 864 ≈ 19.1 毫秒约 52约 52 token/s
816 + 8 × 0.54 ≈ 20.3 GB20.3 ÷ 864 ≈ 23.5 毫秒约 438 ÷ 0.0235 ≈ 340 token/s

8 个人一起解码,总吞吐涨到原来的六倍多,每个人的 token 间隔却从 19.1 毫秒拉长到 23.5 毫秒。权重只读一遍,缓存却是每人一份,所以批越大,每步要读的缓存越多,算的量也越接近算力上限,单个用户就越慢。

除了每步变慢,单个请求还可能要先等着组批。连续批处理允许完成的请求离开、新请求加入,不必让所有序列等到最长序列结束。

并发还会增加 KV 缓存需求。PagedAttention 论文针对动态变化的缓存使用分页式管理,减少碎片和重复,从而支持更有效的服务调度。它改善的是特定内存管理问题,并不会消除权重、缓存容量或算力上限。

长提示词与短输出任务主要关心输入处理和 TTFT;短提示词与长输出任务更能暴露 decode 成本。降精度、增加批量与缩短上下文各自改变不同成本,不能互相替代:降精度让每次少读权重,增加批量让一次读权重分给更多人,缩短上下文让每步少读缓存。

缓存命中需要单独报告​

模型驻留、内核预热和前缀缓存是不同的“热”。模型已经载入,不代表本次提示词有缓存;重复完全相同的系统提示又可能让第二次明显更快。对比运行时应分别测试冷启动、驻留但无前缀命中、预期业务下有前缀命中的情况。

缓存只能在相应输入和模型配置一致时复用。把每次请求都变成同一个提示词,会测出理想重复访问速度,无法代表不断变化的真实任务。修改模型、适配器或上下文后,也要避免把旧缓存当作有效证据。

测量要保留失败与尾部等待​

上面按标称带宽和算力估出的是理想下限,真实瓶颈要在目标设备上测。固定模型、量化、运行时、输入长度和并发,记下中位数和高百分位延迟,把超时和失败也留在结果里;多用户服务的到达率和施压方式见本地模型评测。

最后把推理速度放回任务:回答是否正确、工具调用能否完成、是否需要重试。高 token/s 不能抵消错误工作造成的总耗时。具体运行时的兼容性见运行时选型,本地任务成败的评价见模型与 Agent 评测。

探索关联打开关联网络