Skip to content
Published on

26B 模型跑在 2GB 内存里的原理 — 常驻内存和工作集不是同一个数字

分享
Authors

引言 — 「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 Air8 GB未能确认83 ms5.1~6.3 tok/s
M4(带宽参考用)2,031 MB/s约 7 tok/s
M5 Pro24 GB6,323 MB/s约 22 tok/s12 ms31~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 个。

读本地推理的性能主张时,该抛出的问题永远是同一个。那个数字在数什么,它没有数的东西又在哪里。