原始问题
我有以下内核使用非均匀节点执行插值,我想优化它:
__global__ void interpolation(cufftDoubleComplex *Uj, double *points, cufftDoubleComplex *result, int N, int M)
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int PP;
double P;
const double alfa=(2.-1./cc)*pi_double-0.01;
double phi_cap_s;
cufftDoubleComplex temp;
double cc_points=cc*points[i];
double r_cc_points=rint(cc*points[i]);
temp = make_cuDoubleComplex(0.,0.);
if(i<M) {
for(int m=0; m<(2*K+1); m++) {
P = (K*K-(cc_points-(r_cc_points+m-K))*(cc_points-(r_cc_points+m-K)));
if(P>0.) phi_cap_s = (1./pi_double)*((sinh(alfa*sqrt(P)))/sqrt(P));
if(P<0.) phi_cap_s = (1./pi_double)*((sin(alfa*sqrt(-P)))/sqrt(-P));
if(P==0.) phi_cap_s = alfa/pi_double;
PP = modulo((r_cc_points + m -K ),(cc*N));
temp.x = temp.x+phi_cap_s*Uj[PP].x;
temp.y = temp.y+phi_cap_s*Uj[PP].y;
}
result[i] = temp;
}
}
K和cc是常数,点包含节点,Uj是要插值的值。 modulo是一个基本上以%为单位的函数,但正确地扩展为负值。对于某种安排,内核调用需要2.3ms。我已经确认最贵的部件是
if(P>0.) phi_cap_s = (1./pi_double)*((sinh(alfa*sqrt(P)))/sqrt(P));
if(P<0.) phi_cap_s = (1./pi_double)*((sin(alfa*sqrt(-P)))/sqrt(-P));
if(P==0.) phi_cap_s = alfa/pi_double;
占总时间的40%左右,
PP = modulo((r_cc_points + m -K ),(cc*N));
temp.x = temp.x+phi_cap_s*Uj[PP].x;
temp.y = temp.y+phi_cap_s*Uj[PP].y;
大约需要60%。通过Visual Profiler,我已经验证了前者的性能不受if
语句的影响。请注意,我想要双精度,所以我避免使用__exp()解决方案。我怀疑,对于后者,“随机”内存访问Uj [PP]可能是这么多计算百分比的原因。有关减少计算时间的技巧或评论的建议吗?提前谢谢。
评论和答案后面的版本
根据答案和评论中提供的建议,我最终得到了以下代码:
__global__ void interpolation(cufftDoubleComplex *Uj, double *points, cufftDoubleComplex *result, int N, int M)
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int PP;
double P,tempd;
const double alfa=(2.-1./cc)*pi_double-0.01;
cufftDoubleComplex temp = make_cuDoubleComplex(0.,0.);
double cc_points=cc*points[i];
double r_cc_points=rint(cc_points);
cufftDoubleComplex rtemp[(2*K+1)];
double phi_cap_s[2*K+1];
if(i<M) {
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
PP = modulo(((int)r_cc_points + m -K ),(cc*N));
rtemp[m] = Uj[PP]; //2
P = (K*K-(cc_points-(r_cc_points+(double)(m-K)))*(cc_points-(r_cc_points+(double)(m-K))));
if(P<0.) {tempd=rsqrt(-P); phi_cap_s[m] = (1./pi_double)*((sin(alfa/tempd))*tempd); }
else if(P>0.) {tempd=rsqrt(P); phi_cap_s[m] = (1./pi_double)*((sinh(alfa/tempd))*tempd); }
else phi_cap_s[m] = alfa/pi_double;
}
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
temp.x = temp.x+phi_cap_s[m]*rtemp[m].x;
temp.y = temp.y+phi_cap_s[m]*rtemp[m].y;
}
result[i] = temp;
}
}
特别是: 1)我将全局存储器变量Uj移动到大小为2 * K + 1的寄存器rtemp数组(在我的情况下,K是一个等于6的常数); 2)我将变量phi_cap_s移动到2 * K + 1大小的寄存器; 3)我使用if ... else语句而不是之前使用的if语句(条件P <0和P> 0具有相同的发生概率); 3)我为平方根定义了额外的变量; 4)我使用rsqrt而不是sqrt(只要我知道,sqrt()由CUDA计算为1 / rsqrt());
我一次添加一个新功能,验证原始版本的改进,但我必须说没有一个给我任何相关的改进。
执行速度受以下因素限制: 1)sin / sinh函数的计算(大约40%的时间);是否有任何方法可以通过某种方式利用内在数学作为“起始猜测”来计算双精度算术中的它们? 2)由于映射索引PP,许多线程最终访问相同的全局存储器位置Uj [PP];避免它的一种可能性是使用共享内存,但这意味着强大的线程合作。
我的问题是。我做完了吗?也就是说,改善代码有什么意义吗?我通过NVIDIA Visual Profiler分析了代码,结果如下:
IPC = 1.939 (compute capability 2.1);
Global Memory Load Efficiency = 38.9%;
Global Memory Store Efficiency = 18.8%;
Warp Execution Efficiency = 97%;
Instruction Replay Overhead = 0.7%;
最后,我想注意这个讨论与CUDA: 1-dimensional cubic spline interpolation in CUDA
的讨论有关使用共享内存的版本
我已经就使用共享内存进行了可行性研究。我考虑了N=64
,以便整个Uj
适合共享内存。下面是代码(基本上是我的原始版本)
__global__ void interpolation_shared(cufftDoubleComplex *Uj, double *points, cufftDoubleComplex *result, int N, int M)
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int PP;
double P;
const double alfa=(2.-1./cc)*pi_double-0.01;
double phi_cap_s;
cufftDoubleComplex temp;
double cc_points=cc*points[i];
double r_cc_points=rint(cc*points[i]);
temp = make_cuDoubleComplex(0.,0.);
__shared__ cufftDoubleComplex Uj_shared[128];
if (threadIdx.x < cc*N) Uj_shared[threadIdx.x]=Uj[threadIdx.x];
if(i<M) {
for(int m=0; m<(2*K+1); m++) {
P = (K*K-(cc_points-(r_cc_points+m-K))*(cc_points-(r_cc_points+m-K)));
if(P>0.) phi_cap_s = (1./pi_double)*((sinh(alfa*sqrt(P)))/sqrt(P));
if(P<0.) phi_cap_s = (1./pi_double)*((sin(alfa*sqrt(-P)))/sqrt(-P));
if(P==0.) phi_cap_s = alfa/pi_double;
PP = modulo((r_cc_points + m -K ),(cc*N));
temp.x = temp.x+phi_cap_s*Uj_shared[PP].x;
temp.y = temp.y+phi_cap_s*Uj_shared[PP].y;
}
result[i] = temp;
}
}
结果再次没有显着改善,尽管这可能取决于输入数组的小尺寸。
VERBOSE PTXAS OUTPUT
ptxas : info : Compiling entry function '_Z13interpolationP7double2PdS0_ii' for 'sm_20'
ptxas : info : Function properties for _Z13interpolationP7double2PdS0_ii
352 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
ptxas : info : Used 55 registers, 456 bytes cumulative stack size, 52 bytes cmem[0]
P的值,第一次WAR并且m = 0
0.0124300933082964
0.0127183892149176
0.0135847002913749
0.0161796378170038
0.0155488126345702
0.0138890822153499
0.0121163187739057
0.0119998374528905
0.0131600831194518
0.0109574866163769
0.00962949548477354
0.00695850974164358
0.00446426651940612
0.00423369284281705
0.00632921297092537
0.00655137618976198
0.00810202954519923
0.00597974034698723
0.0076811348379735
0.00604267951733561
0.00402922460255439
0.00111841719893846
-0.00180949615796777
-0.00246283218698551
-0.00183256444286428
-0.000462696661685413
0.000725108980390132
-0.00126793006072035
0.00152263101649197
0.0022499598348702
0.00463681632275836
0.00359856091027666
MODULO FUNCTION
__device__ int modulo(int val, int modulus)
{
if(val > 0) return val%modulus;
else
{
int P = (-val)%modulus;
if(P > 0) return modulus -P;
else return 0;
}
}
根据答案优化的模块功能
__device__ int modulo(int val, int _mod)
{
if(val > 0) return val&(_mod-1);
else
{
int P = (-val)&(_mod-1);
if(P > 0) return _mod -P;
else return 0;
}
}
答案 0 :(得分:1)
//your code above
cufftDoubleComplex rtemp[(2*K+1)] //if it fits into available registers, assumes K is a constant
if(i<M) {
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
PP = modulo((r_cc_points + m -K ),(cc*N));
rtemp[m] = Uj[PP]; //2
}
#pragma unroll
for(nt m=0; m<(2*K+1); m++) {
P = (K*K-(cc_points-(r_cc_points+m-K))*(cc_points-(r_cc_points+m-K)));
// 1
if(P>0.) phi_cap_s = (1./pi_double)*((sinh(alfa*sqrt(P)))/sqrt(P));
else if(P<0.) phi_cap_s = (1./pi_double)*((sin(alfa*sqrt(-P)))/sqrt(-P));
else phi_cap_s = alfa/pi_double;
temp.x = temp.x+phi_cap_s*rtemp[m].x; //3
temp.y = temp.y+phi_cap_s*rtemp[m].y;
}
result[i] = temp;
}
添加了if if和else,因为这些条件是互斥的,如果可以,您应该在发生概率后对语句进行排序。例如。如果P <0。大多数时候,你应该先评估一下。
这会将请求的内存提取到多个寄存器,之前您所做的事情肯定会导致该线程上的阻塞,因为没有及时的内存可用于计算。请记住,如果一个线程在经线中阻塞,则整个扭曲被阻止。如果就绪队列中没有足够的warp,程序将阻塞,直到任何warp准备就绪。
这应该起作用的原因如下:
来自GMEM中的内存请求大约为&gt; ~400-600个小时。如果一个线程试图对内存不可用的内存执行操作,它将阻塞。这意味着如果每个内存请求不在L1-L2中,则每个warp必须等待该时间或更长时间才能继续。
我怀疑temp.x+phi_cap_s*Uj[PP].x
正在这样做。通过暂存(步骤2)每个存储器传输到寄存器,并继续进行下一个阶段,您将通过允许您在传输内存时执行其他工作来隐藏延迟。
当你到达第3步时,希望可以使用内存,或者你必须等待更短的时间。
如果rtemp
不适合寄存器以达到100%的占用率,您可能需要分批进行。
您也可以尝试将phi_cap_s
放入数组并将其放入第一个循环中,如下所示:
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
//stage memory first
PP = modulo((r_cc_points + m -K ),(cc*N));
rtemp[m] = Uj[PP]; //2
P = (K*K-(cc_points-(r_cc_points+m-K))*(cc_points-(r_cc_points+m-K)));
// 1
if(P>0.) phi_cap_s[m] = (1./pi_double)*((sinh(alfa*sqrt(P)))/sqrt(P));
else if(P<0.) phi_cap_s[m] = (1./pi_double)*((sin(alfa*sqrt(-P)))/sqrt(-P));
else phi_cap_s[m] = alfa/pi_double;
}
#pragma unroll
for(nt m=0; m<(2*K+1); m++) {
temp.x = temp.x+phi_cap_s[m]*rtemp[m].x; //3
temp.y = temp.y+phi_cap_s[m]*rtemp[m].y;
}
表达
P = (K*K-(cc_points-(r_cc_points+(double)(m-K)))*(cc_points-(r_cc_points+(double)(m-K))));
可以分解为:
const double cc_diff = cc_points-r_cc_points;
double exp = cc_diff - (double)(m-K);
exp *= exp;
P = (K*K-exp);
这可能会减少使用的指令数量。
__global__ void interpolation(cufftDoubleComplex *Uj, double *points, cufftDoubleComplex *result, int N, int M)
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int PP;
double P,tempd;
cufftDoubleComplex rtemp[(2*K+1)];
double phi_cap_s[2*K+1];
if(i<M) {
const double cc_points=cc*points[i];
cufftDoubleComplex temp = make_cuDoubleComplex(0.,0.);
const double alfa=(2.-1./cc)*pi_double-0.01;
const double r_cc_points=rint(cc_points);
const double cc_diff = cc_points-r_cc_points;
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
PP = m-k; //reuse PP
double exp = cc_diff - (double)(PP); //stage exp to be used later, will explain
PP = modulo(((int)r_cc_points + PP ),(cc*N));
rtemp[m] = Uj[PP]; //2
exp *= exp;
P = (K*K-exp);
if(P<0.) {tempd=rsqrt(-P); phi_cap_s[m] = (1./pi_double)*((sin(alfa/tempd))*tempd); }
else if(P>0.) {tempd=rsqrt(P); phi_cap_s[m] = (1./pi_double)*((sinh(alfa/tempd))*tempd); }
else phi_cap_s[m] = alfa/pi_double;
}
#pragma unroll //unroll the loop
for(int m=0; m<(2*K+1); m++) {
temp.x = temp.x+phi_cap_s[m]*rtemp[m].x;
temp.y = temp.y+phi_cap_s[m]*rtemp[m].y;
}
result[i] = temp;
}
}
我所做的是移动到if语句中的所有计算中,以便在计算和内存提取方面释放一些资源,不知道你在第一个if语句if(i<M)
上的分歧。由于m-K
在代码中出现了两次,我首先将其放在PP
中,以便在您计算exp
和PP
时使用。
你还可以做的是尝试订购指令,这样,如果你设置一个变量,在下一次使用所述变量之间尽可能多的指令,因为它需要大约20个抽头。设置到寄存器中。因此,我将常量cc_diff置于顶部,因为这只是一条指令,它可能没有任何好处。
__device__ modulo(int val, int _mod) {
int p = (val&(_mod-1));// as modulo is always the power of 2
if(val < 0) {
return _mod - p;
} else {
return p;
}
}
由于我们_mod
总是作为2的幂的整数(cc = 2, N = 64, cc*N = 128
),我们可以使用此函数而不是mod运算符。这应该“更快”。检查它,所以我有算术正确。它来自Optimizing Cuda - Part II Nvidia第14页。
答案 1 :(得分:0)
您可能希望了解的一个优化是使用快速数学。使用内在数学函数并使用-use-fast-math选项进行编译。