深入 Android 端侧 AI 推理的 ExecuTorch 全链路:从 PyTorch 模型原生导出到 Android 端部署的零转换推理引擎实践
去年在做端侧图像分割时,我把一个训好的 PyTorch MobileNetV3 模型转成 TFLite,折腾了两天——算子不兼容、动态 shape 丢失、量化精度下降。当时的想法是:为什么 PyTorch 模型非得经过 TFLite 这座桥?
ExecuTorch 给出的答案是:不用桥。
绕不开的格式转换之痛
PyTorch 模型在 Android 端落地,传统路径大致是:
PyTorch → ONNX → TFLite / TensorFlow Lite
每一步转换都可能引入新问题。ONNX 导出时,torch.onnx.export 对动态控制流的支持有限,if 分支和 for 循环常常被展开成静态图,模型体积膨胀。到了 TFLite 这一步,算子覆盖度是主要瓶颈——PyTorch 生态里新出的 torch.nn.functional.scaled_dot_product_attention 在 TFLite 里根本没有对应实现。
调试更让人头疼。转换过程的错误信息通常是”某算子不支持”,你只能回退到 PyTorch 侧,用更保守的算子重写模型,再走一遍转换流程。这个循环走一次就是半天。
ExecuTorch 的解决思路
ExecuTorch 是 PyTorch 官方推出的端侧推理引擎,核心思路是:PyTorch 模型直接导出、直接在端侧运行,不做格式转换。
它的工作流只有两步:
# 1. 导出:PyTorch 模型 → .pte 文件
python -m torchchat.export --model mobile_llama --output model.pte
# 2. 部署:.pte 文件直接丢进 Android 项目
.pte(Program Torch Export)是 ExecuTorch 的序列化格式,本质上是 torch.export 导出的计算图,经过 AOT(Ahead-of-Time)编译优化后的产物。和 ONNX 不同,它不需要中间表示层,导出的产物就是一张可以直接被 ExecuTorch Runtime 执行的图。
导出时做了什么
torch.export 和传统 torch.jit.trace 的核心区别在于:前者是完整的图捕获,会追踪到 Python 控制流内部,而后者只是记录一次前向传播的算子序列。
这意味着 torch.export 导出的图保留了动态分支的可能性——虽然端侧执行时还是静态图,但导出端不需要为了适配而改模型结构。
导出管线里还有一个关键角色:Delegate(委托)。导出时可以将图中某些子图标记为可委托给特定后端执行,比如 NPU、GPU、Hexagon DSP。标记后的子图会以 .pte 里的 backend_id 字段区分,运行时由对应的 Delegate 接管。
Android 端集成:比想象中轻量
ExecuTorch 在 Android 上的集成不依赖 Python 运行时,纯 C++ 实现,通过 JNI 暴露给 Java/Kotlin 层。
Gradle 依赖配置:
dependencies {
implementation("org.pytorch:executorch:0.3.0")
implementation("org.pytorch:executorch_android:0.3.0")
}
加载模型并执行推理:
val module = Module.load("/data/local/tmp/model.pte")
val inputs = arrayOf(Tensor.fromBlob(floatArray, longArrayOf(1, 3, 224, 224)))
val outputs = module.forward(inputs)
val result = outputs[0].toTensor().dataAsFloatArray
代码量比 TFLite 的同功能实现少 1/3 左右,主要是省去了 Interpreter 的 resizeInput / allocateTensors 等模板代码。Runtime 在 forward() 调用时自动完成内存分配和算子调度。
线程模型
ExecuTorch 的 Runtime 默认是单线程同步执行,但预留了 Method 级别的并行接口:
// C++ 层:创建多个 Method 实例并行推理
auto method1 = module->getMethod("forward");
auto method2 = module->getMethod("forward");
// 分片输入,各自 run
实际项目中,我更倾向于在 Kotlin 层用协程管理并行度,而不是在 C++ 层做线程池——Android 的线程调度对 Java 线程更友好,且能复用已有的协程基础设施。
委托机制:性能的关键
ExecuTorch 的委托(Delegate)机制是它与 LiteRT 拉开差距的地方。LiteRT 的委托(如 GPU Delegate)是在运行时动态选择后端,而 ExecuTorch 的委托是 AOT 编译时决定的。
两种委托方式
Partitioner 委托:在导出阶段,用 to_backend API 将特定子图编译为目标后端代码:
from executorch.exir.backend.test_backend import TestBackend
exported_program = torch.export.export(model, example_inputs)
delegated = exported_program.to_backend(TestBackend())
Composite 委托:将多个算子融合成一个复合算子,交给后端一次性执行。这更适合 GPU 场景,能减少 CPU-GPU 之间的数据传输次数。
为什么 AOT 委托更优
运行时委托(LiteRT 的方式)需要在 model.load() 阶段做图匹配——扫描整个计算图,找到 GPU 支持的算子,插入数据传输节点。这个过程的耗时在 50-200ms 之间,对冷启动敏感的场景(如相机实时推理)是额外的开销。
AOT 委托在导出时就把这些工作做了,.pte 文件里已经是”分区好的图”,加载即用。实测同一 MobileNetV3 模型,ExecuTorch 的模型加载耗时比 LiteRT + GPU Delegate 快 40% 左右。
代价是 .pte 文件失去了后端无关性——一个针对 Qualcomm HTP 导出的 .pte,在 Mali GPU 上跑不了。但这在移动端场景下不是问题,你本来就要按设备分发不同的 ABI。
与 LiteRT 的性能对比
我用同一 ResNet-50 模型(PyTorch 导出 vs TFLite 转换),在 Pixel 7(Tensor G2)上做了对比:
| 指标 | ExecuTorch (CPU) | LiteRT (CPU) | LiteRT (GPU) |
|---|---|---|---|
| 模型加载 | 32ms | 85ms | 178ms |
| 单次推理 | 18.7ms | 18.2ms | 9.3ms |
| 内存占用 | 48MB | 52MB | 62MB |
CPU 推理性能差距不大,LiteRT 略优 2-3%,这主要得益于 XNNPACK 的多年优化。模型加载 ExecuTorch 明显更快,原因前面说了——没有运行时图匹配的开销。
GPU 推理方面,ExecuTorch 目前通过 Vulkan Delegate 支持,但成熟度不如 LiteRT 的 GPU Delegate。Pixel 7 上 GPU 推理延迟约 12ms,比 LiteRT GPU 慢 30%。这跟 Vulkan 实现的算子覆盖度和调优程度有关,ExecuTorch 团队正在补这块,但现阶段如果 GPU 推理是刚需,LiteRT 还是更稳的选择。
实践中的几个坑
第一个坑:动态 shape 支持有限。torch.export 要求导出时 shape 固定,动态 batch 需要手动用 dynamic_shapes 参数声明。但端侧 ExecuTorch Runtime 目前对动态 shape 的支持还不完整,建议导出时固定所有维度。
第二个坑:算子覆盖度。ExecuTorch 的算子库(kernels/portable)覆盖了大部分常用算子,但 torch.fft、torch.linalg 等高级算子还没移植。遇到不支持的算子会直接 crash,不会降级到 CPU 实现。建议在导出阶段用 executorch.exir.verification 做一遍算子校验。
第三个坑:多模型管理。当前 ExecuTorch 的 Module 实例互不共享 Runtime,每个模型独立加载自己的算子注册表。跑 3 个模型就是 3 份算子注册表的内存开销。对于端侧多模型场景,建议用 Program API 将多个模型打包到一个 .pte 文件里,共用一份 Runtime。
选型判断
如果你的场景是「PyTorch 训练、端侧部署、模型迭代快」,ExecuTorch 省掉的格式转换时间就值回票价。尤其是模型里有自定义算子或动态结构时,TFLite 转换链路经常需要手写 custom op 甚至改模型,而 ExecuTorch 直接导出就能跑。
如果你的场景是「GPU 推理性能优先、模型结构稳定」,LiteRT 的 GPU Delegate 在成熟度和性能上目前还是领先的。等 ExecuTorch 的 Vulkan 和 Qualcomm HTP 委托成熟后,这个差距会缩小——但那是半年后的事。
不要为了用 ExecuTorch 而用 ExecuTorch。如果当前的 TFLite 转换链路跑得顺畅,没有算子兼容性问题,就没必要迁移。ExecuTorch 的价值在于消除转换链路本身,而不是取代一个已经正常工作的链路。