ExecuTorch 实战:在 Arm CPU 与 NPU 上跑起端侧 AI 推理

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

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

预计阅读时间:10 分钟

把大模型塞进手机、嵌入式板卡里本地跑推理,不再是实验室里的概念验证——ExecuTorch 把 PyTorch 的导出、量化、部署流水线直接搬到了资源受限的边缘设备上。Arm 近期发布了一组配套 Jupyter Labs,让开发者不用啃完整文档就能上手。这篇文章拆解 ExecuTorch 的关键机制,并给出一条从模型导出到 Arm 设备推理的可操作路径。

从 PyTorch 到边缘:ExecuTorch 做了什么

PyTorch 的训练生态已经非常成熟,但把一个 nn.Module 部署到内存只有几百 MB、算力远低于云端 GPU 的设备上,中间有大量工作:算子裁剪、精度压缩、后端对接。ExecuTorch 的定位就是填补这段空白。

核心流程分三步:

  1. Export——用 torch.export 把训练好的模型导出为一份与 Python 解耦的中间表示(Edge Dialect IR),剥离动态控制流,留下纯计算图。
  2. Compile——针对目标后端(Arm CPU、Ethos NPU 等)做算子映射与优化,生成设备可执行的二进制或微码。
  3. Execute——在设备端通过轻量 runtime 加载并运行,runtime 本身只依赖 C/C++,不拉 PyTorch 大包。

这套流程的关键收益:模型 IR 与后端解耦,同一个导出结果可以分别编译给 Cortex-A CPU 和 Ethos-U55 NPU,不用为每个硬件重写推理代码。

Arm 后端:CPU 与 NPU 的分工

Arm 为 ExecuTorch 提供了两个官方后端:

  • XNNPACK 后端——面向 Cortex-A 系列 CPU,用高度优化的浮点/量化算子库跑推理。适合模型较大、NPU 不支持全部算子的场景,也作为 NPU 链路的 fallback。
  • Ethos-U 后端——面向 Ethos-U55/U65 NPU,把编译后的微码推到 NPU 上执行。NPU 的 MAC 利用率和能效比远高于 CPU,但算子覆盖范围有限,遇到不支持的算子会自动回退到 XNNPACK。

实际部署时,一个模型往往被拆成多段:NPU 能跑的部分走 NPU,剩下的走 CPU,由 runtime 自动调度。这种混合执行模式是 Arm 方案的核心优势——开发者不需要手动切分模型。

动手跑起来:从导出到推理的完整示例

下面用一段可复制的脚本演示整个链路。假设你有一台 Linux 开发机(用于导出和编译),目标设备是搭载 Cortex-A55 + Ethos-U55 的板卡。

环境准备

# 创建工作目录并安装 ExecuTorch
python -m venv executorch_env
source executorch_env/bin/activate

pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install executorch

# 安装 Arm XNNPACK 后端依赖
pip install executorch-backends-xnnpack

# 安装 Ethos-U 后端(需要 aarch64 交叉编译工具链)
# 注意:Ethos-U 编译步骤依赖 Arm 的 Vela 工具,先安装它
pip install ethos-u-vela

导出模型并编译为 Edge Dialect

import torch
from executorch.exir import to_edge

# 用一个轻量 MobileNetV2 示例;实际可替换为你自己的模型
model = torch.hub.load("pytorch/vision:v0.16", "mobilenet_v2", pretrained=True)
model.eval()

# 标准导出:捕获静态计算图
sample_input = (torch.randn(1, 3, 224, 224),)
exported_program = torch.export.export(model, sample_input)

# 转为 Edge Dialect IR
edge_program = to_edge(exported_program)

# 保存中间表示,后续可分别编译给不同后端
edge_program.save("mobilenet_v2_edge.pte")
print("Edge Dialect 导出完成")

为 XNNPACK 后端编译并量化

from executorch.exir import to_edge
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
from executorch.backends.xnnpack.quantizer.xnnpack_quantizer import XnnpackQuantizer
from torch.ao.quantization.quantize_pt2e import convert_pt2e, prepare_pt2e

# 重新加载导出结果(独立脚本场景)
exported_program = torch.export.export(model, sample_input)

# XNNPACK 量化:8-bit 权重 + 激活量化
quantizer = XnnpackQuantizer()
prepared_program = prepare_pt2e(exported_program, quantizer)

# 用校准数据跑几轮,收集量化统计
for _ in range(3):
    prepared_program.model()(torch.randn(1, 3, 224, 224))

converted_program = convert_pt2e(prepared_program)

# 转 Edge + 分区给 XNNPACK
edge_program = to_edge(converted_program)
edge_program = edge_program.to_executorch(XnnpackPartitioner)

# 生成最终 PTE 文件,拷到设备上即可运行
with open("mobilenet_v2_xnnpack.pte", "wb") as f:
    f.write(edge_program.buffer)
print("XNNPACK 编译完成,输出 mobilenet_v2_xnnpack.pte")

在 Arm 设备上运行推理

设备端只需要 ExecuTorch 的 C++ runtime(约几百 KB),不装 Python:

# 交叉编译 runtime(以 aarch64 为例)
git clone https://github.com/pytorch/executorch.git
cd executorch
mkdir build && cd build

cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=../cmake/aarch64-linux-gnu.toolchain.cmake \
  -DEXECUTORCH_BUILD_XNNPACK=ON \
  -DEXECUTORCH_BUILD_ETHOS_U=ON

cmake --build . -j$(nproc)

# 把生成的 runtime 二进制和 .pte 文件拷到目标板卡
scp libexecutorch.so mobilenet_v2_xnnpack.pte user@arm-board:/tmp/

在板卡上用 ExecuTorch 的 C++ API 加载并推理:

#include <executorch/runtime/executor/program.h>
#include <executorch/runtime/executor/method.h>

using namespace executorch::runtime;

int main() {
    // 加载 PTE 文件
    Program program = Program::load("/tmp/mobilenet_v2_xnnpack.pte");
    Method method = program.loadMethod("forward");

    // 准备输入 tensor(EValue 是 ExecuTorch 的统一数据容器)
    float input_data[1 * 3 * 224 * 224];
    EValue input = EValue(torch::from_blob(
        input_data, {1, 3, 224, 224}, torch::kFloat32));

    // 执行推理
    std::vector<EValue> outputs;
    method.execute({input}, &outputs);

    // outputs[0] 即分类 logits
    printf("推理完成,输出 tensor 大小: %ld\n",
           outputs[0].toTensor().numel());
    return 0;
}

运行前确认:交叉编译工具链路径、Ethos-U firmware 版本需要与板卡实际配置匹配。如果板卡没有 NPU,去掉 -DEXECUTORCH_BUILD_ETHOS_U=ON 即可纯 CPU 运行。

Arm Jupyter Labs:不用搭环境也能学

Arm 提供的 Jupyter Labs 覆盖了上述流程的每个关键节点,但省掉了交叉编译和设备配置的麻烦——你在浏览器里就能跑导出、量化、分区、性能对比。Labs 的几个重点实验:

  • 算子覆盖率检查:导出模型后自动列出哪些算子被 XNNPACK 接管、哪些走 Ethos-U、哪些需要 fallback,一目了然。
  • 量化精度对比:同一模型分别跑 float32 和 int8,给出精度损失和延迟收益的量化表格。
  • NPU 利用率分析:Vela 编译输出会报告 MAC 利用率、SRAM 占用、带宽需求,帮助判断模型是否适合当前 NPU 配置。

如果你只是想快速理解 ExecuTorch 的分区和编译机制,先跑一遍 Labs 比直接啃源码高效得多。

落地前的几条务实建议

  1. 先确认算子覆盖。Ethos-U55 支持的算子集合有限(主要是卷积、深度卷积、Elementwise 等)。如果你的模型用了大量自定义算子或复杂注意力机制,NPU 分区比例可能很低,收益有限。用 to_edge 后的 IR 检查分区结果再决定是否投入 NPU 链路。

  2. 量化校准数据要贴近真实分布。XNNPACK 的 int8 量化依赖校准统计,用随机噪声跑校准会导致精度明显下降。建议从实际业务数据中抽 100-500 条样本做校准。

  3. 模型大小与 SRAM 的权衡。Ethos-U55 的 SRAM 通常在 0.5-2 MB 范围,模型权重 + 激活中间值如果超出,就会频繁访问主内存,能效优势被抵消。Vela 编译报告里的 SRAM 占用是关键指标。

  4. 混合执行是常态。不要期望整个模型 100% 跑在 NPU 上。Cortex-A CPU 的 XNNPACK fallback 性能也不差——在 Cortex-A55 上跑量化后的 MobileNetV2,单帧延迟可以控制在 10-20 ms 量级。先让整条链路跑通,再逐段优化 NPU 占比。

  5. 版本对齐。ExecuTorch 仍在快速迭代,Arm 后端接口有变动。锁定 executorchexecutorch-backends-xnnpack 的版本号再进生产,避免上游更新导致分区行为变化。


端侧 AI 推理的工程难点不在"能不能跑",而在"跑得省不省、稳不稳"。ExecuTorch 提供了从 PyTorch 直通边缘设备的标准化管道,Arm 的 CPU/NPU 后端和配套 Labs 让这条管道变得可触摸。建议从 Labs 入手理解机制,再用上面的脚本在自己的模型和硬件上验证收益——量化后的精度损失和 NPU 分区比例,才是决定是否上线的硬指标。


相关推荐