昨晚刷 Hacker News 的时候,一个项目把我看愣了:有人用纯 C + Metal Shader 在一台 48GB 内存的 MacBook Pro 上跑通了 Qwen3.5-397B-A17B——一个 3970 亿参数的 MoE 模型,速度 4.4 tokens/s,还能正常做 Function Calling。
我第一反应是:这怎么可能?模型 209GB,内存才 48GB,它是怎么塞进去的?
研究了一晚上源码和论文,我终于搞明白了。今天就把这个项目拆开来聊聊,顺便说说本地推理这条路到底走到哪了。
Flash-MoE 是什么
Flash-MoE 是一个纯 C/Objective-C 写的推理引擎,专门针对 Apple Silicon 的 MoE(Mixture of Experts)模型优化。
它跑的是 Qwen3.5-397B-A17B,这个模型的架构很特别:
- 总参数 397B(3970 亿),但每个 token 只激活 17B
- 60 层 Transformer,每层 512 个专家,每次只用 4 个
- 45 层 GatedDeltaNet(线性注意力)+ 15 层标准全注意力
关键点在于:虽然模型巨大,但 MoE 架构意味着推理时只需要加载一小部分权重。Flash-MoE 就是抓住了这个特性。
核心黑魔法:SSD 当显存用
传统思路是把模型全部加载到内存(或显存)里。397B 模型 4-bit 量化后还有 209GB,48GB 内存根本装不下。
Flash-MoE 的做法很暴力也很优雅:把 SSD 当成一个超大的权重池。
推理一个 token 的流程:
1. Router 决定激活哪 4 个专家
2. 从 SSD 按需读取这 4 个专家的权重(每个约 6.75MB)
3. GPU 完成计算
4. 进入下一层,重复
每层只读 4 × 6.75MB ≈ 27MB 权重,60 层总共约 1.6GB。Apple 的 NVMe SSD 顺序读取带宽实测 17.5 GB/s,完全跟得上。
而且它没有自己实现什么复杂的缓存机制,直接用 pread() 系统调用读文件,让 macOS 的 page cache(约 35GB)自动管理热点数据。作者称之为 "Trust the OS" 策略——灵感来自 Apple 自己的论文《LLM in a Flash》。
// 伪代码:并行读取多个专家权重
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < K; i++) {
dispatch_group_async(group, queue, ^{
pread(fd, expert_buf[i], expert_size, expert_offset[i]);
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
这个思路看似简单,但要做到 4.4 tok/s 的速度,背后的工程优化非常硬核。
Metal Shader 手搓到极致
Flash-MoE 没有用任何现成的推理框架(没有 llama.cpp,没有 MLX,没有 Python),全是手写的 Metal compute shader:
FMA 优化的 4-bit 反量化核
标准做法是 (nibble * scale + bias) * x,三步运算。Flash-MoE 把它改写成 fma(nibble, scale*x, bias*x),提前算好 scale*x 和 bias*x,让 GPU 的 FMA 单元一条指令完成反量化 + 乘法。效果?快了 12%。
// 优化前
float result = (nibble * scale + bias) * x;
// 优化后:预计算 scale_x 和 bias_x
float scale_x = scale * x; // 每个权重组只算一次
float bias_x = bias * x;
float result = fma(nibble, scale_x, bias_x); // 一条 FMA 指令
其他手搓的 Shader 还包括:
- 融合 SwiGLU 激活函数
- 两阶段 RMS 归一化
- 批量 GPU 注意力(Q@K^T → softmax → scores@V)
- 融合 RoPE + Q 重排 + K 归一化
- MoE 合并 + 残差 + Sigmoid 门控(全融合到一个 kernel)
每个 kernel 都是手动 tiled、SIMD reduce、shared memory cache。这种级别的优化在大厂推理团队里常见,但一个开源项目做到这种程度,确实少见。
4-bit vs 2-bit:精度的取舍
Flash-MoE 同时支持 4-bit 和 2-bit 量化:
| 配置 | 速度 | 磁盘占用 | 质量 |
|---|---|---|---|
| 4-bit + FMA | 4.36 tok/s | 209GB | 优秀,Function Calling 正常 |
| 2-bit | 5.74 tok/s | 120GB | 尚可,但 JSON 输出会出错 |
| 2-bit 峰值 | 7.05 tok/s | 120GB | 热缓存爆发,不稳定 |
2-bit 快是快,但有个致命问题:它会把 JSON 里的双引号 "name" 输出成 \\name\\,直接导致 Function Calling 和结构化输出废掉。对于需要调用工具的场景,4-bit 是唯一的生产选择。
这其实反映了一个普遍问题:极端量化在开放对话上表现还行,但一碰到格式敏感的任务就露馅了。
本地推理 vs API 调用:什么时候用哪个
Flash-MoE 让本地跑 397B 模型成为可能,但"可以"不等于"应该"。来理性分析一下:
本地推理适合的场景
# 场景 1:隐私敏感的数据处理
# 比如处理公司内部代码、医疗数据、法律文档
# 数据不出本机,合规无忧
import subprocess
result = subprocess.run(
["./flash-moe", "--prompt", sensitive_data],
capture_output=True
)
- 隐私合规:数据不离开本地
- 离线可用:飞机上、内网环境、网络不稳的地方
- 无限制调用:不按 token 计费,跑多少都行
- 定制自由:想换量化精度、调推理参数随便折腾
API 调用适合的场景
# 场景 2:需要最强模型 + 稳定服务的生产环境
import openai
client = openai.OpenAI(
base_url="https://api.ofox.ai/v1",
api_key="sk-xxx"
)
response = client.chat.completions.create(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "帮我 review 这段代码"}],
stream=True
)
- 模型更新快:新模型一出,API 直接切换,不用下载几百 GB
- 并发能力强:10 个用户同时请求?API 弹性扩展,本地只能排队
- 总体成本低:不是每个人都有 M3 Max + 1TB SSD
- 稳定性高:不用操心 OOM、散热、磁盘空间
我的实际选择
说实话,日常开发我还是以 API 为主。原因很简单:
- 模型迭代太快了。上个月还是 Claude 3.5,这个月 Claude 4 就出了。本地部署意味着每次都要重新下载、量化、调优
- 4.4 tok/s 对交互式使用来说偏慢。习惯了 API 几十 tok/s 的速度后,确实回不去
- 多模型切换的需求。我经常在 Claude、GPT、DeepSeek 之间切换找最优解,这个 API 做起来一行代码的事
但如果你的场景是:处理敏感数据、需要离线运行、或者单纯想折腾学习——Flash-MoE 这类项目绝对值得一试。
这个项目教会我的事
除了技术本身,Flash-MoE 还有几点很值得学习:
1. 选择最适合的工具而不是最流行的
作者没用 Python,没用 PyTorch,没用 llama.cpp。纯 C + Metal,因为这是在 Apple Silicon 上做推理最高效的路径。很多时候我们选技术栈是因为"大家都在用",而不是"它最适合这个问题"。
2. 理解硬件才能做好优化
FMA 指令优化、SSD 带宽利用、page cache 策略——每一个优化都建立在对硬件深刻理解的基础上。用高层框架你永远触碰不到这些细节。
3. MoE 架构改变了推理的游戏规则
传统 dense 模型,397B 参数意味着 397B 都要加载。MoE 模型只需要加载活跃的那一小部分。这意味着未来我们可能看到参数量更大但推理成本更低的模型。
DeepSeek 已经证明了这一点,Qwen 也在跟进。MoE 不是新概念,但 Flash-MoE 证明了它在消费级硬件上的可行性。
写在最后
AI 推理正在两极分化:
- 一端是云端 API,追求极致的速度、稳定性和便利性
- 另一端是本地推理,追求隐私、自由和对硬件的极致利用
Flash-MoE 代表的是后者的极限探索。虽然 4.4 tok/s 还不够快,虽然需要 209GB 磁盘空间,但它证明了一件事:消费级硬件的天花板比我们想象的高得多。
对于开发者来说,最好的策略可能是两条腿走路:日常开发用 API 保证效率,敏感场景或学习目的切本地模型。毕竟这两条路不矛盾。
如果你有一台 Apple Silicon 的 Mac,强烈建议去 GitHub 仓库 看看源码。就算不跑模型,那些 Metal Shader 的优化技巧也很值得学习。
如果这篇文章对你有帮助,点个赞👍收藏一下呗。有问题欢迎评论区交流。