Ollama 0.33.3 改变了 prompt_eval_duration 的含义

HN AI Best Practices 2026-09-15T06:21:40.098934

如果你一直按老规矩从 Ollama 的指标里算预填充(prefill)吞吐量,那你的数字从 0.33.3 起就是错的——而且是错得好看的那种。

缓存命中的情况下,我自己的工具报出了 4375 tok/s,真实值却只有 115 tok/s。虚高了 38 倍,既没有报错,也没有警告;除了多出一个新字段,没有任何版本层面的提示。

改了什么

0.33.3 给 /api/generate/api/chat 的响应加上了 prompt_eval_cached_count,并重新定义了 prompt_eval_duration:它现在只覆盖未命中缓存的那部分 prompt token。

prompt_eval_count 没动。它表示的仍然是 prompt 总大小,包含命中缓存的 token。

于是那个标准公式——

prompt_eval_count / prompt_eval_duration

——就变成了拿总 token 数,去除以只评估其中一部分所花的时间。误差会随缓存命中率一起变大:冷启动测试时误差最小,在你真正关心的稳态场景下误差最大。

下面是对 qwen2.5:7b 连续发两次完全相同的请求的结果:

冷启动那行没问题,这也是它容易被忽略的原因之一——在刚启动的守护进程上随手一验,看起来完全合理。

修复办法

除以真正被评估的 token 数:

prompt_eval_count - prompt_eval_cached_count

Ollama 自己的 Metrics.Summary() 就是这么改的。该字段带 omitempty,所以在旧版守护进程上、或在不报告这个字段的运行器上,它会缺失——建议把「缺失」当成「未知」而不是 0,因为这是两种不同的结论,而只有其中一种意味着出问题。

同一个事实,三个不同的名字

Ollama 通过三种协议暴露同一份信息,形状却各不相同:

Anthropic 兼容端点还埋着第二个坑。那里的 input_tokens 悄悄从「prompt 总量」变成了「总量减去缓存量」。在 0.33.3 上实测:同一个 prompt,冷启动报 input_tokens: 38,缓存命中报 input_tokens: 1。如果你在把那个字段累加起来做统计,那你的总数已经缩水了,而且没有任何东西提醒你。

为什么数字波动这么大

缓存命中与否,会让预填充性能相差两个数量级,而不是几个百分点。原因在于:从某个点开始,KV 缓存(键值缓存)的复用是全有或全无的——只有当提示词从第一个 token 起完全匹配,缓存才有效;一旦有一个 token 不同,它后面的所有内容都会被丢弃并重新计算。

这带来一个实用结论——把稳定的内容放在前面,把易变的内容放在后面。这一点 OpenAI、Anthropic 和 Bedrock 都有文档说明,算不上新发现。但不太明显的是,在本地环境下这个断崖有多残酷,因为本地根本不提供任何关于缓存复用的提示。同一个模型,同一个 324-token 的系统提示词,只把一个 21-token 的时间戳从顶部挪到底部:

同样的词,同样的 token 数量,预填充时间变成 3.5 倍。托管服务商有文档提醒你这么做,但在本地你完全得不到反馈,根本不知道自己有没有遵循这个建议——于是一个提示词模板可能悄悄让你每一轮都付出整个系统提示词的开销,持续好几个月。

我一开始搞错的测量方法

这个值得记下来,因为它当时看起来像是个结论。

我最初的做法是比较布局 A 的缓存有多少能被布局 B 复用——即原始提示词和改写后的提示词对比。改写版得分很差,我差点就发表说改写让情况变糟了。它和 A 的差异在靠前的位置,所以自然几乎无法复用 A 的缓存。

但这问的是错误的问题。真实流量从来不会先发 A 再发 B;它每一轮都发同样的布局,只是值不同。真正该测的是:一个布局能否在它自己的下一轮中存活下来。所以每个布局要发两次,一次用它自己的值,一次用改变后的值,算数的是第二次。只测第一次,只能告诉你一个提示词和自己有多匹配,而这对任何布局来说都接近 100%。

我能发现这个问题,只是因为结果和真实守护进程的行为完全相反。我写的任何单元测试都不可能发现它;代码完全按照我告诉它的去执行了。

局限

一个模型、一个守护进程、一种提示词长度、llama.cpp 运行器。我没测过不同模型系列、长上下文、并发场景,也没在 MLX 运行器上测过。指标变化是通用且可从源码验证的;那个具体的 3.5 倍比例只是示意,不是基准测试结果,我不会拿它当基准测试数据来引用。

查看原文