【问题标题】:OpenMP: splitting loop based on NUMAOpenMP:基于 NUMA 的拆分循环
【发布时间】:2014-09-17 10:18:11
【问题描述】:

我正在使用 8 个 OpenMP 线程运行以下循环:

float* data;
int n;

#pragma omp parallel for schedule(dynamic, 1) default(none) shared(data, n)
for ( int i = 0; i < n; ++i )
{
    DO SOMETHING WITH data[i]
}

由于 NUMA,我想用线程 0,1,2,3 运行循环的前半部分 (i = 0, ..., n/2-1) 后半部分 (i = n/2, ..., n-1),线程数为 4,5,6,7。

基本上,我想并行运行两个循环,每个循环使用一组单独的 OpenMP 线程。

如何使用 OpenMP 实现这一目标?

谢谢

PS:理想情况下,如果一组线程完成了循环的一半,而另一半循环仍未完成,我希望已完成组的线程加入未完成组处理另一半循环。

我正在考虑类似下面的事情,但我想知道我是否可以使用 OpenMP 做到这一点而无需额外的簿记:

int n;
int i0 = 0;
int i1 = n / 2;

#pragma omp parallel for schedule(dynamic, 1) default(none) shared(data,n,i0,i1)
for ( int i = 0; i < n; ++i )
{
    int nt = omp_get_thread_num();
    int j;
    #pragma omp critical
    {
        if ( nt < 4 ) {
            if ( i0 < n / 2 ) j = i0++; // First 4 threads process first half
            else              j = i1++; // of loop unless first half is finished
        }
        else {
            if ( i1 < n ) j = i1++;  // Second 4 threads process second half
            else          j = i0++;  // of loop unless second half is finished 
        }
    }

    DO SOMETHING WITH data[j]
}

【问题讨论】:

  • 你能解释一下为什么你说“由于 NUMA,我想用线程 0,1 运行循环的前半部分 (i = 0, ..., n/2-1) ,2,3 和后半部分 (i = n/2, ..., n-1),线程为 4,5,6,7。"?
  • 因为data是这样分配的,它的前半部分靠近一个套接字(我运行线程0、1、2、3的地方),而后半部分靠近另一个套接字(我在其中运行线程 4、5、6、7)
  • 您的操作系统、硬件和编译器是什么? Linux?两个插槽英特尔至强?海合会?
  • @Zboson RHEL 6.3,8 插槽 Xeon CPU E5-4640(总共 64 个内核)。 1 TB 内存。帖子中的示例已简化。我需要超过 2 组线程。编译器:GCC 4.8.3 或最新的 Intel。
  • 您确定要schedule(dynamic,1) 还是要sechedule(static)

标签: multithreading performance openmp affinity numa


【解决方案1】:

可能最好的方法是使用嵌套并行化,首先在 NUMA 节点上,然后在每个节点内;那么您可以使用dynamic 的基础架构,同时仍然在线程组之间分解数据:

#include <omp.h>
#include <stdio.h>

int main(int argc, char **argv) {

    const int ngroups=2;
    const int npergroup=4;
    const int ndata = 16;

    omp_set_nested(1);
    #pragma omp parallel for num_threads(ngroups)
    for (int i=0; i<ngroups; i++) {
        int start = (ndata*i+(ngroups-1))/ngroups;
        int end  = (ndata*(i+1)+(ngroups-1))/ngroups;    

        #pragma omp parallel for num_threads(npergroup) shared(i, start, end) schedule(dynamic,1)
        for (int j=start; j<end; j++) {
            printf("Thread %d from group %d working on data %d\n", omp_get_thread_num(), i, j);
        }
    }

    return 0;
}

运行它会给出

$ gcc -fopenmp -o nested nested.c -Wall -O -std=c99
$ ./nested | sort -n -k 9
Thread 0 from group 0 working on data 0
Thread 3 from group 0 working on data 1
Thread 1 from group 0 working on data 2
Thread 2 from group 0 working on data 3
Thread 1 from group 0 working on data 4
Thread 3 from group 0 working on data 5
Thread 3 from group 0 working on data 6
Thread 0 from group 0 working on data 7
Thread 0 from group 1 working on data 8
Thread 3 from group 1 working on data 9
Thread 2 from group 1 working on data 10
Thread 1 from group 1 working on data 11
Thread 0 from group 1 working on data 12
Thread 0 from group 1 working on data 13
Thread 2 from group 1 working on data 14
Thread 0 from group 1 working on data 15

但请注意,嵌套方法很可能会改变线程分配,而不是单级线程,因此您可能必须更多地使用 KMP_AFFINITY 或其他机制才能再次正确绑定。

【讨论】:

  • 这是一个聪明的答案。我还没用过omp_set_nested
  • 谢谢 - 一旦我终于理解了这个问题,它就很好地映射到了这个问题上。
  • 谢谢。我想你也可以在外循环中使用任务。不知道会不会有影响。我也试图了解 omp 团队的构造(以前从未使用过它们)。可以使用此功能代替嵌套并行吗?
  • 顶级任务或并行,这并不重要 - 任何使它更容易阅读或写作的东西。循环的好处是它很容易推广到不同数量的顶级 numa 节点。团队确实指的是嵌套并行性,但要小心 - 在 OMP 4 中,团队指的是加速器 (GPU/Phi) 的东西。
  • @JonathanDursi 我刚刚在我的代码中实现了您的方法,并在 2 插槽(总共 16 核)机器上对其进行了测试。使用旧代码时,执行时间从 55 秒下降到 50 秒,而线程数从 8 个变为 16 个。使用支持 NUMA 的代码,16 线程测试在 32 秒内运行!
猜你喜欢
  • 2012-08-11
  • 1970-01-01
  • 2017-07-11
  • 2019-07-20
  • 1970-01-01
  • 2012-01-23
  • 1970-01-01
  • 1970-01-01
  • 2013-03-09
相关资源
最近更新 更多