从随机分布的粒子到规则网格的通信的最佳并行化

时间:2012-11-07 20:18:32

标签: c thread-safety openmp race-condition particles

我正致力于并行化我的粒子在线代码,我用它来模拟2D和3D地球内部的变形。使用OpenMP很容易并行化代码的几个例程并且可以很好地扩展。但是,我在代码的关键部分遇到了问题,这些代码涉及从粒子到网格单元的插值。粒子在每次迭代中移动(根据速度场)。许多计算对于在常规的,不变形的网格上执行是最有效的。因此,每次迭代都涉及从“随机”分布的粒子到网格单元的通信。

问题可以通过以下简化的一维代码来说明:

//EXPLANATION OF VARIABLES (all previously allocated and initialized, 1D arrays)
//double *markerval; // Size Nm. Particle values. Are to be interpolated to the grid
//double *grid; // Size Ng=Nm/100 Grid values. 
//uint *markerpos; // Size Nm. Position of particles relative to grid (each particle
// knows what grid cell it belongs to) possible values are 0,1,...Ng-1

//#pragma omp parallel for schedule(static) private(e)
for (e=0; e<Nm; e++) {
    //#pragma omp atomic
    grid[markerpos[e]]+=markerval[e];
}

在最坏的情况下,粒子位置是随机的,但通常,在内存中彼此相邻的粒子也在空间中彼此相邻,因此也在网格存储器中相邻。

如何有效地并行化此过程?几个粒子映射到同一个网格单元,因此如果直接并行化上述循环,则存在非竞争条件和缓存交换的可能性。使更新原子化可以防止竞争条件,但使代码比顺序情况慢得多。

我还试图为每个线程制作一个网格值的私有副本,然后再添加它们。但是,这可能需要在代码中使用太多内存,对于这个例子,它的线程数不能很好地扩展(原因我不确定)。

第三种选择可能是从网格映射到粒子,然后循环网格索引而不是粒子索引。但是,我担心,这将涉及到并且需要对代码进行重大更改,而且我不确定它会有多大帮助,因为它还需要使用排序例程,这在计算上也很昂贵。

有没有人遇到过这个或类似的问题?

2 个答案:

答案 0 :(得分:2)

一个选项可以是在线程上手动映射迭代:

#pragma omp parallel shared(Nm,Ng,markerval,markerpos,grid)
{
  int nthreads = omp_get_num_threads();
  int rank     = omp_get_thread_num();
  int factor   = Ng/nthreads;

  for (int e = 0; e < Nm; e++) {
    int pos = markerpos[e];
    if ( (pos/factor)%nthreads == rank )
      grid[pos]+=markerval[e];
  }
}

一些评论:

  1. for循环的迭代不在线程之间共享。而是每个线程执行所有迭代。
  2. for循环中的条件决定哪个帖子将更新pos数组的位置grid。此位置仅属于一个线程,因此不需要atomic保护。
  3. 公式(pos/factor)%nthreads只是一个简单的启发式方法。 pos返回0,...,nthreads-1范围内的值的任何函数实际上都可以替换为此表达式,而不会影响最终结果的有效性(如果您有更好的镜头,请随意更改它) 。请注意,此功能选择不当可能会导致负载平衡问题。

答案 1 :(得分:1)

我还必须将分子动态算法与OpenMP并行化。首先,您必须分析算法瓶颈(例如,内存绑定和CPU绑定)。这样你就会知道在哪里改进。

最初,我的MD是内存绑定的,所以我通过将数据布局从结构数组(AOS)更改为数组结构(SOA)来获得大约2倍的速度(适当的空间位置)。对于仅适合RAM的输入,我还应用了阻塞技术。原始算法计算了每个粒子之间的力对,如下所示:

for(int particleI = 0; i < SIZE ; i++)
 for(int particleJ = 0; j < SIZE; j++)
     calculate_force_between(i,j);

基本上,使用块技术,我们通过粒子块聚集力计算。例如,计算10个第一个粒子之间的所有削减力,然后计算下一个10个,依此类推。

使用块技术可以更好地利用时间局部性,因为使用这种技术可以在更短的时间内实现对相同粒子的更多计算时间因此,降低我们尝试访问的值不再在缓存中的可能性。

现在我已经绑定了MD CPU,我可以尝试使用multi-threads来改进它,但首先,您需要:

  • 1)验证算法花费大部分执行时间的位置;
  • 2)找到可以并行完成的任务并确定它们 粒度(检查其并行化是否合理);
  • 3)负载均衡,确保线程间工作的良好负载均衡;
  • 4)尽量减少同步的使用。

由于负载平衡问题,我在扩展MD方面遇到了问题。有些线程比其他线程做得更多,解决方案?

您可以尝试使用openMP中的动态。请注意,在OpenMP中,您可以指定要分配给线程的工作块。但是,在定义块时必须小心!对于动态for,块太小会导致同步开销,而太大会导致负载平衡问题。

我也遇到了同步开销问题。我使用的是关键,算法没有扩展。我通过更精细的颗粒同步替换了那个关键,即锁定,每个颗粒一个。我用这种方法做了一些改进。

作为最后一种方法(处理同步开销),我使用数据冗余。每个粒子都完成了它的工作并将结果保存在一个私有的临时数据结构中。最后,所有线程都减少了它们的值。在所有版本中,这是给我最好结果的版本。

我能够在CPU中实现良好的加速,但没有比我使用GPU版本获得的那些更快。

根据您提供的信息,我会这样做:

omp_lock_t locks [grid_size]; // create an array of locks
int g;
#pragma omp parallel for schedule(static)
for (e=0; e<Nm; e++)
{
    g = markerpos[e];

    omp_set_lock(&locks[g]);
    grid[g]+=markerval[e];
    omp_unset_lock(&locks[g]);
}

从,我理解的问题是你必须使用原子来确保多个线程不会同时访问相同的夹点位置。作为一种可能的解决方案,您可以创建一个锁数组,每次线程必须访问它请求的网格的一个位置并获取与该位置关联的锁。另一种解决方案可以是:

double grid_thread[grid_size][N_threads]; // each thread have a grid
// initialize the grid_threads to zeros

#pragma omp parallel
{
    int idT = omp_get_thread_num();
    int sum;
    #pragma omp parallel for schedule(static)
    for (e=0; e<Nm; e++)
       grid_thread[markerpos[e]][idT]+=markerval[e]; // each thread compute in their 
                                                     // position
    for(int j = 0; j <Nm; j++)
      { 
        sum = 0;
        #pragma omp for reduction(+:sum) 
        for (i = 0; i < idT; i++)                   // Store the result from all
           sum += grid_thread[j][i];                // threads for grid position j

         #pragma barrier                            // Ensure mutual exclusion

         #pragma master
         grid[j] +=sum;                             // thread master save the result  
                                                    // original grid
         #pragma barrier                            // Ensure mutual exclusion
      }
   }

}