MoE大模型超内存运行实战:llama.cpp专家卸载与SSD流式加载

利用MoE稀疏激活与llama.cpp的mmap和专家分层配置,让百亿参数大模型在有限内存的笔记本上运行。
文章解释了为何MoE模型可以超内存运行而稠密模型不行:MoE每个token只激活少量专家,注意力等共享层常驻VRAM,专家权重可留在RAM或由操作系统从SSD按需分页。给出用gguf包估算模型内存分布的脚本,并以经验数据指出RAM达到专家权重70%以上时速度几乎不受影响,低于40%通常不值得。随后给出llama.cpp的配置方法,用-ngl 99配合--n-cpu-moe或-ot把专家层留在CPU,并强调保留默认mmap、避免--no-mmap和--mlock。最后介绍用iostat、maj_flt和vmtouch诊断SSD瓶颈,并给出优先升级NVMe SSD等实操建议。
当100B+参数的MoE模型无法完全装进32GB内存的笔记本时,大多数人会直接放弃。但MoE架构的特殊性让"超内存运行"成为可能:每个token只激活一小部分专家权重,其余部分可以留在SSD上按需加载。本文基于llama.cpp的mmap机制与--n-cpu-moe参数,拆解如何让大模型在有限硬件上跑起来。

MoE为什么能超内存运行,而稠密模型不行
稠密模型如Llama 3.1 70B,每个token都必须经过全部权重。如果权重装不进内存,每个token就要从磁盘重新读取几十GB数据。即便NVMe Gen4的7GB/s顺序读取速度,也只能做到每几秒一个token,几乎没有实用价值。
MoE架构则完全不同。以gpt-oss-120b为例:总参数约117B,但每个token只激活约5.1B。Qwen3-30B-A3B总参数30B,激活仅约3B。每层的路由器只选择少数专家(通常是top-4或top-8,从几十到128个专家中选),其余专家不被触碰。
这带来两个关键结论:
- 注意力层、嵌入层和共享层在每个token都会被使用,体积小,应放在最快的VRAM中。
- 专家FFN权重占据大部分体积,但每次只读取一小部分。这部分放RAM,RAM不够时由操作系统从SSD分页加载。
更关键的是,未被选中的专家并非同等重要。实际使用中,面对同一类提示词(比如全是Python代码),路由器会反复选择同一组专家。Linux页缓存会自动把这组"热"专家留在RAM里,"冷"专家留在SSD上按需读取。Janas等项目的优化正是在这一层做预取和按专家LRU缓存,但仅靠llama.cpp默认的mmap,你已经能获得大部分收益。
mmap 的按需分页机制:llama.cpp 默认使用内存映射文件(mmap)加载 GGUF 模型,即通过 mmap() 系统调用把模型文件映射到进程虚拟地址空间,而不立即把全部内容读入物理内存。当进程访问某个张量地址时,若对应页不在物理 RAM 中,内核触发一次 major page fault,从 SSD 读入该页并放入 Linux 页缓存(page cache)。页缓存是内核管理的一块可回收内存:当空闲 RAM 充足时,被读入的页会一直驻留,后续访问同一页零成本;当 RAM 紧张时,内核按 LRU 类策略淘汰旧页。MoE 推理中,注意力层与共享层会被频繁访问而稳定驻留,专家权重则按路由器选择情况动态换入换出,正是页缓存让“专家在 SSD 上按需加载”成为默认行为。
major fault 与 minor fault 的区别:major page fault 表示目标页不在物理内存、必须从磁盘读取,开销在毫秒级;minor fault 仅需在页表或页缓存中建立映射,开销在微秒级。推理预热阶段 major fault 大量出现,因为模型页正被首次读入;预热结束后大部分访问命中页缓存,只有切换到新专家时才重新产生 major fault。这解释了为什么监控 maj_flt 的增长曲线能判断内存是否颠簸——如果它在生成过程中持续攀升而非趋于平稳,说明工作集已超出 RAM。
下载60GB模型前的内存估算
不要等下载完才发现跑不动。用一个基于gguf包的脚本,读取GGUF元数据并按张量分组统计:
# estimate_moe.py - 估算GGUF模型的内存需求
import sys, re
from gguf import GGUFReader
path = sys.argv[1]
reader = GGUFReader(path)
expert_bytes, other_bytes = 0, 0
for t in reader.tensors:
size = int(t.n_bytes)
# llama.cpp中专家张量命名为 blk.N.ffn_up_exps.weight
if re.search(r"ffn_(up|down|gate)_exps", t.name):
expert_bytes += size
else:
other_bytes += size
gb = lambda b: b / 1024**3
print(f"非专家层(建议放VRAM): {gb(other_bytes):6.1f} GB")
print(f"专家权重(RAM/SSD): {gb(expert_bytes):6.1f} GB")
print(f"总计: {gb(expert_bytes + other_bytes):6.1f} GB")
用约120B模型的Q4量化版测试,非专家部分只有几GB,8GB显卡就能容纳。专家部分约55-60GB。64GB内存的机器几乎可以全部放进页缓存;32GB内存则有一半需要从SSD读取,此时SSD速度开始真正影响体验。
实践经验:
- RAM ≥ 专家权重的70%:速度几乎与全内存运行无异。
- RAM 在40%-70%之间:可以运行,速度随提示词波动。切换话题后前几秒会变慢。
- RAM < 40%:通常不值得,应选择更小的量化或更小的模型。
llama.cpp配置:把专家从GPU赶出去
llama.cpp已支持--n-cpu-moe和--override-tensor(-ot)来精确控制每个张量的存放位置。以下配置适用于12GB显卡:
**`-ngl` 与 `-ot` 的作用范围**:`-ngl N`(`--n-gpu-layers`)指定把前 N 层 offload 到 GPU,`99` 意味着尽量把所有层都放到显卡上。但 MoE 模型每一层内部同时包含注意力张量与专家 FFN 张量,仅靠 `-ngl` 无法把它们分开,于是需要 `-ot`(`--override-tensor`,张量覆盖规则)做更细粒度的控制。`-ot` 接受 `模式=设备` 形式的正则规则,例如 `.ffn_.*_exps.=CPU` 表示把所有名字匹配 `ffn_*_exps` 的专家张量强制放在 CPU/内存侧,其余张量仍按 `-ngl` 规则进 GPU。`--n-cpu-moe N` 是更简洁的语法糖,等价于把前 N 层的专家张量留在 CPU。二者的核心思路一致:注意力层体积小、每 token 必用,放 VRAM 收益最大;专家权重体积大、每 token 只用一小部分,留在 RAM 或 SSD 更划算。
# 编译带CUDA的最新版
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON && cmake --build build -j --config Release
# 方式一:全部层offload到GPU,但强制专家留在CPU
./build/bin/llama-server \
-m ~/models/gpt-oss-120b-Q4_K_M.gguf \
-ngl 99 \
-ot ".ffn_.*_exps.=CPU" \
-c 16384 -t 8 --port 8080
# 方式二:更简洁的语法,前N层专家保留在CPU
# 调整N直到VRAM刚好够用
./build/bin/llama-server -m ~/models/gpt-oss-120b-Q4_K_M.gguf \
-ngl 99 --n-cpu-moe 30 -c 16384
试错中总结的几点:
- 不要加
--no-mmap。该参数强制将模型全部加载到RAM,进程会被OOM killer杀死。默认mmap让内核决定哪些页保留、哪些丢弃。 - 不要用
--mlock。它会锁定所有页到RAM,在大模型场景下直接失败。 - **
-t(线程数)**应设为物理核心数,而非逻辑线程数。专家矩阵乘法受内存带宽限制,开启超线程往往更慢。 - 如果VRAM有余量,减小
--n-cpu-moe把部分专家层移回GPU。每移一层通常提升几个百分点的速度。
判断SSD是否成为瓶颈
当tokens/s低于预期时,第一步是检查磁盘是否在持续读取。Linux下:
**RSS 与页缓存并非同一回事**:`ps` 输出的 `rss`(Resident Set Size)只统计进程自身映射且当前驻留在物理内存的页,而通过 mmap 读入的模型数据也计入 RSS,因此 RSS 会随访问的专家变化而波动。不能简单认为“RSS 远小于模型大小就等于在颠簸”——需要结合 `maj_flt` 增速与 `iostat` 的 `rMB/s` 一起判断。`vmtouch -v` 则从文件视角统计每个模型文件的页有多少已被页缓存覆盖,是判断“工作集是否装得下”的最直接指标。若 `vmtouch` 报告常驻比例长期停在 40% 上下而 `iostat` 持续有大流量读取,基本可以确认 SSD 已成为瓶颈。
# 终端1:每秒监控NVMe读取吞吐量
iostat -xm 1 nvme0n1
# 终端2:统计llama-server的major page fault
PID=$(pgrep -f llama-server)
watch -n1 "ps -o maj_flt,rss -p $PID"
# 查看模型文件有多少已在页缓存中
# 需安装vmtouch: apt install vmtouch
vmtouch -v ~/models/gpt-oss-120b-Q4_K_M.gguf | tail -3
结果解读:
- iostat中
rMB/s在生成期间持续走高(数GB/s)意味着内存颠簸(thrashing)。工作集超过RAM,需要减小上下文、换更小的量化,或关掉Chrome。 maj_flt在头几十个token期间快速增长然后停止是正常的,那是预热阶段。vmtouch显示文件已驻留的比例。如果想在演示前预热缓存,对模型文件执行vmtouch -t。
macOS与Apple Silicon有所不同,统一内存模糊了VRAM与RAM的边界。此时只需关注总内存,用sudo fs_usage -f diskio | grep llama检查磁盘是否被持续读取。
结论
运行超过内存的大模型不是魔法。它只对MoE可行,且只在专家工作集能装进页缓存时才高效。可立即执行的行动:
- 硬件受限时优先选MoE而非稠密模型。16GB笔记本上跑30B-A3B,体验通常好过过度量化的14B稠密模型。
- 下载前先跑估算脚本,了解RAM与专家权重的比例。
- 使用
-ngl 99配合--n-cpu-moe(或-ot)让注意力层在GPU、专家在CPU。保持默认mmap,去掉--no-mmap和--mlock。 - 如果经常跑大模型,优先升级NVMe Gen4 SSD而非GPU。随机读取速度直接影响缓存未命中时的性能。
- 用
iostat和vmtouch测量而非猜测。遇到的大多数慢速问题都是颠簸而非CPU瓶颈。
Janas等项目正在用基于路由器的智能预取把这条路走得更远。但掌握mmap和张量放置的基础后,你已经可以在个人设备上运行一年前还需要服务器才能跑的模型。
相关推荐

“黑色十月”:AI挖出史上3%严重漏洞?
Reddit 热帖称过去一个月发现了史上 3% 的严重软件漏洞,被称为「黑色十月」。本文拆解这一说法的统计口径问题,分析 AI 辅助代码审计如何加速漏洞发现,以及开发者和企业该如何应对补丁窗口缩短的挑战。

微软Project Gecko:用AI为非洲农民架起语言桥梁
微软Project Gecko项目推出PharmaChat应用,让非洲农民用母语提问,通过AI返回精准视频片段和农耕书籍章节。项目还发布三个交互式手册,推动包容性AI发展。

Node.js语音笔记App:语音转文字API选型指南
为Node.js语音笔记应用选择语音转文字API?对比AssemblyAI、Deepgram、Google Cloud和Whisper的免费额度、定价与Node集成体验,给出分阶段选型建议。