voidmerge_sequential(int*A,int*B,int*C,intm,intn){inti=0,j=0,k=0;// Indices for A, B, and C
while(i<m&&j<n){if(A[i]<B[j]){C[k++]=A[i++];}else{C[k++]=B[j++];}if(i==m){// Done with A[], handling remaining B
while(j<n){C[k++]=B[j++];}}else{// Done with B[], handling remaining A
while(i<m){C[k++]=A[i++];}}}}
intco_rank(intk,int*A,intm,int*B,intn){// C[k] comes from A[i] of B[j]
// k = i + j
inti=k<m?k:m;// max starting search value for A, i.e. A[k-1] < B[0]
inti_low=0>(k-n)?0:k-n;// when B is done, min starting search value for A is k-n
intj=k-i;intj_low=0>(k-m)?0:(k-m);intdelta;boolactive=true;while(active){// Binary search for C[k]
if(i>0&&j<n&&A[i-1]>B[j]){delta=(i-i_low+1)>>1;j_low=j;j+=delta;i-=delta;}elseif(j>0&&i<m&&B[j-1]>A[i]){delta=(j-j_low+1)>>1;i_low=i;i+=delta;j-=delta;}else{// Found the correct position for C[k]
active=false;}returni;}}
在剩下的小节里,我们假设输入数组 A 和 B 存储在全局内存中,一个内核被启动用来合并两个输入数组,输出一个同样位于全局内存中的数组 C.
下面内核是并行归并的直接实现。它首先通过计算当前线程 (k_curr) 和下一个线程 (k_next) 产生的输出子数组的起点来确定负责输出的范围。然后分别调用自己和后一个线程的 co_rank 函数来确定对应的 A 和 B 输入子数组的范围。最后调用顺序合并函数来合并两个输入子数组,并将结果写入输出子数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
__global__voidmergre_basic_kernel(int*A,int*B,int*C,intm,intn){// Each thread handles a section of C
inttid=blockIdx.x*blockDim.x+threadIdx.x;intelementsPerThread=ceil(m+n)/(blockDim.x*gridDim.x);intstart=tid*elementsPerThread;intend=std::min(start+elementsPerThread,m+n);// Determin the range of A and B to be merged for this thread
inti_curr=co_rank(start,A,m,B,n);inti_next=co_rank(end,A,m,B,n);intj_curr=start-i_curr;intj_next=end-i_next;merge_sequential(A+i_curr,B+j_curr,C+start,i_next-i_curr,j_next-j_curr);}
注意到相邻线程使用的 A 和 B 子数组在内存中彼此相邻。我们可以为为每个块调用 co-rank 函数来获得其 A 和 B 子数组的起始和结束位置。
Info
回忆一下改进内核内存合并的主要策略有三种:
重新组织线程到数据的映射。
重新组织数据本身。
以合并的方式在全局内存和共享内存之间传输数据,并在共享内存中执行不规则访问。
下图展示了分段合并内核的块级别设计。A_S 和 B_S 可能无法覆盖块的整个输入子数组,因此在每次迭代期间,块中的所有线程将协作从块的 A 和 B 子数组中加载 x 个元素。这样每个块有足够的输入元素来生成至少 x 个输出数组元素 (在最坏的情况下,当前输出部分的所有元素可能都来自 A 或 B 的子数组)。假设每个块负责 y 个输出元素,则需要进行 y/x 次归并。每个块中的线程将在每次迭代中使用 A_S 的一部分和 B_S 的一部分 (深灰色部分)
__global__voidmerge_tiled_kernel(int*A,int*B,int*C,intm,intn,inttile_size){/* Part 1: Identify block-level output & input subarrays */// Use extern keywords to determine
// the shared memory size at runtime rather than compilation
extern__shared__intshared_AB[];int*A_s=&shared_AB[0];// Start index of ShareA
int*B_s=&shared_AB[tile_size];// Start index of ShareB
intC_curr=blockIdx.x*ceil((m+n)/gridDim.x);// Start index of C for this block
intC_next=std::min(C_curr+int(ceil((m+n)/gridDim.x)),m+n);// End index of C for this block
if(threadIdx.x==0){A_s[0]=co_rank(C_curr,A,m,B,n);// Make block level co-rank values visible
A_s[1]=co_rank(C_next,A,m,B,n);// Next threads co-rank values in the block
}__synctyhreads();intA_curr=A_s[0];intA_next=A_s[1];intB_curr=C_curr-A_curr;intB_next=C_next-A_next;
第二部分线程使用它们的 threadIdx.x 的值来确定要加载的元素,因此连续的线程加载连续的元素,内存访问是合并的。每次迭代从 A 和 B 数组中加载当前tile的起始点取决于块的所有线程在之前的迭代中消耗的 A 和 B 元素的总数。下图说明了 while 循环第二次迭代的索引计算。每个块在第一次迭代中消耗的 A 元素部分 为 A 子数组开头的白色小部分 (用竖条标记)。if 语句确保线程只加载 A 子数组剩余部分中的元素。
/* Part 2: Loading A & B elements into the shared memory */intcounter=0;intlenC=C_next-C_curr;intlenA=A_next-A_curr;intlenB=B_next-B_curr;intnum_iterations=ceil(lenC/tile_size);// index of completed merge in
intC_completed=0;intA_completed=0;intB_completed=0;while(counter<num_iterations){// Each iter threads in a block will generate tile_size C elements
// Loading tile_size A and B elements into shared memory
for(inti=0;i<tile_size;i+=blockDim.x){// Coalecsing loading from global memory
if(i+threadIdx.x<lenA-A_completed){A_s[i+threadIdx.x]=A[i+threadIdx.x+A_curr+A_completed];}if(i+threadIdx.x<lenB-B_completed){B_s[i+threadIdx.x]=B[i+threadIdx.x+B_curr+B_completed];}}__syncthreads();
/* Part 3: All threads merge their subarrays in prallel */intc_curr=threadIdx.x*(tile_size/blockDim.x);// Output index in shared memory
intc_next=c_curr+(tile_size/blockDim.x);c_curr=(c_curr<=lenC-C_completed)?c_curr:lenC-C_completed;c_next=(c_next<=lenC-C_completed)?c_next:lenC-C_completed;// find co-rank for c_curr and c_next
inta_curr=co_rank(c_curr,A_s,std::min(tile_size,lenA-A_completed),B_s,std::min(tile_size,lenB-B_completed));intb_curr=c_curr-a_curr;inta_next=co_rank(c_next,A_s,std::min(tile_size,lenA-A_completed),B_s,std::min(tile_size,lenB-B_completed));intb_next=c_next-a_next;// merge the subarrays
merge_sequential(A_s+a_curr,B_s+b_curr,C+C_curr+C_completed+c_curr,a_next-a_curr,b_next-b_curr);// Update completed indices
C_completed+=tile_size;A_completed+=co_rank(tile_size,A_s,tile_size,B_s,tile_size);// Idx of A_s to generate tile_size Idx of merged A_s and B_s
B_completed+=tile_size-A_completed;}}