3. ONNX Runtime Mobile 核心概念
好,咱们进入正题。ONNX Runtime Mobile,说白了就是微软把标准版 ORT 砍掉冗余、重新编译后,塞进手机里的一个轻量推理引擎。我最早接触它是在一个相机实时滤镜项目里,当时被坑得不轻——后面会细说。
3.1 ONNX Runtime 整体架构
先看一张架构图,把 ORT 的层次关系理清楚。
从上往下看,应用层调用 API,API 创建 Session,Session 把模型交给 Execution Provider(简称 EP),EP 再调度底层硬件。我习惯把 Session 理解成「推理管家」——它负责加载模型、管理输入输出、协调 EP 干活。
3.2 Mobile 版本 vs 标准版
很多人问我:ORT Mobile 和标准版到底差在哪?我直接列个表,一目了然。
| 对比维度 | 标准版 ORT | ORT Mobile |
|---|---|---|
| 二进制体积 | 30~80 MB(含全部 EP 和优化器) | 1~5 MB(按需裁剪后) |
| 支持的 EP | CUDA、TensorRT、OpenVINO 等桌面端 | XNNPACK、CoreML、NNAPI、QNN |
| 模型优化器 | 全量优化(Level 99) | 基本优化(Level 1~3) |
| 动态图支持 | 完整支持 | 有限支持(推荐静态输入) |
| 构建方式 | pip install / CMake 全量编译 | ORT Mobile 包 / 自定义 Build |
| 典型场景 | 服务器、PC、云端推理 | 手机、IoT、嵌入式相机 |
关键点:ORT Mobile 不是标准版的「阉割版」,而是针对移动端重新设计的轻量引擎。它去掉了桌面端 EP 和高级优化器,但保留了核心推理管线。
我记得第一次在 Android 相机项目里直接塞标准版 ORT,APK 直接飙到 120MB。后来换成 ORT Mobile,裁剪后只有 3.2MB——嗯,差距就是这么大。
3.3 Session 与 Execution Provider
Session 和 EP 的关系,我打个比方:Session 是项目经理,EP 是施工队。项目经理(Session)拿到图纸(模型),决定让哪个施工队(EP)干活——CPU 队、GPU 队还是 NPU 队。
Session 的核心职责
- 模型加载:从文件或内存加载 .onnx 模型
- 环境配置:设置线程数、内存模式、日志等级
- 推理调度:调用 Run() 方法执行前向计算
- 资源管理:释放 IO 绑定、缓存中间张量
Execution Provider 的选择策略
在相机场景下,我建议按优先级尝试:
- NNAPI(Android):调用高通/联发科 DSP,功耗最低
- CoreML(iOS):苹果神经网络引擎,延迟最稳
- XNNPACK:纯 CPU 但优化极好,兼容性最强
- CPU(默认):兜底方案,任何设备都能跑
避坑指南:我曾经在某个骁龙 8 Gen1 设备上直接启用 NNAPI,结果推理结果全是 NaN。后来发现是 NNAPI 对某些算子精度有问题。我的做法是:先跑 CPU EP 验证正确性,再切 NNAPI 做性能测试。
3.4 模型加载与推理流程
直接上代码,这是我在相机项目里实际用过的加载流程。
// 1. 创建环境(ORT Mobile 推荐使用 Ort::Env)
Ort::Env env(OrtLoggingLevel::ORT_LOGGING_WARNING, "camera_inference");
// 2. 配置 Session 选项
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(4); // 内部算子线程数
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
// 3. 添加 Execution Provider(以 XNNPACK 为例)
OrtXnnpackProviderOptions xnnpack_options = {0};
session_options.AppendExecutionProvider_Xnnpack(xnnpack_options);
// 4. 加载模型(从内存加载,避免文件 IO 延迟)
std::vector<uint8_t> model_data = LoadModelFromAssets("model.onnx");
Ort::Session session(env, model_data.data(), model_data.size(), session_options);
// 5. 获取输入输出信息
Ort::AllocatorWithDefaultOptions allocator;
size_t num_input_nodes = session.GetInputCount();
size_t num_output_nodes = session.GetOutputCount();
// 6. 准备输入张量(假设输入是 640x480 的 RGB 图像)
std::vector<int64_t> input_shape = {1, 3, 480, 640};
size_t input_tensor_size = 1 * 3 * 480 * 640;
std::vector<float> input_data(input_tensor_size);
// ... 填充图像数据(BGR 转 RGB、归一化等)
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
memory_info, input_data.data(), input_tensor_size,
input_shape.data(), input_shape.size());
// 7. 运行推理
const char* input_names[] = {"input"};
const char* output_names[] = {"output"};
Ort::Value output_tensor = session.Run(Ort::RunOptions{nullptr},
input_names, &input_tensor, 1,
output_names, 1);
// 8. 获取输出结果
float* output_data = output_tensor.GetTensorMutableData<float>();
// ... 后处理(NMS、阈值过滤等)
注意:在相机预览线程里做推理时,千万不要在主线程同步调用 Run()。我见过太多 ANR 了。正确的做法是:用单独的推理线程 + 双缓冲机制,一帧推理一帧采集。
3.5 移动端推理的典型流程
把上面的代码串起来,整个流程就是:
- 初始化阶段:创建 Env → 配置 SessionOptions → 添加 EP → 加载模型
- 预处理阶段:相机帧 → 缩放 → 格式转换 → 归一化 → 排布为 NCHW
- 推理阶段:创建输入 Tensor → Run() → 获取输出 Tensor
- 后处理阶段:解析输出 → 阈值过滤 → NMS → 绘制结果
- 资源回收:Session 析构 → 释放 Tensor → 关闭相机
你想想看,整个流程里最耗时的其实是预处理和后处理——推理本身在 XNNPACK 下通常只要 5~15ms。所以优化的时候别光盯着模型,数据搬运和格式转换才是大头。
我的经验:在相机项目里,我习惯把预处理用 NEON 指令集手写一遍,推理用 ORT Mobile,后处理用 OpenMP 并行。这样整条流水线能控制在 30ms 以内,达到 30FPS 的实时要求。
嗯,核心概念就这些。Session 和 EP 的搭配是 ORT Mobile 的精髓,后面几章我们会深入每个 EP 的适配细节。