首页 / 本地大模型

Token/s 怎么看?本地大模型速度的正确测试方法

作者 JC · 更新于 · 最后查证

简体版由繁体自动转换,少数用词可能和你习惯的不同。

要点速览

Token/s 是每秒处理或产生多少个 token,但一定要分清楚是「处理提示词(prefill)」还是「逐字生成(decode)」,两者的瓶颈和意义完全不同,只看一个数字会误判。正确的测法是固定模型、量化、提示词长度与生成长度,先暖机再重复量测,并记录所有设置;读别人的数字时也要先问清楚这些条件。

Token/s 是本地大模型最常被拿来比较的速度数字,也是最容易被误读的一个。同样写「每秒 XX token」,有人量的是模型读完你贴进去的长文章有多快,有人量的是它一个字一个字吐出答案有多快,这是两件不同的事,瓶颈也不同。这篇会先把名词拆清楚,再给你一套可以照着做的测试步骤,最后教你怎么判断别人贴的数字能不能信、能不能拿来和你的机器比。

这篇不会给任何「某某显卡跑某某模型是多少 tokens/s」的数字,因为那些数字取决于你的整套设置,没有出处就不该写。文中出现的数字,若不是来自官方文档(会标明)就是示范计算的假设值(会标明「假设」)。

Token/s 是什么?

token 是模型处理文字的最小单位,一个字、一个词或一段字根都可能是一个 token(详见术语表的 token)。Token/s 就是每秒处理或产生多少个 token。问题在于,一次完整的请求其实分成两个阶段,Hugging Face Transformers 的连续批量处理文档把一个请求的过程列成:等待排队、Prefilling(提示词的 token 在一次前向运算中被处理)、Decoding(输出的 token 一次产生一个)、完成。

  • prefill(预填充,又称 prompt processing):模型把你送进去的整段提示词读完、并建好 KV 缓存的阶段。llama.cpp 的 llama-bench 把它叫做 pp(prompt processing),README 的说法是「分批处理一段提示词」。
  • decode(解码,又称 text generation):模型接着一个 token 一个 token 往后生成的阶段。llama-bench 叫它 tg(text generation),README 的说法是「生成一串 token」。
  • KV 缓存(key-value cache):模型把已经处理过的 token 的中间结果存起来,之后生成新 token 时不必重算。Hugging Face 的教学说它能显著加速,代价是长输入或多轮对话时,这份缓存本身会占用很多内存。

所以「Token/s」至少有两个版本:prefill 的 Token/s(读提示词的速度)和 decode 的 Token/s(写答案的速度)。你平常在聊天界面「感觉到的快慢」,是这两段加起来的结果。

速度相关的名词,一次讲清楚

下面这些名词在不同工具里常被混用,所以每一个都先说它量的是什么。vLLM 的官方文档自己也提醒:各基准测试工具的指针术语并没有标准化,比较结果时要看量测点与算式,不要只看名字。

  • TTFT(time to first token,首个 token 时间):vLLM 文档定义为从送出请求到收到第一个串流输出的时间;NVIDIA GenAI-Perf 的定义也是「请求送出到收到第一个回应」。对本地用户来说,它大致就是「按下送出后要等多久才看到第一个字」,主要由排队、处理提示词(prefill)决定。
  • ITL(inter-token latency,token 间隔):vLLM 文档说它记录连续串流输出之间的时间间隔,统计时把所有成功请求的间隔合并计算。这是「字一个一个跳出来的节奏」。
  • TPOT(time per output token,每个输出 token 的时间):vLLM 文档的算式是「(端到端延迟 − TTFT)÷(输出 token 数 − 1)」,每个请求算一次再汇总,不含第一个 token。标准解码下每次串流输出通常只含一个 token,所以 ITL 和 TPOT 通常很接近。
  • 请求延迟(request latency):GenAI-Perf 的定义是从请求送出到收到最后一个回应的时间,也就是你等完整个答案的时间。
  • 输出 token 吞吐量(output token throughput):GenAI-Perf 定义为整段基准测试的输出 token 总数除以测试总时间。这是「服务器整体」同时服务多个请求时的产出,不是单一用户感受到的速度;GenAI-Perf 另外有「每位用户的输出吞吐量」(单一请求的输出 token 数除以其生成阶段的时间)。
  • goodput:GenAI-Perf 定义为「满足指定指针门槛的请求,每秒完成几个」,例如只算 TTFT 和 ITL 都低于门槛的请求。个人本地使用通常用不到,但看服务器型的测试报告时会遇到。

用 vLLM 的 TPOT 算式移项,可以得到一条好用的关系式(这是代数推导,不是新数据):

请求延迟 = TTFT + TPOT × (输出 token 数 − 1)

在标准解码下,每个 token 的间隔约等于 1 ÷ decode 速度。这可以用 llama.cpp 服务器 README 里 timings 范例的字段互相验算:predicted_per_token_ms 的倒数乘以 1000,就是 predicted_per_second。

llama.cpp 服务器回传的 timings 字段

如果你用 llama.cpp 的服务器(llama-server)当后端,每次回应都会带一个 timings 对象。官方 README 列出的字段如下,名称是官方的,意义依官方字段注解整理:

字段意义
prompt_n这次实际被处理的提示词 token 数
cache_n从先前缓存重用、因此不必重新处理的提示词 token 数
prompt_ms处理提示词花的毫秒数
prompt_per_second处理提示词的速度,也就是 prefill 的 Token/s
predicted_n生成的 token 数
predicted_ms生成花的毫秒数
predicted_per_token_ms平均每个生成 token 花的毫秒数
predicted_per_second生成速度,也就是 decode 的 Token/s

这里有个容易被忽略的细节:官方 README 说 cache_prompt 预设为 true,会重用上一次请求的 KV 缓存,只处理「没见过」的后半段。也就是说,如果你连续送同一段长提示词,后面几次的 prompt_n 会变得很小,算出来的 prompt_per_second 就不是在量「完整读一段长文章」的速度。要量 prefill,必须避免缓存命中(见下面的步骤)。

为什么单一个数字会骗人

假设一个请求有 P 个提示词 token、要生成 N 个 token,prefill 速度是 v_pp、decode 速度是 v_tg(单位都是 token/s),忽略排队与分词时间,则:

TTFT      ≈ P ÷ v_pp
生成时间  ≈ N ÷ v_tg
请求延迟  ≈ P ÷ v_pp + N ÷ v_tg

这是简化模型,只用来看「时间花在哪」。下面用假设数字走一遍(不是任何机器量到的结果,也不代表任何硬件):

  • 假设 v_pp = 500 token/s、v_tg = 20 token/s。
  • 情境 A:贴一篇 4000 token 的文章、要求 300 token 的摘要。TTFT ≈ 4000 ÷ 500 = 8 秒;生成 ≈ 300 ÷ 20 = 15 秒;总共约 23 秒。
  • 情境 B:只问一个 50 token 的短问题、要求 1000 token 的长答案。TTFT ≈ 50 ÷ 500 = 0.1 秒;生成 ≈ 1000 ÷ 20 = 50 秒;总共约 50.1 秒。

同一台机器、同一组速度,情境 A 的等待大半花在 prefill,情境 B 几乎全花在 decode。所以你要先知道自己的典型用法落在哪一边,才知道该看哪个数字。只看「20 token/s」会低估情境 A 的等待,只看「500 token/s」又会高估情境 B 的体验。

另外两件事也会让数字失真:

  1. 量测范围不同。llama-bench 官方 README 特别注明,它的量测不包含分词(tokenization)与采样(sampling)的时间,所以它的数字不等于你在真实应用里感受到的速度,因为少算了这两段时间。
  2. 名称不同、量的却是同一件事(或反过来)。例如 GenAI-Perf 的 TTFT 和 vLLM 的 TTFT 都是请求送出到第一个回应,但「回应」是不是刚好一个 token、有没有包含网络与排队时间,要看工具怎么量。

正确的速度测试方法

下面是一份可以照做的步骤。每一步都先说原则,再说在 llama.cpp 的 llama-bench 里怎么对应。旗标名称属于会变动的细节,一律以官方文档为准,用之前请先看你手上版本的 llama-bench --help。

1. 固定模型档与量化

同一个模型换了量化格式(例如 Q4 换 Q8),要读的数据量就不同,速度自然不同。比较时一次只换一个东西,并记下确切的文件名。量化的概念见量化是什么。llama-bench 用 -m 指定模型档,而且可以多次指定,在同一次运行里逐个比较。

2. 固定提示词长度与生成长度

两个阶段要分开量:

  • 只量 prefill:官方说 pp 测试用 -p 指定提示词 token 数。
  • 只量 decode:tg 测试用 -n 指定生成 token 数。
  • 两段一起量:pg 测试用 -pg <pp,tg>,先处理提示词再生成。

官方默认值是 -p 512、-n 128;你可以改成贴近自己用法的长度。要跳过某一段,README 范例用的是把对应数字设为 0。

3. 先暖机再量测

第一次运行常会比后面慢,因为有些工作只在第一次发生。llama-bench 预设会在正式测试前做暖机,要跳过得明确加 --no-warmup;llama-server 也有 --warmup 选项,官方说明预设为激活。所以用这两个工具时,通常不必自己多跑一轮暖机,但不要把暖机关掉再拿第一次的结果当稳态数字。

自己用其他方式量(例如在聊天界面计时)时,就要手动先送一个请求暖机,丢掉第一次的结果。

4. 重复多次,看中位数与离散程度

单次量测会受背景程序、缓存、温度影响。llama-bench 的 -r 控制每个测试重复几次(官方预设 5),README 说明结果会以平均 tokens/s 和标准差呈现;-o json 之类的输出还会附上每一次重复的个别结果,你可以自己从中算出中位数。

判读时两个数字都看:

  • 标准差相对于平均很大,代表这次量测不稳,先找原因(背景程序、散热、电源模式),不要急着下结论。
  • 平均和中位数差很多,代表有离群值,通常是某几次被其他程序干扰。

5. 记录所有设置

速度数字只有连同设置一起存起来才有意义。至少要记:

  • 模型文件名与量化格式
  • 提示词长度(P)与生成长度(N)
  • 上下文长度设置与测试时的「已填入深度」
  • 批量大小(batch)与 ubatch 大小
  • 线程数、GPU 层数(offload 多少层到显卡)
  • KV 缓存的数据类型、是否激活 flash attention
  • 后端(CPU、CUDA、Metal 等)与软件版本
  • 电源模式与室温(笔电尤其重要)

llama-bench 的 csv、json、sql 输出已经把许多设置(如 n_batch、n_ubatch、n_threads、n_gpu_layers、build 版本、CPU 与 GPU 信息)连同结果一起写出,用这些格式存盘,比手抄可靠。

6. 一次只改一个变量

llama-bench 官方说,除了 -r、-o、-v 之外,其他参数都可以用逗号给多个值,工具会跑所有组合。这很方便,但也容易一次扫太多维度、最后看不出谁造成差异。官方 README 的范例各自只扫一个维度:不同模型、不同批量大小、不同线程数、不同 GPU 层数、不同的预填上下文深度。你也照这个方式,先扫一个维度、看懂了再扫下一个。

7. 区分冷启动与稳态

冷启动(加载模型、第一次请求)和稳态(已暖机、连续请求)是不同的体验,要分开记。如果你的使用情境是「每次都重新开程序」,冷启动的等待就是真实等待,要另外量;如果是常驻服务,稳态才有代表性。

用服务器量真实使用情境

如果想量的是「你的应用实际收到的速度」,可以读 llama-server 回应里的 timings,并且:

  • 想量 prefill 时,请求里把 cache_prompt 设为 false,或每次换一段不同的提示词,避免缓存命中让 prompt_n 变小。
  • 每次都记下 prompt_n、cache_n、predicted_n,才知道这个速度是在多少 token 上量到的。

vLLM 官方文档也有类似的警告:对同一个服务器重复运行基准测试,可能重用留在前缀缓存里的提示词,使吞吐量被灌水;他们建议变更种子、重置或重启服务器。原理在本地跑 llama.cpp 时同样适用:任何形式的缓存命中,都会让 prefill 看起来比真实情况快。

速度指针比较表

下表整理每个指针「在量什么、什么情境该看、受什么影响」。「受什么影响」栏是依上面引用的官方说明与原理推导,不是量测结论。

指针在量什么什么情境该看它主要受什么影响
prefill 速度(pp、prompt_per_second)每秒能读完多少提示词 token常贴长文章、长代码、做 RAG 或长对话历史的用法偏运算量(Hugging Face 说 prefill 属于 compute-bound);提示词长度、批量大小设置、是否重用缓存
decode 速度(tg、predicted_per_second)每秒生成多少个新 token需要长答案、串流阅读、写作与聊天偏内存带宽;模型每个 token 要读的权重量、量化、上下文深度(KV 缓存要重读)
TTFT送出请求到第一个 token 出现的等待在意「按下送出后有没有反应」的交互式使用排队时间、提示词长度 ÷ prefill 速度、是否命中缓存
ITL/TPOT字与字之间的间隔(约为 1 ÷ decode 速度)在意串流输出流不流畅与 decode 速度相同;同时服务的请求变多时也会变长
请求延迟从送出到整个答案结束的总时间脚本与自动化、一次要完整结果的用法TTFT 加上生成时间,也就是 P、N、两个速度共同决定
输出 token 吞吐量(整体)同时服务多个请求时,服务器每秒总共产出多少 token一台机器给多人或多个工作流程共用同时请求数(并发)、批量与槽位设置;单一用户的速度通常会因此变慢

最后一列要特别小心:吞吐量变高不代表你单独使用时变快。llama-server 有 -np(槽位数)和连续批量(continuous batching)等设置(以官方文档为准),把机器切给多个请求共用,整体产出可以提高,但每个请求拿到的速度会下降。个人单人使用时,通常看前五列就够了。

速度是由什么决定的

decode:通常卡在内存带宽

Hugging Face 的对话指南写得很直接:模型越大越慢,因为生成每个 token 时,每个「激活的参数」都必须从内存读一次;这是文字生成的瓶颈,改善的主要方法是量化模型,或换内存带宽更高的硬件,单纯增加运算能力没有明显帮助。同一个文档也说,MoE 模型参数虽多,但每个 token 只激活一小部分,因此对内存带宽的需求通常低得多(MoE 的详细说明见MoE 混合专家模型)。

把这个说法写成算式,可以得到一个简化的上限估算(这是依上述说明做的推导,不是官方公式,实际速度会低于它):

decode 速度上限 ≈ B ÷ (S_权重 + S_KV)

B       :有效内存带宽(bytes/s),是权重所在的那块内存的带宽
S_权重  :生成一个 token 要读的权重量(bytes);MoE 只算被激活的部分
S_KV    :生成一个 token 要重读的 KV 缓存量(bytes),随上下文变长而变大

示范计算(全部是假设数字,只为了说明比例关系):

  • 假设 B = 200 GB/s、S_权重 = 8 GB、S_KV 可忽略:上限 ≈ 200 ÷ 8 = 25 token/s。
  • 同样的 B,量化后 S_权重 变成 4 GB:上限 ≈ 200 ÷ 4 = 50 token/s,也就是权重读取量减半,上限约加倍。
  • 同样 4 GB 权重,但长上下文使 S_KV = 1 GB:上限 ≈ 200 ÷ (4 + 1) = 40 token/s。

这个算式能回答两件事:为什么量化常让 decode 变快(读的数据变少,不只是「省空间」),以及为什么权重放在哪块内存很重要。权重放在显卡的显存(VRAM,显卡上的内存)、还是系统内存(RAM)、还是硬盘,B 差距很大,详见显存、内存、硬盘各扮演什么角色。哪些模型装得进你的显存,则见依显存选模型。

prefill:偏向运算

Hugging Face 的量化指南说,prefill 和大批量属于 compute-bound(运算受限)的情境,而一次一个 token 的生成才是 memory-bound(内存受限)。直觉上的解释(这是简化的推论,不是官方说法)是:prefill 一次处理很多 token,同一份权重读进来后可以供一整批 token 共用,所以比起 decode,更容易先碰到运算能力的上限。这也是为什么同一台机器的 pp 速度数字常常比 tg 大很多:两个数字本来就不是在量同一件事。

上下文变长,decode 为什么变慢

生成每个新 token 时,模型都得读取先前所有 token 的 KV 缓存。Hugging Face 的教学说,自回归解码时,这些大的 key-value 矢量需要在每一步被重新加载,所需的内存带宽可能成为严重的时间瓶颈。

KV 缓存有多大?教学里给的算式是:缓存中的浮点数个数 = 2 × 串行长度 × 层数 × 注意力头数 × 每头维度。教学举了一个假设的 16000 token 输入,对一个范例模型算出约 80 亿个浮点数,以 float16 保存约 15 GB,大约是该模型权重的一半。这是该范例模型的数字,不是你的模型;但它说明了上下文够长时,S_KV 可以大到不能忽略。教学也提到,减少 key-value 头数(如 MQA)可以同时降低缓存大小与带宽压力;各模型的实际设计不同,要看模型本身。

要量这个效应,用 llama-bench 的 -d <n>:官方说明它会在测试前先把 KV 缓存预填 n 个 token,让你在指定的上下文深度下量 pp 与 tg。官方 README 的范例就是比较 -d 0 与另一个深度下的 pp 和 tg。

另外,Hugging Face 教学也提到,自注意力的运算量与内存在预设实作下会随串行长度呈二次方成长(Flash Attention 则让内存需求只随长度线性增加),所以提示词很长时,prefill 的总时间成长会比提示词长度更快。实际比例取决于实作与硬件,请量给自己看。

如何判断你的瓶颈:定性方法

不必先懂硬件细节,用「改一个变量,看哪个指针动」就能推测:

你做的实验如果出现这个现象倾向的判断
换成更小的量化(读取量变少)decode 明显变快,prefill 变化较小decode 受内存带宽限制
换成更小的量化decode 几乎没变瓶颈可能在别处(部分层跑在较慢的设备上、设置问题、散热)
调整批量大小(-b、-ub,以官方文档为准)prefill 跟着明显变动prefill 受运算与批量效率影响
调整线程数CPU 后端的速度明显变化,加到一定程度后不再增加已接近 CPU 运算或内存的上限
提高 -ngl(放到显卡上的层数)层数增加,速度上升,尤其是整个模型都放进去时先前有层留在较慢的内存上拖慢整体
加大上下文深度 -ddecode 随深度下降KV 缓存读取量在增加
长时间连续跑,速度逐渐下降同一组设置,后面几轮比前面慢可能是降频,见下面

运行这些实验时,记得第 6 步:一次只改一个变量,否则你分不出是哪个造成的。

读别人的测试数字时要问什么

在网络上看到「我的机器跑 XX 模型有 YY tokens/s」时,先逐项确认:

  1. 是什么硬件? 显卡型号、显存大小、系统内存,或是 CPU 与统一内存的机种。
  2. 是哪种量化? 同一个模型的 Q4 与 Q8 要读的数据量不同,不能直接比。
  3. 上下文长度多少? 是空上下文量的,还是已经填入长文档后量的。差别可能很大(见上一节)。
  4. 是 prefill 还是 decode? 没有说明的 tokens/s,最常见的情形是指其中一种,但你无从得知是哪一种。
  5. 有没有暖机? 取的是第一次还是多次后的稳态。
  6. 重复了几次? 只有一次的数字,没有标准差可看。
  7. 批量与并发设置? 是单一请求,还是同时服务多个请求的整体吞吐量。
  8. 有没有重用缓存? 特别是量 prefill 时,缓存命中会让数字失真。
  9. 后端与软件版本、是否 offload 到 GPU? 同一个模型换个后端或版本,结果可能不同。
  10. 量测工具量到哪里? 像 llama-bench 就不含分词与采样的时间。

另一个原理上的提醒:不同模型可以有不同的分词器(tokenizer,把文字切成 token 的规则),同样一段文字被切成的 token 数不一定相同,所以跨模型比较「每秒几个 token」时,不能直接当成「每秒几个字」。

答不出其中好几题的数字,只能当作「这个量级有人跑得到」的旁证,不要拿来决定要不要买硬件。你自己的机器最后还是要自己量:因为你的系统内存、散热、电源模式、背景程序、驱动与软件版本都和别人不同,而你真正关心的是自己的典型提示词长度与回答长度下的等待时间。如果你用的是站内介绍的本地大模型工具,可以先看Strata 与 PolyStrata 比较,了解工具各自如何加载与运行模型,再决定用哪种方式量。

笔电与长时间负载

笔电长时间高负载时,芯片可能因过热或电源限制而降频,使速度逐渐下降,所以短时间的测试结果可能比长时间使用时的乐观。这件事的原因与对策请看笔电跑 AI 的散热与电源注意事项,这里只提醒一点:在笔电上测速时,除了照上面的步骤,还要把测试拉长,并分段记录速度有没有随时间下降。

常见误解

  • 「Token/s 越高越好」:要看是哪个阶段、哪种用法。短问题长答案看 decode,长文章短摘要看 prefill 与 TTFT。
  • 「llama-bench 的数字就是我聊天时的速度」:不一定。它不含分词与采样,也没有你的界面开销、系统提示词与历史对话的影响。
  • 「换更强的运算芯片 decode 就会快」:Hugging Face 的说明是,单纯增加运算能力对 decode 没有明显帮助,带宽与读取量才是重点。但 prefill 偏运算,情况不同。
  • 「吞吐量高代表我用起来快」:整体吞吐量和单一请求的速度是两回事,同时服务越多请求,单一请求通常越慢。
  • 「量一次就够」:单次量测没有离散程度可看,至少重复数次,并看标准差。

延伸阅读

常见问题

本地大模型 Token/s 多少才算够用?

没有放诸四海皆准的数字,要看你的用途:实时对话最在意首个 token 出现前要等多久(TTFT)和生成时的流畅度;一次丢进大量文章的用法,提示词处理速度更关键。比较实际的做法是先用自己的典型提示词长度与回答长度,算出整个请求要等多久(算式见文中),再决定能不能接受。

为什么提示词处理速度(pp)和生成速度(tg)差那么多?

两个阶段做的事不一样:处理提示词时,许多 token 可以一次丢进模型运算;逐字生成时,一次只产生一个 token,Hugging Face 的文档指出此时 GPU 大部分时间花在从内存读取权重,而不是运算。所以它们的瓶颈不同,数字本来就不能直接比较。

llama-bench 的结果是平均值,要怎么看中位数?

官方 README 说明每个测试会依 -r 重复,结果以平均 tokens/s 加标准差呈现;json 等输出格式还会附上每一次重复的个别结果。所以你可以输出 json,自己从个别结果算中位数,并用标准差判断这次量测稳不稳定。

同一个模型为什么我量到的 Token/s 和网络上的不一样?

常见原因包括硬件、量化格式、上下文长度、是不是同一个阶段(prefill 或 decode)、有没有暖机、批量与线程设置,以及提示词有没有被缓存重用。这些条件只要有一项不同,数字就不能直接比,所以你自己的机器要自己量。

上下文越长,生成速度为什么会变慢?

生成每个新 token 时,模型都要重新读取先前所有 token 的 KV 缓存(key-value cache)。Hugging Face 的教学指出,这种反复重读对自回归解码来说可能成为严重的内存带宽瓶颈,所以上下文越长,每个 token 要读的数据越多。

笔电跑一阵子之后 Token/s 掉下来,是量错了吗?

不一定是量错,长时间高负载时降频可能让速度逐渐下降。这时单次短测试的数字会偏乐观,建议把测试拉长,并另外记录温度与电源模式,细节见笔电散热那篇。

留言

用 GitHub 账号登录就能留言,留言会公开显示在 GitHub Discussions。留言前请先看社区规则。

本地大模型