Bev Sparse 多任务模型训练

这篇教程主要是告诉大家如何利用HAT在数据集 nuscenes 上从头开始训练一个 BevSparseMultiTask 模型, 包括浮点、量化和定点模型。 该模型是一个bev多任务感知模型,包含感知三大任务的检测头:sparse动态检测头,sparse静态元素检测头和通用障碍物检测头。

训练流程

如果你只是想简单的把 BevSparseMultiTask 的模型训练起来,那么可以首先阅读一下这一章的内容。 和其他任务一样,对于所有的训练,评测任务,HAT统一采用 tools + config 的形式来完成。 在准备好原始数据集之后,可以通过下面的流程,方便地完成整个训练的流程。

数据集准备

这里以nuscense数据集为例,可以从 https://www.nuscenes.org/nuscenes 下载数据集, 对于Occupancy预测任务,还需要下载OCC的GT,可以从 https://github.com/CVPR2023-3D-Occupancy-Prediction/CVPR2023-3D-Occupancy-Prediction 下载数据集。 这里建议将下载好的数据集,解压到nuscense数据集文件夹中的 occ3d/gts 下。 同时,为了提升训练的速度,我们对原始的jpg格式的数据集做了一个打包,将其转换成lmdb格式的数据集。

python3 tools/dataset_converters/create_data_flashocc.py --src-data-path ./tmp_data/nuscenes/ -o ./tmp_data/nuscenes/occ3d

上面这条命令会在 ./tmp_data/nuscenes/occ3d 下面生成 nuscenes_infos_train.pkl 和 nuscenes_infos_val.pkl 两个文件, 接下来执行下面命令进行打包:

python3 tools/datasets/occ3d_nuscenes_packer.py --src-data-dir ./tmp_data/nuscenes/ --pack-type lmdb --target-data-dir ./tmp_data/occ3d_nuscenes --split-name test --anno-file ./tmp_data/nuscenes/occ3d/nuscenes_infos_val.pkl
python3 tools/datasets/occ3d_nuscenes_packer.py --src-data-dir ./tmp_data/nuscenes/ --pack-type lmdb --target-data-dir ./tmp_data/occ3d_nuscenes --split-name train --anno-file ./tmp_data/nuscenes/occ3d/nuscenes_infos_train.pkl

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

tmp_data
    |-- nuscenes
        |-- metas
        |-- v1.0-trainval 
        |-- occ3d
            | -- gts
            | -- nuscenes_infos_train.pkl
            | -- nuscenes_infos_val.pkl
    |-- occ3d_nuscenes
        |-- train_lmdb
        |-- val_lmdb

train_lmdbval_lmdb 就是打包之后的训练数据集和验证数据集,也是网络最终读取的数据集,meta中为评测脚本需要的初始化信息,具体信息是从nuscenes原始数据集中拷贝得来。

模型训练

数据集准备好之后,就可以开始训练浮点型的 BevSparseMultiTask 网络了。在网络训练开始之前,你可以使用以下命令先测试一下网络的计算量和参数数量:

python3 tools/calops.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py --method hook

如果你只是单纯的想启动这样的训练任务,只需要运行下面的命令就可以:

python3 tools/train.py --stage "float" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

由于HAT算法包使用了一种巧妙的注册机制,使得每一个训练任务都可以按照这种train.py加上config配置文件的形式启动。 train.py是统一的训练脚本,与任务无关,我们需要训练什么样的任务、使用什么样的数据集以及训练相关的超参数设置都在指定的config配置文件里面。 config文件里面提供了模型构建、数据读取等关键的dict。

导出定点模型

完成量化训练后,便可以开始导出定点模型。可以通过下面命令来导出:

python3 tools/export_hbir.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

模型验证

模型训练完成之后,我们还可以验证训练出来的模型性能。由于我们提供了float, calibration, qat两阶段的训练过程,相应的我们可以验证这三个阶段训练出来的模型性能,只需要相应的运行以下三条命令即可:

python3 tools/predict.py --stage "float" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 
python3 tools/predict.py --stage "calibration" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

同时,我们还提供了quantization模型的性能测试,只需要运行以下命令,但需要注意是必须要先导出hbir:

python3 tools/predict.py --stage "int_infer" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

这个显示出来的精度才是最终的量化模型的真正精度,当然这个精度和qat验证阶段的精度应该是保持十分接近的。

模型推理和结果可视化

如果你希望可以看到训练出来的模型的可视化效果,我们的tools文件夹下面同样提供了可视化脚本,你只需要运行以下脚本即可:

python3 tools/infer_hbir.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py  --model-inputs ${model_inputs} --save-path ${save_path} --use-dataset

python3 tools/infer_hbir.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py  --model-inputs ${model_inputs} --save-path ${save_path}

仿真上板精度验证

除了上述模型验证之外,我们还提供和上板完全一致的精度验证方法,可以通过下面的方式完成:

python3 tools/validation_hbir.py --stage "align_bpu" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

定点模型检查和编译

在HAT中集成的量化训练工具链主要是为了地平线的计算平台准备的,因此,对于量化模型的检查和编译是必须的。 我们在HAT中提供了模型检查的接口,可以在定义好量化模型之后,先检查能否在 BPU 上正常运行:

python3 tools/model_checker.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

在训练完成之后,可以使用 compile_perf_hbir 的工具用来将量化模型编译成可以上板运行的 hbm 文件,同时该工具也能预估在BPU上的运行性能,可以采用以下脚本:

python3 tools/compile_perf_hbir.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

训练细节

在这个说明中,我们对模型训练需要注意的一些事项进行说明,主要为 config 的一些相关设置。

模型构建

BevSparseMultiTask 模型包含动态,静态,通用障碍物三个任务头。其中动态任务选用的是 SparseBEVOEHead


det_head = dict(
    type="SparseBEVOEHead",
    enable_dn=True,
    level_index=[2],
    cls_threshold_to_reg=0.05,
    instance_bank=dict(
        type="MemoryBankOE",
        num_anchor=384,
        embed_dims=embed_dims,
        num_memory_instances=384,
        anchor=anchor_file,
        num_temp_instances=128,
        confidence_decay=0.6,
    ),
    anchor_encoder=dict(
        type="SparseBEVOEEncoder",
        pos_embed_dims=128,
        size_embed_dims=32,
        yaw_embed_dims=32,
        vel_embed_dims=64,
        vel_dims=3,
    ),
    num_single_frame_decoder=num_single_frame_decoder,
    operation_order=[
        "deformable",
        "ffn",
        "norm",
        "refine",
    ]
    * num_single_frame_decoder
    + [
        "temp_interaction",
        "interaction",
        "norm",
        "deformable",
        "ffn",
        "norm",
        "refine",
    ]
    * (num_decoder - num_single_frame_decoder),
    ffn=dict(
        type="AsymmetricFFNOE",
        in_channels=embed_dims * 2,
        pre_norm=True,
        embed_dims=embed_dims,
        feedforward_channels=embed_dims * 4,
        num_fcs=2,
        ffn_drop=drop_out,
    ),
    deformable_model=dict(
        type="DeformableFeatureAggregationOE",
        embed_dims=embed_dims,
        num_groups=num_groups,
        num_levels=num_levels,
        num_cams=6,
        attn_drop=0.15,
        use_camera_embed=True,
        residual_mode="cat",
        kps_generator=dict(
            type="SparseBEVOEKeyPointsGenerator",
            num_pts=8,
        ),
    ),
    refine_layer=dict(
        type="SparseBEVOERefinementModule",
        embed_dims=embed_dims,
        num_cls=num_classes,
        refine_yaw=True,
    ),
    target=dict(
        type="SparseBEVOETarget",
        num_dn_groups=5,
        num_temp_dn_groups=3,
        dn_noise_scale=[2.0] * 3 + [0.5] * 7,
        max_dn_gt=32,
        add_neg_dn=True,
        cls_weight=2.0,
        box_weight=0.25,
        reg_weights=[2.0] * 3 + [0.5] * 3 + [0.0] * 4,
        cls_wise_reg_weights={
            CLASSES.index("traffic_cone"): [
                2.0,
                2.0,
                2.0,
                1.0,
                1.0,
                1.0,
                0.0,
                0.0,
                1.0,
                1.0,
            ],
        },
    ),
    cls_allow_reverse=[CLASSES.index("barrier")],
    loss_cls=dict(
        type="FocalLoss",
        loss_name="cls",
        num_classes=num_classes + 1,
        gamma=2.0,
        alpha=0.25,
        loss_weight=2.0,
    ),
    loss_reg=dict(type="L1Loss", loss_weight=0.25),
    loss_cns=dict(type="CrossEntropyLoss", use_sigmoid=True),
    loss_yns=dict(type="GaussianFocalLoss"),
    decoder=dict(type="SparseBEVOEDecoder"),
    reg_weights=[2.0] * 3 + [1.0] * 7,
)

静态任务选用的是 SparseMapPerceptionDecoder


om_head = dict(
    type="SparseMapPerceptionDecoder",
    embed_dims=embed_dims,
    num_cam=6,
    num_vec_one2one=om_num_anchor,
    num_vec_one2many=0,
    k_one2many=0,
    num_pts_per_vec=fixed_ptsnum_per_pred_line,
    transform_method="minmax",
    is_deploy=False,
    decoder=dict(
        type="SparseOMOEHead",
        feat_indices=[2],
        num_anchor=om_num_anchor,
        num_pts_per_vec=fixed_ptsnum_per_pred_line,
        num_views=6,
        projection_mat_key="lidar2img",
        anchor_encoder=dict(
            type="SparseOEPoint3DEncoder",
            embed_dims=embed_dims,
            input_dim=fixed_ptsnum_per_pred_line * 2,
        ),
        instance_bank=dict(
            type="InstanceBankOE",
            num_anchor=om_num_anchor,
            embed_dims=embed_dims,
            anchor=map_anchor_file,
            num_temp_instances=0,
            confidence_decay=0.6,
            anchor_grad=True,
            feat_grad=True,
        ),
        instance_interaction=dict(
            type=MultiheadAttention,
            embed_dim=embed_dims,
            num_heads=num_groups,
            batch_first=True,
            attn_drop=drop_out,
            proj_drop=drop_out,
        ),
        norm_layer=dict(
            type=torch.nn.LayerNorm,
            normalized_shape=embed_dims,
        ),
        ffn=dict(
            type="AsymmetricFFNOE",
            in_channels=embed_dims * 2,
            pre_norm=True,
            embed_dims=embed_dims,
            num_fcs=2,
            ffn_drop=0.1,
            feedforward_channels=embed_dims * 4,
        ),
        deformable_model=dict(
            type="DeformableFeatureAggregationOEv2",
            embed_dims=embed_dims,
            num_groups=num_groups,
            num_levels=num_levels,
            num_cams=6,
            attn_drop=0.15,
            residual_mode="cat",
            use_camera_embed=True,
            grid_align_num=4,
            kps_generator=dict(
                type="SparsePoint3DKeyPointsGenerator",
                embed_dims=embed_dims,
                num_sample=fixed_ptsnum_per_pred_line,
                num_learnable_pts=2,
                fix_height=(0, 0.5, -0.5),
                ground_height=-1.84023,  # ground height in lidar frame
            ),
        ),
        refine_layer=dict(
            type="SparsePoint3DRefinementModule",
            embed_dims=embed_dims,
            num_sample=fixed_ptsnum_per_pred_line,
            coords_dim=2,
            num_cls=len(map_classes),
            with_cls_branch=True,
        ),
        num_decoder=num_decoder,
        num_single_frame_decoder=num_single_frame_decoder,
        operation_order=[
            "interaction",
            "norm",
            "deformable",
            "ffn",
            "norm",
            "refine",
        ]
        * num_single_frame_decoder
        + [
            "interaction",
            "norm",
            "deformable",
            "ffn",
            "norm",
            "refine",
        ]
        * (num_decoder - num_single_frame_decoder),
    ),
    aux_seg=map_aux_seg_cfg,
    criterion=dict(
        type="MapTRCriterion",
        dir_interval=1,
        num_classes=num_map_classes,
        code_weights=[1.0, 1.0, 1.0, 1.0],
        sync_cls_avg_factor=True,
        pc_range=map_point_cloud_range,
        num_pts_per_vec=fixed_ptsnum_per_pred_line,  # one bbox
        num_pts_per_gt_vec=fixed_ptsnum_per_gt_line,
        gt_shift_pts_pattern="sparsedrive",
        aux_seg=map_aux_seg_cfg,
        pred_absolute_points=True,
        assigner=dict(
            type="MapTRAssigner",
            cls_cost=dict(type="FocalLossCost", weight=1.0),
            pts_cost=dict(type="OrderedPtsL1Cost", weight=0.5, beta=0.01),
            pc_range=map_point_cloud_range,
            pred_absolute_points=True,
        ),
        loss_cls=dict(
            type="FocalLoss",
            loss_name="cls",
            num_classes=num_map_classes + 1,
            alpha=0.25,
            gamma=2.0,
            loss_weight=1.0,
            reduction="mean",
        ),
        loss_pts=dict(type="PtsL1Loss", loss_weight=0.5, beta=0.01),
        loss_dir=dict(type="PtsDirCosLoss", loss_weight=0),
        loss_pv_seg=dict(type="SimpleLoss", pos_weight=1.0, loss_weight=2.0),
    ),
    post_process=dict(
        type="MapTRPostProcess",
        pc_range=map_point_cloud_range,
        post_center_range=map_post_center_range,
        max_num=om_num_anchor,
        num_classes=num_map_classes,
        pred_absolute_points=True,
    ),
)

通用障碍物任务选用的head是 FlashOccHead

occ_head = dict(
    type="FlashOccHead",
    bev_feat_index=-1,
    bev_upscale=2,
    view_transformer=dict(
        type="LSSTransformer",
        in_channels=256,
        feat_channels=64,
        z_range=(-1.0, 5.4),
        depth=occ_depth,
        num_points=occ_num_points,
        bev_size=occ_bev_size,
        grid_size=occ_grid_size,
        num_views=6,
        grid_quant_scale=grid_quant_scale,
        depth_grid_quant_scale=depth_grid_quant_scale,
        homo_key="lidar2img",
    ),
    bev_encoder=dict(
        type="BevEncoder",
        backbone=dict(
            type="HENet",
            in_channels=64,
            block_nums=[4, 3, 8, 6],
            embed_dims=[64, 128, 192, 384],
            attention_block_num=[0, 0, 0, 0],
            mlp_ratios=[2, 2, 2, 3],
            mlp_ratio_attn=2,
            act_layer=["nn.GELU", "nn.GELU", "nn.GELU", "nn.GELU"],
            use_layer_scale=[True, True, True, True],
            layer_scale_init_value=1e-5,
            num_classes=1000,
            include_top=False,
            extra_act=[False, False, False, False],
            final_expand_channel=0,
            feature_mix_channel=1024,
            block_cls=["GroupDWCB", "GroupDWCB", "AltDWCB", "DWCB"],
            down_cls=["S2DDown", "S2DDown", "S2DDown", "None"],
            patch_embed="origin",
            quant_input=False,
        ),
        neck=dict(
            type="BiFPN",
            in_strides=[2, 4, 8, 16, 32],
            out_strides=[2, 4, 8, 16, 32],
            stride2channels=dict({2: 64, 4: 64, 8: 128, 16: 192, 32: 384}),
            out_channels=48,
            num_outs=5,
            stack=3,
            start_level=0,
            end_level=-1,
            fpn_name="bifpn_sum",
            upsample_type="function",
            use_fx=False,
        ),
    ),
    bev_decoder=dict(
        type="FlashOccDetDecoder",
        use_mask=True,
        num_classes=num_classes_occ,
        occ_head=dict(
            type="BEVOCCHead2D",
            in_dim=48,
            out_dim=128,
            Dz=16,
            num_classes=num_classes_occ,
            use_predicter=True,
            use_upsample=True,
        ),
        loss_occ=dict(
            type="CrossEntropyLoss",
            use_sigmoid=False,
            ignore_index=255,
            loss_weight=6.0,
        ),
    ),
)

三个任务的head和backbone一起构成了这个Bev多任务感知模型,model 在config中的定义为:


task_heads = OrderedDict()
if enable_det_head:
    task_heads["det"] = det_head
if enable_om_head:
    task_heads["om"] = om_head
if enable_occ_head:
    task_heads["occ"] = occ_head

model = dict(
    type="SparseBevFusionMultitaskOE",
    compiler_model=False,
    backbone=dict(
        type="HENet",
        in_channels=3,
        block_nums=[4, 3, 8, 6],
        embed_dims=[64, 128, 192, 384],
        attention_block_num=[0, 0, 0, 0],
        mlp_ratios=[2, 2, 2, 3],
        mlp_ratio_attn=2,
        act_layer=["nn.GELU", "nn.GELU", "nn.GELU", "nn.GELU"],
        use_layer_scale=[True, True, True, True],
        layer_scale_init_value=1e-5,
        num_classes=1000,
        include_top=False,
        extra_act=[False, False, False, False],
        final_expand_channel=0,
        feature_mix_channel=1024,
        block_cls=["GroupDWCB", "GroupDWCB", "AltDWCB", "DWCB"],
        down_cls=["S2DDown", "S2DDown", "S2DDown", "None"],
        patch_embed="origin",
    ),
    neck=dict(
        type="MMFPN",
        in_strides=[2, 4, 8, 16, 32],
        in_channels=[64, 64, 128, 192, 384],
        fix_out_channel=256,
        out_strides=[4, 8, 16, 32],
    ),
    depth_branch=dict(  # for auxiliary supervision only
        type="DenseDepthNetOE",
        embed_dims=embed_dims,
        num_depth_layers=num_depth_layers,
        loss_weight=0.2,
    ),
    head=task_heads,
)

其中, model 下面的 type 表示定义的模型名称,剩余的变量表示模型的其他组成部分。这样定义模型的好处在于我们可以很方便的替换我们想要的结构。 训练脚本在启动之后,会调用 build_model 接口,将这样一个dict类型的model变成类型为 torch.nn.Module 类型的model。

数据增强

model 的定义一样,数据增强的流程是通过在config配置文件中定义 data_loaderval_data_loader 这两个dict来实现的,分别对应着训练集和验证集的处理流程。 Nuscenes 数据集同时包含动态静态和通用障碍物的标注,我们可以在一个数据集上同时训练三个任务。 具体代码实现中,NuscenesBevDataset可以提供动态和occ的标注,NuscenesSparseMapDataset 继承了它, 可以同时提供动态,静态和occ的标注。 动态检测头用到了时序信息,所以 data_loader 采用了 DistStreamBatchSampler,确保数据按时序返回。

train_dataset = dict(
    type="NuscenesSparseMapDataset",
    data_path=os.path.join(data_rootdir, "train_lmdb"),
    map_path=meta_rootdir,
    pc_range=map_point_cloud_range,
    test_mode=False,
    bev_size=(bev_h_, bev_w_),
    fixed_ptsnum_per_line=fixed_ptsnum_per_gt_line,
    padding_value=-10000,
    map_classes=map_classes,
    aux_seg=map_aux_seg_cfg,
    with_bev_bboxes=False,
    with_ego_bboxes=False,
    with_bev_mask=False,
    with_lidar_bboxes=True,
    need_lidar=True,
    num_split=2,
    num_sweeps=9,
    load_dim=5,
    use_dim=[0, 1, 2, 3, 4],
    use_lidar_gt=True,
    with_ego_occ=False,
    with_lidar_occ=True,
    filter_empty_gt=False,
    transforms=[
        dict(type="MultiViewsImgResize", scales=(0.40, 0.47)),
        dict(type="MultiViewsImgCrop", size=(256, 704), random=False),
        dict(type="MultiViewsImgFlip"),
        dict(type="MultiViewsImgRotate", rot=(-5.4, 5.4)),
        dict(type="BevBBoxRotation", rotation_3d_range=(-0.3925, 0.3925)),
        dict(type="MultiViewsPhotoMetricDistortion", use_pil=False),
        dict(
            type="MultiViewsGridMask",
            use_h=True,
            use_w=True,
            rotate=1,
            offset=False,
            ratio=0.5,
            mode=1,
            prob=0.7,
        ),
        dict(
            type="MultiViewsImgTransformWrapper",
            transforms=[
                dict(type="PILToTensor"),
                dict(type="BgrToYuv444", rgb_input=True),
                dict(type="Normalize", mean=128, std=128),
            ],
        ),
    ],
)

data_loader = dict(
    type=torch.utils.data.DataLoader,
    dataset=train_dataset,
    batch_sampler=dict(
        type="DistStreamBatchSampler",
        batch_size=batch_size_per_gpu,
        dataset=train_dataset,
        keep_consistent_seq_aug=True,
        skip_prob=0.0,
        sequence_flip_prob=0.0,
    ),
    num_workers=dataloader_workers,
    pin_memory=True,
    collate_fn=collate_nuscenes,
)

训练策略

为了训练一个精度高的模型,好的训练策略是必不可少的。对于每一个训练任务而言,相应的训练策略同样都定义在其中的config文件中,从 float_trainer 这个变量就可以看出来。

float_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    pretrain_ckpt_dir, "float-checkpoint-best.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                ignore_tensor_shape=True,
                verbose=True,
            ),
        ],
    ),
    data_loader=data_loader,
    optimizer=dict(
        type=torch.optim.AdamW,
        params={
            "backbone": dict(lr=3e-4),
        },
        # eps=1e-8,
        # betas=(0.9, 0.999),
        lr=3e-4,
        weight_decay=0.001,
    ),
    batch_processor=batch_processor,
    num_steps=num_steps,
    stop_by="step",
    # num_epochs=100,
    device=None,
    callbacks=[
        stat_callback,
        loss_show_update,
        dict(type="ExponentialMovingAverage"),
        grad_callback,
        dict(
            type="CosineAnnealingLrUpdater",
            warmup_len=500,
            warmup_by="step",
            warmup_lr_ratio=1.0 / 3,
            step_log_interval=500,
            update_by="step",
            min_lr_ratio=1e-3,
        ),
        val_callback,
        ckpt_callback,
    ],
    train_metrics=dict(
        type="LossShow",
    ),
    sync_bn=True,
    val_metrics=val_metrics,
)

float_trainer 从大局上定义了我们的训练方式,包括使用多卡分布式训练(distributed_data_parallel_trainer),模型训练的epoch次数,以及优化器的选择。 同时 callbacks 中体现了模型在训练过程中使用到的小策略以及您想实现的操作,包括指数移动平均训练(ExponentialMovingAverage), 学习率的变换方式(CosineAnnealingLrUpdater), 在训练过程中验证模型的指标(Validation),以及保存(Checkpoint)模型的操作。当然,如果你有自己希望模型在训练过程中实现的操作,也可以按照这种dict的方式添加。 float_trainer 负责将整个训练的逻辑给串联起来。

注解

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

通过上面的介绍,你应该对config文件的功能有了一个比较清楚的认识。然后通过前面提到的训练脚本,就可以训练一个高精度的纯浮点的检测模型。 当然训练一个好的检测模型不是我们最终的目的,它只是做为一个pretrain为我们后面训练定点模型服务的。

量化模型训练

当我们有了纯浮点模型之后,就可以开始量化模型了。和浮点训练类似,我们可以用下面的命令,对模型进行校准(calibration), 得到伪量化模型。

python3 tools/train.py --stage "calibration" --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

可以看到,我们的配置文件没有改变,只改变了 stage 的类型。此时我们使用的训练策略来自于config文件中的 calibration_trainer


calibration_trainer = dict(
    type="Calibrator",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=float_checkpoint,
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
                load_ema_model=True,
            ),
            dict(
                type="Float2Calibration",
                convert_mode=convert_mode,
                example_inputs=example_inputs,
                qconfig_setter=calibration_qconfig_setter,
            ),
        ],
    ),
    data_loader=calibration_data_loader,
    batch_processor=calibration_batch_processor,
    num_steps=calibration_step,
    device=None,
    callbacks=[
        ckpt_callback,
        val_callback,
    ],
    log_interval=calibration_step / 10,
    val_metrics=val_metrics,
)

当我们训练量化模型的时候,需要设置model_convert_pipeline,它首先载入模型浮点权重,然后用 Float2Calibration 将模型从浮点模型,转化成校准模型。 其中 calibration_qconfig_setter 定义了模型的量化配置。目前的配置是:


sensitivity_templates = []
table_dir = os.path.join(ckpt_dir, "quant_analysis")
sensitive_setting_dict = {
    "output_det.classification_L1_sensitive_ops.pt": 20,
    "output_det.prediction_L1_sensitive_ops.pt": 20,
    "output_det.quality_L1_sensitive_ops.pt": 10,
    "output_om.all_pts_preds_L1_sensitive_ops.pt": 5,
    "output_om.all_bbox_preds_L1_sensitive_ops.pt": 5,
    "output_om.all_cls_scores_L1_sensitive_ops.pt": 5,
}
for k, v in sensitive_setting_dict.items():
    sensitive_table = load_ckpt(os.path.join(table_dir, k))
    sensitivity_templates.append(
        SensitivityTemplate(
            sensitive_table,
            topk_or_ratio=v,
        )
    )
      
backbone = ["backbone"]
int16_max_point_muls = [
    f"head.det.layers.{layer}.point_mul" for layer in range(3, 39, 7)
] + [f"head.om.decoder.layers.{layer}.point_mul" for layer in range(2, 33, 6)]
int16_max_reciprocal_op = [
    f"head.det.layers.{layer}.reciprocal_op" for layer in range(3, 39, 7)
]
int16_add_ops = [f"head.det.layers.{layer}.add2" for layer in range(6, 35, 7)]
# int16_add_ops += [f"head.om.layers.{layer}._generated_add_1" for layer in range(5, 35, 6)]
float32_ops = [
    f"head.det.layers.{layer}.cls_layers" for layer in range(6, 35, 7) # 6, 13, 20, 27, 34
] + [f"head.det.layers.{layer}.quality_layers" for layer in range(6, 35, 7)]
q_template = [ 
    ModuleNameTemplate({"": qint8}),
    MatmulDtypeTemplate(  
        input_dtypes=[qint8, qint8],
        # prefix=[""],  
    ),
    ConvDtypeTemplate( 
        input_dtype=qint8,
        weight_dtype=qint8, 
        # prefix=[""],  
    ),
    ModuleNameTemplate(
        {
            m: {"dtype": qint8, "threshold": 1.0} for m in ["backbone.quant"]
        }, 
        
    ),
    ModuleNameTemplate(
        {
            m: {"dtype": qint16, "threshold": 1.1}
            for m in int16_max_point_muls
        }, 
      
    ),
    ModuleNameTemplate(
        {
            m: {"dtype": qint16, "threshold": 10}
            for m in int16_max_reciprocal_op
        }, 
       
    ),
    ModuleNameTemplate(
        {
            m: {"dtype": qint16, "threshold": 50}
            for m in ["head.det.fc_after"]
        }, 
       
    ),
    ModuleNameTemplate(
        {
            m: {"dtype": qint16, "threshold": 10}
            for m in ["head.det.fc_before"]
        },  
    ),
    ModuleNameTemplate(
        {
            m: {"input": [qint16, qint16], "output": qint16}
            for m in int16_add_ops
        }, 
        
    ),
    *sensitivity_templates,
    ModuleNameTemplate(
        {m: None for m in float32_ops},  # quant int8,固定 scale 方式 1
        freeze=True,
    ),  
]

calibration_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(  # 1. 主要用于获取 observer
        # observer=(observer_v2.MSEObserver)
        observer=(observer_v2.HistogramObserver)
    ),
    templates=q_template,
    enable_optimize=True, 
    save_dir=os.path.join(
        ckpt_dir, "./qconfig_setting"
    ), 
    custom_qconfig_mapping=None,  
)

量化配置的解释

这里的量化配置略微有点复杂,我们将会逐步细致的解释。

首先,calibration_qconfig_setter 的第一个参数 reference_qconfig 定义了一个基础的量化配置,使用的observer是 observer_v2.HistogramObserver. 这是基于直方图统计的校准工具, 它会全局统计模型每一层输入输出 feature 的直方图统计量,然后决定每一层的量化步长 scale.

calibration_qconfig_setter 的第二个参数是 templates 对应的 q_template, 从上到下用了很多template 定义量化配置。 最顶部是 ModuleNameTemplate({"": qint8}) , 定义了全局的量化配置是 qint8 的, 然后两行 MatmulDtypeTemplateConvDtypeTemplate 的配置分别定义了Matmul算子输入和Conv算子输入的类型为 qint8. 接着,许多行使用 ModuleNameTemplate 为模型中的很多op特别设置了量化类型和固定scale,这是基于对模型结构和统计量的观察得出的配置。 用户也可以像这里类似地修改模型每一层的量化配置。 注意到 q_template 中还有一行 *sensitivity_templates , 这是通过敏感度分析的结果配置的高精度模板。

尽管基于经验,我们已经手动配置了一些高精度算子,量化模型还是很难恢复浮点模型的精度,为了分析模型量化的问题,我们建议用户对模型进行敏感度分析。 要运行模型敏感度分析工具,可以跑一下命令:

python3 tools/quant_analysis.py --config configs/bev/bev_sparse_det_maptr_flashocc_henet_tinym_nuscenes.py 

这个命令对应config中的以下配置:

quant_analysis_solver = dict(
    type="QuantAnalysis",
    model=analysis_model,
    device_id=-1,
    dataloader=analysis_dataloader,    
    num_steps=100,
    baseline_model_convert_pipeline=float_predictor["model_convert_pipeline"],
    analysis_model_convert_pipeline=calibration_predictor[
        "model_convert_pipeline"
    ],
    analysis_model_type="fake_quant",
    out_dir=os.path.join(ckpt_dir, "quant_analysis"),
)

敏感度分析的主要原理是:给定数据,工具分别运行浮点模型和量化模型,找到输出相差最大的样本,称之为 bad case. 然后对于量化模型,工具遍历模型每个算子,进行单算子量化,按量化后浮点模型输出比较的误差进行排序,得到模型量化敏感的算子列表。

sensitivity_templates 正是读取了这个敏感度排序列表,将敏感度排序在前列的算子配置成高精度,从而提高模型的量化精度。

本模型经过校准后,模型量化精度已经达到了浮点训练的 99% 以上,我们无需再进行额外的量化感知训练就可以达到精度目标。

关于模型量化的更多信息,请阅读 量化感知训练-精度调优工具使用指南 章节的内容。