在OpenCL中排序和计算元素

时间:2016-01-30 21:00:54

标签: sorting count opencl

我想创建一个OpenCL内核,对数百万个ulong进行排序和计数。 有一种特殊的算法可以满足我的需求,还是应该使用哈希表?

要明确,请注意以下内容:

[42, 13, 9, 42]

我想得到这样的输出:

[(9,1), (13,1), (42,2)]

我的第一个想法是修改计数排序 - 为了排序已经计数 - 但由于范围广泛,它需要太多的内存。 Bitonic或Radix排序以及计算元素的东西可能是一种方式,但我错过了一种快速计算元素的方法。对此有何建议?

额外说明:

  • 我正在开发使用NVIDIA Tesla K40C GPU和Terasic DE5-Net FPGA。到目前为止,主要目标是使其在GPU上运行,但我也对可能非常适合FPGA的解决方案感兴趣。
  • 我知道ulong范围内的某些值未被使用,因此我们可以使用它们来标记无效元素或重复。
  • 我想使用CPU中的多个线程来消耗GPU的输出,因此我们希望避免任何需要进行后处理的解决方案(在我的意思是主机方面),这些解决方案在输出周围有数据依赖性。 / LI>

3 个答案:

答案 0 :(得分:2)

此解决方案需要两次双声道排序,以计算重复项并删除它们(将它们移动到数组的末尾)。 Bitonic排序为O(log(n)^2),因此这将以时间复杂度2(log(n)^2)运行,除非您在循环中运行它,否则不应该成为问题。

为每个元素创建一个简单的结构,包括重复的数量,以及是否将元素添加为重复元素,如:

// Note: If you are worried about space, or know that there 
// will only be a few duplicates for each element, then 
// make the count element smaller
typedef struct {
  cl_ulong value;
  cl_ulong count : 63;
  cl_ulong seen  : 1; 
} Element; 

算法:

您可以首先创建一个比较函数,将重复项移到最后,如果要将它们添加到元素的总计数中,则计算重复项。这是比较函数背后的逻辑:

  1. 如果一个元素是重复元素而另一个元素不重复,则返回非重复元素较小(无论值是多少),这会将所有重复元素移动到最后。
  2. 如果元素是重复的并且右侧元素没有标记为重复(seen=0),则将正确的元素计数添加到左侧元素的计数中并设置正确的元素作为副本(seen=1)。这具有将具有特定值的元素的总计数移动到具有该值的数组中最左边的元素以及具有该值的所有重复的结果。
  3. 否则返回值较小的元素。
  4. 比较功能如下:

    bool compare(const Element* E1, const Element* E2) {
      if (!E1->seen && E2->seen) return true;  // E1 smaller
      if (!E2->seen && E1->seen) return false; // E2 smaller
    
      // If the elements are duplicates and the right element has 
      // not yet been "seen" by an element with the same value 
      if (E1->value == E2->value && !E2->seen) {
        E1->count += E2->count;   
        E2->seen = 1;
        return true;
      }
    
      // They aren't duplicates, and either
      // neither has been seen, or both have
      return E1->value < E2->value;
    }
    

    Bitonic排序具有特定的结构,可以用图表很好地说明。在图中,每个元素由3元组(a,b,c)引用,其中a = valueb = countc = seen

    每个图表显示阵列上的一次bitonic排序(垂直线表示元素之间的比较,水平线向右移动到bitonic排序的下一个阶段)。使用图表和上面的比较函数和逻辑,你应该能够说服自己这样做了。

    运行1:Bitonic Sort Run 1

    运行2:Bitonic Sort Run 2

    在第2次运行结束时,所有元素都按值排列。最后带有seen = 1的重复项,seen = 0的重复项位于正确的位置,count是具有相同值的其他元素的数量。

    <强>实施

    图表采用颜色编码,以说明比特序的子过程。我将蓝色块称为一个阶段(图中每次运行有三个阶段)。通常,每次运行都会有ceil(log(N))个阶段。每个阶段都包含许多绿色块(我将调用这些out-in块,因为比较的形状是在里面)和红色块(我称之为{{1块,因为要比较的元素之间的距离保持不变。

    从图中,constant块大小(每个块中的元素)从2开始,每次传递加倍。每次传递的out-in块大小从constant块大小的一半开始(在第二个(蓝色块)阶段,四个红色块中的每一个都有2个元素,因为绿色块具有4)的大小和相位内红色块的每个连续垂直线的一半。此外,相位中out-in(红色)块的连续垂直线的数量始终与具有0索引的相位数相同(0相的红色块的0垂直线,红色块的1个垂直线)对于阶段1,以及阶段2的2个垂直红色块 - 每个垂直线是调用该内核的迭代。

    然后你可以为constant传递和out-in传递创建内核,然后从主机端调用内核(因为你需要不断同步,这是一个缺点,但你应该仍然看到比顺序实现有很大的性能改进。)

    从主机方面看,整体的比特排序可能如下:

    constant

    然后内核就像(你还需要将struct typedef放在内核文件中,以便OpenCL编译器知道&#39; Element&#39;是什么):

    cl_uint num_elements = 4; // Set number of elements
    cl_uint phases       = (cl_uint)ceil((float)log2(num_elements));
    cl_uint out_in_block_size = 2;
    cl_uint constant_block_size;
    
    // Set the elements_buffer, which should have been created with
    // with clCreateBuffer, as the first kernel argument, and the 
    // number of elements as the second kernel argument
    clSetKernelArg(out_in_kernel, 0, sizeof(cl_mem), (void*)(&elements_buffer));
    clSetKernelArg(out_in_kernel, 1, sizeof(cl_uint), (void*)(&num_elements));
    clSetKernelArg(constant_kernel, 0, sizeof(cl_mem), (void*)(&elements_buffer));
    clSetKernelArg(constant_kernel, 1, sizeof(cl_uint), (void*)(&num_elements));
    
    // For each pass
    for (unsigned int phase = 0; phase < phases; ++phase) { 
      //  -------------------- Green Part ------------------------ //
    
      // Set the out_in_block size for the kernel
      clSetKernelArg(out_in_kernel, 2, sizeof(cl_int), (void*)(&out_in_block_size));
    
      // Call the kernel - command_queue is the clCommandQueue 
      // which should have been created during cl setup 
      clEnqueNDRangeKernel(command_queue    , // clCommandQueue  
                           out_in_kernel    , // The kernel
                           1                , // Work dim = 1 since 1D array
                           NULL             , // No global offset
                           &global_work_size,
                           &local_work_size ,
                           0                ,
                           NULL             ,
                           NULL);
      barrier(CLK_GLOBAL_MEM_FENCE); // Synchronise
    
      // ---------------------- End Green Part -------------------- //
    
      // Set the block size for constant blocks based on the out_in_block_size
      constant_block_size = out_in_block_size / 2;
    
      //  --------------------  Red Part  ------------------------ // 
    
      for (unsigned int i  0; i < phase; ++i) {
        // Set the constant_block_size as a kernel argument
        clSetKernelArg(constant_kernel, 2, sizeof(cl_int), (void*)(&constant_block_size));
    
        // Call the constant kernel
        clEnqueNDRangeKernel(command_queue    , // clCommandQueue  
                             constant_kernel  , // The kernel
                             1                , // Work dim = 1 since 1D array
                             NULL             , // No global offset
                             &global_work_size,
                             &local_work_size ,
                             0                ,
                             NULL             ,
                             NULL);
      barrier(CLK_GLOBAL_MEM_FENCE); // Synchronise
    
      // Update constant_block_size for next iteration
      constant_block_size /= 2; 
      }
      // ------------------- End Red Part ---------------------- //
    }
    

    constant_kernel看起来一样,但是线程映射(你如何确定__global void out_in_kernel(__global Element* elements, unsigned int num_elements, unsigned int block_size) { const unsigned int idx_upper = // index of upper element in diagram. const unsigned int idx_lower = // index of lower element in diagram // Check that both indices are in range (this depends on thread mapping) if (idx_upper is in range && index_lower is in range) { // Do the comparison if (!compare(elements + idx_upper, elements + idx_lower) { // Swap the elements } } } idx_upper)会有所不同。有很多方法可以将线程映射到元素,通常模仿图表(请注意,所需的线程数是元素总数的一半,因为每个线程可以进行一次比较)。

    另一个考虑因素是如何使线程映射成为一般(因此,如果你有许多不是2的幂的元素,算法就不会中断)。

答案 1 :(得分:1)

boost.computeVexCL怎么样?两者都提供排序算法。

答案 2 :(得分:1)

Mergesort在GPU上工作得很好,你可以修改它来排序键+计数而不是键。在合并期间,您还将检查键是否相同,如果是,则在合并期间将它们融合为单个键。 (如果你合并[9 / c:1,42 / c:1]和[13 / c:1,42 / c:1]你会得到[9 / c:1,13 / c:1,42 / c :2]) 您可能必须使用并行前缀和来消除因融合键而导致的间隙。

或者:首先使用常规GPU排序,标记其右侧键不同的所有键(仅在每个唯一键的最后一个键处为真),使用并行前缀和来获取所有唯一键的连续索引并注意它们在排序数组中的位置。然后,您只需要减去前一个唯一键的索引即可获得计数。