Lidar Fusion Sparse-bev Perception Model

This tutorial mainly guides readers on how to use HAT to train a Lidar fusion perception model on the autonomous driving dataset Nuscenes, covering floating-point, quantized, and fixed-point models. Below, the configuration and training of a Lidar fusion sparse-bev perception model are introduced using configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py as an example.

bev_sparse_lidar_fusion_henet_tinym_nuscenes is a multi-modal autonomous driving perception model that takes camera and lidar inputs and outputs 3D detection boxes for dynamic elements.

Training Process

If you simply want to train the bev_sparse_lidar_fusion_henet_tinym_nuscenes model, start by reading this section. Like other tasks, all training and evaluation tasks in HAT are completed using a tools + config format. Once the original dataset is prepared, you can conveniently complete the entire training process by following the steps below.

Dataset Preparation

Take the nuscenes dataset as an example, which can be downloaded from https://www.nuscenes.org/nuscenes. For the Occupancy prediction task, you also need to download the OCC GT, which can be downloaded from https://github.com/CVPR2023-3D-Occupancy-Prediction/CVPR2023-3D-Occupancy-Prediction. It is recommended to decompress the downloaded dataset into the occ3d/gts folder within the nuscenes dataset folder. Then you can run the following commands to package lidar, images, occ gt, and other data into lmdb format:

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

The above two commands correspond to the conversion of training and validation datasets, respectively. After packing is complete, the file structure under the data directory should be as follows:

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

train_lmdb and val_lmdb are the packed training and validation datasets, which are also the datasets that the network ultimately reads.

Model Training

After the dataset is ready, you can start training the floating-point Lidar fusion multi-task perception model bev_sparse_lidar_fusion_henet_tinym_nuscenes .

First, you can use the following command to evaluate the model's computational load:

python3 tools/calops.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

If you want to reproduce the entire model floating-point and fixed-point training process, you need to:

python3 tools/train.py --stage "float" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage "calibration" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage "qat" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

The above commands complete the pre-training of the camera input floating-point model, floating-point model training, and fixed-point model training, respectively. Fixed-point model training requires a well-trained floating-point model as a basis. For specific content, please read the Quantization-Aware Training section.

Exporting Fixed-Point Models

After completing the quantization training, you can start exporting fixed-point models. You can export them through the following command:

python3 tools/export_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

Model Verification

After the training is completed, you can obtain the trained floating-point, quantized, or fixed-point models. Similar to the training method, we can use the same method to verify the trained models to obtain metrics for float , calibration and qat , which are the metrics for floating-point and quantized models, respectively.

python3 tools/predict.py --stage "float" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/predict.py --stage "calibration" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/predict.py --stage "qat" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

Similar to training models, when the --stage parameter is set to "float" , calibration or "qat" , it can complete the verification of the trained floating-point models and quantized models, respectively.

Fixed-point model accuracy verification can also be done using the following command, but it is important to note that hbir must be exported first:

python3 tools/predict.py --stage "int_infer" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

Model Inference

HAT provides the infer_hbir.py script to visualize the inference results of fixed-point models. The following command will save data from the dataset in the ./demo folder and visualize it in the ${save_path} folder.

python3 tools/infer_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py --save-path ${save_path} --use-dataset

Simulation Board Accuracy Verification

In addition to the above model verification, we also provide a accuracy verification method that is completely consistent with the board. It can be completed in the following way:

python3 tools/validation_hbir.py --stage "align_bpu" --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

Fixed-Point Model Checking and Compilation

The quantization training toolchain integrated in HAT is mainly prepared for the Horizon computing platform, so it is necessary to check and compile the quantized models. We provide a model checking interface in HAT, which can be used to check whether the defined quantization model can run normally on the BPU before training:

python3 tools/model_checker.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

After the model training is completed, the compile_perf_hbir script can be used to compile the quantized model into an hbm file that can be run on the board, and this tool can also estimate the running performance on the BPU:

python3 tools/compile_perf_hbir.py --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

The above is the entire process from data preparation to generating a quantizable deployable model.

Training Details

In this section, we explain some of the precautions to be taken when training the model, mainly related to some settings in the config.

Model Construction

The model definition related config is as follows:

lidar_network = dict(
    type="CenterPointDetector",
    feature_map_shape=get_feature_map_size(point_cloud_range, voxel_size),
    pre_process=dict(
        type="CenterPointPreProcess",
        pc_range=point_cloud_range,
        voxel_size=voxel_size,
        max_voxels_num=max_voxels,
        max_points_in_voxel=max_num_points,
        norm_range=[-51.2, -51.2, -5.0, 0.0, 51.2, 51.2, 3.0, 255.0],
        norm_dims=[0, 1, 2, 3],
    ),
    reader=dict(
        type="PillarFeatureNet",
        num_input_features=5,
        num_filters=(64,),
        with_distance=False,
        pool_size=(max_num_points, 1),
        voxel_size=voxel_size,
        pc_range=point_cloud_range,
        bn_kwargs={},
        quantize=True,
        use_4dim=True,
        use_conv=True,
        hw_reverse=True,
    ),
    scatter=dict(
        type="PointPillarScatter",
        num_input_features=64,
        use_horizon_pillar_scatter=True,
        quantize=True,
    ),
    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",
    ),
    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],
    ),
)

model = dict(
    type="SparseBevFusionOE",
    compiler_model=False,
    lidar_net=lidar_network,
    lidar_level_idx=[1, 2],
    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=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=[
            "lidar_deformable",
            "ffn",
            "norm",
            "refine",
        ]
        * num_single_frame_decoder
        + [
            "temp_interaction",
            "interaction",
            "norm",
            "lidar_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="DeformableFeatureAggregationLiFOE",
            fuse_module=dict(
                type="InstanceFuseModule",
                input_channels=[256, 256],
                fuse_channel=256,
            ),
            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",
            point_cloud_range=point_cloud_range,
            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,
    ),
)

Among them, lidar_network defines the encoder for processing lidar input, and model provides the complete model definition. The type under model indicates the name of the defined model, and the remaining variables represent other parts of the model. The advantage of defining the model in this way is that we can easily replace the structures we want.

Data Augmentation

Like the definition of model, the data augmentation process is implemented by defining data_loader and val_data_loader in the config configuration file, corresponding to the processing of the training set and validation set, respectively. For example, the definition of the training set is:

train_dataset = dict(
    type="NuscenesBevDataset",
    data_path=os.path.join(data_rootdir, "train_lmdb"),
    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),
            ],
        ),
    ],
    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],
)

Training Strategy

To train a high accuracy model, a good training strategy is essential. For each training task, the corresponding training strategy is also defined in the config file, which can be seen from the float_trainer variable.

The configuration of float_trainer for the lidar fusion sparse bev model is as follows, we load the single-camera input sparse bev model as a pre-training.

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,
            ),
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    pretrain_ckpt_dir, "float-checkpoint-best.pth.tar"
                ),
                state_dict_update_func=partial(
                    update_state_dict_by_add_prefix, prefix="lidar_net."
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
                ignore_tensor_shape=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_nuscenes_metric],
)

float_trainer defines our training method from a broad perspective, including the use of multi-card distributed training (distributed_data_parallel_trainer), the number of epochs for model training, and the choice of optimizer. At the same time, callbacks reflects the small strategies and operations you want to implement during model training, including the way the learning rate changes (CosineAnnealingLrUpdater), validating model metrics during training (Validation), and saving (Checkpoint) model operations. Of course, if you have operations you hope the model will implement during training, you can also add them in this dict manner.

Through the above introduction, you should have a clear understanding of the functions of the config file. Then, through the aforementioned training scripts, you can train a high accuracy pure floating-point detection model. Of course, training a good detection model is not our ultimate goal, it is just a pretrain for us to train fixed-point models later.

Quantization Model Training

When we have a pure floating-point model, we can start training the corresponding fixed-point model. Like the floating-point training method, we can get a pseudo-quantized model by running the following script:

python3 tools/train.py --stage calibration --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py
python3 tools/train.py --stage qat --config configs/lidar_bevfusion/bev_sparse_lidar_fusion_henet_tinym_nuscenes.py

It can be seen that our configuration file has not changed, only the type of stage has changed. The calibration process can provide better initial parameters for QAT quantization training.

We use the template to configure Float2Calibration and Float2QAT to convert the model into calibration and qat models, respectively. The specific calibration and QAT config are:

cali_qconfig_setter = (default_calibration_qconfig_setter,)
qat_qconfig_setter = (default_qat_fixed_act_qconfig_setter,)

calibration_trainer = dict(
    type="Calibrator",
    model=model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        qconfig_params=dict(
            activation_calibration_observer="mse",
        ),
        converters=[
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    ckpt_dir, "float-checkpoint-last.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
                verbose=True,
            ),
            dict(
                type="Float2Calibration",
                convert_mode=convert_mode,
                example_data_loader=calibration_data_loader,
                qconfig_setter=cali_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_nuscenes_metric],
)

float2qat = dict(
    type="Float2QAT",
    convert_mode=convert_mode,
    example_data_loader=calibration_data_loader,
    state="train",
    qconfig_setter=qat_qconfig_setter,
)


qat_model = copy.deepcopy(model)
qat_model["head"]["enable_dn"] = False
qat_trainer = dict(
    type="distributed_data_parallel_trainer",
    model=qat_model,
    model_convert_pipeline=dict(
        type="ModelConvertPipeline",
        qat_mode="fuse_bn",
        converters=[
            float2qat,
            dict(
                type="LoadCheckpoint",
                checkpoint_path=os.path.join(
                    ckpt_dir, "calibration-checkpoint-last.pth.tar"
                ),
                allow_miss=True,
                ignore_extra=True,
            ),
        ],
    ),
    data_loader=data_loader,
    optimizer=dict(
        type=torch.optim.AdamW,
        eps=1e-8,
        betas=(0.9, 0.999),
        params={
            "backbone": dict(lr=5e-6),
        },
        lr=5e-6,
        weight_decay=1e-3,
    ),
    batch_processor=batch_processor,
    num_steps=num_steps * 0.4,
    stop_by="step",
    device=None,
    callbacks=[
        stat_callback,
        loss_show_update,
        # bn_callback,
        grad_callback,
        dict(
            type="StepDecayLrUpdater",
            lr_decay_id=[int(num_steps * 0.4)],
            step_log_interval=500,
        ),
        val_callback,
        ckpt_callback,
    ],
    train_metrics=dict(
        type="LossShow",
    ),
    val_metrics=[val_nuscenes_metric],
)

Different Training Strategies

As we said before, quantization training is actually a fine-tune on the basis of pure floating-point training. Therefore, during quantization training, our initial learning rate is much smaller compared to float, and the number of training epochs is greatly reduced. Most importantly, when defining model, our pretrained needs to be set to the address of the well-trained pure floating-point model.

After making these simple adjustments, you can start training our quantization model.

Regarding the key steps in quantization training, such as preparing floating-point models, operator replacement, inserting quantization and dequantization nodes, setting quantization parameters, and operator fusion, please read the Quantization-Aware Training section.