【问题标题】:Optimization of OpenMP parallel do loop in FortranFortran 中 OpenMP 并行 do 循环的优化
【发布时间】:2022-11-29 19:27:00
【问题描述】:

背景

我正在使用 Fortran90 和 OpenMP 模拟分子动力学中 N 个带电粒子的运动。施加到每个离子 i 的力的解析表达式是已知的,并且是离子 i 和其他离子 (r_xr_yr_z) 位置的函数。我使用并行的 2 嵌套 do 循环计算每对离子之间的库仑相互作用。我可以确定循环结束时每个离子的加速度(a2_xa2_ya2_z)(然后用 velocity-Verlet 更新速度和位置)。

方法

我在我的程序中使用以下代码来计算应用于每个离子的库仑力。我计算下一个时间步的加速度 (a2_x),从当前时间步的位置 (r_x) 开始。这是一个 3D 问题,我写了所有的行,但其中大部分对于 x、y 和 z 都是一样的,所以在第一次阅读时你可以只考虑 _x 变量来了解它是如何工作的。

我在 C 线程上并行化我的循环,ia 和 ib 是用于将 N 离子分成 C 部分的数组。例如C=4线程和N=16离子(Se编辑下面的评论)

integer, parameter :: ia(C) = [1,5,9,13]
integer, parameter :: ib(C) = [4,8,12,16]

然后库仑计算如下

!$omp parallel default(none) &
!$omp private(im, i,j,rji,r2inv) &
!$omp firstprivate(r_x,r_y,r_z, N, ia, ib) &
!$omp shared(a2_x, a2_y, a2_z)
    im = omp_get_thread_num() + 1 ! How much threads
  ! Coulomb forces between each ion pair
  ! Compute the Coulomb force applied to ion i
    do i = ia(im,1), ib(im,1) ! loop over threads
        do j = 1, N ! loop over all ions
            rji(1)  = r_x(j) - r_x(i) ! distance between the ion i and j over x
            rji(2)  = r_y(j) - r_y(i) ! over y
            rji(3)  = r_z(j) - r_z(i) ! over z
          ! then compute the inverse square root of distance between the current ion i and the neighbor j
            r2inv   = 1.d0/dsqrt(rji(1)*rji(1) + rji(2)*rji(2) + rji(3)*rji(3) + softening)
            r2inv   = r2inv * r2inv * r2inv * alpha(1) ! alpha is 1/4.pi.eps0
          ! computation of the accelerations
            a2_x(i) = a2_x(i) - rji(1)*r2inv
            a2_y(i) = a2_y(i) - rji(2)*r2inv
            a2_z(i) = a2_z(i) - rji(3)*r2inv
        enddo
    enddo
    !$omp end parallel

问题

我正在尝试优化程序中这个耗时的部分。操作数量相当多,扩展速度很快,有N个。你能告诉我你对这个程序的看法吗?我有一些具体问题。

  1. 我被告知我应该将位置r_xr_yr_z作为private变量,这对我来说似乎违反直觉,因为我想使用先前定义的离子位置进入该循环,所以我使用firstprivate。那正确吗 ?

  2. 我不确定关于其他变量的并行化是否是最佳的。 rji 和 r2inv 不应该共享吗?因为为了计算离子 i 和 j 之间的距离,我“超越”了线程,你明白我的意思了吗?我需要分布在两个不同线程上的离子之间的信息。

  3. 我首先分裂离子的方式是否最佳?

  4. 我分别为每个离子遍历所有离子,这将在计算离子 i 和 i 之间的距离时导致除以零。为了防止这种情况,我定义了一个软化变量,它的值非常小,因此它不完全为零。我这样做是为了避免 if i==i 耗时。

  5. 平方根也可能很耗时?

    如需任何其他详细信息,请随时询问。

    编辑(备注)

    1. 我的电脑有一个 10 核 CPU Xeon W2155、32 Go RAM。我打算渲染大约 1000 个离子,同时考虑 4000 个,这需要很多时间。

    2. 我在其他可能消耗一些 CPU 时间的子例程中有这个 Coulomb 子例程。例如,一个可能很耗时的例程专门为每个离子生成随机数,具体取决于它们是否已经被激发,并应用正确的效果,无论它们是否吸收光子。所以这是很多 RNG,如果每个离子。

      编辑(命题测试)

      1. !$omp doschedule(dynamic,1)schedule(guided)schedule(nonmonotonic:dynamic) 和/或 collapse(2) 结合使用并没有缩短运行时间。它至少延长了三倍。建议我的模拟中的元素数量 (N) 太低,看不到明显的改进。如果我尝试渲染更多数量的元素(4096、8192 ...),我会尝试这些选项。

      2. 使用 !$omp do 而不是在内核之间自制离子分布确实显示了运行时间方面的等效性。实施起来更容易,我会保留它。

      3. **(-1/2) 替换逆dsqrt 表明在运行时间方面是等效的。

      4. 延迟平方根并将其与 r2inv 的三次方相结合也是等效的。所以我把整个系列的操作换成了**(-1.5)

      5. rji(1)*r2inv 的想法相同,我之前执行rji*r2inv 并且仅在下一行中使用结果。

【问题讨论】:

  • 只是对风格的评论 - dsqrt 非常 Fortran66。在过去的 50 年里,简单的 sqrt 就足够了。
  • 一条评论:我假设 N=16 只是为了说明,因为对于如此少量的粒子,没有机会观察到多线程的显着加速。在实际应用中,您的典型 N 是多少?
  • @PierU 你的假设是对的。我宁愿使用 1024 个离子运行模拟,但我想尝试更多,例如 4096,但我希望对代码进行优化,因为使用 4096 个离子会耗时更多。在 1024 离子壁时间可以是 30 到 60 分钟,这很好,但在 4096 会更长。

标签: loops parallel-processing fortran openmp


【解决方案1】:
  1. 一般来说,并行区只需要读取的变量可以是shared。但是,在某些情况下,每个线程都有 firstprivate 副本可以提供更好的性能(副本可以在每个内核的本地缓存中),特别是对于重复读取的变量。
  2. 绝对不是!如果你这样做,这些变量就会出现竞争条件
  3. 看起来不错,但使用 !$OMP DO 指令而不是手动将工作分配到不同的线程通常更简单(并且在最坏的情况下也是有效的)
        !$OMP DO
        do i = 1, N ! loop over all ions
            do j = 1, N ! loop over all ions
    
    1. 为什么不呢,前提是你可以选择一个不会改变你的模拟的softening值(这是你必须针对if解决方案进行测试的东西)
    2. 不知何故,但在某些时候你无法避免求幂。我会延迟 sqrt 和这样的划分:
    r2inv   = (rji(1)*rji(1) + rji(2)*rji(2) + rji(3)*rji(3) + softening)
    r2inv   = r2inv**(-1.5) * alpha(1) ! alpha is 1/4.pi.eps0
    

    将工作除以 2

    这些力是对称的,对于给定的 (i,j) 对只能计算一次。这也很自然地避免了 i==j 的情况和软化值。但是,迭代之间的工作负载是高度不平衡的,并且需要 dynamic 子句。这实际上是一个案例,手动将迭代分发到线程可以更有效;)...

    !$omp parallel default(none) &
    !$omp private(im, i,j,rji,r2inv) &
    !$omp firstprivate(r_x,r_y,r_z, N, ia, ib) &
    !$omp shared(a2_x, a2_y, a2_z)
      ! Coulomb forces between each ion pair
      ! Compute the Coulomb force applied to ion i
        !$omp do schedule(dynamic,1)
        do i = 1, N-1 ! loop over all ions
            do j = i+1, N ! loop over some ions
                rji(1)  = r_x(j) - r_x(i) ! distance between the ion i and j over x
                rji(2)  = r_y(j) - r_y(i) ! over y
                rji(3)  = r_z(j) - r_z(i) ! over z
              ! then compute the inverse square root of distance between the current ion i and the neighbor j
                r2inv   = (rji(1)*rji(1) + rji(2)*rji(2) + rji(3)*rji(3))
                r2inv   = r2inv**(-1.5) * alpha(1) ! alpha is 1/4.pi.eps0
              ! computation of the accelerations
                rji(:) = rji(:)*r2inv
                a2_x(i) = a2_x(i) - rji(1)
                a2_y(i) = a2_y(i) - rji(2)
                a2_z(i) = a2_z(i) - rji(3)
                a2_x(j) = a2_x(j) + rji(1)
                a2_y(j) = a2_y(j) + rji(2)
                a2_z(j) = a2_z(j) + rji(3)
            enddo
        enddo
        !$omp end do
        !$omp end parallel
    

    或者,可以使用 guided 子句,在迭代中进行一些更改以使第一个迭代中的工作负载较低:

        !$omp do schedule(guided)
        do i = 2, N ! loop over all ions
            do j = 1, i-1 ! loop over some ions
    

【讨论】:

  • schedule(nonmonotonic:dynamic) 也值得一试。它可以显着降低动态调度的成本。虽然它现在被允许作为 schedule(dynamic) 的默认值,但许多实现选择不设置默认的非单调,因为它可能会破坏假定单调实现的旧代码。您也可以在循环中尝试collapse(2),可能使用块大小来稍微增加计划的项目。
  • @PierU 我实施了建议的修改,但没有按预期工作。首先,针对 N=1024 个离子测试了我的原始版本:壁时间为 417 秒。添加 !omp$ do 和延迟的 sqrt 也会得到 417s。尽管如此,按照建议重新安排循环 i=1, N-1 ... 并使用 schedule(dynamic,1) 给出了超过 15 分钟(>900 秒)的墙时间。使用 schedule(guided) 我的挂断时间超过 30 分钟。我明天会仔细检查。我的代码中有这个 Coulomb 例程,它还运行其他东西,特别是在每个时间步长为每个离子生成随机数,解释为什么变化没有效果?
  • 我还要补充一点,!omp$ do 单独使用 softening 和我原来的 do 循环 i = 1, N ... 也给出了 417 秒的墙时间。混乱确实来自 schedule(dynamic,1) 和循环的重新排列 do i = 1, N-1... do j = i+1, N` ?这可能与变量的定义方式有关吗?私有和共享属性?
  • @Aldehyde dynamic 计划有很大的开销,与开销相比,这里每次迭代的工作量可能太低了。可能值得尝试@JimCownie schedule(nonmonotonic:dynamic) 的建议。除此之外,即使 N=1024,总工作量也可能不够大(1024**2 次迭代不是那么多),无法充分利用 OpenMP,如果周围的串行代码代表重要总时间的百分比。您应该只对并行部分进行计时以评估多线程加速(参见omp_get_wtime()
  • @PierU @Jim Cownie。使用带或不带collapse(2)schedule(nonmonotonic:dynamic),以及减半的循环,并没有改善运行时间。超过30分钟。这些指令与!$omp do 写在同一行。执行时间是使用omp_get_wtime() 测量的,但不仅仅针对库仑循环,因为它集成在一个更大的程序中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-08
  • 2021-09-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多