Neural Networks API

Android Neural Networks API (NNAPI) 是一个 Android C API,专为在 Android 设备上运行机器学习的计算密集型运算而设计。NNAPI 旨在为更高级别的机器学习框架(如 TensorFlow Lite 和 Caffe2)提供一个基本功能层,用来建立和训练神经网络。搭载 Android 8.1(API 级别 27)或更高版本的所有 Android 设备上都提供该 API。

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

设备上推断具备诸多优势:

  • 延迟:您不需要通过网络连接发送请求并等待响应。例如,这对处理来自摄像头的连续帧的视频应用至关重要。
  • 可用性:即使在网络覆盖范围之外,应用也能运行。
  • 速度:专用于神经网络处理的新硬件提供的计算速度明显快于单纯的通用 CPU。
  • 隐私:数据不会从 Android 设备中泄露出去。
  • 费用:所有计算都在 Android 设备上执行,不需要服务器场。

开发者还应在以下几个方面做出权衡取舍:

  • 系统利用率:评估神经网络涉及大量的计算,而这可能会增加电池电量消耗。如果您担心自己的应用耗电量会增加(尤其是对于长时间运行的计算),应考虑监控电池运行状况。
  • 应用大小:应注意模型的大小。模型可能会占用数兆字节的空间。如果在 APK 中绑定较大的模型会对用户造成过度影响,那么您可能需要考虑在应用安装后下载模型、使用较小的模型或在云端运行计算。NNAPI 未提供在云端运行模型的功能。

请参阅 Android Neural Networks API 示例,查看关于如何使用 NNAPI 的一个示例。

了解 Neural Networks API 运行时

NNAPI 应由机器学习库、框架和工具调用,这样可让开发者在设备外训练他们的模型,并将其部署在 Android 设备上。应用一般不会直接使用 NNAPI,而会使用更高级别的机器学习框架。这些框架进而又可以使用 NNAPI 在受支持的设备上执行硬件加速的推断运算。

根据应用的要求和 Android 设备的硬件功能,Android 的神经网络运行时可以在可用的设备上处理器(包括专用的神经网络硬件、图形处理单元 (GPU) 和数字信号处理器 (DSP))之间高效地分配计算工作负载。

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

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

图 1. Android Neural Networks API 的系统架构

Neural Networks API 编程模型

要使用 NNAPI 执行计算,您需要先构造一张有向图来定义要执行的计算。此计算图与您的输入数据(例如,从机器学习框架传递过来的权重和偏差)相结合,构成 NNAPI 运行时求值的模型。

NNAPI 使用四个主要抽象概念:

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

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

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

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

图 2. Android Neural Networks API 的编程流程

本部分的其余内容将介绍一些具体步骤,说明如何设置 NNAPI 模型以执行计算、编译模型并执行编译的模型。

提供训练数据访问权限

您的训练权重和偏差数据可能存储在一个文件中。要让 NNAPI 运行时能够高效地访问此数据,请创建一个 ANeuralNetworksMemory 实例,方法是调用 ANeuralNetworksMemory_createFromFd() 函数并传入已打开的数据文件的文件描述符。您也可以指定内存保护标志和文件中共享内存区域开始处的偏移。

// 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 实例,方法是调用 ANeuralNetworksMemory_createFromAHardwareBuffer 函数并传入该 AHardwareBuffer 对象,如以下代码示例所示:

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

    // 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(),指定是否允许计算范围或精度像 IEEE 754 16 位浮点格式的范围或精度一样低的 ANEURALNETWORKS_TENSOR_FLOAT32

  7. 调用 ANeuralNetworksModel_finish() 来最终确定模型的定义。如果没有出现错误,此函数将返回结果代码 ANEURALNETWORKS_NO_ERROR

        ANeuralNetworksModel_finish(model);
        

创建模型后,您可以将其编译任意次数,也可以将每项编译执行任意次数。

编译

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

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

  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_getDevice 设置一个 ANeuralNetworksDevice 实例,使其引用该设备。

设置设备引用后,您可以使用以下函数找到有关该设备的其他信息:

设备分配

使用 ANeuralNetworksModel_getSupportedOperationsForDevices 可了解模型的哪些运算可以在特定设备上运行。

要控制用于执行的加速器,请调用 ANeuralNetworksCompilation_createForDevices 来代替 ANeuralNetworksCompilation_create。像往常一样使用生成的 ANeuralNetworksCompilation 对象。如果提供的模型包含选定设备不支持的运算,此函数将返回错误。

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

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

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

执行

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

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

  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_compute 以对运行时进行同步推断调用,而不是调用 ANeuralNetworksExecution_startCompute 以对运行时进行异步推断调用。对 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 像往常一样在使用前配置而在完成时消耗。不过,您是在对 ANeuralNetworksExecution_burstCompute 函数的调用中将各个 ANeuralNetworksExecution 对象与同一 ANeuralNetworksBurst 配对,而不是使用 ANeuralNetworksExecution_startComputeANeuralNetworksExecution_compute 执行推断。

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

大小动态变化的输出

要支持输出大小取决于输入数据(即,在模型执行时无法确定大小)的模型,请使用 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());
    

衡量性能

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

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

驱动程序中的执行时间不包括一些开销,如运行时本身以及运行时与驱动程序进行通信所需 IPC 的开销。

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

例如,如果推断 1 先开始,而后驱动程序为了执行推断 2 而停止工作,后来又继续执行并完成推断 1,那么推断 1 的执行时间将包括为了执行推断 2 而停止工作的时间。

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

使用此功能时,请记住以下几点:

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

清理

清理步骤可以释放用于计算的内部资源。

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

运算数详细说明

接下来这部分介绍了关于使用操作数的高级主题。

量化张量

量化张量是一种表示 N 维浮点值数组的简洁方式。

NNAPI 支持 8 位非对称量化张量。对于这些张量,每个单元格的值都通过一个 8 位整数表示。与张量相关联的是一个比例和一个零点值。这几项可用于将 8 位整数转换成要表示的浮点值。

公式为:

    (cellValue - zeroPoint) * scale
    

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

与 32 位浮点值的张量相比,8 位量化张量具有以下两个优势:

  • 您的应用变得更小,因为训练的权重占 32 位张量大小的四分之一。
  • 计算通常可以更快地执行。这是因为您只需要从内存提取少量数据,而且 DSP 等处理器进行整数数学运算的效率更高。

尽管您可以将浮点值模型转换成量化模型,但我们的经验表明,直接训练量化模型可以获取更好的结果。事实上,神经网络会通过学习来补偿每个值增大的粒度。对于每个量化张量,scale 和 zeroPoint 值会在训练过程中确定。

在 NNAPI 中,您可以将 ANeuralNetworksOperandType 数据结构的类型字段设为 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM 来定义量化张量类型。您还可以在该数据结构中指定张量的 scale 和 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 基准

AOSP 中提供了 NNAPI 基准,具体位于 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
        

    在基准运行结束时,其结果将显示为传递给 xdg-open 的 HTML 页面。