对于当前的 OpenCL GPGPU 项目,我需要根据具有 64 个可能值的某个键对数组中的元素进行排序。我需要最终的数组使所有具有相同键的元素都是连续的。new_index[old_index]
将关联数组作为此任务的输出就足够了。
我把任务分成两部分。首先,我为每个可能的键(桶)计算带有这个键的元素的数量(进入那个桶)。我扫描这个数组(生成一个前缀总和),它指示每个桶的元素的新索引范围,比如每个桶的“开始”索引。
然后,第二步必须为每个元素分配一个新索引。如果我要在 CPU 上实现它,算法将是这样的:
for all elements e:
new_index[e] = bucket_start[bucket(e)]++
当然,这在 GPU 上不起作用。每个项目都需要以bucket_start
读写模式访问数组,这本质上是所有工作项目之间的同步,这是我们能做的最糟糕的事情。
一个想法是将一些计算放在工作组中。但是我不确定这应该如何准确地完成,因为我在 GPGPU 计算方面没有经验。
在全局内存中,我们使用上述前缀 sum 初始化存储桶起始数组。对这个数组的访问是用一个原子 int “互斥”的。(我是新手,所以可能会在这里混用一些词。)
每个工作组都被隐式分配了输入元素数组的一部分。它使用包含新索引的本地存储桶数组,相对于我们尚不知道的(全局)存储桶开始。在这些“本地缓冲区”之一已满后,工作组必须将本地缓冲区写入全局数组。为此,它锁定对全局存储桶起始数组的访问,将这些值增加当前本地存储桶大小,解锁,然后可以将结果写入全局new_index
数组(通过添加相应的偏移量)。重复此过程,直到处理完所有分配的元素。
出现两个问题:
这是一个好方法吗?我知道从/向全局内存读取和写入很可能是这里的瓶颈,特别是因为我试图获得对全局内存(至少只有一小部分)的同步访问。但也许有更好的方法来做到这一点,也许使用内核分解。请注意,我尽量避免在内核期间将数据从 GPU 读回 CPU(以避免 OpenCL 命令队列刷新,这也很糟糕,因为我很坚强)。
在上面的算法设计中,如何实现锁定机制?下面的代码会起作用吗?特别是,当硬件在 SIMD 组中执行“真正并行”的工作项目时,我预计会出现问题,例如 Nvidia“扭曲”。在我当前的代码中,工作组的所有项目都将尝试以 SIMD 方式获取锁。我应该将此限制为仅第一个工作项吗?并使用障碍使它们在本地保持同步?
#pragma OPENCL EXTENSION cl_khr_global_int32_base_atomics : enable __kernel void putInBuckets(__global uint *mutex, __global uint *bucket_start, __global uint *new_index) { __local bucket_size[NUM_BUCKETS]; __local bucket[NUM_BUCKETS][LOCAL_MAX_BUCKET_SIZE]; // local "new_index" while (...) { // process a couple of elements locally until a local bucket is full ... // "lock" while(atomic_xchg(mutex, 1)) { } // "critical section" __local uint l_bucket_start[NUM_BUCKETS]; for (int b = 0; b < NUM_BUCKETS; ++b) { l_bucket_start[b] = bucket_start[b]; // where should we write? bucket_start[b] += bucket_size[b]; // update global offset } // "unlock" atomic_xchg(mutex, 0); // write to global memory by adding the offset for (...) new_index[...] = ... + l_bucket_start[b]; } }