- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 「2GB」是在数什么的数字
- 14.3GB 是怎么来的 — 先从存储容量的算术开始
- MoE 造出来的两种权重
- 为什么丢掉 mmap 转向 pread
- 每个 token 285MB — 带宽定上限
- 常驻内存与工作集 — 2GB 为真的地方和有误导性的地方
- 4 比特的质量代价,以及这套做法合适的位置
- 结语 — 先确认数字在数什么,大部分魔法就解释得通了
引言 — 「2GB」是在数什么的数字
2026 年 7 月 29 日,一个叫 TurboFieldfare 的项目发到了 Show HN 上。它是用 Swift 和 Metal 写的推理运行时,附带的主张是能在 M 系列 Mac 上用约 2GB 内存跑 Gemma 4 26B-A4B。仓库 README 写的数值是:M2 MacBook Air 8GB 上每秒 5.1~6.3 个 token,M5 Pro 24GB 上 31~35 个。
「26B 塞进 2GB」这句话让人反射性地起疑。可这个项目的数字彼此咬合得很好。真去算一遍,大部分都准确。有意思的地方不在于主张是错的,而在于2GB 这个数字到底在数什么。它是进程的常驻内存,和系统实际使用的内存是两个值。而这个差别,解释了 M2 Air 与 M5 Pro 之间六倍性能差距的相当一部分。
本文不直接接受 README 和 HN 讨论帖里的数值,而是自己推导后对一遍。算术对得上的项目,和对不上、需要另一种解释的项目,分开来看。
14.3GB 是怎么来的 — 先从存储容量的算术开始
先看磁盘。README 把纯文本模型的安装容量写成约 14.3GB,把量化方式写成「MLX 仿射 4 比特、分组 64、路由器 8 比特」。按 Gemma 4 模型卡,26B-A4B 的总参数是 25.2B,激活 3.8B,上下文 256K。
明明是 4 比特,为什么不是 25.2B 的一半即 12.6GB 呢。因为按组做的仿射量化并不是只存权重。
分组 64 仿射 4 比特的实际存储成本
权重 64 个 x 4 比特 = 32 字节
缩放 1 个 x fp16 = 2 字节
零点 1 个 x fp16 = 2 字节
------------------------------------------
合计 = 36 字节 / 64 权重
每权重比特数 = 36 x 8 / 64 = 4.5 bpw
模型整体 = 25.2e9 x 4.5 / 8 = 14.18e9 字节 = 约 14.2 GB
与报告的 14.3GB 在 1% 以内吻合。剩下的 0.1GB,用 8 比特路由器、嵌入层处理方式的差异和元数据来解释就够了。
这里已经出来一条教训 — 「4 比特量化」这个说法,并不会把文件大小变成参数量的一半。分组越小,质量越好,开销也越大。如果是分组 32,bpw 就变成 5.0,文件变成 15.8GB。做本地部署规划时,这 0.5 比特就是 1.5GB 的差别。权重与 KV 缓存怎么用公式算,我在在本地跑 LLM 到底需要多少 VRAM那一篇里整理过。
MoE 造出来的两种权重
现在来看常驻内存里的 1.35GB 到底是什么。README 把它叫作「共享核心」。在 MoE 结构里,参数分成两类。
- 所有 token 都要用的部分 — 嵌入、注意力、共享专家、路由器。这部分必须常驻。
- 每个 token 挑着用的部分 — 路由专家。每层大约在 128 个里激活 8 个。
从激活参数 3.8B 和总参数 25.2B 这两个数字,可以把这个切分反推出来。
S = 总要用的参数, R = 路由专家的参数
激活比例 f = 激活专家数 / 全部专家数
S + R = 25.2B
S + f x R = 3.8B (假设 f = 8/128 = 0.0625)
0.9375 R = 21.4B -> R = 22.8B, S = 2.4B
把 S 按 4.5 bpw 存储
2.4e9 x 4.5 / 8 = 1.35e9 字节 = 1.35 GB
与 README 公布的共享核心 1.35GB 精确一致。这说明激活专家 8 对 128 这个结构假设是对的,同时也展示了这个设计的核心。只要把整体的 5% 常驻,剩下的 95% 只需按 token 取来需要的那一小块。
这里要强调的是,这套做法之所以成立,是因为 Gemma 4 是 MoE。如果是同样大小的稠密(dense)模型,每个 token 都要读完整的 25.2B,那是任何 SSD 都扛不住的。流式推理不是量化技法,而是架构打开的那扇门。
为什么丢掉 mmap 转向 pread
要惰性加载权重,第一个想到的方法是 mmap。把文件映射进地址空间,一访问,内核就会自动把页取过来。代码简单,包括 llama.cpp 在内的大多数本地推理引擎都用这个方式。
可是按作者在 HN 讨论帖里说的,mmap 的实现停在每秒 0.5 个 token,换成并行的 pread 调用之后上到了约 4 个 token。八倍。原因在于两种方式的并发模型不同。
用 mmap 访问的页如果还不在内存里,就会缺页,那个线程会停到缺页被解决为止。内核的预读(readahead)假定的是顺序访问,而专家路由造出来的访问模式,是散落在整个文件里的随机读。预测不准,于是每次都只能一个一个地等。相比之下 pread 是显式请求,可以把需要的偏移量列表一次性丢出去,把 SSD 的队列深度填满。NVMe 不是把单个请求处理得很快的设备,而是同时处理几十个请求来填满带宽的设备,所以这个差别就原样表现为八倍。
实际实现还在上面加了两样东西。每层放一个 16 槽的 LFU 缓存,把常用的专家扣住;在 prefill 阶段最多按 128 个 token 打包处理,让取过来一次的专家去处理多个 token。而在 CPU 读取下一个专家的同时,Metal 计算共享专家分支 — 把输入输出与计算叠起来的经典技法。
每个 token 285MB — 带宽定上限
现在可以算性能上限了。
路由专家整体 = 22.8e9 x 4.5 / 8 = 12.8 GB
每 token 激活比例 = 8 / 128 = 6.25%
完全没有缓存时每个 token 要读的量
12.8 GB x 0.0625 = 800 MB
作者在 HN 上报告的实测: 每个 token 250~320 MB
反推的缓存命中率 = 1 - (250~320) / 800 = 60% ~ 69%
作者另外报告的值是 M2 上 59~69%,用 16 槽缓存约 67%。和我推导的 60~69% 对得上。也就是说,散落在 README 和 HN 里的数字,出自同一个自洽的模型。把每个 token 的实际传输量取成 285MB 左右,按设备算上限就是下面这样。
| 设备 | 内存 | 引用的 SSD 顺序读 | 按每 token 285MB 算的理论上限(推导) | 报告的每 token 磁盘时间 | 报告的吞吐 |
|---|---|---|---|---|---|
| M2 Air | 8 GB | 未能确认 | — | 83 ms | 5.1~6.3 tok/s |
| M4(带宽参考用) | — | 2,031 MB/s | 约 7 tok/s | — | — |
| M5 Pro | 24 GB | 6,323 MB/s | 约 22 tok/s | 12 ms | 31~35 tok/s |
M2 Air 那一侧算得很吻合。每个 token 83 毫秒的磁盘时间,上限就是每秒 12 个 token,再加上剩余的计算时间,实测 5~6 个 token 是自然的。
问题出在 M5 Pro 这一行。用引用的 SSD 带宽算出来的上限约为 22 个 token,而实测是 31~35 个 token。要在每个 token 12 毫秒里读完 285MB,需要每秒 23.75GB,这是任何消费级 NVMe 都给不出来的速度。不是算术不对,而是说这些读取里有相当一部分并非来自 SSD。
常驻内存与工作集 — 2GB 为真的地方和有误导性的地方
答案是 macOS 的统一缓冲区缓存。用 pread 读进来的文件页会被内核留在内存里,再读同一个偏移量就不会碰 SSD。在 24GB 的机器上,14.3GB 的模型文件有相当一部分可以留在缓存里。热身结束之后的所谓「磁盘读取」,实际上更接近内存拷贝。
可是这些页不会被计入进程的常驻内存。基于文件的页属于内核的页缓存,内存一有压力,内核就会回收。所以在活动监视器里,进程看上去仍然是 2GB。这个主张不是假的 — 只是数的对象不一样。
归纳起来是这样。
- 为真的部分:这个运行时扣在自己堆里的权重,是共享核心 1.35GB 再加上 4K 的 KV 缓存这么多,进程 RSS 在 2GB 附近。在只有 8GB 内存的机器上,26B 级别的模型能跑起来这件事本身,就是这个设计的成果。
- 具有误导性的部分:整个系统用掉的内存不是 2GB。想拿到性能,内核页缓存就得握着模型文件的一大块,而那块内存要和别的应用竞争。开二十个浏览器标签把缓存挤掉,吞吐就会掉到 SSD 带宽上限。M2 Air 的 5~6 个 token 与 M5 Pro 的 31~35 个 token 之间的差距,不只是芯片性能之差,也是可以拿来当缓存用的空闲内存之差。
- 因此应当怀疑的部分:HN 上出现的「收益是不是来自 OS 缓存而不是技法」这个指摘,说对了一半。不过从
mmap换到pread带来的八倍,是缓存解释不了的,所以技法的贡献也是实在的。两个因素同时存在。
另外还有一项算术合不拢。每层 16 槽的缓存占全部 128 个的 12.5%,所以如果它在所有层上都完整常驻,就是 12.8GB 的 12.5%,约 1.6GB。再加上共享核心 1.35GB 就超过 2GB 了。要么是实际实现对槽的计数方式不同,要么是那块缓冲区里相当一部分是基于文件的、因而不计入 RSS,二者必居其一,而我没能读源码确认是哪一种。无论是哪一种,结论指向同一个方向 — 2GB 是进程记账簿上的数字。
4 比特的质量代价,以及这套做法合适的位置
最后是不免费的那部分。关于 4 比特量化的质量损失,被广泛引用的说法是:相对 FP16,困惑度会差 1~3%,而且总参数越大损失越小。也有说法认为,MoE 的总参数大,所以即便激活量小也相对能扛住 4 比特。不过这些都是聚合类文章里反复出现的一般性说法,并不是对这个特定构建做的测量。
准确地说是这样。Gemma 4 26B-A4B 的公开基准是未量化模型的成绩。模型卡上登载的 MMLU Pro 82.6、GPQA Diamond 82.3、LiveCodeBench v6 77.1 这类数字,不是压到 4.5 bpw 的这个构建的分数。而这个构建的分数,还没有人量过。README 也只是挂了一句「模型可能会重复或给出错误答案,重要结果请自行核实」。在谈论量化后本地模型的质量时去引用原始模型卡的数字,正是本文主张不该做的那一类事。
那么这套做法适合放在哪里呢。
- 合适的位置:内存不够的机器上、离线、对延迟不敏感的活儿。文档摘要、本地代码讲解、没有网络的环境。每秒 5 个 token 用来对话是憋屈的,但用作后台批处理是够的。如果数据不能外发,可选项本来就不多。
- 不合适的位置:内存充足的机器。24GB 以上的话,直接把 14.3GB 的模型整个装进去更简单也更快。这套做法的存在理由是内存不足,不是性能。而常驻运行的服务器就更不合适了 — 这个运行时明确写着每个实例只有单一进程拥有模型,不支持并发执行。
- 没有被确认的部分:关于 SSD 寿命没有定量资料。因为以读为主,写入磨损看上去不会大,但仓库也说明自己没有把这一点量化。另外 macOS 26 与 Metal 4 是硬性要求,想在 macOS 15 上用,需要放弃 2.4 倍 prefill 加速的绕行方案。从 M1 到 M5 全世代都能得到同样结果,也还没有依据。
结语 — 先确认数字在数什么,大部分魔法就解释得通了
「26B 塞进 2GB」不是夸张。4 比特分组 64 得出的 4.5 bpw,从总 25.2B 与激活 3.8B 反推出来的共享核心 1.35GB,路由专家 12.8GB 的 6.25% 即每个 token 800MB,被缓存压到 285MB 的实际传输量 — 这四个数字连成一条线,而且与报告的值吻合。这是好的工程。
同时,2GB 是进程的常驻内存,不是系统的工作集。M5 Pro 的实测吞吐超过 SSD 带宽上限这一事实就是证据,而填上这个差的是内核页缓存。所以你把这个项目挂到 8GB 的 Mac 上时,该期待的数字不是 31~35 个 token,而是 5~6 个。
读本地推理的性能主张时,该抛出的问题永远是同一个。那个数字在数什么,它没有数的东西又在哪里。