神经网络 API

Android 神经网络 API (NNAPI) 是 Android C API,旨在用于在 Android 设备上运行机器学习的计算密集型操作。NNAPI 旨在为更高层的机器学习框架提供基础功能层,例如 TensorFlow Lite 和 Caffe2,这些框架构建和训练神经网络。该 API 在运行 Android 8.1(API 级别 27)或更高版本的 Android 设备上均可用。

NNAPI 通过将来自 Android 设备的数据应用于先前训练的、开发人员定义的模型来支持推理。推理的示例包括对图像进行分类、预测用户行为以及选择对搜索查询的适当响应。

设备上推理具有许多优势

  • 延迟:您无需通过网络连接发送请求并等待响应。例如,这对于处理来自相机的连续帧的视频应用程序来说至关重要。
  • 可用性:应用程序即使在没有网络覆盖的情况下也能运行。
  • 速度:专用于神经网络处理的新硬件提供了比单独的通用 CPU 快得多的计算速度。
  • 隐私:数据不会离开 Android 设备。
  • 成本:当所有计算都在 Android 设备上执行时,不需要服务器场。

还有一些开发人员需要注意的权衡取舍

  • 系统利用率:评估神经网络涉及大量计算,这可能会增加电池电量使用量。如果这对您的应用程序是一个问题,您应该考虑监控电池健康状况,尤其是在进行长时间运行的计算时。
  • 应用程序大小:注意模型的大小。模型可能占用多个兆字节的空间。如果将大型模型捆绑到 APK 中会过度影响您的用户,您可能需要考虑在安装应用程序后下载模型,使用更小的模型或在云中运行计算。NNAPI 不提供在云中运行模型的功能。

请参阅 Android 神经网络 API 示例,以查看如何使用 NNAPI 的一个示例。

了解神经网络 API 运行时

NNAPI 旨在由机器学习库、框架和工具调用,这些库、框架和工具允许开发人员在设备外训练模型并在 Android 设备上部署模型。应用程序通常不会直接使用 NNAPI,而是使用更高层的机器学习框架。这些框架反过来可以使用 NNAPI 在支持的设备上执行硬件加速的推理操作。

基于应用程序的要求和 Android 设备的硬件功能,Android 的神经网络运行时可以有效地将计算工作负载分布到可用的设备上处理器,包括专用神经网络硬件、图形处理单元 (GPU) 和数字信号处理器 (DSP)。

对于缺少专用供应商驱动程序的 Android 设备,NNAPI 运行时在 CPU 上执行请求。

图 1 显示了 NNAPI 的高级系统架构。

图 1. Android 神经网络 API 的系统架构

神经网络 API 编程模型

要使用 NNAPI 执行计算,您首先需要构建一个有向图来定义要执行的计算。此计算图,加上您的输入数据(例如,从机器学习框架传递下来的权重和偏差),构成了 NNAPI 运行时评估的模型。

NNAPI 使用四个主要的抽象

  • 模型:一个包含数学运算的计算图和通过训练过程学习的常数值。这些操作特定于神经网络。它们包括二维 (2D) 卷积、逻辑 (sigmoid) 激活、线性整流 (ReLU) 激活等等。创建模型是一个同步操作。成功创建后,它可以在线程和编译之间重复使用。在 NNAPI 中,模型表示为一个 ANeuralNetworksModel 实例。
  • 编译:表示将 NNAPI 模型编译为较低级别代码的配置。创建编译是一个同步操作。成功创建后,它可以在线程和执行之间重复使用。在 NNAPI 中,每个编译都表示为一个 ANeuralNetworksCompilation 实例。
  • 内存:表示共享内存、内存映射文件以及类似的内存缓冲区。使用内存缓冲区可以让 NNAPI 运行时更有效地将数据传输到驱动程序。应用程序通常创建一个包含定义模型所需的所有张量的共享内存缓冲区。您还可以使用内存缓冲区来存储执行实例的输入和输出。在 NNAPI 中,每个内存缓冲区都表示为一个 ANeuralNetworksMemory 实例。
  • 执行:用于将 NNAPI 模型应用于一组输入并收集结果的接口。执行可以同步或异步执行。

    对于异步执行,多个线程可以等待同一个执行。当此执行完成时,所有线程都会被释放。

    在 NNAPI 中,每个执行都表示为一个 ANeuralNetworksExecution 实例。

图 2 显示了基本的编程流程。

图 2. Android 神经网络 API 的编程流程

本节的其余部分描述了设置 NNAPI 模型以执行计算、编译模型和执行已编译模型的步骤。

提供对训练数据的访问

您的训练权重和偏差数据可能存储在文件中。为了让 NNAPI 运行时能够有效地访问此数据,通过调用 ANeuralNetworksMemory_createFromFd() 函数并传入已打开的数据文件的描述符来创建一个 ANeuralNetworksMemory 实例。您还指定内存保护标志和共享内存区域在文件中的起始偏移量。

// Create a memory buffer from the file that contains the trained data
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);

虽然在本例中我们只使用一个 ANeuralNetworksMemory 实例来存储所有权重,但可以使用多个 ANeuralNetworksMemory 实例来存储多个文件。

使用原生硬件缓冲区

您可以使用 原生硬件缓冲区 来存储模型输入、输出和常量操作数的值。在某些情况下,NNAPI 加速器可以访问 AHardwareBuffer 对象,而无需驱动程序复制数据。 AHardwareBuffer 具有许多不同的配置,并非每个 NNAPI 加速器都支持所有这些配置。由于此限制,请参阅 ANeuralNetworksMemory_createFromAHardwareBuffer 参考文档 中列出的约束,并提前在目标设备上进行测试,以确保使用 AHardwareBuffer 进行编译和执行的行为符合预期,使用 设备分配 来指定加速器。

要允许 NNAPI 运行时访问 AHardwareBuffer 对象,通过调用 ANeuralNetworksMemory_createFromAHardwareBuffer 函数并传入 AHardwareBuffer 对象来创建一个 ANeuralNetworksMemory 实例,如以下代码示例所示。

// Configure and create AHardwareBuffer object
AHardwareBuffer_Desc desc = ...
AHardwareBuffer* ahwb = nullptr;
AHardwareBuffer_allocate(&desc, &ahwb);

// Create ANeuralNetworksMemory from AHardwareBuffer
ANeuralNetworksMemory* mem2 = NULL;
ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);

当 NNAPI 不再需要访问 AHardwareBuffer 对象时,释放相应的 ANeuralNetworksMemory 实例。

ANeuralNetworksMemory_free(mem2);

注意

  • 您只能将 AHardwareBuffer 用于整个缓冲区;不能将其与 ARect 参数一起使用。
  • NNAPI 运行时不会刷新缓冲区。您需要确保在调度执行之前可以访问输入和输出缓冲区。
  • 不支持同步栅栏文件描述符。
  • 对于具有供应商特定格式和使用位的 AHardwareBuffer,由供应商实现来确定客户端或驱动程序负责刷新缓存。

模型

模型是 NNAPI 中计算的基本单元。每个模型都由一个或多个操作数和操作定义。

操作数

操作数是在定义图时使用的數據对象。这些包括模型的输入和输出、包含从一个操作流向另一个操作的数据的中间节点,以及传递给这些操作的常量。

可以添加到 NNAPI 模型中的操作数有两种类型:标量张量

标量表示单个值。NNAPI 支持布尔值、16 位浮点数、32 位浮点数、32 位整数和无符号 32 位整数格式的标量值。

NNAPI 中的大多数操作都涉及张量。张量是 n 维数组。NNAPI 支持具有 16 位浮点数、32 位浮点数、8 位 量化、16 位量化、32 位整数和 8 位布尔值的张量。

例如,图 3 表示一个具有两个操作的模型:加法后乘法。模型接受一个输入张量并生成一个输出张量。

图 3. NNAPI 模型的操作数示例

上面的模型有七个操作数。这些操作数由它们添加到模型的顺序索引隐式标识。第一个添加的操作数的索引为 0,第二个的索引为 1,依此类推。操作数 1、2、3 和 5 是常量操作数。

添加操作数的顺序无关紧要。例如,模型输出操作数可以是第一个添加的操作数。重要的是在引用操作数时使用正确的索引值。

操作数具有类型。这些类型在将其添加到模型时指定。

操作数不能既用作模型的输入又用作模型的输出。

每个操作数必须是模型输入、常量或恰好一个操作的输出操作数。

有关使用操作数的更多信息,请参阅 操作数更多信息

操作

操作指定要执行的计算。每个操作都包含以下元素

  • 一种操作类型(例如,加法、乘法、卷积),
  • 操作使用用于输入的操作数的索引列表,以及
  • 操作使用用于输出的操作数的索引列表。

这些列表中的顺序很重要;请参阅 NNAPI API 参考,了解每种操作类型的预期输入和输出。

您必须在添加操作之前将操作使用的操作数或操作产生的操作数添加到模型中。

添加操作的顺序无关紧要。NNAPI 依靠操作数和操作的计算图建立的依赖关系来确定执行操作的顺序。

下表汇总了 NNAPI 支持的操作

类别 操作
逐元素数学运算
张量操作
图像操作
查找操作
规范化操作
卷积操作
池化操作
激活操作
其他操作

API 级别 28 中的已知问题:当将 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM 张量传递给在 Android 9(API 级别 28)及更高版本上可用的 ANEURALNETWORKS_PAD 操作时,NNAPI 的输出可能与来自更高级机器学习框架(如 TensorFlow Lite)的输出不匹配。您应该改为只传递 ANEURALNETWORKS_TENSOR_FLOAT32。该问题已在 Android 10(API 级别 29)及更高版本中解决。

构建模型

在以下示例中,我们创建了在 图 3 中找到的两个操作模型。

要构建模型,请按照以下步骤操作

  1. 调用 ANeuralNetworksModel_create() 函数来定义一个空模型。

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. 通过调用 ANeuralNetworks_addOperand() 将操作数添加到您的模型中。它们的类型使用 ANeuralNetworksOperandType 数据结构定义。

    // In our example, all our tensors are matrices of dimension [3][4]
    ANeuralNetworksOperandType tensor3x4Type;
    tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
    tensor3x4Type.scale = 0.f;    // These fields are used for quantized tensors
    tensor3x4Type.zeroPoint = 0;  // These fields are used for quantized tensors
    tensor3x4Type.dimensionCount = 2;
    uint32_t dims[2] = {3, 4};
    tensor3x4Type.dimensions = dims;

    // We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;

    // Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
  3. 对于具有常数值的操作数,例如您的应用程序从训练过程获得的权重和偏差,请使用 ANeuralNetworksModel_setOperandValue()ANeuralNetworksModel_setOperandValueFromMemory() 函数。

    在以下示例中,我们从与我们在 提供对训练数据的访问 中创建的内存缓冲区相对应的训练数据文件中设置常数值。

    // In our example, operands 1 and 3 are constant tensors whose values were
    // established during the training process
    const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize
    ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
    ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);

    // We set the values of the activation operands, in our example operands 2 and 5 int32_t noneValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue)); ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
  4. 对于您想要计算的有向图中的每个操作,通过调用 ANeuralNetworksModel_addOperation() 函数将操作添加到您的模型中。

    作为此调用的参数,您的应用程序必须提供

    • 操作类型
    • 输入值的计数
    • 输入操作数索引的数组
    • 输出值的计数
    • 输出操作数索引的数组

    请注意,操作数不能同时用作同一操作的输入和输出。

    // We have two operations in our example
    // The first consumes operands 1, 0, 2, and produces operand 4
    uint32_t addInputIndexes[3] = {1, 0, 2};
    uint32_t addOutputIndexes[1] = {4};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);

    // The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
  5. 通过调用 ANeuralNetworksModel_identifyInputsAndOutputs() 函数,确定模型应该将哪些操作数视为其输入和输出。

    // Our model has one input (0) and one output (6)
    uint32_t modelInputIndexes[1] = {0};
    uint32_t modelOutputIndexes[1] = {6};
    ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
    
  6. 可选地,通过调用 ANeuralNetworksModel_relaxComputationFloat32toFloat16(),指定是否允许 ANEURALNETWORKS_TENSOR_FLOAT32 以 IEEE 754 16 位浮点格式的范围或精度进行计算。

  7. 调用 ANeuralNetworksModel_finish() 来完成您的模型定义。如果没有错误,此函数将返回结果代码 ANEURALNETWORKS_NO_ERROR

    ANeuralNetworksModel_finish(model);
    

创建模型后,您可以对其进行任意次数的编译,并对每个编译进行任意次数的执行。

控制流

要在 NNAPI 模型中加入控制流,请执行以下操作

  1. 构建相应的执行子图(thenelse 子图用于 IF 语句,conditionbody 子图用于 WHILE 循环)作为独立的 ANeuralNetworksModel* 模型

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. 在包含控制流的模型中创建引用这些模型的操作数

    ANeuralNetworksOperandType modelType = {
        .type = ANEURALNETWORKS_MODEL,
    };
    ANeuralNetworksModel_addOperand(model, &modelType);  // kThenOperandIndex
    ANeuralNetworksModel_addOperand(model, &modelType);  // kElseOperandIndex
    ANeuralNetworksModel_setOperandValueFromModel(model, kThenOperandIndex, &thenModel);
    ANeuralNetworksModel_setOperandValueFromModel(model, kElseOperandIndex, &elseModel);
    
  3. 添加控制流操作

    uint32_t inputs[] = {kConditionOperandIndex,
                         kThenOperandIndex,
                         kElseOperandIndex,
                         kInput1, kInput2, kInput3};
    uint32_t outputs[] = {kOutput1, kOutput2};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF,
                                      std::size(inputs), inputs,
                                      std::size(output), outputs);
    

编译

编译步骤确定您的模型将在哪些处理器上执行,并要求相应的驱动程序为其执行做好准备。这可能包括生成特定于您的模型将运行的处理器的机器代码。

要编译模型,请按照以下步骤操作

  1. 调用 ANeuralNetworksCompilation_create() 函数来创建一个新的编译实例。

    // Compile the model
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);
    

    可选地,您可以使用 设备分配 来显式选择要执行的设备。

  2. 您可以选择性地影响运行时在电池电量使用和执行速度之间如何进行权衡。您可以通过调用 ANeuralNetworksCompilation_setPreference() 来实现这一点。

    // Ask to optimize for low power consumption
    ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
    

    您可以指定的首选项包括

  3. 您可以选择性地通过调用 ANeuralNetworksCompilation_setCaching 来设置编译缓存。

    // Set up compilation caching
    ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
    

    cacheDir 使用 getCodeCacheDir()。指定的 token 必须对应用程序中的每个模型都是唯一的。

  4. 通过调用 ANeuralNetworksCompilation_finish() 完成编译定义。如果没有错误,此函数将返回结果代码 ANEURALNETWORKS_NO_ERROR

    ANeuralNetworksCompilation_finish(compilation);
    

设备发现和分配

在运行 Android 10(API 级别 29)及更高版本的 Android 设备上,NNAPI 提供了允许机器学习框架库和应用程序获取有关可用设备的信息并指定要用于执行的设备的函数。提供有关可用设备的信息,使应用程序能够获取设备上找到的驱动程序的确切版本,以避免已知的兼容性问题。通过使应用程序能够指定哪些设备要执行模型的不同部分,应用程序可以针对其部署的 Android 设备进行优化。

设备发现

使用 ANeuralNetworks_getDeviceCount 获取可用设备的数量。对于每个设备,使用 ANeuralNetworks_getDeviceANeuralNetworksDevice 实例设置为该设备的引用。

获得设备引用后,您可以使用以下函数找出有关该设备的更多信息

设备分配

使用 ANeuralNetworksModel_getSupportedOperationsForDevices 发现模型的哪些操作可以在特定设备上运行。

要控制使用哪些加速器进行执行,请调用 ANeuralNetworksCompilation_createForDevices 来代替 ANeuralNetworksCompilation_create。正常使用生成的 ANeuralNetworksCompilation 对象。如果提供的模型包含不受所选设备支持的操作,该函数将返回错误。

如果指定了多个设备,运行时负责在这些设备之间分配工作。

与其他设备类似,NNAPI CPU 实现由名称为 nnapi-reference 且类型为 ANEURALNETWORKS_DEVICE_TYPE_CPUANeuralNetworksDevice 表示。在调用 ANeuralNetworksCompilation_createForDevices 时,CPU 实现不用于处理模型编译和执行的失败情况。

应用程序负责将模型划分为可以在指定设备上运行的子模型。不需要进行手动分区的应用程序应继续调用更简单的 ANeuralNetworksCompilation_create 来使用所有可用设备(包括 CPU)来加速模型。如果模型无法完全由您使用 ANeuralNetworksCompilation_createForDevices 指定的设备支持,则返回 ANEURALNETWORKS_BAD_DATA

模型分区

当有多个设备可用于模型时,NNAPI 运行时会将工作分配到这些设备。例如,如果向 ANeuralNetworksCompilation_createForDevices 提供了多个设备,则在分配工作时将考虑所有指定的设备。请注意,如果 CPU 设备不在列表中,则 CPU 执行将被禁用。在使用 ANeuralNetworksCompilation_create 时,将考虑所有可用设备,包括 CPU。

分配是通过从可用设备列表中为模型中的每个操作选择支持该操作并声明最佳性能(即最快的执行时间或最低的功耗,具体取决于客户端指定的执行首选项)来完成的。此分区算法不考虑不同处理器之间 I/O 造成的潜在效率低下,因此,在指定多个处理器时(无论是在使用 ANeuralNetworksCompilation_createForDevices 时显式指定,还是在使用 ANeuralNetworksCompilation_create 时隐式指定),务必对结果应用程序进行性能分析。

要了解您的模型是如何由 NNAPI 分区的,请检查 Android 日志中是否存在消息(在 INFO 级别,标签为 ExecutionPlan

ModelBuilder::findBestDeviceForEachOperation(op-name): device-index

op-name 是图中操作的描述性名称,device-index 是候选设备在设备列表中的索引。此列表是提供给 ANeuralNetworksCompilation_createForDevices 的输入,或者,如果使用 ANeuralNetworksCompilation_createForDevices,则是使用 ANeuralNetworks_getDeviceCountANeuralNetworks_getDevice 遍历所有设备时返回的设备列表。

消息(在 INFO 级别,标签为 ExecutionPlan

ModelBuilder::partitionTheWork: only one best device: device-name

此消息表明整个图已在设备 device-name 上加速。

执行

执行步骤将模型应用于一组输入,并将计算输出存储到您的应用程序分配的一个或多个用户缓冲区或内存空间。

要执行已编译的模型,请按照以下步骤操作

  1. 调用 ANeuralNetworksExecution_create() 函数来创建一个新的执行实例。

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. 指定您的应用程序从何处读取计算的输入值。您的应用程序可以通过分别调用 ANeuralNetworksExecution_setInput()ANeuralNetworksExecution_setInputFromMemory() 从用户缓冲区或分配的内存空间读取输入值。

    // Set the single input to our sample model. Since it is small, we won't use a memory buffer
    float32 myInput[3][4] = { ...the data... };
    ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
    
  3. 指定应用程序写入输出值的位置。应用程序可以通过调用 ANeuralNetworksExecution_setOutput()ANeuralNetworksExecution_setOutputFromMemory() 将输出值写入用户缓冲区或分配的内存空间。

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. 通过调用 ANeuralNetworksExecution_startCompute() 函数调度执行开始。如果没有任何错误,此函数将返回结果代码 ANEURALNETWORKS_NO_ERROR

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. 调用 ANeuralNetworksEvent_wait() 函数等待执行完成。如果执行成功,此函数将返回结果代码 ANEURALNETWORKS_NO_ERROR。等待可以在与启动执行的线程不同的线程上进行。

    // For our example, we have no other work to do and will just wait for the completion
    ANeuralNetworksEvent_wait(run1_end);
    ANeuralNetworksEvent_free(run1_end);
    ANeuralNetworksExecution_free(run1);
    
  6. 可以选择使用相同的编译实例创建新的 ANeuralNetworksExecution 实例,将不同的输入集应用于已编译的模型。

    // Apply the compiled model to a different set of inputs
    ANeuralNetworksExecution* run2;
    ANeuralNetworksExecution_create(compilation, &run2);
    ANeuralNetworksExecution_setInput(run2, ...);
    ANeuralNetworksExecution_setOutput(run2, ...);
    ANeuralNetworksEvent* run2_end = NULL;
    ANeuralNetworksExecution_startCompute(run2, &run2_end);
    ANeuralNetworksEvent_wait(run2_end);
    ANeuralNetworksEvent_free(run2_end);
    ANeuralNetworksExecution_free(run2);
    

同步执行

异步执行会花费时间来生成和同步线程。此外,延迟可能会有很大差异,从通知或唤醒线程的时间到线程最终绑定到 CPU 内核的时间,最长延迟可能达到 500 微秒。

为了提高延迟,您可以改为将应用程序定向到对运行时进行同步推理调用。该调用仅在推理完成后才返回,而不是在推理开始后就返回。应用程序不是调用 ANeuralNetworksExecution_startCompute 来对运行时进行异步推理调用,而是调用 ANeuralNetworksExecution_compute 来对运行时进行同步调用。对 ANeuralNetworksExecution_compute 的调用不接受 ANeuralNetworksEvent,也不与对 ANeuralNetworksEvent_wait 的调用配对。

突发执行

在运行 Android 10(API 级别 29)及更高版本的 Android 设备上,NNAPI 通过 ANeuralNetworksBurst 对象支持突发执行。突发执行是指对同一编译的快速连续执行序列,例如对相机捕获的帧或连续音频样本进行操作的序列。使用 ANeuralNetworksBurst 对象可能会导致更快的执行,因为它们指示加速器可以在执行之间重用资源,并且加速器应在突发持续时间内保持高性能状态。

ANeuralNetworksBurst 只对正常的执行路径进行了很小的改变。您可以使用 ANeuralNetworksBurst_create 创建一个突发对象,如以下代码片段所示

// Create burst object to be reused across a sequence of executions
ANeuralNetworksBurst* burst = NULL;
ANeuralNetworksBurst_create(compilation, &burst);

突发执行是同步的。但是,您不是使用 ANeuralNetworksExecution_compute 执行每个推理,而是将各种 ANeuralNetworksExecution 对象与同一个 ANeuralNetworksBurst 配对,并在调用 ANeuralNetworksExecution_burstCompute 函数时使用这些对象。

// Create and configure first execution object
// ...

// Execute using the burst object
ANeuralNetworksExecution_burstCompute(execution1, burst);

// Use results of first execution and free the execution object
// ...

// Create and configure second execution object
// ...

// Execute using the same burst object
ANeuralNetworksExecution_burstCompute(execution2, burst);

// Use results of second execution and free the execution object
// ...

当不再需要 ANeuralNetworksBurst 对象时,请使用 ANeuralNetworksBurst_free 释放它。

// Cleanup
ANeuralNetworksBurst_free(burst);

异步命令队列和带围栏的执行

在 Android 11 及更高版本中,NNAPI 支持通过 ANeuralNetworksExecution_startComputeWithDependencies() 方法以另一种方式调度异步执行。当您使用此方法时,执行会等待所有依赖事件发出信号,然后再开始评估。执行完成后,输出准备好被使用后,返回的事件将发出信号。

根据处理执行的设备,事件可能由 同步围栏 支持。您必须调用 ANeuralNetworksEvent_wait() 来等待事件并恢复执行使用的资源。您可以使用 ANeuralNetworksEvent_createFromSyncFenceFd() 将同步围栏导入到事件对象中,也可以使用 ANeuralNetworksEvent_getSyncFenceFd() 从事件对象中导出同步围栏。

动态大小的输出

要支持输出大小取决于输入数据的模型(即大小不能在模型执行时确定),请使用 ANeuralNetworksExecution_getOutputOperandRankANeuralNetworksExecution_getOutputOperandDimensions

以下代码示例演示了如何执行此操作

// Get the rank of the output
uint32_t myOutputRank = 0;
ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank);

// Get the dimensions of the output
std::vector<uint32_t> myOutputDimensions(myOutputRank);
ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());

清理

清理步骤处理为计算释放内部资源。

// Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);

错误管理和 CPU 回退

如果在分区期间发生错误,如果驱动程序无法编译(部分)模型,或者如果驱动程序无法执行已编译的(部分)模型,NNAPI 可能会回退到其对一个或多个操作的自己的 CPU 实现。

如果 NNAPI 客户端包含操作的优化版本(例如,TFLite),则禁用 CPU 回退并使用客户端的优化操作实现处理故障可能更有利。

在 Android 10 中,如果使用 ANeuralNetworksCompilation_createForDevices 执行编译,则将禁用 CPU 回退。

在 Android P 中,如果驱动程序上的执行失败,NNAPI 执行将回退到 CPU。当使用 ANeuralNetworksCompilation_create 而不是 ANeuralNetworksCompilation_createForDevices 时,在 Android 10 上也是如此。

第一次执行将为该单个分区回退,如果仍然失败,它将尝试在 CPU 上重试整个模型。

如果分区或编译失败,将尝试在 CPU 上运行整个模型。

在某些情况下,CPU 上不支持某些操作,在这种情况下,编译或执行将失败,而不是回退。

即使在禁用 CPU 回退后,模型中可能仍然存在调度到 CPU 上的操作。如果 CPU 在提供给 ANeuralNetworksCompilation_createForDevices 的处理器列表中,并且是唯一支持这些操作的处理器或声称对这些操作的性能最佳的处理器,它将被选为主要(非回退)执行器。

要确保没有 CPU 执行,请使用 ANeuralNetworksCompilation_createForDevices,同时将 nnapi-reference 从设备列表中排除。从 Android P 开始,可以在 DEBUG 版本中通过将 debug.nn.partition 属性设置为 2 来在执行时禁用回退。

内存域

在 Android 11 及更高版本中,NNAPI 支持内存域,这些域为不透明内存提供分配器接口。这允许应用程序在执行之间传递设备本机内存,这样 NNAPI 在对同一驱动程序执行连续执行时就不会不必要地复制或转换数据。

内存域功能 предназначен для тензоров, которые в основном являются внутренними для драйвера и не требуют частого доступа к клиентской стороне. Примерами таких тензоров являются тензоры состояния в последовательных моделях. Для тензоров, которые требуют частого доступа к ЦП на клиентской стороне, вместо этого используйте общие пулы памяти.

要分配不透明内存,请执行以下步骤

  1. 调用 ANeuralNetworksMemoryDesc_create() 函数创建一个新的内存描述符

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. 通过调用 ANeuralNetworksMemoryDesc_addInputRole()ANeuralNetworksMemoryDesc_addOutputRole() 指定所有预期的输入和输出角色。

    // Specify that the memory may be used as the first input and the first output
    // of the compilation
    ANeuralNetworksMemoryDesc_addInputRole(desc, compilation, 0, 1.0f);
    ANeuralNetworksMemoryDesc_addOutputRole(desc, compilation, 0, 1.0f);
    
  3. 可以选择通过调用 ANeuralNetworksMemoryDesc_setDimensions() 指定内存维度。

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. 通过调用 ANeuralNetworksMemoryDesc_finish() 完成描述符定义。

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. 通过将描述符传递给 ANeuralNetworksMemory_createFromDesc(),分配您需要的内存数量。

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. 当不再需要内存描述符时,请释放它。

    ANeuralNetworksMemoryDesc_free(desc);
    

客户端只能使用创建的 ANeuralNetworksMemory 对象,通过 ANeuralNetworksExecution_setInputFromMemory()ANeuralNetworksExecution_setOutputFromMemory() 来使用,这取决于在 ANeuralNetworksMemoryDesc 对象中指定的 roles。偏移量和长度参数必须设置为 0,表示使用整个内存。客户端还可以使用 ANeuralNetworksMemory_copy() 显式设置或提取内存的内容。

您可以创建具有未指定维度或秩的角色的不透明内存。在这种情况下,如果底层驱动程序不支持,则内存创建可能会失败,并出现 ANEURALNETWORKS_OP_FAILED 状态。鼓励客户端通过分配一个由 Ashmem 或 BLOB 模式 AHardwareBuffer 支持的足够大的缓冲区来实现回退逻辑。

当 NNAPI 不再需要访问不透明内存对象时,请释放相应的 ANeuralNetworksMemory 实例

ANeuralNetworksMemory_free(opaqueMem);

衡量性能

您可以通过衡量执行时间或分析来评估应用程序的性能。

执行时间

如果您想通过运行时确定总执行时间,可以使用同步执行 API 并测量调用所花费的时间。如果您想通过软件堆栈的较低级别确定总执行时间,可以使用 ANeuralNetworksExecution_setMeasureTimingANeuralNetworksExecution_getDuration 来获取

  • 加速器上的执行时间(不在驱动程序中,驱动程序在主机处理器上运行)。
  • 驱动程序中的执行时间,包括加速器上的时间。

驱动程序中的执行时间不包括运行时本身和运行时与驱动程序通信所需的 IPC 等开销。

这些 API 测量的是提交工作和完成工作事件之间的时间段,而不是驱动程序或加速器用于执行推理的时间,这些时间可能会因上下文切换而中断。

例如,如果推理 1 开始,然后驱动程序停止工作以执行推理 2,然后恢复并完成推理 1,则推理 1 的执行时间将包括停止工作以执行推理 2 的时间。

此计时信息可能对应用程序的生产部署有用,以收集遥测数据以供离线使用。您可以使用计时数据修改应用程序以获得更高的性能。

在使用此功能时,请牢记以下几点

  • 收集计时信息可能会带来性能成本。
  • 只有驱动程序才能计算在自身或加速器上花费的时间,不包括在 NNAPI 运行时和 IPC 中花费的时间。
  • 您只能对使用 ANeuralNetworksCompilation_createForDevices 创建的 ANeuralNetworksExecution 使用这些 API,其中 numDevices = 1
  • 不需要驱动程序就能报告计时信息。

使用 Android Systrace 分析您的应用程序

从 Android 10 开始,NNAPI 会自动生成 systrace 事件,您可以使用这些事件来分析您的应用程序。

NNAPI 源代码附带了一个 parse_systrace 实用程序,用于处理应用程序生成的 systrace 事件并生成一个表格视图,显示模型生命周期的不同阶段(实例化、准备、编译执行和终止)和应用程序的不同层所花费的时间。应用程序划分的层级如下

  • Application:主要应用程序代码
  • Runtime:NNAPI 运行时
  • IPC:NNAPI 运行时与驱动程序代码之间的进程间通信
  • Driver:加速器驱动程序进程。

生成分析配置文件数据

假设您在 $ANDROID_BUILD_TOP 处检出了 AOSP 源代码树,并使用 TFLite 图像分类示例 作为目标应用程序,您可以按照以下步骤生成 NNAPI 分析配置文件数据

  1. 使用以下命令启动 Android systrace
$ANDROID_BUILD_TOP/external/chromium-trace/systrace.py  -o trace.html -a org.tensorflow.lite.examples.classification nnapi hal freq sched idle load binder_driver

-o trace.html 参数表示跟踪将写入 trace.html。在分析自己的应用程序时,您需要将 org.tensorflow.lite.examples.classification 替换为应用程序清单中指定的进程名称。

这将使您的一个 shell 控制台保持繁忙,不要在后台运行该命令,因为它会交互式地等待 enter 来终止。

  1. 启动 systrace 收集器后,启动您的应用程序并运行基准测试。

在我们的示例中,您可以从 Android Studio 或直接从测试手机 UI 启动“图像分类”应用程序(如果该应用程序已安装)。为了生成一些 NNAPI 数据,您需要配置应用程序以使用 NNAPI,方法是在应用程序配置对话框中选择 NNAPI 作为目标设备。

  1. 测试完成后,按自步骤 1 开始就一直活动的控制台终端上的 enter 来终止 systrace。

  2. 运行 systrace_parser 实用程序来生成累积统计信息

$ANDROID_BUILD_TOP/frameworks/ml/nn/tools/systrace_parser/parse_systrace.py --total-times trace.html

解析器接受以下参数: - --total-times:显示在层级中花费的总时间,包括在调用底层层级时等待执行所花费的时间 - --print-detail:打印从 systrace 收集的所有事件 - --per-execution:仅打印执行及其子阶段(作为每个执行的时间),而不是所有阶段的统计信息 - --json:以 JSON 格式生成输出

下面显示了一个输出示例

===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock)                                                      Execution
                                                           ----------------------------------------------------
              Initialization   Preparation   Compilation           I/O       Compute      Results     Ex. total   Termination        Total
              --------------   -----------   -----------   -----------  ------------  -----------   -----------   -----------   ----------
Application              n/a         19.06       1789.25           n/a           n/a         6.70         21.37           n/a      1831.17*
Runtime                    -         18.60       1787.48          2.93         11.37         0.12         14.42          1.32      1821.81
IPC                     1.77             -       1781.36          0.02          8.86            -          8.88             -      1792.01
Driver                  1.04             -       1779.21           n/a           n/a          n/a          7.70             -      1787.95

Total                   1.77*        19.06*      1789.25*         2.93*        11.74*        6.70*        21.37*         1.32*     1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers

如果收集的事件不代表完整的应用程序跟踪,则解析器可能会失败。特别是,如果跟踪中存在标记部分结束的 systrace 事件,但没有关联的部分开始事件,则解析器可能会失败。这通常发生在您启动 systrace 收集器时生成了一些来自先前分析配置文件会话的事件。在这种情况下,您需要重新运行分析配置文件。

将应用程序代码的统计信息添加到 systrace_parser 输出中

parse_systrace 应用程序基于内置的 Android systrace 功能。您可以使用 systrace API (对于 Java对于本机应用程序) 使用自定义事件名称为应用程序中的特定操作添加跟踪。

要将自定义事件与应用程序生命周期的阶段关联起来,请在事件名称前面加上以下字符串之一

  • [NN_LA_PI]:应用程序级事件,用于初始化
  • [NN_LA_PP]:应用程序级事件,用于准备
  • [NN_LA_PC]:应用程序级事件,用于编译
  • [NN_LA_PE]:应用程序级事件,用于执行

以下是如何通过为 Execution 阶段添加 runInferenceModel 部分以及包含其他部分 preprocessBitmap(不会被 NNAPI 跟踪考虑在内)的 Application 层来更改 TFLite 图像分类示例代码。 runInferenceModel 部分将成为由 nnapi systrace 解析器处理的 systrace 事件的一部分

Kotlin

/** Runs inference and returns the classification results. */
fun recognizeImage(bitmap: Bitmap): List {
   // This section won’t appear in the NNAPI systrace analysis
   Trace.beginSection("preprocessBitmap")
   convertBitmapToByteBuffer(bitmap)
   Trace.endSection()

   // Run the inference call.
   // Add this method in to NNAPI systrace analysis.
   Trace.beginSection("[NN_LA_PE]runInferenceModel")
   long startTime = SystemClock.uptimeMillis()
   runInference()
   long endTime = SystemClock.uptimeMillis()
   Trace.endSection()
    ...
   return recognitions
}

Java

/** Runs inference and returns the classification results. */
public List recognizeImage(final Bitmap bitmap) {

 // This section won’t appear in the NNAPI systrace analysis
 Trace.beginSection("preprocessBitmap");
 convertBitmapToByteBuffer(bitmap);
 Trace.endSection();

 // Run the inference call.
 // Add this method in to NNAPI systrace analysis.
 Trace.beginSection("[NN_LA_PE]runInferenceModel");
 long startTime = SystemClock.uptimeMillis();
 runInference();
 long endTime = SystemClock.uptimeMillis();
 Trace.endSection();
  ...
 Trace.endSection();
 return recognitions;
}

服务质量

在 Android 11 及更高版本中,NNAPI 通过允许应用程序指示其模型的相对优先级、准备给定模型所需的预期最大时间以及完成给定计算所需的预期最大时间来提供更好的服务质量 (QoS)。Android 11 还引入了其他 NNAPI 结果代码,使应用程序能够了解诸如错过执行截止日期之类的错误。

设置工作负载的优先级

要设置 NNAPI 工作负载的优先级,请在调用 ANeuralNetworksCompilation_finish() 之前调用 ANeuralNetworksCompilation_setPriority()

设置截止日期

应用程序可以为模型编译和推理都设置截止日期。

关于操作数的更多信息

以下部分介绍了关于使用操作数的进阶主题。

量化张量

量化张量是一种紧凑的方式,用于表示浮点数值的 n 维数组。

NNAPI 支持 8 位非对称量化张量。对于这些张量,每个单元格的值由一个 8 位整数表示。与张量相关联的是一个比例和一个零点值。这些用于将 8 位整数转换为正在表示的浮点数。

公式如下

(cellValue - zeroPoint) * scale

其中 zeroPoint 值是一个 32 位整数,比例是一个 32 位浮点数。

与 32 位浮点数的张量相比,8 位量化张量具有两个优点

  • 您的应用程序更小,因为训练后的权重仅为 32 位张量大小的四分之一。
  • 计算通常可以更快地执行。这是因为需要从内存中获取的数据量更少,以及诸如 DSP 之类的处理器在执行整数数学运算方面的效率更高。

虽然可以将浮点模型转换为量化模型,但我们的经验表明,直接训练量化模型可以获得更好的结果。实际上,神经网络会学习补偿每个值的粒度增加。对于每个量化张量,比例和 zeroPoint 值在训练过程中确定。

在 NNAPI 中,您可以通过将 ANeuralNetworksOperandType 数据结构的 type 字段设置为 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM 来定义量化张量类型。您还需要在该数据结构中指定张量的比例和 zeroPoint 值。

除了 8 位非对称量化张量之外,NNAPI 还支持以下内容

可选操作数

一些操作(如 ANEURALNETWORKS_LSH_PROJECTION)接受可选操作数。为了在模型中指示省略了可选操作数,请调用 ANeuralNetworksModel_setOperandValue() 函数,将 NULL 传递给缓冲区,并将 0 传递给长度。

如果操作数是否存在取决于每次执行,则可以通过使用 ANeuralNetworksExecution_setInput()ANeuralNetworksExecution_setOutput() 函数(将 NULL 传递给缓冲区,并将 0 传递给长度)来指示操作数被省略。

秩未知的张量

Android 9(API 级别 28)引入了维度未知但秩已知(维度数)的模型操作数。Android 10(API 级别 29)引入了秩未知的张量,如 ANeuralNetworksOperandType 所示。

NNAPI 基准测试

NNAPI 基准测试在 AOSP 中的 platform/test/mlts/benchmark(基准测试应用程序)和 platform/test/mlts/models(模型和数据集)目录中可用。

该基准测试评估延迟和准确性,并将驱动程序与使用在 CPU 上运行的 TensorFlow Lite 完成的相同工作进行比较,用于相同的模型和数据集。

要使用该基准测试,请执行以下操作:

  1. 将目标 Android 设备连接到您的计算机,打开一个终端窗口,并确保设备可以通过 adb 访问。

  2. 如果连接了多个 Android 设备,请导出目标设备的 ANDROID_SERIAL 环境变量。

  3. 导航到 Android 的顶层源代码目录。

  4. 运行以下命令:

    lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available
    ./test/mlts/benchmark/build_and_run_benchmark.sh
    

    在基准测试运行结束时,其结果将以 HTML 页面形式呈现,并传递给 xdg-open

NNAPI 日志

NNAPI 在系统日志中生成有用的诊断信息。要分析日志,请使用 logcat 工具。

通过将属性 debug.nn.vlog(使用 adb shell)设置为以下值列表(用空格、冒号或逗号分隔),为特定阶段或组件启用详细的 NNAPI 日志记录:

  • model:模型构建
  • compilation:生成模型执行计划和编译
  • execution:模型执行
  • cpuexe:使用 NNAPI CPU 实现执行操作
  • manager:NNAPI 扩展、可用接口和功能相关信息
  • all1:以上所有元素

例如,要启用完整的详细日志记录,请使用命令 adb shell setprop debug.nn.vlog all。要禁用详细日志记录,请使用命令 adb shell setprop debug.nn.vlog '""'

启用后,详细日志记录将在 INFO 级别生成日志条目,标签设置为阶段或组件名称。

除了 debug.nn.vlog 控制的消息外,NNAPI API 组件还提供其他日志条目,这些条目在不同级别,每个条目使用特定的日志标签。

要获取组件列表,请使用以下表达式搜索源代码树:

grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"

此表达式目前返回以下标签:

  • BurstBuilder
  • Callbacks
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Manager
  • Memory
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • 操作
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Utils
  • ValidateHal
  • VersionedInterfaces

要控制 logcat 显示的日志消息级别,请使用环境变量 ANDROID_LOG_TAGS

要显示完整的 NNAPI 日志消息集并禁用其他任何消息,请将 ANDROID_LOG_TAGS 设置为以下值:

BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.

您可以使用以下命令设置 ANDROID_LOG_TAGS

export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')

请注意,这只是一个适用于 logcat 的过滤器。您仍然需要将属性 debug.nn.vlog 设置为 all 才能生成详细的日志信息。