【参考算法】地平线 MixVarGENet参考算法-V1.2.1

0 概述

在深度学习领域,backbone作为神经网络特征提取的重要部分,在一定程度上决定了任务的最终结果。为了应对更高等级的自动驾驶场景,地平线提出了与J5芯片深度耦合的高效网络结构MixVargGENet。本文将基于ImageNet数据集,对以MixVargGENet为backbone的分类算法进行介绍和使用说明。

该示例为参考算法,仅作为在J5上模型部署的设计参考,非量产算法

1 性能精度指标

dataset

输入大小

分类精度

帧率(J5/双核)

ImageNet

1x3x224x224

0.7132(浮点)0.7066(定点)

5790.35

2 模型介绍

2.1 模型设计思路

MixVarGENet高度秉承了“软硬结合”的设计理念,针对J5芯片的大算力特性做了一些定制化设计,其设计思路可以总结为:

  1. 小channel时使用normal conv,发挥J5大算力优势;
  2. 大channel时引入group conv,缓解带宽压力;
  3. Block内部扩大channel,提升网络算法性能;
  4. 缩短feature复用时间间隔,减少SRAM到DDR访存。

2.2 源码说明

2.2.1 config文件

configs/classification/mixvargenet_imagenet.py 为MixVarGENet的配置文件,定义了模型结构、数据集加载和整套训练流程。配置文件主要内容包括:

task_name = "MixVarGENet_cls"
#类别数设置
num_classes = 1000
#GPU显卡等基础参数配置
batch_size_per_gpu = 128
device_ids = [0, 1, 2, 3, 4, 5, 6, 7]
ckpt_dir = "./tmp_models/%s" % task_name
cudnn_benchmark = True
seed = None
log_rank_zero_only = True
march = March.BAYES
convert_mode = "fx"
#MixVarGEBlock配置
net_config = [
    [
        MixVarGENetConfig(
            in_channels=32,
            out_channels=32,
            head_op="mixvarge_f2",##小通道使用normal conv,对应于模型设计思路1
            stack_ops=[],
            stack_factor=1,
            stride=1,
            fusion_strides=[],
            extra_downsample_num=0,
        ),  # noqa
    ],  # stride 2
    ....
    [
        MixVarGENetConfig(
            in_channels=96,
            out_channels=160,
            head_op="mixvarge_f2_gb16",#大通道时使用group conv,对应于模型设计思路2
            stack_ops=["mixvarge_f2_gb16", "mixvarge_f2_gb16"],
            stack_factor=1,
            stride=2,
            fusion_strides=[],
            extra_downsample_num=0,
        ),  # noqa
    ],  # stride 32
]
#模型定义
model = dict(
    type="Classifier",
    backbone=dict(
        type="MixVarGENet",
        net_config=net_config,
        num_classes=1000,
        bn_kwargs={},
        bias=True,
    ),
    losses=dict(type="CEWithLabelSmooth"),
)
#部署模型定义
deploy_model = dict()
deploy_inputs = dict(img=torch.randn((1, 3, 224, 224)))
#数据集加载和预处理
data_loader = dict()
val_data_loader = dict()
#不同stage的训练策略配置
float_trainer = dict()
calibration_trainer = dict()
int_infer_trainer = dict()
#模型编译相关配置
compile_dir = os.path.join(ckpt_dir, "compile")
compile_cfg = dict()

# 不同stage的预测策略配置
float_predictor = dict()
calibration_predictor = dict()
int_infer_predictor = dict()
#推理图像预处理
infer_transforms = []
#仿真上板精度验证
align_bpu_data_loader = dict()
align_bpu_predictor = dict()
#单帧推理可视化输入预处理
def process_inputs(infer_inputs, transforms=None):
    ...
#单帧推理可视化输出处理
def process_outputs(model_outs, viz_func, vis_inputs):
    ...
#单帧推理可视化脚本配置
infer_cfg = dict()
#qat onnx导致配置
onnx_cfg = dict()

注: 如果需要复现精度,config中的训练策略最好不要修改。否则可能会有意外的训练情况出现。

MixVarGEBlock

/usr/local/lib/python3.8/dist-packages/hat/models/backbones/mixvargenet.py定义了 MixVarGENet的backbone,backbone由一个3x3的卷积层mod1()和五个MixVarGEBlock组成。MixVarGEBlock包括head op, stack op,downsample layers,fusion layers四个基本模块,其中后三个模块都是可选模块。如下为MixVarGEBlock的结构图:

并且,head_op 和stack_op都是由BasicMixVarGEBlock(如mixvarge_f2,mixvarge_f4,mixvarge_f2_gb16)这样的基本单元构成。

MixVarGENet模型的主要构成可以用BasicMixVarGEBlock表示:

stage 0

stage 1

stage 2

stage 3

stage 4

head_op

mixvarge_f2

mixvarge_f4

mixvarge_f4

mixvarge_f2_gb16

mixvarge_f2_gb16

stack_op

None

2*mixvarge_f4

2*mixvarge_f4

6*mixvarge_f2_gb16

2*mixvarge_f2_gb16

注:这里需要指出stage0是不含有stack_op的特殊MixVarGEBlock。

head op和stack op

head op 和stack op 是MixVarGENet中基本的计算单元,共享相同的结构空间,它们都是由BasicMixVarGEBlock组成。

BasicMixVarGEBlock

BasicMixVarGEBlock是构成head_op和stack_op的基本单元,基于J5芯片计算特性对其结构进行了定制化设计。BasicMixVarGEBlock结构示意图如下所示:

BasicMixVarGEBlock的基本结构由两个conv及一个shortcut组成,它在小通道数时使用normal conv,大通道数时使用group conv,还在block内部通过factor扩大通道数来提高模型性能。下面是BasicMixVarGEBlock中的两个conv层的定义代码:

mid_channle = (
            out_channels if factor == 1 else int(in_channels * factor)
        )#对应于模型设计思路3,通过factor扩大内部通道数

        conv1_groups = (
            1
            if conv1_group_base is None
            else int(in_channels / conv1_group_base)
        )
        conv2_groups = (
            1
            if conv2_group_base is None
            else int(out_channels / conv2_group_base)
        )
self.conv = nn.Sequential(
            ConvModule2d(
                in_channels, #Block输入channel
                mid_channle, #中间层输入channle
                conv1_kernel_size,
                padding=(conv1_kernel_size - 1) // 2, #0
                stride=stride,
                groups=conv1_groups,#可选的group conv base,值为1时是normal conv
                bias=bias,
                norm_layer=nn.BatchNorm2d(mid_channle, **bn_kwargs),
                act_layer=nn.ReLU(inplace=True),
            ),
            ConvModule2d(
                mid_channle, #中间层输出channle
                out_channels, #Block输出channle
                conv2_kernel_size,#1
                padding=padding,
                stride=1,
                groups=conv2_groups,#可选的group conv base
                bias=bias,
                norm_layer=nn.BatchNorm2d(out_channels, **bn_kwargs),
            ),
        )

BasicMixVarGEBlock的内部的两个conv层参数mid_channle、conv1_kernel_size、conv1_groups、conv2_kernel_size、conv2_groups都是由类似于"mixvarge_f2"的BasicMixVarGEBlock中的键值对定义的,可以通过配置不同的BasicMixVarGEBlock来组成head op和stack op。

def forward(self, x):
        if isinstance(x, list):
            fusion_inputs = x[1:]
            x = x[0]
        else:
            fusion_inputs = []
        identity = x #identity mapping
        if self.downsample is not None:
            identity = self.downsample(x)
        out = self.conv(x) #两个conv层
        out = self.short_add.add(out, identity) #short cut连接
        if len(fusion_inputs) == 0:
            out = self.relu(out)

        # fusion inputs
        if self.fusion_layers is not None:
            assert len(fusion_inputs) == len(self.fusion_layers), (
                len(fusion_inputs),
                self.fusion_layers,
            )
            for _idx, (fusion_input, fusion_layer, fusion_add) in enumerate(
                zip(fusion_inputs, self.fusion_layers, self.fusion_adds)
            ):
                fusion_ouput = fusion_layer(fusion_input)
                #特征融合
                out = fusion_add.add(fusion_ouput, out)
            out = self.fusion_relu(out)
        return out

对应代码:/usr/local/lib/python3.8/dist-packages/hat/models/base_modules/basic_mixvargenet_module.py

BLOCK_CONFIG

mixvarge_f2,mixvarge_f4,mixvarge_f2_gb16的结构是在/usr/local/lib/python3.8/dist-packages/hat/models/base_modules/basic_MixVarGENet_module.py中进行定义的,分别对应着不同结构的BasicMixVarGEBlock,相关配置细节如下表所示:

BasicMixVarGEBlock

Block Config

mixvarge_f2

{“conv2_kernel_size”: 1, “padding”: 0, “factor”: 2}

mixvarge_f4

{“conv2_kernel_size”: 1, “padding”: 0, “factor”: 4}

mixvarge_f2_gb16

{ “conv2_kernel_size”: 1, “padding”: 0, “factor”: 2, “conv1_group_base”: 16,}

上表中的参数含义是:

  • “conv2_kernel_size” 和"padding"定义了ConvModule2d卷积核的大小和padding的方式;
  • “factor” 是Block内部扩大channel的因子,它的取值根据featuremap大小来确定,通常为2的倍数,如果factor=2,那么中间卷积层mid_channle的大小就是in_channels * 2;
  • “conv1_group_base” 定义了ConvModule2d组卷积运算参数,它的取值根据芯片的计算模式,通常为8的倍数,这样就不会引入额外的padding;

downsample layers

downsample layers是kernel=3,stride=2的卷积,将当前stage的featuremap下采样,以便后续的stage 的fusion layer使用。如下为downsample layers中卷积层的代码:

downsample_fusion_layers = []
        for _idx in range(downsample_num):
            downsample_fusion_layers.append(
                ConvModule2d(
                    block_ch,
                    block_ch,
                    kernel_size=3,#3x3卷积
                    padding=1,
                    stride=2,
                    norm_layer=nn.BatchNorm2d(block_ch, **bn_kwargs),
                )
            )
        self.downsample_fusion_layers = (
            ExtSequential(downsample_fusion_layers) if downsample_num else None
        )

代码路径: /usr/local/lib/python3.8/dist-packages/hat/models/base_modules/basic_mixvargenet_module.py

fusion layer

fusion layers是pointwise卷积,用于将前几个stage的featuremap映射到当前stage,比如Stride 16的head_op 的fusion_strides包含了Stride 4和Stride 8的下采样结果。fusion layers的示意图如下所示:

代码路径:/usr/local/lib/python3.8/dist-packages/hat/models/base_modules/basic_mixvargenet_module.py

fusion_layers = []
        #fusion_channels(list): Channels of input fusion layer
        for input_channel in fusion_channels:
            layer = ConvModule2d(
                input_channel,
                out_channels,
                kernel_size=1,#pointwise卷积
                padding=0,
                stride=1,
                norm_layer=nn.BatchNorm2d(out_channels, **bn_kwargs),
            )
            fusion_layers.append(layer)
        self.fusion_layers = (
            ExtSequential(fusion_layers) if len(fusion_channels) else None
        )
        #relu激活函数
        self.fusion_relu = (
            nn.ReLU(inplace=True) if len(fusion_channels) else None
        )
        self.fusion_adds = ExtSequential(
            [
                nn.quantized.FloatFunctional()
                for i in range(len(fusion_channels))
            ]
        )

MixVarGENetBlock forward函数代码:/usr/local/lib/python3.8/dist-packages/hat/models/base_modules/basic_mixvargenet_module.py

def forward(self, x):
       #head op层
        x = self.head_layer(x)
       #stack op层
        x = self.stack_layers(x)
       #没有输出下采样层时直接输出x
        if not self.ouput_downsample:
            return x
        ds_ret = []
        ds = x
        #下采样层
        if self.downsample_fusion_layers is not None:
            for downsample in self.downsample_fusion_layers:
                ds = downsample(ds)
                ds_ret.append(ds)
        return x, ds_ret  # downsample fusion layer output from stride4

3 浮点模型训练

3.1 Before Start

3.1.1 发布物及环境部署

step1:获取发布物-
下载OE包horizon_j5_open_explorer_v$version$.tar.gz,获取方式见地平线开发者社区 OpenExplorer算法工具链 版本发布-

step2:解压发布包

tar -xzvf horizon_j5_open_explorer_v$version$.tar.gz

解压后文件结构如下:

|-- bsp
|-- ddk
|   |-- package
|   `-- samples
|       |-- ai_benchmark
|       |-- ai_forward_view_sample
|       |-- ai_toolchain
|       |   |-- ...
|       |   |-- horizon_model_train_sample
|       |   `-- model_zoo
|       |-- model_zoo
|       `-- vdsp_rpc_sample
|-- README-CN
|-- README-EN
|-- resolve_all.sh
`-- run_docker.sh

其中horizon_model_train_sample为参考算法模块,包含以下模块:

|-- horizon_model_train_sample  #参考算法示例
|   |-- plugin_basic  #qat 基础示例
|   `-- scripts  #模型配置文件、运行脚本

step3:拉取docker环境

docker pull openexplorer/ai_toolchain_ubuntu_20_j5_gpu:v$version$
#启动容器,具体参数可根据实际需求配置
#-v 用于将本地的路径挂载到 docker 路径下
nvidia-docker run -it --shm-size="15g" -v `pwd`:/WORKSPACE openexplorer/ai_toolchain_ubuntu_20_j5_gpu:v$version$

3.1.2 数据准备

3.1.2.1 数据集下载

在开始训练模型之前,第一步是需要准备好数据集,在官网下载ImageNet 数据集,下载之后的数据集格式为:

ILSVRC2012_img_train.tar
ILSVRC2012_img_val.tar
ILSVRC2012_devkit_t12.tar.gz

3.1.2.2 数据集打包

将数据集解压之后,使用以下命令进行打包:

#进入scripts文件夹
cd ddk/samples/ai_toolchain/horizon_model_train_sample/scripts
#pack train_Set
python3 tools/datasets/imagenet_packer.py --src-data-dir ${src-data-dir} --target-data-dir ./tmp_data/imagenet --split-name train --pack-type lmdb
#pack val_Set
python3 tools/datasets/imagenet_packer.py --src-data-dir ${src-data-dir} --target-data-dir ./tmp_data/imagenet - --split-name val  --pack-type lmdb

上面两条命令分别对应转换训练数据集和验证数据集,打包完成之后,tmp_data目录下的文件结构应该如下所示:

├── tmp_data
│   ├──── imagenet
│   │   ├── train_lmdb  
│   │   ├── val_lmdb

train_lmdb 和 val_lmdb 就是打包之后的训练数据集和验证数据集,也是神经网络最终读取的数据集。

3.1.3 config配置

在进行模型训练和验证之前,需要对configs文件中的部分参数进行配置,一般情况下,我们需要配置以下参数:

  • device_ids、batch_size_per_gpu:根据实际硬件配置进行device_ids和每个gpu的batchsize的配置;
  • ckpt_dir: 浮点、calib、量化训练的权重路径配置,权重下载链接在config文件夹下的README中;
  • data_loader中的data_path:上节中打包的train_lmdb路径;
  • val_data_loader中的data_path :上节中打包的val_lmdb路径;

3.2 浮点模型训练

configs/classification/mixvargenet_imagenet.py下配置参数,需要将相关硬件配置和数据集路径配置修改后使用以下命令训练浮点模型:

python3 tools/train.py --config configs/classification/mixvargenet_imagenet.py --stage float

float训练后模型ckpt的保存路径为config配置的ckpt_callback中save_dir的值,默认为ckpt_dir。

3.3 浮点模型精度验证

浮点模型训练完成以后,可以使用以下命令验证已经训练好的浮点模型精度:

python3 tools/predict.py --config configs/classification/mixvargenet_imagenet.py --stage float

4 模型量化和编译

完成浮点训练后,还需要进行量化训练和编译,才能将定点模型部署到板端。地平线对该模型的量化采用horizon_plugin框架,经过Calibration+定点模型转换后,使用compile的工具将量化模型编译成可以上板运行的hbm文件。

4.1 Calibration

模型完成浮点训练后,便可进行 Calibration。calibration在forward过程中通过统计各处的数据分布情况,从而计算出合理的量化参数。 通过运行下面的脚本就可以开启模型的Calibration过程:

python3 tools/train.py --config configs/classification/mixvargenet_imagenet.py --stage calibration

4.2 Calibration 精度验证

Calibration完成以后,可以使用以下命令验证经过calib后模型的精度:

python3 tools/predict.py --config configs/classification/mixvargenet_imagenet.py --stage calibration

对于MixVarGENet模型,仅做Calibration即可满足量化精度,无需做qat训练!

4.3 量化模型精度验证

Calibration完成后,通过运行以下命令进行量化模型的精度验证:

python3 tools/predict.py --config configs/classification/mixvargenet_imagenet.py --stage int_infer

4.4 仿真上板精度验证

python3 tools/align_bpu_validation.py --config configs/classification/mixvargenet_imagenet.py

### 4.5 模型量化编译

在量化训练完成之后,可以使用`compile`的工具用来将量化模型编译成可以上板运行的`hbm`文件,同时该工具也能预估在BPU上的运行性能,可以采用以下脚本:
```language
python3 tools/compile_perf.py --config configs/classification/mixvargenet_imagenet.py --out-dir ./ --opt 3

opt为优化等级,取值范围为0~3,数字越大优化等级越高,编译时间也会越长;out_dir为编译后产出物的存放路径。

运行后,会在out-dir目录下产出以下文件:

|-- compile
|   |-- .html #模型在bpu上的静态性能数据
|   |-- .json 
|   |-- model.hbm  #板端部署的模型
|   |-- model.hbir #
    `-- model.pt   #模型的pt文件

5 结果可视化

如果你希望可以看到训练出来的模型对于单帧的检测效果,我们的tools文件夹下面同样提供了单帧预测及可视化的脚本, 你只需要按照infer.py中的格式给出config文件的路径, 然后运行以下脚本即可:

python3 tools/infer.py --config configs/classification/mixvargenet_imagenet.py --save-path ${save-path}

1.输入图像路径通过修改config文件中infer_cfg字段中的infer_inputs来配置;-
2.–save-path为可视化结果的保存路径。

6 板端部署

本节将介绍hbm模型编译完成后,在板端使用dnn工具进行性能评测和运行AI-Benchmark示例进行性能和精度评测的流程。

6.1 dnn工具评测

hbm模型编译成功后,可以在板端使用hrt_model_exec perf工具评测hbm模型的FPS,参考命令如下:

hrt_model_exec perf --model_file {model}.hbm \
                    --thread_num 8 \
                    --core_id 0 \
                    --frame_count 1000\
                    --profile_path '.'

命令运行结束后,会在本地会产出profile.log和profile.csv日志文件,用以分析算子耗时和调度耗时。

6.2 AI Benchmark示例

OE开发包中提供了MixVarGENet的AI Benchmark示例,位于:ddk/samples/ai_benchmark/j5/qat/script/classification/mixvargenet,具体使用可以参考开发者社区J5算法工具链产品手册-AIBenchmark评测示例-

可在板端使用以下命令执行做模型评测:

#性能数据
sh fps.sh
#单帧延迟数据
sh latency.sh

运行后会在终端打印出fps和latency数据。如果要进行精度评测,请参考开发者社区J5算法工具链产品手册-AIBenchmark示例精度评测 进行数据的准备和模型的推理。

十分详细,棒棒哒