OpenCL浮点数减少

时间:2013-12-16 14:15:15

标签: multithreading parallel-processing opencl race-condition reduction

我想对我的内核代码(1维数据)应用reduce:

__local float sum = 0;
int i;
for(i = 0; i < length; i++)
  sum += //some operation depending on i here;

我不想只有一个执行此操作的线程,而是希望有n个线程(n = length),最后有1个线程来计算总和。

在伪代码中,我希望能够写出这样的内容:

int i = get_global_id(0);
__local float sum = 0;
sum += //some operation depending on i here;
barrier(CLK_LOCAL_MEM_FENCE);
if(i == 0)
  res = sum;

有办法吗?

我的总和有竞争条件。

4 个答案:

答案 0 :(得分:7)

为了帮助您入门,您可以执行以下示例(see Scarpino)。在这里,我们还通过使用OpenCL float4数据类型来利用向量处理。

请记住,下面的内核会返回一些部分总和:每个本地工作组一个,返回主机。这意味着您必须通过将所有部分金额加回主机来执行最终总和。这是因为(至少在OpenCL 1.2中)没有屏障功能可以同步不同工作组中的工作项。

如果不希望对主机上的部分和求和,可以通过启动多个内核来解决这个问题。这引入了一些内核调用开销,但在某些应用程序中,额外的惩罚是可接受的或无关紧要的。要使用下面的示例执行此操作,您需要修改主机代码以重复调用内核,然后包含逻辑以在输出向量的数量低于本地大小后停止执行内核(详细信息留给您或查看{{ 3}})。

编辑:为输出添加了额外的内核参数。添加了点积来对float 4向量求和。

__kernel void reduction_vector(__global float4* data,__local float4* partial_sums, __global float* output) 
{
    int lid = get_local_id(0);
    int group_size = get_local_size(0);
    partial_sums[lid] = data[get_global_id(0)];
    barrier(CLK_LOCAL_MEM_FENCE);

    for(int i = group_size/2; i>0; i >>= 1) {
        if(lid < i) {
            partial_sums[lid] += partial_sums[lid + i];
        }
        barrier(CLK_LOCAL_MEM_FENCE);
    }

    if(lid == 0) {
        output[get_group_id(0)] = dot(partial_sums[0], (float4)(1.0f));
    }
}

答案 1 :(得分:2)

我知道这是一篇很老的文章,但是从我尝试过的所有内容来看,Bruce的答案都行不通,而Adam的答案由于全局内存使用和内核执行开销而效率低下。

Jordan对Bruce答案的评论是正确的,因为该算法在每次迭代中单元数不均的情况下都会崩溃。但是,它实际上是与在多个搜索结果中可以找到的相同的代码。

我为此花了几天的时间挠头,部分原因是我选择的语言不是基于C / C ++的事实,而且即使不是不可能在GPU上调试也很棘手。最终,我找到了一个可行的答案。

这是布鲁斯和亚当的答案的结合。它将源从全局内存复制到本地,然后通过将上半部分反复折叠到底部来减少,直到没有数据为止。

结果是一个缓冲区,其中包含与使用的工作组相同数量的项目(这样可以分解非常大的缩减量),该值必须由CPU求和,或者从另一个内核调用以执行此操作在GPU上的最后一步。

这部分让我有些烦恼,但是我相信,该代码还通过基本上按顺序读取本地内存来避免存储区切换问题。 **很乐意得到任何认识的人的确认。

注意:如果您的数据从零偏移开始,则可以从源中省略全局'AOffset'参数。只需将其从内核原型以及用作数组索引一部分的第四行代码中删除即可。

__kernel void Sum(__global float * A, __global float *output, ulong AOffset, __local float * target ) {
        const size_t globalId = get_global_id(0);
        const size_t localId = get_local_id(0);
        target[localId] = A[globalId+AOffset];

        barrier(CLK_LOCAL_MEM_FENCE);
        size_t blockSize = get_local_size(0);
        size_t halfBlockSize = blockSize / 2;
        while (halfBlockSize>0) {
            if (localId<halfBlockSize) {
                target[localId] += target[localId + halfBlockSize];
                if ((halfBlockSize*2)<blockSize) { // uneven block division
                    if (localId==0) { // when localID==0
                        target[localId] += target[localId + (blockSize-1)];
                    }
                }
            }
            barrier(CLK_LOCAL_MEM_FENCE);
            blockSize = halfBlockSize;
            halfBlockSize = blockSize / 2;
        }
        if (localId==0) {
            output[get_group_id(0)] = target[0];
        }
    }

https://pastebin.com/xN4yQ28N

答案 2 :(得分:1)

减少数据的一种简单快捷的方法是将数据的上半部分重复折叠到下半部分。

例如,请使用以下简单易懂的CL代码:

__kernel void foldKernel(__global float *arVal, int offset) {
    int gid = get_global_id(0);
    arVal[gid] = arVal[gid]+arVal[gid+offset];
}

使用以下Java / JOCL主机代码(或将其移植到C ++等):

    int t = totalDataSize;
    while (t > 1) {
        int m = t / 2;
        int n = (t + 1) / 2;
        clSetKernelArg(kernelFold, 0, Sizeof.cl_mem, Pointer.to(arVal));
        clSetKernelArg(kernelFold, 1, Sizeof.cl_int, Pointer.to(new int[]{n}));
        cl_event evFold = new cl_event();
        clEnqueueNDRangeKernel(commandQueue, kernelFold, 1, null, new long[]{m}, null, 0, null, evFold);
        clWaitForEvents(1, new cl_event[]{evFold});
        t = n;
    }

主机代码循环log2(n)次,因此即使使用大型数组也能快速完成。 “m”和“n”的小提琴是处理非二次幂阵列。

  • OpenCL可轻松实现任何GPU平台的并行化(即快速)。
  • 内存不足,因为它可以正常使用
  • 使用非二次幂数据大小有效工作
  • 灵活,例如你可以改变内核做“min”而不是“+”

答案 3 :(得分:0)

如果您支持OpenCL C 2.0功能,则可以使用新的work_group_reduce_add()函数在单个工作组内减少总和