OMP SECTIONS 和 DO 在同一个 PARALLEL 块中

Posted

技术标签:

【中文标题】OMP SECTIONS 和 DO 在同一个 PARALLEL 块中【英文标题】:OMP SECTIONS and DO in the same PARALLEL block 【发布时间】:2016-10-23 07:34:11 【问题描述】:

我有 3 个任务完全相互独立,因此非常适合并行执行:

任务 1:执行名为 subA() 的(单线程)子例程。

任务 2:执行名为 subB() 的(单线程)子例程。

任务 3:在 DO 循环中填充数组。 DO 循环的每次迭代都独立于所有其他迭代。

假设我有 8 个线程。我希望线程 0 处理任务 1,线程 1 处理任务 2,线程 2-7 处理任务 3。在 Fortran 中,我想像这样:

COMPLEX*8, EXTERNAL :: func

!$OMP PARALLEL

!$OMP SECTIONS
!$OMP SECTION
  !
  ! Task 1, performed by one thread
  !
  CALL subA()
!$OMP SECTION
  !
  ! Task 2, performed by one thread
  !
  CALL subB()
!$OMP END SECTIONS NOWAIT

!$OMP DO
  !
  ! Task 3, performed by all threads
  !
  DO j=1,nn             
    vals(j) = func(j)
  END DO
!$OMP END DO NOWAIT

!$OMP END PARALLEL

但是上面的代码并不是我想要的。在任务 1 和 2 上工作的线程也被安排在任务 3 中的 DO 循环上工作,这似乎减慢了一切,大概是因为这 2 个线程“迟到”到达 DO 循环,因此所有其他线程必须在 PARALLEL 区域末端的隐式屏障处等待它们。

在这种情况下处理线程调度的正确方法是什么?

冒着提供太多信息的风险,我已经知道 subA() 和 subB() 是计算密集型的,而 func(j) 的每次评估都比较快。完成 subA() 和 subB() 所需的时间与完成后一个任务的多个线程时完成整个 DO 循环所需的时间大致相同。

一些注意事项:

    将 NUM_THREADS(N) 子句添加到“OMP PARALLEL”并将 NUM_THREADS(N-2) 子句添加到“OMP DO”是很诱人的。但我认为“OMP DO”构造不接受 NUM_THREADS 子句。 一种解决方案是在 DO 循环上使用 DYNAMIC 调度。这样,并行 DO 循环将在执行 subA() 和 subB() 完成后拾取线程。这可行,但并不令人满意,因为 DYNAMIC 调度增加了不小的开销,因此增加了 DO 循环的执行时间,令人不快。 此处出现相关问题:put multiple do-s and section-s in the same parallel environment。但是,该问题的答案仅表明可以将 SECTIONS 和 DO 与相同的 PARALLEL 环境结合使用,但并未解决我在此处提出的调度问题。 我最初在英特尔的开发者论坛here 上提出了这个问题,并建议我交叉发布到 ***。

ETA:英特尔的某个人指出我最初的问题是模棱两可的:不清楚我的 DO 循环是否只是从 func(1..nn) 到 vals(1..nn) 的 memcpy() 还是我正在调用一个名为 func() 的函数 nn 次。后者是我的意图,我在示例代码中对此进行了说明。

【问题讨论】:

正如我在 Ryan 的原始帖子中已经提到的,在 OMP DO 中添加 schedule 子句似乎过于频繁,无法防止在我的不同情况下进入矢量化分支。这可以通过任意使其成为嵌套的 simd 内部并行外部循环来避免。这个问题可能被认为是“开销”,但在我的情况下,它不是 OpenMP 开销,而是未能使用 simd 移动指令,而是命中未优化的剩余循环。在 Ryan 的情况下,可能会有自动 memcpy 替换。 【参考方案1】:

使用 OpenMP 任务,类似这样(未经测试)

!$omp parallel
! Have a single thread create all the tasks. 
!$omp single
!$omp task
   call subA()
!$omp end task
!$omp task
   call subB()
!$omp end task
   DO j=1,nn    
!$omp task         
      vals(j) = func(j)
!$omp end task
   END DO
!$omp end single
!$omp end parallel

【讨论】:

我怀疑如果 nn 相对较小,这种方法会很好地工作。但就我而言,nn 是中等的(数十万个),创建数十万个 OpenMP 任务会大大降低速度。 如果您有一个 OpenMP 4.5 编译器,您可以使用 taskloop 构造将内部循环替换为其显式任务创建。然后您可以在任务循环上指定粒度。或者(如果你只有一个较旧的编译器)你可以自己强制它(在块上有一个外部循环来创建任务,然后执行一个内部循环),它仍然会比你在下面提出的解决方案更小,更便携。 (当然,除非您想在具有 72 个 coresx4threads 的 KNL 上运行时编写 288 个 switch case :-))。【参考方案2】:

到目前为止,我发现的最佳解决方案是手动执行我希望 OpenMP 自动为我执行的调度。特别是,让 nthr 为我希望使用的线程数。然后我使用 SELECT CASE 构造如下:

SELECT CASE (nthr)
CASE (3)
  ! Use 3 threads. See example below
CASE (4)
  ! Use 4 threads. See example below
CASE (5)
  ! And so on...
CASE DEFAULT
  ! Catch-all
END SELECT

如果我有 4 个线程,我会执行以下操作:

!$OMP PARALLEL PRIVATE(j) NUM_THREADS(4)
!$OMP SECTIONS
!$OMP SECTION
  !
  ! Task 1, performed by one thread
  !
  CALL subA()

!$OMP SECTION
  !
  ! Task 2, performed by one thread
  !
  CALL subB()

!$OMP SECTION
  !
  ! One thread does half the loop...
  !
  DO j=1,nn/2
    vals(j)=func(j)
  END DO

!$OMP SECTION
  !
  ! ...and one thread does the other half
  !
  DO j=nn/2+1,nn
    vals(j)=func(j)
  END DO

!$OMP END SECTIONS NOWAIT
!$OMP END PARALLEL

nthr 的其他值的情况是对上述代码的明显修改。

从正确和快速的角度来看,此解决方案运行良好,但从软件工程的角度来看,代码重复使其不是最理想的。

【讨论】:

以上是关于OMP SECTIONS 和 DO 在同一个 PARALLEL 块中的主要内容,如果未能解决你的问题,请参考以下文章

OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内

通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销

在 OpenMP 中,我们如何并行运行多个代码块,其中每个代码块包含 omp single 和 omp for 循环?

omp 刷新和 cc-NUMA 架构

如何在 omp 并行中使特定部件串行?

Ruby do/end vs 大括号