对于当前的OpenCL GPGPU项目,我需要根据具有64个可能值的某个键对数组中的元素进行排序。我需要最后一个数组让所有具有相同键的元素都是连续的。将关联数组new_index[old_index]
作为此任务的输出就足够了。
我将任务分成两部分。首先,我计算每个可能的密钥(桶)使用此密钥(进入该桶)的元素数量。我扫描这个数组(生成一个前缀sum),它表示每个桶的元素的新索引范围,比如每桶的“start”索引。
然后,第二步必须为每个元素分配一个新索引。如果我要在CPU上实现它,算法将是这样的:
for all elements e:
new_index[e] = bucket_start[bucket(e)]++
当然,这在GPU上不起作用。每个项目都需要以读写模式访问bucket_start
数组,这实际上是所有工作项之间的同步,这是我们能做的最糟糕的事情。
一个想法是在工作组中进行一些计算。但我不确定应该如何完成,因为我没有GPGPU计算经验。
在全局内存中,我们使用前缀sum初始化了bucket start数组,如上所述。使用原子int“访问”此数组的“互斥”。 (我是新手,所以也许在这里混合一些词。)
每个工作组都隐式分配了输入元素数组的一部分。它使用包含新索引的本地存储桶数组,相对于我们尚不知道的(全局)存储桶启动。在其中一个“本地缓冲区”已满后,工作组必须将本地缓冲区写入全局数组。为此,它锁定对全局存储区启动数组的访问,将这些值递增当前本地存储区大小,解锁,然后将结果写入全局new_index
数组(通过添加相应的偏移量)。重复此过程,直到处理完所有已分配的元素。
出现两个问题:
这是一个好方法吗?我知道从全局内存读取和写入很可能是这里的瓶颈,特别是因为我正在尝试获取同步访问权限(at至少只是全球记忆的一小部分。但也许有更好的方法来做到这一点,也许使用内核分解。请注意,我尝试避免在内核期间将数据从GPU读回到CPU(以避免OpenCL命令队列刷新,这也是我所采用的那样糟糕。)
在上面的算法设计中,如何实现锁定机制?以下代码会起作用吗?特别是,当硬件在SIMD组中执行“真正并行”的工作项时,我会遇到问题,比如Nvidia“warps”。在我当前的代码中,工作组的所有项目都会尝试以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];
}
}
答案 0 :(得分:3)
首先不要尝试在GPU上实现锁定算法。它将陷入僵局并停滞不前。 这是因为GPU是SIMD设备,并且线程不像CPU那样独立执行。 GPU同步执行称为WARP / WaveFront的设置线程。因此,如果Wave Front中的一个线程停止,它将停止Wave Front中的所有其他线程。如果解锁线程处于停滞波前,它将不执行和解锁互斥锁。
原子操作没问题。
您应该考虑的是无锁方法。请参阅本文以获得解释和示例CUDA代码: http://www.cse.iitk.ac.in/users/mainakc/pub/icpads2012.pdf/
它描述了无锁哈希表,链表和跳过列表以及一些示例CUDA代码。
建议的方法是创建一个两级数据结构。
第一级是无锁跳过列表。每个跳过列表条目具有用于重复值的无锁链接列表的第二级结构。以及条目数的原子数。
插入方法
1)生成64桶密钥 2)在跳过列表中找到关键字 3)如果未找到则插入跳过列表 4)将数据插入到链表中 5)增加此桶的原子计数器
插入前缀后,跳过列表桶的所有计数器,以便找到的偏移量 out put。
答案 1 :(得分:0)
我发现了一种将本地缓冲区附加到全局数组的更简单方法。它只需要两个步骤,其中一个步骤涉及原子操作。
第一步是在全局目标数组中分配索引,其中每个线程将写入其元素。为此,我们可以在atomic_add(__global int*)
中使用添加要追加的元素数量。在此具体示例中的bucket_start
上使用此函数。 atomic_add
的返回值是旧值。
在第二步中,我们使用此返回值作为复制目标数组中本地缓冲区的基本索引。如果我们决定将一个完整的线程组用于一个这样的追加操作,我们将本地缓冲区分发到线程组内的全局数组&#34;像往常一样#34;。在上面的bucket排序示例中,我们复制了多个数组,当数组的数量(=桶数)等于工作组的大小时,我们可以为每个线程分配一个桶,它将被循环复制。
答案 2 :(得分:0)
我最近不得不解决一个类似的问题,并且找到了一个更加优雅和有效的解决方案。我以为我会分享。
一般算法如下:
1。内核1:每个元素的线程数
2。内核2:每个存储区的线程数
3。内核3:每个元素的线程数
散布元素。
对于输入中的每个元素: output [i] = prefix_sum [input [i]] +偏移量[i];
棘手的部分是生成在第三内核中使用的offsets数组。
在第一个内核上,我们定义了一个本地缓存,其中包含每个工作组存储桶的直方图。我使用了一个事实,atomic_add返回此计数器的先前值-“当前”偏移量。这是关键。
__kernel void bucket_histogram(__global uint *input,__global uint *histogram,__global uint *offsets) {
__local local_histogram[NUM_BUCKETS];
size_t local_idx = get_local_id(0);
size_t global_idx = get_global_id(0);
// zero local mem
if (local_idx < NUM_BUCKETS)
{
local_histogram[local_idx] = 0;
}
barrier(CLK_LOCAL_MEM_FENCE);
// increment local histogram, save the local offset for later
uint value = input[global_idx];
uint local_offset = atomic_add(&local_histogram[value], 1);
barrier(CLK_LOCAL_MEM_FENCE);
// store the buckets in the global histogram (for later prefix sum)
if (local_idx < NUM_BUCKETS)
{
uint count = local_histogram[local_idx];
if (count > 0)
{
// increment the global histogram, save the offset!
uint group_offset_for_the_value_local_idx = atomic_add(&histogram[local_idx], count);
local_histogram[local_idx] = group_offset_for_the_value_local_idx;
}
}
barrier(CLK_LOCAL_MEM_FENCE);
// now local_histogram changes roles, it contains the per-value group offset from the start of the bucket
offsets[global_idx] = local_offset + local_histogram[value];
第二个内核执行前缀总和以计算每个存储区的开始。 第三个内核简单地合并了所有偏移量:
__kernel void bucket_sort_scatter(__global uint *input, __global uint* prefix_sum_histogram, __global uint* offsets, __global data_t *output) {
size_t global_idx = get_global_id(0);
uint value = input[global_idx];
uint scatter_target = offsets[global_idx] + prefix_sum_histogram[value];
output[scatter_target] = value;