3、NNAPI核心概念详解
好,咱们今天来啃硬骨头。NNAPI 的四个核心对象——ANeuralNetworksModel、ANeuralNetworksCompilation、ANeuralNetworksExecution、ANeuralNetworksMemory——说白了,就是整个神经网络推理管线的四个阶段。
我刚开始接触 NNAPI 时,总觉得这玩意儿抽象。后来发现,你把它想象成「做饭」就通了:Model 是菜谱,Compilation 是备菜切菜,Execution 是下锅炒,Memory 是锅碗瓢盆。这么一想,是不是清晰多了?
3.1 ANeuralNetworksModel —— 菜谱,定义网络结构
ANeuralNetworksModel 是 NNAPI 的起点。它描述了一个神经网络长什么样——有哪些操作(Operand)、哪些张量(Tensor)、数据怎么流动。
我个人习惯把 Model 的构建分成三步:
- 创建模型句柄:
ANeuralNetworksModel_create(&model) - 添加操作数和操作:告诉 NNAPI 有哪些输入、输出、中间张量,以及用什么算子(比如 CONV_2D、RELU)
- 完成模型定义:
ANeuralNetworksModel_finish(model)
关键点:Model 只是「图纸」,不包含实际数据。你定义了一个卷积层,但卷积核的权重值还没放进去。权重数据是通过 ANeuralNetworksModel_setOperandValue 绑定的。
我在项目中遇到过一个问题:模型定义完成后,想修改某个算子的参数。结果发现 ANeuralNetworksModel_finish 之后,模型就变成只读了。嗯,所以一定要在 finish 之前把所有东西都配好。
3.2 ANeuralNetworksCompilation —— 备菜,编译优化
Model 定义好了,接下来要编译。为什么需要编译?你想想看,同一个模型在不同设备上跑,性能天差地别。高通骁龙的 DSP 和联发科的 APU,底层指令集完全不一样。
ANeuralNetworksCompilation 就是干这个的——把模型编译成目标硬件能理解的指令。
// 创建编译实例
ANeuralNetworksCompilation *compilation;
ANeuralNetworksCompilation_create(model, &compilation);
// 设置编译偏好(可选)
ANeuralNetworksCompilation_setPreference(compilation,
ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER);
// 完成编译
ANeuralNetworksCompilation_finish(compilation);
| 编译偏好 | 说明 | 适用场景 |
|---|---|---|
| PREFER_LOW_POWER | 优先省电 | 后台任务、低功耗设备 |
| PREFER_FAST_SINGLE_ANSWER | 优先速度 | 实时推理、交互式应用 |
| PREFER_SUSTAINED_SPEED | 持续高性能 | 长时间运行的推理任务 |
避坑指南:我曾经在编译阶段踩过一个坑——同一个 Compilation 对象不能并发使用。如果你有多个线程要同时推理,必须创建多个 Compilation 实例,或者用 Execution 的并发模式。切记!
3.3 ANeuralNetworksExecution —— 下锅,执行推理
Compilation 编译好了,就像菜切好码好了。接下来就是下锅炒——ANeuralNetworksExecution。
Execution 负责:
- 绑定输入数据(喂数据)
- 指定输出缓冲区(取结果)
- 启动推理计算
- 等待计算完成
// 创建执行实例
ANeuralNetworksExecution *execution;
ANeuralNetworksExecution_create(compilation, &execution);
// 设置输入(假设输入是 float 数组)
float input_data[224*224*3];
ANeuralNetworksExecution_setInput(execution, 0, NULL,
input_data, sizeof(input_data));
// 设置输出
float output_data[1000];
ANeuralNetworksExecution_setOutput(execution, 0, NULL,
output_data, sizeof(output_data));
// 启动推理(同步方式)
ANeuralNetworksExecution_compute(execution);
// 或者异步方式
ANeuralNetworksExecution_startCompute(execution, callback_fn, user_data);
这里有个细节:setInput 和 setOutput 的第二个参数是索引号。这个索引对应你在 Model 定义时添加操作数的顺序。我见过有人搞混索引顺序,结果推理结果完全不对——嗯,调试了半天才发现是输入输出搞反了。
注意:Execution 对象是轻量级的,可以重复使用。但如果你要修改输入数据重新推理,记得先调用 ANeuralNetworksExecution_reset 重置状态。否则可能出现数据残留的问题。
3.4 ANeuralNetworksMemory —— 锅碗瓢盆,内存管理
最后是 Memory。说实话,这个对象最容易被忽略,但恰恰是性能瓶颈的关键。
ANeuralNetworksMemory 管理的是共享内存缓冲区。为什么需要它?因为 NNAPI 的底层硬件(比如 NPU、DSP)通常不能直接访问 Android 的普通堆内存。你需要把数据放到一块「双方都能访问」的共享内存里。
创建 Memory 有两种方式:
- 从文件描述符创建:
ANeuralNetworksMemory_createFromFd(size, prot, fd, offset)—— 适合大块数据,比如模型权重 - 从 AHardwareBuffer 创建:
ANeuralNetworksMemory_createFromAHardwareBuffer(buffer)—— 适合与 Camera、GPU 共享数据
实战经验:我做过一个实时视频风格迁移的应用。一开始用普通的 setInput 传数据,每帧都要拷贝一次内存,帧率只有 15fps。后来改用 ANeuralNetworksMemory 配合 AHardwareBuffer,直接把 Camera 的预览帧共享给 NNAPI,零拷贝!帧率直接飙到 30fps。
说白了,Memory 就是 NNAPI 的「快递员」。它负责把数据从 CPU 内存搬到 NPU 内存,或者反过来。用得好,性能翻倍;用得不好,反而增加开销。
小结一下
这四个核心对象的关系,我用一句话总结:Model 定义「做什么」,Compilation 决定「怎么做」,Execution 负责「开始做」,Memory 提供「做菜的材料和容器」。
实际开发中,Model 和 Compilation 通常是「一次构建,多次执行」——你编译一次模型,然后反复用它做推理。而 Execution 和 Memory 则是「高频使用」的对象,每次推理都会涉及。
我个人建议:刚开始学 NNAPI 时,先把这四个对象的生命周期搞清楚。什么时候 create,什么时候 finish,什么时候 destroy。搞混了,轻则内存泄漏,重则应用崩溃。我曾经就因为忘记释放 Compilation,导致手机跑了一晚上后内存爆了……嗯,那是个难忘的教训。
公众号:蓝海资料掘金营,微信deep3321