在 C 中使用 OpenMP 进行外循环矢量化的问题

Problem with outer loop vectorization using OpenMP in C

提问人:Seth 提问时间:2/23/2023 最后编辑:Seth 更新时间:2/25/2023 访问量:88

问:

我正在研究如何使用 OpenMP 使代码使用多个处理器。最近,我尝试使用 OpenMP 使我的 Ewald 求和傅里叶零件并行。下面是我尝试使用 OpenMP 编译指示并行PME_fourier_oprimized的函数,其中,

ParticleN = Number of cahrged particles in the System (an integer number)
kcount = an integer number
qi = prop[i][0] (a double float) which gives charge of a particle i
Ext_L[0] = x dimension of the system (a double float number)
Ext_L[1] = y dimension of the system (a double float number)
Ext_L[1] = z dimension of the system (a double float number)
**r_pos = ParticleNx3 array (r_pos[i][0], r_pos[i][1], and r_pos[i][2] = x, y , and z postions of a particle i)
**PME_mev_ksq = kcountx4 array
**f_kewald = ParticleNx3 array which stores the forces
 double PME_Fourier_optimized(int kcount, double **r_pos, double **PME_mvec_ksq, double **prop, double **f_kewald, double *Ext_L){
    
    // Auto alpha determination //
    double TRTF = 50*5.5;
    double box_VOL = 8*Ext_L[0]*Ext_L[1]*Ext_L[2]; // calculate the volume of the box //
    alpha = pow((ParticleN*M_PI*M_PI*M_PI*TRTF)/(box_VOL*box_VOL),1./6.); // Ewald cutoff parameter //

    double GAMMA = -0.25/(alpha*alpha);
    double recip = 2*M_PI*ONE_PI_EPS0/(8*Ext_L[0]*Ext_L[1]*Ext_L[2]);
    double e_kewald=0.0;
    
    double fx_kewald[ParticleN];
    double fy_kewald[ParticleN];
    double fz_kewald[ParticleN];

    #pragma omp parallel for
    for (int i=0;i<ParticleN;i++){
        fx_kewald[i] = 0.0;
        fy_kewald[i] = 0.0;
        fz_kewald[i] = 0.0;
        for (int j=0;j<dimen;j++){
            f_kewald[i][j] = 0.0; 
        }
    }
    
    #pragma omp parallel for reduction(+:e_kewald,fx_kewald[:ParticleN],fy_kewald[:ParticleN],fz_kewald[:ParticleN])
    for(int k=0; k<kcount; k++){
        
        double ak_cos=0.0;
        double ak_sin=0.0;
        
        double mx = PME_mvec_ksq[k][0]; 
        double my = PME_mvec_ksq[k][1];
        double mz = PME_mvec_ksq[k][2];
        double ksq = PME_mvec_ksq[k][3];

        #pragma omp parallel for reduction(+:ak_sin,ak_cos)
        for(int i=0;i<ParticleN;i++){
            double qi = prop[i][0]; // charge of particle i //
            double kdotr = mx*r_pos[i][0] + my*r_pos[i][1] + mz*r_pos[i][2];

            ak_sin -= qi*sin(kdotr);
            ak_cos += qi*cos(kdotr);
        }
        
        double a = ak_cos;
        double b = ak_sin;

        double akak = (a*a + b*b);
        double tmp = recip * exp(GAMMA*ksq)/ksq;
        
        #pragma omp parallel for reduction (+:fx_kewald[:ParticleN],fy_kewald[:ParticleN],fz_kewald[:ParticleN])
        for(int i=0;i<ParticleN;i++){
            double qi = prop[i][0];
            double kdotr = mx*r_pos[i][0] + my*r_pos[i][1] + mz*r_pos[i][2];
            double tmp2 = 2*tmp*qi*(sin(kdotr) * a + cos(kdotr) * b);
            
            //Edit3: Following @Laci advice to use 1D array to store the forces and using the reduction to gain a huge acceleration compared to only using omp critical (See the comment section for more details).

            fx_kewald[i] += tmp2 * mx; 
            fy_kewald[i] += tmp2 * my; 
            fz_kewald[i] += tmp2 * mz;
        }
        
        e_kewald += tmp * akak; // add the energies for each k //
    }
    
    //Edit3: Finally storing the calculated values in the 2D array //
    #pragma omp parallel for
    for(int i=0;i<ParticleN;i++){
        f_kewald[i][0] = fx_kewald[i];
        f_kewald[i][1] = fy_kewald[i];
        f_kewald[i][2] = fz_kewald[i];
    }

    //printf("PME KEwald: %lf\n", e_kewald);
    return e_kewald; 
}

我将此代码输出与我的序列号版本进行了比较,发现小数点后 2 位后的能量和f_kewald力e_kewald存在错误。有趣的是,每次运行它时我都会得到不同的结果(也许是数据竞争的情况)。

当我在外部 k 循环之前删除第一个 pragma 命令 ( #pragma omp parallel for private(k, mx, my, mz, ksq, qi, kdotr, tmp2) reduction(+:e_kewald) ) 时,开始完美工作并与串行代码结果完美匹配。我做错了什么吗?我也不确定我使用 pargma 的方式是否正确。任何帮助都非常感谢。

编辑1:在f_kewald数组之前使用 #pragma omp critical解决问题后。但是,不确定这是否是最好的方法。

我的 2d 数组是使用以下函数定义的:

double **alloc_2d_double(int rows, int cols){
    int i=0;
    double *data = (double *)malloc(rows*cols*sizeof(double));
    double **array= (double **)malloc(rows*sizeof(double *));
    for (i=0; i<rows; i++)
        array[i] = &(data[cols*i]);

    return array;
} 

我仍然想知道 f_kewald[i][0]、f_kewald[i][1]、f_kewald[i][2] 是否可以在还原下使用。

编辑2:在循环中声明变量,并将大括号放在omp关键部分。

颗粒N ~ 5000-10000 kcount ~ 2000

编辑3:使用1D数组(fx_kewald,fy_kewald,fz_kewald)来获得增强的并行化速度,而不是使用2D数组的opm关键部分。(感谢下面@Laci的评论)

C 并行处理 OpenMP 争用条件 编译指示

评论

0赞 Simon Goater 2/23/2023
akak 和 tmp 不应该是私有的吗?
0赞 Seth 2/23/2023
我把 akak 和 tmp 放在私人里,但仍然没有运气。
1赞 Victor Eijkhout 2/23/2023
在循环标头中声明所有循环变量。从字面上看,这解决了所有关于OpenMP的问题的一半。
1赞 Victor Eijkhout 2/23/2023
所有这些东西都不是编程的好方法。你没有提供代码,所以我看不到你是否正确地创建了数组,但让我们假设是这样。对于内存局部性,最好将 2D 数组分配为连续块。**
1赞 Gilles 2/23/2023
只是我的一个愚蠢的问题:你在这里做嵌套并行。这是故意的吗?你是如何管理它的?

答:

1赞 anatolyg 2/24/2023 #1

代码的简化视图:

    #pragma omp parallel for ...
    for(k=0; k<kcount; k++){
            ...
            f_kewald[i][0] += ...
            ...
    }

很明显,不同的线程会同时更新,从而导致错误。这发生在所有人身上。f_kewald[i][0]i

当您将所有更新的代码塞入关键部分时,这会有所帮助 - 现在线程会相互等待。但是,更新仍然是任意顺序的,并且关键部分不区分 的不同值。f_kewaldi

我建议你只在一个级别上并行化你的计算 - 决定你是要在外循环还是内循环进行计算,而不是两者兼而有之。假设你的足够大,你不需要并行化你的外循环。这是简单的情况。ParticleN


如果你可以很小,最好并行化你的外循环,但你必须以某种方式解决同步问题。一些想法:ParticleNf_kewald

  • 从里到外切换您的循环
  • 将外环分成几块,每块只包含一个内环

这是一个糟糕的情况——你必须调整你的代码,这可能很困难或不可能。如果你能完全避免它(见上文),请这样做!


不管怎样,很多人都建议不要使用来控制变量对线程的分配。这是个好建议!您应该在适当的 C 范围内声明变量,而不是使用 .这将使转换代码变得更加容易(如果需要的话),并且可以防止令人不快的意外(当您忘记巨型列表中的一个变量时)。privateprivateprivate

评论

0赞 Seth 2/24/2023
在我的情况下,ParticleN ~ 5000 和 kcount ~2000,在这种情况下,我假设内循环并行化可以正常工作。我现在已经从私有中删除了变量并在其中声明了它们。但是,有没有办法在 f_kewald[i][0] 中不使用 omp critical 并使用 reduction?