同一个模型,同一套提示词,同一张卡。
我自己出的 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,然后只看返回里 A、B、C 这几个字母各自的 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. 照着做的顺序
下次评估任何"让小模型快速做判断"的方案,按这个顺序走:
- 先找公开数据集,别先出题。 自己造的题只配当冒烟测试,不配当成绩单。要造也行,造完必须再跑一遍公开集,两个数字并排放。
- 把选项顺序反过来再问一遍。 翻转率比准确率更能暴露一个判断器稳不稳。
- 单独拎出算术题和注入类的题看。 单次前向做不了算术;注入类的题两个大模型在我这儿都不稳,那种东西交给规则层过滤。
- 跑一条并发曲线,找到吞吐封顶的那一点。 之后延迟与并发成正比,
p50 ≈ 并发 ÷ 饱和吞吐当上界估,别指望再加机器解决延迟。 - 读分和直接输出各跑一臂。 正确率大概率打平,你买到的是延迟和"输出必在候选集内",把预期摆正。
- 给"没读到合法标签"单独留一条报错路径。 退化成均匀分布再默默选第一个,这种失败在日志里长得跟正常决策一模一样。
第 3 条是我交学费换来的:那道题里命令什么都没输出,下一行一句 echo "已确认",模型给了 0.993。