这是我最初 16GB GPU 实验的后续,这次采用一致的量化、重复的基准测试,以及一些意想不到的结果。

我最近发表了一篇关于在我的笔记本电脑上试验混合专家(MoE)卸载的文章。

我测试了五个不同的模型,比较了传统的层卸载与选择性专家卸载,并得出了一些相当有趣的结果。

有些模型在选择性卸载下表现更好。另一些在使用传统层拆分时则明显更快。

而有一个模型 LFM2.5 在选择性卸载下表现极差,我真的很想弄清楚到底发生了什么。

嗯,这篇文章获得了一些很好的反馈。

人们指出了我没有恰当考虑的地方,质疑了我的一些假设,并建议了更好的方法来测试我所看到的现象。

老实说,这正是我喜欢得到的那种反馈。

所以自然而然地,我又一头扎回了这个兔子洞。

而这一次,一些结果非常不同。

首先,需要修复什么?

如果你还没读过原文,可以在这里找到:

MoE 卸载并不总是更快:我在 16GB GPU 上测试了 5 个模型

在那次实验中,我测试的是:当运行那些大到无法完全放入我 GPU 显存的模型时,选择性 MoE 卸载能否提升性能。

其思路是让某些操作继续在 GPU 上运行,同时将选定的专家权重及其计算卸载到 CPU。

我使用了llama.cpp配合--n-cpu-moe选项,并将其与使用-ngl的传统层拆分进行了比较。

但我最初的测试存在几个问题。

这些模型并非都使用相同的量化。

我让 Phi-3.5 运行在 Q4_K_M,Apex 运行在 Q5_K_M,Qwen 运行在 Q6_K,而 Gemma 和 LFM 运行在 Q8_0。

这使得很难判断性能差异究竟与模型架构、量化还是其他因素有关。

有几个人在评论中提到了这一点,尤其是关于 LFM 糟糕的表现。

我也没有做足够多的重复测量。

另一个人指出,笔记本散热、后台活动和普通的测量噪声都可能影响结果。他建议将不同配置交错运行,而不是先完整跑完一种策略再切换到另一种。

这一点说得很有道理。

我最初的基准测试有助于发现有趣的行为,但其中一些较小的差异未必有意义。

所以我决定再做一轮测试。

新的测试设置

我用的还是同一台笔记本:

  • GPU:NVIDIA RTX 4090 Laptop GPU,16GB 显存

  • CPU:Intel Core i9-14900HX,24 核 / 32 线程

  • 内存:32GB DDR5

  • 操作系统:CachyOS / Arch Linux

  • 推理引擎:llama.cpp,启用 CUDA 和 Flash Attention

但我对测试流程做了几处改动。

现在五个模型都以Q4_K_M运行。

我还更新了 llama.cpp,调整了卸载配置以适配更小的量化模型,并保留了足够的空闲显存,避免把 GPU 推到显存上限。

对每个模型,我都使用相同的提示词:

“使用 Arch Linux 的 3 个实际好处是什么?

每个配置在测量前都会先收到一个预热请求。

然后我以交错顺序对每种策略重复测量三次,每次运行之间短暂冷却。

我不再依赖单次结果,而是使用生成速度的中位数。

这并不能让实验做到完全受控。特别是,改变了量化方式、卸载设置和 llama.cpp 构建版本,意味着我无法把每一处差异都单独归因于量化。

但它确实让我更清楚地了解了这些模型在一致的 Q4_K_M 设置下的表现。

新的结果

以下是初始 100 token 的 Q4_K_M 测试结果:

模型 传统拆分 选择性 MoE 结果
Phi-3.5-MoE 6.17 tok/s 5.15 tok/s 传统 +19.8%
Apex-35B 8.20 tok/s 9.70 tok/s 选择性 +18.3%
Qwen3.5-35B 7.44 tok/s 9.00 tok/s 选择性 +21.0%
LFM2.5-24B 32.96 tok/s 35.44 tok/s 选择性 +7.5%
Gemma 4 26B 13.58 tok/s 13.58 tok/s 持平

嗯,这很有意思。

在我最初的测试中,传统拆分在 Qwen、LFM 和 Gemma 上更快。

现在选择性卸载在 Qwen 和 LFM 上领先,而 Gemma 基本持平。

Phi-3.5 的结果也朝相反方向变化了。

但我还没准备好宣布那些结果就是最终结论。

其中一些单独运行的结果波动相当明显,包括一个异常快的 Apex 结果和一个异常慢的 LFM 结果。

于是我把测试扩展到几个模型上的 512 token 生成。

而事情在这里变得更加有趣了。

Qwen:量化确实似乎有影响

这大概是我最想深入调查的结果。

在我最初的测试中,Q6_K 下的 Qwen3.5-35B 使用传统层拆分表现更好。

但 Apex-35B 采用了相关的 256 专家 MoE 架构,在 Q5_K_M 下使用选择性卸载表现更好。

我怀疑量化差异是否促成了这种行为。

新的 Q4_K_M 结果确实表明可能是这样。

于是我又往前走了一步。

我测试了同一个 Qwen3.5-35B 检查点在 Q4_K_M 和 Q6_K 下的表现,使用相同的 llama.cpp 构建和更长的 512 token 生成。

以下是我的发现:

量化 传统拆分 选择性 MoE
Q6_K 6.79 tok/s 6.75 tok/s
Q4_K_M 6.47 tok/s 8.38 tok/s

在 Q6_K 下,两种策略几乎打平。

在 Q4_K_M 下,选择性 MoE 卸载快了29.5%。

这是相当大的差异。

而且与最初 Apex 对 Qwen 的对比不同,这次我比较的是同一个模型的不同量化版本,且使用相同的推理引擎构建。

我仍然不能说量化是唯一的因素,因为层放置也需要调整以适应不同的模型大小。

但这是强得多的证据,表明量化和内存放置的组合对选择性卸载性能有重大影响。

这也改变了我对细粒度 MoE 架构的看法。

最初,我怀疑拥有 256 个路由专家的模型对 CPU 卸载可能特别成问题。

这些结果表明事情远没有那么简单。

一个 256 专家的模型在选择性卸载下可以表现得相当好,具体取决于配置。

LFM:最大的意外需要再看一眼

如果你读过我的第一篇文章,你可能还记得 LFM2.5。

就是那个在 Q8_0 下使用选择性卸载时只跑出相当惨淡的2.11 tokens/sec的模型。

传统拆分达到了 9.03 tok/s。

快了四倍以上。

差异如此之大,以至于好几个人都对此发表了评论。

Mudassir 提出,更重的 8 位专家权重可能给 CPU 内存带宽带来了过大压力。

我怀疑有类似的原因,但没有证据将其与架构本身区分开来。

嗯,切换到 Q4_K_M 后带来了巨大的提升。

最初的 100 token 运行达到了35.44 tok/s并启用了选择性 MoE 卸载。

这大约是原始结果的 16.8 倍。

传统拆分也有相当大的改善,达到了 32.96 tok/s。

但有个问题。

其中一次选择性运行一路降到了 11.23 tok/s,而另外两次都在 35 tok/s 以上。

这种波动幅度相当离谱。

于是我用更长的 512 token 生成重复了实验。

这一次,结果看起来相当不同:

策略 生成中位数
传统拆分 21.72 tok/s
选择性 MoE 21.88 tok/s

基本上打平。

那个奇怪的 11.23 tok/s 低谷没有在更长的运行中重现,但两种配置都明显慢于它们最初的 100 token 中位数。

这引出了关于工作负载长度、持续性能以及生成过程中可能发生什么变化的问题。

我能说的是,LFM 显然并非天生就不擅长选择性专家卸载。

在新的 Q4_K_M 配置下,它表现得相当不错。

但这一提升主要来自量化、不同的层放置、llama.cpp 的改动,还是三者兼有,仍需进一步调查。

而更长的测试也让我想起了一件重要的事。

一个在 100 token 上看起来更快的配置,在 512 token 上未必有同样的优势。

这是我今后对模型做基准测试时一定会牢记的一点。

然后我尝试了 Gemma 4 的投机解码

因为显然五个模型和两种卸载策略还不足以让我玩得尽兴。

我还想看看多 Token 预测(MTP)投机解码能否在使用选择性 MoE 卸载时提升性能。

Gemma 4 26B-A4B 有一个独立的 MTP 草稿模型,可以为主模型提出多个 token 供其验证。

其思路是,同时生成并验证多个 token 有时比逐个生成更快。

理论上,这听起来很有前景。

实践中,我的结果并不特别令人鼓舞。

在没有 MTP 的情况下,选择性卸载达到了:

13.58 tokens/秒

启用 MTP 草稿模型后:

7.24 tokens/秒

所以它不仅没有加速,反而使吞吐量降低了约 46.7%。

我想弄明白原因。

在一次更长的 512 token 测试中,我捕获了草稿模型的遥测数据:

draft acceptance = 0.11678103 accepted / 882 generatedmean len = 1.35

只有11.68% 的候选草稿 token 被接受。

这个接受率相当糟糕。

更长的 MTP 运行也只测到 6.03 tok/s。

推测解码存在开销。如果草稿模型的预测无法被目标模型足够频繁地接受,额外的工作量就可能超过你期望获得的任何加速。

低接受率给了我一个比单纯假设 CPU/GPU 同步是罪魁祸首更具体的理由去调查这一减速。

我仍然想检查草稿模型的配置或其他兼容性问题是否导致了糟糕的接受率。

所以我不会由此得出结论说推测解码和 CPU 卸载从根本上不兼容。

只能说这个特定配置确实没有帮助。

重复测试还揭示了另一点:噪声

Riccardo 的主要建议之一就是考虑测量波动。

看到这些结果后,我很庆幸自己这么做了。

例如,三次 Apex 选择性卸载运行分别是:

9.21 tok/s9.70 tok/s25.50 tok/s

最后一次运行受到了过早生成序列结束符的影响,因此与其他运行相比不太可比。

LFM 也产生了那个异常缓慢的 11.23 tok/s 结果。

而 Gemma 的传统卸载运行则在 8.98 到 16.85 tok/s 之间波动。

这些差异可不小。

交错运行各配置有助于让测试过程更公平,但它并没有神奇地消除所有波动来源。

使用中位数有助于降低个别离群值的影响,尽管三次测量仍不足以建立特别可靠的性能分布。

较长的测试特别有用,因为它们表明,当工作负载发生变化时,一些表面上的优势会变得小得多。

展望未来,我认为更长的生成和更多重复测量将成为我实验的标准做法。

这要多花一点功夫,但显然值得。

那么我实际学到了什么?

我最初做这个实验,是想弄清楚选择性 MoE 卸载是否是在显存有限的 GPU 上运行大模型的更好方式。

我的第一个结论是,这取决于模型。

这似乎仍然成立,但现在我会补充更多内容。

量化可以改变结果。

Qwen 从传统拆分在 Q6_K 下略占优势,变成了选择性卸载在 Q4_K_M 下获得显著优势。

卸载配置很重要。

使用同一模型的两种配置,可能在 CPU 和 GPU 上分配非常不同的工作量。占用的显存量并不能说明全部问题。

短基准测试可能具有误导性。

LFM 在 100 token 测试中表面上的 7.5% 选择性优势,在更长的生成中几乎消失了。

投机解码并不是免费的速度提升。

Gemma 的 MTP 草稿模型接受率非常低,在我测试的设置下使生成明显变慢。

也许最重要的是,测量和解释不是一回事。

我可以测量某件事运行得更快还是更慢。

准确解释为什么有时需要多得多的测试。

随着我在这件事上越深入,这种区别变得越来越明显。

接下来是什么?

我还没有完全做完实验。

我还想在完全相同的检查点上测试更多量化级别,最好使用控制得更仔细的卸载配置。

我还对在推理期间监控实际的 CPU 内存带宽和 GPU 行为感兴趣。

这可能有助于回答一些关于这些变慢来自何处的问题,而不是依赖有根据的猜测。

最终,我还想在一台有 64GB 内存的机器上重复其中一些测试。

我尤其好奇,这能为将专家权重保留在系统内存中提供多大的灵活性,而不至于陷入交换压力。

但就目前而言,我觉得从这两轮测试中学到了不少东西。

最后的思考

公开分享实验最让我喜欢的一点是,有时别人会注意到你遗漏的东西。

我原本觉得自己的结果很有意思,现在依然这么认为。

但有好几个人就方法论、量化差异,以及这些测量是否足够可靠到能支撑我的某些结论提出了很好的问题。

他们问得对。

当我回头去测试这些内容时,我的一些结论发生了变化。

我很高兴它们变了。

我宁愿发现我最初的解释并不完全正确,也不愿因为它听起来合理就一直重复它。

这正是折腾这些东西的乐趣所在。

你从一个问题开始,发现一个有趣的结果,然后突然之间你就在研究三件你之前根本没考虑过的完全不同的事情。

偶尔你会意识到,你整个周末都在做语言模型的基准测试,而不是去做任何别的事情。

你知道的,完全正常的行为。

非常感谢每一位花时间阅读原文并提供深思熟虑反馈的人。这确实帮助让第二轮实验变得更好。

如果有人对我接下来该测试什么有建议,尤其是围绕量化、CPU 内存带宽或推测解码方面,我很乐意听取。

显然我盯着每秒 token 数的测量结果看的时间还不够长。