我使用的机器有 2 个 Xeon CPU,每个 CPU 16 个核心。有 2 个 NUMA 域,每个 CPU 一个。
我有密集的计算,也使用大量内存,而且一切都是多线程的。代码的全局结构是:
!$OMP PARALLEL DO
do i = 1, N
!$OMP CRITICAL
! allocate memory and populate it
!$OMP END CRITICAL
end do
!$OMP END PARALLEL DO
...
...
!$OMP PARALLEL DO
do i = 1, N
! main computations part 1
end do
!$OMP END PARALLEL DO
...
...
!$OMP PARALLEL DO
do i = 1, N
! main computations part 2
end do
!$OMP END PARALLEL DO
...
...
N 通常约为 10000,每次迭代需要几秒钟。
在迭代 #i 的计算中读取/写入的数据大约有 50% 在预先在迭代 #i 分配的内存中,其余 50% 在其他迭代中分配的内存中(但往往接近于#i).
对所有循环使用相同的静态调度可确保在给定的迭代 #i 中,计算期间访问的内存的 50% 已由与处理迭代相同的线程分配,因此它位于相同的线程中NUMA 域。
此外,将线程与 OMP_PROC_BIND 和 OMP_PLACES 绑定(CPU #0 上的线程 0-15 和 CPU #1 上的线程 16-31)可确保相邻迭代可能在同一 NUMA 域中分配其分配的内存。
到目前为止一切顺利...
唯一的问题是迭代之间的计算工作量没有很好地平衡。这还不错,但最多可能有+/-20%...通常,在计算阶段使用一些动态调度会有所帮助,但在这里它会破坏具有相同线程分配然后计算的整个策略迭代#i。
至少,我希望迭代 1...N/2 由线程 0-15 处理,迭代 N/2+1...N 由线程 16-31 处理。因此,第一级静态分块(2 个大小为 N/2 的块),以及每个块内部的第二级动态调度。这至少可以确保每个线程访问的内存大部分位于同一个 NUMA 域中。
但我根本不知道如何使用 OpenMP 做到这一点...可能吗?
编辑:
schedule(nonmonotonic:dynamic)
可能是这里的一个解决方案,但在我使用的 HPC 集群上,我坚持使用不实现此调度的编译器版本(最好是英特尔编译器 2021)。
考虑到每次迭代的执行时间为秒,任务不会增加显着的运行时开销。使用 LLVM/Intel OpenMP 运行时,任务将在线程本地排队,线程在完成迭代后将开始窃取:
!$OMP PARALLEL DO SCHEDULE(static)
do i = 1, N
!$OMP CRITICAL
! allocate memory and populate it
!$OMP END CRITICAL
end do
!$OMP END PARALLEL DO
...
...
!$OMP PARALLEL DO SCHEDULE(static)
do i = 1, N
!$OMP TASK
! main computations part 1
!$OMP END TASK
end do
!$OMP END PARALLEL DO
...
...
!$OMP PARALLEL DO SCHEDULE(static)
do i = 1, N
!$OMP TASK
! main computations part 2
!$OMP END TASK
end do
!$OMP END PARALLEL DO
...
...
当前的 GNU libgomp 实现具有单个任务队列,因此上面的代码在任务创建期间会出现严重的拥塞,而且也没有数据局部性。
为了确保迭代与套接字的绑定,可以使用嵌套并行区域并使用 @MichaelKlemm 建议的运行时调度的替代方案:
export KMP_HOT_TEAMS_MAX_LEVEL=2
export OMP_PLACES=sockets,cores
export OMP_PROC_BIND=spread,close
export OMP_NUM_THREADS=2,16
export OMP_SCHEDULE=static_steal
!$OMP PARALLEL private(nOuter)
nOuter = omp_get_num_threads()
!$OMP DO SCHEDULE(static)
do s = 1,nOuter
!$OMP PARALLEL DO SCHEDULE(static)
do i = 1 + (s-1)*(N/nOuter), s*N/nOuter
!$OMP CRITICAL
! allocate memory and populate it
!$OMP END CRITICAL
end do
!$OMP END PARALLEL DO
end do
!$OMP END DO
!$OMP END PARALLEL
...
...
!$OMP PARALLEL private(nOuter)
nOuter = omp_get_num_threads()
!$OMP DO SCHEDULE(static)
do s = 1,nOuter
!$OMP PARALLEL DO SCHEDULE(runtime)
do i = 1 + (s-1)*(N/nOuter), s*N/nOuter
! main computations part 1
end do
!$OMP END PARALLEL DO
end do
!$OMP END DO
!$OMP END PARALLEL
...
...
!$OMP PARALLEL private(nOuter)
nOuter = omp_get_num_threads()
!$OMP DO SCHEDULE(static)
do s = 1,nOuter
!$OMP PARALLEL DO SCHEDULE(runtime)
do i = 1 + (s-1)*(N/nOuter), s*N/nOuter
! main computations part 2
end do
!$OMP END PARALLEL DO
end do
!$OMP END DO
!$OMP END PARALLEL
...
...