问题描述:求这样一个step*16矩阵中每列的最小值,并存到第一行返回输出。
0 | 1 | 2 | step=3360*64 | ||||||||
group0 | data0->data0.min[0] | min[1] | min[2] | ... | ... | ... | ... | ... | ... | min[step-1] | |
group1 | data1 | ||||||||||
group2 | data2 | ||||||||||
... | |||||||||||
group15 | data15 |
具体情况:初始想法希望通过归约算法实现,但是在cuda的核函数中出现了如下问题。
错误代码:
调用该核函数,在核函数内自己循环4次,则计算结果出错(i的步长是2,限制条件是<9)。
__global__ void qumin_error (float *idata_dev)
{
int blockId = blockIdx.x + blockIdx.y * gridDim.x;
int threadId = blockId * (blockDim.x * blockDim.y)+ (threadIdx.y * blockDim.x) + threadIdx.x;
unsigned int index = threadId;
int step=3360*64;
for (int i = 1; i <9; i =i* 2)//16组数据,进行4次循环归约
{
if ((index % (2 * i * step))<(step* i))
{
if (idata_dev[index] > idata_dev[index + (i)* step])
{
idata_dev[index] = idata_dev[index + (i)* step];
}
}
}
}
int main(void)
{
qumin_error << <dimgrid1, dimblock >> >(idata_dev_min);//错误调用方法
}
为了解决这个问题,本文给出了三种优化方法得到正确答案,三种方法的实现逐步加深,层层递进,可依次查看。
优化1:循环多次调用核函数
核函数内只做一次归约运算,主函数多次单独调用下面的函数qumin_simple函数,也就是把原来核函数的循环拿了出来,结果正确。
__global__ void qumin_proj1(float *idata_dev,int num)
{
int blockId = blockIdx.x + blockIdx.y * gridDim.x;
int threadId = blockId * (blockDim.x * blockDim.y)+ (threadIdx.y * blockDim.x) + threadIdx.x;
unsigned int index = threadId;
for (int i = num; i <num +1; i = i * 2)
{
if ((index % (2 * i * Step_Sum))<(Step_Sum * i))
{
if (idata_dev[index] > idata_dev[index + (i)* Step_Sum])
{
idata_dev[index] = idata_dev[index + (i)* Step_Sum];
}
}
}
}
int main(void)
{
for (int i = 1; i < 9; i=i*2)
{
qumin_proj1<< <dimgrid1, dimblock >> >(idata_dev_min,i);//正确调用方法
}
}
下面记录一下正确结果的前20个值:
错误结果的前20个值:
其中:调用核函数语句没有任何改变,没有具体考虑线程数与数据量的对应关系。
dimgrid1=(2,16*105) ; dimblock=(32,32)
由于核函数的功能是归约求最小值,我们可以看出正确结果的前20个值比错误结果的更小,可以猜想错误函数可能并没有遍历到所有待比较数据。
优化2:改变调用架构
为了处理此问题,我们首先选择了一种优化算法,它放弃了归约求和的逻辑架构。
这是一个thread线程求16个数的最小值的运算。由于Step_Sum=64*3360,一个block无法处理这么多数据,我们设定一个block中有1024个thread,一个grid中,有210个block(1024*210=step),block0可以计算出前1024列数据的最小值,以此类推。
并且我们用tmp作为中间变量去存储计算过程中的最小值,最后再将最值数据赋给idata_dev,这样得到的访存效率是最高的。这种计算架构虽然循环次数增加了,但是对硬件的计算性能发挥到了最大,比原始归约算法更高效。
block0 | block1 | ||||||||||
0 | 1 | 2 | ... | 1023 | 1024 | 2048 | step=3360*64 | ||||
group0 | data0->data0.min[0] | min[1] | min[2] | ... | ... | ... | ... | ... | ... | min[step-1] | |
group1 | data1 | ||||||||||
group2 | data2 | ||||||||||
... | |||||||||||
group15 | data15 |
__global__ void qumin_proj2(float *idata_dev)//最优解法
{
int blockId = blockIdx.x;//210
int threadId = blockId * (blockDim.x * blockDim.y) + threadIdx.x;// (0~240)*1024+1024
unsigned int index = threadId;
float tmp;
tmp = idata_dev[index];
for (int i = 1; i <16; i++ )
{
if (tmp > idata_dev[index + (i)* Step_Sum])
{
tmp = idata_dev[index + (i)* Step_Sum];
}
}
idata_dev[index] = tmp;
return;
}
int main(void)
{
qumin_proj2<<<210,1024>>>(idata_dev_min);
}
反思:如果一定要用归约,如何实现?
应该是一个数组中数据量太大,在设备端上各线程的计算速度不同,在执行中,速度快的线程可能会读取速度慢的线程还未写好的那块内存空间。这是很危险的操作。此时,我们可以选择用共享内存,利用同一线程块的线程的同步来抑制这种情况。
优化3: 正确的归约运算实现
正确实现归约算法的核心思想是:再一次循环内,要让一个block中的线程去做这一次数据归约,不同block中的thread绝对不能读写重复地址的数据,从而避免脏读脏写。
共享内存的适用情况:对于本机,sharedmemoryperblock=48k,而每个float为32bit=4byte,所以我们可以初始化的shared数组的最大为48*1024/4=12288.。只要不超出这个数就都可以,在这里为了方便计算我选择sharedmemory大小为1024。
目前有两种设定block数和thread数的方案:
1.blockDim.x=1;threadDim.x=1024
2.blockDim.x=3360;threadDim.x=1024
3.blockDim.x=3360*16;threadDim.x=64
1.方案1:每一个block中的数据个数刚好为1024个,他每次只处理16组数中每64个数据的最小值。循环3360次,实现功能。
如下图所示:每一列表示一组数据,每一次循环都是将16组数据的最小值放到黄色的第一列中。
group0 | group1 | group15 | |||
第1次循环 | [0,63] | [0,63] | ... | [0,63] | |
第2次循环 | [64,127] | [64,127] | ... | [64,127] | |
....... | .. | .. | ... | ||
第3360次循环 | [3359*64,3360*64-1] | [3359*64,3360*64-1] |
2.方案2是方案1的优化,将方案1的3360次循环改成了调用3360个block,每3360/16=210个block负责一组数据,每一个block中的数据个数刚好为1024个,所以每一个thread负责一个数据,它会将这个数据和后面15组中同位置的数据进行归约比较,将最小值放到第一个位置处。
经过试验:方案1、2成功。
代码:
方案1:
__global__ void qumin_project3_1(float *idata_dev)
{
__shared__ float cache[1024];
int blockId = blockIdx.x;
int threadId = blockId * blockDim.x + threadIdx.x;
int tid = threadIdx.x; //(0,63)
int shared_step = 64;
int shang = tid / shared_step;//一共1024个线程,对64取商相当于计算该线程应该负责第几组数据
int yushu = tid % shared_step;//对64取余相当于计算该线程应该负责这组数据中64个数中的哪一个
int cache_idx = tid + shang * 64;
for (int j = 0; j<3360; j++)
{
//if (64 * j < yushu<64 * (j + 1))
cache[tid] = idata_dev[shang * Step_Sum + yushu+j*64];//store the data into cache [64*16]
for (int i = 1; i <9; i*=2)
{
if (tid % (2 * i*shared_step) < (shared_step*i))
{
if (cache[tid] > cache[tid + (i)* shared_step])
{
cache[tid] = cache[tid + (i)* shared_step];//compare and change the num
}
}
__syncthreads();
if (tid<64)
idata_dev[tid + j*64] = cache[tid];//put the min data back into idata_dev
}
}
}
void main()
{
qumin_type2 << <1, 1024 >> >(idata_dev_min);
}
测试成功后,我尝试了一下去掉__syncthreads()这条线程同步命令,输出结果就和先前的错误结果相同,所以我们可以断定:前面归约算法的问题出在线程同步上。
方案2:优化实现很简单,只是将方案1中的外层循环j,换成blockIdx.x就可以了
__global__ void qumin_project3_2(float *idata_dev)
{
__shared__ float cache[1024];
int blockId = blockIdx.x;
int threadId = blockId * blockDim.x + threadIdx.x;
int tid = threadIdx.x; //(0,63)
int index = threadId;//(0,63)(64,127)...(3359*64,3360*64-1)
int shared_step = 64;
int yushu = tid % shared_step;
int shang = tid / shared_step;
int cache_idx = tid + shang * 64;
cache[tid] = idata_dev[shang * Step_Sum + yushu + blockIdx.x*shared_step];//store the data into cache [64*16]
for (int i = 1; i <9; i *= 2)
{
if (tid % (2 * i*shared_step) < (shared_step*i))
{
if (cache[tid] > cache[tid + (i)* shared_step])
{
cache[tid] = cache[tid + (i)* shared_step];//compare and change the num
}
}
__syncthreads();
if (tid<64)
idata_dev[tid + blockIdx.x * 64] = cache[tid];//put the min data back into idata_dev
}
}
void main()
{
qumin_type2 << <3360, 1024 >> >(idata_dev_min);
}
初始bug出现原因思考:
具体冲突表现描述为:折半归约的过程中,黄色区域与红色区域比较大小,将小值写入黄色区域。在归约的下一次循环中,黄色区域要去和绿色区域比较,但是由于硬件限制,绿色区域的某些线程仍然没有完成上一轮次循环的数据写入,就被读取走了数据,导致发生错误。这样得到的最小值是有问题的。
d1 | d2 | d3 | d4 | d5 | d6 | d7 | d8 |
最小值 |
在原始程序中,我们虽然加上了__syncthreads()命令,但是该命令同步的是同一个线程块内的数据,原情况中是由不同的线程块之间计算最值,所以__syncthreads()无法同步这些计算线程。这也解释了为什么优化1中,只是确保了上一次写完了在进行下一次读取和计算就能得到正确结果。