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

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

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

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

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

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

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

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

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

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

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

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

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

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

Например:

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

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

  • по умолчанию Вы получите некоторое разумное (удовлетворительное) быстродействие, если значение ключа «- ppn» в команде mpirun не превосходит 12 (10 в случае, если используется shmem-express),
  • если Вы желаете прибегнуть к тонкой оптимизации, или желаете использовать более 12 (10) процессов на узле, то сразу после вызова MPI_Init() следует выполнить явную привязку, например, способом, описанным ниже,
  • если Вы используете shmem-экспресс, то явно привязывать свои процессы к процессорам с номерами 5 и 11 категорически не рекомендуется.

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

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

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

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

…………
#include <setaffinity.h>
// Маска привязываемых процессоров (все процессоры включены):
int mask[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
……………
// Нитей в параллельных областях должно быть 12:
omp_set_num_threads( 12 );
MPI_Init( &argc, &argv );
// Привязываем:
setaffinity( 12, mask );
……………

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

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

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

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

 
 
 
 
 
 
 
  Тел. +7(499)220-79-72; E-mail: inform@kiam.ru