8. 设备发现与选择:ANeuralNetworks_getDeviceCount、ANeuralNetworks_getDevice、指定设备执行(CPU/GPU/NPU/DSP)
好,咱们继续往下走。上一章我们把NNAPI的完整执行流程跑通了,从创建模型到编译再到执行。但有个关键问题我一直没细讲——你的模型到底跑在哪个硬件上?
是CPU?GPU?还是手机里那颗专门的NPU?说实话,这个问题在真实项目中特别重要。我见过不少开发者,模型在模拟器上跑得飞快,一上真机就卡成PPT。为什么?因为模拟器默认走CPU,真机可能自动切到了DSP,但DSP对某些算子支持不好,又回退到CPU了。嗯,这里面的门道,我们今天一次性说清楚。
8.1 为什么要手动选择设备?
NNAPI默认会帮你选一个"最合适"的设备。但"最合适"不一定是"最快"的。你想想看,系统怎么知道你的需求?
- 你要低功耗?那DSP可能更好。
- 你要高吞吐?GPU往往更强。
- 你要低延迟?NPU的专用加速器可能最合适。
所以,手动发现和选择设备,是性能调优的第一步。我个人习惯,在项目初始化阶段,一定会先枚举设备,然后根据场景做选择。
8.2 发现设备:getDeviceCount 和 getDevice
先看怎么拿到设备列表。NNAPI提供了两个核心函数:
ANeuralNetworks_getDeviceCount— 获取可用设备数量ANeuralNetworks_getDevice— 根据索引获取具体设备句柄
代码很简单,但坑不少。我直接上代码:
uint32_t deviceCount = 0;
// 第一步:获取设备数量
ANeuralNetworks_getDeviceCount(&deviceCount);
// 注意:返回的是ANeuralNetworksStatus,一定要检查!
if (deviceCount == 0) {
// 没有可用设备?不可能,至少有个CPU
// 但我在某款低端平板上确实遇到过...
return;
}
// 第二步:遍历设备
for (uint32_t i = 0; i < deviceCount; i++) {
ANeuralNetworksDevice* device = nullptr;
ANeuralNetworks_getDevice(i, &device);
// 获取设备名称
const char* name = nullptr;
ANeuralNetworksDevice_getName(device, &name);
// 获取设备类型
int32_t type = 0;
ANeuralNetworksDevice_getType(device, &type);
// 获取设备版本
const char* version = nullptr;
ANeuralNetworksDevice_getVersion(device, &version);
// 打印设备信息
ALOGI("Device[%d]: name=%s, type=%d, version=%s",
i, name, type, version);
}
设备类型枚举值:
ANEURALNETWORKS_DEVICE_UNKNOWN= 0 — 未知类型ANEURALNETWORKS_DEVICE_CPU= 1 — CPUANEURALNETWORKS_DEVICE_GPU= 2 — GPUANEURALNETWORKS_DEVICE_ACCELERATOR= 3 — 专用加速器(NPU/DSP等)
我的经验:不要只看type字段。有些厂商把NPU也标记为ACCELERATOR,但DSP也是ACCELERATOR。怎么区分?我一般会结合name字段做字符串匹配,比如name包含"npu"、"dsp"、"tpu"等关键词。
8.3 指定设备执行:从编译开始控制
拿到设备列表后,怎么让模型跑在指定设备上?关键在编译阶段。
之前我们用的是ANeuralNetworksCompilation_create,它默认让系统选设备。要指定设备,得用另一个函数:
// 假设我们已经选好了设备
ANeuralNetworksDevice* targetDevice = ...; // 从遍历结果中选一个
// 创建编译对象,指定设备
ANeuralNetworksCompilation* compilation = nullptr;
ANeuralNetworksCompilation_createForDevices(
model,
&targetDevice, // 设备数组
1, // 设备数量
&compilation
);
// 后续步骤不变
ANeuralNetworksCompilation_finish(compilation);
// ... 创建执行对象,运行模型
注意:createForDevices传入的是设备数组,你可以传多个设备。但NNAPI会按你传入的顺序尝试使用。如果第一个设备不支持某个算子,会自动fallback到第二个。我曾经踩过坑:把GPU放前面,CPU放后面,结果GPU不支持某个算子,默默回退到CPU,性能反而更差。所以,要么只传一个设备,要么确保设备列表的顺序符合你的预期。
8.4 实战:CPU/GPU/NPU/DSP 的选择策略
不同设备各有优劣。我整理了一张表,方便你快速决策:
| 设备 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| CPU | 兼容性最好,所有算子都支持 | 速度慢,功耗高 | 调试、兜底方案 |
| GPU | 并行计算强,适合大模型 | 功耗高,部分算子不支持 | 图像处理、大矩阵运算 |
| NPU | 速度快,功耗低,专用加速 | 算子支持有限,厂商定制 | 推理加速、实时应用 |
| DSP | 超低功耗,适合持续运行 | 精度可能受限,速度一般 | 语音唤醒、传感器数据处理 |
那具体怎么选?我分享一个我在项目中用的策略:
- 先枚举所有设备,打印出来给开发者看。
- 默认走NPU(如果有),性能最好。
- 如果NPU不支持某个算子,回退到GPU。
- GPU也不行,最后用CPU兜底。
- 特殊场景:比如做语音唤醒,我建议强制走DSP,因为需要一直开着,功耗是关键。
核心逻辑:不要把所有鸡蛋放在一个篮子里。多设备分级fallback,才能保证兼容性和性能的平衡。
8.5 知识体系:设备发现与选择流程
下面这张图,把整个流程串起来了。建议你保存下来,写代码时对照着看:
8.6 避坑指南
最后,分享几个我踩过的坑:
- 不要假设设备一定存在。 有些低端设备只有CPU,没有GPU或NPU。代码里一定要做空指针检查。
- 设备名称不是标准化的。 高通叫"qti-dsp",联发科叫"mtk-npu",华为叫"kirin-npu"。做字符串匹配时,建议用
strstr而不是strcmp。 - createForDevices可能失败。 如果传入的设备不支持模型中的某个算子,函数会返回错误码。我曾经遇到过,传了NPU进去,结果模型里有个算子NPU不支持,编译直接失败。所以,一定要检查返回值,并做好fallback。
- 性能测试不能只看一次。 不同设备在不同模型、不同输入尺寸下表现差异很大。我建议做自动化测试,跑100次取平均值,再决定用哪个设备。
一个小技巧:如果你不确定设备支持哪些算子,可以用ANeuralNetworksDevice_getSupportedOperations来查询。这个函数会返回一个布尔数组,标记每个算子是否被支持。嗯,这个我们在后面的章节会详细讲。
好了,设备发现与选择就讲到这里。说白了,就是三步:枚举 → 筛选 → 指定。代码不多,但逻辑要严谨。下一章,我们会深入算子的支持查询,看看怎么精确控制模型的执行路径。