6、模型编译与执行:编译选项、执行会话管理、同步与异步执行
好,咱们接着往下走。模型加载完了,图也构建好了,接下来就是最关键的一步——编译与执行。
这一步说白了,就是把咱们搭好的“神经网络积木”真正跑起来。我刚开始接触 NNAPI 的时候,觉得编译不就是点个按钮吗?后来才发现,这里面的门道可不少。你想想看,同样的模型,在不同设备上跑出来的速度可能差好几倍,这跟编译选项怎么设置有直接关系。
6.1 编译选项:ANeuralNetworksCompilation_setPreference
先聊聊编译。NNAPI 的编译过程,其实是在做两件事:一是把咱们的模型结构转换成底层硬件能理解的指令,二是根据咱们的偏好来优化执行策略。
ANeuralNetworksCompilation_setPreference 这个函数,就是用来告诉编译器:“嘿,我到底想要什么?”
支持的偏好选项
| 偏好常量 | 含义 | 适用场景 |
|---|---|---|
ANeuralNetworksPreference_LOW_POWER |
优先考虑功耗 | 电池供电设备、后台任务 |
ANeuralNetworksPreference_FAST_SINGLE_ANSWER |
优先考虑速度 | 实时推理、用户交互场景 |
ANeuralNetworksPreference_SUSTAINED_SPEED |
持续高性能 | 长时间运行的推理任务 |
我个人习惯是,默认先用 FAST_SINGLE_ANSWER。为什么?因为大部分场景下,用户等不起。你做个实时滤镜,如果推理慢了,画面卡顿,体验就很糟糕。
核心代码示例:设置编译偏好
// 创建编译对象
ANeuralNetworksCompilation* compilation;
ANeuralNetworksCompilation_create(model, &compilation);
// 设置偏好:优先速度
ANeuralNetworksCompilation_setPreference(
compilation,
ANeuralNetworksPreference_FAST_SINGLE_ANSWER
);
// 完成编译
ANeuralNetworksCompilation_finish(compilation);
嗯,这里要注意:setPreference 必须在 finish 之前调用。我曾经见过有人把顺序搞反了,结果编译出来的模型性能完全不对。
小技巧:如果你不确定设备支持哪种偏好,可以先查一下 ANeuralNetworksDevice_getCapabilities。有些老设备可能只支持 LOW_POWER,你硬要设成 FAST_SINGLE_ANSWER,它也不会报错,但效果可能打折扣。
6.2 执行会话管理
编译完成之后,咱们就有了一个“可执行”的模型。但怎么跑呢?这就涉及到执行会话的概念。
你可以把执行会话想象成一个“工作台”。每次推理,就是在这个工作台上放好输入数据,然后启动机器,拿到输出结果。
创建与配置执行会话
// 创建执行对象
ANeuralNetworksExecution* execution;
ANeuralNetworksExecution_create(compilation, &execution);
// 绑定输入数据(假设输入是 float 数组)
float inputData[INPUT_SIZE] = { ... };
ANeuralNetworksExecution_setInput(
execution,
0, // 输入索引
nullptr, // 数据类型描述(nullptr 表示沿用模型定义)
inputData,
sizeof(inputData)
);
// 绑定输出缓冲区
float outputData[OUTPUT_SIZE] = { 0 };
ANeuralNetworksExecution_setOutput(
execution,
0,
nullptr,
outputData,
sizeof(outputData)
);
这里有个坑,我踩过好几次:输入输出的内存生命周期。如果你用异步执行,一定要确保 inputData 和 outputData 在推理完成之前不会被释放。否则,轻则数据错乱,重则直接崩溃。
警告:不要在执行完成前修改或释放输入缓冲区!NNAPI 内部可能直接引用你传进去的内存地址,而不是拷贝一份。这是为了性能考虑,但也带来了风险。
6.3 同步执行 vs 异步执行
好,会话准备好了,数据也绑定了。接下来就是执行。NNAPI 提供了两种方式:同步和异步。
同步执行
同步执行最简单直接。调用 compute 之后,线程会阻塞在那里,直到推理完成。
// 同步执行
ANeuralNetworksExecution_compute(execution);
// 执行完成后,outputData 里就是结果
printf("推理结果: %f\n", outputData[0]);
什么时候用同步?我个人习惯是:推理时间很短(比如 < 10ms),或者你在主线程之外单独开了个工作线程。如果你在主线程里做同步推理,而且模型比较重,那 UI 就会卡住。用户会骂娘的。
异步执行
异步执行就灵活多了。你启动推理后,可以立刻返回去做别的事,等结果出来了再处理。
// 准备一个事件对象
ANeuralNetworksEvent* event;
// 异步执行
ANeuralNetworksExecution_startCompute(execution, &event);
// 这里可以做其他事情...
doSomeOtherWork();
// 等待推理完成
ANeuralNetworksEvent_wait(event);
// 释放事件资源
ANeuralNetworksEvent_free(event);
// 现在可以安全地读取 outputData 了
printf("异步推理结果: %f\n", outputData[0]);
你看,startCompute 启动后,你可以去更新 UI、处理用户输入、或者准备下一批数据。等 wait 的时候再拿结果。
关键区别:
- 同步:调用
compute→ 阻塞等待 → 返回结果 - 异步:调用
startCompute→ 立即返回 → 做其他事 → 调用wait获取结果
6.4 核心流程总览
说了这么多,咱们用一张图来串一下整个流程。从模型创建到执行完成,每一步都环环相扣。
6.5 实际项目中的选择策略
说了这么多理论,咱们聊聊实际项目中怎么选。
场景一:实时视频滤镜
每帧推理时间约 5-8ms。我建议用同步执行,放在单独的渲染线程里。为什么?因为推理时间短,同步的开销反而比异步小。异步需要管理事件对象,多了一层复杂度。
场景二:语音识别
语音片段可能长达几秒。这时候用异步执行更合适。你可以在主线程里显示“正在识别...”的动画,后台慢慢跑推理。等 wait 返回了,再更新结果。
场景三:批量图片分类
比如一次处理 100 张图。我个人的做法是:创建多个执行会话,用异步并行跑。每个会话绑定不同的输入输出,然后统一 wait。这样能充分利用多核 CPU 或 GPU 的并行能力。
避坑指南:我曾经在某个项目里,为了省事,所有推理都用同步。结果在低端机上,UI 卡得跟幻灯片一样。后来改成异步,体验立马好了。所以,如果推理时间 > 16ms(一帧的时间),强烈建议用异步。
6.6 资源释放
最后,别忘了清理资源。NNAPI 的对象都需要手动释放,否则会有内存泄漏。
// 释放顺序:先执行,再编译,最后模型
ANeuralNetworksExecution_free(execution);
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
嗯,这里有个细节:如果你用了异步执行,一定要确保 ANeuralNetworksEvent_wait 调用之后,再释放执行对象。否则,你可能会在推理还没完成的时候就把资源给干掉了,后果嘛...你懂的。
好了,这一章的内容就到这儿。编译选项怎么设、执行会话怎么管、同步异步怎么选,咱们都聊透了。下一章,咱们会深入聊聊性能调优和调试技巧,到时候会用到今天讲的知识点。