我有以下问题:给定两个排序的数组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功能解决此问题?
答案 0 :(得分:6)
任何CUDA计划的两个最重要的优化目标应该是:
在优化过程中肯定会考虑许多其他事项,但这些是首先要解决的两个最重要的事项。
合并操作(与合并排序不完全相同)乍一看是一种固有的串行操作。我们无法正确决定选择哪个项目,从A或B输入数组到下一个输出数组C,直到我们在C中进行了所有先前的选择。在这方面,合并算法(在此实现中) )使暴露并行性变得困难,而问题中链接的论文几乎完全集中在该主题上。
本文描述的算法的目标是将两个输入矢量A和B分解成可以独立工作的多个较小的片段,以暴露并行性。特别是,目标是使所有SM在GPU中保持忙碌,并使所有SP保持在SM忙碌状态。一旦完成了足够的工作分解,每个SP最终都会执行顺序合并(如本文所述):
- 合并阶段 - 每个核心合并两个子数组 它是使用与a相同的算法给出的 简单的顺序合并。
醇>
但是,正如您所指出的,您想要做的事情有些不同。您已经拥有许多阵列,并且希望在这些阵列上执行独立合并操作。由于您的阵列数大约为100,000,因此考虑将每个阵列映射到GPU SP(即线程)是足够的独立工作。这意味着我们就像在论文中一样,在每个核心/ SP /线程上使用简单的顺序合并。因此,暴露并行性的问题在你的情况下已经完成(可能已经足够程度了)。
此时我们可以考虑按原样实施。我稍后展示的代码将此作为比较的起点。然而,我们发现性能不是很好,这是因为合并算法从根本上具有依赖于数据的访问序列,因此(更难)安排在GPU上进行合并访问。本文的作者提出通过首先将数据(以合并方式)读入共享内存,然后让算法在共享内存中对其进行处理来缓解此问题,其中对无组织访问的惩罚较少。
我建议采用不同的方法:
这是一个实现上述想法的有效例子,在每个线程中运行一个简单的顺序合并,每个线程将一个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
$
注意:
当内存组织为行主要时,GPU实际上比天真的单线程CPU代码慢。但对于列主要组织(往往会增加合并访问的机会),GPU代码比我的测试用例的CPU代码快约10倍。对于GPU MergePath 32位整数加速与x86串行合并,此~10倍加速因子大致在加速因子的范围(~10-20x)内。
使用int
与float
数据类型会对CPU时序产生显着影响。 int
似乎更快(在CPU上),所以我在这里显示该版本。 (这种差异也在论文中提到。)
添加到编译命令的-DTIMING
开关减少了第一个验证功能,以便它只进行CPU合并操作,用于计时。
基本合并代码模板化,能够处理不同的数据类型,并进行参数化,以便可以在列主要操作或行主要操作中使用。
为了简化陈述,我已经免除了CUDA错误检查。但是,如果您在使用CUDA代码时遇到问题,则应始终使用proper cuda error checking。
如何使用推力(正如我在评论中所建议的那样)?应该可以使用thrust :: merge与合适的设备/顺序执行策略,或多或少地模仿我上面所做的。 然而,推力期望向量是连续的,因此,没有额外的复杂性,它只能用于行主要情况,我们已经看到它受到不良内存访问模式的严重惩罚。应该可以在推力中创建一组置换迭代器,这将允许列主要的跨步访问改善内存场景,但我没有追求。