GPU上的高效桶式排序

时间:2013-05-27 23:57:39

标签: synchronization opencl semaphore gpgpu bucket-sort

对于当前的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数组(通过添加相应的偏移量)。重复此过程,直到处理完所有已分配的元素。

出现两个问题:

  1. 这是一个好方法吗?我知道从全局内存读取和写入很可能是这里的瓶颈,特别是因为我正在尝试获取同步访问权限(at至少只是全球记忆的一小部分。但也许有更好的方法来做到这一点,也许使用内核分解。请注意,我尝试避免在内核期间将数据从GPU读回到CPU(以避免OpenCL命令队列刷新,这也是我所采用的那样糟糕。)

  2. 在上面的算法设计中,如何实现锁定机制?以下代码会起作用吗?特别是,当硬件在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];
        }
    }
    

3 个答案:

答案 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;