CUDA: 如何在GPU内将数组所有元素求和为一个数字?

9
首先,让我声明我充分意识到我的问题已经被问过了:CUDA中的块约简 然而,正如我希望表明的那样,我的问题是对那个问题的补充,并且我有特殊的需求,使得那个OP找到的解决方案不合适。
那么,让我解释一下。在我的当前代码中,我在while循环的每次迭代中运行一个Cuda内核,对一个数组的值进行一些计算。例如,可以将其视为以下内容:
int max_iterations = 1000;
int iteration = 0;
while(iteration < max_iterations)
{
    __global__ void calcKernel(int* idata, int* odata)
    {
        int i = blockIdx.x*blockDim.x + threadIdx.x;
        if (i < n)
        {
            odata[i] = (idata[i] + 2) * 5;
        }
    }

    iteration++;
}

然而,接下来我需要为GPU执行看似困难的任务。在调用内核的while循环的每次迭代中,我必须对odata生成的所有值求和,并将结果保存在一个名为result的int数组中,该数组的位置对应于当前迭代。这必须在内核内完成,或者至少仍然在GPU中完成,因为由于性能限制,只能在所有迭代完成后才能检索result数组。一个错误的天真尝试可能看起来像以下内容:
int max_iterations = 1000;
int iteration = 0;
while(iteration < max_iterations)
{
    __global__ void calcKernel(int* idata, int* odata, int* result)
    {
        int i = blockIdx.x*blockDim.x + threadIdx.x;
        if (i < n)
        {
            odata[i] = (idata[i] + 2) * 5;
        }
    }

    result[iteration] = 0;
    for(int j=0; j < max_iterations; j++)
    {
        result[iteration] += odata[j];            
    }

    iteration++;
}

当然,由于GPU将代码分配到线程中,上面的代码无法工作。为了学习如何正确地做到这一点,我一直在阅读本站关于使用CUDA进行数组缩减的其他问题。特别是,我发现提到了一份非常好的NVIDIA有关此主题的pdf文档,该文档也在我之前提到的SO问题中讨论:http://developer.download.nvidia.com/compute/cuda/1.1-Beta/x86_website/projects/reduction/doc/reduction.pdf 然而,虽然我完全理解这些幻灯片中描述的代码步骤以及一般优化,但我不明白如果代码实际输出一个完整的(尺寸不确定的)数组,那种方法如何将一个数组逐个求和并缩减为一个数字。请问有人能够为我解答并举例说明它是如何工作的吗?(即如何从输出数组中获取一个数字)?
现在,回到我之前提到的那个问题(Block reduction in CUDA)。请注意,其被接受的答案仅建议阅读我上面链接的pdf文件 - 该文件没有讨论如何处理代码生成的输出数组。在评论中,该问题的OP提到他/她能够通过在CPU上对输出数组求和来完成任务 - 这是我无法做到的,因为这意味着每次while循环迭代都要下载输出数组。最后,在该链接的第三个答案中,建议使用库来完成此操作 - 但我有兴趣学习本地的实现方式。
或者,我也非常感兴趣听取关于如何实现我所描述的其他建议。

您是否考虑使用多个内核,而不是单个内核? - Ricardo Ortega Magaña
@RicardoOrtegaMagaña 当然可以,只要我不在GPU内存之间传输,调用多个内核本身不会成为问题。 - user123443563
1
我建议使用两个内核,一个用于第一次计算,另一个内核仅用于加法,并将循环放在主程序中。我需要更多关于您的程序如何工作的信息,但根据您向我们展示的代码,这可能是解决您问题的简单方法。 - Ricardo Ortega Magaña
@RicardoOrtegaMagaña 我现在正在尝试这样做,但我在最后一步遇到了困难。这与我在链接的问题中最初提出的完全相同(但是在CPU端已经解决)。也就是说,我现在正在使用内核来进行缩减,但我最终得到的是一个数组,而不是一个单独的数字,我希望在GPU内进一步将这个最终数组缩减为一个单独的数字。 - user123443563
在我给你的例子中,结果在数组的第一个元素上,如果你想要它在单个变量(内存位置)中,就在内核中发送一个指针,并将该值分配到你想要该值的位置,这样,当你想要从主机或设备读取它时,只需提供该指针即可。 - Ricardo Ortega Magaña
你会考虑使用“Thrust” API 来为你做加法吗?它将功能解耦,易于使用。根据我的经验,在 GPU 上进行 API 调用不会太耗时。数十微秒就可以完成任务。 - WDC
2个回答

8
你已经找到了有关块并行规约的规范信息,因此我不会重复。如果您不想自己编写大量新代码来执行此操作,我建议查看CUB库的block_reduce实现链接1,该库通过向现有内核添加约4行代码提供了最佳的块规约操作。
在这里,对于真正的问题,如果您像这样做,就可以做您想做的事情:
__global__ void kernel(....., int* iter_result, int iter_num) {

    // Your calculations first so that each thread holds its result

    // Block wise reduction so that one thread in each block holds sum of thread results

    // The one thread holding the adds the block result to the global iteration result
    if (threadIdx.x == 0)
        atomicAdd(iter_result + iter_num, block_ressult);
}

关键在于使用原子函数来安全地更新内核运行结果,而不会出现内存竞争。在运行内核之前,绝对必须初始化iter_result,否则代码将无法工作,但这是基本的内核设计模式。

非常有见地。我刚刚成功地实现了它并且它运行良好。在我的情况下唯一的缺点是我正在使用字符,而atomicAdd不适用于字符,所以我不得不转换相关部分,浪费了一些内存和性能。尽管如此,在纯比较“atomicAdd”与“仅将块结果保存在输出数组中”的情况下,我惊讶于使用atomicAdd时损失的性能很少。 - user123443563
@talonmies 对于非常大的数组,使用归约内核的递归或迭代方式在内核的一次遍历/迭代后比原子加法更快,对吗?假设我有一个包含262144个元素的数组,我的每个块的线程数为256,块的线程数为1024。因此,我们将首先从每个块的输出中收集索引0处的1024个值,然后对这1024个元素进行某些操作。如果使用原子操作,那么相对于再次减少这1024个元素,它不总是会更慢吗?也许user123443563也可以提供指导。(我是CUDA的新手,如果问题太基础请见谅)。 - Mashhood Ahmad

5

如果您将2个连续的数字相加,并将结果保存在其中任何一个保存这些数字的插槽中,那么您只需要运行多次相同的内核,就可以不断将数组的总和减少2的幂,就像这个例子中一样:

用于求和的数组:

[·1,·2,·3,·4,·5,·6,·7,·8,·9,·10]

首先运行n/2个线程,对连续的数组元素求和,并将其存储在每个元素“左侧”,数组现在看起来像:

[·3,2,·7,4,·11,6,·15,8,·19,10]

运行同样的内核,运行n/4个线程,现在将每个2个元素相加,并将结果存储在最左边的元素中,数组现在看起来像这样:

[·10,2,7,4,·26,6,15,8,·19,10]

运行相同的内核,运行n/8个线程,现在将每4个元素相加,并存储在数组中最左侧的元素中,以得到:
[·36,2,7,4,26,6,15,8,·19,10]

运行最后一次,使用单个线程将每8个元素相加,并存储在数组中最左边的元素中,得到以下结果:
[55,2,7,4,26,6,15,8,19,10]

以这种方式,您只需使用一些线程作为参数运行内核,即可在结束时获得redux,在第一个元素(55)中查看“点”(·)以查看数组中“活动”的元素以将它们加起来,每次运行。


谢谢你的回答。虽然我理解这个逻辑,但我不知道如何实现它。你能给我一个简单的例子吗?看起来你说的正是在问题中链接的那个 PDF 中提到的东西。 - user123443563

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接