随笔
在 48GB Mac mini 上跑 27B 大模型:量化、KV 缓存与上下文的工程取舍
48GB Mac mini 本地部署 27B 模型的实测记录:量化方案、KV 缓存推导、统一内存 OOM 与上下文取舍。
浏览统计暂不可用
本文目录 8
在 48GB 统一内存的 Mac mini 上跑 27B 量级的本地模型,成本最低,坑也最多。这篇记录的不是"能跑通",而是为什么这样配、为什么那样会崩——把量化、KV 缓存、统一内存三个子系统拆到参数级。
结论速览
| 决策点 | 结论 | 原因 |
|---|---|---|
| 模型规模 | 27B Q4 | 权重 16GB 留出 32GB 给 KV 与激活 |
| 量化方案 | oQ4 类 4-bit(含异常值通道) | 纯 4-bit 在长文本质量上劣化,异常值通道是折中 |
| KV 缓存 | 4-bit | 每 token 开销减半,长上下文救命 |
| 上下文 | 32K 封顶 | KV 膨胀 + 激活峰值,64K 不可行 |
| MTP | 开启 | 多 token 预测头,花 ~2GB 换 30%+ 吞吐 |
| 定位 | 辅助/回退模型 | 64K 硬门槛决定它不能当主模型 |
1. 模型选型:27B 是甜点,但“Q4”不是一回事
权重内存的构成(以 4-bit + 异常值通道 + MTP 方案为例):
总权重 ≈ 主干参数(4-bit) + Embedding(通常保持 16-bit) + MTP 头(4-bit) + 输出头(16-bit)27B 参数若全部压到 4-bit:27B × 0.5B ≈ 13.5GB。但工程上不会这么干——Embedding 和输出头对精度敏感,通常保持高位,主干里也会有少量异常值通道保持高位。最终落到 16GB 左右,是"可用精度"和"内存预算"的平衡点。
量化方案对比:
| 方案 | 权重占用 | 精度 | 速度 | 结论 |
|---|---|---|---|---|
| FP16 | ~54GB | 基准 | 最慢 | 48GB 机器不可行 |
| INT8 | ~27GB | 几乎无损 | 中 | 上下文预算被压缩 |
| 纯 4-bit(NF4 类) | ~16GB | 长文本偶发质量劣化 | 快 | 可用但不够稳 |
| 4-bit + 异常值通道(oQ4 类) | ~16GB | 接近 8-bit | 快 | 推荐 |
4-bit 的核心矛盾:权重矩阵里存在少量"异常值"通道,数值幅度远大于其他通道,一刀切压 4-bit 会让这些通道量化误差爆炸。oQ4 类方案的思路是把这些通道单独识别出来保持高位——代价是几个百分比的额外内存,换来长文本质量的明显稳定。
MTP(多 token 预测)头:不是"预测下一个词",而是并行预测未来若干个 token,再用小模型逐级校验接受。吞吐提升通常 30–50%,代价是一份小头参数 + 校验计算,总内存开销约 2GB。对 48GB 机器,这笔交易值得做。
2. KV 缓存:上下文真正的成本中心
自回归生成时,每个 token 的注意力 Key/Value 都要缓存,KV 缓存随上下文线性增长——这是长上下文最大的隐性成本。
KV 每 token 开销公式:
KV/token = 2 × 层数 × KV头数 × head_dim × 每元素字节数以典型 GQA 架构为例(48 层、8 个 KV 头、head_dim 128,代入你所用模型 config.json 的实际值):
| 缓存位宽 | 每 token | 8K 上下文 | 32K 上下文 | 64K 上下文 |
|---|---|---|---|---|
| FP16 | ~192KB | 1.5GB | 6.3GB | 12.6GB |
| 4-bit | ~96KB | 0.75GB | 3.2GB | 6.3GB |
GQA(分组查询注意力)是关键。 8 个 KV 头对 48 个查询头,KV 开销是全注意力的 1/6。如果没有 GQA,32K 上下文的 KV 缓存会直接翻倍到 12.6GB,这台机器基本告别长上下文。
由此看 64K 为什么不可行:
- KV 64K × 4-bit ≈ 6.3GB(理论可接受)
- 但峰值激活值、生成时的临时缓冲、以及 Metal 堆的碎片,会让实际峰值远超线性估算
- 在
wired_limit约束下,长上下文推理触发 Metal OOM 是必然,不是偶然
KV 4-bit 的精度代价:小数位被压掉后,长程注意力的细微排序可能出错。本地工具场景(代码补全、摘要、问答)影响可忽略;但涉及长文档精确定位的任务,需要实测验证。这也是"先用、再验"的原因。
3. 统一内存:为什么“内存还有剩”也会 OOM
Apple Silicon 是统一内存——CPU 和 GPU 共享同一块物理内存,但被划分为不同堆区:
- CPU 堆:进程地址空间,权重加载、中间张量走这里
- Metal GPU 堆:推理计算与 KV 缓存的主战场,受
iogpu.wired_limit_mb约束 - wired 限制:GPU 可占用的"已钉住"内存上限
关键坑:Activity Monitor 里显示"内存充足"不代表 GPU 堆没满。GPU 堆的可用空间由 wired 限制和已分配缓冲共同决定——OOM 常常发生在"总内存还有富余"的时候。
排障顺序:
- 看日志
gateway.error.log中的Metal OOM行 - 确认 KV 位宽、上下文长度是否超过预算
- 检查
model_settings.json是否生效(改完必须重启服务——KV 缓存分配器在加载时初始化,热改无效) - 调整
iogpu.wired_limit_mb前先确认这是"限制"而非"瓶颈"——曾提到 45219,重启后失效,说明该参数并非每次启动都持久化,需要逐次验证
4. 上下文长度:32768 是怎么来的
模型本身支持远大于 32K 的上下文,但实际交付被三重约束卡在 32768:
- 显存预算:见第 2 节,KV 线性膨胀 + 激活峰值
- 服务端配置:
model_settings.json中的上下文上限,是保护性封顶 - 工具链门槛:Agent 框架主模型要求 64K 起步,32K 直接不满足,只能降级为辅助/回退
配置层的正确姿势:按 provider 设上下文,而不是全局一个值。 全局 context_length 一个值会卡死所有模型——云端大窗口模型被压小浪费预算,本地小窗口模型被拉大直接 OOM。正确做法是每个 provider 独立配置(如 xfyun 给 262144、omlx 给 32768),框架在路由时按模型能力分配任务。
与 prompt caching 的关系:KV 缓存同时也是 prompt caching 的载体。前缀稳定时,重复 prompt 的 KV 可以直接命中复用,省掉重复 prefill 计算。这要求请求侧保持前缀结构稳定——这也是为什么"上下文工程"(预算、卸载、压缩)和 KV 量化是同一枚硬币的两面,而非两个话题。
5. 排障手册
| 症状 | 排查路径 |
|---|---|
| 生成突然中断、报错 | 看 gateway.error.log 的 Model fallback 与 Metal OOM |
| 输出明显变慢 | 上下文接近上限时 KV 膨胀,检查 agent.log 每轮实际上下文 |
| 配置改了没生效 | ~/.omlx/bin/omlx restart,确认服务重启后再测 |
| 长 prompt 直接崩 | 降上下文封顶,或临时降 KV 位宽验证 |
| 内存够却 OOM | 查 iogpu.wired_limit_mb 与 GPU 堆实际占用,别信系统总内存 |
6. 给 48GB 机器的推荐配置
| 项目 | 推荐值 | 说明 |
|---|---|---|
| 模型 | 27B Q4(4-bit + 异常值通道 + MTP) | 权重 16GB,留 32GB 给 KV 与激活 |
| 量化 | 4-bit + 异常值通道 | 精度与内存的折中 |
| KV 缓存 | 4-bit | 32K 上下文从 6.3GB 降到 3.2GB |
| 上下文封顶 | 32K | 务实上限,不要追 64K |
| MTP | 开启 | 吞吐 30%+ 提升,成本 ~2GB |
| 配置生效 | 改完必须 restart | model_settings.json 热改无效 |
| 角色定位 | 辅助/回退模型 | 主模型留给 64K+ 的云端方案 |
7. 总结
48GB Mac mini 跑本地 27B 模型,本质是在三个子系统之间做预算分配:
- 量化预算:权重压到 16GB,用异常值通道保住精度
- KV 预算:4-bit + GQA 让 32K 上下文落在 3.2GB,64K 则翻倍到 6.3GB 并触发 OOM
- 内存预算:统一内存不是无限池,GPU 堆受 wired 限制,OOM 与总内存余量无关
把这套参数配好、重启生效、按 provider 设上下文,本地推理就能稳定服务于编码辅助与敏感数据处理。剩下的——更大的上下文、更高的吞吐——交给更大的内存,或云端。
本地模型的意义不是替代云端,而是补上云端给不了的那一格:隐私、离线、可控。
写给未来的自己合上文章前,留下一点自己的理解。
你的私人学习便签,只存在当前浏览器,不会上传或公开。清理浏览器数据会丢失,请导出留存。三种语言共用这篇文章的便签。
接下来,走哪条路?
这篇文章暂时没有对应的已发布关联内容。 回到学习路线选择下一步 →
依据文章关联与同主题已发布内容整理,不使用随机推荐。