Я пишу рекурсивную подпрограмму на Фортране, которая расширяется как двоичное дерево (т.е. процедура вызывает себя дважды, пока не достигнет конца ветки). Общая алгоритмическая логика такова:
'''
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=Истина
Кстати, я подозреваю, что то, что вы видите, похоже на stackoverflow.com/questions/61983425/… и является результатом вложенного параллелизма, а не задач, но без кода я не могу сказать.
Я не уверен, смогу ли я отобразить точный код, поскольку это прототип для моей работы, но я могу создать эквивалентный пример для демонстрации. Я наткнулся на упомянутый вами поток, хотя, к сожалению, ограничение максимального количества потоков с помощью omp_set_num_threads не решило проблему. Спасибо за предложение использовать «задачи», а не вложенный параллелизм. Я не рассматривал это, так как у меня не было четкого понимания разницы.
Это больше похоже на то, что ваш код создает слишком много потоков из-за очень глубокой рекурсии. Есть способы его смягчить. Например, в 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
должен быть с level
равным 0
.
Независимо от того, как именно вы это реализуете, вам нужно будет поэкспериментировать со значением максимального уровня. Оптимальное значение будет зависеть от количества процессоров/ядер и арифметической сложности кода и будет варьироваться от системы к системе.
Лучшей альтернативой конструкции 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
вложенности, задачи становятся окончательными, что означает, что они не будут отложены (т. е. будут выполняться последовательно), и каждая вложенная задача также будет финальной, эффективно останавливая параллельное выполнение дальше по дереву рекурсии. Циклы задач — это удобная функция, представленная в 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
для кода задач будет значительно отличаться от оптимального значения для вложенного параллельного кода.
Спасибо Христо! Я пробовал второй подход с ограниченным успехом. Программа, кажется, застревает в определенных точках (возможно, ожидая синхронизации различных параллельных регионов). Я, должно быть, просматривал старую документацию, поскольку не видел переменной «OMP_MAX_LEVELS». Как предположил Ян, «параллельный do' может не подходить для этого приложения.
Это понятно. OMP_MAX_ACTIVE_LEVELS
впервые появился в OpenMP 4.5. Сама спецификация была выпущена в ноябре 2015 года, а через некоторое время появились компиляторы с ее полной поддержкой.
В parallel do
нет ничего плохого, если оба рекурсивных подвызова занимают более или менее одинаковое количество времени. Конечно, есть лучшие решения. Например - задачи OpenMP. Но и здесь применимо ограничение параллелизма на определенном уровне рекурсии.
Это может быть проблемой в моей конкретной реализации, поскольку некоторые ветки, вероятно, завершатся намного быстрее, чем другие. Математически каждый уровень дерева не зависит друг от друга и гипотетически может обрабатываться отдельными процессорами, но преимущество исчезает, если они проводят большую часть своего времени в ожидании разрешения родственных ветвей.
В этом случае задачи OpenMP являются лучшей альтернативой. Я обновлю ответ соответственно.
Хорошо знать. Я провел несколько тестов с использованием задач OMP, и, хотя это намного надежнее, у меня не было стабильного прироста производительности. Мне придется еще немного поэкспериментировать, чтобы выяснить, плохо ли я использую OMP или структура моего кода не очень подходит для многопоточности. Если я сделаю какие-либо прорывы или полезные открытия, я обновлю эту тему. *не каламбур
Пожалуйста, покажите полный код - дьявол кроется в деталях. Задачи — это обычный способ сделать это в OMP. но без минимального примера очень сложно помочь.