赞
踩
自动驾驶之心推出的《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考
本次课程我们来学习下课程第八章——实战:CUDA-BEVFusion部署分析,一起来学习 BEVPool 的优化方案
Note:之前在学习柒柒老师的课程中有简单记录过 LSS 算法的一些知识,感兴趣的可以看下:二. BEV感知算法基础模块讲解
课程大纲可以看下面的思维导图
本小节目标:学习 BEVPool 是什么,LSS 的 2D 到 BEV 的投影方法,BEVFusion 和 CUDA-BEVFusion 中对 BEVPool 的加速方案
这节我们讲第八章的第 6 小节,学习一下 BEVPool 的优化方案,那我们要学习 BEVPool 的优化方案,我们得先知道 BEVPool 它是干什么的,那学习 BEVPool 我们首先必须要聊 LSS 这篇论文,它是 BEV 感知算法的开山之作,由 NVIDIA 发表于 ECCV2020,这篇论文大家如果感兴趣的话必须要读一下,链接:Lift, splat, shoot: Encoding images from arbitrary camera rigs by implicitly unprojecting to 3d
我们需要知道 LSS 中 2D 到 BEV 的一个投影方式是怎么做的,那知道这个之后我们就可以知道 BEVPool 它的整个计算瓶颈,它哪些地方计算量比较大,那知道这个问题之后我们就可以去思考如果优化的话,我们可以从哪些地方去优化,那 BEVFusion 和 CUDA-BEVFusion 其实已经给了我们答案,对于 BEVPool 它们其实提供了一些加速方案,我们这个小节就来分析一下 BEVPool 的加速方案是怎么做的
我们在谈 BEVPool 之前,我们先看一下 BEVFusion 中 Camera 视角下提取完特征图之后干了什么
在上面的 BEVFusion 框图中我们可以看到 6 个不同视角的图像经过一个 2D Backbone 之后提取图像特征,那这个 2D Backbone 可以是 ResNet、Swin Transformer 等等都行,提取完特征之后我们需要通过视角转换模块把 Camera Feature 投影到 BEV 上,那这里其实会出现一个问题,我们在 二. BEV感知算法基础模块讲解 中讲过 2D 到 3D 的投影其实是一条射线,图像中的每一个像素与空间中的一条射线是对应的,是点线的对应关系,那具体投影到射线上的哪一个点呢,我们无从得知,假设 BEV Grid 的大小是 180x180,那么每一个 grid 对应的是哪个 Camera 上的哪一个坐标呢?这个其实是大家需要考虑的事情
另外我们都知道 Camera 传感器拍摄的图片其实是没有深度信息的,我们从相机的小孔成像原理中可以知道沿着光线的角度去看一个物体的时候,它其实无论是在远处还是在近处,在图片上看到的都是一个点,那这个就导致图片的深度信息其实是丢失的,那么如果深度信息是丢失的话,我们要怎么去学习它,我们要怎么去把这个深度信息给拿到之后去做一个 BEV 上的投影呢,这个其实在 LSS 这篇文章里面就有讲,我们下面来简单看一下
我们将 Camera Feature 给 project 投影到 BEV Grid 时,从特征图的形状变化来说是:
[
6
,
32
,
88
,
80
]
➡
[
1
,
80
,
360
,
360
]
(
[
cameraB,cameraH,cameraW,cameraC
]
➡
[
bevB,cameraC,bevH,bevW
]
)
其中 B,H,W,C 分别代表的是 Batch,Height,Width,Channel
OK,我们要谈 BEVPool 之前我们得需要把 LSS 这篇论文好好读一读,其实后面很多跟 BEV 相关的一些论文比如 CaDDN、BEVDet、BEVFusion 等等都是基于 LSS 算法去做的一些改进,它们的 2D 到 3D 的转换其实都参考了 LSS 这个方案
我们先来看 BEVFusion 中对每一个点 camera ray 预测深度分布的解释,在上图中有一个相机,前面是相机拍摄的一幅图片,在图片中有 A、B、C 三个点,我们只知道这几个点在三维世界中的 x,y 方向的坐标,不知道 z 方向的坐标,也就是不知道深度到底是多少
从小孔成像我们知道 2D 到 3D 的投影其实是一条射线,那么射线上所有点都有可能是投影点,具体哪个才是投影点才是我们要的深度呢?那现在我们可以给它假定几个深度,对每个像素点我们都去估计预测 D 个深度,D 代表的就是我们要估算几个深度,比如说我们估算 100 个,那这个深度就是每隔 1m 我们估计一个,那么每一个像素它可能出现的深度就是从 1、2、…、100 共有 100 个可能性的一个数据
那有了 100 个可能性的数据之后我们要给做一个深度的概率,看这 100 个数据中到底在哪一个深度的概率是最高的,比如说图中的 C 像素点我们给它估计 100 个离散的深度值,那这 100 个离散的深度值在第 33 个深度值的概率最高,比如说是 0.5,那说明 C 点离我们的距离在 33m 的可能性是最高的,那其它的可能性就比较低了,比如它不可能在 200m 远,在 10m 的概率也偏小,那么我们就可以这样去学习它。也就是说我们对每一个点去预测 D 个深度值,之后把这些深度值做一个 softmax 变成概率图
那么我们再来看上面这个图就比较好理解了,这是 LSS 中对每一个点 camera ray 预测深度分布的解释,它对于每一个像素点都有一个概率图,比如说对于某个点它的概率最高,那么就说明它最有可能是在这个地方出现,那么对于 D 个离散的点都有概率值
这就意味着我们深度学习在设计网络的时候我们需要给它设计两个分支,我们这里拿 BEVFusion 中相机骨干网络模块的 ONNX 即 camera.backbone.onnx 简单看一下
可以看到 camera backbone 模块中有两个头,一个是 camera_feature,维度是 6x32x88x80,分别代表着 NxHxWxC,另一个是 camera_depth_weights 即 depth 的概率图,维度是 6x118x32x88 分别代表着 NxDxHxW,这里的 D 表示对每一个特征像素 D 个 depth 的概率分布,图中 D 是 118,说明我们为图片上每个像素点估计 118 个深度值,之后对 118 个深度值做一个 softmax 看哪个点出现的可能性最大,这就是 camear backbone 的输出,它和普通的图像特征提取网络如 resnet 有所不同,多了一个深度概率分支
得到这两个输出之后我们下一步要干什么呢?我们接下来要将 Depth Weights 和 Camera Feature 做个内积,内积之后我们可以得到一个 NxDxHxWxC 维度的特征图,如上图所示。由于这个特征图是具有 3D 信息的,所以论文中比较喜欢用 Camera Feature Point Cloud 来表示这个特征
它可以理解为是在 camera 方向上模拟预测得到的一个点云的信息,这个特征图比较特殊,它不仅有 camera feature,它还有 depth feature,也就是说它是三维的。NxDxHxWxC 可以理解为:在第 n 个 camera 的 (x,y) 坐标上的第 z 个深度有 C 维特征
这里需要注意的是:NxDxHxW 里的数据由于是经过 softmax 的,所以除了某个坐标被准确估测到的 z 坐标的概率很高以外,其余的 z 坐标的概率会很低。这些 z 方向的概率信息与 camera feature 内积后,就会代表每一个 z 坐标上的 camera feature 会有不同的比重
OK,我们再看下一步我们怎么做,不考虑 C 方向的维度后,NxDxHxWxC 其实代表了三维空间上的 N*D*H*W 个点。我们可以假设这是个“伪点云”,那么我们可以把这些“点”给投影到 BEV 空间上 1xCxBHxBW,如下图所示:
不考虑 C 方向的维度,BEV 的坐标的个数是 BH*BW 个,BH*BW 是远小于 N*D*H*W 的,以 BEVFusion 为例,BH=BW=360,N=6,D=118,H=32,W=88,所以
注意这里面的投影的方法其实是可以通过相机内外参算出来的
现在我们需要把这 200 万个点给 project 投影到 13 万的 grid 上面去,那也就意味着 BEV 中每一个 grid 可以有多个 camera feature point cloud 的点,所以这也就形成了体素化。值得注意的是,每一个点其实都是有 C 维特征的,那么一个 grid 中会有 x*C 个数据,数据还是比较多的,既然每个 grid 里面有多个点,我们需要做一个处理,怎么做呢?
如上图所示,我们可以将映射到每一个 grid 里的所有点进行相加,就可以只剩下一个点,也就是 1*C 个数据,那么结合 grid 的数量,最终形成的 BEV Feature 的大小就是 1*C*BH*BW
比如图中有四个点落在 grid0,两个点落在 grid 1,每个点有 C 个数据,总共有 N 个 grid,我们对每个 grid 的数据都做一个相加可以,最后每个 grid 可以得到一个 1*C 的数据,这个就是 BEVPool 需要做的。
总结下来 BEVPool 有这么几个流程
我们思考一下,这个过程的计算量有多少
也就是说有接近 200 万个点需要做这一系列计算,这个耗时是非常大的。BEVFusion 的论文里说,整个推理过程中,BEVPool 需要 500ms,其余的所有部分需要 100ms
BEVFusion 和 CUDA-BEVFusion 中对这一部分做了加速,主要考虑两个方面:
第一个加速是 Precomputation 预计算,如果说相机内外参固定,camera 在预测 depth 时 D 的数量也是固定的,那么 BVE Grid 中每一个 grid 所对应的相机中的坐标也是固定的,不同场景的推理都是一样的。那么 camera 中的特征坐标和 BEV 中的 grid 的索引是可以提前计算出来的。
比如说 grid[20] 中包含的点有 camera 中的:
[ n 1 , d 1 , h 1 , w 1 , C ] , [ n 1 , d 4 , h 7 , w 9 , C ] , [ n 2 , d 10 , h 8 , w 90 , C ] . . . . . . [ n 4 , d 81 , h 90 , w 30 , C ] [n_1,d_1,h_1,w_1,C], [n_1,d_4,h_7,w_9,C],[n_2,d_{10},h_8,w_{90},C]......[n_4,d_{81},h_{90},w_{30},C] [n1,d1,h1,w1,C],[n1,d4,h7,w9,C],[n2,d10,h8,w90,C]......[n4,d81,h90,w30,C]
由于这个关系是不会变的,所以不需要在推理的时候计算这个对应关系,而是在初始化的时候把这个对应关系计算出来,之后对每一个 grid 处理的时候,找相应的特征坐标的点就好了,可以省去大量计算。
另外一个加速是 Interval Reduction,对于求和的部分,我们可以使用 CUDA 加速,让每一个 thread 负责一个 grid 里的所有计算,包括求和、内积这些可以用一个 kernel 完成
我们可以在 CUDA-BEVFusion 的代码中找到这些优化计算的存在
对于 Precomputation 我们可以在 src/bevfusion/camera-geometry.cu 中找到如下代码:
在 main 函数中的初始化时,读取相机内外参后通过 update 可以调用这个 kernel 做预处理
对于 Interval Reduction 我们可以在 src/bevfusion/camera-bevpool.cu 中找到如下代码:
在 bevpool 进行 forward 时,调用这个 kernel 做 accumulate,可以看到这一部分的实现本身非常简单
OK,以上就是 BEVPool 的全部内容了,后续我们在代码讲解部分会提到相关实现
这节课程我们主要学习了 BEVPool 及其优化方案,首先我们学习了 LSS 论文中的 2D 到 3D 的投影方案,然后学习了 BEVFusion 中的投影和 BEVPool 的实现,之后针对 BEVPool 计算量大的问题我们学习了 Precomputation 和 Interval Reduction 两种优化方案,一种通过预计算减少参数量,另一种通过 CUDA 中的 thread 处理多个 grid。
OK,以上就是第 6 小节有关 BEVPool 优化方案的全部内容了,下节我们将去学习分析 BEVFusion 中各个 ONNX,敬请期待声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/369790
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。