GPU求解三角方程测试,结果很郁闷!(上代码了)

最近在研究GPU求解下三角矩阵方程Ax=b,

计算步骤:
for n=1:N
1对A的第n行的前n-1个元素和x的前n-1个元素做一次向量点乘;
2将b的第n个元素减去前一步内积的结果;
3最后将第2步中的结果除以A的第n行的第n个元素。
end

实现方法:
由于A为稀疏矩阵,存储时采用行稀疏存储,在global memory的三个矩阵value,index,count中分别记录了A每一行中非0元素的值、列坐标和第一个非0元素在value或index矩阵中的位置。

为了避免线程空置,循环在内核函数之外实现,每次循环内部的计算由内核函数完成,每次循环开始时将块(block)数设置为需要求解的方程个数,块中线程个数设置为A的第n行中非0元素的个数。第一步中先多线程并行完成两个向量的元素相乘,并将结果写入shared memory,然后将进行规约求和。最后再让第一个线程完成第二步和第三步。编程中避免了bank conflicts并实现了global memory的coalesces访问(即访问shared memory和global memory时让连续的线程访问连续的存储空间);尽量减少了对global memory的访问;整个计算过程中内存和GPU之间无数据交换。

内核函数
global void HH_device(float* b,float* v_gpu,int* i_gpu,int* worder_gpu,int* wst_gpu)
{
const int tid = threadIdx.x;
const int bid = blockIdx.x;
int i,k,l,n,m,count1,count2,count3,ii,jj,windex;
float temp1r,temp2r,temp3r,temp4r;
windex=worder_gpu[bid+wst_gpu[0]];
i=i_gpu[0];
count1=blockDim.x;
count2=2windexnodecount_gpu[0];
count3=windex*(nodecount_gpu[0]+1);
shared float Mv[2050];
ii=tex1Dfetch(rT_Hcount2,i+count3);
jj=tex1Dfetch(rT_Hcount2,i+1+count3)-ii-1;
if (tid==0)
{
Mv[0]=0;
Mv[count1]=0;
Mv[2050-2]=tex1Dfetch(rT_HMre2,ii+jj);
Mv[2050-1]=tex1Dfetch(rT_HMim2,ii+jj);
}
// 向量元素相乘
if (tid<jj)
{
temp1r=tex1Dfetch(rT_HMre2,ii+tid);
temp2r=tex1Dfetch(rT_HMim2,ii+tid);
k=tex1Dfetch(rT_Hindex2,ii+tid);
temp3r=v_gpu[k+count2];
temp4r=v_gpu[k+count2+nodecount_gpu[0]];
Mv[tid]=temp1rtemp3r-temp2rtemp4r;
Mv[tid+count1]=temp1rtemp4r+temp2rtemp3r;
}
__syncthreads();

//规约求和
k=ceilf(log2f(jj*1.0));
m=jj;
n=jj;
for(l=0;l<k;l++)
{
m=floorf(n/2.0);

if(tid<m)
{
Mv[tid]=Mv[tid]+Mv[n-tid-1];
Mv[tid+count1]=Mv[tid+count1]+Mv[count1+n-tid-1];
}
n=n-m;
__syncthreads();
}
//第二步和第三步中的减法和除法
if (tid==0)
{
temp1r=Mv[2050-2]*Mv[2050-2]+Mv[2050-1]*Mv[2050-1];
temp3r=b[i+count2]-Mv[0];
temp4r=b[i+count2+nodecount_gpu[0]]-Mv[count1];
v_gpu[i+count2]=(Mv[2050-2]*temp3r+Mv[2050-1]*temp4r)/temp1r;
v_gpu[i+count2+nodecount_gpu[0]]=(Mv[2050-2]*temp4r-Mv[2050-1]*temp3r)/temp1r;
}
//行数加1
i=i+1;
if (tid==0)
{
i_gpu[0]=i;
}
}

主函数
tempsize[0]=0;
cudaMemcpy(i_gpu,tempsize, sizeof(int) *1,cudaMemcpyHostToDevice);
grid.x=wcount-wst[0];
for (i=0;i<nodecount;i++)
{
block.x=Hblock2;
HH_device<<<grid, block>>>(r_gpu,p_gpu,i_gpu,worder_gpu,wst_gpu);
}
//r_gpu为b,p_gpu为x。HMre为A矩阵非0元素实部,HMim为虚部,Hindex为非0元素列坐标,Hcount为每行非0元素起始位置,后面4个矩阵已事先拷入显存并绑定为纹理。

运行结果:
GTX580 3G 对比 I7 3770 RAM 32G,当只求解一个方程时,GPU完全处于劣势,求解方程个数增加到20个左右时GPU并行求解速度和CPU串行求解速度基本相当,当求解方程个数为54时,加速比还不到2,方程数为2592时加速比才5左右,似乎并行计算的效率不高啊。

PS:其实实际计算时间很短,每个方程的未知数个数为15000左右,求解2592个方程总的计算时间为2 秒,但不知是不是I7性能变态,搞得加速比很难看。

不知各位对这个问题有什么好办法没有?

LZ您好,您整个意思描述还是比较清楚的,但是仅凭您给出的叙述,我们还是难以得知您的实现细节,因为难以判断您的问题所在。

所以建议将您的具体问题分散开,一点一点说,以及最好配上实现的代码段,这样才能让看帖子的人明白和理解。

以及,您也可以跑跑profiler,看看有什么分析意见,问题出在哪里。这些信息都有助于我们优化代码。

大致建议如上,祝您预祝您腊八节愉快~

从楼主描述的情况来看,很多优化的细节都注意到了。有个问题我想问一下,首先是你的程序是计算的双精度还是单精度?其次有可能是因为规模较小所以GPU得优势没有发挥出来!

变量都是float 和int 类型的,那些加减乘除和求余计算应该是单精度的吧,计算的最大规模被显存限制了。执行的block个数到了2000多,每个block的内部还有几百个线程,并行规模应该不算小了。

楼主可以用visual profile跑一次你的程序,然后分析一下性能的瓶颈在哪里!至少看代码我觉得大部分都没什么问题,唯一能改进的是reduction的部分。还是先跑一次visual profile吧,或许能找到问题所在!

LZ在顶楼说“求解2592个方程总的计算时间为2 秒”,同时在4#说计算规模被显存限制住了。
我觉得这个有可能是访存限制了效能。

建议LZ跑profiler看看,以及把kernel拆的小一点,更清晰一些。

祝您编码顺利~

更新一下进展:
仔细检查代码之后发现各个稀疏矩阵同一行的非0元素差异比较大,以此后面的规约求和次数也有不同,这导致了我前面代码中必须让一个block去负责一个方程的计算,而且block的大小只能按照所有矩阵同一行中最大的非0元素个数来设置,因此浪费了很多线程。
现在我在前面计算中先统一了各个稀疏矩阵同一行的非零元素个数,以实现同一个block中计算多个方程。以warp为单位,看一个矩阵的每一行非0元素需要多少个warp,并限制每一个block的warp个数不超过8个,将多个方程的计算合并到了一个block中,大大减少了block的个数,提高了计算效率。现在2592个方程的计算时间降低到了0.9秒,加速比大约为14.5倍。新的代码如下:

global void HH_device(float* b,float* v_gpu,int* i_gpu,int* worder_gpu,int* wst_gpu)
{
const int tid = threadIdx.x;
const int bid = blockIdx.x;
int i,k,l,count1,count2,count3,ii,jj,windex,nodeindex;
float temp1r,temp2r,temp3r,temp4r;
shared float Mre[272];
shared float Mim[272];
i=i_gpu[0];
ii=tex1Dfetch(rT_Hcount2,i);
jj=tex1Dfetch(rT_Hcount2,i+1)-ii-1;
k=wcount_gpu[0]-wst_gpu[0];
count1=((jj+31)>>5)32;
count1=max(count1,32);
count2=tid/count1;
windex=blockDim.x/count1;
windex=windex
bid+count2;
if (windex<k)
{
windex=worder_gpu[wst_gpu[0]+windex];
nodeindex=tid%count1;
count3=tex1Dfetch(rT_Hcount2,nodecount_gpu[0]);
ii=ii+windexcount3;
count3=2
windexnodecount_gpu[0];
if (nodeindex==0)
{
Mre[count1
count2]=0;
Mim[count1count2]=0;
Mre[256+count2]=tex1Dfetch(rT_HMre2,ii+jj);
Mim[256+count2]=tex1Dfetch(rT_HMim2,ii+jj);
Mre[264+count2]=b[i+count3];
Mim[264+count2]=b[i+count3+nodecount_gpu[0]];
}
if (nodeindex<jj)
{
temp1r=tex1Dfetch(rT_HMre2,ii+nodeindex);
temp2r=tex1Dfetch(rT_HMim2,ii+nodeindex);
k=tex1Dfetch(rT_Hindex2,ii+nodeindex);
temp3r=v_gpu[k+count3];
temp4r=v_gpu[k+count3+nodecount_gpu[0]];
Mre[count1
count2+nodeindex]=temp1rtemp3r-temp2rtemp4r;
Mim[count1count2+nodeindex]=temp1rtemp4r+temp2r*temp3r;
}
__syncthreads();

ii=jj;
for(l=jj>>1;l>0;l=ii>>1)
{
if(nodeindex<l)
{
Mre[count1count2+nodeindex]=Mre[count1count2+nodeindex]+Mre[count1count2+ii-nodeindex-1];
Mim[count1
count2+nodeindex]=Mim[count1count2+nodeindex]+Mim[count1count2+ii-nodeindex-1];
}
ii=ii-l;
__syncthreads();
}
if (nodeindex==0)
{
temp1r=Mre[256+count2]Mre[256+count2]+Mim[256+count2]Mim[256+count2];
temp3r=Mre[264+count2]-Mre[count1
count2];
temp4r=Mim[264+count2]-Mim[count1
count2];
v_gpu[i+count3]=(Mre[256+count2]*temp3r+Mim[256+count2]*temp4r)/temp1r;
v_gpu[i+count3+nodecount_gpu[0]]=(Mre[256+count2]*temp4r-Mim[256+count2]*temp3r)/temp1r;
}
__syncthreads();
}
i=i+1;
if ((tid==0)&&(bid==0))
{
i_gpu[0]=i;
}
}

如果继续拆分kernel的话第一步向量乘法之后的结果需要放置到显存中保存,然后再在第二步规约求和时再从显存中读到共享内存,这个操作的访问延迟很有可能淹没掉线程个数减少一半所带来的效率提高。我这个三角矩阵方程求解是ICCG迭代法的一部分,为了避免迭代中内存和显存的数据交换,其他计算步骤的数据也必须全部读入显存,因此进一步拆分kernel也难以提高并行的规模。

profiler貌似挺复杂的,还在摸索中!

首先恭喜LZ取得进展,visual profiler神马的不难上手的,建议先看看自带的pdf手册,论坛一些帖子也有涉及。

预祝您春节愉快~