在Mac上跑大模型,MLX 不是终点

21 阅读14分钟

在Mac上跑大模型,MLX 不是终点

Apple Silicon 让 Mac 成为唯一能在消费级硬件上流畅运行 72B 参数大模型的平台。MLX 作为 Apple 官方机器学习框架,提供了优雅的推理管线——但它只用了 Apple Silicon 一半的算力。

问题出在量化策略上。MLX 内置的 W4A16/W8A16 方案只量化权重,计算仍然走 FP16 路径。而从 M5 系列开始,Apple Silicon 引入了 INT8 TensorOps 专用计算单元——这块硬件算力在 MLX 生态中完全处于闲置状态。

明略科技开源的 Cider SDK 正是为填补这一空白而设计:在 MLX 框架之上实现 W8A8 激活量化,打通 INT8 TensorOps 计算路径,将 prefill 阶段速度提升 1.4–2.2 倍。Cider 最初为明略科技的 Mano-P 项目(端侧 GUI-VLA Agent)开发,但其设计完全通用,适用于所有基于 MLX 的模型。

本文从硬件架构出发,逐层拆解 MLX 量化的结构性限制、激活量化的技术难点、以及 Cider SDK 的内核实现,最后用实测数据验证端侧大模型推理的性能上限。

Apple Silicon 统一内存架构:为什么 Mac 天然适合跑 LLM

大模型推理的核心瓶颈是内存带宽。一个 70B 参数模型在 FP16 下占 140GB,即使量化到 INT4 也需要 35GB。在传统 PC 架构中,GPU 显存和系统内存物理隔离,模型要么完全装进显存(受限于 24GB),要么走 PCIe 总线在 CPU/GPU 间搬运数据——PCIe 4.0 x16 的双向带宽仅 32 GB/s。

Apple Silicon 的 Unified Memory Architecture(UMA)从根本上消除了这个瓶颈:

  • 物理统一:CPU、GPU、Neural Engine 共享同一块物理内存,无需数据搬运
  • 大容量:M5 Pro 最高 64GB,M5 Max 最高 192GB,M5 Ultra 可达 512GB——70B 模型完整装入成为可能
  • 高带宽:M5 Pro 提供 307 GB/s 内存带宽,M5 Max 达 614 GB/s,远超 DDR5 台式机(~90 GB/s)

这意味着在 decode 阶段(memory-bound),Apple Silicon 拥有结构性优势:无需担心显存溢出,也无需承受 CPU-GPU 数据搬运的延迟。M5 Pro 的 307 GB/s 带宽意味着一个 35GB 的 INT4 模型理论上可以达到 ~114 ms/token 的加载速度,对应约 70 tok/s 的 decode 上限——实测数据(80 tok/s)表明 MLX 的内存访问优化已经接近硬件极限。

但 prefill 阶段是另一回事。Prefill 是 compute-bound:需要对整段输入序列做一次完整的矩阵乘法。此时决定性能的不是内存带宽,而是 GPU 的算力(FLOPS)。这正是 INT8 TensorOps 的用武之地——如果能把 FP16 GEMM 替换为 INT8 GEMM,理论上可以获得 2x 的计算吞吐提升。

MLX 的权重量化:做对了什么,缺了什么

W4A16/W8A16 的工作原理

MLX 内置的量化方案属于 Weight-Only Quantization(仅权重量化)。以 W4A16 为例,其推理流程为:

离线阶段:
  FP16 权重 → per-group 非对称量化 → INT4 权重 + scale + zero_point

推理阶段(每层 Linear):
  1. 从内存加载 INT4 权重(体积为 FP16 的 1/42. Dequantize:INT4 → FP16(乘 scale,加 zero_point)
  3. FP16 激活 × FP16 权重 → FP16 输出(标准 FP16 GEMM)

核心设计意图是用 INT4 存储换内存带宽:权重在内存中以 INT4 格式存放,搬运到 GPU 时数据量仅为 FP16 的 1/4,但实际计算仍然发生在 FP16 精度上。Dequantize 操作被融合进 GEMM kernel 的数据加载阶段,几乎零开销。

这套方案的收益与天花板

收益明显

  • 显存占用大幅下降(8B 模型从 16GB 降到 4GB)
  • Decode 速度提升(权重搬运量减少 → memory-bound 阶段受益)
  • 精度损失可控(W4A16 在大多数模型上 PPL 增长 < 0.5)

但存在结构性天花板

Prefill 阶段是 compute-bound,此时性能瓶颈不在"权重搬多快",而在"矩阵乘法算多快"。W4A16 的 FP16 GEMM 路径在 prefill 阶段无法突破 FP16 算力上限——即使权重只有 4-bit,计算仍然要展开到 FP16 做乘加。

M5 系列的 INT8 TensorOps 提供了比 FP16 GEMM 更高的峰值吞吐。但 MLX 的量化实现没有暴露 INT8 计算路径——dequantize-to-FP16-then-compute 的架构决策使其无法利用整数计算单元。

这不是 MLX 的 bug,而是一个设计取舍:MLX 选择了更通用的方案(所有 Apple Silicon 都支持 FP16 GEMM),而没有针对 M5 的新硬件特性做专项优化。Cider 的定位正是填补这一空白。

激活量化:为什么比权重量化难一个量级

权重是静态的,激活是动态的

权重量化之所以简单,是因为权重在推理时不变——可以离线统计分布、精心选择量化参数,甚至做 calibration 微调。量化一次,终身使用。

激活值则完全不同:

  • 数据依赖:每个输入 token 产生不同的激活分布
  • 逐层变化:同一输入在模型不同层的激活范围差异巨大
  • 动态范围大:某些 channel 的激活值可能是其他 channel 的 100 倍以上

这意味着激活量化必须在线完成——每次前向传播都要实时计算量化参数,且必须足够快,不能成为新的瓶颈。

Outlier 问题:激活量化的核心难点

大模型的激活值存在著名的 "outlier" 现象(Dettmers et al., 2022):少量 channel 的激活值异常大(magnitude 可达其他 channel 的 10–100 倍),而绝大多数 channel 的激活值集中在很小的范围内。

如果用 per-tensor 对称量化(整个激活矩阵共享一个 scale),那么 scale 会被 outlier 主导,导致正常 channel 的有效精度从 8-bit 退化到 4–5 bit——信息损失严重。

解决方案有两个方向:

方向一:per-channel / per-group 量化

为每个 channel(或每组 channel)计算独立的 scale,将 outlier 的影响限制在局部:

per-tensor:   scale = max(|X|) / 127          → outlier 污染全局
per-channel:  scale_c = max(|X[:, c]|) / 127  → 每列独立,互不影响
per-group:    scale_g = max(|X[:, g:g+gs]|) / 127  → 粒度介于两者之间

粒度越细,精度越高,但计算开销也越大(更多 scale 参数需要存取和参与计算)。

方向二:离群值特殊处理

SmoothQuant 等方法通过数学等价变换,将激活中的 outlier "迁移"到权重上(权重是静态的,可以离线处理)。但这需要额外的 calibration 数据和模型修改,不适合即插即用的场景。

W8A8 对称量化:Cider 的选择

Cider 采用 per-token 对称量化 作为激活量化策略:

对每个 token 向量 x ∈ R^d:
  scale = max(|x|) / 127
  x_int8 = round(x / scale)        → clamp to [-128, 127]
  
反量化(融合进 GEMM 输出):
  y_fp16 = (x_int8 · w_int8) * scale_x * scale_w

选择 per-token(而非 per-tensor)的原因:不同 token 的激活范围差异大,per-token 量化确保每个 token 都能充分利用 INT8 的 [-128, 127] 表示范围。同时,per-token 粒度在 prefill 阶段(batch 中有多个 token)自然地提供了类似 per-row 的隔离效果。

选择对称量化(而非非对称)的原因:对称量化无需 zero_point 参数,INT8 GEMM 的输出可以直接通过乘 scale 反量化,避免了非对称量化带来的额外加法运算和复杂的 kernel 实现。在 8-bit 精度下,对称量化的精度损失极小(不同于 4-bit 场景)。

Cider SDK 技术实现详解

明略科技开源的 Cider SDK 以 MLX Custom Primitive 的形式实现 INT8 计算路径,完整融入 MLX 的 lazy evaluation 和计算图优化机制。

Mano-P Architecture

INT8 TensorOps 内核设计

Cider 的核心是一组手写 Metal Shader,直接调用 M5 芯片的 INT8 矩阵乘法硬件单元。内核设计遵循以下原则:

Tiling 策略:将大矩阵分割为适合 SIMD group 处理的 tile(典型配置为 32×32 或 64×64),每个 threadgroup 负责一个输出 tile 的计算。Tile 大小需要平衡寄存器压力和计算密度——太小则 TensorOps 利用率低,太大则 register spilling 导致性能退化。

数据布局:权重预排列为 TensorOps 友好的 interleaved 格式(推理前一次性完成),消除运行时的数据重排开销。激活值在量化后直接以 row-major INT8 布局送入 kernel。

Accumulator 精度:INT8 × INT8 的乘积累加到 INT32 accumulator 中,避免中间溢出。最终输出通过乘以 scale_activation * scale_weight 转回 FP16。这一反量化步骤被融合在 kernel 的 store 阶段,不产生额外的 memory round-trip。

双路径自动切换

  • Prefill(seq_len > 1):走 INT8 GEMM kernel,充分利用 TensorOps 的矩阵吞吐
  • Decode(seq_len = 1):走专用 INT8 Matrix-Vector kernel,避免 GEMM kernel 在小 batch 下的 wave occupancy 损失

CiderLinear 模块在运行时根据输入 tensor 的形状自动选择最优路径,对上层完全透明。

三种量化粒度的实现

Cider 提供三种计算粒度,对应不同的精度-速度权衡:

模式Scale 粒度Prefill 加速(vs MLX W4A16)适用场景
Per-channel每输出通道 1 个 scale~1.8x速度优先,精度容忍度高
Per-group gs=128每 128 元素 1 个 scale~1.5x平衡选择
Per-group gs=64每 64 元素 1 个 scale~1.3x精度优先

Per-channel 最快是因为 scale 参数最少,GEMM kernel 内部的反量化逻辑最简单(每列只需乘一个 scalar)。Per-group 模式下,每组需要独立的 scale,反量化变为分段乘法,增加了指令开销。

条件编译:M5+ 芯片才启用

Cider 的安装策略确保跨硬件兼容:

# setup.py 中的条件编译逻辑(简化)
if platform.machine() == "arm64" and has_m5_tensorops():
    # 完整编译:C++ 扩展 + Metal Shader + INT8 kernel
    ext_modules = [CMakeExtension("cider._C")]
else:
    # 纯 Python 包:仅包含接口定义,无原生扩展
    ext_modules = []

运行时检测:

from cider import is_available, convert_model

if is_available():  # M5+: True; M4 及以下: False
    convert_model(model)  # 替换 Linear → CiderLinear
    # 后续推理自动走 INT8 TensorOps
else:
    pass  # 原样使用 MLX 标准推理,无需任何代码改动

这一设计保证了 Cider 可以作为无条件依赖安装——在不支持的硬件上静默回退,不引入运行时错误。

与 MLX 框架的集成方式

Cider 的集成通过 MLX Custom Primitive API 实现,不修改 MLX 源码:

  1. 注册 Custom Op:将 INT8 GEMM 注册为 MLX 的自定义原语,获得 lazy evaluation 支持
  2. 模型转换convert_model() 遍历模型的所有 nn.Linear 层,替换为 CiderLinear(包含预量化的 INT8 权重 + scale 参数)
  3. 计算图兼容CiderLinear 的 forward 输出与原始 nn.Linear 类型一致(FP16 tensor),下游层无需任何适配
  4. 梯度支持:虽然 Cider 目前专注推理,但 Custom Primitive 接口保留了反向传播扩展的可能性

性能实测

以下数据严格基于 Cider 开源仓库 的 benchmark 结果,测试硬件为 Apple M5 Pro, 64GB RAM, 307 GB/s 内存带宽。

端到端推理性能(Context: 4516 tokens)

量化方案Prefill 时间Decode 速度说明
W8A16(MLX 基线)2.839s80.1 tok/sMLX 原生 INT8 权重量化
W8A8(Cider)2.519s79.5 tok/sINT8 TensorOps 激活量化

分析

  • Prefill 加速 ~12.7%(2.839s → 2.519s)。这是在 4516 tokens 的真实多模态输入上的端到端结果,包含了模型所有层的开销
  • Decode 速度基本持平(80.1 vs 79.5 tok/s)。Decode 是 memory-bound,INT8 在这一阶段的优势有限,Cider 的 MV kernel 主要确保不引入退化
  • 4516 tokens 的 context 长度代表了 VLM(视觉语言模型)的典型输入规模——一张截图的 visual tokens + 文本指令

独立算子基准(Per-channel W8A8 vs MLX 基线)

矩阵规模 Mvs W8A16 加速vs W4A16 加速
1281.43x1.19–1.28x
10241.80–1.82x1.64–1.66x
40961.59–1.84x1.50–1.75x
81921.58–1.86x1.48–1.73x

分析

  • 当 M ≥ 1024 时(对应 prefill 阶段的典型 batch size),加速比稳定在 1.6–1.9x(vs W8A16)和 1.4–1.7x(vs W4A16)
  • M=128 时加速比相对较低(1.43x),这是因为小矩阵的 kernel launch 和 tiling 开销占比增大
  • 对比基线为 MLX 的 W4A16(当前社区最常用的量化方案),Cider W8A8 per-channel 仍然提供 1.4–1.75x 的加速,且权重只多占 1x 存储空间(INT8 vs INT4)

VLM 真实场景(Qwen3-VL-2B,M5 Pro)

Prompt TokensW8A16 PrefillW8A8 Prefill加速比
13342065 tok/s3242 tok/s1.57x
23931847 tok/s2983 tok/s1.61x
34551741 tok/s2796 tok/s1.61x

分析

  • VLM 场景(图片输入)产生大量 visual tokens,prefill 阶段占整体推理时间的比重显著高于纯文本场景
  • 加速比随 prompt 长度增加保持稳定(1.57x → 1.61x),说明 INT8 GEMM kernel 的效率不依赖特定输入规模
  • 对于 Mano-P 这类需要处理屏幕截图的 GUI Agent,prefill 加速的实际收益尤为显著

精度验证(Wikitext2 Perplexity)

模型方案PPL相对增长
Qwen3-8BFP16(基线)9.726
Qwen3-8BW8A16 (MLX)9.707-0.02
Qwen3-8BW8A8 per-channel (Cider)9.756+0.03
Llama3-8BFP16(基线)6.138
Llama3-8BW8A16 (MLX)6.147+0.01
Llama3-8BW8A8 per-channel (Cider)6.271+0.13

W8A8 per-channel 的 PPL 增长极小(Qwen3-8B +0.03,Llama3-8B +0.13),验证了 8-bit 对称量化在激活侧的鲁棒性。实际使用中,这一精度差异对生成质量几乎无感知影响。

Mano-P + Cider:端侧大模型的完整验证

明略科技的 Mano-P 项目是 Cider SDK 的第一个大规模验证场景。Mano-P 是一个端侧 GUI-VLA(Vision-Language-Action)Agent:接收屏幕截图,理解 GUI 元素,规划操作步骤,执行鼠标键盘动作——完整的感知-决策-执行闭环,全部在本地 Mac 上完成,数据不出设备。

Mano-P 在 OSWorld 基准测试中取得 58.2% 的专项最高分,证明了端侧模型在 GUI 自动化任务上的能力上限。但 GUI Agent 的实用性高度依赖响应延迟——用户发出指令后等待 5 秒和等待 2.5 秒,是"不可用"和"可用"的分界线。

Cider 在这个场景中的价值:

  • Prefill 加速:GUI Agent 的输入包含完整屏幕截图的 visual tokens(通常 2000–5000 tokens),prefill 是主要延迟来源。Cider 将这一阶段的耗时从 2.839s 降到 2.519s
  • Decode 不退化:Agent 的输出是结构化的动作序列(较短),decode 速度维持 ~80 tok/s 确保输出阶段无瓶颈
  • 零侵入集成:Mano-P 的模型代码无需针对 Cider 做任何修改,convert_model() 一行代码完成加速

这种"端侧 72B 模型 + 硬件级推理加速"的组合,验证了一个产品级判断:大模型的 GUI 理解能力不需要依赖云端 API,在消费级 Mac 上已经可以达到实用水平。 明略科技的 Mano-P 项目是目前业界为数不多的端侧 VLA Agent 开源实现,Cider 则确保其推理性能触达 Apple Silicon 的硬件极限。

总结:MLX 是起点,INT8 TensorOps 是下一步

MLX 为 Mac 上的大模型推理建立了坚实的基础设施——优雅的 API、高效的内存管理、活跃的社区生态。但其权重量化方案在 prefill 阶段存在结构性天花板:FP16 GEMM 路径无法利用 M5 芯片的 INT8 TensorOps 算力。

明略科技开源的 Cider SDK 以非侵入的方式补全了这一路径:

  • 适用于所有 MLX 模型(已验证 Qwen3、Qwen3-VL、Llama3)
  • Prefill 加速 1.4–2.2x(vs MLX W4A16),per-channel 模式最高 1.8x
  • 精度损失可忽略(PPL 增长 < 0.15)
  • 条件编译,M5+ 启用,旧硬件自动回退

对于在 Mac 上做大模型推理的开发者,Cider 代表的不只是一个加速库,而是一个信号:Apple Silicon 的整数计算能力是一块尚未被充分开发的资源,MLX 生态正在向硬件极限逼近。


开源地址:

Cider 仓库包含完整的 benchmark 脚本、Metal Kernel 源码、以及 INT8 GEMM 优化教程(tutorial/how_to_write_efficient_int_gemm_m5_zh.md),欢迎 Star 和 Issue 反馈。