过去在 Mac 上做推理,要么老老实实跑 CPU,要么绕道转成 CoreML 模型——中间的格式转换和精度损失让人头疼。Apple 的 MLX 框架已经证明 M 系列芯片的 GPU 能跑出相当不错的推理速度,但 PyTorch 生态一直缺少一条直通路径。ExecuTorch MLX Delegate 的出现,把这条路铺通了:PyTorch 模型导出后,通过 MLX delegate 直接在 Apple Silicon GPU 上加速推理,不再需要手动转换格式。
MLX Delegate 做了什么
ExecuTorch 是 Meta 推出的端侧推理框架,核心思路是"导出一次,多平台部署"。它通过 delegate 机制把算子下沉到各平台的后端——XNNPACK 负责 CPU 优化,CoreML 负责 Apple 神经引擎,而新加入的 MLX delegate 专门瞄准 Apple Silicon 的 GPU。
MLX delegate 的工作流程:
- 用
torch.export把 PyTorch 模型导出为 Edge Dialect IR。 - 在导出的 IR 中,MLX delegate 识别并接管它能加速的算子子图。
- 接管的子图被编译为 MLX 可执行的计算图,其余算子仍由 ExecuTorch 的默认 CPU 后端执行。
- 运行时,MLX 后端通过 Metal Command Buffer 把计算提交到 GPU,CPU 和 GPU 之间的数据交换由 MLX 的统一内存管理自动处理。
这意味着你不需要把整个模型都交给 MLX——只有被识别的子图才走 GPU,其余部分保持 CPU 执行,混合调度对模型兼容性更友好。
和 CoreML Delegate 的区别
Apple 生态里已经有 CoreML delegate,为什么还需要 MLX?
| 维度 | CoreML Delegate | MLX Delegate |
|---|---|---|
| 编译时机 | 模型导出时预编译为 CoreML .mlpackage | 运行时动态编译 MLX 计算图 |
| 算子覆盖 | 依赖 CoreML 支持的算子列表,覆盖面较窄 | MLX 算子库持续扩展,对动态 shape 更友好 |
| 模型更新 | 模型改动需重新导出和编译 | 动态编译,迭代更快 |
| 精度控制 | CoreML 有自己的量化路径 | 直接使用 MLX 的 float16/bfloat16 支持 |
| 适用场景 | 追求极致低功耗、ANE 加速 | 需要灵活调试、GPU 大规模并行 |
简单说:CoreML 更适合部署到 iOS App、走 ANE 管线;MLX 更适合在 Mac 上做本地推理和快速实验,尤其是需要 GPU 大算力的场景(比如本地跑 LLM)。
实战:导出并运行一个模型
下面用一个具体的例子走完整个流程——把一个小型 ResNet 模型导出并通过 MLX delegate 在 Mac GPU 上推理。
安装依赖
# 创建独立环境
conda create -n executorch_mlx python=3.11 -y
conda activate executorch_mlx
# 安装 ExecuTorch(从源码构建,确保拿到最新 MLX delegate)
pip install --upgrade pip
pip install cmake ninja
git clone https://github.com/pytorch/executorch.git
cd executorch
# 安装 ExecuTorch Python 库
pip install -e .
# 构建 MLX delegate runtime
# macOS 上需要先安装 MLX
pip install mlx
# 构建 ExecuTorch runtime 库(包含 MLX delegate)
bash examples/apple/mlx/build_mlx_delegate.sh
# 安装 MLX delegate 的 Python 端导出工具
pip install -e ".[mlx]"
构建脚本会编译出包含 MLX delegate 的
executorchruntime 库。如果遇到 Metal 编译问题,确认 Xcode Command Line Tools 已更新:xcode-select --install。
导出模型并注入 MLX Delegate
import torch
import torchvision
from executorch.exir import ExportConfig, to_edge
from executorch.sdk import generate_etrecord
# 加载预训练 ResNet18
model = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)
model.eval()
# 示例输入
example_input = (torch.randn(1, 3, 224, 224),)
# Step 1: torch.export 导出为 ExportedProgram
exported_program = torch.export.export(
model,
example_input,
dynamic_shapes=None, # MLX delegate 当前对静态 shape 更稳定
)
# Step 2: 转换为 Edge Dialect
edge_program = to_edge(exported_program)
# Step 3: 注入 MLX delegate——把可加速的子图交给 MLX
from executorch.extension.apple_mlx.delegate import MlxDelegate
mlx_delegate = MlxDelegate()
edge_program = edge_program.to_backend(mlx_delegate)
# Step 4: 转换为 ExecutorchProgram,准备序列化
executorch_program = edge_program.to_executorch()
# Step 5: 保存 .pte 模型文件
with open("resnet18_mlx.pte", "wb") as f:
f.write(executorch_program.buffer)
print("模型已导出: resnet18_mlx.pte")
导出完成后,你拿到一个 resnet18_mlx.pte 文件。这个文件里既包含 MLX delegate 接管的 GPU 子图,也包含 CPU fallback 的算子——运行时 ExecuTorch 自动调度。
在 Mac 上运行推理
import torch
from executorch.runtime import ExecutorchProgramModule
# 加载导出的模型
module = ExecutorchProgramModule("resnet18_mlx.pte")
# 准备输入(注意:输入需要是 float32,MLX 运行时自动转 float16 到 GPU)
input_tensor = torch.randn(1, 3, 224, 224)
# 执行推理——MLX delegate 的子图自动走 GPU
outputs = module.execute([input_tensor])
# outputs 是一个 list,取第一个结果
result = outputs[0]
print(f"输出 shape: {result.shape}")
print(f"Top-5 预测类别索引: {result.topk(5).indices.tolist()}")
运行这段代码时,打开 Activity Monitor 的 GPU History 面板,你应该能看到 GPU 使用率有明显跳动——这就是 MLX delegate 在工作。
用 Python SDK 检查 Delegate 覆盖率
想知道模型里有多少算子被 MLX delegate 接管了?可以用 ExecuTorch 的调试工具:
from executorch.sdk import InspectableETRecord
# 加载导出时生成的 ETRecord(如果你在导出时调用了 generate_etrecord)
# 这里直接用 edge_program 检查
print("Delegate 分配信息:")
for op in edge_program.graph_module.graph.nodes:
if op.op == "call_delegate":
print(f" MLX 接管: {op.target}")
覆盖率越高,GPU 加速效果越明显。如果关键算子(如大矩阵乘法)没被接管,推理速度可能反而不如纯 CPU——因为 CPU→GPU 的数据搬运有开销。
性能预期与调优建议
根据 MLX 框架在 Apple Silicon 上的已有表现,可以给出一些实际预期:
- 小模型(< 10M 参数):GPU 加速幅度有限,CPU 和 GPU 之间数据搬运的开销可能吃掉收益。这类模型用 XNNPACK CPU 后端可能更划算。
- 中等模型(10M–100M 参数):MLX delegate 通常能带来 2–4 倍加速,尤其是卷积和线性层密集的模型。
- 大模型(> 100M 参数,如 LLM):MLX 的 float16/bfloat16 推理在 M2/M3/M4 上表现突出,batch 推理时 GPU 利用率可以拉满。
几个调优方向:
- 尽量用静态 shape 导出。MLX delegate 对动态 shape 的支持还在完善中,固定 shape 能让 MLX 更好地做算子融合和 Metal kernel 编译。
- 关注内存布局。MLX 对 contiguous tensor 更友好,导出前可以插入
torch.contiguous_format的预处理。 - 量化配合。MLX 支持 float16 和 bfloat16,对 LLM 场景可以先用
torch.quantization做 int4/groupwise 量化,再让 MLX delegate 跑量化后的子图,内存占用和速度都有收益。 - 混合 delegate。如果模型里有些算子 MLX 不支持但 CoreML 支持,可以同时注入两个 delegate——ExecuTorch 支持多 delegate 共存。
什么时候该用,什么时候该等
MLX delegate 目前还在快速迭代阶段,适合以下场景:
- 你在 Mac 上做本地推理实验,需要快速迭代模型,不想每次都走 CoreML 的编译流程。
- 你的模型较大,CPU 推理速度不够,GPU 加速有明显收益。
- 你需要 float16/bfloat16 的灵活精度控制,而不是 CoreML 的固定量化路径。
暂时不适合的场景:
- 部署到 iOS/iPadOS 设备——CoreML delegate 走 ANE 更省电,MLX delegate 目前只面向 macOS。
- 模型里有大量 MLX 尚未支持的算子——delegate 覆盖率低时,频繁的 CPU↔GPU 切换会拖慢整体速度。
- 生产环境要求绝对稳定——MLX delegate 的 API 和算子支持还在变化,建议先在实验环境验证。
上手检查清单:
- [ ] Mac 为 M1 及以上芯片,macOS 13+
- [ ] Xcode Command Line Tools 已更新
- [ ] ExecuTorch 从源码构建,MLX delegate 编译成功
- [ ] 模型导出时使用静态 shape
- [ ] 用调试工具确认关键算子被 MLX delegate 接管
- [ ] 对比纯 CPU(XNNPACK)和 MLX delegate 的实际推理耗时,确认 GPU 加速有净收益
MLX delegate 把 PyTorch 生态和 Apple Silicon GPU 之间的断层补上了。对于在 Mac 上做本地推理的开发者来说,这是一条值得立刻尝试的路径——尤其是你已经在用 MLX 跑 LLM,现在可以把同样的硬件能力用到更广泛的 PyTorch 模型上。