我正在尝试使用CUDA构建并行算法,该算法采用整数数组并删除所有0
,无论是否保持顺序。
示例:
全球记忆:{0,0,0,0,14,0,0,17,0,0,0,0,13}
主机内存结果:{17,13,14,0,0,...}
最简单的方法是使用主机在0
时间内移除O(n)
。但考虑到我有大约1000
个元素,在发送它之前将所有内容留在GPU上并首先压缩它可能会更快。
首选方法是创建一个设备上的堆栈,这样每个线程都可以弹出和推送(以任何顺序)进入或离开堆栈。但是,我不认为CUDA有这个实现。
等效(但速度要慢得多)的方法是继续尝试写入,直到所有线程都写完为止:
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))
相对时间内运行。我认为可能存在比这更快的算法,因为我们不需要浪费时间排序(交换)零零元素对和非零非零元素对(不需要保持顺序)。
所以我不太确定哪种方法最快,我还是 认为有更好的方法来处理这个问题。有什么建议吗?
答案 0 :(得分:8)
您要求的是一种称为流压缩 1 的经典并行算法。
如果Thrust是一个选项,您可以简单地使用thrust::copy_if
。这是一个稳定的算法,它保留了所有元素的相对顺序。
粗略草图:
#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>());
如果Thrust 不是选项,您可以自己实现流压缩(有很多关于该主题的文献)。这是一个有趣且相当简单的练习,同时也是更复杂的并行原语的基本构建块。
(1) 严格地说,传统意义上它不是完全流压缩,因为流压缩传统上是一种稳定的算法,但是你的要求不包括稳定性。这个宽松的要求可能会导致更有效的实施?
答案 1 :(得分:2)
通过这个答案,我只是想为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步。每个线程使用warp内部函数计算所提到的偏移量,并最终写入输出数组。应注意,步骤#3的执行与扭曲调度有关。因此,输出数组中的元素顺序不一定反映输入数组中的元素顺序。
在上面的三个步骤中,第二个步骤由CUDA Thrust的exclusive_scan
基元执行,并且计算上的要求远低于其他两个。
对于2097152
元素数组,上述方法已在0.38ms
卡片上NVIDIA GTX 960
执行,与CUDA推力1.0ms
copy_if
相反。上述方法似乎更快,原因有两个:
1)它专门用于支持经线内在元素的卡片;
2)该方法不保证输出排序。
应该注意到,我们已经针对inkc.sourceforge.net提供的代码测试了该方法。虽然后一个代码安排在一个内核调用中(它不使用任何CUDA Thrust原语),但与三内核版本相比,它没有更好的性能。
完整的代码可用here,与原始的Davide Spataro常规相比略有优化。
[1] M.Biller, O. Olsson, U. Assarsson, “Efficient stream compaction on wide SIMD many-core architectures,” 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, “InK-Compact: in-kernel stream compaction and its application to multi-kernel data visualization on General-Purpose GPUs,” Computer Graphics Forum, vol. 32, n. 6, pp. 178-188, 2013.
答案 2 :(得分:1)
流压缩是一个众所周知的问题,编写了批量代码(Thrust,Chagg引用两个在CUDA上实现流压缩的库)。
如果你有一个相对较新的支持CUDA的设备,它支持__ballot(计算cdapability&gt; = 3.0)这样的内在功能,那么尝试一个小的CUDA程序来执行流压缩很多是比较快的。推力。
这里找到代码和最小文档。 https://github.com/knotman90/cuStreamComp
使用单一内核方式的投票功能来执行压缩。