并行前缀和 - 最快的实现

Posted

技术标签:

【中文标题】并行前缀和 - 最快的实现【英文标题】:Parallel prefix sum - fastest Implementation 【发布时间】:2012-04-07 10:21:21 【问题描述】:

我想使用 C++ 实现并行前缀和算法。我的程序应该采用输入数组x[1....N],它应该在数组y[N] 中显示输出。 (注意N的最大值为1000。)

到目前为止,我浏览了许多研究论文,甚至是***中的算法。 但我的程序还应该显示输出、步骤以及每个步骤的操作/指令。

我想要最快的实现,就像我想要最小化操作数量和步骤一样。

例如::

x = 1, 2, 3,  4,   5,   6,   7,  8  - Input
y = ( 1, 3, 6, 10, 15, 21, 28, 36) - Output

但是除了显示 y 数组作为输出之外,我的程序还应该显示每个步骤的操作。我也参考了这个帖子calculate prefix sum,但可以从中得到很多帮助。

【问题讨论】:

您的具体问题是什么?这似乎是一个非常简单的算法就足够了。 @ Niklas B::我实际上想要,我的程序应该使用最小步数和最小操作数。比如如果 N 是 1000,我的程序应该使用小于 20 的步数和小于 2100 的操作数。 尝试自己写一个!只需将数字相加即可。 @Niklas B :他想要“并行”前缀和算法。 你实现了***文章中关于并行前缀和的算法吗?如果是这样,请在此处或在ideone上发布,我们将帮助您完成“显示输出、步骤以及每个步骤的操作/说明”部分。 【参考方案1】:

这个问题的答案在这里:Parallel Prefix Sum (Scan) with CUDA 和这里:Prefix Sums and Their Applications。 NVidia 文章提供了使用 CUDA GPU 的最佳实现,卡内基梅隆大学 PDF 论文解释了该算法。我还使用 MPI 实现了 O(n/p) 前缀和,您可以在此处找到:In my github repo。

这是通用算法的伪代码(与平台无关):

示例 3. 工作效率高的和扫描算法的上扫(减少)阶段(Blelloch 1990 之后)

 for d = 0 to log2(n) – 1 do 
      for all k = 0 to n – 1 by 2^(d+1) in parallel do 
           x[k +  2^(d+1) – 1] = x[k +  2^d  – 1] + x[k +  2^(d+1) – 1]

示例 4. 高效并行和扫描算法的向下扫描阶段(Blelloch 1990 之后)

 x[n – 1] = 0
 for d = log2(n) – 1 down to 0 do 
       for all k = 0 to n – 1 by 2^(d+1) in parallel do 
            t = x[k +  2^d  – 1]
            x[k +  2^d  – 1] = x[k +  2^(d+1) – 1]
            x[k +  2^(d+1) – 1] = t +  x[k +  2^(d+1) – 1]

其中x是输入数据,n是输入的大小,d是并行度(CPU数量) .这是一个共享内存计算模型,如果它使用分布式内存,您需要向该代码添加通信步骤,就像我在提供的 Github 示例中所做的那样。

【讨论】:

很好的回应,但 x[k + 2^d +1 – 1] 应该是 x[k + 2^(d +1) – 1] 在这里和你链接到的 CUDA 文章(我相信他们给出的代码的图表 - 我相信下标符号是误丢在那里)。 你是对的。我查看了 Blelloch 的文章,似乎 NVidia 的文章不正确。 在上述算法的下扫阶段,假设n=10,对于d=2和k=8,索引k+2^​​d–1>n。对于 k+2^(d+1)–1>n 也是如此。这会导致应用程序核心转储。我们应该如何处理 n 不是 2 的幂的情况? 你需要用 0 填充它,所以它是 2 的幂。 我很难理解for all k = 0 to n – 1 by 2^(d+1) in parallel do 究竟做了什么。我得到了for all k = 0 to n – 1 部分,但by 2^(d+1) 是什么意思?我知道d 来自外部循环,所以可以肯定,我可以计算出2^(d+1),但是我应该怎么做,或者by 关键字是什么意思?如果您能澄清这一点,那就太好了。谢谢!【参考方案2】:

我只实现了数组中所有元素的总和(Blelloch 的向上扫描减少部分),而不是在 java/opencl 中使用 Aparapi (https://code.google.com/p/aparapi/) 实现的完整前缀总和。它可在https://github.com/klonikar/trial-aparapi/blob/master/src/trial/aparapi/Reducer.java 获得,它是针对一般块大小(在代码中称为 localBatchSize)而不是 2 编写的。我发现块大小为 8 最适合我的 GPU。

虽然实现有效(求和计算是正确的),但它的性能比顺序求和差得多。在我的core-i7(8核)CPU上,8388608(8MB)个数字的顺序求和大约需要12ms,GPU上的并行执行(NVidia Quadro K2000M with 384 cores)大约需要 100 毫秒。我什至已经优化为仅在计算后传输最终总和,而不是整个数组。如果没有这种优化,它会多花 20 毫秒。该实现似乎是根据@marcel-valdez-orozco 的回答中描述的算法。

【讨论】:

我还实现了比较开放 CL 和线程与直接顺序执行的其他算法,我发现英特尔酷睿 i7 CPU 已经做了一些重大改进。您还需要查看编译选项,如果您想要真正的顺序执行从编译中删除所有优化,即使这样也很难击败顺序求和,CPU 在计算总和方面非常有效,我不知道究竟是什么优化在运行时完成,使其如此之快。【参考方案3】:

下面的代码将完成这项工作

void prfxSum()

    int *x=0;
    int *y=0;
    int sum=0;
    int num=0;
    int i=0;

    cout << "Enter the no. of elements in input array : ";
    cin >> num;

    x = new int[num];
    y = new int[num];

    while(i < num)
    
        cout << "Enter element " << i+1 << " : ";
        cin >> x[i++];
    

    cout << "Output array :- " << endl;
    i = 0;
    while(i < num)
    
        sum += x[i];
        y[i] = sum;
        cout << y[i++] << ", ";
    

    delete [] x;
    delete [] y;

以下是执行时的输出

Enter the no. of elements in input array : 8
Enter element 1 : 1
Enter element 2 : 2
Enter element 3 : 3
Enter element 4 : 4
Enter element 5 : 5
Enter element 6 : 6
Enter element 7 : 7
Enter element 8 : 8
Output array :- 
1, 3, 6, 10, 15, 21, 28, 36

您可以通过从文件中提供数据来避免用户输入数组 x[] 的 1000 个元素。

【讨论】:

这到底是怎么一回事?这完全是顺序的。我不知道为什么 OP 接受了这个作为答案。

以上是关于并行前缀和 - 最快的实现的主要内容,如果未能解决你的问题,请参考以下文章

前缀和的并行化 (Openmp)

在 ORACLE 中搜索最长前缀的最快方法

OpenMP 并行前缀和加速

在不使用推力的情况下,每个线程具有多个元素的并行前缀总和

OpenMP 中的并行累积(前缀)总和:线程之间的通信值

Python - 通过列表中的前缀和后缀删除元组