作者:一皓,蒙武,君合,心彦,飞木,羽砺等
前言
本章节主要是基于框架的量化,编译,推理的相关的开发模式。把它作为“含光十八式”的第一式,是因为这是比较基础同时也是高层的使用方法。借助于框架的功能,用户即使不了解含光NPU和底层编程接口的一些细节,也能比较方便地使用NPU,做基于NPU的开发。通过NPU软件栈配合框架,去充分发挥NPU硬件的性能。
当你准备阅读此文时,如果你没有阅读过《含光800NPU编程模型》,请你一定要先仔细地读一读编程模型。它能帮助你更好地理解下面的开发指南和例程。同时,本文将是一个指引,更具体的文档在《HanGuangAI SDK》。如想了解更细节的信息,请阅读开发文档。
框架和模型
HanGuangAI软件栈对框架和模型的支持选择,都是根据框架模型在集团的业务使用情况为优先级。根据我们当前的业务统计情况,我们的安排的支持优先级大概是:
- Tensorflow,MxNet,Caffe框架,当前已经支持Tensorflow,MxNet框架,Caffe正在开发当中,马上就可以发布了。
- 内部在线推理引擎Blaze,目前也正在开发当中。
- ONNX的支持还在增加中,比较好的一点是,ONNX模型可以通过外部转换成其他模型,然后利用比如MxNet框架来支持,所以优先级低一些。
- PyTorch主要还是在研究领域,工业界暂时还没起来,其动态模型的方式不是特别适合含光NPU,所以优先级会排在比较后面。不过新的TorchScript API支持更友好了。
如果是新的NPU应用开发,我们推荐基于TensorFlow和MxNet框架,支持比较完善一点。
编译中间件
另外还有一个思考,是否需要对接开源编译器和推理引擎?使用他们之后减少一些上层对接工作,这是我们后期可以做进一步研究的地方。比如,当前几个主流的编译器框架:
- TVM,TVM团队已经成立创业公司OctoML来打造一个可扩展,开放,中立的端到端的栈,可减少AI应用公司为特定硬件开发和深度学习软件部署所画的成本和时间。
- GLOW,比如Kabana对GLOW的后端支持已开源。现在还是一种开源生态,加入的公司不多。
- MLIR,基于MLIR的开发,可以或许加速异构的模型(NPU和其他设备)在TensorFlow上的执行。
比较麻烦的是这些还不成熟,而且各自为政,没办法应对集团内的各式各样的需求。所以我们更多的是借鉴他们的一些优点,加在我们自己的软件栈里。以后会根据业务的情况来看是否有必要对接某个编译器IR和后端。
基于框架的开发模式
框架模型
在上一篇《含光800NPU编程模型》中的“深度网络模型”章节中,提到了从开发者和用户的角度,深度网络模型在HanGuangAI上有4个模式:
- 原始模型(Original Float Model)
- 校准模型(Calibration Model)
- 量化模型(Quantized Model)
- 编译模型(Compiled Model)
并且这四种模式都是以原始的网络模型的格式存在的,比如原来是Tenorflow的pb格式,量化,编译后后输出还是pb文件格式。原来是json+params的MxNet网络模型,每一步处理后输出的还是json+params的格式。这种处理方式和其他框架,或者推理引擎的处理方式都不一样。我们这么选择有一些理由:
- 首先,利用原有的框架的支持,我们不用急着做一个自己的独立推理引擎,或者深度学习框架。我们可以更专注于自己的核心优化编译方面。对于我们不支持的算子,可以充分地利用原有框架的优化做异构支持。
- 其次,校准模型的校准运行,量化模型的精度验证运行,只要添加一下只定义的校准和量化后的算子,就可以在原有框架上执行,而且可以不用依赖NPU硬件,也就是说可以在CPU,GPU运行。模型的编译也可以脱离NPU硬件离线AoT编译。
- 虽然现在还不支持对量化后的网络做fine-tuning,但这种工作模式提供了基础,以后能比较方便地基于量化网络做retrain。
支持这种基于框架的开发模式,只需要在原有的框架里添加用于校准,量化算子,以及编译后的NPU引擎算子就可以了。HanGuangAI的SDK提供了相关的算子库。同时,HanGuangAI提供了一些列的Python API,用于支持相关的校准,量化和编译操作。
框架自定义算子
每个支持框架都需要实现一下一些算子,包括:
- MinMax算子
用于校准的自定义算子,根据选择的不同量化算法有不同的实现。存在于校准模型中。
- Quant算子
float32到int8或者int16的量化算子。存在于量化模型中。
- Dequant算子
反量化算子,用于将int8/int16的数转化为float32,和量化算子一起存在于量化模型中,也可能会在编译模型的EngineOp算子的前后。
- EngineOp算子
编译后的含光NPU的引擎算子,包含编译后的网络信息,以及scheme信息。存在于编译模型中。
HanGuangAI安装包中包含了所支持框架的这些自定义算子。
HanGuangAI Python编程接口
现阶段,模型的量化和编译的功能是以Python接口提供的。使用相关的API,可以将离线地将原始的Float类型的模型转化成能在含光NPU上运行的模型。暂时还未提供C语言的编译接口和实时编译链接(JIT)的接口。以后根据需求来看是否需要添加。
模型的转化提供了单模型的转化模式,和多模型的转化模式。对于量化来说,单模型的模式就足够了,因为量化与设备核心资源的信息没有关系,不需要多模型链接。但对于编译来说,需要多模型的方式,编译器根据这些模型的总体情况和资源的情况,进行跨模型的资源分配,链接的优化。
下面大致地介绍一下相关的API。
单模型量化和编译
QuantConfig
这是用于设置量化和编译配置参数的类,其构造函数是:
QuantConfig(frontend, version=None)
配置的详细设置见quantize_config的文档。里面包含不要设置的输入输出节点,输入的尺寸(shape)等,其他的是有一些可选性,没有设置的时候,使用对应的默认值。
特别需要注意的有几点:
- 关于量化算法,当前支持三种量化算法:
-
-
'naive'
:直接使用数据的minmax值,若有多个Iteration,则使用moving average方法做滑动平均。此方法保存的数据量较少,适合于快速enable。量化过程中会自动根据数据的分布来选择INT8或UINT8。 -
'kld'
:根据最小KL散度损失的方法,重新计算数据的minmax,支持INT8和UINT8。此方法需要存储calibration过程中的大量原始数据,占用几十GB的存储空间,kld计算的速度也是相对较慢。 -
'naive_kld_mix'
:如果在使用kld的方法过程中发现某些node使用kld算法可以提升INT8模型精度,则可以指定kld_nodes来加快量化速度。
-
如果开发者对默认的naive的算法精度不满意,可以尝试另外两种量化算法。但并不是说后面两种方法一定比第一种的精度更好,还是要看实际情况的。另外,kld对临时存储的要求比较高。
- 关于量化精度,当前的节点的精度是默认用的int8。但内部会做一些自动的调整,有些节点会自动使用16bit的精度。后续希望能提供一个选择给用户,在性能和精度两个方面做一个用户自己的选择。类似Habana的SynapseAI提供的那种可选性能等级的接口。
- 现在HanGuangAI还不正式支持开发者直接修改网络来做其他的量化算法。虽然逻辑上说,开发者是可以自己使 用我们的Quant算子和Dequant算子来实现自己的量化算法,这样的网络直接放入编译接口看起来也是行得通的。但这样做有不少副作用,我们暂时不提供这种做法的支持。
converter
这是用在各个前端获得转化模型的转换实现类ConvertImpl的函数。函数的定义是:
converter(graph_def, config: dict, device_info={})
每个前端都有一个自己的converter函数接口,比如ratelnn.frontend.tensorflow.converter和ratelnn.frontend.mxnet.converter。函数的返回值是ConvertImpl类的一个对象。
ConvertImpl类的构造函数是ConvertImpl(model_handler, graph_def, config, device_info={})。它的主要函数是以下三个转换函数:
-
'to_cal(self)'
:返回calibration模型,需要通过框架(tensorflow/mxnet)运行,此模型可以运行在CPU/GPU上, 运行过程中会向指定目录(config中的output_dir)写入文件。 -
'to_quant(self)'
:返回量化模型,需要通过框架(tensorflow/mxnet)运行,主要用于中间模型的暂存或者精度DEBUG, 此模型可以在CPU/GPU上运行。 -
'to_npu(self, graph_quant=None)'
:参数graph_quant: 量化模型,此参数可以为空。如果为空,则内部会自动进行量化过程;如果不为空, 则会使用用户输入的量化模型。 返回值:NPU模型,需要通过框架(tensorflow/mxnet)运行,此模型可以在NPU上运行。
下面三张图,分别是ResNet50_v1的前面一段网络的校准模型,量化模型,和编译模型。
多模型编译
多模型编译是含光NPU和HanGuangAI所特有的一种模式。在《含光800NPU编程模型》,权重装载模型中讲到过相关的模式。那里是以“多引擎”,即编译后的引擎为单位进行描述的。在这里,编译输入的时候,API输入是以模型为单位进行输入的。
多模型编译的支持还不是很完善。当前,我们提供的是内建的基于Cost Model的黑盒配置方法。由用户输入模型和配置的列表,以及设备和核心的情况,编译器在生成编译模型和引擎的时候,同时生成执行计划(Execution Scheme)。虽然开发者编译后的Graph,可以利用原来的可视化工具打开查看相关信息,包括有几个NPU的EngineOp。但Scheme信息现在没有Python的查询接口。只提供了C API读取Scheme的信息。后续,可以提供Python的API来读取整个Execution里的引擎和执行计划信息,方便开发者根据编译的结果做调整。
ratelnn_runtime
ratelnn运行时前端是ratelnn包中用于运行时检测系统当下npu device信息的模块。获取的device信息可以用于convert的时候设置device_info。当前主要有两个函数:
- ratelnn_get_device_count(): 当下系统中npu device数量检测;
- ratelnn_get_device_core_num(device_id): 某个npu device所具有的core number检测。
joint_converter
多模型转化接口函数:
joint_converter(graph_def_list, config_list, device_info={})
用于获取JointConvertImpl类的对象,JointConvertImpl是多模型的转化实现类,主要用于编译多模型。这时候,graph_def_list, config_list是对应的多个模型和配置信息。device_info是分配给整个模型组的设备核心信息。
AGraph
AGraph是我们提供的Python级别的ModelZoo,提供了FP32/NPU模型的下载,以及量化、编译的高层接口。
AGraph包含了11个Tensorflow模型和9个Mxnet的模型。一般来说,用户基于训练好的 FP32 模型,需要经过量化、编译、转换成 AliNPU 模型,最终才能在 AliNPU 上运行。为了方便用户跳过量化流程而直接在NPU上运行,AGraph中也提供了预编译好的NPU模型,用户可以直接使用这些NPU模型复现出我们发布的精度数据。另外,为了能够更便捷的使用ratelnn的接口,AGraph对ratelnn的converter接口做了简单封装,让量化流程更加流畅。
数据集的处理不在AGraph中,因此用户需要完成文档中的数据集预处理教程,之后再运行AGraph的代码,运行中AGraph会检查数据集是否已经放在指定位置上。
AGraph的使用详细介绍见AGraph_Intro。
添加模型
用户可以基于AGraph在模型库(ModelZoo)中添加新的模型。
- 准备好数据集和模型:请给你的数据集和模型取名字,取名有如下要求
-
- 名字要有区分度,不能用test_image等不明确的名字,不能太短,否则很容易重名;
- 数据集必须要打包,.tar或者.tgz都可以;
- 模型的文件必须与模型的名字一致,例如:
-
-
- tf: model_name.pb
- mxnet: model_name-symbol.json, model_name-0000.params
-
- 准备评估脚本:每个模型都要有对应的评估脚本,此脚本必须符合以下要求:
-
- 评估脚本都叫eval.py,但是需要创建一个文件夹,包含eval.py和model.py;
- 必须包含evalate()这个函数,而且必须支持相同的入参;
- 这个评估脚本必须返回精度数据,而且必须要使用ModelMetrics接口。
- 在相关文件中加入对应的数据集,模型,以及评估函数调用。
实例
业务场景
我们以一种比较常用的与场景,对视频里的多种物体,或者一个物体的多个部分,进行检测识别跟踪,然后提取属性/特征,最后动作识别或者做一些决策。大致的框图如下:
以两类物体为例,整个流程有M1~M6一共6个模型,其中有些模型可能是同一类的,但训练出来的模型参数不同,视为不同的模型。
假设想把上面的业务跑在一个有4个核心的NPU上,可以有很多种不同的装载布局方法,比如
- 如果4种模型都比较小,一个核心能放下4个,可以分布为
- 如果一个核放不下4个,就差一点点,比如,M1比较大,可能会尝试M1拆分为M1-0~M1-3,使用多核协作模式:
- 如果核心都很大,每个都接近单核,可以使用简单的串行
- 当然还可以使用一些间于中间的一些模式,比如,只是两个两个核心协作
还有其他很多种方法的布局。对于开发者来说,当前最好的方式是交给编译器来做Cost Model分析,给出一种合适的布局方案。
量化
校准和量化可以单个模型一个一个处理,也可以一起处理。推荐先一次一个模型,这样不用考虑最终编译的时候的布局,接口调用比较简单,有助于问题分解。
- 把原始模型转化为校准模型
import tensorflow as tf import ratelnn;ratelnn.tf_init() from ratelnn.frontend.tensorflow import converter # 从pb文件载入一个模型 graph_def = load_pb(pb_file_name) # 设置相关的配置,详见quantize_config model_config = { 'input_shapes': [[32, 299, 299, 3]], 'input_nodes': ['input'], 'output_nodes': ['logits', 'classes'], 'output_dir': './output_resnet_v2_50' } # 设置device0, 4个核心 dev_info = {0 : 4} # 使用TF的converter,获得ConvertImpl对象c c = converter(graph_def, model_config, device_info = dev_info) # 直接使用to_cal获得校准模型, graph_cal = c.to_cal()
- 运行校准模型,获得校准参数
现在graph_cal就是校准模型,可以存储为calibration模型文件,使用这个模型文件在你的运行代码里读入这个文件,进行推理的校准。通常100+ batch就可以了。为了准确起见,你可以选择多一些。
# 做为例子,AGraph里面实现了eval_model() eval_model(graph_cal)
运行过程中会自动收集各个节点的统计数据。并保存在output_dir
目录下。
- 做量化,转化为量化模型
做了校准之后,紧接着做量化,获得量化模型。使用同一个ConvertImpl对象,它会去output_dir读取相关的模型和数据做为输入。
# 转化生成量化模型。 graph_quant = c.to_quant()
- 验证量化模型,可以在CPU上跑一下graph_quant,看看相关的精度情况。
重复1-4步,对每个模型生成对应的graph_quant模型。
组合编译
假设想把上面的业务跑在一个有4个核心的NPU上,最简单的方法就是使用多模型编译接口,把所有模型和设备信息一起交给编译器来负责分配。只需要将六个模型和配置组成列表,如下:
# 从pb文件载入所有六个模型 graph_def_1 = load_pb(pb_file_name_1) ... graph_def_6 = load_pb(pb_file_name_6) graph_def_list = [graph_def_1, ..., graph_def_6] # 分别设置相关的配置 model_config_1 = { ... } ... model_config_6 = { ... } model_config_list = [model_config_1, ..., model_config_6] # 设置device0, 4个核心 dev_info = {0 : 4} # joint_converter,获得JointConvertImpl对象c c = joint_converter(graph_def_list, model_config_list, device_info = dev_info) # 利用之前生成的所有量化模型graph_quant,直接调用to_npu()做编译,得到含光NPU的编译模型。 graph_npu_list = c.to_npu()
把这些编译用在业务程序中,就可以走通整个推理业务流程了。
自由组合
现在的组合编译结果不一定是最佳的,因为有些因素还没有考虑进去,比如模型之间依赖等。开发者可以尝试做一些小粒度的编译,比如,将M1+M2一起放在core0+1上编译。然后M3+M5一起使用一个核编译,M4+M6一起使用一个核编译,这样,比较容易得到上面#3或者#4的结果。当然,这样不一定是好的选择,开发者可以多尝试对比做评估。
下一章介绍运行时API,结合相关的API,用户也可以小粒度的编译,然后运行时指定特定的核心来执行莫一个引擎。这比较适合大型端上的多模型业务,有单独的推理引擎的场景。
后记
我们的软件栈还在快速地开发中,一些新的特性和接口都在计划当中。特别是针对多模型的编译,我们希望提供更友好,更能得到高性能的编译结果的方式。同时,我们也在收集更多的客户反馈来改进API。
在下一章节中,会介绍独立推理引擎的NPU支持和开发。