【问题标题】:Upload data in shared memory for convolution kernel为卷积核上传共享内存中的数据
【发布时间】:2014-01-27 12:10:17
【问题描述】:

我在理解 cmets 中提到的批量加载时遇到了一些困难。为了计算像素中的卷积,大小为 5 的掩码必须以该特定像素为中心。图像被分成瓷砖。应用卷积掩码后的这些图块是最终输出图块,其大小为TILE_WIDTH*TILE_WIDTH。对于属于输出图块边界的像素,当该图块属于图像的边界时,掩码必须从相邻图块借用一些像素。否则,这些借用的值被分配为零。这两个步骤在

中描述
if (srcY >= 0 && srcY < height && srcX >= 0 && srcX < width)
    N_ds[destY][destX] = I[src];
else
    N_ds[destY][destX] = 0;

因此,共享内存数组的每一侧都有TILE_WIDTH + Mask_width - 1 维度。我不清楚代码的以下部分。

  1. destYdestX 索引。 将输出索引除以输入平铺宽度是什么意思?
  2. srcY 添加srcX 索引。 为什么destYdestX索引参与srcY添加srcX索引?

    srcY = blockIdx.y * TILE_WIDTH + destY - Mask_radius;

    srcX = blockIdx.x * TILE_WIDTH + destX - Mask_radius;

  3. 为什么在第二次加载中我们使用偏移量TILE_WIDTH * TILE_WIDTH
  4. 一般来说,有两个加载的直观解释是什么?
  5. 能否在所有这些问题之后根据下图给出一个直观的示例?
  6. 谢谢!

编辑:图片已添加。绿色是输出图块,红色是掩码以 114 索引为中心。很明显,面具借用了不同瓷砖的元素。 最后,这张图片指向一个频道。

示例:根据下图,我尝试编写了一个示例。基于destY=0destX=0,输出图块具有blockIdx.x=1blockIdx.y=1。还, srcY = 1*6+0-3=3srcX = 3src = (3*18+3)*3+0=171。根据计算和图像示例,我们没有匹配项。在第一个共享内存位置中,应该存储的值是具有全局索引57 的值。上述计算有什么问题?有人可以帮忙吗?

#define Mask_width  5
#define Mask_radius Mask_width/2
#define TILE_WIDTH 16
#define w (TILE_WIDTH + Mask_width - 1)
#define clamp(x) (min(max((x), 0.0), 1.0))

__global__ void convolution(float *I, const float* __restrict__ M, float *P,
                            int channels, int width, int height) {
   __shared__ float N_ds[w][w];
   int k;
   for (k = 0; k < channels; k++) {
      // First batch loading
      int dest = threadIdx.y * TILE_WIDTH + threadIdx.x,
         destY = dest / w, destX = dest % w,
         srcY = blockIdx.y * TILE_WIDTH + destY - Mask_radius,
         srcX = blockIdx.x * TILE_WIDTH + destX - Mask_radius,
         src = (srcY * width + srcX) * channels + k;
      if (srcY >= 0 && srcY < height && srcX >= 0 && srcX < width)
         N_ds[destY][destX] = I[src];
      else
         N_ds[destY][destX] = 0;

      // Second batch loading
      dest = threadIdx.y * TILE_WIDTH + threadIdx.x + TILE_WIDTH * TILE_WIDTH;
      destY = dest / w, destX = dest % w;
      srcY = blockIdx.y * TILE_WIDTH + destY - Mask_radius;
      srcX = blockIdx.x * TILE_WIDTH + destX - Mask_radius;
      src = (srcY * width + srcX) * channels + k;
      if (destY < w) {
         if (srcY >= 0 && srcY < height && srcX >= 0 && srcX < width)
            N_ds[destY][destX] = I[src];
         else
            N_ds[destY][destX] = 0;
      }
      __syncthreads();

      float accum = 0;
      int y, x;
      for (y = 0; y < Mask_width; y++)
         for (x = 0; x < Mask_width; x++)
            accum += N_ds[threadIdx.y + y][threadIdx.x + x] * M[y * Mask_width + x];
      y = blockIdx.y * TILE_WIDTH + threadIdx.y;
      x = blockIdx.x * TILE_WIDTH + threadIdx.x;
      if (y < height && x < width)
         P[(y * width + x) * channels + k] = clamp(accum);
      __syncthreads();
   }
}

【问题讨论】:

  • 好的,我认为它变得更加清晰了。我阅读了为了从 1D 索引到 2d,您执行以下操作:Y = index1D / 宽度,X = index1D % 宽度。我不知道。
  • 但它不除以TILE_WIDTH?相反,除以输入瓦片宽度,w。为什么会这样?
  • ...因为索引在元素中,而不是在图块中。
  • 我希望我能理解你。你是什​​么意思“在元素中”?通过将宽度为TILE_WIDTH 的索引方案与另一个平铺宽度分开,我们有什么转换?你能不能更直观一点。在过去的两天里,我试图在脑海中弄清楚这段代码,但缺少重要的知识或理解深度。谢谢。
  • 使用共享内存适应3D卷积:[这里][1] [1]:stackoverflow.com/questions/22577857/…

标签: cuda gpu


【解决方案1】:

您的问题在概念上与我在 StackOverflow 上的第一个问题相似:Moving a (BS_X+1)(BS_Y+1) global memory matrix by BS_XBS_Y threads

您面临以下问题:每个大小为TILE_WIDTHxTILE_WIDTH 的线程块应填充大小为(TILE_WIDTH + Mask_width - 1)x(TILE_WIDTH + Mask_width - 1) 的共享内存区域

4) 一般来说,有两个载荷的直观解释是什么?

由于共享内存区域(TILE_WIDTH + Mask_width - 1)x(TILE_WIDTH + Mask_width - 1)大于块大小TILE_WIDTHxTILE_WIDTH并假设它小于2xTILE_WIDTHxTILE_WIDTH,那么每个线程最多应该将两个元素从全局内存移动到共享内存。这就是为什么要进行两阶段加载的原因。

1) destYdestX 索引。输出索引除以输入瓦片宽度是什么意思?

这涉及到第一个加载阶段,它被指定从全局内存中加载TILE_WIDTHxTILE_WIDTH 元素并填充共享内存区域的最上部。

所以,操作

dest = threadIdx.y * TILE_WIDTH + threadIdx.x;

同时展平通用线程的二维坐标

destX = dest % w;
destY = dest / w; 

进行逆运算,计算通用线程相对于共享内存区域的二维坐标。

2) srcY 添加 srcX 索引。为什么destYdestX索引参与srcY添加srcX索引?

srcY = blockIdx.y * TILE_WIDTH + destY - Mask_radius;

srcX = blockIdx.x * TILE_WIDTH + destX - Mask_radius;

(blockIdx.x * TILE_WIDTH, blockIdx.y * TILE_WIDTH) 将是全局内存位置的坐标,如果块大小和共享内存大小相同。由于您也是从邻居瓷砖“借用”内存值,因此您必须将上述坐标移动(destX - Mask_radius, destY - Mask_radius)

3) 为什么在第二次加载中我们使用偏移量TILE_WIDTH * TILE_WIDTH?

您有这个偏移量是因为在第一个内存阶段,您已经填充了共享内存的“第一个”TILE_WIDTHxTILE_WIDTH 位置。

编辑

下图说明了扁平化线程索引dest与共享内存位置的对应关系。在图片中,蓝色框表示通用图块的元素,而红色框表示相邻图块的元素。蓝色和红色框的并集对应于整体共享内存位置。可以看到,一个线程块的所有256线程都参与了填充绿色线以上的共享内存的上部,而只有145参与了填充绿色线以下的共享内存的下部线。现在你也应该了解TILE_WIDTH x TILE_WIDTH 偏移量了。

请注意,由于参数的特殊选择,每个线程最多有2 内存负载。例如,如果你有TILE_WIDTH = 8,那么一个线程块中的线程数是64,而共享内存大小是12x12=144,这意味着每个线程至少负责执行 /em> 2 共享内存自 144/64=2.25 开始写入。

【讨论】:

  • @JackOLantem 感谢您的回答!我会尽快带着 cmets 回来:) 再次感谢。
  • 4) 中,您提到每个“线程最多从全局内存中移动两个元素”。你能举个例子吗?我不确定我是否理解它。在1) 中,您提到“全局内存中的TILE_WIDTHxTILE_WIDTH 元素并填充共享内存区域的最上部”,在上方的网格中是哪些元素?在3) 中,您提到了“第一个”TILE_WIDTHxTILE_WIDTH“位置。您介意为此也提供一个算术示例吗?您的帮助对于理解这些概念非常宝贵!谢谢!
  • 你好,图片不见了:)
  • @Thoth 我很抱歉。我解决了这个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-31
  • 2020-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多