FP16是半精度浮点格式,相比常用的FP32单精度浮点,数据宽度降低了一半。2016年Arm更新了Armv8.2-A Extension扩展指令集,其中包含FP16半精度浮点运算。Arm NEON向量指令长度为128位,一条FP32向量可完成4个单精度浮点数运算,一条FP16向量可完成8个半精度浮点数运算,使理论峰值性能翻倍。如果该指令用于加速网络推理,相比于FP32预期能达到2倍加速。

1.2 为什么要支持Arm32位FP16指令加速?

智能手机分为Arm32和Arm64位两种架构,其中Arm64占绝大比例,苹果从2013年9月发布iPhone5s后,所有机型全都是Arm64架构。在Arm64架构手机上,App编译为64位可以获得最大的性能,但Arm64架构也支持按照32位编译的APP运行,而Arm32架构无法支持按照64位编译的APP运行。因此出于机型覆盖率以及软件包大小的考虑,当前众多App只会发布32位编译的版本。

经调研,行业开源推理框架如ncnn、MNN等仅支持Arm64位FP16指令加速,这样32位App无法享受FP16指令加速效果。针对这个行业缺失,TNN在架构兼容、模型兼容、代码结构设计等方面率先进行探索,对Arm64位和Arm32架构均实现了FP16指令优化,让64位和32位App都能发挥硬件FP16向量加速的能力。

 

2.1 架构兼容性设计

由于深度学习网络的算子种类繁多,并且随着新模型不断被开发,算子类型也会随之增加,因此,难以一次性为所有层提供FP16加速。我们采用TNN中广泛使用的注册机制,以实现FP16加速的增量开发。实现如下:

①在ArmDevice下维护一个全局的layer_precision_map,将算子类型映射到其支持的数据类型;

②每实现一个FP16加速算子,使用REGISTER_ARM_PRECISION_FP16+LayerType,更新layer_precision_map;

③在运行模型的初始化过程,TNN根据layer_precision_map的信息,为模型中的每一层分发计算精度。仅当算子已支持FP16加速,并且运行平台具备FP16加速硬件时,该层才会使用FP16精度计算。当用户设置的网络精度为PRECISION_HIGH时,可以强制禁用FP16加速。

2.2 模型兼容性设计

部署时模型的算子各种各种,为了获得最大的性能,TNN支持模型按照FP32和FP16混合运行加速。对模型中已实现FP16加速的算子,TNN默认自动按照FP16加速,而对模型中未实现FP16加速的算子,TNN在静态图中自动插入Reformat层转为FP32加速。如下图所示:

 

在TNN的图优化过程中,当发现Pad层不支持FP16加速时(如图a所示),会在其输入和输出分别插入Reformat层。Reformat层负责将FP16和FP32数据格式以及数据排布做相互转换,以支持Pad层单独采用FP32计算,其余层仍采用FP16计算。

如果模型中存在多个相连的层不支持FP16(如图b所示),TNN的图优化机制会避免在这些层之间插入成对的Reformat层,以提高运行效率。

2.3 代码结构设计

为了在32位和64位库中都支持FP16,整体代码结构如下图所示。其主要分为五个部分,通过CMake中不同的配置项,可编译出不同的target。各个部分的主要区别体现在针对不同Arm指令集实现了特定优化。

 

由上图可知,aarch32和aarch64 FP16指令代码独立于其他部分。因为编译FP16指令需要添加特定的编译选项,如果对TNN代码全局添加该选项,会导致编译器将选项应用到所有代码中,然后基于Armv8.2-A架构生成目标文件。

例如在Arm64 Target中,在编译Armv8指令代码时添加该选项,会生成一些Armv8.1或Armv8.2指令集中独有的指令。这些指令若在不支持v8.1和v8.2的Armv8 CPU上运行,会直接导致程序崩溃。因此,为了最大限度地提高兼容性,Armv8.2-A FP16指令代码被单独剥离,单独使用编译选项,避免影响其他部分。

综上所述,TNN库会同时包含两种架构的指令:

  • 64位库:包含Armv8指令和aarch64 Armv8.2-A FP16指令。
  • 32位库:包含Armv7指令和aarch32 Armv8.2-A FP16指令。

2.4 运行时兼容性设计

由上可知,TNN库中包含两种架构的指令。当运行64位或32位库时,若在不支持Armv8.2-A的CPU上执行Armv8.2-A指令,会直接导致程序崩溃,在运行时造成兼容性问题。

针对上述问题,在执行推理之前,TNN会判断当前运行的CPU是否在白名单中、是否支持Armv8.2-A。如果支持,则会运行FP16算子,否则仍然运行FP32算子,避免执行Armv8.2-A FP16指令。具体判断方式如下:

①在IOS和OSX下,通过系统调用sysctlbyname("hw.cpufamily"),获取CPU型号,然后与维护的白名单比较,判断CPU是否支持FP16加速。目前的白名单包括IOS的A11-A14,和OSX的M1。

②在Android和Linux下,通过系统调用getauxval(AT_HWCAP),获取hwcap flag,然后与HWCAP_FPHP和HWCAP_ASIMDHP掩码比较,当FPHP位和ASIMDHP位都为1时,CPU可支持FP16加速。

由于Android的C库在API级别18及更高版本中才支持 getauxval,在低版本的32位Android系统中,该系统调用可能会失败。为了支持aarch32 FP16加速,采用直接解析/proc/cpuinfo的方式,判断CPU是否支持FP16加速。通过/proc/cpuinfo获取处理器的MIDR(Main ID Register),根据该标志可判断处理器厂商和型号,例如Arm的Cortex-A55,海思的Cortex-A76 (HiSilicon)等。然后与维护的白名单比较,最终判断硬件是否支持FP16加速。

 

TNN已对部分算子实现了FP16优化,在64位和32位库中均取得了不错的加速效果,相比于一些开源框架也具备一定的性能优势,如下图中的对比数据所示。

 

 

在A76大核上,TNN的FP32和FP16性能均能保持前列。在A55小核上,Bolt框架针对A55单独做了特殊优化,在性能测试时效果会更好。但实际应用中,由于进程可能会在小核和大核上动态调度,A55特殊优化版本在A76大核上运行的性能较差,所以实际应用的表现不一定会好。TNN采取的是相对折中的实现,在大核和小核上都能取得不错的性能表现。

 

TNN已经对大量的实现细节做了封装,只需要对少量参数进行配置,就可以轻松获得FP16的加速。

1)编译

TNN工程的根目录下提供了各个平台的编译脚本,只需要在这些脚本的基础上打开TNN_ARM82_ENABLE,就可以将Armv8.2的优化代码编译到TNN的lib中,当前默认是OFF。

 

2)运行

在初始化时,将precision参数设置成PRECISON_AUTO,TNN内部就会根据CPU的型号以及layer的实现情况自动去调用FP16的代码,当前默认是PRECISION_AUTO,所以不用做任何修改。

 

 

TNN当前正在与PCG光影团队合作,后续还会支持更多算子的Armv8.2-A FP16优化,同时也会尝试去实现Armv8.2-A的Dot扩展指令,优化在最新机型上的int8模型性能。