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 的高级系统架构。
神经网络 API 编程模型
要使用 NNAPI 执行计算,您首先需要构建一个有向图,该图定义要执行的计算。此计算图与您的输入数据(例如,从机器学习框架传递下来的权重和偏差)一起构成 NNAPI 运行时评估的模型。
NNAPI 使用四个主要的抽象概念
- 模型:数学运算的计算图和通过训练过程学习的常数值。这些运算特定于神经网络。它们包括二维 (2D) 卷积、逻辑 (sigmoid) 激活、修正线性单元 (ReLU) 激活等等。创建模型是一个同步操作。成功创建后,它可以在线程和编译之间重复使用。在 NNAPI 中,模型表示为一个
ANeuralNetworksModel
实例。 - 编译:表示将 NNAPI 模型编译成较低级别代码的配置。创建编译是一个同步操作。成功创建后,它可以在线程和执行之间重复使用。在 NNAPI 中,每个编译都表示为一个
ANeuralNetworksCompilation
实例。 - 内存:表示共享内存、内存映射文件和类似的内存缓冲区。使用内存缓冲区可以让 NNAPI 运行时更有效地将数据传输到驱动程序。应用通常会创建一个共享内存缓冲区,其中包含定义模型所需的每个张量。您还可以使用内存缓冲区来存储执行实例的输入和输出。在 NNAPI 中,每个内存缓冲区都表示为一个
ANeuralNetworksMemory
实例。 执行:用于将 NNAPI 模型应用于一组输入并收集结果的接口。执行可以同步或异步执行。
对于异步执行,多个线程可以等待相同的执行。当此执行完成时,所有线程都将被释放。
在 NNAPI 中,每次执行都表示为一个
ANeuralNetworksExecution
实例。
图 2 显示了基本的编程流程。
本节的其余部分描述了设置 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 表示一个具有两个操作的模型:加法后跟乘法。该模型接收一个输入张量并产生一个输出张量。
上面的模型有七个操作数。这些操作数由它们添加到模型的顺序索引隐式标识。添加的第一个操作数的索引为 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中找到的包含两个操作的模型。
要构建模型,请按照以下步骤操作
调用
ANeuralNetworksModel_create()
函数来定义一个空模型。ANeuralNetworksModel* model = NULL; ANeuralNetworksModel_create(&model);
通过调用
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对于具有常数值的操作数(例如,应用程序从训练过程中获得的权重和偏差),请使用
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));对于要计算的有向图中的每个操作,通过调用
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);通过调用
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);
可以选择指定是否允许
ANEURALNETWORKS_TENSOR_FLOAT32
的计算范围或精度低至 IEEE 754 16 位浮点格式,方法是调用ANeuralNetworksModel_relaxComputationFloat32toFloat16()
。调用
ANeuralNetworksModel_finish()
来完成模型定义。如果没有错误,此函数将返回结果代码ANEURALNETWORKS_NO_ERROR
。ANeuralNetworksModel_finish(model);
创建模型后,您可以对其进行任意多次编译,并执行每次编译的任意多次执行。
控制流
要在 NNAPI 模型中加入控制流,请执行以下操作
构建相应的执行子图(
then
和else
子图用于IF
语句,condition
和body
子图用于WHILE
循环)作为独立的ANeuralNetworksModel*
模型ANeuralNetworksModel* thenModel = makeThenModel(); ANeuralNetworksModel* elseModel = makeElseModel();
创建引用包含控制流的模型内的这些模型的操作数
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);
添加控制流操作
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);
编译
编译步骤确定模型将在哪些处理器上执行,并要求相应的驱动程序准备执行。这可能包括生成特定于模型运行的处理器的机器代码。
要编译模型,请按照以下步骤操作
调用
ANeuralNetworksCompilation_create()
函数创建一个新的编译实例。// Compile the model ANeuralNetworksCompilation* compilation; ANeuralNetworksCompilation_create(model, &compilation);
可以选择使用设备分配来明确选择要执行的设备。
您可以选择性地影响运行时如何在电池电量使用和执行速度之间进行权衡。您可以通过调用
ANeuralNetworksCompilation_setPreference()
来做到这一点。// Ask to optimize for low power consumption ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
您可以指定的首选项包括
ANEURALNETWORKS_PREFER_LOW_POWER
:优先以最大限度地减少电池消耗的方式执行。这对于经常执行的编译非常理想。ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER
:优先尽快返回单个答案,即使这会导致更高的功耗。这是默认设置。ANEURALNETWORKS_PREFER_SUSTAINED_SPEED
:优先最大限度地提高连续帧的吞吐量,例如处理来自摄像机的连续帧时。
您可以选择通过调用
ANeuralNetworksCompilation_setCaching
来设置编译缓存。// Set up compilation caching ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
对于
cacheDir
,请使用getCodeCacheDir()
。指定的token
必须对于应用程序中的每个模型都是唯一的。通过调用
ANeuralNetworksCompilation_finish()
来完成编译定义。如果没有错误,此函数将返回结果代码ANEURALNETWORKS_NO_ERROR
。ANeuralNetworksCompilation_finish(compilation);
设备发现和分配
在运行 Android 10(API 级别 29)及更高版本的 Android 设备上,NNAPI 提供允许机器学习框架库和应用获取可用设备信息并指定要用于执行的设备的函数。提供有关可用设备的信息允许应用获取设备上找到的驱动程序的确切版本,以避免已知的兼容性问题。通过赋予应用指定哪些设备要执行模型的不同部分的能力,可以针对部署它们的 Android 设备优化应用。
设备发现
使用ANeuralNetworks_getDeviceCount
获取可用设备的数量。对于每个设备,使用ANeuralNetworks_getDevice
将ANeuralNetworksDevice
实例设置为对该设备的引用。
获得设备引用后,您可以使用以下函数查找有关该设备的更多信息
ANeuralNetworksDevice_getFeatureLevel
ANeuralNetworksDevice_getName
ANeuralNetworksDevice_getType
ANeuralNetworksDevice_getVersion
设备分配
使用ANeuralNetworksModel_getSupportedOperationsForDevices
发现模型的哪些操作可以在特定设备上运行。
要控制要用于执行的加速器,请调用ANeuralNetworksCompilation_createForDevices
来代替ANeuralNetworksCompilation_create
。照常使用生成的ANeuralNetworksCompilation
对象。如果提供的模型包含所选设备不支持的操作,则该函数将返回错误。
如果指定多个设备,则运行时负责跨设备分配工作。
与其他设备类似,NNAPI CPU 实现由名称为nnapi-reference
且类型为ANEURALNETWORKS_DEVICE_TYPE_CPU
的ANeuralNetworksDevice
表示。调用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_create
,则是使用ANeuralNetworks_getDeviceCount
和ANeuralNetworks_getDevice
迭代所有设备时返回的设备列表。
消息(INFO 级别,标签为ExecutionPlan
)
ModelBuilder::partitionTheWork: only one best device: device-name
此消息表明整个图已在device-name
设备上加速。
执行
执行步骤将模型应用于一组输入,并将计算输出存储到应用分配的一个或多个用户缓冲区或内存空间中。
要执行已编译的模型,请按照以下步骤操作:
调用
ANeuralNetworksExecution_create()
函数创建一个新的执行实例。// Run the compiled model against a set of inputs ANeuralNetworksExecution* run1 = NULL; ANeuralNetworksExecution_create(compilation, &run1);
指定应用从何处读取计算的输入值。应用可以通过调用
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));
指定应用将输出值写入的位置。应用可以通过调用
ANeuralNetworksExecution_setOutput()
或ANeuralNetworksExecution_setOutputFromMemory()
分别将输出值写入用户缓冲区或分配的内存空间。// Set the output float32 myOutput[3][4]; ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
通过调用
ANeuralNetworksExecution_startCompute()
函数安排执行开始。如果没有任何错误,此函数将返回结果代码ANEURALNETWORKS_NO_ERROR
。// Starts the work. The work proceeds asynchronously ANeuralNetworksEvent* run1_end = NULL; ANeuralNetworksExecution_startCompute(run1, &run1_end);
调用
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);
或者,您可以使用相同的编译实例创建新的
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_getOutputOperandRank
和ANeuralNetworksExecution_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.nn.partition
属性设置为 2,在 DEBUG 版本的运行时禁用回退。
内存域
在 Android 11 及更高版本中,NNAPI 支持内存域,这些内存域为不透明内存提供分配器接口。这允许应用程序在执行之间传递设备原生内存,以便 NNAPI 在对同一驱动程序执行连续执行时不会不必要地复制或转换数据。
内存域功能适用于主要在驱动程序内部且不需要频繁访问客户端的张量。此类张量的示例包括序列模型中的状态张量。对于需要在客户端频繁访问 CPU 的张量,请改用共享内存池。
要分配不透明内存,请执行以下步骤:
调用
ANeuralNetworksMemoryDesc_create()
函数创建一个新的内存描述符。// Create a memory descriptor ANeuralNetworksMemoryDesc* desc; ANeuralNetworksMemoryDesc_create(&desc);
通过调用
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);
可选地,通过调用
ANeuralNetworksMemoryDesc_setDimensions()
指定内存维度。// Specify the memory dimensions uint32_t dims[] = {3, 4}; ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
通过调用
ANeuralNetworksMemoryDesc_finish()
完成描述符定义。ANeuralNetworksMemoryDesc_finish(desc);
通过将描述符传递给
ANeuralNetworksMemory_createFromDesc()
,根据需要分配尽可能多的内存。// Allocate two opaque memories with the descriptor ANeuralNetworksMemory* opaqueMem; ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
当不再需要内存描述符时,释放它。
ANeuralNetworksMemoryDesc_free(desc);
客户端只能根据ANeuralNetworksMemoryDesc
对象中指定的 roles 使用创建的ANeuralNetworksMemory
对象与ANeuralNetworksExecution_setInputFromMemory()
或ANeuralNetworksExecution_setOutputFromMemory()
。偏移量和长度参数必须设置为 0,表示使用整个内存。客户端还可以使用ANeuralNetworksMemory_copy()
显式设置或提取内存的内容。
您可以创建具有未指定维度或秩的角色的不透明内存。在这种情况下,如果底层驱动程序不支持,则内存创建可能会失败,并返回ANEURALNETWORKS_OP_FAILED
状态。建议客户端实现回退逻辑,分配由 Ashmem 或 BLOB 模式AHardwareBuffer
支持的足够大的缓冲区。
当 NNAPI 不再需要访问不透明内存对象时,释放相应的ANeuralNetworksMemory
实例。
ANeuralNetworksMemory_free(opaqueMem);
测量性能
您可以通过测量执行时间或分析来评估应用程序的性能。
执行时间
如果要确定运行时的总执行时间,可以使用同步执行 API 并测量调用的时间。如果要确定软件堆栈较低级别的总执行时间,可以使用ANeuralNetworksExecution_setMeasureTiming
和ANeuralNetworksExecution_getDuration
来获取:
- 加速器上的执行时间(不在驱动程序中,驱动程序在主机处理器上运行)。
- 驱动程序中的执行时间,包括加速器上的时间。
驱动程序中的执行时间不包括运行时本身的开销以及运行时与驱动程序通信所需的 IPC。
这些 API 测量提交工作和完成工作事件之间的时间,而不是驱动程序或加速器用于执行推理的时间(可能被上下文切换中断)。
例如,如果推理 1 开始,然后驱动程序停止工作以执行推理 2,然后恢复并完成推理 1,则推理 1 的执行时间将包括停止工作以执行推理 2 的时间。
此计时信息可能对应用程序的生产部署有用,以便收集用于离线使用的遥测数据。您可以使用计时数据来修改应用程序以提高性能。
使用此功能时,请记住以下几点:
- 收集计时信息可能会产生性能成本。
- 只有驱动程序才能计算自身或加速器中花费的时间,不包括在 NNAPI 运行时和 IPC 中花费的时间。
- 您只能将这些 API 与使用
ANeuralNetworksCompilation_createForDevices
且numDevices = 1
创建的ANeuralNetworksExecution
一起使用。 - 不需要驱动程序就能报告计时信息。
使用 Android Systrace 分析您的应用程序
从 Android 10 开始,NNAPI 会自动生成systrace事件,您可以使用这些事件来分析您的应用程序。
NNAPI 源代码附带一个parse_systrace
实用程序,用于处理应用程序生成的 systrace 事件并生成一个表格视图,显示在模型生命周期的不同阶段(实例化、准备、编译执行和终止)以及应用程序的不同层中花费的时间。应用程序划分的层级为:
Application
:主应用程序代码Runtime
:NNAPI 运行时IPC
:NNAPI 运行时和驱动程序代码之间的进程间通信Driver
:加速器驱动程序进程。
生成性能分析数据
假设您在 $ANDROID_BUILD_TOP 处检出了 AOSP 源代码树,并使用TFLite 图片分类示例作为目标应用程序,您可以通过以下步骤生成 NNAPI 性能分析数据:
- 使用以下命令启动 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
键以终止。
- 启动 systrace 收集器后,启动您的应用程序并运行基准测试。
在本例中,您可以从 Android Studio 启动*图像分类*应用程序,或者如果应用程序已经安装,则直接从测试手机 UI 启动。要生成一些 NNAPI 数据,您需要将应用程序配置为使用 NNAPI,方法是在应用程序配置对话框中选择 NNAPI 作为目标设备。
测试完成后,按步骤 1 中激活的控制台终端上的
enter
键终止 systrace。运行
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 ListrecognizeImage(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()
。
设置截止时间
应用程序可以为模型编译和推理设置截止时间。
- 要设置编译超时,请在调用
ANeuralNetworksCompilation_finish()
之前调用ANeuralNetworksCompilation_setTimeout()
。 - 要设置推理超时,请在开始编译之前调用
ANeuralNetworksExecution_setTimeout()
。
关于操作数的更多信息
以下部分介绍了关于使用操作数的高级主题。
量化张量
量化张量是一种表示浮点值 n 维数组的紧凑方式。
NNAPI 支持 8 位非对称量化张量。对于这些张量,每个单元的值由一个 8 位整数表示。与张量关联的是比例值和零点值。这些用于将 8 位整数转换为正在表示的浮点值。
公式为
(cellValue - zeroPoint) * scale
其中零点值是 32 位整数,比例值是 32 位浮点值。
与 32 位浮点值张量相比,8 位量化张量具有两大优势:
- 您的应用程序更小,因为训练后的权重大小只有 32 位张量的四分之一。
- 计算通常可以更快地执行。这是因为需要从内存中获取的数据量较少,以及诸如 DSP 之类的处理器在进行整数数学运算方面的效率更高。
虽然可以将浮点模型转换为量化模型,但我们的经验表明,直接训练量化模型可以获得更好的结果。实际上,神经网络学习补偿每个值的粒度增加。对于每个量化张量,比例值和零点值是在训练过程中确定的。
在 NNAPI 中,您可以通过将 ANeuralNetworksOperandType
数据结构的 type 字段设置为 ANEURALNETWORKS_TENSOR_QUANT8_ASYMM
来定义量化张量类型。您还可以在该数据结构中指定张量的比例值和零点值。
除了 8 位非对称量化张量之外,NNAPI 还支持以下内容:
ANEURALNETWORKS_TENSOR_QUANT8_SYMM_PER_CHANNEL
,您可以将其用于表示CONV/DEPTHWISE_CONV/TRANSPOSED_CONV
操作的权重。ANEURALNETWORKS_TENSOR_QUANT16_ASYMM
,您可以将其用于QUANTIZED_16BIT_LSTM
的内部状态。ANEURALNETWORKS_TENSOR_QUANT8_SYMM
,它可以作为ANEURALNETWORKS_DEQUANTIZE
的输入。
可选操作数
一些操作(例如 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 完成的相同工作进行比较(针对相同的模型和数据集)。
要使用基准测试,请执行以下操作:
将目标 Android 设备连接到您的计算机,打开终端窗口,并确保可以通过 adb 访问该设备。
如果连接了多个 Android 设备,请导出目标设备
ANDROID_SERIAL
环境变量。导航到 Android 顶级源目录。
运行以下命令:
lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available ./test/mlts/benchmark/build_and_run_benchmark.sh
基准测试运行结束时,其结果将作为传递给
xdg-open
的 HTML 页面呈现。
NNAPI 日志
NNAPI 在系统日志中生成有用的诊断信息。要分析日志,请使用 logcat 实用程序。
通过设置属性 debug.nn.vlog
(使用 adb shell
)为特定阶段或组件启用详细的 NNAPI 日志记录,该属性的值如下所示,值之间用空格、冒号或逗号分隔:
model
:模型构建compilation
:模型执行计划的生成和编译execution
:模型执行cpuexe
:使用 NNAPI CPU 实现执行操作manager
:NNAPI 扩展、可用接口和相关能力信息all
或1
:以上所有元素
例如,要启用完整的详细日志记录,请使用命令 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
才能生成详细的日志信息。