在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/4)
2. 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 和计算图优化机制。

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 源码:
- 注册 Custom Op:将 INT8 GEMM 注册为 MLX 的自定义原语,获得 lazy evaluation 支持
- 模型转换:
convert_model()遍历模型的所有nn.Linear层,替换为CiderLinear(包含预量化的 INT8 权重 + scale 参数) - 计算图兼容:
CiderLinear的 forward 输出与原始nn.Linear类型一致(FP16 tensor),下游层无需任何适配 - 梯度支持:虽然 Cider 目前专注推理,但 Custom Primitive 接口保留了反向传播扩展的可能性
性能实测
以下数据严格基于 Cider 开源仓库 的 benchmark 结果,测试硬件为 Apple M5 Pro, 64GB RAM, 307 GB/s 内存带宽。
端到端推理性能(Context: 4516 tokens)
| 量化方案 | Prefill 时间 | Decode 速度 | 说明 |
|---|---|---|---|
| W8A16(MLX 基线) | 2.839s | 80.1 tok/s | MLX 原生 INT8 权重量化 |
| W8A8(Cider) | 2.519s | 79.5 tok/s | INT8 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 基线)
| 矩阵规模 M | vs W8A16 加速 | vs W4A16 加速 |
|---|---|---|
| 128 | 1.43x | 1.19–1.28x |
| 1024 | 1.80–1.82x | 1.64–1.66x |
| 4096 | 1.59–1.84x | 1.50–1.75x |
| 8192 | 1.58–1.86x | 1.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 Tokens | W8A16 Prefill | W8A8 Prefill | 加速比 |
|---|---|---|---|
| 1334 | 2065 tok/s | 3242 tok/s | 1.57x |
| 2393 | 1847 tok/s | 2983 tok/s | 1.61x |
| 3455 | 1741 tok/s | 2796 tok/s | 1.61x |
分析:
- VLM 场景(图片输入)产生大量 visual tokens,prefill 阶段占整体推理时间的比重显著高于纯文本场景
- 加速比随 prompt 长度增加保持稳定(1.57x → 1.61x),说明 INT8 GEMM kernel 的效率不依赖特定输入规模
- 对于 Mano-P 这类需要处理屏幕截图的 GUI Agent,prefill 加速的实际收益尤为显著
精度验证(Wikitext2 Perplexity)
| 模型 | 方案 | PPL | 相对增长 |
|---|---|---|---|
| Qwen3-8B | FP16(基线) | 9.726 | — |
| Qwen3-8B | W8A16 (MLX) | 9.707 | -0.02 |
| Qwen3-8B | W8A8 per-channel (Cider) | 9.756 | +0.03 |
| Llama3-8B | FP16(基线) | 6.138 | — |
| Llama3-8B | W8A16 (MLX) | 6.147 | +0.01 |
| Llama3-8B | W8A8 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 SDK:github.com/Mininglamp-…
- Mano-P:github.com/Mininglamp-…
Cider 仓库包含完整的 benchmark 脚本、Metal Kernel 源码、以及 INT8 GEMM 优化教程(tutorial/how_to_write_efficient_int_gemm_m5_zh.md),欢迎 Star 和 Issue 反馈。