在13年前的Xeon服务器上运行Gemma 4 26B模型
我家地下室有台老服务器,按说根本跑不动现代语言模型。它是改装过的HP StoreVirtual存储机箱,距今约13年,搭载两颗Ivy Bridge架构Xeon处理器,没有GPU。这机子本来是用来存硬盘的,不是做运算的。但从这周起,它能运行谷歌的Gemma 4——一个260亿参数的开源混合专家模型(MoE),速度约为每秒5个token,差不多是正常阅读速度。
| 硬件配置 | 改装HP StoreVirtual:双Xeon E5-2690 v2(Ivy Bridge,2013款),DDR3内存,无GPU |
|---|---|
| 指令集 | 仅支持AVX1——无AVX2、无FMA3 |
| 模型版本 | Gemma 4 26B-A4B(MoE),Q8_0量化版 |
| 解码速度 | ~5.2 token/秒 |
| 提示词处理速度 | ~16 token/秒 |
| 机箱成本 | 低于300美元 |
租个GPU谁都会,但要让现代MoE模型和一台过气企业级机箱兼容,可就没那么简单了——这正是我写这篇文章的原因。如今“懂AI”悄悄变成了“付得起订阅费”,但我认为真正的本事并非如此:你得足够了解模型,能让它解决那些没人打包好的问题,还得能判断它给出的答案到底对不对。与其空谈能力,不如拿这个“完全不搭”的硬件组合做个实操案例。
缘起:一篇帖子
几周前,一篇题为《一台10年前的Xeon就够了》“A 10 year old Xeon is all you need”的文章在Hacker News上火了。作者用一台2016年的单Xeon处理器(无GPU,配128GB低速DDR3内存),借助ik_llama.cpp和25个精心挑选的参数,成功运行了Gemma 4。文章写得很棒,把现代推理技术手册里的技巧用了个遍: speculative decoding(投机解码)、CPU适配的混合专家路由、移植到CPU的flash attention、运行时权重重组,实打实的工程技术。
“我也有Xeon啊,”我心想——不止一台呢。于是我照着试了试,结果根本跑不起来。
AI助手的真正价值
程序启动就失败了。我把报错信息丢给Claude,问它问题出在哪。答案来得又快又具体:作者的2016年芯片是Broadwell架构,而我的是Intel称为“v2”的Ivy Bridge架构。那个分支代码里的快速内核默认需要AVX2和FMA3指令集,而这两个指令集直到2014年的Haswell(即“v3”代)架构才推出。我的CPU比代码依赖的指令集还老,根本找不到对应的优化执行路径。
于是我顺理成章地追问:那我们能不能让它跑起来?我之前试过一个免费模型,差一点成功但没能落地。Claude接过我那套未完成的方案,认可了思路的正确性,并完成了后续工作——重写了关键路径,让代码在不支持AVX2的芯片上能平稳降级,而不是一味调用不存在的指令。
这正是我在意的地方。这可不是输入一句“修复它”就拿到可用补丁那么简单。得有人读懂别人写的性能关键型C++代码,找出内核在特定微架构上失效的原因,还要绕开问题的同时保留原分支的优化价值。这些活儿都是Claude干的。我的角色更聚焦:做正确的实验,判断最终输出是否符合预期。结果出来后,我深感震撼。
最终成果
现在,Gemma 4的260亿参数混合专家模型,能在这台比模型架构诞生还早的老硬件上,以阅读速度生成文本。原帖只说了“阅读速度”,没给出具体token每秒数值,那我来补上:在这台13年前的芯片上,速度约为每秒5个token,成本几乎可以忽略不计。

运行证明:Gemma 4 26B在地下室的机箱上纯CPU运行并作答。
相关补丁已提交至ikawrakow/ik_llama.cpp#2138——写这篇文章时,补丁还在等待维护者审核,所以暂时请从对应分支获取代码。希望其他手里有闲置旧企业级硬件的人,也能用上本地模型:付费API故障时可以作为备选,或者在按token付费不划算的场景下,低成本处理批量慢任务。
技术细节:问题到底出在哪
先坦白:我不是C++程序员。我能看懂堆栈跟踪,也熟悉构建系统,但那些量化矩阵乘法引擎的内核降级代码不是我写的,我也不会假装是我写的。我只是“司机”:做实验、读输出、问问题,知道“正确结果”该是什么样。诊断和补丁都来自服务器上运行的Claude。我让它写下修复过程,下面就是它的总结,我稍作了编辑。如果是从Hacker News来想看技术拆解的,这部分就是为你准备的。
故障根源
我们用的是ik_llama.cpp——ikawrakow基于llama.cpp的分支版本,添加了Gemma 4的MoE推理所需的优化。它默认要求最低支持AVX2指令集,但我这台机子的Xeon E5-2690 v2只有AVX1,没有AVX2。如果在编译时关闭GGML_USE_IQK_MULMAT,代码库大部分内容都会兼容:快速路径会被编译掉,模型会降级到普通的标量/SSE运算。对于常规Q8_0矩阵乘法来说,这没问题。
但有两个图运算例外。Gemma 4的MoE前馈网络会生成MOE_FUSED_UP_GATE(融合了SwiGLU的单专家门控+上采样矩阵乘法)和FUSED_UP_GATE(对应的密集型运算)。这两个运算在计算调度器里都用#if绑定了GGML_USE_IQK_MULMAT,但图构建器仍会无条件生成它们。在我们的编译配置下,调度器的分支里没有这两个运算的处理逻辑,于是它们会落到默认分支,导致每个专家FFN的目标张量完全没被计算。Gemma 4 26B有30层,每层每个token对应8个激活专家,所以每次前向传播都会把内存缓冲区里原有的随机数据当成约240个张量来用。
症状是生成的文本看起来流畅,但内容是多语言乱码:token ID均匀分布在26.2万词表中,模型会随意输出泰文、韩文、<unused>标记或英文片段。温度设为0时结果完全确定,单线程和多线程运行结果字节级一致,全程没有NaN值。本质是每一层的隐藏状态都被一个大常量干扰,直到最终的softmax输出变得扁平。
正是这种确定性帮我们找到了问题。Claude对采样前的原始logit做了监控,打印出前5个token及其范围、均值和NaN数量。数据泄露了真相:第一个预测token的logit均值为+16,而正常情况下应该接近0,且约80%的词表logit为正值。随机 corruption不会是这个样子。这么规整的偏差,只有当大量隐藏状态是未初始化内存,且恰好存储着小的正浮点数时才会出现。
修复方案
在原分支的main基础上提交了三个commit:
编译修复:
iqk_quantize.cpp中quantize_row_q8_0_x4和quantize_row_q8_1_x4_T的标量#else分支,实际上并非真正的标量实现——它们仍引用了hsum_i32_8等AVX2辅助函数。我们将其重写为可移植的标量循环,并在ggml.c和ggml-quants.c中遗漏的几个IQK调用周围添加了#if GGML_USE_IQK_MULMAT保护,还补了一个缺失的头文件,让iqk_cpu_ops.cpp能独立编译。没有这些修复,分支代码在非AVX2硬件上根本编译不了。运行时故障修复:我们没改动调度器,而是让图构建器生成当前编译配置下有计算路径的运算。在
ggml_moe_up_gate中,当GGML_USE_IQK_MULMAT关闭时:如果权重是合并的up_gate_exps张量(形状为[n_embd, 2*n_ff, n_experts],前半部分是门控,后半部分是上采样),就将其拆分为两个ggml_view_3d切片,分别调用两次ggml_mul_mat_id,再用ggml_fused_mul_unary(gate, up, SILU)合并结果;如果门控和上采样权重已经分开,就跳过拆分,直接执行两次矩阵乘法和融合运算。非MoE层使用的密集型运算ggml_fused_up_gate也做了同样处理。用到的所有运算都已有可用的非IQK实现(mul_mat_id是标准ggml函数,fused_mul_unary能一次性完成SILU和乘法操作)。整个修改都放在#if !GGML_USE_IQK_MULMAT条件下,因此AVX2编译版本的结果和之前完全一致。CI桩代码修复:iqk源码中的
#else桩代码与iqk_mul_mat.h不同步,导致ci/run.sh在非AVX2硬件上都无法构建:缺少<cstdint>头文件、桩函数签名错误(多了一个前置参数,少了sinks参数等),还有几个函数根本没有桩代码,导致链接时出现未定义引用。这些工作很枯燥,但没有它,用这类硬件的人连测试套件都跑不了。
降级方案会有一点性能损耗:用两次独立的矩阵乘法替代了融合内核,但这台CPU本来就受限于内存带宽,而且融合内核本身只支持AVX2,所以我们并没有损失什么。最终,26B-A4B MoE模型的解码速度约为5.2 token/秒,提示词处理速度约为16 token/秒。
还有个坑要注意:--run-time-repack参数会在启动时将量化权重重排为仅支持AVX2的交错布局(Q8_0_R8),这会在AVX1硬件上导致输出乱码。这是另一个独立bug,本次补丁没有修复它,运行脚本只需去掉这个参数即可。
指令集不匹配很容易发现,但无提示的分支遗漏却很难排查。我通读代码排除了所有明显的可疑点:RMSNorm辅助函数看起来没问题,ggml_vec_dot_q8_0_q8_0中的AVX1降级逻辑也没问题,而且单线程运行结果字节级一致,排除了线程问题。直到监控logit发现均值固定为+16,且所有长尾token的数值几乎相同,排查范围才缩小到“大量残差流未初始化”。之后在调度器中搜索#if GGML_USE_IQK_MULMAT,不到一分钟就找到了那两个缺失的分支。
复现步骤
如果你有一台不支持AVX2的机子,想试试这个方案:
- 硬件:双Xeon E5-2690 v2(Ivy Bridge架构,仅支持AVX1,无AVX2),DDR3内存,无GPU。
- 编译:从上述分支编译
ik_llama.cpp,关闭GGML_USE_IQK_MULMAT。正是那些编译修复,让代码能在非AVX2硬件上顺利编译。 - 模型:Gemma 4 26B-A4B,Q8_0量化版。
- 运行:使用常规的
ik_llama.cppCPU参数,但去掉--run-time-repack(它会将权重重排为仅支持AVX2的布局,导致AVX1硬件上输出乱码)。
就是这么简单:纯CPU运行,机箱里完全没有GPU,解码速度约为每秒5个token。
为什么发在公司博客
订阅服务是最简单的事,真正难的是愿意打开“引擎盖”,读懂陌生人的代码,不断尝试直到让一台13年前的CPU完成它从未被设计过的任务。这种工作,和维护一个15年历史的Rails应用、或是接手团队没人懂的数据库是一样的:需要有人深挖,找到问题的关键,发现工具不会主动告诉你的信息。
如果你手里有闲置的非AVX2老硬件,试了这个分支,欢迎告诉我在你的机子上表现如何——这种兼容性能支持到多老的CPU?bug报告可以提交到PR讨论区。如果你的“老伙计”不是13年前的Xeon,而是15年前的Rails应用,那正是我们的本职工作。
给好奇的人补充两个小细节:这台服务器成本不到300美元,关于为什么地下室的老机子比每月1500美元的云服务更划算,我算了一笔账;另外,把一台噪音巨大的企业级设备改得安静又能正常启动,本身也是个小项目,感兴趣的可以看看相关记录。