在 Apple Silicon GPU 上跑 PyTorch 模型:ExecuTorch MLX Delegate 实战

2026-05-18 35 预计阅读时间:1 分钟
来源:pytorch.org AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

过去在 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 的工作流程:

  1. torch.export 把 PyTorch 模型导出为 Edge Dialect IR。
  2. 在导出的 IR 中,MLX delegate 识别并接管它能加速的算子子图。
  3. 接管的子图被编译为 MLX 可执行的计算图,其余算子仍由 ExecuTorch 的默认 CPU 后端执行。
  4. 运行时,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 的 executorch runtime 库。如果遇到 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 利用率可以拉满。

几个调优方向:

  1. 尽量用静态 shape 导出。MLX delegate 对动态 shape 的支持还在完善中,固定 shape 能让 MLX 更好地做算子融合和 Metal kernel 编译。
  2. 关注内存布局。MLX 对 contiguous tensor 更友好,导出前可以插入 torch.contiguous_format 的预处理。
  3. 量化配合。MLX 支持 float16 和 bfloat16,对 LLM 场景可以先用 torch.quantization 做 int4/groupwise 量化,再让 MLX delegate 跑量化后的子图,内存占用和速度都有收益。
  4. 混合 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 模型上。


相关推荐