神经网络 API

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

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

设备端推理有许多优势:

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

开发者也应注意一些权衡:

  • 系统利用率:评估神经网络涉及大量计算,这会增加电池电量消耗。如果这对你的应用来说是个问题,尤其是对于长时间运行的计算,应考虑监控电池健康状况。
  • 应用大小:注意你的模型大小。模型可能会占用数 MB 空间。如果将大型模型打包到你的 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 使用四种主要抽象:

  • 模型:数学运算和通过训练过程学习的常量值的计算图。这些运算特定于神经网络。它们包括 2 维 (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 运行时不会刷新缓冲区。在调度执行之前,你需要确保输入和输出缓冲区是可访问的。
  • 不支持同步 fence 文件描述符。
  • 对于具有供应商特定格式和使用位的 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 张量传递给 ANEURALNETWORKS_PAD 操作时(在 Android 9 (API 级别 28) 及更高版本中可用),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() 指定是否允许以低至 IEEE 754 16 位浮点格式的范围或精度计算 ANEURALNETWORKS_TENSOR_FLOAT32

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

    ANeuralNetworksModel_finish(model);

创建模型后,可以编译任意次,并执行每次编译任意次。

控制流

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

  1. 将相应的执行子图(用于 IF 语句的 thenelse 子图,用于 WHILE 循环的 conditionbody 子图)作为独立的 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_createForDevices 时,所有可用设备都将被考虑在内,包括 CPU。

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

要了解 NNAPI 如何对你的模型进行分区,请检查 Android 日志中是否有消息(INFO 级别,tag 为 ExecutionPlan):

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

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

消息(INFO 级别,tag 为 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);

同步执行

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

为了改善延迟,你可以指示应用向运行时发出同步推理调用。该调用只有在推理完成后才会返回,而不是在推理开始后就返回。应用不是调用 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);

异步命令队列和 fenced 执行

在 Android 11 及更高版本中,NNAPI 支持通过 ANeuralNetworksExecution_startComputeWithDependencies() 方法调度异步执行的额外方式。使用此方法时,执行会等待所有依赖事件发出信号后才开始评估。执行完成后且输出准备就绪后,返回的事件就会发出信号。

根据哪些设备处理执行,事件可能由同步 fence 支持。你必须调用 ANeuralNetworksEvent_wait() 等待事件并回收执行使用的资源。可以使用 ANeuralNetworksEvent_createFromSyncFenceFd() 将同步 fence 导入到事件对象,并使用 ANeuralNetworksEvent_getSyncFenceFd() 从事件对象导出同步 fence。

动态大小的输出

为了支持输出大小取决于输入数据(即执行模型时大小无法确定)的模型,请使用 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。在 Android 10 中使用 ANeuralNetworksCompilation_create 而不是 ANeuralNetworksCompilation_createForDevices 时也是如此。

首次执行会回退到该单个分区,如果仍失败,则会在 CPU 上重试整个模型。

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

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

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

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

内存域

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

内存域功能适用于那些主要在驱动程序内部、不需要频繁访问客户端的张量。此类张量的示例包括序列模型中的状态张量。对于需要频繁在客户端上访问 CPU 的张量,请使用共享内存池。

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

  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);

客户端只能根据 ANeuralNetworksMemoryDesc 对象中指定的角色,通过 ANeuralNetworksExecution_setInputFromMemory()ANeuralNetworksExecution_setOutputFromMemory() 使用创建的 ANeuralNetworksMemory 对象。偏移量和长度参数必须设置为 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(其中 numDevices = 1)创建的 ANeuralNetworksExecution 中使用这些 API。
  • 任何驱动程序都不需要能够报告计时信息。

使用 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]:执行阶段的应用级别事件

下面是一个示例,展示如何通过添加用于“执行”阶段的 runInferenceModel 部分和包含另一个不包含在 NNAPI 跟踪中的 preprocessBitmap 部分的“应用”层来修改 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 等处理器在进行整数数学运算方面效率较高。

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

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

除了 8 位非对称量化张量外,NNAPI 还支持以下类型:

可选操作数

一些操作(例如 ANEURALNETWORKS_LSH_PROJECTION)接受可选操作数。要在模型中指示省略了可选操作数,请调用 ANeuralNetworksModel_setOperandValue() 函数,并将 buffer 传递 NULL,length 传递 0。

如果操作数是否存在取决于每次执行的不同,则通过使用 ANeuralNetworksExecution_setInput()ANeuralNetworksExecution_setOutput() 函数并传递 NULL 作为 buffer,0 作为 length 来指示省略了操作数。

未知等级的张量

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

NNAPI 基准测试

NNAPI 基准测试在 AOSP 中的 platform/test/mlts/benchmark(基准测试应用)和 platform/test/mlts/models(模型和数据集)中提供。

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

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

  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 才能生成详细日志信息。