CUDA流压缩算法

CUDA stream compaction algorithm

我正在尝试使用CUDA构造并行算法,该算法采用整数数组并删除所有0,而不考虑是否保留顺序。

示例:

全局内存:{0,0,0,0,14,0,0,17,17,0,0,0,0,13}

主机内存结果:{17,13,14,0,0,...}

最简单的方法是使用主机在O(n)时间内删除0。但是考虑到我周围有1000元素,将所有内容保留在GPU上并先进行压缩,然后再发送它可能会更快。

首选方法是创建设备上的堆栈,这样每个线程都可以弹出(按任何顺序)插入或退出堆栈。但是,我认为CUDA没有实现此功能。

等效的方法(但要慢得多)是继续尝试写入,直到所有线程完成写入为止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kernalRemoveSpacing(int * array, int * outArray, int arraySize) {
    if (array[threadId.x] == 0)
        return;

    for (int i = 0; i < arraySize; i++) {

         array = arr[threadId.x];

         __threadfence();

         // If we were the lucky thread we won!
         // kill the thread and continue re-reincarnated in a different thread
         if (array[i] == arr[threadId.x])
             return;
    }
}

此方法的唯一好处是我们将在O(f(x))时间内执行,其中f(x)是数组中存在的非零值的平均数量(对于我的实现,f(x) ~= ln(n)为时间,因此为O(ln(n)) ,但O常数高)

最后,诸如quicksort或mergesort之类的排序算法也可以解决该问题,并且实际上在相对时间O(ln(n))中运行。我认为甚至有一种算法可以比这更快,因为我们不需要浪费时间排序(交换)零零元素对和非零非零元素对(不需要保留顺序)。

So I'm not quite sure which method would be the fastest, and I still
think there's a better way of handling this. Any suggestions?


您要的是一种经典的并行算法,称为流压缩1。

如果选择"推力",则可以简单地使用thrust::copy_if。这是一个稳定的算法,它保留了所有元素的相对顺序。

草图:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <thrust/copy.h>

template<typename T>
struct is_non_zero {
    __host__ __device__
    auto operator()(T x) const -> bool {
        return T != 0;
    }
};

// ... your input and output vectors here

thrust::copy_if(input.begin(), input.end(), output.begin(), is_non_zero<int>());

如果没有选择"推力",则可以自己实现流压缩(有关该主题的文献很多)。这是一个有趣且相当简单的练习,同时也是更复杂的并行基元的基本构建块。

(1)严格来说,它不是传统意义上的流压缩,因为流压缩在传统上是一种稳定的算法,但您的要求不包括稳定性。放宽要求可能会导致更有效的实施?


有了这个答案,我只是想为Davide Spataro的方法提供更多细节。

如前所述,流压缩包括根据谓词除去集合中不需要的元素。例如,考虑整数数组和谓词p(x)=x>5,则将数组A={6,3,2,11,4,5,3,7,5,77,94,0}压缩为B={6,11,7,77,94}

流压缩方法的一般思想是将不同的计算线程分配给要压缩的数组的不同元素。每个此类线程都必须根据是否满足相关谓词来决定将其对应的元素写入输出数组。因此,流压缩的主要问题是让每个线程知道必须在输出数组中写入相应元素的位置。

[1,2]中的方法是上述Thrust copy_if的替代方法,包括三个步骤:

  • 步骤1。假设P为启动线程的数量,而N为要压缩的向量的大小,而N>P为。输入向量被划分为大小等于块大小的S子向量。利用__syncthreads_count(pred)块内在函数,该函数对满足谓词pred的块中的线程数进行计数。第一步的结果是,数组d_BlockCounts的每个元素的大小为N/P,其中包含与相应块中的谓词pred相符的元素数量。

  • 第2步。独占扫描操作在数组d_BlockCounts上执行。作为第二步的结果,每个线程都知道前面的块中有多少个元素写入一个元素。因此,它知道在哪里写入其相应元素的位置,但知道与其自身块相关的偏移量。

  • 第3步。每个线程使用扭曲内在函数计算所提到的偏移量,并最终写入输出数组。应当注意,步骤#3的执行与翘曲调度有关。因此,输出数组中的元素顺序不一定反映输入数组中的元素顺序。

  • 在以上三个步骤中,第二个步骤由CUDA Thrusta的exclusive_scan原语执行,并且在计算上比其他两个步骤的要求低得多。

    对于2097152元素的数组,与CUDA Thrusta的copy_if1.0ms相比,上述方法已在NVIDIA GTX 960卡上的0.38ms中执行。提到的方法似乎更快,原因有两个:
    1)专为支持扭曲固有元素的卡片量身定制;
    2)该方法不能保证输出顺序。

    请注意,我们还针对inkc.sourceforge.net上提供的代码测试了该方法。尽管后面的代码安排在单个内核调用中(它不使用任何CUDA Thrust原语),但与三内核版本相比,它的性能并不更好。

    完整的代码在这里可用,并且与原始Davide Spataro的例程相比略有优化。

    1
    2
    [1] M.Biller, O. Olsson, U. Assarsson, a€?Efficient stream compaction on wide SIMD many-core architectures,a€? Proc. of the Conf. on High Performance Graphics, New Orleans, LA, Aug. 01 - 03, 2009, pp. 159-166.
    [2] D.M. Hughes, I.S. Lim, M.W. Jones, A. Knoll, B. Spencer, a€?InK-Compact: in-kernel stream compaction and its application to multi-kernel data visualization on General-Purpose GPUs,a€? Computer Graphics Forum, vol. 32, n. 6, pp. 178-188, 2013.


    流压缩是一个众所周知的问题,编写了很多代码(Thrust,Chagg引用了两个在CUDA上实现流压缩的库)。

    如果您有一个相对较新的支持CUDA的设备,该设备支持__ballot的固有功能(计算cdapability> = 3.0),则值得尝试一个小的CUDA过程,该过程执行流压缩的速度比Thrust快得多。

    在这里找到代码和最小文档。
    https://github.com/knotman90/cuStreamComp

    以单内核方式使用投票功能来执行压缩。