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 — CPU
  • ANEURALNETWORKS_DEVICE_GPU = 2 — GPU
  • ANEURALNETWORKS_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 超低功耗,适合持续运行 精度可能受限,速度一般 语音唤醒、传感器数据处理

那具体怎么选?我分享一个我在项目中用的策略:

  1. 先枚举所有设备,打印出来给开发者看。
  2. 默认走NPU(如果有),性能最好。
  3. 如果NPU不支持某个算子,回退到GPU。
  4. GPU也不行,最后用CPU兜底。
  5. 特殊场景:比如做语音唤醒,我建议强制走DSP,因为需要一直开着,功耗是关键。

核心逻辑:不要把所有鸡蛋放在一个篮子里。多设备分级fallback,才能保证兼容性和性能的平衡。

8.5 知识体系:设备发现与选择流程

下面这张图,把整个流程串起来了。建议你保存下来,写代码时对照着看:

设备发现与选择流程 getDeviceCount() getDevice(index) getName() / getType() 选择目标设备 createForDevices(deviceArray) 设备类型 CPU (type=1) GPU (type=2) ACCELERATOR (type=3) NPU / DSP / TPU 需结合name字段区分 选择策略 NPU > GPU > CPU 低功耗场景用DSP

8.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要假设设备一定存在。 有些低端设备只有CPU,没有GPU或NPU。代码里一定要做空指针检查。
  • 设备名称不是标准化的。 高通叫"qti-dsp",联发科叫"mtk-npu",华为叫"kirin-npu"。做字符串匹配时,建议用strstr而不是strcmp
  • createForDevices可能失败。 如果传入的设备不支持模型中的某个算子,函数会返回错误码。我曾经遇到过,传了NPU进去,结果模型里有个算子NPU不支持,编译直接失败。所以,一定要检查返回值,并做好fallback。
  • 性能测试不能只看一次。 不同设备在不同模型、不同输入尺寸下表现差异很大。我建议做自动化测试,跑100次取平均值,再决定用哪个设备。

一个小技巧:如果你不确定设备支持哪些算子,可以用ANeuralNetworksDevice_getSupportedOperations来查询。这个函数会返回一个布尔数组,标记每个算子是否被支持。嗯,这个我们在后面的章节会详细讲。

好了,设备发现与选择就讲到这里。说白了,就是三步:枚举 → 筛选 → 指定。代码不多,但逻辑要严谨。下一章,我们会深入算子的支持查询,看看怎么精确控制模型的执行路径。


公众号:蓝海资料掘金营,微信deep3321