【问题标题】:Small sized binary searches on CUDA GPUsCUDA GPU 上的小型二进制搜索
【发布时间】:2016-01-05 20:48:36
【问题描述】:

我有一个int64_t 类型的大型设备阵列inputValues。该数组的每 32 个元素按升序排序。我有一个未排序的搜索数组removeValues

我的意图是在inputValues 中查找removeValues 中的所有元素并将它们标记为-1。实现这一目标的最有效方法是什么?如果有帮助,我正在使用 3.5 cuda 设备。

我不是在寻找更高级别的解决方案,即我不想使用推力或 cub,但我想使用 cuda 内核编写它。

我最初的方法是在线程块的共享内存中加载每个32 值。每个线程还从removeValues 加载单个值,并对共享内存数组进行独立的二进制搜索。如果找到,则使用 if 条件设置该值。

这种方法不会涉及很多银行冲突和分行分歧吗?您认为在实现二分搜索时可以通过使用三元运算符来解决分支分歧吗?即使解决了,银行冲突如何消除?由于排序数组的大小为 32,是否可以使用 shuffle 指令实现二进制搜索?会有帮助吗?

编辑:我添加了一个示例来展示我打算实现的目标。

假设inputValues 是一个向量,其中每 32 个元素排序:
[2, 4, 6, ... , 64], [95, 97, ... , 157], [1, 3, ... , 63], [...]

这个数组的典型大小可以在 32*2 到 32*32 之间。值的范围可以从0INT64_MAX

removeValues 的示例是:
[7, 75, 95, 106]

此数组的典型大小范围为 11024

在操作removeValues 之后将是: [-1, 75, -1, 106]

inputValues 中的值保持不变。

【问题讨论】:

  • 请添加一个小数字示例来说明您想要实现的目标,也许有一种推力方式可以做到; inputValues 有多大? removeValues 有多大?
  • 我认为说明所需功能的示例会有所帮助。我已经阅读了这个问题两次,但仍然不清楚有问题的代码到底应该做什么。为什么不发布您已经拥有的代码,以便将其用作基线实现?
  • 银行冲突应该不是问题,至少就所描述的提案而言。在 cc3.5 上,将共享内存置于 64-bit-bank-mode。一个给定的 warp 只会查看 32 个连续的位置,因此不可能发生银行冲突。从同一位置读取的经线中的线程将由广播机制提供服务。只有在同一银行中具有多个数据项的数据数组才可能发生银行冲突。总共 32 个连续项目(从特定经线的角度来看)每个银行不可能有超过一个项目。
  • 我不认为经线分歧会是灾难性的。 naive binary search 最多应该有 2 路发散,编译器可能会在一定程度上通过预测来缓解这种情况。在 32 元素数组上进行 warp-shuffle 驱动的二进制搜索应该是可能的,不涉及发散(我认为),但是 warp-shuffle 本身只处理 32 位数量,因此每个 64 位操作需要两个 shuffle 操作。我也想知道inputValuesremoveValues 有多大,通常
  • 也不清楚您是否打算用-1 标记removeValuesinputValues。当你标记时,你是要使用单独的标记数组,还是将给定的值替换为-1。如果是后者,那也会引发有关所涉及范围的问题(也许只有非负值?)

标签: c++ cuda binary-search


【解决方案1】:

我同意@harrism 的答案(现已删除)和评论。由于我在非推力方法上付出了一些努力,所以我将介绍我的发现。

我尝试使用 __shfl() 在 warp 级别天真地实现二进制搜索,然后在数据集上重复该二进制搜索,将数据集传递给每个 32 元素组。

这很尴尬,但我的代码比推力慢了大约 20 倍(事实上,如果你用nvprof 仔细计时,它可能会比这更糟)。

我使数据量比问题中提出的要大一些,因为问题中的数据量太小了,以至于时间都尘埃落定了。

这是 2 种方法的完整示例:

  1. 问题中大致概述了什么,即使用 warp shuffle 创建一个二分搜索,该搜索可以针对 32 个元素的有序数组搜索多达 32 个元素。对尽可能多的 32 元素有序数组重复此过程,将整个数据集传递给每个有序数组(希望您现在可以开始看到一些低效率。)

  2. 使用推力,与@harrism 概述的基本相同,即对分组数据集进行排序,然后对其运行矢量化thrust::binary_search

示例如下:

$ cat t1030.cu
#include <stdio.h>
#include <assert.h>
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/sort.h>
#include <thrust/binary_search.h>

typedef long mytype;

const int gsize = 32;
const int nGRP = 512;
const int dsize = nGRP*gsize;//gsize*nGRP;

#include <time.h>
#include <sys/time.h>
#define USECPSEC 1000000ULL

unsigned long long dtime_usec(unsigned long long start){

  timeval tv;
  gettimeofday(&tv, 0);
  return ((tv.tv_sec*USECPSEC)+tv.tv_usec)-start;
}

template <typename T>
__device__ T my_shfl32(T val, unsigned lane){
  return __shfl(val, lane);
}

template <typename T>
__device__ T my_shfl64(T val, unsigned lane){
  T retval = val;
  int2 t1 = *(reinterpret_cast<int2 *>(&retval));
  t1.x = __shfl(t1.x, lane);
  t1.y = __shfl(t1.y, lane);
  retval = *(reinterpret_cast<T *>(&t1));
  return retval;
}

template <typename T>
__device__ bool bsearch_shfl(T grp_val, T my_val){
  int src_lane = gsize>>1;
  bool return_val = false;
  T test_val;
  int shift = gsize>>2;
  for (int i = 0; i <= gsize>>3; i++){
    if (sizeof(T)==4){
      test_val = my_shfl32(grp_val, src_lane);}
    else if (sizeof(T)==8){
      test_val = my_shfl64(grp_val, src_lane);}
    else assert(0);
    if (test_val == my_val) return_val = true;
    src_lane += (((test_val<my_val)*2)-1)*shift;
    shift>>=1;
    assert ((src_lane < gsize)&&(src_lane > 0));}
  if (sizeof(T)==4){
    test_val = my_shfl32(grp_val, 0);}
  else if (sizeof(T)==8){
    test_val = my_shfl64(grp_val, 0);}
  else assert(0);
  if (test_val == my_val) return_val = true;
  return return_val;
}

template <typename T>
__global__ void bsearch_grp(const T * __restrict__ search_grps, T *data){

  int idx = threadIdx.x+blockDim.x*blockIdx.x;
  int tid = threadIdx.x;
  if (idx < gsize*nGRP){
    T grp_val = search_grps[idx];
    while (tid < dsize){
      T my_val = data[tid];
      if (bsearch_shfl(grp_val, my_val)) data[tid] = -1;
      tid += blockDim.x;}
  }
}


int main(){

  // data setup
  assert(gsize == 32);  //mandatory (warp size)
  assert((dsize % 32)==0);  //needed to preserve shfl capability
  thrust::host_vector<mytype> grps(gsize*nGRP);
  thrust::host_vector<mytype> data(dsize);
  thrust::host_vector<mytype> result(dsize);
  for (int i = 0; i < gsize*nGRP; i++) grps[i] = i;
  for (int i = 0; i < dsize; i++) data[i] = i;
  // method 1: individual shfl-based binary searches on each group
  mytype *d_grps, *d_data;
  cudaMalloc(&d_grps, gsize*nGRP*sizeof(mytype));
  cudaMalloc(&d_data, dsize*sizeof(mytype));
  cudaMemcpy(d_grps, &(grps[0]), gsize*nGRP*sizeof(mytype), cudaMemcpyHostToDevice);
  cudaMemcpy(d_data, &(data[0]), dsize*sizeof(mytype), cudaMemcpyHostToDevice);
  unsigned long long my_time = dtime_usec(0);
  bsearch_grp<<<nGRP, gsize>>>(d_grps, d_data);
  cudaDeviceSynchronize();
  my_time = dtime_usec(my_time);
  cudaMemcpy(&(result[0]), d_data, dsize*sizeof(mytype), cudaMemcpyDeviceToHost);
  for (int i = 0; i < dsize; i++) if (result[i] != -1) {printf("method 1 mismatch at %d, was %d, should be -1\n", i, (int)(result[i])); return 1;}
  printf("method 1 time: %fs\n", my_time/(float)USECPSEC);
  // method 2: thrust sort, followed by thrust binary search
  thrust::device_vector<mytype> t_grps = grps;
  thrust::device_vector<mytype> t_data = data;
  thrust::device_vector<bool> t_rslt(t_data.size());
  my_time = dtime_usec(0);
  thrust::sort(t_grps.begin(), t_grps.end());
  thrust::binary_search(t_grps.begin(), t_grps.end(), t_data.begin(), t_data.end(), t_rslt.begin());
  cudaDeviceSynchronize();
  my_time = dtime_usec(my_time);
  thrust::host_vector<bool> rslt = t_rslt;
  for (int i = 0; i < dsize; i++) if (rslt[i] != true) {printf("method 2 mismatch at %d, was %d, should be 1\n", i, (int)(rslt[i])); return 1;}
  printf("method 2 time: %fs\n", my_time/(float)USECPSEC);

  // method 3:  multiple thrust merges, followed by thrust binary search



  return 0;
}

$ nvcc -O3 -arch=sm_35 t1030.cu -o t1030
$ ./t1030
method 1 time: 0.009075s
method 2 time: 0.000516s
$

我在 linux、CUDA 7.5、GT640 GPU 上运行它。显然,不同 GPU 的性能会有所不同,但如果任何 GPU 显着缩小差距,我会感到惊讶。

简而言之,建议您使用经过良好调整的库,例如推力或幼崽。如果你不喜欢推力的整体性,你可以试试 cub。我不知道 cub 是否有二进制搜索,但是针对整个排序数据集的单个二进制搜索是 not a difficult thing to write,它所涉及的时间较少(对于方法 2——可使用 nvprof 或其他计时码)。

由于您的 32 元素分组范围已经排序,我还考虑了使用多个 thrust::merge 操作而不是单个排序的想法。我不确定哪个会更快,但由于推力方法已经比 32 元素随机搜索方法快得多,我认为推力(或幼崽)是显而易见的选择。

【讨论】:

    猜你喜欢
    • 2014-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-21
    • 1970-01-01
    • 2014-07-10
    • 2014-03-22
    • 1970-01-01
    相关资源
    最近更新 更多