当前位置:   article > 正文

【OpenPCDet】稀疏卷积SPConv-v1.2代码解读(1)_spconv.submconv3d

spconv.submconv3d

【3D卷积】

        以下左图展示了一个2D卷积,使用一个3x3的卷积核,在单通道图像上进行卷积,其中Padding为1,得到输出。右图为一个单通道的3D卷积,与2D卷积不同之处在于,输入图像多了一个 depth 维度,卷积核也多了一个depth维度,之前2D卷积上3x3的卷积核现在变成了3x3x3。这里的3D不是通道导致的,而是深度(多层切片,多帧视频),因此,虽然输入和卷积核和输出都是3D的,但都可以是单通道的。
 

【3D稀疏卷积】

        标准的3D卷积直接用于类似3D点云检测/分割等3D任务时,因为该类场景中输入特征的稀疏性,会带来严重的冗余计算量。所以,面对稀疏场景发展除了3D稀疏卷积。当然,2D也有稀疏卷积。

对于稀疏卷积有两种:

一种是Spatially Sparse Convolution ,在spconv中为SparseConv3d。就像普通的卷积一样,只要kernel 覆盖一个 active input site,就可以计算出output site。

另一种是Submanifold Sparse Convolution, 在spconv中为SubMConv3d。只有当kernel的中心覆盖一个 active input site时,卷积输出才会被计算。

【Second引入3D稀疏卷积】

        Second论文中,作者在VoxleNet论文的基础上作了进一步的发展。考虑到VoxleNet模型中3D卷积运算量较大,速度不佳。作者引入了稀疏3D卷积来代替,在检测速度和内存使用方面都做了优化。作和开源了3D稀疏卷积的实现:GitHub - traveller59/spconv: Spatial Sparse Convolution Library

截止目前已更新至spconv 2.x版本。我这里作代码解读仍然时基于早期的1.2版本,对于理解思想来说,问题不大。不得不佩服作者强大的代码工程能力!

【Second网络结构中的3D稀疏卷积】

        分析OpenPCDet中Second的网络结构,3D稀疏卷积使用在3D骨干网模块:VoxelBackBone8x中。它接收MeanVFE模块的结果,经过精心设计好的3D稀疏卷积和3D稀疏子流卷积的有效组合,得到输出特征,并送入HeightCompression作深度方向的压缩,后面就是我们熟悉的2D检测网络的结构:2D骨干网络-->RPN-->分类/回归-->后处理。

 在Second原始论文中给出了BACKBONE_3D部分的示意图,但是这与OpenPCDet中Second具体的网络参数有所差异,注意区分。下图是Second中BACKBONE_3D部分的表示。

OpenPCDet中的Second我们可以参考具体的代码实现。

  1. class VoxelBackBone8x(nn.Module):
  2. def __init__(self, model_cfg, input_channels, grid_size, **kwargs):
  3. super().__init__()
  4. self.model_cfg = model_cfg
  5. norm_fn = partial(nn.BatchNorm1d, eps=1e-3, momentum=0.01)
  6. self.sparse_shape = grid_size[::-1] + [1, 0, 0] #e.g. array([ 41, 1600, 1408])
  7. self.conv_input = spconv.SparseSequential(
  8. spconv.SubMConv3d(input_channels, 16, 3, padding=1, bias=False, indice_key='subm1'),
  9. norm_fn(16),
  10. nn.ReLU(),
  11. )
  12. block = post_act_block
  13. self.conv1 = spconv.SparseSequential(
  14. block(16, 16, 3, norm_fn=norm_fn, padding=1, indice_key='subm1'),
  15. )
  16. self.conv2 = spconv.SparseSequential(
  17. # [1600, 1408, 41] -> [800, 704, 21]
  18. block(16, 32, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv2', conv_type='spconv'),
  19. block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),
  20. block(32, 32, 3, norm_fn=norm_fn, padding=1, indice_key='subm2'),
  21. )
  22. self.conv3 = spconv.SparseSequential(
  23. # [800, 704, 21] -> [400, 352, 11]
  24. block(32, 64, 3, norm_fn=norm_fn, stride=2, padding=1, indice_key='spconv3', conv_type='spconv'),
  25. block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),
  26. block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm3'),
  27. )
  28. self.conv4 = spconv.SparseSequential(
  29. # [400, 352, 11] -> [200, 176, 5]
  30. block(64, 64, 3, norm_fn=norm_fn, stride=2, padding=(0, 1, 1), indice_key='spconv4', conv_type='spconv'),
  31. block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),
  32. block(64, 64, 3, norm_fn=norm_fn, padding=1, indice_key='subm4'),
  33. )
  34. last_pad = 0
  35. last_pad = self.model_cfg.get('last_pad', last_pad)
  36. self.conv_out = spconv.SparseSequential(
  37. # [200, 150, 5] -> [200, 150, 2]
  38. spconv.SparseConv3d(64, 128, (3, 1, 1), stride=(2, 1, 1), padding=last_pad,
  39. bias=False, indice_key='spconv_down2'),
  40. norm_fn(128),
  41. nn.ReLU(),
  42. )
  43. self.num_point_features = 128
  44. ....
  45. def forward(self, batch_dict):
  46. """
  47. Args:
  48. batch_dict:
  49. batch_size: int
  50. vfe_features: (num_voxels, C)
  51. voxel_coords: (num_voxels, 4), [batch_idx, z_idx, y_idx, x_idx]
  52. Returns:
  53. batch_dict:
  54. encoded_spconv_tensor: sparse tensor
  55. """
  56. voxel_features, voxel_coords = batch_dict['voxel_features'], batch_dict['voxel_coords']
  57. pdb.set_trace()
  58. batch_size = batch_dict['batch_size']
  59. input_sp_tensor = spconv.SparseConvTensor(
  60. features=voxel_features, #e.g. torch.Size([16000, 4])
  61. indices=voxel_coords.int(), #e.g. torch.Size([16000, 4])
  62. spatial_shape=self.sparse_shape, #e.g. array([41, 1600, 1408])
  63. batch_size=batch_size
  64. )
  65. x = self.conv_input(input_sp_tensor)
  66. x_conv1 = self.conv1(x)
  67. x_conv2 = self.conv2(x_conv1) #stride 2,downsample
  68. x_conv3 = self.conv3(x_conv2) #stride 2,downsample
  69. x_conv4 = self.conv4(x_conv3) #stride 2,downsample
  70. # for detection head
  71. # [200, 176, 5] -> [200, 176, 2]
  72. out = self.conv_out(x_conv4)
  73. ....

对于VoxelBackbone8x模块的前向推理(forward)部分,其输入字典中最重要的内容为voxel_features和voxel_coords。他们分别表示有效的输入特征,以及这些有效特征的空间位置。voxel_features的size为(N,4),通常不同帧点云的有效特征的数量是不同的,N表示当前batch中总的有效输入特征的数量。可见,就VoxelBackBone8x模块来说,输入feature map其实是不固定的。具体到无论是3D标准稀疏卷积还是3D子流形卷积,其输入特征也是可变的。这会给我们做进一步做spconv的部署带来挑战。就我们常用的TensorRT推理引擎来说,它就要求输入特征是固定的。注意,这里说的固定跟TensorRT中的dynamic shape不是一回事。

【spconv模块】

        在Second中spconv是作为Pytorch的一个自定义扩展模块在使用。对于一般的操作,我们扩展Pytorch模块很容易,只用使用Python来扩展即可。只需要继承torch.nn.Module并实现其__init__,forward等方法,求导的函数是不需要设置的,会自动按照求导规则求导师。像Second代码中的HeightCompression模块就是这样一个例子。这种扩展方式即插即用,不需要编译。

  1. class HeightCompression(nn.Module):
  2. def __init__(self, model_cfg, **kwargs):
  3. super().__init__()
  4. self.model_cfg = model_cfg
  5. self.num_bev_features = self.model_cfg.NUM_BEV_FEATURES
  6. def forward(self, batch_dict):
  7. """
  8. Args:
  9. batch_dict:
  10. encoded_spconv_tensor: sparse tensor
  11. Returns:
  12. batch_dict:
  13. spatial_features:
  14. """
  15. encoded_spconv_tensor = batch_dict['encoded_spconv_tensor']
  16. spatial_features = encoded_spconv_tensor.dense()
  17. N, C, D, H, W = spatial_features.shape #e.g. torch.Size([1, 128, 2, 200, 176])
  18. spatial_features = spatial_features.view(N, C * D, H, W)
  19. batch_dict['spatial_features'] = spatial_features
  20. batch_dict['spatial_features_stride'] = batch_dict['encoded_spconv_tensor_stride']
  21. return batch_dict

但是对于像对3D稀疏卷积这样复杂的操作进行优化,实现其扩展模块,单纯靠Pytorch已实现的operator的组合已经无法做到。正如spconv的实现,它采用C++和CUDA来扩展自定义模块。在 PyTorch 中直接扩展底层C++算子主要有三种方式,native_functions.yaml、C++ extension方式、OP register方式。spconv中使用了OP register这种方式。spconv代码主要分为python部分代码和c++/cuda部分代码两部分,对于其中重要内容后文我们做详细分析。

  python部分目录

  1. ├── setup.py
  2. ├── spconv
  3. │   ├── conv.py
  4. │   ├── functional.py
  5. │   ├── identity.py
  6. │   ├── __init__.py
  7. │   ├── modules.py
  8. │   ├── ops.py
  9. │   ├── pool.py
  10. │   ├── tables.py
  11. │   ├── test_utils.py
  12. │   └── utils
  13. │   ├── __init__.py
  14. │   └── __pycache__

c++/cuda部分目录

  1. ├── include
  2. │   ├── cuhash
  3. │   ├── paramsgrid.h
  4. │   ├── spconv
  5. │   ├── tensorview
  6. │   ├── torch_utils.h
  7. │   └── utility
  8. │   └── timer.h
  9. ├── src
  10. │   ├── cuhash
  11. │   ├── spconv
  12. │   │   ├── all.cc
  13. │   │   ├── CMakeLists.txt
  14. │   │   ├── cublas_gemm.cc
  15. │   │   ├── indice.cc
  16. │   │   ├── indice.cu
  17. │   │   ├── maxpool.cc
  18. │   │   ├── maxpool.cu
  19. │   │   ├── pillar_scatter.cu
  20. │   │   ├── pool_ops.cc
  21. │   │   ├── reordering.cc
  22. │   │   ├── reordering.cu
  23. │   │   └── spconv_ops.cc
  24. │   └── utils
  25. │   ├── all.cc
  26. │   ├── CMakeLists.txt
  27. └── third_party

【参考文献】

稀疏卷积 Sparse Convolution Net - 知乎

PyTorch扩展自定义PyThon/C++(CUDA)算子的若干方法总结 - 知乎

这可能是关于Pytorch底层算子扩展最详细的总结了! - 知乎

PyTorch算子底层源码解读--Op Registration - 知乎

通俗易懂的解释Sparse Convolution过程 - 知乎

Spconv代码解读 - 知乎

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/490506
推荐阅读
相关标签
  

闽ICP备14008679号