标签:macOS Sequoia、vLLM-Metal、Apple Silicon、Metal LLM、launchd、Too many open files 适用:Mac M 系列、macOS 15 Sequoia、SIP 默认开启(不建议关 SIP)
前言
最近在 macOS 15 Sequoia 上使用 vLLM-Metal 本地部署大模型,高并发压测时频繁遇到 Too many open files。 很多人会沿用旧 macOS(Sierra ~ Monterey)的老办法:写 /Library/LaunchDaemons 全局 maxfiles plist,但是这套方案在 Sonoma / Sequoia 已经彻底失效。
macOS 15 开始,在 SIP 开启状态下,Apple 在内核层面做了保护:禁止 root 通过 launchctl limit 修改用户会话全局文件句柄硬上限,常规全局配置无法突破 2048。 而 vLLM-Metal 作为本地推理服务,多并发请求、prefill、网络连接、缓存文件都会快速消耗文件描述符,很容易撞上这个限制。
先划重点:不要关闭 SIP 去绕开限制,安全代价很高;优先使用进程单独资源限制方案。
背景:macOS 的文件句柄两套体系
Linux 习惯用 /etc/security/limits.conf,但是 macOS 很早就不识别这个文件。
- macOS 10.12 Sierra 到 Monterey:可以通过全局 launchdaemon 执行
launchctl limit maxfiles全局调高句柄 - macOS 14 Sonoma / 15 Sequoia(SIP 开启):
sudo launchctl limit maxfiles xxx直接报错
Could not set resource limits: 150: Operation not permitted while System Integrity Protection is engaged
两个容易混淆的内核参数,SIP 开启时同样无法修改:
sysctl kern.maxfiles
sysctl kern.maxfilesperproc
kern.maxfiles 是全系统总 vnode 池;launchctl limit maxfiles 是进程资源上限,二者互相独立,不要混为一谈。
查看当前限制的命令:
# 查看 launchd 会话限制
launchctl limit maxfiles
# 当前 shell 的 ulimit
ulimit -Sn
ulimit -Hn
方案选型(按推荐优先级)
方案一:临时调试(适合手动跑 vllm serve)
最简单,只对当前终端以及该终端启动的子进程生效,重启、新开终端、GUI 拉起的程序不继承。
ulimit -n 65536
# 再启动 vLLM-Metal
vllm serve Qwen/Qwen3.5-0.8B --gpu-memory-utilization 0.6
如果想每次打开终端自动设置,写入 zsh 配置:
echo "ulimit -n 65536" >> ~/.zshrc
source ~/.zshrc
局限:不适合后台常驻服务,通过 launchd 自动拉起的 vLLM 不会继承这个 ulimit。
方案二:LaunchAgent 单独给 vLLM-Metal 设置资源上限(推荐,SIP 无需关闭)
macOS 15 官方支持:在单个服务 plist 内部声明 SoftResourceLimits / HardResourceLimits,仅对这个进程生效,不改动系统全局策略。 适合长期后台常驻、开机自启 vLLM 推理服务。
文件路径:~/Library/LaunchAgents/com.vllm-metal.serve.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.vllm-metal.serve</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/vllm</string>
<string>serve</string>
<string>Qwen/Qwen3.5-0.8B</string>
<string>--gpu-memory-utilization</string>
<string>0.6</string>
<string>--distributed-executor-backend</string>
<string>uni</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
</array>
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>65536</integer>
</dict>
<key>HardResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>200000</integer>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
加载与卸载命令:
# 设置权限
chmod 644 ~/Library/LaunchAgents/com.vllm-metal.serve.plist
# 加载服务
launchctl load ~/Library/LaunchAgents/com.vllm-metal.serve.plist
# 修改配置前先卸载
# launchctl unload ~/Library/LaunchAgents/com.vllm-metal.serve.plist
说明:
~/Library/LaunchAgents:当前用户代理,不需要 root;如果是系统级服务放到/Library/LaunchDaemons--distributed-executor-backend uni:macOS vLLM-Metal 推荐,避开 Ray 多进程带来更多 fd、内存不稳定问题--gpu-memory-utilizationM 系列统一内存建议 0.5~0.7,预留内存给系统、Metal 驱动、内核编译缓存
方案三:关闭 SIP(强烈不推荐)
关闭 SIP 之后,老式全局 com.limit.maxfiles.plist 方案才会恢复可用。 Apple Silicon 需要进入恢复模式执行 csrutil disable。 风险:系统完整性保护、系统签名校验全部失效,整机安全边界大幅下降,能不碰就不碰。
方案四:Docker / OrbStack 容器场景
容器内部 ulimit 由虚拟化层控制,不受 macOS 主机 2048 限制,在 compose 里直接配置:
services:
llm:
ulimits:
nofile:
soft: 65536
hard: 200000
验证与测试
查看 vLLM 进程的文件句柄限制:
# 找到pid
ps aux | grep vllm
# 查看进程资源限制
launchctl procinfo <你的pid> | grep NumberOfFiles
OpenAI 兼容接口测试:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3.5-0.8B",
"messages": [{"role":"user","content":"你好"}]
}'
vLLM-Metal 在 macOS15 的额外避坑清单
python3 -c "import platform; print(platform.machine())"
# 输出 arm64 才正确
- 模型下载慢,启用 HF 镜像:
export HF_ENDPOINT=https://hf-mirror.com
- 并发太高依旧容易爆 fd:可以用
--max-concurrent-batches控制推理并发,从业务侧降低文件描述符压力 - 老全局 plist 不要再尝试:
<!-- 这个在macOS15 SIP开启下无效,仅作历史参考 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.limit.maxfiles</string>
<key>ProgramArguments</key>
<array>
<string>launchctl</string>
<string>limit</string>
<string>maxfiles</string>
<string>65536</string>
<string>200000</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>ServiceIPC</key>
<false/>
</dict>
</plist>
小结
macOS 15 Sequoia + SIP 开启跑 vLLM-Metal:
- 抛弃旧的全局 maxfiles launchdaemon 方案,内核层面已经拦截;
- 手动调试:先
ulimit -n 65536再启动服务; - 长期后台部署:使用带
SoftResourceLimits / HardResourceLimits的 LaunchAgent,给 vLLM 进程单独放开文件句柄上限,是当前最稳妥、符合系统策略的方案; - 优先
uni后端,少用 Ray,降低并发压力,减少 fd 与内存异常。