返回博客

2026.09.20

同一个模型,我出的题考 94 分,公开题库 76.7 分——中间那 17 分是谁送的?

让模型只做一次前向、不解码、直接读选项字母的概率,这套玩法能有多快、多准。85 道自造题上它考了 94 分,换成 300 道公开题只剩 76.7 分。顺带拆一条并发曲线里的除法,和一个让我读出 6/20 荒谬数字的探针 bug。

LLMbenchmark本地推理实测

同一个模型,同一套提示词,同一张卡。

我自己出的 85 道题,它考 80 分,94.1%。换成公开数据集抽的 300 道题,76.7%

中间差 17.4 个百分点。模型没变,变的是出题的人。

这篇记的就是这 17 分是怎么被我自己送出去的,顺带记下这套"只做一次前向读概率"的玩法到底快在哪、买到了什么、以及一个我原本笃定会成立、实测却完全没有的收益。

想直接拿并发判据的看第 3 章,那是一个除法,我算出来 314ms、实测 311ms;想知道我怎么把自己的题库考砸的,看第 5 章。

1. 这套玩法:不让它说话,只看它想说什么

正常用大模型做判断,你会让它吐一段 JSON:{"action": "B", "reason": "..."}。它得一个一个 token 往外蹦,慢。

这套玩法把它掐住:请求里设 max_tokens=1,再要 top_logprobs=20,然后只看返回里 ABC 这几个字母各自的 logprob,做一次 softmax,谁大选谁。模型只跑了一次前向——把整段提示词读进去、算一遍——就没了,没有第二步。

好处不用解释也看得出来:解码每生成一个 token 都要把全部激活权重再读一遍,你要 30 个 token 就读 30 遍;读 logit 只读一遍。我这边一次决策的提示词是 165 个 token,一次性过完权重,读一次摊在 165 个 token 上。

机制本身一点都不新,是十年前就有的 logit scoring。最近有人把它包成开源项目重新发出来,用一个 4B 模型读选项概率,我是顺着那个项目开始测的。它自带的单测在我本机显卡上跑通,11 passed,然后我开始出题。

2. 85 道题,三个模型,排名在最后一批翻了

题全是我自己造的中文场景,照着我手头真要判的东西写:一个自动化脚本的某一步算不算成功、一条事件要不要升级、一段文本该归哪一类。

参赛的三个:一个 35B 的 MoE(内部分成很多"专家",每次只叫醒其中几个干活,所以它虽然写着 35B,每次真正激活的参数少得多)、一个 27B 的稠密模型(每次全员上阵)、和那个开源项目自带的 4B。全都走标准接口,同一个玩法,改都不用改。

滚了四批,85 题:

批次 35B-A3B 27B dense 4B
第一批 35 题 35 34 33
第三批 25 题(陷阱专场) 24 24 21
第四批 25 题(推理专场) 21 22 20
累计 85 80 80 74

前三批 35B 一路领先。到了需要两跳推理的第四批,27B 反超一分。累计打平,都是 80。

打平的时候选谁?看延迟:35B 单次 65ms,27B 155ms,2.4 倍。没什么好犹豫的。

还有一个容易漏的指标:把选项顺序反过来再问一遍,看它改不改判。第一批里 35B 是 0 次翻转,4B 翻了 2 次。一个会因为选项换了个位置就改主意的判断器,准确率再高也不能用。

三个模型全错的两道题最值钱。一道是磁盘容量——150G 的东西两块盘都放不下,三个模型齐刷刷选了那块 80G 的。一道是日期算术——"上周三"对应哪一天。单次前向做不了算术,这类判据别交给它,交给几行代码。

最刺眼的是 35B 独错的那道:命令跑完没有任何输出,紧接着一句 echo "已确认"。它判成功,置信度 0.993。假证据照单全收,而且非常自信。

3. 并发曲线里藏着一个除法

有人拿这套玩法实时操控游戏,所以我想知道:多开几路,总吞吐会涨,还是大家平分?

跑了一条并发曲线,每条请求带唯一前缀防止命中缓存:

并发 决策/s p50
1 13.5 64ms
4 34 112ms
16 51 311ms
32 51 0.5s
64 51 1.1s

16 路封顶。之后吞吐一动不动,延迟线性往上爬——那就是纯排队了,不是并行。

于是有了这条检验式:

饱和之后,p50 ≈ 并发数 ÷ 饱和吞吐

拿 16 路对账:16 ÷ 51 = 314ms,实测 311ms。这不是什么经验规律,就是一个除法。

32 路算出来 627ms,实测 500ms;64 路算出来 1.25s,实测 1.1s。公式偏高 20% 左右,方向是固定的。原因也简单:p50 是中位数,排队的长尾把均值抬上去了,中位数没跟着走。所以这个除法应该当上界用,不当预测值用。

顺便还能算出并发到底值不值得开:

并发收益上限 = 饱和吞吐 ÷ 单路吞吐 = 51 ÷ 13.5 ≈ 3.8 倍

实测 1→16 路涨了 3.7 倍。对上了。

这条曲线本身还顺手否掉了一个说法。有人说这种单次前向"只吃算力不吃带宽"。跟解码比确实反过来了,但要是单路已经吃满算力,加并发不可能还有收益——实际涨了 3.7 倍,说明单路那 64ms 里大头是在搬激活参数和启动 kernel,算力空着。

所以设计上就两句话:吞吐型任务开 16 路,延迟敏感的走单路或 2–4 路。中间没有更聪明的选择。

4. 我以为读分会更准,实测一点没有

这是我下注最重、输得最干净的一条。

我的先验是:不让模型自由生成,它就没机会跑偏,正确率应该更高。这个直觉挺顺的——约束越紧,出错空间越小。

于是我把同一个模型的两种读法放一起比:一边读 logit,一边让它照常直接输出一个选项字母。结论只有一句话——读分模式对正确率没有帮助。同样的权重、同样的提示,两种读法选出来的答案一样。

想通了也不奇怪:softmax 之后取 argmax,和贪心采样吐出第一个 token,本来就是同一个运算。我等于把一个恒等式当成了优化。

那它到底买到了什么?两样,都很实在:

一是延迟。省掉的是你原本那段 JSON 的全部长度,不止一个 token。{"action": "B", "reason": "..."} 要几十个 token,就是几十趟权重读取。

二是输出一定落在候选集里。生成式的答案永远可能给你一个不存在的选项,读 logit 从物理上不可能——候选之外的字母压根不在你要读的那几个里。

这两样都值钱。但它们都不叫"更准"。

这里得插一句缺陷:我只比了正确率,没有把两种读法的延迟放在同一条件下对照跑过。所以"省掉几十趟权重读取"这句话是从机制推出来的,不是我测出来的。别拿它当数字用。

5. 然后我的题库考砸了

拿到 94.1% 那天我是挺得意的。

然后我换了三套公开数据集——两套意图分类、一套文本推断,各抽 300 样本先跑。所有模型在同一台三卡机上、同一台客户端发起、逐模型串行,延迟是端到端含局域网往返:

  • 云端决策 API:84.0%,p50 323ms(不含 11 次失败重试)
  • 27B:77.0%,p50 528ms
  • 35B:76.7%,p50 104ms
  • 4B:68.0%,p50 100ms
  • 2B 和 0.8B:都在 30% 附近,p50 60ms 上下

94.1 掉到 76.7。同一个模型,同一套提示词。

自己出的卷子自己考,考 94 分那不是废话吗。但我当时真没往这儿想,因为我造题的时候是照着真实场景写的,主观上觉得很公允。回头复盘,偏差至少有三处,而且每一处我当时都没察觉:

第一,我知道答案才写的题干。写"这个操作算不算成功"的时候,我脑子里已经有成功的判据了,于是那个判据会不自觉地漏进题干的措辞里。

第二,我下意识避开了自己也拿不准的边界。一道题要是我得想三分钟,我多半换一道——而那恰恰是最该测的题。

第三,题目分布是我的直觉分布,不是真实分布。公开数据集里意图类别有几十上百个,还带拒识样本;我造的题一道最多三四个选项。

第三点还有个硬证据:云端那个 API 的选项上限是 255 个,我送 300 个直接被拒。而我自己那 85 道题,没有一道超过 4 个选项。我压根没测到它会在哪儿塌。

顺着 255 这个数往下挖,还挖出一条能直接拿走的判据。这个上限不是拍脑袋定的,它来自标签池:

候选数上限 = 单 token 标签的个数

每个候选得配一个字母标签,而这个标签必须是一个 token。本地服务实测接受 255、拒绝 256;云端接受 255、拒绝 300——两套完全独立的实现撞在同一堵墙上。更有意思的是,我按顺序编标签,走到第 69 个就遇上一个多 token 的标签,得跳过去接着往下筛。跳号之后仍然能凑满 255 个,因为需要连续的是候选索引,标签拼写可以跳号

文本推断那套题上,本地和云端的差距明显收窄:云端 74.7%,35B 74.0%,9B 两种精度 71% 左右,27B 70.3%,4B 63.0%。同一批模型换个任务,名次和差距全变。这也说明任何"本地比云端差 X 个百分点"的单一数字都不该信。

探针也骗了我一次

真跑这套评测之前,我还被自己的读数代码坑过一轮。

第一版我拿 token.strip() 建字典,想着把前后空格清掉好匹配。结果返回里带前导空格的 ' B'(logprob −9.25)把真正的 'B'(logprob −0.0)覆盖了。报出来一个 6/20 的荒谬数字,我差点去怀疑模型。

救我的是数量级断言:贪心采样吐出来的 token 明明是对的,跟我自己算出来的 argmax 对不上。两个东西本该恒等,对不上就只能是我的代码错。

读 logprob 匹配字母必须精确匹配 token,任何 normalize 都会让变体覆盖真值。

同一类坑还有一个变种,长得不一样但病根一致:读数层会把一个标签的概率复制给前缀相同的更长标签;要是一个合法标签都没读到,它还会退化成均匀分布,然后默默选第一个候选。这种情况必须炸出来报错——把"没有证据"伪装成"一次正常选择",比直接选错更难查。

6. 拿去拆穿别人的数字

看到任何一份"本地小模型做决策"的 benchmark,按这四句问,基本能把水分挤干净:

第一,题是谁出的。 自造题库和公开题库,同一个模型能差 17.4 个百分点。作者自己出题又自己评分的数字,默认按对折看。

第二,延迟含不含网络往返,并发几路。 我报的 104ms 是端到端含局域网往返的;别人报 20ms 很可能只是 GPU 上那次 forward。更隐蔽的是并发——同一台机器同一个模型,单路 64ms、16 路 311ms,差 4.9 倍。只报一个延迟数字而不说并发几路,等于让人随便挑。

第三,候选有几个。 255 以内一次前向就完事;超了就得先选桶再选桶内元素,多一层调用。我这边那个分层模块优化完还要约 200ms,仍然是平铺路径的两倍以上。拿 4 个候选测出来的延迟,去承诺 400 个候选的场景,数字会翻。

第四,读分和直接输出比过没有。 大概率一样。谁要是说"改成读 logit 之后准确率提升了",让他把同模型直接输出的那一臂也跑出来。

7. 这些数字的边界

前面每个数我都敢复现,但得说清它们管不到哪儿:

  • 300 题是分层抽样的首轮,用来验夹具的,不是完整公开测试集。
  • 27B 跑的是常驻配置、带投机解码,没按这个任务调过,它那个 528ms 不能当它的上限。
  • 云端那个 84.0% 不含 11 次失败重试。把失败计进去它会掉,掉多少我没算。
  • 4B 的 44ms 地板是被拖慢过的——那台机器上某个线性注意力层缺了加速库,跑的是参考实现,日志里明写着 fallback。装上之后它应该更快,这条我没测,不当结论。
  • 本地走局域网,云端走公网。这个对比量的是部署体验,不是纯推理速度。
  • 最重要的一条:模型能力、提示适配、推理配置这三样我没有分开。所以"本地比云端低 7 到 16 个百分点"回答的是"差多少",回答不了"为什么差"。

8. 照着做的顺序

下次评估任何"让小模型快速做判断"的方案,按这个顺序走:

  1. 先找公开数据集,别先出题。 自己造的题只配当冒烟测试,不配当成绩单。要造也行,造完必须再跑一遍公开集,两个数字并排放。
  2. 把选项顺序反过来再问一遍。 翻转率比准确率更能暴露一个判断器稳不稳。
  3. 单独拎出算术题和注入类的题看。 单次前向做不了算术;注入类的题两个大模型在我这儿都不稳,那种东西交给规则层过滤。
  4. 跑一条并发曲线,找到吞吐封顶的那一点。 之后延迟与并发成正比,p50 ≈ 并发 ÷ 饱和吞吐 当上界估,别指望再加机器解决延迟。
  5. 读分和直接输出各跑一臂。 正确率大概率打平,你买到的是延迟和"输出必在候选集内",把预期摆正。
  6. 给"没读到合法标签"单独留一条报错路径。 退化成均匀分布再默默选第一个,这种失败在日志里长得跟正常决策一模一样。

第 3 条是我交学费换来的:那道题里命令什么都没输出,下一行一句 echo "已确认",模型给了 0.993。