Главная
Вычислительные ресурсы
C чего начать
Вопросы - ответы
Документация
Исследования
Контакты

Автор: А.О. Лацис

Как правильно размещать процессы параллельной программы на процессорных ядрах в пределах вычислительного узла.

1. Основные термины.

Вычислительный узел суперкомпьютера – многопроцессорная машина. Он состоит из нескольких процессорных микросхем, а каждая процессорная микросхема, в свою очередь, состоит из нескольких процессорных ядер. Количество процессорных микросхем и процессорных ядер в каждой из них может быть разным в вычислительных узлах разных суперкомпьютеров. Для конкретности рассмотрим пример вычислительного узла суперкомпьютера К-60, в котором 2 процессорных микросхемы по в каждой.

С логической точки зрения каждое ядро представляет собой отдельный процессор. Все такие процессоры в пределах вычислительного узла занумерованы подряд от нуля, и имеют общую память. Группировка ядер в процессорные микросхемы может, в некоторых случаях, иметь значение для быстродействия, но логически прозрачна. Нумерация ядер идет в порядке их расположения по микросхемам: ядра с 0-го по 14-е находятся в одной процессорной микросхеме, ядра с 15-го по 28-е – в другой. Далее будем использовать термины «ядро» и «процессор» как синонимы. Это несколько отличается от общепринятой практики, согласно которой словом «процессор» принято называть процессорную микросхему, но позволит нам избежать терминологической путаницы, возникающей из-за того, что ядро также иногда называют «процессором».

2. Привязка процессов к процессорам.

Процессы параллельной программы, выполняющиеся в пределах одного вычислительного узла, вообще говоря, не привязаны к конкретным процессорам, и могут мигрировать между ними по ходу счета. Как правило, такая миграция отрицательно влияет на быстродействие, и ее желательно запретить. Это можно сделать, обратившись к специальной системной функции (подпрограмме) привязки процесса к конкретному набору процессоров, в простейшем случае – к одному конкретному процессору.

Есть, как минимум, два случая, когда привязывать процесс к конкретному процессору нельзя или крайне нежелательно:

  1. Когда на вычислительном узле запущено больше процессов, чем в нем имеется процессоров, например, путем задания большого значения ключа «-ppn» команды mpirun,
  2. Когда процессы порождаются по ходу счета, например, при использовании OpenMP. Поскольку порожденные процессы наследуют привязку процесса-родителя, привязывание OpenMP-программы к одному конкретному процессу делает использование OpenMP практически бессмысленным: все нити OpenMP-программы будут разделять один, родительский процессор, вместо того, чтобы распространиться по нескольким процессорам с целью разделения работы и, как следствие, повышения быстродействия.

3. Проблема привязки процессов в разных системах параллельного программирования.

Если программист использует для параллельной реализации своего приложения библиотеки MPI, shmem, их комбинацию, или какую-либо похожую систему параллельного программирования на базе порождаемых статически полновесных процессов (UPC, Co-Array Fortran, PVM, HPF), но не использует OpenMP или других средств динамического порождения процессов, то возможны всего два варианта правильной привязки процессов к процессорам:

  1. Пусть процессов на одном вычислительном узле запущено не больше, чем имеется процессоров. Тогда желательно привязать каждый процесс к отдельному процессору,
  2. Пусть процессов, по той или иной причине, на одном вычислительном узле запускается больше, чем имеется процессоров. Тогда следует привязать каждый процесс ко всем процессорам, или же как-то распределить процессы по процессорным кристаллам (например, процессы с четными номерами привязать к процессорам с 0-го по 14-й, а процессы с нечетными номерами – к процессорам с 15-го по 28-й).

Проблема в том, что разработчики различных версий MPI, shmem и аналогичных библиотек и языков, хотя и следуют этой общей логике, но каждый – своим, неповторимым путем.

Например:

  • Intel MPI привязывает каждый процесс ко всем процессорам,
  • Open MPI привязывает каждый процесс к одному процессору, то есть оптимизирует быстродействие для случая, когда процессоров хватает всем процессам,
  • shmem-express (в комбинации с любым вариантом MPI) привязывает процессы соответственно, запускает свои, служебные процессы,
  • и так далее.

Таким образом, рекомендация по привязке процессов к процессорам для программистов, использующих MPI и подобные инструменты, но не использующих OpenMP, следующая:

Если программист использует для параллельной реализации своего приложения, помимо библиотек MPI, shmem или им подобных, также OpenMP, то ему настоятельно рекомендуется выполнить явную привязку процессов к процессорам (см. ниже). Легко видеть, что среди описанных выше вариантов поведения разных версий MPI есть совсем не совместимые с OpenMP – например, такие, которые самостоятельно привязывают каждый MPI-процесс к единственному процессору. Попытка работать по умолчанию, без явной привязки, может привести к использованию для размещения нитей OpenMP не всех доступных процессоров, или даже к размещению всех нитей на единственном процессоре.

Рекомендация по привязке процессов к процессорам для программистов, использующих MPI и подобные инструменты совместно с OpenMP, следующая:

  • до обращения к MPI_Init() следует вызовом omp_set_num_threads() задать число нитей, на которые данный процесс будет ветвиться в своих параллельных областях,
  • сразу после обращения к MPI_Init() следует выполнить привязку данного MPI-процесса, который будет родителем всех порождаемых впоследствии нитей, к некоторому набору процессоров, в котором процессоров хватает для всех нитей,
  • если Вы используете shmem-экспресс, то занимать не следует.

Например, пусть Вы желаете использовать в рамках каждого MPI-процесса 28 нитей (число процессоров в вычислительном узле К-60), и не планируете использовать shmem-экспресс. Тогда программа должна выглядеть примерно так:

4. Функция (подпрограмма) привязки процесса к набору процессоров.

Вызов этой функции (подпрограммы) привязывает вызвавший процесс к процессорам указанного набора, и только к ним. Привязка распространяется на потомков процесса, например, на нити OpenMP.

Прототип функции (для использования в программах на С и С++) находится в заголовочном файле setaffinity.h, а сама функция – в библиотеке libaffinity.a (при сборке программы использовать ключ «–laffinity»). Имеется также подпрограмма для вызова из программ на Фортране, с тем же названием и теми же аргументами.

Первый аргумент функции задает размер процессорной маски, второй – саму маску. Маска задается как целочисленный массив, i-й элемент которого управляет привязкой процесса к i-му процессору. Если значение элемента отлично от нуля, то процессор включается в число тех, к которым привязывается данный процесс, если же равно нулю, то исключается. Если задана маска неполной длины, то не заданные элементы считаются нулями.

На вычислительных узлах К-60 включен режим гипертрединга. Это означает, что ОС вычислительного узла "видит" на каждом вычислительном узле 56 логических процессоров, в то время как в действительности физических процессоров (то есть процессорных ядер) на вычислительном узле всего 28. Каждые два логических процессора, таким образом, реализуются на одном физическом, то есть на одном комплекте вычислительного оборудования.

Сделано это потому, что некоторые, достаточно редкие и очень специальные, задачи способны заметно ускориться, если дать им много логических процессоров, даже таких "неполноценных", то есть попарно использующих один комплект арифметического оборудования, как в нашем случае.

Задачи, способные ускориться за счет гипертрединга, чрезвычайно редки, и пишутся людьми, хорошо разбирающимися в системных тонкостях. Поэтому предусмотренное в системе планирования "учетное" число процессоров на вычислительный узел осталось тем же, что и раньше - 28. Все числовые значения, указываемые в команде mpirun, сохраняют свой прежний смысл, если только Вы не указываете явно значение -ppn, превосходящее 28.

С другой стороны, некоторые вполне типичные задачи способны из-за режима гипертрединга несколько замедлиться (в пределах, максимум, 10%). Далее написано, как этого избежать.

Чтобы не допустить отрицательного влияния гипертрединга на Ваши задачи, следует внести в .bash_profile следующие добавления:

— строку: 
export I_MPI_PIN=on
следует добавить в любом случае;
— строку:
export I_MPI_PIN_PROCESSOR_LIST=allcores
следует добавить в случае, если Вы используете для параллельной реализации Вашей программы только MPI, и размещаете на каждом вычислительном узле не более 28 процессов (или вообще не знаете, сколько именно Вы их размещаете);
— строку:
export I_MPI_PIN_DOMAIN=\[fffffff\]
следует добавить в случае, если Вы используете MPI совместно с OpenMP.

Замечания:

  • чтобы не ошибиться при наборе добавляемых строк, скопируйте снова вариант настроечных файлов из /common/profile.versions/intel, и раскомментируйте там ту из двух альтернативных строк, какую нужно. К варианту настроечных файлов gcc_ipoib все сказанное здесь не относится;
  • не задавайте обе альтернативные строки сразу, это бессмысленно. Если сомневаетесь, какой вариант выбрать, выбирайте тот, где OpenMP. Если хотите понимать, что за всем этим стоит, читайте https://software.intel.com/sites/default/files/Reference_Manual_1.pdf, раздел "Process Pinning". Если совсем коротко: вариант без OpenMP обеспечивает прикрепление каждого MPI-процесса к конкретному физическому процессору, вариант с OpenMP - прикрепление всех MPI-процессов, находящихся на данном вычислительном узле, ко всем его физическим процессорам. Если этой информации мало - читайте https://software.intel.com/sites/default/files/Reference_Manual_1.pdf, раздел "Process Pinning";
  • во втором варианте альтернативной строки (где OpenMP) букв "f" внутри квадратных скобок должно быть 7 (семь). Обратная косая черта перед каждой квадратной скобкой - не опечатка и не ошибка форматирования. Должна быть.
 
 
 
 
 
 
 
  Тел. +7(499)220-79-72; E-mail: inform@kiam.ru