3. ONNX Runtime Mobile 核心概念

好,咱们进入正题。ONNX Runtime Mobile,说白了就是微软把标准版 ORT 砍掉冗余、重新编译后,塞进手机里的一个轻量推理引擎。我最早接触它是在一个相机实时滤镜项目里,当时被坑得不轻——后面会细说。

3.1 ONNX Runtime 整体架构

先看一张架构图,把 ORT 的层次关系理清楚。

ONNX Runtime 架构分层 应用层(App / Camera SDK) C/C++ / C# / Python API 接口 Session(会话管理 / 模型加载 / 推理调度) Execution Provider(CPU / GPU / NNAPI / CoreML / XNNPACK) 硬件层(ARM CPU / GPU / DSP / NPU) Session 是推理入口,EP 是硬件加速的桥梁

从上往下看,应用层调用 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 的选择策略

在相机场景下,我建议按优先级尝试:

  1. NNAPI(Android):调用高通/联发科 DSP,功耗最低
  2. CoreML(iOS):苹果神经网络引擎,延迟最稳
  3. XNNPACK:纯 CPU 但优化极好,兼容性最强
  4. 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 移动端推理的典型流程

把上面的代码串起来,整个流程就是:

  1. 初始化阶段:创建 Env → 配置 SessionOptions → 添加 EP → 加载模型
  2. 预处理阶段:相机帧 → 缩放 → 格式转换 → 归一化 → 排布为 NCHW
  3. 推理阶段:创建输入 Tensor → Run() → 获取输出 Tensor
  4. 后处理阶段:解析输出 → 阈值过滤 → NMS → 绘制结果
  5. 资源回收:Session 析构 → 释放 Tensor → 关闭相机

你想想看,整个流程里最耗时的其实是预处理和后处理——推理本身在 XNNPACK 下通常只要 5~15ms。所以优化的时候别光盯着模型,数据搬运和格式转换才是大头。

我的经验:在相机项目里,我习惯把预处理用 NEON 指令集手写一遍,推理用 ORT Mobile,后处理用 OpenMP 并行。这样整条流水线能控制在 30ms 以内,达到 30FPS 的实时要求。

嗯,核心概念就这些。Session 和 EP 的搭配是 ORT Mobile 的精髓,后面几章我们会深入每个 EP 的适配细节。