我正在使用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);
为了简洁起见,我省略了clSetKernelArg
,clCreateBuffer
和其他OpenCL调用。请注意,在执行此内核之前,我还有另一个内核将GPU上的输出d_s
数组归零。
我很难理解GPU上的线程如何运行以达到此结果。任何有关这方面的见解将不胜感激。
答案 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;
}
这绝不是计算输出的最快方法,但它会为您提供正确的结果。通过仅对输出的每个元素写入一次来解决遍历条件写入全局存储器的问题。
有很多改进空间
可以避免检查,因为您知道坐标将落在输入图像的范围内。内核的其余部分保持不变。
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)
以下是您询问的“行和列”方法。我们的想法是同时处理输出图像的非重叠区域。
我使用的16x16像素示例可以扩展到任何图像大小。每个黑框代表一个像素。不同的蓝色阴影仅用于区分工作项的区域以进行计算。
如果将图像划分为两像素宽的列,则可以在单独的线程中处理每一列。这可以防止列之间的全局写入冲突。重复奇数列 - 即dx = 1,以计算第二张图像显示的与列重叠的2x2分组。此方法仅防止水平方向上的写入冲突,但您仍需要考虑行。
为避免列内的写入冲突,您需要进一步将列划分为多行,如上图所示。让我们有一个工作项计算单个2x2输出,以使内核尽可能简单。行也分两个阶段计算--dy = 0,dy = 1。总的来说,这是一个四步算法,其中每个步骤都可以称为“令人尴尬的并行”。这些步骤可以通过它们与原点的位置偏移来引用:(dx,dy)= A(0,0),B(1,0),C(1,0)和D(1,1)。请注意,这些组不一定使用相同数量的工作项,具体取决于最终行/列是否足够大以进行计算。
显示内核调用期间两个示例工作项负责的图示。 “行和列”基本上变成了“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)将输出缓冲区读回主机
请注意,在完成所有内核执行之前,不需要将输出复制到主机。
<强>优点:强>
<强>缺点:强>
你去吧。请告诉我这是如何为您解决的,如果我需要进行任何更正。