一 笔记
1.1reinterpret_cast
reinterpret_cast<类型说明符>(表达式)
将表达式强制转换成类型说明符的类型
1.2 模型
由数学运算和通过训练学习到的常量值构成的计算图。创建成功后可以在线程和编译期间复用
1.3 LOAD_FUNCTION
根据共享对象HANDLE中找到所引用的运行时地址命名为NAME的符号,也就是说根据要调用的函数名找到函数的实体。
1.4 NNAPI
NNAPI是 Android 8.1(API等级27)以后提供的专门处理机械学习的 Native 库。
1.5 模型训练
通过良好的模型将海量的资料计算出一组模型权重资料。
1.6 Android NN 的委托
将Android nn的接口进行一个重新的封装,使得相关调用的接口组合在一起,得到一个功能比较完整的接口。
1.7 android.bp
android.bp是用来配置android nn 生成动态库的一个配置文件。该语言设计简单,没有条件判断或控制流语句,采用在Go语言编写控制逻辑。
1.8 模型编译
将NNAPI模型编译成为低级代码的配置。编译成功后在线程和执行中可以复用,
1.9 内存
代表共享内存,内存映射文件与相似的内存缓存。使用内存缓存可以高效的把运行数据传输到驱动层。一个应用程序通常会创造一个共享内存缓存,包含每个tensor定义的模型。也可以用内存缓存来存储实例的输入输出。在NNAPI中,每个内存缓存代表一个ANeuralNetworksMemory实例。
1.10 模型执行
用于NNAPI输入和收集结果的接口。执行是一个异步操作,多线程可以在相同执行中等待。当执行完成后,所有线程都会被释放。在NNAPI中,每个执行代表一个ANeuralNetworksExecution的实例。
1.11 训练数据的访问
训练的权重和偏差数据可能存储在文件中。为了提供NNAPI运行时能够访问数据,通过调用ANeuralNetworksMemory_createFromFd()方法来创建一个ANeuralNetworksMemory实例,然后将打开数据文件的文件描述符作为参数传递到函数中。
1.12 NNAPI的工作原理
狭义上,NNAPI单指一组C语言API。广义上,NNAPI由NNAP、NN Runtime、NN HAL组成。
1.13 NPU硬件处理模型
如果NPU要执行模型任务,需要创建NNAPI Delegate,然后通过NNAPI Delegate将tflite模型转换为AndroidNN model。
1.14 Interpreter
解释型推理框架,在执行阶段首先需要解析不同的深度学习模型文件,但是NNAPI并没有提供相关的Parser功能,在ANDROID NN子系统中是通过TFlite深度学习库来完成模型的解析功能,并通过TFlite库中的NNAPI Delegate模块来完成TFLite模型到Android nn模型的转换。
1.15 NN Runtme中的模型分段执行功能
首先通过NN HIDL Interface查询已注册的计算设备对算子支持能力、算子计算性能和功耗能力。然后根据分段策略来选择不同设备的最优选择,在使用时需要考虑模型分段后的数据在不同device中传输的性能开销。
1.16 NPU HAL实现的功能
NPU HAL实现了设备支持算子的检验功能,检测待执行的andriodnn模型中的操作符和其对应的参数在NPU设备上是否支持。同时还提供算子执行的性能和功耗能力。
1.17 NN AIDL Interface
用于指导不同的计算设备厂商实现计算设备的HAL软件,并通过XTS测试来保证设备厂商实现符合平台兼容性的要求。
1.18 android nn模型构建
提供android nn描述符和构建功能,在加入操作符和关联的操作数之前会对相关的约束条件进行校验,并重新reorder后加入操作符队列。
1.19 android nn模型分段
根据已经创建的nn device设备对算子的支持和算子的计算性能将构建的android nn模型进行算子列表分割,从而指定不同的算子执行设备。
1.20 andriod nn模型编译
将指定到不同设备的算子列表信息,使用不同的设备中实现的编译预处理接口将分段后的子模型进行预处理,生成可在设备上执行的命令流。
1.21 android nn模型执行
分段后的子模型都会创建对应的step,android nn runtime 会根据step队列顺序来调度执行对应的子模型,最后返回执行结果。
1.22 Android NN HIDL Interface
用于指导不同的计算设备厂商实现不同的计算设备HAL软件。
1.23 Java API
提供不同AI业务场景的接口。
使用:
应用先通过ServiceManager 获取到AIEngine service 句柄,再根据业务需求获取到对应业务场景的实例,成功后传入需要执行的模型文件(可配置是否使用cache后的模型文件);模型加载完成后设置需要处理的数据。最终调用对应业务场景实例的执行接口完成推理任务获取到执行后的数据。
该功能需要AIEngine服务根据AI业务类型完成对应业务场景的开发。
1.24 AI Engine
实现不同的AI业务场景编程,包括proprocess;提供独立的AIengine服务。
1.25 Native API
TFLite的C接口,应用基于接口完成模型加载,编译,推理和输出数据的获取。
使用:
TFLite 库会加载到Native应用进程中,应用调用TFLite模型加载接口直接将模型映射到应用进程的内存空间;再创建Interpreter对内存中的flatbuffer model进行操作符和操作数参数解析并标记Tensor的生命周期和读写类型。应用将模型执行任务下发到NPU硬件处理必须先创建NNAPI Delegate将TFlite模型转换为AndroidNN model。最后应用将输入的数据进行前处理以满足模型对输入数据的要求。使用Interpreter Invoke 接口完成任务推理工作。
Native接口在Android AOSP代码中已经提供,无额外的开发工作。
1.27 Prepare
组件名称 功能描述
ExecutionPlan ExecutionPlan 控制整个AndroidNN模型执行;其由若干个 ExecutionStep 组成。ExecutionStep 的执行由ExecutionPlan 控制,在ExecutionPlan::finish
中会对将要执行的子网络进行预处理
ExecutionStep 网络拆分出的子网络。每个 ExecutionStep 都完整地在一个硬件设备上运行
CPU PrepareModel 在子网络回退到 CPU 运行时使用。
1.28 regester.cc
定义算子的支持。,可以通过自己定义的算子支持来适配自己编写的算子
1.29 TVM中模型图分割
在TVM中,模型中的图分割是在relay阶段。之后生成一个RelayIR Model的中间子图模型。
1.30 ReplaceNodeSubsetsWithDelegateKernels
TFLite中用来进行图分割的函数是ReplaceNodeSubsetsWithDelegateKernels。
1.31 PartitionGraphIntoIndependentNodeSubsets
分析图以找到所有独立的node_subsets,将图形划分为独立的节点子集。
1.32 Android Neural Networks API子图划分的依据
Android nn 的划分是根据设备是否支持算子,还根据各个设备的性能,功耗,以及用户运行时的划分偏好。具体的划分工作由ExecutionPlan 主导以及其他多个模块共同完成。
1.33 网络划分后的执行过程
ExecutionPlan和其他两个模块协助,从而控制网络划分后的执行过程
(1) Controller控制控制这网络的一次执行状态,记录了当前执行到多少步(ExecutionStep)。
(2) ExecutionStep是网络拆分出来的之网络。每个EexcutionStep都完整的运行在一个子设备上。
1.34 执行步骤在执行过程中的控制器
StepExecutor:将ExecutionStep分配到某个硬件上执行。
CPU Fallback:退回到CPU执行时使用,他包含一个小的runtime和在TensorFlow Lite实现的算子
1.34 DeviceManager
DeviceManager 负责管理并进一步抽象底层设备。抽象的工作包括抹除 Android Neural Networks API 不同版本间的差异。
二 模型创建
2.1 ANeuralNetworksMemory_createFromFd()
功能:在NNAPI运行时为一类数据提供高效的访问途径如存储训练权重和偏差数据的文件。
输入:数据文件的文件描述符
输出:无
2.2 ANeuralNetworksModel_create()
功能:构建模型
输入:模型类型的指针
输出:无
2.3 ANeuralNetworksModel_addOperation ()
功能:将运算数添加到模型中
输入:模型,运算数类型,输入数据的数目,输入操作数的索引数组,输出数组的数目,输出操作数的索引数组
输出:无
2.4 ANeuralNetworksModel_identifyInputsAndOutputs()
功能:设置运算数是作为模型的输入还是输出
输入:模型、输入数组的条数、输入的索引,输出数组的条数,输出的索引
输出:无
2.5ANeuralNetworksModel_relaxComputationFloat32toFloat16()
功能:设置计算范围和精度
输入:模型,计算范围和精度的设置
输出:无
2.6 ANeuralNetworksModel_finish()
功能:完成模型的定义
输入:模型
输出:成功返回ANEURALNETWORKS_NO_ERROR
三 模型编译
3.1 ANeuralNetworksCompilation_create()
功能:创建一个编译实例
输入:要编译的模型、编译实例的地址
输出:如果成功返回ANEURALNETWORKS_BAD_DATA
3.2 ANeuralNetworksCompilation_setPreference()
功能:设置电池电量消耗与执行速度的适配
输入:要设置的编译实例、设置的模式
输出,如果成功则返回ANEURALNETWORKS_NO_ERROR
可以设置的模式:
ANEURALNETWORKS_PREFER_LOW_POWER:倾向于以最大限度减少电池消耗的方式执行。这种设置适合经常执行的编译。
ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER:倾向于尽快返回单个答案,即使这会耗费更多电量。这是默认值。
ANEURALNETWORKS_PREFER_SUSTAINED_SPEED:倾向于最大限度地提高连续帧的吞吐量,例如,在处理来自相机的连续帧时。
3.3 ANeuralNetworksCompilation_setCaching()
功能:设置编译缓存
输入:要设置的编译实例、缓存数据的目录、设置模型的标签
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
3.4 ANeuralNetworksCompilation_finish()
功能:完成编译定义
输入:编译实例
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
四 模型执行
4.1 ANeuralNetworksExecution_create()
功能:创建一个执行实例
输入:编译后的模型,执行的实例
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.2 ANeuralNetworksExecution_setInput()
功能:从用户缓冲区读取输入
输入:要执行的实例、输入缓冲区的索引、NULL、读取的缓冲区、读取的大小
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.3 ANeuralNetworksExecution_setInputFromMemory()
功能:从内存空间读取输入
输入:要执行的实例、输入缓冲区的索引、NULL、读取的内存空间、要读取内存的偏移量、读取的大小
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.4 ANeuralNetworksExecution_setOutput()
功能:设置数据的输出缓冲区
输入:执行的实例、输出缓冲区的索引、输出的缓冲区、缓冲区的大小
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.5 ANeuralNetworksExecution_setOutputFromMemory()
功能:设置数据输出的内存空间
输入:要执行的实例、输出缓冲区的索引、NULL、输出的内存、要输出内存的偏移量、输出内存的大小
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.6 ANeuralNetworksExecution_startCompute()
功能:安排开始执行实例执行
输入:要执行的实例、实例的中止地址
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
4.7 ANeuralNetworksEvent_wait()
功能:等待执行实例执行完
输入:实例的中止地址
输出:如果成功返回ANEURALNETWORKS_NO_ERROR
五 流程图
5.1 Android中 NNAPI的整体使用流程
5.2 NNAPI的接口的简单调用流程
六 在委托中主要封装NNAPI的函数
6.1 TfLiteStatus NNAPIDelegateKernel::Init函数
主要封装了模型创建相关的函数
6.2 TfLiteStatus NNAPIDelegateKernel::Prepare函数
主要封装了模型编译相关的函数
6.3 TfLiteStatus NNAPIDelegateKernel::Invoke函数
主要封装了模型执行相关的函数
七 Android 中NNAPI接口的源码实现
7.1 模型创建
Function:创建一个模型实例并分配内存
Description: 传入一个用来保存模型创建的地址的指针,然后在函数中调用new创建一个 模型实例并返回指针,然后将地址保存到传入的指针中
Calls: int ANeuralNetworksModel_create(ANeuralNetworksModel** model)
Input1: ANeuralNetworksModel** model
Return: int
实现:
函数执行流程:模型创建函数中先调用initVLogMask()来构建日志掩码,之后通过new函数申请一块内存并在这块内存中调用模型创建的构造函数来创建一个模型对象实例,然后通过reinterpret_cast将模型的指针强制换行成为ANeuralNetworksModel类型的指针,最后将转换后的指针保存到传入函数的模型参数中。
7.2 在模型中添加运算数
Function:将运算数添加到模型中
Description:
Calls: ANeuralNetworksModel_addOperation()
Input1: ANeuralNetworksModel** model
Input2: ANeuralNetworksOperationType type
Input3: uint32_t inputCount
Input4: const uint32_t* inputs
Input5: uint32_t outputCount
Input6: const uint32_t* outputs
Return: int
实现:
函数执行流程:将模型,运算数,输入数据的数目,输入数组,输出数组的数目,输出数组等变量作为参数传如到函数中,之后通过判断传入模型,输入数组,输出数组是否正确。如过都正确,就会往下运行,通过reinterpret_cast将传入的模型强制转换成ModelBuilder类型,如果转换成功就将转换得到的指针赋值给m,之后通过m调用addOperation添加运算数。
7.3 设置运算数在模型中是输入还是输出
Function:设置模型中运算数的输入输出
Description:
Calls: ANeuralNetworksModel_identifyInputsAndOutputs ()
Input1: ANeuralNetworksModel** model
Input2: uint32_t inputCount
Input3: const uint32_t inputs
Input4: uint32_t outputCount
Input5: const uint32_t* outputs
Return: int
实现:
函数执行流程: 将模型、输入数组的条数、输入的索引,输出数组的条数,输出的索引等变量作为参数传如到函数中,之后通过判断传入模型,输入数组,输出数组是否正确。如过都正确,就会往下运行,通过reinterpret_cast将传入的模型强制转换成ModelBuilder*类型,如果转换成功就将转换得到的指针赋值给m,之后通过m调用identifyInputsAndOutput函数来设置操作数是作为模型的输入还是输出。
八 Android nn 的Hal层接口
HAL 库([email protected])包含了一组硬件设备供应商要实现的功能
8.1 getCapabilities
获取硬件的能力。
8.2 getSupportedOperations
检查硬件支持模型中的那些算子,结果以 vector 返回
8.3 prepareModel
异步地“准备”模型。将 Android Neural Networks API 定义的模型提供给底层实现,底层实现可以将其转换为自己的格式。转换结果通过回调函数通知 Android Neural Networks API 。
Android Neural Networks API 的 prepareModel 出发点是让底层驱动做包括:模型转换(Android Neural Networks API 到 驱动自身描述)、图优化、常量内联、编译等。prepareModel 结束的含义是上层可以运行这个网络了,至于驱动的优化做到什么程度,这不是 Android Neural Networks API 关系的事情。
8.4 getStatus
获取状态。
8.5 types.hal
types.hal 包含大量的神经网络相关的 operation 和 operand 描述。在此之外,还包含了对模型的描述、对数据的内存布局的描述等等。看起来似乎两个版本差异不大。在模型描述中,采用的方式和 caffe 有点类似,使用 vector 作为容器,且输入输出的描述直接使用 operand 的索引。这让底层实现可以做得比较简单
8.6 张量(operand)的类别以数据类型和生命周期区分
• 数据类型比较直观,生命周期包含几种:临时变量(网络“内部”的张量,即非输入输出,非参数的张量)、输入/输出、常量(网络的参数)。
• 张量名为 operand 。operand 包含张量和标量两种,张量带 TENSOR_ 前缀。
• 张量的数据结构描述了张量的类型、维度、后继算子数量、量化的缩放系数和偏移、生命周期、内存地址等。
o 其中,张量的维度可以是未指定的,因为这可以从输入中一步一步推算出来,但确定的维度可以帮助网络的编译优化。
o 当然,参数、输入输出还是需要明确指定维度的。
o 内存以 内存池索引 + 池内偏移 + 长度 描述。
8.7 算子(operation)
算子的数据结构比较简单,只包含类型、输入张量的索引、输出张量的索引。对于比较关键的参数,还不确定是如何描述的:直接对 operand 用 CONSTANT 描述,算子层面只描述有那几个 operand ,对这些 operand 的使用交给底层驱动。
8.8 模型(model)
包含了图、常量等信息,唯一缺乏的可能是输入张量的形状。
• 模型中以数组形式记录了张量、算子、输入输出张量的索引、张量值、内存池等。
• 对于张量的数据,张量值和内存池分别记录了生命周期为 CONSTANT_COPY 和 CONSTANT_REFERENCE 类型的张量数据。
8.9 RequestArgument
描述一个张量的“更新信息”,包含内存地址和形状变化信息(维度不可变)。似乎只用于网络的输入输出。 Request 是对 RequestArgument 的包装,包含若干个输入、输出和对应的内存池。Request 有两个主要任务:
• 在运行网络时提供输入输出数据
• 给出“模型准备时期”没有指定的输入张量的元信息
8.10 Capability
包含设备的性能数据。性能包含执行时间和功耗,以设备相对于 cpu 的比率记录。这是一种粗粒度的记录,只包含 float32 性能和 8bit 量化性能。
版本 1.1 的几个改的包括:
• getCapabilities 和 Capabilities 包含了“relaxed calculation”(使用 float32 的数据,但计算时使用 float16 的精度)性能。
• 增加了一些算子的支持。
• 执行的偏好——低功耗、快速等。
8.11 IPreparedModelCallback
当 prepareModel(Model, IPreparedModelCallback) 执行完毕后,驱动应当调用 IPreparedModelCallback.notify(IPreparedModel) 来通知 Android Neural Networks API 模型已经准备完毕,可以运行了。由 Android Neural Networks API 提供给 HAL 驱动。
8.12 IPreparedModel
一个可执行的模型,由 HAL 驱动提供给 Android Neural Networks API 。和 prepareModel 类似,IPreparedModel 中的 execute 方法也是异步的。execute(Request, IExecutionCallback) 传递了网络的输入输出 Request 和一个通知 Android Neural Networks API 网络执行完毕的回调函数。IPreparedModel 的内部实现对 Android Neural Networks API 无所谓,而其中的 execute 方法由 Android Neural Networks API 调用。
8.13 IExecutionCallback
只包含一个状态返回。网络的输出结果内存已经在 IPreparedModel.execute() 中给定。当 HAL 驱动执行完网络,调用IExecutionCallback.notify() 方法是获取执行状态,Android Neural Networks API 只需要回去检查 execute() 方法当时给定的 Request 即可得到输出。
8.14 VTS(Vendor Test Suite)
vts 背景可以参考 https://source.android.com/compatibility/vts/
Android Neural Networks API 的 VTS 包含了上面所有接口的测试,即算子、张量、模型和 runtime 等。在大多数时候,抛开测试的大背景,这里的 VTS 主要在模拟 libneutalnetworks.so 的行为去使用 android.hardware.neuralnetworks@1.[0|1].so 。下面将重点介绍 libneutalnetworks.so 。
九 核心系统
位置:aosp/frameworks/ml/nn
这部分包含了 Android Neural Networks API 的 runtime、暴露给开发者的 NDK 接口等。
9.1 driver sample
驱动主要的工作是继承 IDevice 之类的类,并实现其中的方法。驱动将以服务的形式注册到系统中,SampleDriver::run() 。Android Neural Networks API 的 runtime 则通过 HAL (via android.hardware.neuralnetworks.so) 的方式使用底层驱动。
9.2 common文件夹
一些基础性的实现,被多个模块共享。
9.3 Operations文件夹
common/operations 存放的是 cpu 实现的算子。这些算子的最终计算使用的是 TensorFlow Lite 中的实现(tensorflow/contrib/lite/kernels/internal/optimized/optimized_ops.h)。这里的代码是对 TensorFlow Lite 算子实现的一种封装。(注意,在 Android Neural Networks API 中,cpu 实现只是一种备选方案,并不意味着 Android Neural Networks API 和 TensorFlow Lite 的结构是混杂的。)算子的声明在 common/include/Operations.h 中。CpuExecutor.cpp 会将计算任务分配到各个算子。
9.4 CpuExecutor
CpuExecutor 是 cpu 上的执行引擎。
• RunTimeOperandInfo:包含张量的类型、维度等——和 HAL 层中的信息量差不多,不过号称这里是可更改的。
• RunTimePoolInfo:封装了 IMemory 作为内存池。内存有 ashmem 和 mmap_fd 两种 HIDL 方式,也有直接给定缓冲区地址。(即共三种)
• CpuExecutor:run 方法接收模型、模型内存池、输入输出、输入输出内存池等,按照 Model 中的顺序依次执行算子。由 StepExecutor::startComputeOnCpu() 调用。执行步骤包括:
o initializeRunTimeInfo 输入参数配置这里的“小 runtime”的张量、内存信息。
o 遍历 Model.operations[] 中的算子,依次用 executeOperation 执行。根据算子的类型校验算子的输入输出张量数目,然后调用算子的对应实现(common/operations)
o CpuExecutor 还会追踪张量被使用的情况,主动地回收不再使用的张量内存。
其他,common 中还包含一些“基础设施”代码:
• GraphDump 可以将网络定义转成 Graphviz 格式呈现。测试使用的小工具,没有加入到执行流中。
• Traceing 主要为了抓取 Android Neural Networks API 的执行时间,也可以监视驱动、cpu各自的执行时间和调用序列。Tracing 包含插入代码中的宏定义、android systrace、systrace parser 等。有尚未公开的文档 NNAPI Systrace design and HOWTO 。
• ValidateHal 验证 HAL 的各种数据结构类型及其约束。
9.5 runtime
这部分包含 NeuralNetworks、(Device)Manager、VersionedIDevice 等。
NeuralNetworks 包含接口相关的实现。这一层似乎不维护任何数据或结构,C 风格的接口导致资源都是由上层(框架)维护。Android Neural Networks API 上层接口的数据结构和 HAL 层定义的接口的数据结构基本上类似——去掉 ANeuralNetworks 前缀。
DeviceManager 是单例模式,因此只有一个集中化的 DeviceManager。Android Neural Networks API 使用多个后端设备,它们通过 DeviceManager 管理。
• 构造时从 ServiceManager 中找到所有的 Android Neural Networks API 设备,将其记录下来。上层通过 getDrivers() 获取所有的可用设备。
• 所有的设备以
• 记录了划分策略。一共三种策略:
o 不划分
o 划分,如果划分失败就回退到不划分的模式
o 划分,但禁止回退
Device 封装了 VersionedIDevice,记录
• Device 记录 VersionedIDevice 对象和设备的性能参数。
• 上层逻辑在使用设备时一般是 DeviceManager::getDrivers()[i]::getInterface(): 而不是直接使用 Device,比较奇怪。但 getSupportedOperations 使用的是 Device ,看起来是在做容错—— 当和设备的通信失败时,将该设备标记成不支持任何算子。
VersionedIDevice 负责管理版本,将不同版本的差异对 runtime 的其他部分屏蔽。方法也比较简单:一个设备必定是 1.1 或 1.0 版本,如果是 1.1 版本,那么直接使用;如果是 1.0 ,对不支持的接口按语义给出兼容结果
9.6 Memory 是内存管理组件
)
• Memory 作为基类,使用的内存是通过 HIDL 机制分配的,这种内存可以在驱动和 HAL 层使用。
• MemoryFd 继承了 Memory
o MemoryFd 使用的内存是通过 mmap(fd) 分配的,内存会转换为 HIDL 描述后记录。即, Memory 和 MemoryFd 虽然内存的创建方式不一样,但后续的使用方法一致的。
o 外部接口总是从文件创建内存对象 MemoryFd—— ANeuralNetworksMemory_createMemoryFd()
• MemoryTracker 是用来管理 Memory 的工具,方便上层同时管理多个 Memory 对象。
o 使用者包括 ExecutionBuilder, StepExecutor, ModelBuilder。
o 目前不清楚 Memory 和 DataLocation 之间的关联——在 HAL 层中,模型的内存是在 Model 中管理的。
9.7 Callbacks
Callbacks 是 Android Neural Networks API 和 HIDL 驱动线程之间同步的主要方法。
• 异步任务总是会绑定一个 Callback 对象。所有的回调都要继承 CallbackBase 。
• CallbackBase 的接口包括:
o wait 将阻塞直到 notify;wait_for也会阻塞,不过带超时功能
o on_finish(func)设置一个函数,该函数在 notify 时执行(在阻塞线程 wakeup 之前)
o bind_thread 和 join_thread 提供了资源回收的方法。具体目的还不清楚
o notify 唤醒阻塞线程。这里会先执行 on_finish 设置的 post_work,然后唤醒所有线程。
• PreparedModelCallback 继承了 CallbackBase 和 IPreparedModelCallback
o 这是 Android Neural Networks API 通过 HIDL 传递给 HAL 驱动的回调,HAL 驱动会给 Android Neural Networks API 一个 PreparedModel
• ExecutionCallback 继承了 CallbackBase 和 IExecutionCallback
o 这里不需要传递数据,只需要传递执行完毕的状态
9.8 ModelBuilder
ModelBuilder 内部存储了用户通过 ANeuralNetworksModel_* 系列接口创建的算子、张量等。
9.9 张量与内存管理
• setOperandValue 可以指定张量的内存。
o 根据内存大小(128字节),采用拷贝或引用的方式存储数据。
o 拷贝的数据都存在 mSmallOperandValues 数组中;引用的数据存在 mLargeOperandValues 中(直接拿缓冲区指针)。
o 看起来这个接口专门用于指定常量–网络参数。
• copyLargeValuesToSharedMemory
o 通过 HIDL 创建一块内存,将所有的引用型常量拷贝到这块内存中
o 在 finish 时被调用,能有效地减少内存碎片。
• setOperandValueFromMemory
o 可以指定在特定 Memory 中的参数
o 以引用形式记录该这块参数,在 copyLargeValuesToSharedMemory 时被合并
• identifyInputsAndOutputs 将修改输入输出张量的生命周期为输入输出,也会更新输入输出数组。
9.10 运行过程、流程
• sortIntoRunOrder 对算子排序,确保算子的运行顺序是正确的–满足拓扑排序
o 首先建立”张量和使用某个张量的算子”映射、某算子还有多少个输入没区别出类型,并将输入算子标记为”可计算”
o 执行循环:从”可计算”的集合中取出一个算子,将其加入到执行顺序中。那么,该算子的输出是可得,所有”使用该算子输出作为输入”的算子的”尚为计算出的输入”数量减一。扫描所有算子,将所有输入都可知的算子加入到计算任务中
o 在循环结束后,所有的算子都拥有了正确的执行顺序
• finish 表示完成了模型的配置,步骤包括
o identifyInputsAndOutputs (应该在 finish 之前调用)
o copyLargeValuesToSharedMemory 会固化参数
o 对模型进行验证工作,确保符合 HAL 描述
o sortIntoRunOrder
9.11 和其他模块对接
• createCompilation 创建一个编译对象,因为编译要和模型对应起来
• findBestDeviceForEachOperation 和制定执行计划的 ExecutionPlan 息息相关。
o 针对每个算子,找到执行该算子最优的设备
o 最优设备,性能优先时看执行速度,功耗优先时看功耗分数
• partitionTheWork 划分计算任务,在 ExecutionPlan 中实现
• setHidlModel 修改 Model 中的算子、张量、输入输出、内存池等。用在任务划分建立的”子模型”和验证模型中
partitionTheWork 是划分任务。在 ModelBuilder 中声明,在 ExecutionBuilder 中定义,在 CompilationBuilder::finish() 中使用。
• 如果只有 cpu ,那么不划分任务
• 使用 findBestDeviceForEachOperation 找到算子的最优设备。如果所有算子的最有设备都是同样的设备,那么不划分任务
• 任务划分的两个核心步骤如下
o 为每个设备创建一个算子队列,根据最优设备将算子加入到各个队列中
o 为每个设备创建执行步骤 ExecutionStep ,将设备的算子队列加入到任务中
o 这个过程中包含了和 sortIntoRunOrder 非常类似的排序工作,进入队列的算子是那些已经完全清楚了计算顺序的算子
• 结束划分 ExecutionPlan::finish
9.12 CompilationBuilder
CompilationBuilder 是从 Model 创建的对象。
• 虽然名为编译,但在接口上并没有体现编译,更多的是记录设备和用户的编译相关的偏好信息——性能和划分等。
• createExecution 是在“编译”完成后创建执行对象
• finish 调用 partitionTheWork 划分执行任务。当划分失败时,如果可以回退,那么会忽略划分错误。
9.13 ExecutionBuilder
ExecutionBuilder 是另一个比较重要的模块,它从 CompilationBuilder 创建。
ModelArgumentInfo 是模型输入输出的描述,信息主要包括维度形状和内存。其中内存可以以指针、Memory 等多种形式指定。每个 ModelArgumentInfo 描述一个输入/输出,多个以数组形式组织。
ExecutionBuilder 是高层接口的运行逻辑。
• 在高层接口中以 ANeuralNetworksExecution_* 接口大多是在配置输入输出。startCompute 是核心逻辑接口。
• startCompute(ExecutionCallback) 中,上层应用和下层 HAL 驱动是异步的(在其他的 finish 接口中,应用和 Android Neural Networks API 是同步的,Android Neural Networks API 和 HAL 驱动是异步的)。
ExecutionBuilder::startCompute 的核心逻辑根据(编译选项)是否需要划分任务又分两种情况。
• (编译选项)划分任务的情况
o 使用 ExecutionPlan::makeContrioller 构造运行控制器
如果控制器构造失败,且允许回退,那么会退到 cpu 模式执行(使用 StepExecutor::startCompute)
o 启动线程异步地执行 asyncStartComputePartitioned ,立即将 ExecutionCallback 对象当做 Event 返回给应用。
o asyncStartComputePartitioned 是一个包含下面循环的函数
从 ExecutionPlan 中取出下一个 StepExecutor 。(成功则继续,失败则全部回退到 cpu 或报错)
创建 ExecutionCallback 并执行刚取出的 StepExecutor 子任务。(成功则继续,失败则这部分回退到 cpu 或报错)
等待任务执行完毕(成功则继续,失败则这部分回退到 cpu 或报错)
o 当执行整个网络时,使用 ExecutionBuilder 构造 StepExecutor;当执行部分网络时,一般使用 ExecutionPlan 构造 StepExecutor 。
• (编译选项)禁止划分任务的情况
o 找到一个能支持所有算子的设备
o 映射输入输出,StepExecutor::startCompute
StepExecutor 是执行“一步”计算的对象,要求在单一设备上运行。
• StepExecutor 也有不少内容在处理各种形式的输入输出的内存。
• allocatePointerArgumentsToPool 将外部提供的输入输出内存(Memory)记录到 StepExecutor 中。缓冲区、参数的起始地址等均依赖于之前提供的操作数信息。
StepExecutor::startCompute 执行最终的计算任务,分为 cpu 和设备上两种情况。
• startComputeOnCpu 在 cpu 上运行
o 创建用于 HIDL 的模型并拷贝模型信息、设置输入输出、内存池等
o 创建 ExecutionCallback
o 启动线程 asyncStartComputeOnCpu 异步地计算,并立即将 Callback 对象传递给上层(最终作为 event 传递给用户)
计算线程创建 CpuExecutor 来运行网络,并在结束后用 Callback 通知上层执行结束的状态
• startComputeOnDevice 在设备上执行
o 如果设备上的模型(IPreparedMOdel)尚未生成,则要先生成。生成完毕后继续
创建 IPreparedModelCallback 后调用 IDevice::perpareModel() 生成模型
在生成完毕后,驱动将 IPreparedModel 传递到这里,Android Neural Networks API 记录下
o 类似地,针对各种情况配置好输入输出;创建 ExecutionCallback
o 调用 IPreparedModel::execute() 执行计算。计算将异步地执行,但 Android Neural Networks API (暂时)会在这里同步等待。(以后应该会删掉这个同步操作)
o 检查状态、根据情况同步输出数据
o 将 ExecutionCallback 返回给上层。(目前这里的计算是同步的,以后应该会改成异步的)
9.14 ExecutionPlan
一个 ExecutionPlan 由若干个 ExecutionStep 组成。ExecutionStep 的执行由 ExecutionPlan::Controller 控制,它和每个 ExecutionStep 一一对应。每个 ExecutionStep 都拥有自己的 ModelBuilder ,当然这个 ModelBuilder 的内部不再是可划分的。Controller 之间用 ExecutionStep 在 ExecutionPlan 中的索引串联起来,这些串联关系构成了 ExecutionStep 之间的拓扑关系。
9.15 ExecutionStep
ExecutionStep 是“一次运行”的粒度,拥有自身的模型、设备、输入输出等。
• 其中模型的算子、张量均来自于“母模型”,因此维护了一系列映射。
o 这些映射有
o 主要用于描述子模型的输入输出——这些张量在母模型中可能是输入、输出或临时张量,甚至输出作为输入之类
o 也有“集中式”描述子模型输入输出到母模型的索引,和描述所有张量的映射 mOperandMap
• addOperand 增加新的被分配到“本执行步骤”的张量
o 这些张量的信息(类型、维度、内存等)会通过 ModelBuilder 接口添加到 mSubModel
o 这里更新 mOperandMap,还根据情况张量在母模型和子模型中的属性更新相应的映射
o 有一点值得注意,更新映射时使用了 OperandKind::[INPUT|OUTPUT] ,这本来是应该根据“子模型”输入输出来设置映射,但 addOperation 调用 addOperand 时给出的却是“算子的输入输出”。不确定这里是不是有问题。
• addOperation 增加新的被分配到“本执行步骤”的算子
o 将算子的输入输出加入到 ExecutionStep 中。这里将算子的输入输出作为了子模型的输入输出,不确定是否合理
o 将算子信息添加到 mSubModel 中,添加时使用的输入输出张量索引是以子模型为基准的
• finishSubModel 在 ExecutionPlan::CompoundBody::finish() 中被调用,完成某个子模型运行前的“准备工作”
o 首先进一步更新子模型的输入输出,并设置给 mSubModel
o 调用 compile 完成从 Android Neural Networks API 模型到 HIDL 驱动 IPreparedModel 的转换。compile 会阻塞直到转换完毕
9.16 ExecutionPlan
ExecutionPlan 是模型的整个执行计划。
• ExecutionPlan 内部定义了多个辅助对象
o Controller
记录了 ExecutionPlan 和 ExecutionBuilder,通过 ExecutionPlan::makeController 构造
mNextStepIndex 记录了,在所有的执行步骤中,下一次要执行的步骤的索引——标记了执行状态
没看出来有什么特别的作用
o Body 是一个基类。派生类有 SimpleBody 和 CompoundBody 两种
SimpleBody 负责执行“整个”模型,拥有设备、模型等。SimpleBody::finish() 将用 compile 编译模型
CompoundBody 在网络分步执行时使用,包含若干个 ExecutionStep 。CompoundBody::finish() 依次调用各个 ExecutionStep::finishSubModel()
• 网络分段与否的逻辑与信息
o ExecutionPlan::mState 记录了使用哪种模式,一般为分步网络
o 在 ModelBuilder::partitionTheWork 划分网络时
如果网络没有分段则用 ExecutionPlan::becomeSingleStep 设置 SimpleBody
划分网络时,用 ExecutionPlan::createNewStep 创建新的 ExecutionStep 来放置新的子网络。
o ExecutionPlan::createNewStep 简单地用设备、步骤索引创建一个新的步骤。该步骤将被记录到 ExecutionPlan::compound()::mSteps 中
• 网络的分段运行中,ExecutionPlan 的主要任务是 next 查找下一个执行步骤
o 对于 SimpleBody 整个网络,直接从模型、控制器、设备、准备好的模型中创建 StepExecutor,并准备好模型的输入输出数据(整个网络的准备比较简单)
o 对于 CompoundBory 分步网络,选择“下一个”网络,用子模型、设备等创建 StepExecutor 。然后要设置好子模型的输入输出内存(这部分还是比较绕的)
o next 在 asyncStartComputePartitioned 中被调用
• 似乎,一个既定的网络模型可以有多个 Controller 控制某次计算
十 Android NNAPI 的系统性
10.1具体运行过程
Android Neural Networks API 的设计引入了跨系统多层的回调,执行逻辑比较复杂,本小节将完整地描述一次推理中软件各部分发生的交互。
上图是整个执行流程的总览。图中以数字为索引标注了模块之间主要的任务执行顺序。如图例,黑色文字和箭头表示常规的流程,蓝色表示模型划分阶段,红色表示模型执行阶段。模型的执行是异步的,各个模块之间有自身的线程,不同的线程用不用的背景颜色区别开。
首先是模型准备阶段(步骤 0 - 1)。这部分比较简单,不再多做介绍。
其次是模型编译阶段(步骤 2 - 4)。这部分已经获得了整个网络的描述,可以根据可选的设备划分执行计划。
再次是模型执行阶段(步骤 5 - 14)。这部分可分为几个子步骤:
十一 算子与张量之间的关联
可以看到,在各个接口或框架层面,都没有对算子和张量的关联有特别明确的处理——在描述网络时,算子使用的张量用张量在张量表中的索引给出,算子的张量之间也没什么明确的指代。
当我们在描述一个算子的时候,一般是有算子的输入输出张量、参数的张量。而在 Android Neural Networks API 的体系中,描述算子的时候纯粹只是给了个张量的数组,并没有指明各个张量都是什么。这看起来像是,从高层框架(例如 TensorFlow Lite)到底层驱动实现,对于某个特定的算子,大家都遵循特定的约束。只有这样才能合理地解释为什么 Android Neural Networks API 中从未提及参数的各种类型等等。
在 HIDL 描述中,算子的输入排列以注释文档的形式给出,全连接算子的输入输出文档摘录如下。高层的 NDK 头文件接口也有几乎一致的文档。文档详细描述了算子中『第 N 个』张量的属性。
/**
* Denotes a fully (densely) connected layer, which connects all elements
* in the input tensor with each element in the output tensor.
*
* Supported tensor rank: up to 4.
*
* Inputs:
* * 0: A tensor of at least rank 2, specifying the input. If rank is
* greater than 2, then it gets flattened to a 2-D Tensor. The
* (flattened) 2-D Tensor is reshaped (if necessary) to
* [batch_size, input_size], where “input_size” corresponds to the
* number of inputs to the layer, matching the second dimension of
* weights, and “batch_size” is calculated by dividing the number of
* elements by “input_size”.
* * 1: A 2-D tensor, specifying the weights, of shape
* [num_units, input_size], where “num_units” corresponds to the number
* of output nodes.
* * 2: A 1-D tensor, of shape [num_units], specifying the bias. For input
* tensor of {@link OperandType::TENSOR_FLOAT32}, the bias should
* also be of {@link OperandType::TENSOR_FLOAT32}. For input tensor
* of {@link OperandType::TENSOR_QUANT8_ASYMM}, the bias should be
* of {@link OperandType::TENSOR_INT32}, with zeroPoint of 0 and
* bias_scale == input_scale * filter_scale.
* * 3: An {@link OperandType::INT32} scalar, and has to be one of the
* {@link FusedActivationFunc} values. Specifies the activation to
* invoke on the result.
*
* Outputs:
* * 0: The output tensor, of shape [batch_size, num_units]. For output
* tensor of {@link OperandType::TENSOR_QUANT8_ASYMM}, the following
* condition must be satisfied:
* output_scale > input_scale * filter_scale.
*/
FULLY_CONNECTED = 9,
在 TensorFlow Lite 中,TensorFlow 的参数序列以一种相对比较复杂的方法转换成顺序排列的。针对每个算子:
首先将输入张量加入到 Android Neural Networks API 中「广义」的输入中
// Add the parameters.
std::vector
node.inputs->data, node.inputs->data + node.inputs->size);
std::vector
node.outputs->data, node.outputs->data + node.outputs->size);
然后使用 add_scalar_int32 等方法将参数加入到 Android Neural Networks API 的模型中
auto add_scalar_int32 = [&nn_model, &augmented_inputs,
&next_id](int value) {
ANeuralNetworksOperandType operand_type{.type = ANEURALNETWORKS_INT32};
CHECK_NN(ANeuralNetworksModel_addOperand(nn_model, &operand_type))
CHECK_NN(ANeuralNetworksModel_setOperandValue(nn_model, next_id, &value,
sizeof(int32_t)))
augmented_inputs.push_back(next_id++);
};
auto add_scalar_float32 = [&nn_model, &augmented_inputs,
&next_id](float value) {
ANeuralNetworksOperandType operand_type{.type = ANEURALNETWORKS_FLOAT32};
CHECK_NN(ANeuralNetworksModel_addOperand(nn_model, &operand_type))
CHECK_NN(ANeuralNetworksModel_setOperandValue(nn_model, next_id, &value,
sizeof(float)))
augmented_inputs.push_back(next_id++);
};
// ...
auto add_fully_connected_params = [&add_scalar_int32](void* data) {
auto builtin = reinterpret_cast(data);
add_scalar_int32(builtin->activation);
};
在高通的 DSP 底层驱动 Hexagon NNlib 中,全连接算子的代码片段如下。可以看到,驱动以既定的顺序从算子的若干个输出中取出。
static int fullyconnected_execute(struct nn_node *self, struct nn_graph *nn)
{
const struct tensor *in_tensor = self->inputs[0];
const struct tensor *weight_tensor = self->inputs[1];
const struct tensor *suma_tensor = self->inputs[2];
const struct tensor *bias_tensor = self->inputs[3];
const struct tensor *precip_tensor = self->inputs[4];
struct tensor *out_tensor = self->outputs[0];
struct tensor *out_min_tensor = self->outputs[1];
struct tensor *out_max_tensor = self->outputs[2];
// …
}
这样一来,通过 Android Neural Networks API 贯穿 NDK 和 HAL 层的统一接口描述,高层深度学习框架和底层硬件驱动所使用的算子和张量的语义描述,是遵循同一套约束的,不需要额外的转换或配置。