cuda随笔1.基数排序

闲聊

其实笔者会写下这篇文章来源与一个问题:TOPK。
在写cpu端的串行代码时,往往会用k个大小的堆来解决这类问题(时间复杂度nlog(k)),当k较大时(k >> n)一般会采用快排解决(相对内存跨度太大的堆来说会有更好的cache命中率)应该会有很多这方面的优质讲解资源,这里简单提一下,如有错误,多谢指出。
当然上面聊的只是为了勾起你的回忆(如果有的话)。写下来介绍一下并行计算中解决topk的方式。
浏览很多深度学习框架会发现topk问题在并行计算中常用基数排序来解决,当然是因为它具有良好的并行性。

基数排序

1.cpu串行实现。
闲聊结束,来看看今天的主角。如何在cpu端实现这个10大排序之一的基数排序。
先讲一下思路:一个数的大小其实是由它的位数和每位上的数字决定的,我们可以从低位(高位也是一样的)按位比较这些数(用两个桶,一个装该位为1的数,一个装该位为0的数,有点像map的感觉):(假设排序 1111,101,110,1,0这5个数)图来:

第一位排序(最低位),先将所有数据按顺序放入桶内,再将代表1的桶接到0桶后(注意每一步的输出为下一步的输入)。
在这里插入图片描述

第二位:
在这里插入图片描述
第三位:
在这里插入图片描述
第四位(结果):
在这里插入图片描述
实现代码(时间复杂度显然为bitsize*N):
在这里先提议下一个处理各种类型正负值的小技巧(这里以float为演示):
由于float是由符号位、指数位、尾数组成。对于正浮点而言按位比较就是比较结果(高位越大,该数越大),但对于负浮点而言刚好相反。又考虑到正负浮点的按位比较,所以将正浮点第一位置为1,负浮点全部取反,这样就能统一正负浮点的按位比较。当然最后要转回来。详细代码:

__device__ __host__ unsigned int convert(float v) {
  // if v >= 0 v |= 2^31 else v = ~(v) - 1,ensure anyif v >=0 or v < 0 by bitwise compare is vaild
  unsigned int cmp = *reinterpret_cast<unsigned int*>(&v);
  unsigned int ret = (cmp & (1<<31)) ?  ~(cmp): (cmp | 0x80000000);
  return ret;
}

__device__ __host__ float deconvert(unsigned int v) {
  unsigned int tmp = (v & (1 << 31)) ?  (v ^ 0x80000000) : ~(v);
  return *reinterpret_cast<float*>(&tmp);
}

cpu基数排序实现代码:

void RadixSortHost(float *v,unsigned int size) {
  unsigned int sort_tmp0[size],sort_tmp1[size]; 
  for(int bit = 0;bit < 32;bit++) {
    unsigned int mask = 1 << bit;
    unsigned int cnt0 = 0,cnt1 = 0;
    for (int i = 0;i < size;i++) {
      unsigned int elem = ((bit == 0) ?convert(v[i]):sort_tmp0[i]);
      if((elem & mask) != 0) {
        sort_tmp1[cnt1++] = elem;
      }
      else {
        sort_tmp0[cnt0++] = elem;
      }
    }
    for(int i = 0;i < cnt1;i++) {
      sort_tmp0[cnt0 + i] = sort_tmp1[i];
    }
  }
  for(int i = 0;i < size;i++) {
    v[i] = deconvert(sort_tmp0[i]);
  }
}

2.并行基数排序的实现
为啥要详细讲解基数排序的串行实现,相信很容易猜到:将基数排序分组在并归就是一个并行的基数排序。
2.1分组排序
本人在Cuda C编程权威指南1.并行规约分化+循环展开章节讲过内存合并(交叉读取),这里就不加赘述了。
举一个最简单的例子:
在这里插入图片描述
thread0使用基数排序排序data0和data2,thread1使用基数排序排序data1和data3。
对应代码(桶的存储也统一按照交叉的形式,这样线程间互不干扰、方便):

for (unsigned int bit = 0;bit < 32;bit++) {
      unsigned int mask = 1<<bit;
      unsigned int cnt0 = 0,cnt1 = 0;
      for(unsigned int i = tid;i < n;i += blockDim.x) {
        unsigned int elem = (bit == 0 ? convert(data[i]):sort_tmp0[i]);
        if((elem&mask) != 0) {
          sort_tmp1[cnt1 + tid] = elem;
          cnt1 += blockDim.x;
        }
        else {
          sort_tmp0[cnt0 + tid] = elem;
          cnt0 += blockDim.x;
        }
      }
      for (unsigned int i = 0;i < cnt1;i += blockDim.x) {
        sort_tmp0[cnt0 + i + tid] = sort_tmp1[i + tid];
      }
  }

上面的代码和串行代码几乎一模一样(读取存储的方式为交叉,串行为顺序)。

优化:if((elem&mask) != 0)(可能会出现线程束分化,不过这里修改为下面的代码并未显著提升性能,应该是增加了指令,不过这个代码的性能瓶颈在并归,源码中未作修改)。

for (unsigned int bit = 0;bit < 32;bit++) {
      unsigned int mask = 1<<bit;
      unsigned int cnt[2] = {0,0};
      for(unsigned int i = tid;i < n;i += blockDim.x) {
        unsigned int elem = (bit == 0 ? convert(data[i]):sort[i]);
        unsigned int index_type = (mask&elem)>>bit;
        sort[cnt[index_type] + tid + index_type * n] = elem;
        cnt[index_type] += blockDim.x;
      }
      for (unsigned int i = 0;i < cnt[1];i += blockDim.x) {
        sort[cnt[0] + i + tid] = sort[i + tid + n];
      }
  }

还有一点,每个线程的排序可以放到local来做,可能可以提升性能(先从global复制到本地寄存器再做排序),不过有导致occupancy降低的风险(要注意寄存器的使用,若是线程内使用过多的寄存器同时活跃的线程束数量将受到限制,在最后的源码中也有这个版本在对N=1024*256时耗时减少10%,还是要说的是性能瓶颈在并归)
2.2 归并得出结果
按照上面那张图,归并操作其实就是合并两组有序序列了。是一个很简单的问题,用m个(这里其实就是两个)指针就能解决。(假设这两个有序序列为{1,3 },{2,4})
在这里插入图片描述

代码:

__shared__ unsigned int min_value, min_tid;
  __shared__ unsigned int list_idx[512];//512为blockDim.x,即block内的线程数
  unsigned int elem = 0xffffffff;//初始化全为unsigned int max
  list_idx[tid] = 0;//初始化所有指针指向序列头部
  __syncthreads();
  for(unsigned int i = 0;i < n;i++) {
    unsigned int x = (list_idx[tid] * blockDim.x) + tid;//tid 线程的第 list_idx[tid]个数据(经过基数排序从小到大的数据)
    if(x < n) {
      elem = sort_tmp0[x];//合法数据
    }
    else {
      elem = 0xffffffff;//非法数据,置为unsigned int max。意未不参与最小值比较(或最小值就为该值)
    }
    if(tid == 0) {
      min_value = min_tid = 0xffffffff;//block内第一个线程初始化min_value
    }
    __syncthreads();
    atomicMin(&min_value,elem);//原子操作,每个线程的序列头都与min_value比较决出最小值
    __syncthreads();


    if(min_value == elem) {//如果有多个最小值,取thread号最小的那个
      atomicMin(&min_tid, tid);
    }
    __syncthreads();
    if(min_tid == tid) {
      list_idx[tid]++;//序列头右移
      data[i] = deconvert(min_value);//得出第i个结果
    }
    __syncthreads();
  }

宏观来理解上面的代码:在一个block内并归(可以使用共享内存,并且同步方便,因为并归依赖之前的结果,只能按序并归),每个线程都管理着一个有m个数据的有序序列,我们的任务就是合并这些序列(回到上面那张图)。
每个线程都判断自己序列头的值是否为所有序列头中的最小值,是的话得出第i个结果(如果有重复选取最小thread号的那个)。

由于为了最后的并归方便这里只使用了一个block,可以使用更多的block去处理上面的分组排序,再并归。(说实话速度有点慢,有很大的改进空间)

完整代码(看到这不给个星星吗,求求了):
代码

参考

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
cuda_runtime.h是一个CUDA运行时库的头文件,用于在CUDA程序中访问CUDA运行时API。CUDA(Compute Unified Device Architecture)是由NVIDIA推出的一种并行计算平台和编程模型,可用于利用NVIDIA GPU的并行计算能力。 要下载cuda_runtime.h,需要进行以下步骤: 1. 前往NVIDIA官方网站(https://www.nvidia.com)。 2. 在网站上的搜索框中输入"CUDA",然后点击搜索按钮。 3. 在搜索结果中找到CUDA官方页面,点击进入。 4. 在CUDA页面上找到对应版本的CUDA运行时库,确保该版本与您的系统及CUDA编译器兼容。 5. 在CUDA运行时库页面上,找到相关的下载链接或按钮。 6. 点击下载链接或按钮,开始下载CUDA运行时库的安装包。 7. 下载完成后,使用解压软件解压安装包。 8. 在解压后的文件夹中,找到cuda_runtime.h头文件。 9. 将cuda_runtime.h文件复制到您的CUDA程序所在的目录中。 10. 在CUDA程序中,使用#include指令包含cuda_runtime.h头文件,并通过其中的函数和数据结构访问CUDA运行时API。 需要注意的是,下载和使用CUDA运行时库需要确保您的计算机上有兼容的NVIDIA GPU并已正确安装了适当的驱动程序。另外,也要确保您的编译器支持CUDA编程。根据不同的操作系统和CUDA版本,下载和安装过程可能会有所不同,建议根据您的具体环境和需求,在NVIDIA官方网站上查找相应的文档和指南进行操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值