写点什么

地平线 3D 目标检测 Bevformer 参考算法 V2.0

  • 2025-02-07
    广东
  • 本文字数:7654 字

    阅读完需:约 25 分钟

地平线 3D 目标检测 Bevformer 参考算法 V2.0

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

简介

BEVFormer 是当前热门的自动驾驶系统中的 3D 视觉感知任务模型。BEVFormer 是一个端到端的框架,BEVFormer 可以直接从原始图像数据生成 BEV 特征,无需依赖于传统的图像处理流程。它通过利用 Transformer 架构和注意力机制,有效地从多摄像头图像中学习生成高质量的鸟瞰图(Bird's-Eye-View, BEV)特征表示。相较于其他的 BEV 转换方式:


  1. 时空注意力机制:模型结合了空间交叉注意力(Spatial Cross-Attention, SCA)和时间自注意力(Temporal Self-Attention, TSA),使网络能够同时考虑空间和时间维度上的信息。融合历史 bev 特征来提升预设的 BEV 空间中的 query 的自学能力,得到 bev 特征。

  2. Deformable attn:通过对每个目标生成几个采样点和采样点的 offset 来提取采样点周围的重要特征,即只关注和目标相关的特征,减少计算量。

  3. transformer 架构:能够有效捕捉序列中的长期依赖关系,适用于处理图像序列。

性能精度指标

模型参数:



性能精度表现:


模型介绍



·公版 BEVFormer 模型主要可以分为以下几个关键部分:


  1. Backbone 网络:用于从多视角摄像头图像中提取特征,本文为 tiny 版本,因此为 ResNet50。

  2. 时空特征提取:BEVFormer 通过引入时间和空间特征来学习 BEV 特征。具体来说,模型包括:

  3. Temporal Self-Attention(时间自注意力):利用前一时刻的 BEV 特征作为历史特征,通过自注意力机制来计算当前时刻的 BEV 特征。

  4. Spatial Cross-Attention(空间交叉注意力):进行空间特征注意力,融合多视角图像特征。

  5. Deformable Attention(可变形注意力):BEVFormer 使用可变形注意力机制来加速运算,提高模型对不同视角图像特征的适应性。

  6. BEV 特征生成:通过时空特征的融合,完成环视图像特征向 BEV 特征的建模。

  7. Decoder:设计用于 3D 物体检测的端到端网络结构,基于 2D 检测器 Deformable DETR 进行改进,以适应 3D 空间的检测任务。

地平线部署说明

公版 bevformer 在 征程 6 上部署相比于 征程 5 来说更简单了,需要考虑的因素更少。征程 6 对非 4 维的支持可以和 4 维的同等效率,因此 征程 6 支持公版的注意力实现,不再限制维度,因此无需对维度做 Reshape,可直接支持公版写法。但需注意的是公版的 bev_mask 会导致动态 shape。征程 6 不支持动态输入,因此 bev_mask 无法使用。在精度上,我们修复了公版的 bug 已获得了精度上的提升,同时通过对关键层做 int16 的量化精度配置以保障 1%以内的量化精度损失。


下面将部署优化对应的改动点以及量化配置依次说明。

性能优化

改动点 1:


将 attention 层的 mean 替换为 conv 计算,使性能上获得提升。


/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/bevformer/attention.py


self.query_reduce_mean = nn.Conv2d(    self.num_bev_queue * self.reduce_align_num,    self.reduce_align_num,    1,    bias=False,)
# init query_reduce_mean weightquery_reduce_mean_weight = torch.zeros( self.query_reduce_mean.weight.size(), dtype=self.query_reduce_mean.weight.dtype,)for i in range(self.reduce_align_num): for j in range(self.num_bev_queue): query_reduce_mean_weight[i, j * self.reduce_align_num + i] = ( 1 / self.num_bev_queue )self.query_reduce_mean.weight = torch.nn.Parameter( query_reduce_mean_weight, requires_grad=False)
复制代码


改动点 2:


公版中,在 Encoder 的空间融合模块,会根据 bev_mask 计算有效的 query 和 reference_points,输出 queries_rebatch 和 reference_points_rebatch,作用为减少交互的数据量,提升模型运行性能。对于稀疏的 query 做 crossattn 后再将 query 放回到 bev_feature 中。


以上提取稀疏 query 步骤的主要算子为 gather,放回 bev_feature 步骤的主要算子为 scatter。由于工具链对这两个算子暂未支持(gather 算子 930 已支持)而且 bev_mask 为动态的,为了提升模型的运行性能,工具链提供了 gridsample 算子的替换方式,index 计算只与内外参有关,因此作为前处理,将计算好的 index 作为模型输入即可。


gather


gather 为根据 bevmask 来提取稀疏 query,降低 cross attn 的数据量,提升运行效率。


代码路径:<code>/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/bevformer/<span style="caret-color: #000000; color: #000000; font-family: monospace; font-size: medium; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #e8e8e8; text-decoration: none; display: inline !important; float: none;">view_transformer.py</span>


        reference_points_cam = torch.clamp(            reference_points_cam, min=-2.1, max=2.1        )        reference_points_cam = reference_points_cam.permute(2, 1, 3, 0, 4)        bev_mask = bev_mask.permute(2, 1, 3, 0, 4).squeeze(-1)        bev_mask_ori = bev_mask.clone()        max_len = self.virtual_bev_h * self.virtual_bev_w        queries_rebatch_grid = reference_points_cam.new_zeros(            [B * self.numcam, self.virtual_bev_h, self.virtual_bev_w, 2]        )        for camera_idx, mask_per_img_bs in enumerate(bev_mask):            for bs_id, mask_per_img in enumerate(mask_per_img_bs):                temp_grid = (                    torch.zeros(                        (max_len, 2),                        device=queries_rebatch_grid.device,                        dtype=torch.float32,                    )                    - 1.5                )                index_query_per_img = (                    mask_per_img.sum(-1).nonzero().squeeze(-1)                )                num_bev_points = index_query_per_img.shape[0]                camera_idx_tensor_x = index_query_per_img % self.bev_w                camera_idx_tensor_y = index_query_per_img // self.bev_w                index_grid = torch.stack(                    [                        camera_idx_tensor_x / (self.bev_w - 1),                        camera_idx_tensor_y / (self.bev_h - 1),                    ],                    dim=-1,                )                index_grid = index_grid * 2 - 1                temp_grid[:num_bev_points] = index_grid                temp_grid = temp_grid.reshape(                    self.virtual_bev_h, self.virtual_bev_w, 2                )                queries_rebatch_grid[                    bs_id * self.numcam + camera_idx                ] = temp_grid        reference_points_rebatch = (            reference_points_cam.flatten(-2)            .permute(1, 0, 3, 2)            .flatten(0, 1)            .reshape(B * self.numcam, D * 2, self.bev_h, self.bev_w)        )        reference_points_rebatch = (            F.grid_sample(                reference_points_rebatch,                queries_rebatch_grid,                mode="nearest",                align_corners=True,            )            .flatten(-2)            .permute(0, 2, 1)            .reshape(B * self.numcam, max_len, D, 2)        )
复制代码


query_rebatch


        queries_rebatch = (            query.unsqueeze(1)            .repeat(1, self.num_cams, 1, 1)            .reshape(                bs * self.num_cams, self.bev_h, self.bev_w, self.embed_dims            )            .permute(0, 3, 1, 2)        )        queries_rebatch = F.grid_sample(            queries_rebatch,            queries_rebatch_grid,            mode="nearest",            align_corners=True,        )        queries_rebatch = queries_rebatch.flatten(-2).permute(0, 2, 1)        reference_points_rebatch = reference_points_rebatch.flatten(            -2        ).unsqueeze(-2)
复制代码


scatter


scatter 操作对经过 deformable_attention 后的 query 放入到 bevfeature 中,然后求平均。


代码路径为:/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/bevformer/attention.py


        slots = self.restore_outputs(            restore_bev_grid,            queries_out,            bev_pillar_counts,            bs,            queries_rebatch_grid,        )    def restore_outputs(        self,        restore_bev_grid: Tensor,        queries_out: Tensor,        counts: Tensor,        bs: int,        queries_rebatch_grid: Tensor,    ):        """Restore outputs to bev feature."""        queries_out = queries_out.reshape(            bs, self.num_cams, self.embed_dims, -1        )        queries_out = queries_out.permute(0, 2, 1, 3)        queries_out = queries_out.reshape(            bs,            self.embed_dims,            self.num_cams * queries_rebatch_grid.shape[1],            queries_rebatch_grid.shape[2],        )        bev_queries = F.grid_sample(            queries_out, restore_bev_grid, mode="nearest", align_corners=True        )        bev_queries = bev_queries.reshape(bs, -1, self.bev_h, self.bev_w)        slots = self.query_reduce_sum(bev_queries).flatten(-2).permute(0, 2, 1)        slots = self.mul_pillarweight.mul(slots, counts)        return slots
复制代码


其中 restore_bev_grid,根据 bevmask 反算回 bev_feature 的位置:


        restore_bev_grid = (            reference_points_cam.new_zeros(                B, self.max_camoverlap_num * self.bev_h, self.bev_w, 2            )            - 1.5        )        for bs_id, bev_mask_ in enumerate(bev_mask):            bev_pillar_num_map = torch.zeros(                (self.bev_h, self.bev_w), device=bev_mask_.device            )            count = bev_mask_.sum(-1) > 0            camera_idxs, bev_pillar_idxs = torch.where(count)            camera_idx_offset = 0            for cam_id in range(self.numcam):                camera_idx = torch.where(camera_idxs == cam_id)                bev_pillar_idx_cam = bev_pillar_idxs[camera_idx[0]]                num_camera_idx = len(camera_idx[0])                camera_idx_tmp = camera_idx[0] - camera_idx_offset                camare_tmp_idx_x = camera_idx_tmp % self.virtual_bev_w                camare_tmp_idx_y = camera_idx_tmp // self.virtual_bev_w                grid_x = camare_tmp_idx_x                grid_y = cam_id * self.virtual_bev_h + camare_tmp_idx_y                bev_pillar_idx_cam_x = bev_pillar_idx_cam % self.bev_w                bev_pillar_idx_cam_y = bev_pillar_idx_cam // self.bev_w                bev_pillar_num_map_tmp = bev_pillar_num_map[                    bev_pillar_idx_cam_y, bev_pillar_idx_cam_x                ]                grid_h = (                    bev_pillar_num_map_tmp * self.bev_h + bev_pillar_idx_cam_y                ).to(torch.int64)                grid_w = (bev_pillar_idx_cam_x).to(torch.int64)                restore_bev_grid[bs_id, grid_h, grid_w, 0] = grid_x / (                    self.virtual_bev_w - 1                )                restore_bev_grid[bs_id, grid_h, grid_w, 1] = grid_y / (                    self.numcam * self.virtual_bev_h - 1                )                bev_pillar_num_map[                    bev_pillar_idx_cam_y, bev_pillar_idx_cam_x                ] = (                    bev_pillar_num_map[                        bev_pillar_idx_cam_y, bev_pillar_idx_cam_x                    ]                    + 1                )                camera_idx_offset = camera_idx_offset + num_camera_idx        restore_bev_grid = restore_bev_grid * 2 - 1
复制代码

精度优化

浮点精度

改动点 3:


公版通过 can_bus 初始化 ref 来做时序融合,然而这个时候 bev feat 并没有对齐,在 attention 计算时不能简单的 concat 起来。因此我们换了一种时序对齐的方式,通过前后两帧的 ego2global 坐标系转换矩阵将当前帧的 bev 特征和上一帧对齐,此时 ref 都是一样的。(非 征程 6 不支持,为公版 bug),精度上获得提升。


/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/bevformer/view_transformer.py` `get_prev_bev` `get_fusion_refpre_scene = prev_meta["scene_token"]for i in range(bs):    if pre_scene[i] != cur_meta["meta"][i]["scene"]:        prev_bev[i] = torch.zeros(            (self.bev_h * self.bev_w, self.embed_dims),            dtype=torch.float32,            device=device,        )##公版:shift_ref_2d = ref_2d.clone()shift_ref_2d += shift[:, None, None, :]bs, len_bev, num_bev_level, _ = ref_2d.shapehybird_ref_2d = torch.stack([shift_ref_2d, ref_2d], 1).reshape(    bs*2, len_bev, num_bev_level, 2)##地平线版本shift_ref_2d = ref_2d.clone()bs, len_bev, num_bev_level, _ = ref_2d.shapehybird_ref_2d = torch.stack([shift_ref_2d, ref_2d], 1).reshape(    bs * 2, len_bev, num_bev_level, 2)
复制代码


改动点 4:


修复了个 tsa 公版的 batchsize 不等于 1 的 bug。BEVFormer/projects/mmdet3d_plugin/bevformer/modules/temporal_self_attention.py at master · fundament

量化精度

为量化精度保证,我们将以下的算子配置为 int16 或 int32 输出:


view_transformer:输入节点做 int16 量化:


        int16_models = [            self.quant_hybird_ref_2d,            self.quant_norm_coords,            self.quant_restore_bev_grid,            self.quant_reference_points_rebatch,            self.quant_queries_rebatch_grid,        ]        for m in int16_models:            m.qconfig = qconfig_manager.get_qconfig(                activation_qat_qkwargs={"dtype": qint16},                activation_calibration_qkwargs={                    "dtype": qint16,                },                activation_calibration_observer="mix",            )
复制代码


attention 层:最后两个 conv 和 add 开启 int16


    def set_qconfig(self) -> None:        """Set the quantization configuration."""        from hat.utils import qconfig_manager        int16_module = [            self.output_proj,            self.add_res,        ]
复制代码


decoder 层:cls_branches、reg_branches 的 conv 配置为 int32 输出;sigmoid 和 reference_points 配置为 int16


    def set_qconfig(self) -> None:        """Set the quantization configuration."""        from hat.utils import qconfig_manager        for _, m in enumerate(self.cls_branches):            m[0].qconfig = qconfig_manager.get_qconfig(                activation_qat_qkwargs={"dtype": qint16},                activation_calibration_qkwargs={                    "dtype": qint16,                },                activation_calibration_observer="mix",            )            m[3].qconfig = qconfig_manager.get_qconfig(                activation_qat_qkwargs={"dtype": qint16},                activation_calibration_qkwargs={                    "dtype": qint16,                },                activation_calibration_observer="mix",            )            m[-1].qconfig = qconfig_manager.get_default_qat_out_qconfig()        self.reg_branches[-1][            -1        ].qconfig = qconfig_manager.get_default_qat_out_qconfig()        self.query_embedding.qconfig = None        int16_module = [            self.reference_points,            self.sigmoid,        ]        for m in int16_module:            m.qconfig = qconfig_manager.get_qconfig(                activation_qat_qkwargs={"dtype": qint16},                activation_calibration_qkwargs={                    "dtype": qint16,                },                activation_calibration_observer="mix",            )
复制代码

总结与建议

训练建议

  • 浮点和公版一致即可

  • qat 训练需要将 lr 降低,下降策略建议使用 StepDecayLrUpdater。

部署建议

  • 建议 bev size 的选择考虑性能影响。征程 6 相比于 征程 5 带宽增大,但仍需注意 bevsize 过大导致访存时间过长对性能的影响,建议考虑实际部署情况选择合适的 bevsize 做性能验证。

  • 使用 bevmask 来提升运行性能,可参考 4.1 章节使用 gridsample 替换不支持的 scatter。

  • 在注意力机制中存在一些 ElementWise 操作,对于导致性能瓶颈的可以考虑 conv 替换,对于造成量化风险的可以根据敏感度分析结果合理选择更高的量化精度,以确保注意力机制的部署。


本文通过对 Bevformer 在地平线征程 6 上量化部署的优化,使得模型在该计算方案上用低于 1%的量化精度损失,得到 latency 为 45.74ms 的部署性能,同时,通过 Bevformer 的部署经验,可以推广到其他模型部署优化,例如包含 MSDA 模型结构、transformer-based BEV 的部署。

附录

  1. 论文:https://arxiv.org/pdf/2203.17270

  2. 公版代码:https://github.com/fundamentalvision/BEVFormer


用户头像

还未添加个人签名 2021-03-11 加入

还未添加个人简介

评论

发布
暂无评论
地平线 3D 目标检测 Bevformer 参考算法 V2.0_自动驾驶;_地平线开发者_InfoQ写作社区