RoIAlign/RoIPooling CUDA源码解读

程序来自mmdetection的源码,和原版的roialign有所改动,但是效果一样。此外修复了mmdetction部分冗余代码。

mmdetection代码库注释:

本文的注释代码上传至(包括RoIPooling):

CUDA的部分

这里宏定义函数CUDA_1D_KERNEL_LOOP(i, n),表示线程数大于当前grid开启上限时,一直在block中循环线程计算直到完成任务。后面会传入参数实例化;
当前开辟的所有线程数是blockDim.x * gridDim.x ;
当需要并行的任务总数超过了当前开辟的所有线程数时,可以让线程循环的完成任务。一种常见的用法;
比如,一共开辟了5*2共十个线程,一共有30个任务,0号线程在干完任务0后,可以继续干任务0+10,之后可以继续干任务0+10+10;
同理1号线程可以按顺序去做任务1,11,21。

#define CUDA_1D_KERNEL_LOOP(i, n)                            \
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; \
       i += blockDim.x * gridDim.x)

每个block开辟的线程数1024;

#define THREADS_PER_BLOCK 1024

双线性插值

template <typename scalar_t
/* scalar_t:  是一个宏,特化的时候会传入具体的类型。
bottom_data:需要进行roialign的featuremap的首地址指针(depth=1),注意特征图是(h*w)的一维数组。(关于指针和数组调用关系,参加forward函数开始的注释)
height/width:特征图的高宽
xy : 要差值的点的坐标
*/
__device__ scalar_t bilinear_interpolate(const scalar_t *bottom_data,
                                         const int height, const int width,
                                         scalar_t y, scalar_t x) {
  // deal with cases that inverse elements are out of feature map boundary
  if (y < -1.0 || y > height || x < -1.0 || x > width) {
    return 0;
  }

  if (y <= 0) y = 0;
  if (x <= 0) x = 0;
 
  int y_low = (int)y;
  int x_low = (int)x;
  int y_high;
  int x_high;
  // 避免越界
  if (y_low >= height - 1) {
    y_high = y_low = height - 1;
    y = (scalar_t)y_low;
  } else {
    y_high = y_low + 1;
  }

  if (x_low >= width - 1) {
    x_high = x_low = width - 1;
    x = (scalar_t)x_low;
  } else {
    x_high = x_low + 1;
  }

  // w1-w4分别是双线性插值公式四项的权重
  scalar_t ly = y - y_low;  // 采样点到下边距离
  scalar_t lx = x - x_low;  // 采样点到左边距离
  scalar_t hy = 1. - ly;    // 采样点到上边距离
  scalar_t hx = 1. - lx;    // 采样点到右边距离
  // do bilinear interpolation 

  scalar_t lt = bottom_data[y_low * width + x_low];
  scalar_t rt = bottom_data[y_low * width + x_high];
  scalar_t lb = bottom_data[y_high * width + x_low];
  scalar_t rb = bottom_data[y_high * width + x_high];
  scalar_t w1 = hy * hx, w2 = hy * lx, w3 = ly * hx, w4 = ly * lx;

  scalar_t val = (w1 * lt + w2 * rt + w3 * lb + w4 * rb);

  return val;
}

前向传播

template <typename scalar_t>
/* 
---------参数--------
nthreads:     线程总数。
bottom_data:  同上,需要进行roialign的featuremap的首地址
bottom_rois:  存储rois的首地址
spatial_scale:特征图和原图之间的比例。特征图的height/原图的height
sample_num:   采样点数
height/width: 特征图尺寸
pooled_height/pooled_width: 一般是7
top_data:pooling结果的首地址,最后的结果存储在这里。
*/
__global__ void ROIAlignForward(const int nthreads, const scalar_t *bottom_data,
                                const scalar_t *bottom_rois,
                                const scalar_t spatial_scale,
                                const int sample_num, const int channels,
                                const int height, const int width,
                                const int pooled_height, const int pooled_width,
                                scalar_t *top_data) {
  // 用函数宏定义中的内容代替,即index代替for循环中的i,nthreads代替for循环中的n
  // 表示会线程数大于当前grid开启上限时,一直在block中循环线程计算直到完成任务
  // 具体:pooling后的所有RoI像素点总数量进行同步/循环的计算,每各单独计算核单次求取一个点的坐标
  CUDA_1D_KERNEL_LOOP(index, nthreads) {  
    // (n, c, ph, pw) is an element in the aligned output

    int pw = index % pooled_width;
    int ph = (index / pooled_width) % pooled_height;
    int c = (index / pooled_width / pooled_height) % channels;
    int n = index / pooled_width / pooled_height / channels;

    /*
    offset_bottom_bottom_rois以5位单位
    0位置放当前roi属于当前batch中的第几张图片(从0开始排序),也就是batch_index
    注意缩放:1-4位置放当前roi左上角,右下角坐标,针对 真实图像大小而言的,所以需要通过spatial_scale  缩放!!;
             spatial_scale乘子将roi坐标缩放到featuremap后,是float型,无量化损失!!!
    */

    /* 
    第一行关于指针:
    定义一个指向bottom_rois + n * 5位置的指针,指向第n个roi的首地址,其有五个参数bs x1 y1 x2 y2;
    可以直接将指针作为新的数组索引,0从当前所指的位置开始
    */
    const scalar_t *offset_bottom_rois = bottom_rois + n * 5;   
    int roi_batch_ind = offset_bottom_rois[0];    // 指针指向的地址内容进行取出
    scalar_t roi_start_w = offset_bottom_rois[1] * spatial_scale;   // 这个xyxy是ROI的坐标是要align的区域,float型
    scalar_t roi_start_h = offset_bottom_rois[2] * spatial_scale;   // 分别是左上点和右下点的坐标(不是像素)
    scalar_t roi_end_w = (offset_bottom_rois[3] + 1) * spatial_scale;  // 这里+1应该纯粹为了避免重叠
    scalar_t roi_end_h = (offset_bottom_rois[4] + 1) * spatial_scale;

    // Force malformed ROIs to be 1x1
    scalar_t roi_width = fmaxf((scalar_t)roi_end_w - roi_start_w, 0.);  // roi区域宽度,float,无损失(与0比较以取正值)
    scalar_t roi_height = fmaxf((scalar_t)roi_end_h - roi_start_h, 0.); // roi区域高度

    scalar_t bin_size_h = roi_height / pooled_height; // 划分成多个bin,每个bin的高和宽
    scalar_t bin_size_w = roi_width / pooled_width;

 
    const scalar_t *offset_bottom_data =
        bottom_data + (roi_batch_ind * channels + c) * height * width;  

    int sample_num_h = (sample_num > 0)   // 三目运算,设置了>0的sample_num,那么x方向取这么多个点
                           ? sample_num
                           : ceil(roi_height / pooled_height);  // e.g., = 2
    int sample_num_w =    
        (sample_num > 0) ? sample_num : ceil(roi_width / pooled_width);  // y方向同理,总共2*2=4个采样点

    // 下面四行代码更本没用,就是抄人的挪过来忘了删掉,它实现的方式插值都形式不一样,这四个变量完全没必要
    scalar_t h = (scalar_t)(ph + 0.5) * bin_size_h + roi_start_h; // h/w是bin中心点在特征图的坐标(也就是bottom)
    scalar_t w = (scalar_t)(pw + 0.5) * bin_size_w + roi_start_w;
    int hstart = fminf(floor(h), height - 2); //和width-2比较取较小值,是因为现在求的是左上角,要给右下角留下位置,不能让右下角超出featuremap范围
    int wstart = fminf(floor(w), width - 2);

    scalar_t output_val = 0;
    // y方向遍历
    for (int iy = 0; iy < sample_num_h; iy++) {
      // 计算采样点的y坐标:roi的h + bin的位置(如:7*7的第几个bin)+ bin内的偏移(bin宽高除以采样点个数)
      const scalar_t y = roi_start_h + ph * bin_size_h +
                         (scalar_t)(iy + scalar_t(.5f)) * bin_size_h /
                             (scalar_t)(sample_num_h);
      // x方向遍历采样
      for (int ix = 0; ix < sample_num_w; ix++) {
        // 计算采样点x坐标,原理相同不赘述
        const scalar_t x = roi_start_w + pw * bin_size_w +
                           (scalar_t)(ix + scalar_t(.5f)) * bin_size_w /
                               (scalar_t)(sample_num_w);
        scalar_t val = bilinear_interpolate<scalar_t>(offset_bottom_data,
                                                      height, width, y, x);  // 双线性插值得到结果
        output_val += val;
      }
    }
    output_val /= (sample_num_h * sample_num_w);    // 这里的align取值方式是均值
    // 最终Roi上的这个点插值计算完毕赋值即可
    top_data[index] = output_val;
  }
}

反向传播

原理类似不赘述,见github的注释。

发布于 2019-07-25 16:55