共计 4284 个字符,预计需要花费 11 分钟才能阅读完成。
上周把手上这台 M2 Pro(12 核 CPU / 19 核 GPU / 32GB 统一内存)清出来当推理机用,目标很明确:跑一个能写代码、能读长文档的 7B~14B 量化模型,全程离线,接口兼容 OpenAI。折腾两天下来,Ollama 和 MLX 两条路都跑通了,也踩了几个不看文档根本想不到的坑——尤其是 macOS 默认的 GPU 显存上限,直接把 14B 模型顶到了 swap 里,速度掉到 3 tok/s。这篇把配置、数据和排障过程完整记下来。
一、痛点拆解:Mac 跑大模型的真实约束
Apple Silicon 的卖点是统一内存,CPU 和 GPU 共享同一块物理内存,理论上 32GB 机器能塞下 20GB 左右的模型权重。但“理论上”三个字后面藏着三层限制:
第一层是 wired memory 上限。macOS 默认允许 GPU 锁定的内存大约只有物理内存的 66%~75%,超过之后要么分配失败,要么触发频繁换页。跑 14B 的 Q4 模型(约 9GB 权重 + 长上下文 KV Cache)时,我就撞上了这条线,vm_stat 里 pageout 一路涨,风扇拉满,生成速度从 17 tok/s 掉到 3 tok/s。
第二层是 KV Cache 的隐性开销。 很多人只看模型文件大小,忽略了上下文长度带来的缓存膨胀。以 Qwen2.5-7B 为例,28 层、4 个 KV head、head_dim 128,fp16 下每 token 的 KV Cache 约 28KB,32K 上下文就是接近 0.9GB;14B 模型大约翻倍。开 128K 上下文时,这部分开销比模型本身还夸张。
第三层是散热。 笔记本形态的 M2 Pro 持续满载 10 分钟以上就会降频,短时 benchmark 的漂亮数字在长任务里并不成立。
二、Ollama 路线:三行命令背后的参数选择
Ollama 的优势是工程化程度高,安装即用,自带模型仓库和 OpenAI 兼容接口。我的安装和常驻配置:
# 安装(brew 版不带 GUI,适合当后台服务)brew install ollama
# 前台验证
ollama serve &
ollama pull qwen2.5-coder:7b-instruct-q4_K_M
# 检查是否走 Metal
OLLAMA_DEBUG=1 ollama serve 2>&1 | grep -i metal
# 期望输出 libggml-metal: found 1 Metal devices / recommendedMaxWorkingSetSize = 22906 MiB
真正决定体验的不是命令行,而是 Modelfile 里这四个参数:
FROM qwen2.5-coder:7b-instruct-q4_K_M
# 上下文窗口:默认 4096,按需放大,但每翻一倍 KV Cache 也翻倍
PARAMETER num_ctx 16384
# 并发请求数,个人用保持 1,超过会成倍吃内存
PARAMETER num_parallel 1
# 让模型在空闲后尽快释放显存(默认 5 分钟)PARAMETER keep_alive 10m
# 温度,写代码场景压低一点
PARAMETER temperature 0.2
# 关键:把尽量多的层卸载到 GPU,99 = 全部
PARAMETER num_gpu 99
执行 ollama create qwen-coder-16k -f Modelfile 之后,用 ollama run qwen-coder-16k 启动。这里有个容易忽略的点: 同一个模型的不同 num_ctx 会各占一份内存 ,我一开始同时开了 4K 和 32K 两个实例,内存直接吃满开始 swap。
常见误区:Q4_K_M 不是越小越好
我对比过 Q4_0、Q4_K_M、Q5_K_M、Q8_0 四档,同一段 200 行 Python 重构任务里,Q4_0 已经出现明显的 API 幻觉(编造不存在的库函数),Q4_K_M 基本可用,Q5_K_M 质量接近 Q8 但只多占 1.2GB。个人建议:7B 用 Q5_K_M,14B 用 Q4_K_M,这是显存和质量之间比较甜的点。
三、MLX 路线:Apple 自家后端到底快多少
MLX 是 Apple 机器学习团队出的数组框架,Metal 后端是一等公民,配合 mlx-community 下的预量化模型(通常 4bit group_size=64)直接可跑。安装很轻:
# 建议独立 venv,MLX 对 Python 版本有要求(3.9~3.12)python3.12 -m venv ~/venvs/mlx && source ~/venvs/mlx/bin/activate
pip install -U mlx-lm
# 一次性拉模型并生成
mlx_lm.generate \
--model mlx-community/Qwen2.5-7B-Instruct-4bit \
--prompt "用 Python 写一个带重试的 HTTP 客户端" \
--max-tokens 512 \
--temp 0.2
# 起一个 OpenAI 兼容服务(本地 8080)mlx_lm.server --model mlx-community/Qwen2.5-7B-Instruct-4bit --port 8080
mlx_lm.server 这个子命令知道的人不多,它实现了 /v1/chat/completions,可以直接把 Cherry Studio、Continue、Open WebUI 的 base_url 指过去,和 Ollama 的接口体验一致。缺点是并发能力弱, 它是单请求串行处理的 ,两个客户端同时打会排队,这点不如 Ollama。
关键一步:手动抬高 GPU 显存上限
这是全文最重要的一条命令。在跑 14B 及以上模型前,先确认当前上限:
sysctl iogpu.wired_limit_mb
# 输出 iogpu.wired_limit_mb: 0 表示走系统默认(约物理内存的 75%)# 32GB 机器临时调到 26GB(注意:不要超过物理内存,留够系统开销)sudo sysctl iogpu.wired_limit_mb=26624
我调到 26GB 之后,14B Q4 模型从“频繁 swap、3 tok/s”变成稳定的 15~17 tok/s。 但这个值重启失效 ,需要持久化的话写 launchd:
# ~/Library/LaunchAgents/local.iogpu.limit.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>local.iogpu.limit</string>
<key>ProgramArguments</key>
<array>
<string>/usr/sbin/sysctl</string>
<string>iogpu.wired_limit_mb=26624</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><false/>
</dict>
</plist>
然后 launchctl load ~/Library/LaunchAgents/local.iogpu.limit.plist。别超过物理内存的 85%,否则系统 UI 会出现掉帧和输入延迟。
四、实测数据:两套方案横向对比
测试条件统一:M2 Pro 32GB,电源接电,关闭其他占内存的应用,prompt 为 1200 字中文技术文档要求生成摘要,max_tokens=400,跑 3 次取中位数,每轮之间间隔 90 秒让机器降温。
模型 方案 生成速度 首字延迟 峰值内存
Qwen2.5-7B Q4_K_M Ollama 33 tok/s 0.6 s 7.1 GB
Qwen2.5-7B 4bit MLX 44 tok/s 0.4 s 6.4 GB
Qwen2.5-14B Q4_K_M Ollama 16 tok/s 1.4 s 12.8 GB
Qwen2.5-14B 4bit MLX 21 tok/s 1.0 s 11.9 GB
Qwen2.5-32B Q4_K_M Ollama 6 tok/s 4.2 s 22.1 GB(偶发 swap)
结论很清楚: 同等模型规格下 MLX 比 Ollama 快 25%~35%,首字延迟也低,原因是少了 GGUF → Metal 的算子转换开销,量化格式也更贴合 Apple 的矩阵乘法单元。但模型支持广度上 Ollama 碾压,社区里五花八门的 GGUF 微调模型它都能直接拉。
32B 这一档在 32GB 机器上属于极限操作,虽然靠 iogpu.wired_limit_mb 能塞进去,但系统整体会发卡,跑长任务时还会因为散热降频掉到 4 tok/s。 我的建议是 32GB 内存止步 14B,想玩 32B 至少上 64GB 的 M4 Pro/Max。
五、几个不看文档会踩的坑
1. Ollama 的默认上下文是 4096,不是你以为的 128K。 很多模型卡片写着支持 128K,但客户端不显式传 num_ctx 时会被截断,长文档总结任务会莫名丢内容。我排查了半小时才发现是参数问题,而不是模型能力问题。
2. 用 htop 看内存会误判。Mac 上要看 vm_stat 的 pageout 计数和 memory_pressure 输出,只要 pageout/s 持续大于 0,就说明在换页,速度必然崩。活动监视器里的“内存压力”图表更直观。
3. 笔记本合盖外接显示器时性能会打折。 部分型号在 clamshell 模式下 GPU 频率受限,我实测 7B 模型速度从 44 tok/s 掉到 36 tok/s。真要当推理机,还是开着盖子或者用 Mac mini / Studio。
4. 别用 brew services 跑 Ollama。 它默认继承的 PATH 和 HOME 有时会让模型目录解析异常,更稳的做法是写一个带 OLLAMA_MODELS 环境变量的 launchd plist,把模型统一放外置 SSD。
总结与选型建议
如果你的诉求是“一个接口、一个客户端、模型随便换”,用 Ollama,配好 num_ctx 和 num_parallel 就足够日常开发;如果你追速度、做批量推理脚本、或者想把模型嵌进自己的 Python 工具链,用 MLX,配合 mlx_lm.server 起服务,代码几乎不用改就能替换。
内存 16GB 的机器建议老实跑 3B~7B 的 Q4 模型,先把 iogpu.wired_limit_mb 调到 11GB 左右;24GB 可以上 7B 的 Q8;32GB 是 14B Q4 的舒适区;64GB 以上再考虑 32B 和 MoE 结构。最后一句实话: 本地小模型的价值不在“替代 GPT-4”,而在于隐私敏感任务、离线环境、以及高频低成本的结构化抽取 ——把它塞进你的自动化流水线,而不是当成聊天玩具,才算真正用起来。