点云前处理介绍

为了更好地帮助您理解DSP示例包提供的 CenterPoint 五维点云前处理,本章节从算法原理出发,分析CPU和DSP参考实现,同时对CPU和DSP参考实现进行一致性校验和性能评测。示例包中同时也提供了 PointPillars 四维点云前处理,与 CenterPoint 类似, 两者前处理的主要特点是通过体素化生成柱体( pillars )。本章节主要介绍 CenterPoint 前处理实现,并使用单独的一节介绍 PointPillars 前处理在实现上的不同之处。

原理介绍

本章节使用五维 CenterPoint 模型作为示例介绍,其相应的原理和前处理算法实现也可适用于四维的 PointPillars 模型。模型的输入为五维点云数据,最大点数为30万。前处理主要由体素化,特征编码,量化和转置组成。前处理输出 1x5x20x40064(nchw) 的特征图和 1x1x40000x4(nhwc) 的坐标信息。

pointpillars_model

体素化

根据输入的五维点云数据的 xy 坐标计算出柱体的坐标,最大的柱体数目为 40000 ,每个柱体最大支持的点数为 20 。柱体坐标的计算原理如下:

  1. 判断点云的 xyz 是否在有效的范围内,跳过无效的点云数据。

  2. 点云的 xy 信息可以被视为伪图像,伪图像中的每一个像素对应一个柱体坐标。

  3. 输入点云的坐标是无序的,因此需要按照坐标的输入顺序确定柱体坐标,当柱体数超过模型限制的最大值之后,不会生成新的柱体,而是将点写入到最后一个柱体中。

  4. 相同坐标的点会放入同一个柱体中,模型限制每一个柱体中点的数目,当超过最大限制时,则直接跳过该点。

cur_pillar_id = 0
max_pillar_id = 39999
max_pt_id_in_pillar = 19

for point_cloud in point_clouds:
x = point_cloud[0]
y = point_cloud[1]
z = point_cloud[2]
// 跳过无效的点云数据
if x or y or z invalid:
	continue;

// 计算点云输入x,y的伪图像坐标
idx = (x - back) / x_scale
idy = (y - right) / y_scale

// 计算柱体坐标
if pillars[{idx,idy}] not exist:
	if cur_pillar_id < max_pillar_id:
	pt_pillar_id = cur_pillar_id
	pillars[{idx,idy}] = cur_pillar_id
	cur_pillar_id += 1
	else:
	pillars[{idx,idy}] = max_pillar_id
	pt_pillar_id = max_pillar_id
else:
	pt_pillar_id = pillars[{idx,idy}]
	
// 计算点在柱体中的索引
if pillar_nums[pt_pillar_id] < max_pt_id_in_pillar
	pt_id_in_pillar = pillar_nums[pt_pillar_id]
	pillar_nums[pt_pillar_id] += 1
else
	pt_id_in_pillar = max_pt_id_in_pillar

特征编码

点云数据的特征编码使用如下的公式:

P0=P0backfrontbackP_0' = \frac{P_0 - \text{back}}{\text{front} - \text{back}}

P1=P1rightleftrightP_1' = \frac{P_1 - \text{right}}{\text{left} - \text{right}}

P2=P2bottomtopbottomP_2' = \frac{P_2 - \text{bottom}}{\text{top} - \text{bottom}}

P3=P3lowerupperlowerP_3' = \frac{P_3 - \text{lower}}{\text{upper} - \text{lower}}

P4=P4P_4' = P_4

量化

量化的scale可以通过模型获取,使用以下公式完成量化:

P=PscaleP' = \frac{P}{\text{scale}}

转置

模型输入的特征图 layout 模式为 NCHW ,转置将特征图的 shape1x40000x20x5 转换为 1x5x20x40064

CPU参考实现

CPU参考实现的源码位于 AI Benchmark 中,其前处理基本流程与原理介绍部分保持一致,其中量化合并到特征编码步骤中。

体素化

void QATCenterPointPreProcessMethod::GenVoxel(int start, int end) {
	for (int i = start; i < end; i++) {
		float *point;
		point = point_cloud_data_ + i * config_->kdim;

		float &point_x = point[0];
		float &point_y = point[1];
		float &point_z = point[2];
		float &point_r = point[3];

		// 判断点云输入的x,y,z是否在有效范围内,跳过无效的点云数据
		if (point_x <= config_->kback_border || point_x >= config_->kfront_border ||
			point_y <= config_->kright_border || point_y >= config_->kleft_border ||
			point_z <= config_->kbottom_border || point_z >= config_->ktop_border) {
			continue;
		}

		// 计算伪图像坐标
		int idx = (point_x - config_->kback_border) / config_->kx_scale;
		int idy = (point_y - config_->kright_border) / config_->ky_scale;

		// 将伪图像坐标转换为一维索引,该索引作为key,判断点对应的
		// 柱体坐标是否已经设置过
		int pillar_index = idy * config_->kx_length + idx;
		int voxelidx = config_->coor_to_voxel_idx[pillar_index];
		// config_->coor_to_voxel_idx开始会初始化为-1
		// -1表示该点的柱体坐标没有设置过
		if (voxelidx == -1) {
        // 如果柱体数小于最大值,则更新柱体坐标,否则使用最大值
        if (voxel_num_ < config_->kmax_num_point) {
          voxelidx = voxel_num_;
          voxel_num_ += 1;
        } else {
          voxelidx = config_->kmax_num_point - 1;
        }
        // 更新柱体坐标
        config_->coor_to_voxel_idx[pillar_index] = voxelidx;
        
        // 写入柱体的坐标信息
        config_->coors[voxelidx * 4 + 1] = 0;
        config_->coors[voxelidx * 4 + 2] = idy;
        config_->coors[voxelidx * 4 + 3] = idx;
		}

		if (config_->pillar_point_num[voxelidx] >= config_->kmax_num_point_pillar) {
			continue;
		} else {
			// 根据点的柱体坐标和点在柱体中的索引,确定点云数据写入的位置
			auto total_offset = (config_->kmax_num_point_pillar * voxelidx +
								config_->pillar_point_num[voxelidx]) *
								config_->kdim;
			// 更新柱体中的点数
			config_->pillar_point_num[voxelidx] += 1;

			// 将点云数据写入到对应的柱体中
			*(voxel_data_ + total_offset) = point_x;
			*(voxel_data_ + total_offset + 1) = point_y;
			*(voxel_data_ + total_offset + 2) = point_z;
			*(voxel_data_ + total_offset + 3) = point_r;
			*(voxel_data_ + total_offset + 4) = point[4];
		}
	}
}

特征编码 & 量化

void QATCenterPointPreProcessMethod::GenFeatureDim5(float scale) {
	for (int i = 0; i < voxel_num_; i++) {
		int idx = i * config_->kmax_num_point_pillar * config_->kdim;
		for (int j = 0; j < config_->kmax_num_point_pillar; ++j) {
			if (config_->pillar_point_num[i] > config_->kmax_num_point_pillar_vec[j]) {
				int index = idx + j * config_->kdim;
				// 特征编码 & 量化
				voxel_data_[index + 0] = (voxel_data_[index + 0] - config_->kback_border) / config_->kx_range / scale;
				voxel_data_[index + 1] = (voxel_data_[index + 1] - config_->kright_border) / config_->ky_range / scale;
				voxel_data_[index + 2] = (voxel_data_[index + 2] - config_->kbottom_border) / config_->kz_range / scale;
				voxel_data_[index + 3] = (voxel_data_[index + 3] - config_->kr_lower) / config_->kr_range / scale;
				if (voxel_data_[index + 4] != 0) {
					voxel_data_[index + 4] = voxel_data_[index + 4] / scale;
				}
			}
		}
	}
}

转置

// 浮点数采用TONEAREST的round模式
static inline float round(float const input) {
	std::fesetround(FE_TONEAREST);
	float const result{std::nearbyintf(input)};
	return result;
}

void QATCenterPointPreProcessMethod::TransposeDim5(int model_aligned_w) {
  // 转置为 1x5x20x40000
  int kWC = config_->kmax_num_point_pillar * config_->kdim;
  int kHW = config_->kmax_num_point * config_->kmax_num_point_pillar;
  // 这里c h w并不是模型nchw
  for (int c = 0; c < config_->kdim; ++c) {
    int input_wh = c * config_->kmax_num_point_pillar * model_aligned_w;
    for (int w = 0; w < config_->kmax_num_point_pillar; ++w) {
      int input_h = input_wh + w * model_aligned_w;
      for (int h = 0; h < voxel_num_; ++h) {
        int old_index = h * kWC + w * config_->kdim + c;
        int new_index = h + input_h;
        float features_tmp = round(static_cast<float>(voxel_data_[old_index]));
        // 将特征数据范围限制到量化后的[-128.0f, 127.0f]
		    features_tmp = std::min(std::max(features_tmp, -128.f), 127.f);
        features_s8_[new_index] = static_cast<int8_t>(features_tmp);
      }
    }
  }
}

DSP优化加速

主要从两个方面考虑算子在DSP上的加速实现,第一是算子的计算部分, J6上的DSP支持1024bit的SIMD ,因此将计算部分尽可能向量化可以充分利用DSP的算力。第二是算子的访存部分,J6上的DSP有两个256kB的TCM ,其访存性能接近CPU上的cache,因此将数据尽可能搬移到TCM上 ,能有效地节约访存的开销。同时DSP也提供向量化的访存指令,也可以提高访存的效率。

DSP加速思路

向量化

对于五维模型,可以将计算过程分为5个cycle,每个cycle计算一个维度,使用IVP向量指令完成运算。

point_cloud_vectorized

具体到 CenterPoint 点云前处理算法,在计算伪图像的坐标,特征编码以及量化等计算过程,都可以将每个维度独立计算,因此很容易转换成向量化的计算。

访存优化

点云输入数据从x,y维度来看是无序的,导致计算出的柱体坐标和柱体中点的索引也是无序的,从而导致访存是随机而不是效率更高的顺序操作。由于DSP上没有data cache,随机访存非常不友好,因此需要充分利用DSP上的TCM。DSP上可用的TCM只有两个256kB,而特征数据输出的地址空间为40000x20=781.25kB,远大于TCM的内存大小。同时计算体素坐标时,也需要一个伪图像坐标与柱体坐标的查找表,该表所需的内存为512x512x4=1MB(512为伪图像的宽和高,4为柱体坐标所需的字节数),也是远远超出TCM的内存大小。考虑到将随机访存搬移到DSP代价太大,因此将前处理算法中的计算和访存进行分离,计算部分放到DSP上,而把部分随机访存放到CPU上执行。同时为了减少访存的数据量,DSP加速实现调整了CPU实现的步骤,将特征编码和量化提前到计算伪图像坐标之后。通过这样的处理,后续的体素化和转置步骤访存所需的数据量将大大减少。 虽然这会增加更多的计算量,但是通过实际评测可见,减少访存数据量带来的收益会更大,特别是在有效点云数较大的情况下。

分块策略

虽然有两个256kB的TCM,但是每一个计算周期只使用其中一个,原因主要有以下两个:

  1. 需要使用Pingpong buffer,减少等待数据拷贝完成的时间;

  2. Pingpong buffer需要位于不同的TCM中,减少data bank conflict。

DSP上的计算被拆为两个部分,一个是计算伪图像坐标,另一个是特征编码,量化和转置:

  1. 计算伪图像坐标,每个计算周期,输入为五维的点云数据,输出为单个维度的数据,输入与输出比为5:1,因此输入与输出的TCM内存大小比例也为5:1;

  2. 特征编码,量化和转置,每个计算周期,输入为五维点云数据,输出为量化后的单维度数据,输入与输出比为20:1。

DSP优化实现

DSP的IVP指令可以参考cadence提供的文档 ISA/NewISAhtml/index.html ,TCM内存管理接口的详细介绍可以参考 Xtensa Vision Tile Manager Library User's Guide ,IDMA相关接口详细介绍可以参考 Xtensa System Software Reference Manual

伪图像坐标计算,特征编码和转置(DSP计算部分)

与CPU参考实现中计算伪图像坐标方法基本一致,主要区别有以下几点:

  1. 去掉一维坐标的计算,通过32位的高低16位存储伪图像x和y坐标(示例模型伪图像的宽高为512,16位已足够存储),同时以二维坐标作为索引,用于查找对应的柱体坐标;

  2. 在过滤无效的点云数据时,使用mask move取代条件判断语句。因为在内循环中使用条件判断会明显降低运算性能;

#define CALC_POINTPILLAR_ID()                                               \
  xb_gsr gs = IVP_GATHERAN_2XF32(p_input, offset[0]);                       \
  xb_vecN_2xf32 const point_x = IVP_GATHERDN_2XF32(gs);                     \
                                                                            \
  gs = IVP_GATHERAN_2XF32(p_input, offset[1]);                              \
  xb_vecN_2xf32 const point_y = IVP_GATHERDN_2XF32(gs);                     \
                                                                            \
  gs = IVP_GATHERAN_2XF32(p_input, offset[2]);                              \
  xb_vecN_2xf32 const point_z = IVP_GATHERDN_2XF32(gs);                     \
                                                                            \
  gs = IVP_GATHERAN_2XF32(p_input, offset[3]);                              \
  xb_vecN_2xf32 const point_r = IVP_GATHERDN_2XF32(gs);                     \
                                                                            \
  /* 使用 IVP 指令计算伪图像坐标 */                                           \
  xb_vecN_2xf32 const vf_idx =                                              \
      IVP_DIVN_2XF32(IVP_SUBN_2XF32(point_x, (xb_vecN_2xf32)param->back),   \
                     (xb_vecN_2xf32)param->x_scale);                        \
  xb_vecN_2xf32 const vf_idy =                                              \
      IVP_DIVN_2XF32(IVP_SUBN_2XF32(point_y, (xb_vecN_2xf32)param->right),  \
                     (xb_vecN_2xf32)param->y_scale);                        \
                                                                            \
  /* float32_t -> int32_t */                                                \
  xb_vecN_2x32v const vi_idx = IVP_TRUNCN_2XF32(vf_idx, 0);                 \
  xb_vecN_2x32v const vi_idy = IVP_TRUNCN_2XF32(vf_idy, 0);                 \
                                                                            \
  /* 将 idy 存储在高 16 位,将 idx 存储在低 16 位 */                          \
  xb_vecN_2x32v pillar_index = IVP_SLLN_2X32(vi_idy, 16);                   \
  pillar_index = IVP_ADDN_2X32(pillar_index, vi_idx);                       \
                                                                            \
  /* 检查点云数据是否有效 */                                                  \
  vboolN_2 const px_le = IVP_ULEN_2XF32(point_x, param->back);              \
  vboolN_2 const px_ge = IVP_ULEN_2XF32(param->front, point_x);             \
  vboolN_2 const py_le = IVP_ULEN_2XF32(point_y, param->right);             \
  vboolN_2 const py_ge = IVP_ULEN_2XF32(param->left, point_y);              \
  vboolN_2 const pz_le = IVP_ULEN_2XF32(point_z, param->bottom);            \
  vboolN_2 const pz_ge = IVP_ULEN_2XF32(param->top, point_z);               \
                                                                            \
  vboolN_2 skip = IVP_ORBN_2(px_le, px_ge);                                 \
  skip = IVP_ORBN_2(skip, py_le);                                           \
  skip = IVP_ORBN_2(skip, py_ge);                                           \
  skip = IVP_ORBN_2(skip, pz_le);                                           \
  skip = IVP_ORBN_2(skip, pz_ge);                                           \
  /* 将无效点云数据的伪图像坐标掩盖为-1 */                                     \
  pillar_index = IVP_MOVN_2X32T(-1, pillar_index, skip);                    \
  /* 将伪图像坐标存储到输出 */                                                \
  IVP_SCATTERN_2X32(pillar_index, p_output, id_offset)

#define CHECK_POINTPILLAR_PARAM()                                            \
  LOGE_IF_COND_RETURN(((param.stage != 0U && param.stage != 1U)),            \
                      HB_ERR_INVALID_ARGUMENT, "stage only support 0 or 1"); \
  LOGE_IF_COND_RETURN((param.num_points <= 0U), HB_ERR_INVALID_ARGUMENT,     \
                      "num_points must greater than 0");                     \
  LOGE_IF_COND_RETURN((param.max_num_points <= 0U), HB_ERR_INVALID_ARGUMENT, \
                      "max_num_points must greater than 0");                 \
  LOGE_IF_COND_RETURN((param.max_num_points_align <= 0U),                    \
                      HB_ERR_INVALID_ARGUMENT,                               \
                      "max_num_points_align must greater than 0");           \
  LOGE_IF_COND_RETURN((param.max_num_point_pillars <= 0U),                   \
                      HB_ERR_INVALID_ARGUMENT,                               \
                      "max_num_point_pillars must greater than 0");          \
  LOGE_IF_COND_RETURN((param.max_num_point_pillars_align <= 0U),             \
                      HB_ERR_INVALID_ARGUMENT,                               \
                      "max_num_point_pillars_align must greater than 0");    \
  LOGE_IF_COND_RETURN(!(param.front > param.back), HB_ERR_INVALID_ARGUMENT,  \
                      "front must greater than back");                       \
  LOGE_IF_COND_RETURN(!(param.left > param.right), HB_ERR_INVALID_ARGUMENT,  \
                      "left must greater than right");                       \
  LOGE_IF_COND_RETURN(!(param.top > param.bottom), HB_ERR_INVALID_ARGUMENT,  \
                      "top must greater than bottom");                       \
  LOGE_IF_COND_RETURN(!(param.x_scale > 0.0f), HB_ERR_INVALID_ARGUMENT,      \
                      "x_scale must greater than 0.0f");                     \
  LOGE_IF_COND_RETURN(!(param.y_scale > 0.0f), HB_ERR_INVALID_ARGUMENT,      \
                      "y_scale must greater than 0.0f");                     \
  LOGE_IF_COND_RETURN(!(param.scale > 0.0f), HB_ERR_INVALID_ARGUMENT,        \
                      "scale must greater than 0.0f")

static FORCE_INLINE void gen_feature_kernel(
    TileBuffer *input, TileBuffer *output, xb_vecN_2x32Uv const *offset,
    xb_vecN_2xf32 const *sub, xb_vecN_2xf32 const *div,
    xb_vecN_2x32Uv id_offset, xb_vecN_2x32Uv xyzr_offset,
    xb_vecN_2x32Uv t_offset, hbDSPPointPillarPreProcessParam const *param) {
  valign a_store = IVP_ZALIGN();

  // 根据 SIMD 数据宽度和输入维度的长度计算每个周期处理的数据大小
  int32_t const in_data_per_cycle = (XCHAL_IVPN_SIMD_WIDTH / 2) * k_dim;
  int32_t const out_data_per_cycle = in_data_per_cycle * 3 / k_dim;

  int32_t data_width =
      input->data_size / (in_data_per_cycle * sizeof(float32_t));
  if ((input->data_size % (in_data_per_cycle * sizeof(float32_t)) != 0) ||
      data_width == 0) {
    ++data_width;
  }

  float32_t const *__restrict p_input =
      (float32_t const *__restrict)input->tile_buffer;
  int32_t *__restrict p_output = (int32_t * __restrict) output->tile_buffer;

  float32_t const x_scale = param->x_scale;
  float32_t const y_scale = param->y_scale;
  float32_t const scale = param->scale;

  for (int32_t i = 0; i < data_width; ++i) {
    CALC_POINTPILLAR_ID();

    xb_vecN_2x32v const vi_feature_x =
        encoder_cp(point_x, sub[0], div[0], scale);

    xb_vecN_2x32v const vi_feature_y =
        encoder_cp(point_y, sub[1], div[1], scale);

    xb_vecN_2x32v const vi_feature_z =
        encoder_cp(point_z, sub[2], div[2], scale);

    xb_vecN_2x32v const vi_feature_r =
        encoder_cp(point_r, sub[3], div[3], scale);

    gs = IVP_GATHERAN_2XF32(p_input, offset[4]);
    xb_vecN_2xf32 const point_t = IVP_GATHERDN_2XF32(gs);
    xb_vecN_2x32v const vi_feature_t = quantize_round_nearest(point_t, scale);

    // 将量化的 xyzr 特征合并到 int32_t
    xb_vecN_2x32v const vi_feature_x_y = IVP_SELN_2X32I(
        vi_feature_y, vi_feature_x, IVP_SELI_8B_INTERLEAVE_1_EVEN);

    xb_vecN_2x32v const vi_feature_z_r = IVP_SELN_2X32I(
        vi_feature_r, vi_feature_z, IVP_SELI_8B_INTERLEAVE_1_EVEN);

    xb_vecN_2x32v const vi_feature_x_y_z_r = IVP_SELN_2X32I(
        vi_feature_z_r, vi_feature_x_y, IVP_SELI_INTERLEAVE_1_EVEN);

    IVP_SCATTERN_2X32(vi_feature_x_y_z_r, p_output, xyzr_offset);

    // 将量化的 t 特征存储到 int32_t
    IVP_SCATTERN_2X32(vi_feature_t, p_output, t_offset);

    p_input += in_data_per_cycle;
    p_output += out_data_per_cycle;
  }
  IVP_SCATTERW();
}

体素化(CPU访存部分)

计算逻辑和CPU参考实现基本一致,主要有以下几点区别:

  1. 使用二维数组查找表,并且直接使用伪图像的x,y作为索引;

  2. 输出voxel_data的索引计算方式不同,因为DSP gather/scatter不能超出64kB的地址空间限制,因此提前按照最终特征数据的shape进行计算输出索引;

void QATCenterPointPreProcessMethod::GenVoxelForDsp(int32_t *coords,
                                                    int32_t num_points) {
  // 初始化体素映射表坐标
  static VoxelInfo const invalid_voxel_info{0U, 0xFFFFU};
  static std::vector<VoxelInfo> const init_vec(config_->ky_width,
                                               invalid_voxel_info);
  for (auto &&voxel_infos : coord_to_voxel_id_) {
    voxel_infos.resize(config_->ky_width, invalid_voxel_info);
    memcpy(voxel_infos.data(), init_vec.data(),
           init_vec.size() * sizeof(VoxelInfo));
  }

  uint16_t max_voxel_idx{0U};
  uint16_t max_voxel_idy{0U};

  FeatureInfo *p_feature{
      reinterpret_cast<FeatureInfo *>(pillar_id_mem_.virAddr)};
  int8_t *voxel_data{reinterpret_cast<int8_t *>(voxel_mem_.virAddr)};
  uint32_t const k_hc{static_cast<uint32_t>(config_->align_padding_point) *
                      static_cast<uint32_t>(config_->kdim)};
  for (int32_t i{0}; i < num_points; ++i) {
    uint16_t idx{p_feature[i].idx};
    uint16_t idy{p_feature[i].idy};

    if (idx == 0xFFFFU || idy == 0xFFFFU) {
      continue;
    }

    VoxelInfo *pvoxel_info{&(coord_to_voxel_id_[idx][idy])};
    uint32_t voxel_idx{pvoxel_info->index};
    uint32_t voxel_num{pvoxel_info->num};

    if (voxel_idx == 0xFFFFU) {
      if (voxel_num_ < config_->kmax_num_point) {
        voxel_idx = voxel_num_;
        ++voxel_num_;
        coord_to_voxel_id_[idx][idy].index = voxel_idx;
        max_voxel_idx = idx;
        max_voxel_idy = idy;
      } else {
        voxel_idx = config_->kmax_num_point - 1;
        pvoxel_info = &(coord_to_voxel_id_[max_voxel_idx][max_voxel_idy]);
        voxel_num = pvoxel_info->num;
      }
      coords[voxel_idx * 4] = 0;
      coords[voxel_idx * 4 + 1] = 0;
      coords[voxel_idx * 4 + 2] = idy;
      coords[voxel_idx * 4 + 3] = idx;
    }

    if (voxel_num >= config_->kmax_num_point_pillar) {
      continue;
    }
    pvoxel_info->num = voxel_num + 1U;

    uint32_t const total_offset{
        voxel_idx * static_cast<uint32_t>(config_->kdim) + voxel_num * k_hc};
    int32_t *voxel_xyzr{reinterpret_cast<int32_t *>(voxel_data + total_offset)};
    *voxel_xyzr = p_feature[i].xyzr;
    voxel_data[total_offset + 4] = static_cast<int8_t>(p_feature[i].t);
  }

  int32_t const invalid_coords{config_->kmax_num_point - voxel_num_};
  if (invalid_coords > 0) {
    int32_t *invalid_coors_start{coords + voxel_num_ * 4};
    uint32_t index{0U};
    for (uint32_t i{0U}; i < invalid_coords; ++i) {
      invalid_coors_start[index] = 0;
      invalid_coors_start[index + 1] = -1;
      invalid_coors_start[index + 2] = -1;
      invalid_coors_start[index + 3] = -1;
      index += 4;
    }
  }
}

转置

AoS 格式的点云转置为 SoA 格式。

static FORCE_INLINE void transpose_kernel(TileBuffer *input, TileBuffer *output,
                                          xb_vecNx16U offset, int32_t dim) {
  int32_t const data_per_cycle = XCHAL_IVPN_SIMD_WIDTH * 2 * dim;
  // 根据 SIMD 数据宽度和输入维度的长度计算每个周期处理的数据大小
  int32_t data_width = input->data_size / (data_per_cycle * sizeof(int8_t));
  if ((input->data_size % (data_per_cycle * sizeof(int8_t)) != 0) ||
      data_width == 0) {
    ++data_width;
  }

  int8_t const *__restrict p_input =
      (int8_t const *__restrict)input->tile_buffer;

  xb_vec2Nx8 *__restrict p_output =
      (xb_vec2Nx8 * __restrict) output->tile_buffer;

  int32_t const input_offset = XCHAL_IVPN_SIMD_WIDTH * dim;

  valign a_store = IVP_ZALIGN();
  for (int32_t i = 0; i < data_width; ++i) {
    xb_gsr gr1 = IVP_GATHERANX8S(p_input, offset);
    xb_gsr gr2 = IVP_GATHERANX8S(p_input + input_offset, offset);

    xb_vec2Nx8 feature = IVP_GATHERD2NX8_L(gr1);
    IVP_GATHERD2NX8_H(feature, gr2);
    IVP_SA2NX8_IP(feature, a_store, p_output);
    p_input += data_per_cycle;
  }
  // 刷新输出数据
  IVP_SAPOS2NX8U_FP(a_store, p_output);
}

一致性校验

分别导出CPU和DSP参考实现的点云前处理输出特征数据和坐标信息到文件,比对文件内容是否一致,如果一致则通过一致性校验。

// dump坐标信息
void dump_coords(int32_t const *coords, std::string const &file, size_t size) {
	std::ofstream fd_output(file, std::ios::trunc);
	VLOG(EXAMPLE_DEBUG) << "dump coords to " << file;
	for (size_t i{0U}; i < size; ++i) {
		fd_output << coords[i] << "\n";
	}
}

// dump特征数据
void dump_features(int8_t const *features,
					std::string const &file,
					size_t size) {
	std::ofstream fd_output(file, std::ios::trunc);
	VLOG(EXAMPLE_DEBUG) << "dump features to " << file;
	for (uint32_t i{0U}; i < size; ++i) {
		fd_output << static_cast<int32_t>(features[i]) << "\n";
	}
}

AI Benchmark中,可以执行完整的全流程精度评测,当前DSP参考实现与CPU参考实现精度完全一致。

小技巧

如果您不要求DSP实现的精度与CPU实现具有完全一致的精度,那么可以通过优化浮点运算来进一步提升性能。例如,通过将耗时的除法运算替换为乘法运算。

性能评测

AI Benchmark中,执行CenterPoint模型的延时测试,比较CPUDSP参考实现的单帧延时。通过实际测试发现,voxel_num对点云前处理的耗时影响较大,因此这里选择几组不同voxel_num的数据,以对比CPU参考实现和DSP参考实现的性能。

单线程单帧延时测试结果如下:

voxel_numCPU参考实现DSP参考实现
64527.978 ms4.809 ms
1189411.779 ms5.037 ms
2457920.428 ms5.320 ms
4000030.222 ms5.677 ms

PointPillars前处理实现简介

PointPillars四维点云前处理与CenterPoint五维点云前处理的基本处理逻辑相同,主要有以下两点区别:

  1. 点云输入维度不同;

  2. 点云前处理后的输出layout不同。

针对以上两点差异,以下将分别阐述DSP加速实现上的不同之处:

  1. 点云输入维度不同
  • PointPillars 四维点云输入经过量化后,其数据大小正好适配 int32_t 类型,无需像 CenterPoint 那样对第五维量化后的数据进行 padding 操作以适配 int32_t 类型。

  • 第一步计算伪图像坐标,特征编码和量化步骤的输入输出TCM大小比例不同。CenterPoint5:3PointPillar2:1,输入为四维浮点点云数据(float32_t),输出为伪图像坐标(int32_t)和量化后的xyzr数据(int32_t)。

  • 由于维度的差异,对输入点云的gather操作所需的offset计算不同。

  1. 点云前处理后的输出layout不同
  • 在体素化(CPU访存)步骤中,voxel_dataoffset计算方式存在差异。

  • 由于输出layout不同,转置部分的输入输出TCM大小比例存在差异。CenterPoint的比例为5:1,而PointPillars的比例为4:1

  • 转置的处理逻辑不同,具体的差异参见下图。

centerpointpointpillar

注解

根据图示,CenterPointPointPillarsmax_point_in_voxelmax_voxel_num的顺序上存在差异,这导致了无效数据的分别不同。 具体来说,PointPillars的无效数据呈现出较为集中的分布特点,这有助于进行连续处理。相反,CenterPoint的无效数据则散布在各个voxel之间,呈现出交叉分布的特点。前处理的参数对算子的实现效率会有一定影响,可以进一步考虑根据不同的参数实现特定的逻辑。