作者用 56 道隐藏测试的编码任务,在同一块 16GB 显卡上对 16 个本地模型进行严谨对标;小模型 Gemma-4-e4b(7.5B)以 42/56 胜过 24B 模型。结果与公开排行榜矛盾。
16 种本地模型配置、56 个隐藏测试编码任务、36 次完整运行、一张 16 GB 显卡
下文中的每一个数字都重新从已提交的 SCORES-*.tsv 文件中统计得出,并非从笔记中转录。原始数据位于 RESULTS-q56.csv:共 36 行,每次运行一行,每行都包含完整的失败列表。
gemma-4-e4b 有 75 亿个参数,文件大小为 4.97 GiB。它取得了 42/56 的成绩。
devstral-small-2-24b 有 240 亿个参数,文件大小为 11.90 GiB。它取得了 40/56。gpt-oss-20b 得到 38 分。gemma-4-12b 的两个变体——其中一个采用 Q8_0,精度超过两倍——分别得到 42.5 分和 41 分。
每个配置都运行了三次。它们的分数区间互不重叠。在小显卡上,5 GiB 的模型并不是需要妥协的选项,而任何公开排行榜都无法告诉你这一点。
关于该用哪个本地模型写代码、运行哪种量化,以及不同 KV cache 设置会带来什么代价,每个人都有自己的看法。几乎所有这些看法都来自公开排行榜,或者来自某人在自己机器上对某个配置的一次运行。
因此,我构建了一个包含 56 个任务、通过隐藏测试评分的编码基准,并在同一张显卡、相同常量设置下测试了 16 种模型配置,且实际记录了每一个保持不变的条件。随后,我又反复运行了其中几个配置——总计完成 36 次完整运行——以确定单个分数中究竟有多少是噪声。
这个排名与任何公开排行榜都不一致;其中一个案例甚至完全颠倒了公开榜单的结果。比排名更让我惊讶的是噪声方面的结果:误差范围是模型本身的属性,而不是基准的属性。某个模型在连续两次完全相同的运行之间会波动 8 分,另一个模型却能逐比特复现结果——而出现波动的并不是较弱的那个模型。
56 个中高难度编码任务通过真实的智能体循环运行——模型读取文件、编辑文件、运行测试并不断迭代,直到它表示任务已经完成。评分通过隐藏验证完成:
模型看到的夹具只附带覆盖正常路径的可见测试。
评分时,验证器会注入隐藏的代码审查测试,并针对模型实际留在磁盘上的内容构建和运行这些测试。
通过或失败由编译器和测试运行器决定。没有任何模型负责评判另一个模型的输出。
最后一点比听上去更加重要。这个项目从其前辈那里继承了一条经验:LLM 评审会虚增分数。模型可以调低确定性真实标准给出的分数,但绝不能将其调高。因此,这里完全没有评审模型——只有 cargo test、pytest 和 node。
任务采用测试优先的准入机制。只有当某个任务的验证器在未经修改的夹具上失败、在夹具加参考解法上通过时,该任务才会进入语料库。这排除了那些意外地已经能够通过的任务,也排除了不可能完成的任务。
事实证明,真正具有区分度的维度并不是算法难度,而是未明确说明的正确性陷阱——退化输入、假值与缺失值之间的区别,以及提示词暗示却从未逐项列出的边界条件。提示词像用户一样陈述目标;隐藏测试则像代码审查者一样评分。
语料库:56 个任务——15 个 Python、14 个 Rust、10 个 JavaScript、9 个 TypeScript、8 个 Shell。按类型划分:18 个规范实现、7 个边界处理、6 个缺陷修复、6 个 API 误用、5 个错误处理、4 个重构、4 个性能优化、4 个多文件任务、2 个并发任务。
机器配置:RTX 5060 Ti 16 GB、Windows 11、LM Studio。每行都保持以下条件不变:ctx 32768、KV cache q8_0、numParallelSessions 1、每次只运行一个智能体会话、使用相同的智能体二进制文件。每一行的实际值都经过记录,而不是想当然地认定它们一致——参见“代价最高的方法论教训”,这是我最希望其他基准测试者阅读的章节。
每一行都运行完整的 56 个任务。低分也是数据,不是失败——一个只报告优胜者的基准无法告诉你整个参赛领域的分布形态。
大小指实测 GGUF 文件的 GiB 数,不是厂商的营销数字,也不是 lms ls 的结果(后者以十进制 GB 报告,并把 mmproj 投影器计算在内)。量化名称精确记录,因为采用 Q8_0 的“gemma-4-12b”和采用 Q4_0 的“gemma-4-12b”是两个不同的实验。
对于标有 ⚠️1 的行:本基准现在已经能够用数字表示单次运行所携带的误差范围,而且这个范围并不小——参见噪声一节。本文没有任何关键结论建立在 n=1 的行上。确实涉及单次运行的地方(12B 的 QAT),我会明确说明,并拒绝据此作出结论。
有一个模型被排除,而不是被评分:nemotron-3-nano-omni-30b-a3b 在语料库中最简单的任务上耗时 299 秒(devstral 为 18 秒),预计完成全部任务大约需要 4.5 小时。在这个速度下,每个任务的超时限制会让任务因时间耗尽而失败,而不是因质量问题失败,从而产生一行无法解读的数据。它的配置已确认无误,模板也运行正常——排除它的唯一原因就是速度。记录模型缺席的原因,胜过留下一个空洞。
本地模型讨论中最常见的说法之一是:“直接运行能装下的最大模型。”下面是 gemma-4 家族的表现:同一种架构、同一家厂商、四种大小。
4.6B → 7.5B +11.0
7.5B → 12B +0.5 ← the plateau
12B → 26B +12.5
从 7.5B 扩展到 12B 只带来了 0.5 分。这就是平坦区间,而且即使 12B 使用了更好的量化,它依旧如此——12B 使用 Q8_0,而 7.5B 使用 Q4_K_M,这已经是偏保守的比较方向。如果让小模型使用相同的精度,差距很可能会朝相反方向发展。
但这个平台的两侧都存在真正的断崖。向下缩减到 4.6B 会损失 11 分;向上扩展到 26B MoE 则会增加 12.5 分。“模型大小没有带来任何收益”在某个区间内确实成立,但这个区间比口号所暗示的更窄。
这正是标题的由来。gemma-4-e4b 的参数量不到 devstral-small-2-24b 的三分之一,文件大小仅为后者的 42%,成绩却超过了它——而且它超越的是整个模型家族边界,并非只是碰巧胜过某一个状态不佳的模型:它也击败了 gpt-oss-20b(38 分)、north-mini-code(42 分,n=1),以及两个 12B gemma,其中一个的精度是它的两倍。对于重复测试的行,每个配置都运行了三次,分数区间互不重叠。
活跃参数量也无法解释这一结果。gemma-4-26b-a4b 每个 token 大约激活 40 亿个参数——大致相当于 4.6B e2b 的活跃参数量——但两者的成绩分别是 55 分和 31 分,相差 24 分。无论真正发挥作用的是什么,单看总参数量或活跃参数量都无法解释。
结果 1 中的任何现象都无法从排行榜上看出来,而这并非偶然——即使在榜首位置,它们给出的排名也与我的结果不同。
某厂商的对比表显示,在 SWE-bench Verified 上,Gemma-4-31B 比 Qwen3.6-35B 低大约 21 分。而在这套语料库中,gemma MoE 以 55 比 50 击败了 qwen 冠军。
LiveCodeBench v6 甚至呈现直接的负相关:gemma 在那里得到 77.1 分,在这里得到 55/56;qwen 在那里得到 80.4 分,在这里得到 50/56。榜单上更高,在我的任务中却更低——对于我最关心的两个模型,两个方向都出现了这种情况。而且 LiveCodeBench 自己针对这些模型的表格中,数据是 0 个已验证、53 个自行报告。
实际情况比“不同基准衡量不同能力”还要糟,因为几个显而易见的替代方案已经完全无法使用:BigCodeBench 自 2025-04-14 起就被冻结了(202 行数据,没有任何 2026 年模型),Aider 的多语言基准则在 2025-10-04 停止更新。与此同时,已有研究表明,仅仅更换脚手架,就能让相同权重的模型在 Terminal-Bench 上产生约 2 倍的成绩差异——官方榜单为 24.6%,厂商宣称的成绩则为 51.5%。
最后这个数字揭示了真正的机制,也解释了为什么我并不认为自己的排名比 LiveCodeBench“更正确”。排行榜衡量的是模型在一个盒子里回答问题的表现。而你实际体验到的,是模型在驱动你的智能体,使用你的工具模式、你的上下文窗口、你的文件编辑格式。脚手架并不是包裹在模型外面的一个细节;按照 Terminal-Bench 自己的数据,它的影响与模型选择本身同样大。
可以使用排行榜来决定下载什么模型。永远不要用它来预测模型在你自己的运行框架中会有怎样的表现。上面的排名花了 34 次完整运行才得以发现,不可能从任何公开表格中推测出来。
同一个 MoE 家族的四种量化配置,使用相同的 KV cache 和上下文:
在整个 3.06 → 4.25 bpw 区间内,成绩都没有变化;与此同时,文件大小相差接近 4 GiB,端到端速度相差约 3 倍。在这套硬件上,四者中 bpw 最低的配置性价比最高,而且优势非常明显——它是唯一能够完全驻留在显存中的配置。
需要明确说明的限制是:这只是一个 MoE 家族在一张 16 GB 显卡上的结果,而且四行数据中有三行只运行了一次。它不意味着“量化永远没有影响”,我也不会假定这一结果能够迁移到稠密模型上,因为稠密模型对量化明显更加敏感。这里的结论更窄,但证据也更充分:对于这个模型家族,在这种大小的显卡上,在大家实际争论的量化区间内,额外的位数没有带来可测量的代码质量提升,却付出了真实的速度代价。
量化感知训练看起来不需要付出代价,而我有两组配对数据似乎能够证明这一点:
这些不是相同的实验,把它们合并为一条"QAT是免费的"论断是错误的,两个方向都不对。在26B时,它比较的是Q4_K_M对Q4_0——都是4比特级别,几乎是一对一的交换,所以"QAT匹配它"是一个温和的说法。在12B时,它比较的是Q8_0对Q4_0,文件大小小45%且精度只有一半,这将是一个真正有力的结果。
但12B QAT的分支只有一次运行,而非QAT分支的两次运行跨度是42–43。差一个点,在两个模型测量的分布范围内。所以我不主张这一点。能够解决这个问题的实验是再做两次gemma-4-12b-qat的运行,在这些存在之前,诚实的表述是:26B的QAT成本可忽略;12B的QAT未经测试。
我之所以详细标记这一点,是因为这是一种声明的形式,在一次运行后会被永远重复,我几乎发表了它。
这是我没想到会有趣的一半。
结果4:误差条是模型的属性,不是基准测试的属性
长期以来我一直使用单个全局噪声数字——"±3"——并将其应用于每个比较。这总是错的,直到复制了几个模型我才看到。
连续运行,配置相同,引擎相同,同一台机器,它们之间没有任何改变:
gpt-oss-20b在三次相同运行中波动8个点。它发表的单次运行数字——如果我只运行一次(像通常那样),40会进入表格——是其范围的顶部,不是中心。单次运行报告让它获利2个点,那次运行中没有任何东西表明这一点。
同时gemma-4-e2b运行三次,不仅产生了相同的分数,而且产生了相同的失败列表——相同的25个任务,相同的错误文本。运行2和运行3之间的唯一差异是测试框架的成品:cargo的并行测试排序,OS线程id,±1秒的时间。
弱点不会导致方差。失败模式才会。
解释gpt-oss波动的明显解释是"它在许多任务上处于边界,所以它掷硬币决定它们"。这个解释已经死了,e2b杀死了它:e2b比gpt-oss失败更多的任务——25对16–24——范围为0。表现差是稳定的。表现不稳定是其他原因。
分隔它们的是它们失败的方式。gpt-oss因为agent机制丢失任务:裸露的顶级return产生ERR_INVALID_TYPESCRIPT_SYNTAX,SyntaxError: Unexpected end of input,写入.mjs的文字\n转义序列而不是真实换行符。其最坏的运行因语法丢失五个任务,不是推理。生成是否以可解析文件的形式出现几乎是一个掷硬币。相比之下,错过边界情况是模型知识的稳定属性。
⇒ 一个模型的误差条由其失败模式预测。这是第一个复制夜之后的一个假设,然后它作为e2b上的预测成立,在质量范围的另一端。
实际后果:你不能导入别人的误差条,也不能导入我的。如果你正在比较两个本地模型,间隔在3个点以下,在你知道每个模型自己的分布之前你没有结果。那是每个分支2–3次额外的运行,对于某些模型,它是一个发现和掷硬币之间的区别。
结果5:"确定性"结果意味着"单次会话内的确定性"
这个是新的,它是一个基准设计的发现,我还没有看到任何本地模型排行榜报告它。
qwen3.5-4b在温度0下运行三次:31、33、33。普通看起来的噪声。然后我查看了哪些任务失败了:
r2和r3有字节相同的失败列表——23个任务,同样的。完全可重复。
r1与两者在十个任务上不同。
r1在不同的一天运行。r2和r3在一个会话内连续运行。
确认:第四次运行在r1的那一天开始并在中途被杀死。在它评分的33个任务中,它与r1完全匹配。所以第一天有两个一致的运行,第二天有两个一致的运行,两个会话彼此不一致。
⇒ ★ 这场活动中的每一项确定性声明都限定在单个会话内。e2b的"31/56三次,范围0,相同失败列表"和e4b的字节相同复制都是在一个会话内连续收集副本时收集的。那个设计无法区分"确定性"和"单个会话内的确定性"。这是我曾经运行的第一个跨日复制,它移动了。
我还不知道机制。候选项:模型服务器在会话间被重启,其状态中有一些不明显的差异;驱动程序或引擎加载顺序效应;机器负载(见下一个结果,它解释了3个翻转中的10个,但不是其他7个)。我能说的是运行你的副本背靠背会低估你的误差条,而那正是每个人运行副本的方式,包括我。
如果你从这篇文章中获取一个方法论的东西,取这个:在会话之间间隔你的副本,或说明你没有。
结果6:聚合是稳定的;失败列表不是
冠军配置,六次在匹配常数下的运行:49、50、50、50、48、52。中位数50,范围4。
现在看看哪些任务失败了:
十三个不同的任务通过失败槽位轮转。恰好一个——Q03——在所有六次中都失败了。频率:Q03 6/6,Q51 5/6,Q25 5/6,Q05 5/6,Q52 4/6,Q13 4/6,Q46 2/6,以及六个恰好失败一次的任务。这是一个在稳定能力水平上有一池边界任务随机解决的模型。它不是一个固定的不能做的事物列表。
所以几乎每一个你读到的"这个本地模型不能处理X"的声明都源自单次运行,根据这个证据,那个声明大约在13个中11个是没有根据的。
我知道,因为我犯了完全相同的错误。三次运行后,我自信地写下了一个"一致失败集"的五个任务。下一次运行通过了其中两个,并产生了两个我从未看到的首次失败。我的五任务集是一个三样本的产物。
它花费了我两个浪费的评估。我在那五个任务上构建了一个廉价的筛选门;它绿灯了两个候选配置,两个都在完整电池上与冠军完全相并。门测量的是轮转,不是质量。
更糟的是,当我重新构建屏幕——12个任务,合计评分,拒绝阈值从界限而不是选择得出——它仍然失败。界限假设挑战者会通过屏幕外的所有内容。三个不同的模型破坏了它,一个八个任务。子集屏幕现在只用于快速拒绝,永远不会推广、界限或加冕。
如果你基准本地模型:在多次运行中聚合评分,永远不要从失败任务的身份得出任何结论。
结果7:KV缓存量化和上下文大小移动墙钟2.8倍,分数根本没有
一个流行的声明是q8 KV缓存对fp16的质量有成本。我测试了两次:
任一上下文大小都+0质量,fp16在两者中都更慢——在匹配上下文处1.13倍,一旦上下文达到65536且VRAM达到96.7%后2.79倍。
仔细注意那个2.8倍实际上是什么。它不是KV精度——它是上下文大小将VRAM推到边缘。我只知道因为第二个fp16运行改变了一个变量;第一个已经移动了两个并将支持错误的结论,自信地。
尴尬的部分:我已经在那个慢角落每天开车两天,基于一个有希望的子集屏幕结果,这结果是结果6的轮转。
另一方面有匹配的悬崖。在GPU和CPU之间分割MoE专家有一个在16.3 GB卡上约15.1–15.6 GB的可用带,两个错误都很昂贵。太多专家在CPU链上卡住了卡:一个模型在坏的分割上运行113秒/任务,在好的上75秒/任务——单个行保存了35分钟,没有分数变化。太少会抖动到Windows共享内存:在97.7% VRAM一个任务用了306秒,重新分割后,37秒。8.3倍。
结果8:生成速度探针不预测端到端速度
我有一个标准探针:三个提示,流式传输,温度0,中位数tok/s,对抗15 tok/s的底线。
它错了两次,方向相反。一个配置探针比冠军快(73对70 tok/s),然后在相同判决任务上运行快2.48倍。另一个在调优改变后探针快1.74倍,交付了2.2倍的端到端。
原因是结构性的:agent电池由提示处理在不断增长的KV缓存上主导,不是在短提示上的令牌生成。探针测量的是不是瓶颈的东西。
使用生成速度探针作为拒绝无望候选的底线。永远不要引用一个作为配置快的证据。
结果9:我的墙钟评分的任务测量的是机器,不是模型
56个任务中的四个按墙钟评分性能——朴素对线性时间,大小对宽分离。比较qwen3.5-4b的跨日复制暴露了那真正测量的是什么:
相同的权重、相同的配置,在同一任务上的耗时却相差 3.75 倍。第 1 天的整轮测试慢了 27%,因为当时我正在使用这台机器。这些判定衡量的是我的机器状态。
这是我的测试语料中一个真实存在的缺陷,我宁愿如实报告,也不愿悄悄放宽阈值。任何依据实际耗时评估智能体性能的人都会继承这个问题。可选的修复方案包括:仅在确认处于空闲状态的机器上运行性能任务;每次运行时记录负载探针,并拒绝受到干扰的运行结果;或者不再依据时钟,而是从结构上评估复杂度。我还没有决定采用哪一种。
⚠️ 这无法解释结果 5 中不同会话之间的差异:10 个判定发生翻转的任务中,有 7 个并非性能任务。机器负载只是一个污染因素,而不是背后的机制。
有几个模型丢分并不是因为推理能力差,而是因为无法生成有效文件:
gpt-oss-20b 在首次运行的 16 次失败中,有 3 次属于这种情况;在表现最差的一次运行中则有 5 次——两个 TypeScript 任务因裸露的顶层 return 而失败,另一个任务把字面量 \n 转义序列写进了 .mjs 文件。
north-mini-code-1.0 也以同样的方式丢了 2 个任务——一个是未终止的三引号字符串,另一个是 Rust E0425。
granite-4.1-8b 同样如此。
devstral 试图使用一个没有被预置到离线沙箱中的 crate。
这些属于编辑机制和环境感知方面的失败,而不是智能水平的失败,仅看通过率完全发现不了它们。对于任何构建智能体的人来说,这也是这里最具可操作性的发现:一个推理能力很强、却无法可靠写入文件的模型,其实际表现会比基准测试分数所显示的更差——而且根据结果 4,它恰恰也是评分最不可信的模型。
这是我所掌握的最有力论据,说明智能体基准测试应当把操作机制与推理能力分开评分。我目前的基准测试还没有做到这一点,这是一个缺口。
该基准测试的规范一直写着:“每一行都保持不变:ctx 32768、KV q8_0、parallel 1”。然而,从来没有任何机制记录或验证这些参数。
LM Studio 中的 KV 缓存设置具有粘性,是全局的、按模型保存的,模型卸载后仍会保留;它不是加载参数,而且 lms ps --json 也不会报告它。我的机器上,这个设置悄无声息地变成了 fp16,并维持了两天。切换前后的测试行被当作可比数据进行了比较。
一个没人测量的“恒定变量”并没有真正保持恒定。现在,每次运行都会把实际的 ctx、KV 类型、并行度、VRAM、GPU/CPU expert 分配、模型路径、智能体版本以及二进制文件的修改时间写入元数据文件;历史数据行则会被明确标记为推断值,而不是实测值,这样重建出来的数据就永远无法被引用为证据。
强制检查发现的问题比我预想的还多。一个加载前配置写入器发现了:
我最喜欢的是 numParallelSessions: 4,因为它会在四个槽位之间拆分 KV 分配,而 lms ps 仍然骄傲地报告 CONTEXT 32768。我此前一直引用为先验依据的两个旧数据,后来发现从未在该基准测试自身规定的恒定配置下测量过。
三个运维陷阱都险些让我在几分钟内发布虚假数据:
lms load 从不创建每个模型对应的配置文件。一个从未配置过的模型没有这种文件,因此预配置器没有任何内容可以纠正,并行度就会悄无声息地默认为 4。只有先在 GUI 中打开一次该模型,才会创建配置文件。
当没有完全匹配的 key 时,lms load 会进行模糊匹配。几个月来,我在所有笔记里写的都是这个简短 id——qwen3.6-35b-a3b-mtp——但它悄无声息地加载了另一个模型,因为此前的一次修复为同系列的另一个量化版本创建了独立的仓库目录,导致生成的 key 扩展了冠军模型的前缀。根本不存在这个简短 key;真正的 key 以 @iq3_s 结尾。我在一次运行开始约 40 秒后发现了这个问题,否则整次运行看起来会完全正常。
GUI 会保留一份过期配置,并可能将其写回。修正磁盘上的配置后,CLI 和 JSON 都读取到了新值,而 GUI 仍然显示旧值——此时如果从 GUI 保存,它就会用过期值覆盖刚刚完成的修复。
总体教训,也是这一节存在的全部理由:
基准测试的完整性故障看起来并不像错误。它们看起来就像成功完成的运行。
在早期数据行和后期数据行之间,LM Studio 于 01:21 自动将其推理引擎从 2.25.2 更新到了 2.27.1,而自动更新默认处于开启状态。因此,我有义务做一组对照实验:在新引擎上,以完全相同的配置重新运行冠军模型。
第一次对照运行得到 48/56,而基线运行从未低于 49;其中有两个此前从未失败过的任务,同时实际耗时慢了 1.33 倍。两个信号都指向同一个方向。写下“引擎更新导致分数下降了 2 分”会非常容易。
第二次对照运行得到 52/56——这是该冠军模型有史以来的最高分——耗时 21 分 23 秒,速度处于正常范围。分数下降和速度变慢都没有复现。在全部六次匹配运行中,中位数仍然恰好是 50。目前无法证实引擎产生了影响,因此也不会重新设定任何基线。
我之所以报告整个过程,而不只是给出结论,是因为只看一次运行时,这原本会成为一个看似足以发表、实际却完全错误的发现;它与结果 6 从不同方向揭示了同一个教训。你的推理引擎仍然是一个变量:固定它、记录它,并预期它会在你睡觉时自行更新——我的引擎就是如此;而且它的自动删除设置还可能通过垃圾回收清除旧引擎,让你失去复现任何结果所需的版本。
一台机器,一张显卡。RTX 5060 Ti 16 GB。模型完全驻留在 VRAM 中还是发生了溢出,是这里影响速度的主要变量,而你的情况会有所不同。
56 个任务的规模很小,16 种配置中有 7 种只运行了一次。对于这些数据行,我现在已经能够估算误差范围,但当时并没有实际测量。
重复运行大多发生在同一会话内——参见结果 5。我唯一一次跨会话重复运行的分数变化了 2 分,并有 10 个任务的判定发生了变化。
这是一个用于短周期评估的工具。每个任务的迭代深度:p50 = 4,p90 = 8,最大值为 15;56 个任务中只有 5 个达到了 9 次或更多迭代。它评估的是首次尝试的代码质量。它无法观察上下文压缩、前缀缓存行为、模型升级或权限处理——测试框架使用自动批准,因此这些路径从未被触发。任何声称通过该基准测试“证明”上下文压缩修复有效的人,测量到的净变化都是零,却得出了错误结论。
分数区间的顶部正在趋于饱和。冠军成绩为 55/