你把 40 GB 的 GGUF 模型放在通过 NFS 挂载的 NAS 上(路径 /mnt/nas/models/),运行 ./llama-server -m /mnt/nas/models/llama-3.1-70b-Q4_K_M.gguf 时会遇到三种情况之一:进程立刻崩溃,报 mmap failed: Invalid argument 或 mmap failed: Operation not permitted;或者加载极慢(30-60 分钟,而本地 SSD 不到 30 秒);又或者模型能加载,但推理过程中随机读取会让服务卡住 5-30 秒,因为 NFS 客户端在等待缺页中断的 I/O。这三种现象的根源都是 llama.cpp 读取模型权重的方式,它默认假设文件在本地低延迟磁盘上。
最快的修复(先做这个): 在启动命令里加 --no-mmap。这会让 llama.cpp 改用普通的 read() 调用把文件读进内存,而不是做内存映射,从而绕开网络文件系统对 mmap 的限制。如果你用的是 2026 年初以后、启用了 DirectIO 的构建,再加上 --no-direct-io,因为 O_DIRECT 同样在网络挂载上失败,只是报错字符串略有不同:read error: Invalid argument。如果你是通过 Ollama 加载,设置环境变量 OLLAMA_NO_MMAP=1。最可靠的长期方案是把模型拷到本地 SSD 一次,然后从本地路径加载。
先对号入座
该用哪个参数,取决于你实际碰到的是哪种现象。截至 2026 年 6 月,llama.cpp 的默认加载方式仍然是 mmap(DirectIO 需要通过 -dio/--direct-io 显式开启),但部分 Windows 版 GGUF 发行包和一些调优脚本会把 DirectIO 打开,这会改变报错字符串,也改变修复方法。
| 现象 | 最可能的原因 | 第一步修复 |
|---|---|---|
加载时崩溃:mmap failed: Invalid argument / Operation not permitted | NFS/CIFS 拒绝 mmap(MAP_SHARED/MAP_PRIVATE) | --no-mmap |
加载时崩溃:read error: Invalid argument | 网络挂载不支持 DirectIO(O_DIRECT) | --no-direct-io(并加 --no-mmap) |
| 能加载但每个 token 要 5-30 秒 | 推理时 mmap 缺页中断要走网络取页 | --no-mmap 或拷到本地盘 |
| 加载“快得反常”、输出是乱码 | 加载途中网络断开,mmap 区域有清零的页 | 重新拷到本地盘;查 dmesg | grep nfs |
打开 .gguf 时 Permission denied | NFS root_squash 把你的 UID 映射成 nobody | 修导出/权限(见原因 6) |
常见原因
按命中率从高到低排列。
1. NFS 挂载不支持 mmap
NFSv3 以及部分 NFSv4 配置不支持客户端侧的 mmap(MAP_SHARED) 内存映射。当 llama.cpp 对文件描述符调用 mmap 时,内核返回 EINVAL 或 EPERM,进程立刻崩溃。这是协议层面的限制,不是权限问题。
怎么判断:先用 mount | grep nfs 确认文件系统类型,再试 python3 -c "import mmap, os; f=open('/mnt/nas/models/test.bin','rb'); m=mmap.mmap(f.fileno(),0,access=mmap.ACCESS_READ)"。如果抛出 OSError: [Errno 22] Invalid argument,说明这个挂载不支持 mmap。
2. SMB/CIFS 挂载被内核禁用 mmap
Linux 的 SMB 客户端(cifs)出于安全考虑,默认对非本地文件禁用 mmap。即便加上 cache=none 或 cache=strict,对 CIFS 挂载文件的 mmap 调用仍会返回 EINVAL。在 Windows 上对应的路径用的是对 UNC 路径(\\server\share\model.gguf)或映射盘符的 CreateFileMappingA,失败方式相同。
怎么判断:运行 mount | grep cifs。任何 CIFS/SMB 挂载都会导致 llama.cpp 的 mmap 失败,除非你显式加 --no-mmap。
3. 启用了 DirectIO(O_DIRECT),而网络挂载不支持
这个原因是 2026 年初才出现的新情况。llama.cpp 新增了一个绕过 mmap、用 O_DIRECT 读取的 DirectIO 加载器(-dio / --direct-io)。在默认的上游构建里它是关闭的,但一些预编译二进制和调优脚本会把它打开。网络文件系统通常不支持 O_DIRECT,于是加载会报 read error: Invalid argument,并且在失败之前日志里会有一行 direct I/O is enabled, disabling mmap。因为 DirectIO 已经把 mmap 关掉了,单加 --no-mmap 在这里没用——你必须把 DirectIO 关掉。
怎么判断:在启动日志里找 direct I/O is enabled, disabling mmap。如果报错字符串是 read error: Invalid argument(而不是 mmap failed: ...),那就是 DirectIO 引起的,用 --no-direct-io 修复。
4. 模型能加载,但推理时缺页中断造成数秒卡顿
即便 mmap 成功(某些 NFS 配置确实允许 mmap),每次前向传播都会读取模型权重的不同部分。在 NFS 上,每一次需要从服务端取页的缺页中断都会引入网络延迟。一个 70B 模型每次前向传播有数千次这样的缺页,于是每个 token 要花 5-30 秒,而不是毫秒级。
怎么判断:生成 token 时运行 vmstat 1。如果推理期间 si(换入/缺页)非零,说明模型权重没有完全常驻内存。
5. mlock 在大体积网络文件上被禁用或失败
llama.cpp 在映射模型权重后会用 mlock 把它们钉在内存里。在网络挂载上,mlock 可能对映射区间成功,但操作系统的 LRU 页回收策略在内存压力下仍会驱逐 NFS 页,导致长时间推理中缺页反复出现。
怎么判断:运行 ulimit -l——如果 mlock 上限(单位 KB)小于模型文件大小,mlock 就无法完整钉住模型。也可以查 /proc/sys/vm/nr_hugepages。
6. NFS 文件权限导致 open 调用失败
某些启用了 root_squash 的 NFS 配置会把 root 用户映射成 nobody,而 nobody 可能没有读模型文件的权限。如果 llama.cpp 以 root 身份运行(在 Docker 容器里很常见),加载模型时就会报权限错误。
怎么判断:用运行 llama.cpp 的那个用户执行 ls -la /mnt/nas/models/*.gguf。如果看到 “Permission denied”,那就是 NFS 权限映射的问题。
7. 加载途中网络中断导致 mmap 状态损坏
如果在模型加载(初始 mmap + 顺序读页阶段)期间到 NFS 服务端的连接断开,llama.cpp 可能会带着一个部分填充、含有零页的 mmap 区域继续运行。在清零的权重上做推理会产生乱码输出,而不是报错。
怎么判断:模型加载快得反常之后,运行 dmesg | grep nfs。如果看到超时或重连消息,模型可能是带着网络中断的页加载进来的。
最短修复路径
Step 1:关闭 mmap(必要时连同 DirectIO),改用普通读取加载
# --no-mmap 用普通 read() 调用把模型读进 RAM
#(初始加载更慢,但推理更快——不会在网络上反复缺页)
# 只有当你的构建/脚本启用了 DirectIO 时才需要 --no-direct-io
./llama-server \
-m /mnt/nas/models/llama-3.1-70b-Q4_K_M.gguf \
--no-mmap \
--no-direct-io \
--n-gpu-layers 80 \
--port 8080
加上 --no-mmap 后,llama.cpp 在加载时把文件顺序读进系统内存(千兆 NFS 上 40 GB 约需 30-120 秒),之后完全从内存运行,推理期间不再产生 NFS I/O。当前版本的参数名是稳定的:--mmap / --no-mmap(mmap 默认开启),以及 -dio / --direct-io / -ndio / --no-direct-io(DirectIO 在上游默认关闭)。
如果你改不了命令行——比如打包好的服务或第三方封装器——就用环境变量。下面这些都能关掉 llama.cpp 加载器的 mmap:
# llama.cpp:只要存在这个变量(任何值)就禁用 mmap
export LLAMA_ARG_NO_MMAP=1
# 或者显式设置:
export LLAMA_ARG_MMAP=false # 也接受 0、off、disabled
# Ollama(内部嵌了 llama.cpp):全局禁用 mmap
export OLLAMA_NO_MMAP=1
# 或者在 Modelfile 里按模型设置:
# PARAMETER use_mmap false
怎么确认修好了:服务应当走到 main: model loaded(或 llama_model_loader: loaded meta data)并开始监听端口,过程中不出现任何 mmap failed: 或 read error: 的行。如果还是崩溃,看清楚具体字符串,回到上面“先对号入座”的表里找对应那一行。
Step 2:先把模型缓存到本地 SSD 再加载
# 拷一次,之后从本地路径加载
rsync --progress \
/mnt/nas/models/llama-3.1-70b-Q4_K_M.gguf \
/home/$USER/models/llama-3.1-70b-Q4_K_M.gguf
./llama-server \
-m /home/$USER/models/llama-3.1-70b-Q4_K_M.gguf \
--n-gpu-layers 80 \
--port 8080
对于反复使用的场景,这是最快的方案。一个 40 GB 模型以 500 MB/s 从 NFS 拷到本地 NVMe 不到 90 秒,之后用 mmap 从本地盘加载不到 30 秒。
Step 3:如果只用 —no-mmap 还不够,调高 mlock 上限
# 查看当前 mlock 上限
ulimit -l
# 显示 "unlimited" 说明 mlock 不是瓶颈
# 显示数字则调高:
# 临时(当前会话)
ulimit -l unlimited
# 永久(写入 /etc/security/limits.conf)
echo "* soft memlock unlimited" | sudo tee -a /etc/security/limits.conf
echo "* hard memlock unlimited" | sudo tee -a /etc/security/limits.conf
Step 4:Docker 部署时,在容器里挂载本地路径而不是 NFS
# docker-compose.yml —— 挂本地 SSD 路径,不要挂 NFS
services:
llama:
image: ghcr.io/ggml-org/llama.cpp:server
volumes:
- /home/user/models:/models # 本地路径,不是 /mnt/nas
command: >
-m /models/llama-3.1-70b-Q4_K_M.gguf
--no-mmap
--n-gpu-layers 80
--port 8080
ports:
- "8080:8080"
bind mount 改变不了这一点:如果宿主机那个目录本身就是 NFS 或 CIFS 挂载,容器里看到的还是同一个网络文件系统,mmap/O_DIRECT 照样会按同样的方式失败。要么挂一个真正本地的宿主机路径,要么在命令里保留 --no-mmap。
Step 5:如果必须用 NFS,加上 async 和 rsize 挂载选项
# NFS 共享的 /etc/fstab 条目
nas:/models /mnt/nas/models nfs \
rw,soft,async,rsize=1048576,wsize=1048576,timeo=600,retrans=5 0 0
# 重新挂载
sudo mount -o remount /mnt/nas/models
# 验证 rsize
mount | grep nas | grep rsize
rsize=1048576(1 MB 读取块大小)能在用 --no-mmap 做初始加载时把顺序读吞吐拉到最大。
预防建议
- 只要在意性能,就把 GGUF 模型文件放在本地 NVMe 或 SSD 上——NFS 是网络文件系统,不是本地存储的替代品。
- 当模型路径在任何网络文件系统(NFS、CIFS、sshfs)上时,启动脚本里始终带上
--no-mmap(DirectIO 开着时再加--no-direct-io)。在服务环境里设一次LLAMA_ARG_NO_MMAP=1就能覆盖所有调用。 - 给模型存储买 NAS 时,也要预留本地 SSD 缓存——使用前先把模型拷到本地盘。
- 在启动脚本里加一个预检,启动前先确认模型文件在本地文件系统上:
FSTYPE=$(stat -f -c %T "$MODEL_PATH" 2>/dev/null || stat -f "$MODEL_PATH" | grep 'Type:' | awk '{print $NF}')
if echo "$FSTYPE" | grep -qiE "nfs|cifs|fuse"; then
echo "Warning: model on network filesystem — using --no-mmap --no-direct-io"
EXTRA_FLAGS="--no-mmap --no-direct-io"
fi
- 团队共用 NAS 时,配一个每晚跑的
rsync任务,把常用模型预先缓存到每台工作站的本地盘上。 - 监控
dmesg | grep nfs的超时/重连消息,它们提示长时间推理中 NFS 不稳定。 - 如果 mmap 必须在网络上工作(例如内存很小的机器),用 NFSv4.1 配 pNFS 加
cache=fscache做本地页缓存。
常见问答 (FAQ)
Q: 我加了 --no-mmap,结果反而报 read error: Invalid argument,发生了什么?
A: 这个字符串说明 DirectIO(O_DIRECT)是开着的,而网络挂载不支持它。启动日志里通常会在失败前显示 direct I/O is enabled, disabling mmap,所以单加 --no-mmap 救不了,要加 --no-direct-io。在 Windows 上你可能反而看到 read error: An attempt was made to move the file pointer before the beginning of the file,那是某些构建上 --no-mmap 的另一个回归 bug;这种情况建议把模型拷到本地盘符。
Q: 模型在 NFS 上时 --mlock 有用吗?
A: --mlock 对 mmap 出来的区域调用 mlock,防止页被驱逐。但在 NFS 上,即便是被锁定的页,首次访问时仍要从网络取——--mlock 只能防止初次缺页之后再被驱逐。--no-mmap 更可靠,因为它在加载时就把所有数据一次性读进 RAM,根本不走缺页这条路。
Q: 能在 NFS 和 llama.cpp 之间用 RAM 盘做一层中转吗?
A: 可以。建一个 tmpfs 挂载(sudo mount -t tmpfs -o size=50G tmpfs /mnt/ramdisk),把模型拷过去(cp /mnt/nas/models/model.gguf /mnt/ramdisk/),再从 /mnt/ramdisk/ 加载。这样既给了 mmap 它需要的本地低延迟文件系统,又把主副本留在 NFS 上。
Q: 我的模型在本地 ext4 SSD 上,但还是报 mmap 错误,为什么?
A: 检查文件系统是不是用 noexec 挂载的,或者模型文件对运行用户缺少读权限。也要看磁盘剩余空间——如果卷已经 100% 满,mmap 写入(模型转换时会用到)会失败。运行 df -h /path/to/model 和 ls -la model.gguf。
Q: LM Studio 也有同样的 NFS mmap 问题吗?
A: 有。LM Studio 用的是同一个 llama.cpp 后端,修复方法相同:把模型拷到本地存储。LM Studio 的界面里没有 --no-mmap 开关,所以实际上唯一可行的办法就是把模型放在本地盘。
相关阅读
- llama.cpp 换更激进量化后质量明显下降
- LM Studio 加载模型时报 out of memory
- 本地模型冷启动后首 token 极慢
- Ollama 下载模型卡住或进度回退
- 本地 RAG 索引重建慢到无法忍受
标签: #local-llm #llama-cpp #排查