Apple Silicon 内存优化指南

在 512GB 统一内存上跑 456B 参数模型的内存管理策略


统一内存架构

Apple Silicon 的核心优势 —— CPU 和 GPU 共享同一块物理内存,零拷贝。但这也意味着:

code
┌────────────────── 512GB 统一内存 ──────────────────┐
│                                                     │
│   CPU 使用    GPU 使用    共享区域                    │
│   ├─ macOS   ├─ Metal    ├─ 模型权重 (mmap)         │
│   ├─ Apps    ├─ Buffers  ├─ KV Cache                │
│   └─ ...     └─ ...     └─ ...                     │
│                                                     │
│   ⚠️ 一方吃太多 → 全部遭殃                           │
└─────────────────────────────────────────────────────┘

GPU 可用上限:约为总内存的 75%,即 ~384GB。超过这个值,系统会回退到 CPU + SSD swap,性能骤降 200 倍。


vm_stat 解读

macOS 的内存分为四种状态:

code
vm_stat
# Pages free:        xxx
# Pages active:      xxx
# Pages inactive:    xxx
# Pages wired down:  xxx
状态含义大模型场景
Wired内核 + Metal GPU buffer,不可换出模型权重的 GPU 副本 (~234GB for 8-bit)
Active正在使用的内存页应用程序 + 推理计算
Inactive最近未使用的 file-backed 缓存mmap 映射的模型文件
Free完全空闲可用于新分配

关键公式

code
Available = Free + Inactive

Inactive 内存可以被立即回收,所以 Available 才是真正可用的内存。不要只看 Free。

M2.5 8-bit 稳态参考

状态数值
Wired~234 GB(模型 GPU buffer)
Active~20 GB(系统 + 应用)
Inactive变化(mmap 缓存)
Free~240 GB

Wired vs Active vs Inactive

Wired 内存 —— 最重要的指标

Wired 内存 = 内核 + Metal GPU buffer。加载大模型后,Wired 会暴涨:

code
模型磁盘大小:    237 GB (safetensors 文件)
                  ↓
mx.load() mmap:  ~237 GB → Inactive(虚拟映射,按需加载)
                  ↓
Metal GPU buffer: ~234 GB → Wired(不可换出!)

为什么磁盘 237GB 的模型占 234GB Wired?

因为 Metal 需要将权重拷贝到 GPU buffer 用于计算。这部分内存被锁定(wired),macOS 无法将其换出到 swap。

Inactive 不是浪费

mx.load() 使用 mmap 映射模型文件。这些页面在 Inactive 状态,但不是浪费 —— 它们是文件缓存,可以被立即回收给其他用途。

监控命令

code
# 快速查看
memory_pressure
 
# 详细状态
vm_stat | head -10
 
# 实时监控(NeoWatch)
# 访问 http://localhost:3939

Metal GPU Buffer

为什么大模型占这么多 GPU 内存?

MLX 将模型权重加载到 Metal GPU buffer 进行推理:

code
safetensors 文件 → mmap 映射 → Metal buffer (GPU)
                                    ↓
                              推理计算在这里进行
                              (矩阵乘法、attention)

Metal buffer 特点:

  • 分配在统一内存中,但被标记为 GPU 独占
  • 属于 Wired 内存,不可被 swap 或压缩
  • 只有进程退出后才会释放

释放时序

这是导致 OOM Panic 的根本原因:

code
pkill mlx_lm.server
    ↓ 进程收到信号
    ↓ Python 开始清理
    ↓ Metal buffer 逐步释放 (10-30 秒)
    ↓ mmap 页面回收 (几秒)
    ↓ 内存完全释放

关键pkill 之后必须等待进程完全退出(轮询 pgrep),不能只 sleep 2


OOM 预防策略

1. 不要同时运行两个大模型

code
M2.5 8-bit:  270 GB 运行时
M2.5 4-bit:  150 GB 运行时
两者同时:    420 GB → 超出 512GB → Panic

使用 ai-switch 切换模型,它会:

  1. launchctl unload 停止旧服务
  2. 轮询等待进程完全退出
  3. 检查内存空闲百分比
  4. 更新 plist 模型路径
  5. launchctl load 启动新服务

2. 设置 KV Cache 上限

长对话的 KV cache 会持续膨胀。oMLX 的 SSD Paged Cache 解决了这个问题:

  • KV cache 自动持久化到 SSD
  • prefix 复用减少重复计算
  • 内存不够时自动驱逐

3. LaunchAgent ThrottleInterval

code
<key>ThrottleInterval</key>
<integer>60</integer>

崩溃后至少等 60 秒再重启。防止崩溃循环把内存压爆。

4. 内存压力监控

code
# 检查内存压力
memory_pressure
 
# 结果解读
# "The system has X free percentage"
# < 30% → 危险,不要启动新服务
# 30-50% → 正常
# > 50% → 充足

量化策略选择

量化磁盘运行时内存速度质量损失推荐
8-bit237 GB~270 GB~25 tok/s基线日常使用
4-bit120 GB~150 GB~51 tok/s<1%多任务 / 快速迭代
3-bit~85 GB~110 GB~51 tok/s3-5%不推荐

M2.5 是 MoE 架构(456B 总参数,45.9B 激活)。MoE 对量化的敏感度比 dense 模型低,因为大部分 expert 在每次推理中不被激活。4-bit 是最佳平衡点。

进阶:Mixed Precision

对不同层使用不同量化精度:

code
# 概念示例
def mixed_quant(name, module):
    if "embedding" in name:
        return {"q_bits": 8}    # 高精度
    elif "experts" in name:
        return {"q_bits": 3}    # 激进量化
    else:
        return {"q_bits": 4}    # 默认

效果:从 230GB 进一步降到 180-200GB。但这是实验性的,日常不需要。


NeoWatch 内存监控

NeoWatch 是 AIOS 的监控仪表盘,实时追踪内存状态:

code
访问: http://localhost:3939

监控项:

  • 统一内存使用(Wired / Active / Inactive / Free)
  • GPU Metal buffer 大小
  • swap 使用量(应该始终为 0)
  • oMLX 推理速度(tok/s)
  • 内存压力阈值告警(通过 Telegram 推送)

告警阈值

指标警告危险
Free 内存< 50 GB< 20 GB
Wired 内存> 300 GB> 400 GB
Swap 使用> 0> 1 GB

常见问题

Q: 为什么 Activity Monitor 显示的内存和 vm_stat 不一样?

Activity Monitor 的 "Memory Used" 包含了 Inactive,实际可用内存 = Free + Inactive。用 memory_pressure 看更准。

Q: 8-bit 和 4-bit 之间切换安全吗?

安全,但必须用 ai-switch。它会确保旧进程完全退出、内存完全释放后才启动新模型。

Q: 如果看到 swap 使用量增加怎么办?

说明内存不够了。检查是否有其他大型进程在运行。如果是模型本身导致的,考虑切换到 4-bit。

Q: Metal GPU Hang 怎么办?

GPU Hang 是 Metal 驱动层面的问题,和内存优化无关。唯一解决方案是重启机器。表现为 tok/s 骤降或推理卡死。


统一内存是双刃剑 —— 省去了 CPU↔GPU 数据传输,但也意味着一方出问题全部遭殃。监控和预防比事后修复更重要。