使用CUDA进行合并排序:小输入数组的高效实现

时间:2015-06-09 10:11:19

标签: arrays sorting merge cuda scientific-computing

我有以下问题:给定两个排序的数组A和B,我必须生成一个带有A和B元素的排序数组C.

我找到了一些使用CUDA解决此问题的解决方案:合并路径,例如http://www.cc.gatech.edu/~bader/papers/GPUMergePath-ICS2012.pdf

然而,他们的问题是由原始数组的大小给出的,至少有10k个元素。我有一个不同的问题。

我要合并的数组要小得多(最多1000个元素),复杂性由要完成的合并次数给出(10次幂的顺序为10,10 ^ 5个数组〜 1000将相互合并)。

他们的部分问题是将原始数组拆分为并行处理的大小相等的部分。我必须合并的数组足够小,完全适合GPU共享内存。

Thrust不是我的第一选择,因为我的过程的输出不是排序数组本身,而是带有元素的计算:所以我认为专门的内核应该比首先排序元素索引更快,然后使用它们计算。

该算法的串行版本如下:

i=0
j=0
k=0
T=4
while i<N and j<M:
    if A[i]<B[j]:
        start_i = max(0,i-T)
        C[k]=sum(A[start_i:i+1])
        i+=1
    else:
        start_j = max(0,j-T)
        C[k]=sum(B[start_j:j+1])
        j+=1
    k+=1

while i<N:
    start_i = max(0,i-T)
    C[k]=sum(A[start_i:i+1])
    i+=1
    k+=1
while j<M:
    start_j = max(0,j-T)
    C[k]=sum(B[start_j:j+1])
    j+=1
    k+=1

如何利用CUDA功能解决此问题?

1 个答案:

答案 0 :(得分:6)

任何CUDA计划的两个最重要的优化目标应该是:

  1. 揭露(足够)并行
  2. 有效利用记忆
  3. 在优化过程中肯定会考虑许多其他事项,但这些是首先要解决的两个最重要的事项。

    合并操作(与合并排序不完全相同)乍一看是一种固有的串行操作。我们无法正确决定选择哪个项目,从A或B输入数组到下一个输出数组C,直到我们在C中进行了所有先前的选择。在这方面,合并算法(在此实现中) )使暴露并行性变得困难,而问题中链接的论文几乎完全集中在该主题上。

    本文描述的算法的目标是将两个输入矢量A和B分解成可以独立工作的多个较小的片段,以暴露并行性。特别是,目标是使所有SM在GPU中保持忙碌,并使所有SP保持在SM忙碌状态。一旦完成了足够的工作分解,每个SP最终都会执行顺序合并(如本文所述):

      
        
    1. 合并阶段 - 每个核心合并两个子数组   它是使用与a相同的算法给出的   简单的顺序合并。
    2.   

    但是,正如您所指出的,您想要做的事情有些不同。您已经拥有许多阵列,并且希望在这些阵列上执行独立合并操作。由于您的阵列数大约为100,000,因此考虑将每个阵列映射到GPU SP(即线程)是足够的独立工作。这意味着我们就像在论文中一样,在每个核心/ SP /线程上使用简单的顺序合并。因此,暴露并行性的问题在你的情况下已经完成(可能已经足够程度了)。

    此时我们可以考虑按原样实施。我稍后展示的代码将此作为比较的起点。然而,我们发现性能不是很好,这是因为合并算法从根本上具有依赖于数据的访问序列,因此(更难)安排在GPU上进行合并访问。本文的作者提出通过首先将数据(以合并方式)读入共享内存,然后让算法在共享内存中对其进行处理来缓解此问题,其中对无组织访问的惩罚较少。

    我建议采用不同的方法:

    1. 安排顺序合并算法,以便A和B的每个元素只需要读一次
    2. column-major 形式安排存储A,B和C,而不是更多&#34; natural&#34;可能考虑的行主存储。这有效地转换了A,B和C向量的存储矩阵。这允许合并访问的改进,因为GPU线程在其各自的A和B向量上通过合并操作导航。它远非完美,但改善很大。
    3. 这是一个实现上述想法的有效例子,在每个线程中运行一个简单的顺序合并,每个线程将一个A向量与一个B向量合并:

      $ cat t784.cu
      #include <stdio.h>
      #include <stdlib.h>
      #include <thrust/sort.h>
      #include <thrust/merge.h>
      
      #define NUM_SETS 100000
      #define DSIZE 100
      typedef int mytype;
      
      // for ascending sorted data
      #define cmp(A,B) ((A)<(B))
      #define nTPB 512
      #define nBLK 128
      
      #include <time.h>
      #include <sys/time.h>
      #define USECPSEC 1000000ULL
      
      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>
      __host__ __device__ void smerge(const T * __restrict__  a, const T * __restrict__ b, T * __restrict__ c, const unsigned len_a, const unsigned len_b, const unsigned stride_a = 1, const unsigned stride_b = 1, const unsigned stride_c = 1){
      
        unsigned len_c = len_a+len_b;
        unsigned nc = 0;
        unsigned na = 0;
        unsigned nb = 0;
        unsigned fa = (len_b == 0);
        unsigned fb = (len_a == 0);
        T nxta = a[0];
        T nxtb = b[0];
        while (nc < len_c){
          if (fa)      {c[stride_c*nc++] = nxta; na++; nxta = a[stride_a*na];}
          else if (fb) {c[stride_c*nc++] = nxtb; nb++; nxtb = b[stride_b*nb];}
          else if (cmp(nxta,nxtb)){
            c[stride_c*nc++] = nxta;
            na++;
            if (na == len_a) fb++;
            else nxta = a[stride_a*na];}
          else {
            c[stride_c*nc++] = nxtb;
            nb++;
            if (nb == len_b) fa++;
            else nxtb = b[stride_b*nb];}}
      }
      
      
      
      template <typename T>
      __global__ void rmtest(const T * __restrict__  a, const T * __restrict__ b, T * __restrict__  c, int num_arr, int len){
      
        int idx=threadIdx.x+blockDim.x*blockIdx.x;
      
        while (idx < num_arr){
          int sel=idx*len;
          smerge(a+sel, b+sel, c+(2*sel), len, len);
          idx += blockDim.x*gridDim.x;}
      }
      
      template <typename T>
      __global__ void cmtest(const T * __restrict__ a, const T * __restrict__ b, T * __restrict__ c, int num_arr, int len, int stride_a, int stride_b, int stride_c){
        int idx=threadIdx.x+blockDim.x*blockIdx.x;
        while (idx < num_arr){
          smerge(a+idx, b+idx, c+idx, len, len, stride_a, stride_b, stride_c);
          idx += blockDim.x*gridDim.x;}
      }
      
      
      
      
      template <typename T>
      int rmvalidate(T *a, T *b, T *c, int num_arr, int len){
      
        T *vc = (T *)malloc(2*len*sizeof(T));
        for (int i = 0; i < num_arr; i++){
          thrust::merge(a+(i*len), a+((i+1)*len), b+(i*len), b+((i+1)*len), vc);
      #ifndef TIMING
          for (int j = 0; j < len*2; j++)
            if (vc[j] != c[(i*2*len)+j]) {printf("rm mismatch i: %d, j: %d, was: %d, should be: %d\n", i, j, c[(i*2*len)+j], vc[j]); return 0;}
      #endif
          }
        return 1;
      }
      
      template <typename T>
      int cmvalidate(const T *c1, const T *c2, int num_arr, int len){
        for (int i = 0; i < num_arr; i++)
          for (int j = 0; j < 2*len; j++)
            if (c1[i*(2*len)+j] != c2[j*(num_arr)+i]) {printf("cm mismatch i: %d, j: %d, was: %d, should be: %d\n", i, j, c2[j*(num_arr)+i], c1[i*(2*len)+j]); return 0;}
        return 1;
      }
      
      int main(){
      
      
        mytype *h_a, *h_b, *h_c, *d_a, *d_b, *d_c;
        h_a = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype));
        h_b = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype));
        h_c = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype)*2);
        cudaMalloc(&d_a, (DSIZE*NUM_SETS+1)*sizeof(mytype));
        cudaMalloc(&d_b, (DSIZE*NUM_SETS+1)*sizeof(mytype));
        cudaMalloc(&d_c, DSIZE*NUM_SETS*sizeof(mytype)*2);
      // test "row-major" storage
        for (int i =0; i<DSIZE*NUM_SETS; i++){
          h_a[i] = rand();
          h_b[i] = rand();}
        thrust::sort(h_a, h_a+DSIZE*NUM_SETS);
        thrust::sort(h_b, h_b+DSIZE*NUM_SETS);
        cudaMemcpy(d_a, h_a, DSIZE*NUM_SETS*sizeof(mytype), cudaMemcpyHostToDevice);
        cudaMemcpy(d_b, h_b, DSIZE*NUM_SETS*sizeof(mytype), cudaMemcpyHostToDevice);
        unsigned long gtime = dtime_usec(0);
        rmtest<<<nBLK, nTPB>>>(d_a, d_b, d_c, NUM_SETS, DSIZE);
        cudaDeviceSynchronize();
        gtime = dtime_usec(gtime);
        cudaMemcpy(h_c, d_c, DSIZE*NUM_SETS*2*sizeof(mytype), cudaMemcpyDeviceToHost);
        unsigned long ctime = dtime_usec(0);
        if (!rmvalidate(h_a, h_b, h_c, NUM_SETS, DSIZE)) {printf("fail!\n"); return 1;}
        ctime = dtime_usec(ctime);
        printf("CPU time: %f, GPU RM time: %f\n", ctime/(float)USECPSEC, gtime/(float)USECPSEC);
      // test "col-major" storage
        mytype *ch_a, *ch_b, *ch_c;
        ch_a = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype));
        ch_b = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype));
        ch_c = (mytype *)malloc(DSIZE*NUM_SETS*sizeof(mytype));
        for (int i = 0; i < NUM_SETS; i++)
          for (int j = 0; j < DSIZE; j++){
            ch_a[j*NUM_SETS+i] = h_a[i*DSIZE+j];
            ch_b[j*NUM_SETS+i] = h_b[i*DSIZE+j];}
        cudaMemcpy(d_a, ch_a, DSIZE*NUM_SETS*sizeof(mytype), cudaMemcpyHostToDevice);
        cudaMemcpy(d_b, ch_b, DSIZE*NUM_SETS*sizeof(mytype), cudaMemcpyHostToDevice);
        gtime = dtime_usec(0);
        cmtest<<<nBLK, nTPB>>>(d_a, d_b, d_c, NUM_SETS, DSIZE, NUM_SETS, NUM_SETS, NUM_SETS );
        cudaDeviceSynchronize();
        gtime = dtime_usec(gtime);
        cudaMemcpy(ch_c, d_c, DSIZE*NUM_SETS*2*sizeof(mytype), cudaMemcpyDeviceToHost);
        if (!cmvalidate(h_c, ch_c, NUM_SETS, DSIZE)) {printf("fail!\n"); return 1;}
      
        printf("GPU CM time: %f\n", gtime/(float)USECPSEC);
        return 0;
      }
      
      $ nvcc -O3 -DTIMING -o t784 t784.cu
      $ ./t784
      CPU time: 0.030691, GPU RM time: 0.045814
      GPU CM time: 0.002784
      $
      

      注意:

      1. 当内存组织为行主要时,GPU实际上比天真的单线程CPU代码慢。但对于列主要组织(往往会增加合并访问的机会),GPU代码比我的测试用例的CPU代码快约10倍。对于GPU MergePath 32位整数加速与x86串行合并,此~10倍加速因子大致在加速因子的范围(~10-20x)内。

      2. 使用intfloat数据类型会对CPU时序产生显着影响。 int似乎更快(在CPU上),所以我在这里显示该版本。 (这种差异也在论文中提到。)

      3. 添加到编译命令的-DTIMING开关减少了第一个验证功能,以便它只进行CPU合并操作,用于计时。

      4. 基本合并代码模板化,能够处理不同的数据类型,并进行参数化,以便可以在列主要操作或行主要操作中使用。

      5. 为了简化陈述,我已经免除了CUDA错误检查。但是,如果您在使用CUDA代码时遇到问题,则应始终使用proper cuda error checking

      6. 如何使用推力(正如我在评论中所建议的那样)?应该可以使用thrust :: merge与合适的设备/顺序执行策略,或多或少地模仿我上面所做的。 然而,推力期望向量是连续的,因此,没有额外的复杂性,它只能用于行主要情况,我们已经看到它受到不良内存访问模式的严重惩罚。应该可以在推力中创建一组置换迭代器,这将允许列主要的跨步访问改善内存场景,但我没有追求。