地平線 3D 目標檢測 bev_sparse 參考演算法-V1.0

地平线智能驾驶开发者發表於2024-10-30

該示例為參考演算法,僅作為在 征程 6 上模型部署的設計參考,非量產演算法

01 簡介

在自動駕駛視覺感知系統中,為了獲得環繞車輛範圍的感知結果,通常需要融合多攝像頭的感知結果。目前更加主流的感知架構則是選擇在特徵層面進行多攝像頭融合。

其中比較有代表性的路線就是這兩年很火的 BEV 方法,繼 Tesla Open AI Day 公佈其 BEV 感知演算法之後,相關研究層出不窮,感知效果取得了顯著提升,BEV 也幾乎成為了多感測器特徵融合的代名詞。

但是,隨著大家對 BEV 研究和部署的深入,BEV 正規化也逐漸暴露出來了一些缺陷:

  • 感知範圍、感知精度、計算效率難平衡:從影像空間到 BEV 空間的轉換,是稠密特徵到稠密特徵的重新排列組合,計算量比較大,與影像尺寸以及 BEV 特徵圖尺寸成正相關。

    在大家常用的 nuScenes 資料中,感知範圍通常是長寬 [-50m, +50m] 的方形區域,然而在實際場景中,我們通常需要達到單向 100m,甚至 200m 的感知距離。

    若要保持 BEV Grid 的解析度不變,則需要大大增加 BEV 特徵圖的尺寸,從而使得端上計算負擔和頻寬負擔都過重;若保持 BEV 特徵圖的尺寸不變,則需要使用更粗的 BEV Grid,感知精度就會下降。

    因此,在車端有限的算力條件下,BEV 方案通常難以實現遠距離感知和高解析度特徵的平衡;

  • 無法直接完成影像域的 2D 感知任務:BEV 空間可以看作是壓縮了高度資訊的 3D 空間,這使得 BEV 正規化的方法難以直接完成 2D 相關的任務,如標誌牌和紅綠燈檢測等,感知系統中仍然要保留影像域的感知模型。

實際上,我們感興趣的目標(如動態目標和車道線)在空間中的分佈通常很稀疏,BEV 正規化中有大量的計算都被浪費了。因此,我們希望實現一個高效能高效率的長時序純稀疏融合感知演算法,一方面能加速 2D->3D 的轉換效率,另外一方面在影像空間直接捕獲目標跨攝像頭的關聯關係更加容易,因為在 2D->BEV 的環節不可避免存在大量資訊丟失。

地平線提出了 Sparse4D 及其進化版本 Sparse4D v2,從 Query 構建方式、特徵取樣方式、特徵融合方式、時序融合方式等多個方面提升了模型的效果。

02 效能精度指標

03 公版模型介紹

Sparse4D 採用了 Encoder-Decoder 結構。其中 Encoder 包括 image backbone 和 neck,用於對多視角影像進行特徵提取,得到多視角多尺度特徵圖。

同時會 cache 歷史幀的影像特徵,用於在 decoder 中提取時序特徵;Decoder 為多層級聯形式,輸入時序多尺度影像特徵圖和初始化 instance,輸出精細化後的 instance,每層 decoder 包含 self-attentiondeformable aggregationrefine module 三個主要部分。

學習 2D 檢測領域 DETR 改進的經驗,我們也重新引入了 Anchor 的使用,並將待感知的目標定義為 instance,每個 instance 主要由兩個部分構成:

  1. Instance feature :目標的高維特徵,在 decoder 中不斷由來自於影像特徵的取樣特徵所更新;

  2. 3D Anchor :目標結構化的狀態資訊,比如 3D 檢測中的目標 3D 框(x, y, z, w, l, h, yaw, vx, vy);公版透過 kmeans 演算法來對 anchor 的中心點分佈進行初始化;

    同時,在網路中會基於一個 MLP 網路來對 anchor 的結構化狀態進行高維空間對映得到 Anchor Embed 𝐸 ,並與 instance feature 相融合。

基於以上定義,透過初始化一系列 instance,經過每一層 decoder 都會對 instance 進行調整,包括 instance feature 的更新,和 anchor 的 refine。基於每個 instance 最終預測的 bounding box。

04 地平線部署說明

公版 sparse4d 在 征程 6 上部署的改動點為:

  • 一階段生成的 featuremap 只使用 FPN 的第二層(stride=16),非公版的使用 4 層。

  • DeformableFeatureAggregation 中 kps_generator 直接使用 offset(linear)生成,非公版的基於 anchor 生成。

  • RefinementModule 對速度沒有做 refine 計算,公版使用了。

下面將部署最佳化對應的改動點以及量化配置依次說明。

4.1 效能最佳化

改動點 1:

一階段生成的 featuremap 只使用 FPN 的 1 層(公版使用 4 層),透過實驗驗證 level_index=2(stride=16)時精度和效能平衡的更好。

neck=dict(
    type="FixFPN",
    in_strides=[2, 4, 8, 16, 32],
    in_channels=[256, 256, 512, 1024, 2048],
    fix_out_channel=256,
    out_strides=[4, 8, 16, 32],
),

head=dict(
        type="SparseBEVOEHead",
        enable_dn=True,
        level_index=[2],
        cls_threshold_to_reg=0.05,

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/head.py

if self.level_index is not None:
    feature_maps_head = []
    for index in self.level_index:
        if compiler_model is False:
            feature_maps_head.append(feature_maps[index].float())
        else:
            feature_maps_head.append(feature_maps[index])
    feature_maps = feature_maps_head
batch_size = feature_maps[0].shape[0] // self.num_views

改動點 2:

  • DeformableFeatureAggregation 中 kps_generator 直接使用 offset(linear)生成,非公版的基於 anchor 生成。(公版的方式會導致延遲增加)。

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/det3d_blocks.py

SparseBEVOEKeyPointsGenerator

def __init__(
        self,
        embed_dims=256,
        num_pts=13,
    ):
    self.offset = nn.Linear(self.embed_dims, self.num_pts * 3)
    self.keypoints_add = FloatFunctional()
def forward(
    self,
    anchor,
    instance_feature,
):
    bs, num_anchor = anchor.shape[:2]
    key_points = self.offset(instance_feature).view(
        bs, num_anchor, self.num_pts, 3
    )
    key_points = self.keypoints_add.add(key_points, anchor[..., None, 0:3])
    return key_points

改動點 3:

RefinementModule 的 velocity 沒有 refine。經過實驗公版的 vx 的 refine 對精度影響較小,基於效能考慮去除該操作。

def forward(
    self,
    instance_feature,
    anchor,
    anchor_embed,
    return_cls=True,
):

    feature = self.add1.add(instance_feature, anchor_embed)
    output = self.layers(feature)

    output = self.add2.add(output, anchor)
    #公版實現:
    # if self.output_dim > 8:
    #     if not isinstance(time_interval, torch.Tensor):
    #         time_interval = instance_feature.new_tensor(time_interval)
    #     translation = torch.transpose(output[..., VX:], 0, -1)
    #     velocity = torch.transpose(translation / time_interval, 0, -1)
    #     output[..., VX:] = velocity + anchor[..., VX:]
    if return_cls:
        assert self.with_cls_branch, "Without classification layers !!!"
        cls = self.cls_layers(instance_feature)
    else:
        cls = None
    if return_cls and self.with_quality_estimation:
        quality = self.quality_layers(feature)
    else:
        quality = None
        return output, cls, quality

4.2 精度最佳化

4.2.1 量化精度

為量化精度保證,我們將以下的運算元配置為 int16 或 int32 輸出:

  • temp_interaction、interaction 操作中對輸出的 instance_featuremap 使用固定 scale 的 int16 量化

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/head.py

注:fix_Scale 不同的資料集資料範圍不同,需要根據私有資料集的資料範圍固定,scale 計算方式為:輸入的最值的絕對值除以 2 的 n-1 次冪(n 為量化位數)

self.fc_after.qconfig = get_default_qat_qconfig(
    dtype="qint16",
    activation_qkwargs={
        "observer": FixedScaleObserver,
        "scale": 50 / QINT16_MAX,
    },
)
  • anchor_embed 時對生成的 anchor 使用 int16 量化,其中 instance_bank 的 anchor 的 update 更新中有多個 op 需要使用 int16 保障精度:

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/instance_bank.py

    self.anchor_quant_stub.qconfig = qconfig_manager.get_qconfig(
        activation_qat_qkwargs={"dtype": qint16, "averaging_constant": 0},
        activation_calibration_qkwargs={
            "dtype": qint16,
        },
        weight_qat_qkwargs={
            "averaging_constant": 1,
        },
    )    
    
    
    
    def set_qconfig(self) -> None:
        """Set the qconfig."""
        from horizon_plugin_pytorch.dtype import qint16

        from hat.utils import qconfig_manager

        self.anchor_cat.qconfig = qconfig_manager.get_qconfig(
            activation_qat_qkwargs={"dtype": qint16, "averaging_constant": 0},
            activation_calibration_qkwargs={
                "dtype": qint16,
            },
            weight_qat_qkwargs={
                "averaging_constant": 1,
            },
        )
        self.anchor_where.qconfig = qconfig_manager.get_qconfig(
            ...
        )
        self.temp_anchor_quant_stub.qconfig = qconfig_manager.get_qconfig(
            ...
        )
  • KeyPointsGenerator:key_points 的生成中對 offset、keypoints_add 做 int16 量化。

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/det3d_blocks.py

self.offset.qconfig = qconfig_manager.get_qconfig(
    activation_qat_qkwargs={"dtype": qint16, "averaging_constant": 0},
    weight_qat_qkwargs={
        "averaging_constant": 1,
    },
    activation_calibration_qkwargs={
        "dtype": qint16,
    },
)
self.keypoints_add.qconfig = qconfig_manager.get_qconfig(
            ...
        )
  • RefinementModule:對中間的 add 層和輸出層做 int16 和 int32 的輸出(注意 cache 不能 int32 輸出,還需輸入給下一幀)

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/det3d_blocks.py

        self.layers[-1].qconfig = qconfig_manager.get_qconfig(
            activation_qat_qkwargs={"dtype": qint16, "averaging_constant": 0},
            activation_calibration_qkwargs={
                "dtype": qint16,
            },
            weight_qat_qkwargs={
                "averaging_constant": 1,
            },
        )
        self.add2.qconfig = qconfig_manager.get_qconfig(
            ...
        )
        if self.with_cls_branch is True:
            self.cls_layers[-1].qconfig = qconfig_manager.get_qconfig(
            ...
        )
        if self.with_quality_estimation is True:
            self.quality_layers[
                -1
            ].qconfig = qconfig_manager.get_default_qat_out_qconfig()
  • DeformableFeatureAggregation:對 point 做固定 scale 和 int16 量化。

/usr/local/lib/python3.10/dist-packages/hat/models/task_modules/sparsebevoe/blocks.py

注:fix_Scale 不同的資料集資料範圍不同,需要根據私有資料集的資料範圍固定,scale 計算方式為:輸入的最值的絕對值除以 2 的 n-1 次冪(n 為量化位數)

        self.point_quant_stub.qconfig = qconfig_manager.get_qconfig(
            activation_qat_qkwargs={"dtype": qint16, "averaging_constant": 0},
            weight_qat_qkwargs={
                "averaging_constant": 1,
            },
            activation_calibration_qkwargs={
                "dtype": qint16,
            },
        )
        self.point_cat.qconfig = qconfig_manager.get_qconfig(
            ...
        )
        self.point_matmul.qconfig = get_default_qat_qconfig(
            dtype="qint16",
            activation_qkwargs={
                "observer": FixedScaleObserver,
                "scale": 60 / QINT16_MAX,
            },
        )
        self.reciprocal_op.qconfig = get_default_qat_qconfig(
            ...
        )
        self.point_mul.qconfig = get_default_qat_qconfig(
            ...
        )

05 總結與建議

5.1 部署建議

  • 效能:可以使用單層 feature 來做特徵提取,根據精度和效能表現選擇某層 stride 特徵。

  • 對精度影響較小但是對效能提升較大或量化損失較大的層可以選擇性的裁剪。

  • 可以嘗試 decoder_layers、num_points、num_anchor、不同 stride 層裁剪對精度的影響來找到符合預期的配置。

  • 對 points 相關的計算(anchor_projection、project_points、key_points 計算)建議開啟 int16 和手動固定 scale 的方式。

  • 使用精度 debug 工具、敏感度分析工具來降低量化損失

本文透過對 SparseBevOE 在地平線征程 6 上量化部署的最佳化,使得模型在該計算方案上用低於 2%的量化精度損失(仍在最佳化中),得到 latency 為 36ms 的部署效能,同時,透過 SparseBevOE 的部署經驗,可以推廣到其他模型部署最佳化,例如包含使用稀疏 BEV 的模型部署。

06 附錄

  1. 論文:https://arxiv.org/abs/2311.11722

  2. 公版程式碼:

    https://link.zhihu.com/?target=https%3A//github.com/linxuewu/Sparse4D

  3. 課程:https://www.shenlanxueyuan.com/open/course/210

  4. 知乎專欄:https://zhuanlan.zhihu.com/p/637096473

相關文章