深度学习计算框架综述(十三)HVX 计算优化实践—Conv 优化

本节主要介绍Conv的HVX实现。
摘要由CSDN通过智能技术生成

本节以Conv为例,介绍Hexagon V66 架构下,Conv的HVX实现。

前面的章节我们分析过VTCM,在VCAP DSP框架中,VTCM是按照下面的区域划分的:

|------------------------------------ 256 KB -------------------------------------|

|----vtcm input-----|----vtcm weight-----|-----vtcm suma----|----remain----|

 

量化计算原理:

 

acc32 = ∑(input - input_offset) * (filt - filt_offset) + bias32                                                                         (1)

          = ∑(input * filt - input_offset * filt - input * filt_offset + input_offset * filt_offset) + bias32                      (2)

          = ∑(input * filt - input * filt_offset  ) +∑(input_offset * filt_offset - input_offset * filt) + bias32              (3)         

let bias_buff = bias32 +  ∑(input_offset * filt_offset - input_offset * filt)                                                           (4)

bias_buff  can be computed offline because all elements in equation (4) are constant. So we rewrite equation (1) as follow:

acc32 = ∑(input * filt - input * filt_offset  )  + bias_buff                                                                                  (5)

DownScaleToInt8(acc32)                                                                                                                              (6)              

下文中weight和filt都可以用来表示权重,含义相同

 

DataFormat:

Feature Map:

在CPU侧,数据的原始排布是NHWC,在DSP中,Feature Map 采用的是D32 Format ,具体的原因我们在 D32 Format浅析 这篇文章中做过简单的分析。

D32 Format的维度信息如下:

N x H x C32 x W4 x 4 x 32 →  N x H x C32 x PADDED_WIDTH x 32

其中:

W4  = (FEATURE_MAP_WIDTH+ 3) >> 2

C32 = (FEATURE_MAP_CHANNEL+ 31) >> 5

对于FEATURE_MAP_CHANNEL <= 4 的模型输入,我们会采用下面的格式,即将 C (C <= 4)补齐到4:

N x H x PADDED_WIDTH x 4

这样做的原因是,通常CNN模型的输入都是3通道或者单通道,如果将其补齐到32,会导致内存占用、计算量剧增,补齐到4可以很好的解决这两个问题。

只针对模型输入这样处理的原因是,Feature Map通常是按照D32 Format排布的,频繁地进行数据排布变换会带来一定地性能损失,所以这也要求我们在

设计算法时,尽量保证模型中的OP都是32对齐。

Weights :

Weights 采用的Data Format是为了适配 D32 Format 对 Weights原始的数据排布HWCN进行了变换,变换后的数据排布和示意图如下:

N32 x H x C32 x W x 8 x 32 x 4 

最后面三项 8 x 32 x 4 中,8和4都是IN_CHANNEL,32 表示 OUT_CHANNEL,  这样在计算vrmpy时,可以同时计算32个OUT_CHANNEL的结果。

采用这种排布的好处就是,满足128byte对齐的要求,可以用vmem指令进行高效的访存,同时完美适配vrmpy指令。

对于IN_CHANNEL <= 4 的Conv, 我们会采用下面的数据排布, 这个排布主要是针对FEATURE_MAP_CHANNEL <= 4 的模型输入:

N32 x H  x W x 32 x 4

 

Conv 优化实践示例:

本文会以Mobilenet V1中的OP为例,介绍Conv3x3s2d4以及conv1x1s1d32两个Conv的优化思想:

Conv3x3s2d4

Mobilenet V1的结构如下图所示(图中的模型输入是NCHW),我们可以看到模型的输入维度是[1, 224, 224, 3](NHWC),第一层卷积的weights是[3, 3, 3, 32] (HWCN)。

所以我们会采用前文提到的 N x H x PADDED_WIDTH x 4 数据排布,即将 [1, 224, 224, 3] 补齐到 [1, 224, 224, 4],实际计算时,我们还需要考虑pads值,

对于第一个conv3x3s2而言,[pad_top, pad_right , pad_bottom , pad_left ] = [0, 1, 1, 0],也就是加上pads后Feature Map变成了[1, 225, 225, 4],由于

我们要求Width是4的整数倍,所以会将Feature Map补齐到[1, 225, 228, 4],但是这几步存在一些差异:

 [1, 224, 224, 3] → [1, 224, 224, 4], 在Channel方向pads的是0,目的是保证Channel 4对齐,同时保证计算的正确性

 [1, 224, 224, 4] → [1, 225, 225, 4], 在Height、Width方向pads的是zeropoint,即128,目的是保证计算的正确性

 [1, 225, 225, 4] → [1, 225, 228, 4], 在Width方向pads的是0,目的是保证Width方向4对齐(其实这一步pads任意值都可以,不影响计算结果,为了统一,此处都pads 0)

 

我们在 DSP访存优化原理 中有分析过,L2 标量的硬件Pipeline 比VTCM更快,所以Input 的上述pads、补齐操作可以在Intermediate Buffer中完成,而不是放在VTCM上,通常Intermediate Buffer的大小为:

num_workers * inter_buffer_height * padded_width * 4

这里解释一下这几个参数的含义:

num_workers 表示线程数

inter_buffer_height 表示Intermediate Buffer的height,通常这个值和weight的kernel height相同,针对上面这个例子 inter_buffer_height 为3

 

接下来,我们来分析Conv3x3s2d4 的实现,conv3x3s2d4_callback 的完整代码如下:

// conv_op.cpp

static void conv3x3s2d4_callback(void *data) {
  conv_callback_t *dptr = (conv_callback_t *)data;
  uint64_t L2FETCH_INPUT_REGISTER = (1ULL << 48) | ((uint64_t)dptr->next_in_width_d4 << 32) | ((uint64_t)((dptr->next_in_width_d4 + 127) & 0xffffff80) << 16) | 3ULL;
  uint32_t thread_id = dspCV_atomic_inc_return((unsigned int *)(&(dptr->job_count))) - 1;
  uint32_t start_height = thread_id * dptr->rows_per_job;
  uint32_t end_height = MIN((thread_id + 1) * dptr->rows_per_job, dptr->top_h);
  uint8_t *input = dptr->input + start_height * dptr->stride_h * dptr->bottom_c * dptr->bottom_w;
  uint8_t *vtcm_input = dptr->vtcm_input + thread_id * dptr->next_in_width_d4 * dptr->vtcm_height;
  uint8_t *output = dptr->output + start_height * dptr->next_out_width_depth;
  L2FETCH(vtcm_input, L2FETCH_INPUT_REGISTER);
  L2FETCH(input, L2FETCH_INPUT_REGISTER);
  const uint32_t d4 = 4;
  uint32_t tmp_pad_val = 0;
  if (end_height == dptr->top_h) {
    end_height -= 1;
  }
  // padding vtcm_buf once in each thread; pad_right = 1 for conv with stride = 2 and kernel_size = 3
  for (uint32_t h = 0; h < dptr->vtcm_height; h++) {
    uint8_t *tmp_vtcm_input = vtcm_input + h * dptr->next_in_width_d4 + dptr->bottom_w * d4;
    tmp_vtcm_input[0] = (uint8_t)dptr->quant_info->bottom_zp;
    tmp_vtcm_input[1] = (dptr->bottom_c > 1) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
    tmp_vtcm_input[2] = (dptr->bottom_c > 2) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
    tmp_vtcm_input[3] = (dptr->bottom_c > 3) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
    if (h == 0) tmp_pad_val = *(uint32_t *) tmp_vtcm_input;
  }
  for (; start_height < end_height; start_height++) {
    // copy from intermediate buffer to vtcm
    uint8_t *tmp_vtcm_input = vtcm_input + dptr->bottom_w * d4;
    COPY_INPUT_TO_VTCM_D4(0, dptr->vtcm_height);
    conv3x3s2d4_asm(vtcm_input, output, data);
    input += dptr->stride_h * dptr->bottom_c * dptr->bottom_w;
    output += dptr->next_out_width_depth;
    if (start_height < end_height - 3)
      L2FETCH(input, L2FETCH_INPUT_REGISTER);
  }
  if (start_height == dptr->top_h - 1) {
    // only two input lines need to be copied
    COPY_INPUT_TO_VTCM_D4(0, 2);
    uint8_t *tmp_vtcm_input = vtcm_input + 2 * dptr->next_in_width_d4;
    std::fill((uint32_t *)tmp_vtcm_input, (uint32_t *)tmp_vtcm_input + dptr->bottom_w, tmp_pad_val);
    conv3x3s2d4_asm(vtcm_input, output, data);
  }
  dspCV_worker_pool_synctoken_jobdone(dptr->token);
}

需要注意的是,代码中的vtcm_input 其实是一块Intermediate Buffer,整段代码的第一步,是pads zeropoint,内容如下:

// padding vtcm_buf once in each thread; pad_right = 1 for conv with stride = 2 and kernel_size = 3
for (uint32_t h = 0; h < dptr->vtcm_height; h++) {
  uint8_t *tmp_vtcm_input = vtcm_input + h * dptr->next_in_width_d4 + dptr->bottom_w * d4;
  tmp_vtcm_input[0] = (uint8_t)dptr->quant_info->bottom_zp;
  tmp_vtcm_input[1] = (dptr->bottom_c > 1) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
  tmp_vtcm_input[2] = (dptr->bottom_c > 2) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
  tmp_vtcm_input[3] = (dptr->bottom_c > 3) ? (uint8_t)dptr->quant_info->bottom_zp : 0;
  if (h == 0) tmp_pad_val = *(uint32_t *) tmp_vtcm_input;
}

每个线程中只需要做vtcm_height=3次width方向的pads,而不是output_height * vtcm_height/num_workers=112 * 3 /4=84次,这样做的好处是可以大大减少数据读写的次数;另外,我们还会注意到一个变量 tmp_pad_val ,这个变量是为了便于后续pads 最后一行,如conv3x3s2d4_callback 的 #42行所示。

整段代码的第二步,是循环执行 COPY_INPUT_TO_VTCM_D4conv3x3s2d4_asm、L2FETCH ,代码如下:

for (; start_height < end_height; start_height++) {
  // copy from input to intermediate buffer
  uint8_t *tmp_vtcm_input = vtcm_input + dptr->bottom_w * d4;
  COPY_INPUT_TO_VTCM_D4(0, dptr->vtcm_height);
  conv3x3s2d4_asm(vtcm_input, output, data);
  input += dptr->stride_h * dptr->bottom_c * dptr->bottom_w;
  output += dpt
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值