如何解释这个GPU计算?

时间:2015-02-04 22:48:21

标签: image-processing opencl gpgpu

我正在使用OpenCL在GPU上实现2D图像处理内核。我的GPU得到了非常令人费解的结果。该代码使用2X2模板并计算模板中每个输入样本的平均值,并将计算出的平均值添加到模板内的输出图像中的每个样本。

以下是CPU的代码:

for(int i2 = 1; i2 < n1; ++i2) {
  for(int i1 = 1; i1 < n2; ++i1) {
    r00 = h_r[i2  ][i1  ];
    r01 = h_r[i2  ][i1-1];
    r10 = h_r[i2-1][i1  ];
    r11 = h_r[i2-1][i1-1];
    rs = 0.25f*(r00+r01+r10+r11);
    s[i2  ][i1  ] += rs;
    s[i2  ][i1-1] += rs;
    s[i2-1][i1  ] += rs;
    s[i2-1][i1-1] += rs;
  }    
}    

使用

2 2 2 2
2 2 2 2
2 2 2 2
2 2 2 2

作为输入图像,我在应用此内核后得到以下输出图像:

2 4 4 2
4 8 8 4
4 8 8 4
2 4 4 2

在我的OpenCL实现中,我有以下内核:

_kernel void soSmoothingNew(__global const float* restrict d_r,
                            __global float* restrict d_s,  
                            int n1, 
                            int n2,)
{
  int g1 = get_global_id(0);
  int g0 = get_global_id(1);

  int i1 = g1+1; 
  int i2 = g0+1; 

  if (i1 >= n2) return;
  if (i2 >= n1) return;

  float r00, r01, r10, r11, rs;
  r01 = d_r[i2*n2+(i1-1)];
  r10 = d_r[(i2-1)*n2 + i1];
  r11 = d_r[(i2-1)*n2 + (i1-1)];
  rs = 0.25f*(r00+r01+r10+r11);
  d_s[i2*n2 + i1] += rs;
  d_s[i2*n2 + (i1-1)] += rs;
  d_s[(i2-1)*n2 + i1] += rs;
  d_s[(i2-1)*n2 + (i1-1)] += rs;

}

结果输出为:

2 2 2 2 
4 6 6 4 
4 6 6 4 
2 4 4 2 

我正在使用以下代码从主机执行内核:

size_t local_group_size[2] = {4,4};
size_t global_group_size_block[2] = {ceil((n1/local_group_size[0]) + 1) * local_group_size[0],
  ceil((n2/local_group_size[1]) + 1) * local_group_size[1]}; 

err = clEnqueueNDRangeKernel(queue, kernel1, 2, NULL, global_group_size_block, local_group_size, 0, NULL, NULL);

为了简洁起见,我省略了clSetKernelArgclCreateBuffer和其他OpenCL调用。请注意,在执行此内核之前,我还有另一个内核将GPU上的输出d_s数组归零。

我很难理解GPU上的线程如何运行以达到此结果。任何有关这方面的见解将不胜感激。

2 个答案:

答案 0 :(得分:3)

正如提到的void_ptr,在写回输出时,你的问题肯定是竞争条件。解决此问题的一种简单方法是让每个工作项负责完全计算其输出像素。您的算法也可以在此过程中简化。

每个像素位于四个2x2像素的盒子内,构成图像的3x3区域。

O O O
O X O  X is the one we want to compute with a work item.
O O O

2x2区域可以称为As,Bs,Cs和Ds:

A A O  O B B  O O O  O O O
A A O  O B B  C C O  O D D
O O O  O O O  C C O  O D D

您可能已经注意到,在使用原始算法时,X像素是4个单独平均值的一部分。结果像素包含正好1.0f * X.每个周围像素可以用类似的方式加权,并使用这个掩码添加到最终输出值:

0.25  0.50  0.25
0.50  1.00  0.50
0.25  0.50  0.25

这些值有助于到达下面的内核。

_kernel void soSmoothing(__global const float* restrict d_r, __global float* restrict d_s, int n1, int n2)
{
    //(idX,idY) represents the position to compute and write to d_s
    int idX = get_global_id(0);
    int idY = get_global_id(1);

    //using zero-base indices  
    if(idX >= n1) return;
    if(idY >= n2) return;

    float outValue = 0.0f;

    if(idX > 0){
        outValue += 0.50f * d_r[idX-1 + idY*n1]  + 0.25 * d_r[idX + idY*n1];
        if (idY > 0){
            outValue += 0.25f * d_r[idX-1 + (idY-1)*n1];
        }
        if (idY < (n2-1)){
            outValue += 0.25f * d_r[idX-1 + (idY+1)*n1];
        }
    }

    if(idX < (n1-1)){
        outValue += 0.50f * d_r[idX+1 + idY*n1] + 0.25 * d_r[idX + idY*n1];
        if (idY > 0){
            outValue += 0.25f * d_r[idX+1 + (idY-1)*n1];
        }
        if (idY < (n2-1)){
            outValue += 0.25f * d_r[idX+1 + (idY+1)*n1];
        }
    }

    if (idY > 0){
        outValue += 0.50f * d_r[idX + (idY-1)*n1] + 0.25 * d_r[idX + idY*n1];
    }
    if (idY < (n2-1)){
        outValue += 0.50f * d_r[idX + (idY+1)*n1] + 0.25 * d_r[idX + idY*n1];
    }

    d_s[idX + idY*n1] = outValue;
}

这绝不是计算输出的最快方法,但它会为您提供正确的结果。通过仅对输出的每个元素写入一次来解决遍历条件写入全局存储器的问题。

有很多改进空间

  1. 它不是最佳的一个重要原因是每个像素平均读取9次 - 一次是每个工作项周围,一次是自己的工作项。如果您遇到此问题,我可以发布更新以解决此问题。
  2. 使此运行更快的第二种方法是避免条件检查。您可以通过在输入周围添加1像素黑色边框来实现此操作,并在x和y轴上调整坐标偏移量为+1 in。我以下面的'+ n1 + 1'的形式做了偏移。
  3. 可以避免检查,因为您知道坐标将落在输入图像的范围内。内核的其余部分保持不变。

    float outValue = d_r[idX + idY*n1 + n1+1];
    outValue += 0.50f * d_r[idX-1 + idY*n1 + n1+1];
    outValue += 0.25f * d_r[idX-1 + (idY-1)*n1 + n1+1];
    outValue += 0.25f * d_r[idX-1 + (idY+1)*n1 + n1+1];
    outValue += 0.50f * d_r[idX+1 + idY*n1 + n1+1];
    outValue += 0.25f * d_r[idX+1 + (idY-1)*n1 + n1+1];
    outValue += 0.25f * d_r[idX+1 + (idY+1)*n1 + n1+1];
    outValue += 0.50f * d_r[idX + (idY-1)*n1 + n1+1];
    outValue += 0.50f * d_r[idX + (idY+1)*n1 + n1+1];
    

答案 1 :(得分:1)

以下是您询问的“行和列”方法。我们的想法是同时处理输出图像的非重叠区域。

Work items groups into columns

我使用的16x16像素示​​例可以扩展到任何图像大小。每个黑框代表一个像素。不同的蓝色阴影仅用于区分工作项的区域以进​​行计算。

如果将图像划分为两像素宽的列,则可以在单独的线程中处理每一列。这可以防止列之间的全局写入冲突。重复奇数列 - 即dx = 1,以计算第二张图像显示的与列重叠的2x2分组。此方法仅防止水平方向上的写入冲突,但您仍需要考虑行。

Making sure to separate the work items row-wise

为避免列内的写入冲突,您需要进一步将列划分为多行,如上图所示。让我们有一个工作项计算单个2x2输出,以使内核尽可能简单。行也分两个阶段计算--dy = 0,dy = 1。总的来说,这是一个四步算法,其中每个步骤都可以称为“令人尴尬的并行”。这些步骤可以通过它们与原点的位置偏移来引用:(dx,dy)= A(0,0),B(1,0),C(1,0)和D(1,1)。请注意,这些组不一定使用相同数量的工作项,具体取决于最终行/列是否足够大以进行计算。

Example work item geography

显示内核调用期间两个示例工作项负责的图示。 “行和列”基本上变成了“4个棋盘”。下面是内核,它将计算2x2像素组的平均值并添加全局缓冲区。只要每个组A,B,C和D都由它自己执行,就不需要检查全局写冲突。

_kernel void soSmoothing(__global const float* restrict inData, __global float* restrict outData, int width, int height, int dx, int dy)
{
    //(idX,idY) represents the position to compute and write to outData
    int idX = get_global_id(0);
    int idY = get_global_id(1);

    //(pixelX, pixelY) is the top-left corner of the square to compute
    int pixelX = idX *2 +dx;
    int pixelY = idY *2 +dy;

    if(pixelX > (width-2)) return;
    if(pixelY > (height-2)) return;

    //do the math and add to the corresponding addresses in outData
    int topLeftAddress = pixelX + pixelY * width;
    float avg = 0.25f * (inData[topLeftAddress] + inData[topLeftAddress +1] + inData[topLeftAddress + width] + inData[topLeftAddress + width +1]);
    outData[topLeftAddress] = outData[topLeftAddress] + avg;
    outData[topLeftAddress +1] = outData[topLeftAddress +1] + avg;
    outData[topLeftAddress +width] = outData[topLeftAddress +width] + avg;
    outData[topLeftAddress +width +1] = outData[topLeftAddress +width +1] + avg;
}

主持人代码的步骤:

1)将命令队列设置为阻塞

2)设置缓冲区:1x输入,1x输出

3)将输入缓冲区复制到设备,在设备上将输出初始化为0

4a)用dx = 0,dy = 0

将内核排入队列

4b)用dx = 1,dy = 0

将内核排入队列

4c)用dx = 0,dy = 1

将内核排入队列

4d)用dx = 1,dy = 1

将内核排入队列

5)将输出缓冲区读回主机

请注意,在完成所有内核执行之前,不需要将输出复制到主机。

<强>优点:

  • 所有4个内核运行的相同输入意味着您可以在最多4个独立设备上轻松运行它,并在主机上汇总它们的输出。也许尝试在cpu上运行1个内核,在gpu上运行3个内核。您还可以分解内核以处理图像的较小区域,并根据需要使用尽可能多的opencl设备。转移的开销最终将成为一个主要的瓶颈,因此这可能不值得付出努力。
  • 简单的内核,易于调试。
  • 内核被“链接”,因此它们会添加到上一个输出中。可以按任何顺序运行。
  • 内核(+主机代码)可以轻松扩展,以支持3x3或其他单元维度。

<强>缺点:

  • 输入图像读取4x
  • 输出像素写入4x
  • 每个工作项一次只计算4个像素。计算更大的区域可能更快,但代码复杂度更高。

你去吧。请告诉我这是如何为您解决的,如果我需要进行任何更正。