使用 OpenMP 在 Fortran 中并行化分支递归子例程时创建线程失败

Failed thread creation when parallelizing a branching recursive subroutine in Fortran with OpenMP

我正在用 Fortran 编写递归子例程,它扩展为二叉树(即过程调用自身两次,直到到达分支的末尾)。大致的算法逻辑是:

'''

call my_subroutine(inputs, output)
  use input to generate possible new_input(:,1) and new_input(:,2)
  do i=1,2
    call my_subroutine(new_input(:,i), new_output(i))
  enddo
  output = best(new_output(1), new_output(2))

'''

原则上,这可以通过并行计算大大加速,但是当我使用 OpenMP 并行化循环时,运行生成的可执行文件中止并出现错误:

libgomp:线程创建失败:资源暂时不可用 线程创建失败:资源暂时不可用

我猜测堆栈大小太大,但我还没有找到解决方法或解决方法。有什么方法可以使用并行计算来提高这种算法的性能吗?

-OpenMP 或 gfortran 是否有帮助避免这些问题的选项?

-仅在树中特定级别之上或之下并行化是否有帮助?

-对于此应用程序,c 或 c++ 是更好的选择吗?

我正在使用 macOS Catalina。堆栈大小的上限为 65532。 我的环境变量是:

OMP_NESTED=真

OMP_DYNAMIC=真

这听起来更像是您的代码由于非常深的递归而创建了太多线程。有一些方法可以减轻它。例如,OpenMP 4.5 引入了由 max-active-levels-var ICV(内部控制变量)控制的最大活动级别的概念。您可以通过设置 OMP_MAX_ACTIVE_LEVELS 环境变量或调用 omp_set_max_active_levels() 来设置它的值。一旦嵌套级别达到 max-active-levels-var 指定的级别,嵌套更深的并行区域将被停用,即它们将按顺序执行而不会产生新线程。

如果您的编译器不支持 OpenMP 4.5,或者如果您希望代码向后兼容旧版编译器,那么您可以通过跟踪嵌套级别和停用并行区域来手动完成。对于后者,有 if(b) 子句,当应用于并行区域时,仅当 b 计算为 .true. 时才激活它。您的代码的示例并行实现:

subroute my_subroutine(inputs, output, level)
  use input to generate possible new_input(:,1) and new_input(:,2)
!$omp parallel do schedule(static,1) if(level<max_levels)
  do i=1,2
    call my_subroutine(new_input(:,i), new_output(i), level+1)
  enddo
!$omp end parallel do
  output = best(new_output(1), new_output(2))
end subroutine my_subroutine

my_subroutine 的顶级调用必须使用等于 0level

无论您如何精确地实现它,您都需要试验最高级别的值。最佳值将取决于CPUs/cores的数量和代码的运算强度,并且会因系统而异。

parallel do 构造的更好替代方法是再次使用 OpenMP 任务,并在一定的嵌套级别进行截止。任务的好处是您可以预先固定 OpenMP 线程的数量,任务运行时将负责工作负载分配。

subroutine my_subroutine(inputs, output, level)
  use input to generate possible new_input(:,1) and new_input(:,2)
!$omp taskloop shared(new_input, new_output) final(level>=max_levels)
  do i=1,2
    call my_subroutine(new_input(:,i), new_output(i), level+1)
  end do
!$omp taskwait
  output = best(new_output(1), new_output(2))
end subroutine my_subroutine

在这里,循环的每次迭代都成为一个单独的任务。如果已达到 max_levels 嵌套,则任务变为 final,这意味着它们不会被延迟(即,将按顺序执行)并且每个嵌套任务也将是 final,从而有效地停止在递归树下进一步并行执行。任务循环是 OpenMP 4.5 中引入的一项便利功能。对于早期的编译器,以下等效代码将执行:

subroutine my_subroutine(inputs, output, level)
  use input to generate possible new_input(:,1) and new_input(:,2)
  do i=1,2
!$omp task shared(new_input, new_output) final(level>=max_levels)
    call my_subroutine(new_input(:,i), new_output(i), level+1)
!$omp end task
  end do
!$omp taskwait
  output = best(new_output(1), new_output(2))
end subroutine my_subroutine

任务代码中没有 parallel 结构。相反,您需要从并行区域内调用 my_subroutine,惯用的方法是这样做:

!$omp parallel
!$omp single
  call my_subroutine(inputs, output, 0)
!$omp end single
!$omp end parallel

嵌套并行版本与使用任务的版本之间存在根本区别。在前一种情况下,在每个递归级别,当前线程一分为二,每个线程并行执行一半的计算。此处需要限制活动并行级别,以防止运行时生成过多线程并耗尽系统资源。在后一种情况下,在每个递归级别创建两个新任务并延迟以供稍后执行,可能由与并行区域关联的线程组并行执行。线程数保持不变,这里的截止限制了任务开销的累积,这比产生新的并行区域的开销要小得多。因此,任务代码的最佳值 max_levels 将与嵌套并行代码的最佳值大不相同。