Bev多任务模型训练

BEV参考算法基于地平线自研深度学习框架开发,训练config位于HAT/configs/bev/路径下。 下文以HAT/configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py为例介绍如何配置并训练BEV参考算法。

训练流程

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

数据集准备

这里以nuscense数据集为例,可以从 https://www.nuscenes.org/nuscenes 下载数据集 。同时,为了提升训练的速度,我们对原始的jpg格式的数据集做了一个打包,将其转换成lmdb格式的数据集。只需要运行下面的脚本,就可以成功实现转换:

python3 tools/datasets/nuscenes_packer.py --src-data-dir WORKSAPCE/datasets/nuscenes/ --pack-type lmdb --target-data-dir . --version v1.0-trainval --split-name val
python3 tools/datasets/nuscenes_packer.py --src-data-dir WORKSAPCE/datasets/nuscenes/ --pack-type lmdb --target-data-dir . --version v1.0-trainval --split-name train

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

tmp_data
    |-- nuscenes
        |-- metas
        |-- v1.0-trainval 
        |-- train_lmdb
        |-- val_lmdb

train_lmdb和val_lmdb就是打包之后的训练数据集和验证数据集,也是网络最终读取的数据集。metas中为分割模型需要的地图信息。

模型训练

数据集准备好之后,就可以开始训练浮点型的bev多任务网络了。

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

python3 tools/train.py --stage "float" --config configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py
python3 tools/train.py --stage "calibration" --config configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py
python3 tools/train.py --stage "qat" --config configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py

这三条命令分别对应以下三个训练阶段:

浮点模型训练(float):这是训练的第一步,模型以浮点精度进行训练,生成一个高精度的浮点模型。 该模型将作为后续校准和量化感知训练的基础。

校准模型训练(calibration): 在这一阶段,模型不会更新权重,而是通过输入真实样本,统计每一层的输入输出范围,生成量化参数(如 scale 和 zero-point)。 校准的目的是为后续的量化感知训练(QAT)做好准备。

量化感知训练(qat): 这是训练的最后阶段,模型在量化参数的基础上进行微调。 量化感知训练会模拟量化过程(如使用低精度计算),以确保模型在实际部署时的性能。

具体内容请阅读 量化感知训练(QAT) 章节的内容。

导出定点模型

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

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

模型验证

在完成训练之后,可以得到训练完成的浮点、量化或定点模型。和训练方法类似,我们可以用相同方法来对训好的模型做指标验证,得到为 FloatCalibrationQuantized 的指标,分别为浮点、量化和完全定点的指标。

python3 tools/predict.py --stage "float" --config configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py
python3 tools/predict.py --stage "qat" --config configs/bev/bevformer_tiny_resnet50_detection_nuscenes.py

和训练模型时类似,--stage 后面的参数为 "float""calibration" 时,分别可以完成对训练好的浮点模型、量化模型的验证。

定点模型精度验证也可使用下面命令,但需要注意是必须要先导出hbir:

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

模型推理和结果可视化

HAT 提供了 infer_hbir.py 脚本提供了对定点模型的推理结果进行可视化展示:

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

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

定点模型检查和编译

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

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

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

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

以上就是从数据准备到生成量化可部署模型的全过程。

训练细节

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

模型构建

model = dict(
    type="BevFormer",
    out_indices=(-1,),
    backbone=dict(
        type="ResNet50",
        num_classes=1000,
        bn_kwargs={},
        include_top=False,
    ),
    neck=dict(
        type="FPN",
        in_strides=[32],
        in_channels=[2048],
        out_strides=[32],
        out_channels=[_dim_],
        bn_kwargs=dict(eps=1e-5, momentum=0.1),
    ),
    view_transformer=dict(
        type="BevFormerViewTransformer",
        bev_h=bev_h_,
        bev_w=bev_w_,
        pc_range=point_cloud_range,
        num_points_in_pillar=4,
        embed_dims=_dim_,
        queue_length=3,
        in_indices=(-1,),
        max_camoverlap_num=2,
        virtual_bev_h=20,
        virtual_bev_w=32,
        positional_encoding=dict(
            type="LearnedPositionalEncoding",
            num_feats=_pos_dim_,
            row_num_embed=bev_h_,
            col_num_embed=bev_w_,
        ),
        encoder=dict(
            type="BEVFormerEncoder",
            num_layers=3,
            return_intermediate=False,
            bev_h=bev_h_,
            bev_w=bev_w_,
            embed_dims=_dim_,
            encoder_layer=dict(
                type="BEVFormerEncoderLayer",
                selfattention=dict(
                    type="HorizonTemporalSelfAttention",
                    embed_dims=_dim_,
                    num_levels=1,
                    grid_align_num=100,
                    reduce_align_num=8,
                    feats_size=[[bev_w_, bev_h_]],
                ),
                crossattention=dict(
                    type="HorizonSpatialCrossAttention",
                    max_camoverlap_num=2,
                    bev_h=bev_h_,
                    bev_w=bev_w_,
                    deformable_attention=dict(
                        type="HorizonMultiScaleDeformableAttention3D",
                        embed_dims=_dim_,
                        num_points=8,
                        num_levels=_num_levels_,
                        grid_align_num=20,
                        feats_size=[[25, 15]],
                    ),
                    embed_dims=_dim_,
                ),
                dropout=0.1,
            ),
        ),
    ),
    bev_decoders=[
        dict(
            type="BEVFormerDetDecoder",
            bev_h=bev_h_,
            bev_w=bev_w_,
            num_query=900,
            embed_dims=_dim_,
            pc_range=point_cloud_range,
            decoder=dict(
                type="DetectionTransformerDecoder",
                num_layers=6,
                return_intermediate=True,
                decoder_layer=dict(
                    type="DetrTransformerDecoderLayer",
                    crossattention=dict(
                        type="HorizonMultiScaleDeformableAttention",
                        embed_dims=_dim_,
                        num_levels=1,
                        batch_first=False,
                        grid_align_num=10,
                        feats_size=[[bev_w_, bev_h_]],
                    ),
                    dropout=0.1,
                ),
            ),
            criterion=dict(
                type="BevFormerCriterion",
                assigner=dict(
                    type="BevFormerHungarianAssigner3D",
                    cls_cost=dict(type="FocalLossCost", weight=2.0),
                    reg_cost=dict(type="BBox3DL1Cost", weight=0.25),
                ),
                loss_cls=dict(
                    type="FocalLoss",
                    loss_name="cls",
                    num_classes=num_classes + 1,
                    alpha=0.25,
                    gamma=2.0,
                    loss_weight=2.0,
                    reduction="mean",
                ),
                loss_bbox=dict(
                    type="L1Loss",
                    loss_weight=0.25,
                ),
                pc_range=point_cloud_range,
            ),
            post_process=dict(
                type="BevFormerProcess",
                post_center_range=[-61.2, -61.2, -10.0, 61.2, 61.2, 10.0],
                pc_range=point_cloud_range,
                max_num=300,
                num_classes=10,
            ),
        ),
    ],
)

其中,model 下面的 type 表示定义的模型名称,剩余的变量表示模型的其他组成部分。这样定义模型的好处在于我们可以很方便的替换我们想要的结构。例如,如果我们想训练一个backbone为resnet50的模型,只需要将 model 下面的 backbone 替换掉就可以。

数据增强

model 的定义一样,数据增强的流程是通过在config配置文件中定义 data_loaderval_data_loader 这两个dict来实现的,分别对应着训练集和验证集的处理流程。以 data_loader 为例:


data_loader = dict(
    type=torch.utils.data.DataLoader,
    dataset=dict(
        type="NuscenesBevSequenceDataset",
        data_path=os.path.join(data_rootdir, "train_lmdb"),
        map_size=map_size,
        map_path=meta_rootdir,
        with_bev_bboxes=False,
        with_ego_bboxes=True,
        bev_range=point_cloud_range,
        num_seq=3,
        transforms=[
            dict(type="MultiViewsImgResize", size=(450, 800)),
            dict(
                type="MultiViewsImgTransformWrapper",
                transforms=[
                    dict(
                        type="TorchVisionAdapter",
                        interface="ColorJitter",
                        brightness=0.4,
                        contrast=0.4,
                        saturation=0.4,
                        hue=0.1,
                    ),
                    dict(type="PILToTensor"),
                    dict(type="Pad", divisor=32),
                    dict(type="BgrToYuv444", rgb_input=True),
                    dict(type="Normalize", mean=128.0, std=128.0),
                ],
            ),
        ],
    ),
    sampler=dict(type=torch.utils.data.DistributedSampler),
    batch_size=batch_size_per_gpu,
    shuffle=False,
    num_workers=2,
    pin_memory=True,
    collate_fn=collate_nuscenes_sequencev2,
)

val_data_loader = dict(
    type=torch.utils.data.DataLoader,
    dataset=dict(
        type="NuscenesBevSequenceDataset",
        data_path=os.path.join(data_rootdir, "val_lmdb"),
        map_size=map_size,
        map_path=meta_rootdir,
        with_bev_bboxes=False,
        with_ego_bboxes=True,
        bev_range=point_cloud_range,
        num_seq=1,
        transforms=[
            dict(type="MultiViewsImgResize", size=(450, 800)),
            dict(
                type="MultiViewsImgTransformWrapper",
                transforms=[
                    dict(type="PILToTensor"),
                    dict(type="Pad", divisor=32),
                    dict(type="BgrToYuv444", rgb_input=True),
                    dict(type="Normalize", mean=128.0, std=128.0),
                ],
            ),
        ],
    ),
    sampler=None,
    batch_size=1,
    shuffle=False,
    num_workers=2,
    pin_memory=True,
    collate_fn=collate_nuscenes_sequencev2,
)

其中type直接用的pytorch自带的接口torch.utils.data.DataLoader,表示的是将 batch_size 大小的图片组合到一起。 这里面唯一需要关注的可能是 dataset 这个变量, CocoFromLMDB 表示从lmdb数据集中读取图片,路径也就是我们在第一部分数据集准备中提到的路径。transforms 下面包含着一系列的数据增强。 val_data_loader 中除了图片翻转(RandomFlip), 其他的数据变换和 data_loader 一致。你也可以通过在 transforms 中插入新的dict实现自己希望的数据增强操作。

训练策略

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


float_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=(
                    "./tmp_pretrained_models/resnet50_imagenet/float-checkpoint-best.pth.tar"  # noqa: E501
                ),
                allow_miss=True,
                ignore_extra=True,
            ),
        ],
    ),
    data_loader=data_loader,
    optimizer=dict(
        type=torch.optim.AdamW,
        params={
            "backbone": dict(lr_mult=0.1),
        },
        lr=4e-4,
        weight_decay=0.01,
    ),
    batch_processor=batch_processor,
    device=None,
    num_epochs=24,
    callbacks=[
        stat_callback,
        loss_show_update,
        grad_callback,
        dict(
            type="CosineAnnealingLrUpdater",
            warmup_len=500,
            warmup_by="step",
            warmup_lr_ratio=1.0 / 3,
            step_log_interval=500,
            stop_lr=2e-4 * 1e-3,
        ),
        val_callback,
        ckpt_callback,
    ],
    sync_bn=True,
    train_metrics=dict(
        type="LossShow",
    ),
    val_metrics=[
        dict(
            type="NuscenesMetric",
            data_root=meta_rootdir,
            version="v1.0-trainval",
            use_lidar=False,
            classes=CLASSES,
            save_prefix="./WORKSPACE/results" + task_name,
            use_ddp=False,
        ),
    ],
)

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

注解

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

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

量化模型训练

当我们有了纯浮点模型之后,就可以开始训练相应的定点模型了。和浮点训练的方式一样,我们只需要通过运行下面的脚本就可以得到伪量化模型了:

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

可以看到,我们的配置文件没有改变,只改变了 stage 的类型。 Bevformer 需要经过 校准(calibration) 和 量化感知训练 (QAT) 两步才能达到量化目标。

模型校准 (Calibration)

我们首先对模型进行量化校准(calibration) ,这一步不改变模型的权重,主要一些真实样本输入统计模型各层输入输出范围,得到模型各层的量化scale。 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=os.path.join(
                    ckpt_dir, "float-checkpoint-best.pth.tar"
                ),
                ignore_extra=True,
                verbose=True,
                allow_miss=True,
            ),
            dict(
                type="RepModel2Deploy",
            ),
            dict(
                type="Float2Calibration",
                convert_mode=convert_mode,
                example_data_loader=calibration_example_data_loader,
                qconfig_setter=cali_qconfig_setter,
            ),
            reset_prev_frame_info,
            dict(
                type="FixWeightQScale",
            ),
            dict(type="DisableSyncConverter"),
        ],
    ),
    data_loader=calibration_data_loader,
    batch_processor=calibration_batch_processor,
    num_steps=calibration_step,
    device=None,
    callbacks=[
        stat_callback,
        calibration_val_callback,
        calibration_ckpt_callback,
    ],
    val_metrics=[
        dict(
            type="NuscenesMetric",
            data_root=meta_rootdir,
            version="v1.0-trainval",
            use_lidar=False,
            classes=CLASSES,
            save_prefix="./WORKSPACE/results" + task_name,
            use_ddp=False,
        ),
    ],
    log_interval=calibration_step / 10,
)

当我们训练量化模型的时候,需要设置model_convert_pipeline,它首先载入模型浮点权重,然后用 Float2Calibration 将模型从浮点模型,转化成校准模型。 其中 calibration_qconfig_setter 定义了模型的量化配置,关于它的基本配置方法,您可以阅读 构建QConfig 章节的内容。

bevformercalibration_qconfig_setter 是这样定义的:

def set_fuse_mode(self):
    from horizon_plugin_pytorch.quantization.fx.fusion_patterns import FuseMode

    return FuseMode.BNAddReLU


QconfigSetter.get_fuse_mode = set_fuse_mode

int16_qconfig = dict()
int16_qconfig["view_transformer.quant_hybird_ref_2d"] = {"output": qint16}
int16_qconfig["view_transformer.quant_norm_coords"] = {"output": qint16}
int16_qconfig["view_transformer.quant_queries_rebatch_grid"] = {
    "output": qint16
}
int16_qconfig["view_transformer.quant_restore_bev_grid"] = {"output": qint16}
int16_qconfig["view_transformer.quant_reference_points_rebatch"] = {
    "output": qint16
}
int16_qconfig["bev_decoders.0.reference_points"] = {"output": qint16}
int16_qconfig["bev_decoders.0.sigmoid"] = {"output": qint16}
int16_qconfig["bev_decoders.0.decoder.reference_points_add1"] = {
    "output": qint16
}
int16_qconfig["bev_decoders.0.decoder.reference_points_add2"] = {
    "output": qint16
}
int16_qconfig["bev_decoders.0.decoder.reference_points_cat"] = {
    "output": qint16
}
int16_qconfig["bev_decoders.0.decoder.new_reference_points_sigmoids"] = {
    "output": qint16
}

for layer in range(3):
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.sa.add_res"] = {
        "output": qint16
    }
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.sa.output_proj"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.sa.sampling_offsets"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.sa.quant_shape"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.sa.norm_offset"
    ] = {"output": qint16}
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.sa.add_offset"] = {
        "output": qint16
    }
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.sa.add1"] = {
        "output": qint16
    }
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.sa.mul1"] = {
        "output": qint16
    }
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.ca.add_res"] = {
        "output": qint16
    }
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.output_proj"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.sampling_offsets"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.quant_shape"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.norm_offset"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.add_offset"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.add1"
    ] = {"output": qint16}
    int16_qconfig[
        f"view_transformer.encoder.layers.{layer}.ca.deformable_attention.mul1"
    ] = {"output": qint16}
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.ffn.add"] = {
        "output": qint16
    }
    int16_qconfig[f"view_transformer.encoder.layers.{layer}.ffn.ffn2"] = {
        "output": qint16
    }

for layer in range(6):
    int16_qconfig[f"bev_decoders.0.cls_branches.{layer}.0"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.cls_branches.{layer}.3"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.reg_branches.{layer}.4"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.add_self_attn"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.add_res"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.output_proj"] = {
        "output": qint16
    }
    int16_qconfig[
        f"bev_decoders.0.decoder.layers.{layer}.ca.sampling_offsets"
    ] = {"output": qint16}
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.quant_shape"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.norm_offset"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.add_offset"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.add1"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ca.mul1"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ffn.add"] = {
        "output": qint16
    }
    int16_qconfig[f"bev_decoders.0.decoder.layers.{layer}.ffn.ffn2"] = {
        "output": qint16
    }

int16_qconfig.pop("bev_decoders.0.reg_branches.5.4")

None_qconfig_template = dict()
None_qconfig_template["view_transformer.positional_encoding"] = None
None_qconfig_template["view_transformer.bev_embedding"] = None
None_qconfig_template["bev_decoders.0.query_embedding"] = None

for layer in range(6):
    None_qconfig_template[f"bev_decoders.0.cls_branches.{layer}.2"] = {
        "activation": None
    }
    None_qconfig_template[f"bev_decoders.0.cls_branches.{layer}.5"] = {
        "activation": None
    }

q_templates = [  # 2. 模板,仅关注 dtype,按次序生效,因此模板的顺序很重要
    # 2.1. 基础配置部分
    ModuleNameTemplate({"": qint8}),  # 全局 feat int8
    ModuleNameTemplate(
        int16_qconfig | None_qconfig_template,
        freeze=True,
    ),
    MatmulDtypeTemplate(  # gemm single int8 input
        input_dtypes=[qint8, qint8],
        # prefix=["head", xxx],  # 名字和 torch.nn.Module.named_module() 返回的一致
    ),
    ConvDtypeTemplate(  # gemm int8 input
        input_dtype=qint8,
        weight_dtype=qint8,  # 成块的 weight 在这里配置
        # prefix=["head", xxx],  # 名字和 torch.nn.Module.named_module() 返回的一致
    ),
]

mix_qconifg = dict()
for k, _ in int16_qconfig.items():
    mix_qconifg[k] = QConfig(
        activation=FakeQuantize.with_args(
            observer=observer_v2.MixObserver,
            dtype=qint16,
        ),
        weight=FakeQuantize.with_args(
            observer=observer_v2.MinMaxObserver,
            dtype=qint8,
            qscheme=torch.per_channel_symmetric,
            ch_axis=0,
        ),
    )
cali_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(observer=(observer_v2.MSEObserver)),
    templates=q_templates,
    enable_optimize=True,
    save_dir=ckpt_dir,
    custom_qconfig_mapping=mix_qconifg,
)

这里可以改成(参考):对于上述代码示例,我们会分步骤为您逐一拆解说明。

  1. QconfigSetter.get_fuse_mode 设置了默认的BN fuse方式为 BNAddReLU 这会把 BN, AddReLU 算子都融合在一起。

  2. 代码构建了两个关键字典:int16_qconfigview_transformerbev_decoders 中对数值敏感的层指定为 qint16 输出; None_qconfig_template 则将位置编码等层设为 None,强制保持浮点计算。

  3. q_templates 定义了配置的生效优先级。首先设置全局默认为 qint8 。然后应用 Int16 和 None 配置,并设置 freeze=True。这相当于将这些关键层的配置锁定,防止被后续规则覆盖。 最后通过通用模板 MatmulDtypeTemplaeConvDtypeTemplate 处理剩余的 ConvMatmul 算子。

  4. 在最终初始化 Setter 前,代码还通过 mix_qconfig 为所有 Int16 层单独指定了 MixObserver ,实现了“权重 Int8 + 激活 Int16”的混合精度策略。

量化感知训练 (QAT)

qat_trainer 的定义为:

qat_qconfig_setter = QconfigSetter(
    reference_qconfig=get_qconfig(observer=(observer_v2.MinMaxObserver)),
    templates=q_templates,
    enable_optimize=True,
    save_dir=ckpt_dir,
    custom_qconfig_mapping=None,
)

qat_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        converters=[
            dict(
                type="RepModel2Deploy",
            ),
            dict(
                type="Float2QAT",
                convert_mode=convert_mode,
                example_data_loader=calibration_example_data_loader,
                qconfig_setter=qat_qconfig_setter,
            ),
            reset_prev_frame_info,
            dict(
                type="FixWeightQScale",
            ),
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    ckpt_dir, "calibration-checkpoint-best.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
        ],
    ),
    data_loader=data_loader,
    optimizer=dict(
        type=torch.optim.AdamW,
        lr=2e-5,
        weight_decay=0.01,
    ),
    batch_processor=batch_processor,
    device=None,
    num_epochs=10,
    callbacks=[
        stat_callback,
        loss_show_update,
        grad_callback,
        dict(
            type="StepDecayLrUpdater",
            lr_decay_id=[5],
            step_log_interval=500,
        ),
        qat_val_callback,
        qat_ckpt_callback,
    ],
    sync_bn=True,
    train_metrics=dict(
        type="LossShow",
    ),
    val_metrics=[
        dict(
            type="NuscenesMetric",
            data_root=meta_rootdir,
            version="v1.0-trainval",
            use_lidar=False,
            classes=CLASSES,
            save_prefix="./WORKSPACE/results" + task_name,
            use_ddp=False,
        ),
    ],
)

量化训练其实是在纯浮点训练基础上的finetue,因此量化训练的时候,我们的初始学习率设置为浮点训练的二十分之一,训练的epoch次数也大大减少。

注意到,qat_qconfig_settercalibration_qconfig_setter 是类似的, 但是observer改成了 MinMaxObserver , 它的速度会比 MSEObserver 快很多,我们推荐qat使用 MinMaxObserver

model 定义的时候,我们先把模型转换成qat模型, 然后模型加载上一步保存的校准模型,开始训练。

若您想了解更多量化训练相关内容,比如准备浮点模型、算子替换、插入量化和反量化节点、设置量化参数以及算子的融合等,请阅读 量化感知训练(QAT) 章节的内容。