УДК 004.021
Оптимизация барьерной синхронизации на асимметричных NUMA-подсистемах процессорных ядер
М. Г. Курносов, Е. И. Токмашева1
Предложен алгоритм MinNumaDist динамического выбора MPI-процесса, в памяти NUMA-узла которого размещаются совместно используемые флаги и счетчики алгоритмов барьерной синхронизации (MPI_Barrier). В качестве минимизируемого показателя используется суммарное расстояние до NUMA-узлов всех процессов (минимум степени близости). Экспериментально показано, что MinNumaDist позволяет сократить на 10-35 % время выполнения алгоритмов операции MPI_Barrier на асимметричных подсистемах процессорных ядер при различном числе процессов на NUMA-узлах или при использовании разного числа NUMA-узлов на нескольких процессорах.
Ключевые слова: барьерная синхронизация, barrier, MPI, NUMA.
1. Введение
Операция барьерной синхронизации (MPl_Barrier) блокирует выполнение процесса параллельной MPI-программы до тех пор, пока каждый процесс коммуникатора не осуществит её вызов. Процесс выходит из барьерной синхронизации только после того, как все процессы коммуникатора начали её выполнение. Основная сфера применения барьера в параллельных программах - измерение времени выполнения отдельных участков кода (профилирование) и синхронизация доступа процессов к общему ресурсу: например, ожидание завершения записи данных в файл или в сегмент разделяемой памяти.
Увеличение числа процессоров и процессорных ядер на одном вычислительном узле (сервере) привело к широкому распространению NUMA-архитектуры, в которой каждый процессор имеет один или несколько интегрированных контроллеров доступа к оперативной памяти. В таких системах время доступа к памяти зависит от размещения процессорного ядра - обращение к памяти своего процессора выполняется через его локальный контроллер, а доступ к памяти другого процессора требует перехода по межпроцессорной шине (например, Intel Ultra Path Interconnect, AMD Infinity Fabric, HiSilicon Hydra). В данной работе рассматривается задача оптимизации времени выполнения барьерной синхронизации процессов MPI-программ в пределах одной многопроцессорной NUMA-системы (сервера) с общей памятью. Такая постановка задачи возникает, если всем процессам MPI-программы достаточно процессорных ядер одного узла или при выполнении иерархических (многоуровневых) алгоритмов барьерной синхронизации, в которых процессы каждого вычислительного SMP/NUMA-узла образуют отдельный MPI-коммуникатор и выполняют барьер [1, 2].
Известные алгоритмы барьерной синхронизации для многопроцессорных систем с общей памятью: глобальный счетчик (central counter) [3], плоское дерево [4], плоское дерево с фазами gather/release [4], объединяющее дерево (combining tree) [5], MCS-барьер [6], турнирный алгоритм (tournament) [7], рассеивающий (dissemination) [8] основаны на использовании счетчиков
1 Работа выполнена при поддержке РФФИ (проект № 20-07-00039).
и флагов в сегменте разделяемой памяти, при помощи которых процессы уведомляют друг друга о достижении барьера или его определенного этапа. Время выполнения барьера зависит от времени доступа к разделяемым переменным. Для NUMA-систем, в которых время доступа к физическим страницам оперативной памяти неоднородно, актуальным является адаптация известных алгоритмов барьерной синхронизации и разработка новых.
В данной работе предложен алгоритм MinNumaDist динамического выбора процесса, в памяти NUMA-узла которого размещаются совместно используемые флаги и счетчики алгоритмов барьерной синхронизации. Алгоритм MinNumaDist выбирает корневым процесс, суммарное расстояние от NUMA-узла которого до NUMA-узлов всех остальных процессов минимально. В следующих разделах работы приведено описание архитектуры NUMA-систем с общей памятью, для известных алгоритмов барьерной синхронизации построены оценки их эффективности, описан разработанный алгоритм MinNumaDist и приведены результаты экспериментов.
2. Многопроцессорные NUMA-системы с общей памятью
В NUMA-системе процессорные ядра разбиты на группы - NUMA-узлы. Каждый NUMA-узел образован ядрами одного процессора и его контроллером доступа к оперативной памяти. При обращении процессорного ядра к памяти своего процессора доступ осуществляется через локальный контроллер памяти, а при обращении к памяти удаленного процессора используется шина межпроцессорного соединения и контроллер памяти удаленного процессора. Некоторые микроархитектуры поддерживают разделения одного процессора на несколько NUMA-узлов. Например, технологии Cluster-on-Die в Intel Haswell и Sub-NUMA-Clustering в Intel Sky-lake позволяют разделить процессор, его кеш-память последнего уровня и интегрированные контроллеры памяти на несколько NUMA-узлов.
2 x AMD EPYC (32 cores, 4 NUMA)
IMC
L3
□ □
□ □ □ □
IMC
L3
□ □
□ □ □ □
IMC
L3
□ □
□ □ □ □
IMC IMC \о IMC IMC
L3 L3 L3 L3
0 1 < 1 < 4 5 8 9
2 3 5; 6 7 10 11
!
IMC
L3
□ □
□ □ □ □
Distance ['][/■ ]
i / / 0 1 2 3 4 5 6 7
0 10 16 16 16 28 28 22 28
1 16 10 16 16 28 28 28 22
2 16 16 10 16 22 28 28 28
3 16 16 16 10 28 22 28 28
4 28 28 22 28 10 16 16 16
5 28 28 28 22 16 10 16 16
6 22 28 28 28 16 16 10 16
7 28 22 28 28 16 16 16 10
Infinity Fabric
Рис. 1. 8 NUMA-узлов сервера на базе двух процессоров с микроархитектурой AMD EPYC (32 ядра, 4 кристалла и 4 контроллера памяти (IMC), режим Channel-Interleaving - 4 NUMA узла на кристалл), межкристальное соединение Infinity Fabric, справа - матрица расстояний между NUMA-узлами (numactl -H); числа на процессорных ядрах - номера процессов
2 x Intel Cascade Lake (20 cores, 2 Sub-NUMA Clusters)
NUMA T
NUMA 0 i UPI
и § Я ■ ■ ■ ■ ■ ■
и § ююююю ююююю
NUMA 2
и
ИШ1
и и
■
■
и
Distance [i][/]
i / / 0 1 2 3
0 10 21 11 21
1 21 10 21 11
2 11 21 10 21
3 21 11 21 10
NUMA 3
Рис. 2. 2 NUMA-узла сервера на базе двух процессоров с микроархитектурой Intel Cascade Lake (20 ядер, 2 контроллера памяти с активным режимом Sub-NUMA Clustering), межпроцессорное соединение Intel UPI; на процессорных ядрах показаны номера процессов
Количество NUMA-узлов на многопроцессорном сервере зависит от микроархитектуры процессора, числа процессоров и активированного в BIOS режима разделения системы на NUMA-узлы (Intel Sub-NUMA Clustering, AMD EPYC Channel-Interleaving, HiSilicon/Huawei Channel Interleaving/One Numa Per Socket). При использовании AMD EPYC (рис. 1) на узле может быть до 8 NUMA узлов, каждый из которых образован 8 ядрами одного кристалла (die) и его контроллером памяти (IMC). Кристаллы (NUMA-узлы) соединены межпроцессорной шиной Infinity Fabric: каждый кристалл имеет прямые соединения со всеми кристаллами своего процессора, а также прямое подключение к одному из кристаллов другого процессора. При обращении ядра NUMA-узла 2 к страницам физической памяти NUMA-узла 1 выполняется один переход по шине Infinity Fabric в пределах процессора (расстояние d2i = 16), а при обращении к страницам памяти NUMA-узла 6 выполняется один межпроцессорный переход и один переход в пределах процессора к узлу (d26 = 28). Аналогично в Intel Cascade Lake: обращение к страницам удаленного процессора требует перехода по шине UPI. Время обращения к памяти зависит от того, с какого процессора оно выполняется. Прикладные программы имеют доступ к матрице расстояний между NUMA-узлами через системные вызовы и библиотеки (например, numa_distance из libnuma в GNU/Linux).
3. Постановка задачи
Имеется p MPI-процессов, которые распределены системой управления ресурсами (SLURM, TORQUE, Altair BPS Pro, IBM Platform LSF) по процессорным ядрам. Каждый процесс привязан к процессорному ядру и входит в состав одного NUMA-узла. При инициализации нового MPI-коммуникатора процессы создают совместно используемый сегмент разделяемой памяти (mmap), который затем используется для передачи сообщений в ходе выполнения алгоритмов барьерной синхронизации. В начале сегмента располагается блок - непрерывная область в виртуальном адресном пространстве - с глобальными счетчиками и флагами, а далее следуют индивидуальные блоки процессов. Длины блоков всех процессов одинаковы и кратны размеру страницы физической памяти. Зная свой номер rank и размер блока, каждый процесс вычисляет начало своего участка в памяти. В блоке каждого процесса хранятся его локальные счетчики и флаги. Чтобы избежать ложного разделения данных (false sharing) под каждый счетчик/флаг отводится область, длина которой равна размеру строки кеш-памяти целевой архитектуры (для Intel 64 - 64 байта), с соответствующим выравниванием начального адреса. Для сокращения времени доступа к данным своего блока каждый процесс пытается выделить физические страницы памяти с локального NUMA-узла. С этой целью каждый процесс самостоятельно инициализирует счетчики в своем блоке. В соответствии с политикой first touch policy ядра операционной системы GNU/Linux это приводит к выделению страницы физической памяти с NUMA-узла ядра, на котором выполняется процесс, осуществивший первую запись в страницу памяти. По умолчанию блок с глобальными счетчиками инициализирует корневой процесс - по умолчанию процесс 0 [3, 4]. Таким образом, глобальные счетчики и флаги будут размещены в памяти NUMA-узла корневого процесса 0. Данное решение не всегда обеспечивает минимум времени выполнения операций доступа к памяти, в частности, на асимметричных подсистемах процессорных ядер. Например, на рис. 1 корневой процесс 0 разместит глобальные флаги в памяти своего NUMA-узла, в то время как значительная часть процессов находится на NUMA-узлах 6 и 5 второго процессора. Учитывая известные расстояния между NUMA-узлами, эффективнее выбрать в качестве корня процесс 4, суммарное расстояние от которого до остальных процессов меньше.
На этапе формирования сегмента разделяемой памяти требуется выбрать корневой процесс root е{0,1,...,p — lj, который разместит в памяти своего NUMA-узла глобальные счетчики и флаги алгоритмов барьерной синхронизации. Цель - обеспечить минимум суммарного времени обращения процессов к глобальным счетчикам и флагам при выполнении заданного алгоритма барьерной синхронизации.
4. Алгоритмы барьерной синхронизации
Рассмотрим известные алгоритмы барьерной синхронизации для многопроцессорных вычислительных систем (ВС) с общей памятью и оценим время их работы.
4.1. Алгоритм с глобальным счетчиком
Алгоритм с глобальным счетчиком (central counter) использует две разделяемые всеми процессами переменные - счетчик counter числа процессов, не достигших барьера, и флаг sense уведомления о достижении барьера каждым процессом. При входе в барьер каждый процесс атомарно уменьшает на единицу глобальный счетчик counter. Процесс rank, который уменьшил счетчик до 0, переустанавливает counter в значение p для следующего вызова барьера и уведомляет процессы о том, что все достигли барьера, - записывает в глобальный флаг sense значение своего локального флага sense_local[rank]. Остальные процессы ожидают, пока значение локального флага sense_local[rank] не станет равным значению глобального флага sense. В начале барьера каждый процесс меняет значение флага sense_local[rank] на противоположное, что обеспечивает корректную работу многократных вызовов барьера (схема sense reversing [3, 4, 6]). Начальные состояния: counter = p, sense = 1, sense_local[rank] = 1.
algorithm BarrierCentralCounter
sense_local[rank] = sense_local[rank] xor 1 if atomic_fetch_and_add(counter, -1) = 1 then counter = p
sense = sense_local[rank] // Уведомление о выходе
eise
while sense != sense_local[rank] do // Ожидание уведомления end if end algorithm
В данном алгоритме заранее неизвестно, какой процесс последним уменьшит значение счетчика до 0 и выполнит запись в флаг sense. Время выполнения алгоритма линейно зависит от числа p процессов
t = P- tAtomicDec + tNotify = P ' tAtomicDec + tW = O (P ) >
где tAtomicDec - время выполнения атомарной операции, tw - время выполнения записи в разделяемый флаг sense. Основной фактор, ограничивающий масштабируемость алгоритма, -конкуренция p процессов за выполнение атомарной операции с глобальным счетчиком.
4.2. Плоское дерево
Алгоритм плоского дерева (flat tree) хранит в разделяемой памяти для каждого процесса счетчик state[rank] числа обращений к барьеру, а также общий для всех флаг sense уведомления о достижении барьера каждым процессом. При входе в барьер каждый процесс rank увеличивает свой счетчик state[rank] на единицу. Корневой процесс root ожидает, пока значение счетчика state[child] каждого из p - 1 дочерних процессов не станет больше или равно значению счетчика state[root] корня. После этого корень уведомляет процессы через флаг sense о том, что все достигли барьера. Начальные состояния: state[rank] = 0, sense = 1, sense lo-cal[rank] = 1. По умолчанию корнем является процесс 0. Флаг sense хранится в памяти NUMA-узла корневого процесса.
algorithm BarrierFlatTree
sense_local[rank] = sense_local[rank] xor 1 state[rank]++ if rank = root then
while true do // Ожидание входа дочерних процессов
narrived = 0
for child = 0 to p - 1 do
if state[child] >= state[root] then narrived++
end for
if narrived = p then break end while
sense = sense_local[rank] // Уведомление о выходе
else
while sense != sense_local[rank] do // Ожидание уведомления end if end algorithm
Время алгоритма линейно зависит от числа процессов. Однако в нем не используются атомарные операции, а для состояний дочерних процессов state[child] выполняется только операция чтения, что положительно сказывается на масштабируемости алгоритма. Время выполнения определяется временем работы корневого процесса, который в худшем случае за s итераций цикла опросаp дочерних процессов дождется изменения их состояния state[child] на требуемое:
t = 5 • p • tR + tW = O (p),
где tR - время чтения состояния дочернего процесса.
4.3. Плоское дерево Gather/Release
В разделяемой памяти для каждого процесса содержится два счетчика gather_state[rank] и release_state[rank] числа обращений к барьеру [4]. На фазе gather корневой процесс ожидает входа в барьер всех процессов - ждет установки ими значения счетчика gather_state[child]. После этого на фазе release корневой процесс уведомляет дочерние процессы о возможности выхода из операции - записывает в их счетчики release_state[child] сигнальное значение, которое они ожидают. Данный алгоритм реализован в run-time библиотеке Intel OpenMP RTL. Главным отличием от плоского дерева является явное уведомление процессов о возможности выхода из барьера через индивидуальные счетчики, а не через глобальный флаг sense, доставкой которого до получателей занимается аппаратный механизм обеспечения когерентности кеш-памяти. Начальные состояния: gather_state[rank] = 0, gather_state_local[rank] = 0, re-lease_state[rank] = 0, release_state_local[rank] = 0.
algorithm BarrierGatherRelease
// Gather: ожидание входа в барьер всех процессов gather_state_local[rank]++ if rank = root then
for child = 0 to p - 1 do
if child = root then continue
while gather_state[child] != gather_state_local[root] do end for end if
gather_state[rank] = gather_state_local[rank] // Release: уведомление о выходе из барьера release_state_local[rank]++ if rank = root then
for child = 0 to p - 1 do
if child = root then continue
release_state[child] = release_state_local[rank] end for else
while release_state[rank] != release_state_local[rank] do end if end algorithm
Время выполнения линейно зависит от числа процессов и определяется шагами корневого процесса:
t = ( p-1)-5 • tR +(p-1) tw = O (p).
Явное уведомление дочерних процессов на фазе Release (запись в счетчики дочерних процессов) увеличивает время работы алгоритма по сравнению с плоским деревом, однако делает возможным его работу на системах без эффективной аппаратной поддержки когерентности кеш-памяти.
4.4. Объединяющее дерево
Алгоритм объединяющего дерева (combining tree) [5, 6] сокращает конкуренцию за доступ p процессов к одному глобальному счетчику (memory contention) путем введения множества глобальных переменных, за каждую из которых конкурирует порядка O(logp) процессов для выполнения атомарной операции. Известно, что алгоритм сокращает конкуренцию, но в то же время может увеличить латентность операции (время её выполнения), так как распространение уведомлений от листьев до корня требует порядка O(logp) шагов. Все процессы логически организованы в завершенное k-арное дерево с корнем в процессе 0 (completed k-ary tree). С каждым процессом rank ассоциирован разделяемый счетчик counter[rank] и локальный флаг sense_local[rank]. После входа в барьер корневой процесс и каждый внутренний узел rank дерева ожидают достижения барьера дочерними узлами - равенства нулю его счетчика coun-ter[rank]. После этого внутренний узел уведомляет родительский процесс о достижении барьера - атомарно уменьшает счетчик counter[parent] родителя на единицу. Выход из барьера осуществляется по сигналу от корневого процесса - он переключает значение глобального флага sense на противоположное, которое ожидают все некорневые процессы. На выходе из барьера процессы переключают флаг sense_local[rank]. Начальные состояния: counter[rank] равно числу дочерних процессов, sense = 0, sense_local[rank] = 1.
algorithm BarrierCombiningTree
while counter[rank] != 0 do // Ожидание дочерних процессов
counter[rank] = tree[rank].nchilds if rank = 0 then
sense = sense xor 1 // Уведомление о выходе из барьера
else
parent = tree[rank].parent
atomic_fetch_and_add(counter[parent], -1) while sense != sense_local[rank] do end if
sense_local[rank] = sense_local[rank] xor 1 end algorithm
Уведомление о достижении процессами барьера распространяется от листьев к корню, на что требуется k\\ogk р] атомарных операций уменьшения разделяемых счетчиков (рис. 3а).
t = tAtomicDec - k loBk Р + TW = O (k loBk P).
counter[0]
Ф ^ ^ ^
counter[3] counter[4] counTer[5] counjfer[6]
i unter[
NUMA 0 NUMA 1
counter[7]
0
1,2,3,..., 7
а)
state[0]
A
Sate[4] sense I
state[2]
, , A
Sate[3] state[4] state[5] state[6]
f © © ©
state[7]
ae
NUMA 0 NUMA 1
0
1,2,3,...,7
6)
Рис. 3. Алгоритм объединяющего дерева на базе завершенного бинарного дерева (к = 2, p = 8, корневой процесс 1, снизу показано распределение процессов по двум КИМЛ-узлам): а) алгоритм с использованием атомарных операций; б) алгоритм без атомарных операций
В алгоритме объединяющего дерева без атомарных операции каждый процесс содержит в разделяемой памяти счетчик state[rank] числа обращений к барьеру (рис. 3б). При входе в барьер каждый листовой процесс rank увеличивает свой счетчик state[rank] на единицу. Корневой и внутренние процессы ожидают, пока значение счетчика state[child] каждого из дочерних процессов не станет больше значения счетчика state[root] корня. После этого корневой и внутренние процессы увеличивают свои счетчики state[rank]. Далее корень уведомляет всех о завершении барьера через глобальный флаг sense.
algorithm BarrierCombiningTreeNoAtomic if tree[rank].nchilds > 0 then
while true do // Ожидание дочерних процессов
narrived = 0
for i = 0 to tree[rank].nchilds - 1 do child = tree[rank].childs[i] if state[child] >= state[rank] then narrived++ end for
if narrived = tree[rank].nchilds then break end while end if
state[rank]++ if rank = 0 then
sense = sense xor 1 // Уведомление о выходе из барьера
else
while sense != sense_local[rank] do end if
sense_local[rank] = sense_local[rank] xor 1 end algorithm
Время выполнения определяется временем работы процесса 0, который в худшем случае ожидает завершения к\logк р] опросов дочерних процессов:
* = я ■ к ■ \оёкр + Тж = О (к \о%к р).
4.5. MCS-барьер
В алгоритме MCS-барьер (Mellor-Crummey-Scott) [6] используется два дерева: дерево ожидания входа процессов в барьер (дерево прибытия, arrival tree) и дерево явного уведомления о выходе из барьера (wakeup tree). Каждый процесс включен в оба дерева. Корень и каждый внутренний процесс дерева прибытия ожидают, пока их дочерние процессы не войдут в барьер
- все флаги childnotready[rank\[child\ не примут значение 0. Затем выполняется уведомление родительского процесса через запись в его флаг childnotready[parent\[child\ значения 0. После получения уведомления корневой процесс 0 информирует всех о выходе из барьера - записывает в флаг sense [i\ каждого дочернего процесса i значение своего локального флага senselo-cal. На выходе из барьера процесс меняет значение флага senselocal на противоположное. Начальные состояния: childnotready[rank\[i\ = 1, sense _local[rank\ = 0, sense[rank\ = 1; дерево прибытия - завершенное k-арное дерево степени fanin (рис. 4а), дерево уведомления о выходе
- завершенное g-арное дерево степени fanout (рис. 46).
Arrival tree (к = 2) Wakeup tree (q = 4)
sense[0]
childnotready[0][2] ^-(O^
fs/ sense[1] sense[2] sense[3] sense[4]
^ШпоШ>с1у[1][2] childnotready[2][2] ® ®
y' sense[5] sense[6] sense[7]
childnotfeädy[3] [2]^
NUMA 0 NUMA 1
Ö © © ©
0
1,2,3,..., 7
а) б)
Рис. 4. Алгоритм МС8-барьера ф = 8, корневой процесс 1, снизу показано распределение процессов
по двум NUMA-узлам): а) бинарное дерево «прибытия»; б) 4-арное дерево уведомления о выходе из барьера
algorithm BarrierMCS
if arrival_tree[rank].nchilds > 0 then
while true do // Ожидание дочерних процессов
narrived = 0
for i = 0 to arrival_tree[rank].nchilds - 1 do if childnotready[rank][i] = 1 then narrived++
end for
if narrived = arrival_tree[rank].nchilds then break end while end if
// Установка флагов для следующего вызова алгоритма for i = 0 to arrival_tree[rank].nchilds - 1 do
childnotready[rank][i] = 1 end for
if rank > 0 then
// Уведомление родителя о входе в барьер i = (rank - 1) % fanin
childnotready[arrival_tree[rank].parent][i] = 0 // Ожидание сигнала от родителя о выходе из барьера while sense[rank] != sense_local[rank] do end if
for i = 0 to wakeup_tree[rank].nchilds - 1 do child = wakeup_tree[rank].childs[i]
sense[child] = sense_local[rank] // Уведомление дочерних о выходе
end for
sense_local[rank] = sense_local[rank] xor 1 end algorithm
Время выполнения каждой из двух фаз алгоритма логарифмически зависит от числа процессов. Активное ожидание выполняется на флагах, ассоциированных с текущим процессом:
t = ((5 •k • tR + k • tW ) logkP + (5 • tR + q • tW ) logqP = O (k logkP + q logqP )•
В литературе описаны вариации MCS-барьера, использующие не только завершенные k-арные деревья, но и k-номиальные [7].
4.6. Турнирный алгоритм
В турнирном алгоритме (tournament) [8] процессы организованы в биномиальное дерево (рис. 5). Каждый процесс выполняет не более Tlog2 р] шагов. На шаге i процесс rank взаимодействует (участвует в «игре») с процессом peer = rank xor 2г. Процесс rank считается проигравшим «игру», если его номер rank больше номера peer соперника. Проигравший процесс устанавливает флаг противника flag[peer][i] в 1, а победивший процесс ожидает установления флага. По окончании турнира процесс 0 уведомляет все процессы о выходе из барьера переключением глобального флага sense. Начальные состояния: flag[rank][i] = 0, sense _local[rank] = 0, sense[rank] = 1.
algorithm BarrierTournament round = 0 mask = 1
while mask < p do
peer = rank xor mask if peer < p then
if rank > peer then
flag[peer][round] = 1 // Проигравший уведомляет
else
while flag[rank][round] != 1 do // Победитель ожидает
flag[rank][round] = 0 end if end if
round = round + 1 mask = mask * 2 end while
if rank = 0 then // Уведомление о выходе из барьера
sense = sense xor 1 else
while sense != sense_local[rank] do end if
sense_local[rank] = sense_local[rank] xor 1 end algorithm
f/ag[0][3]
sense{ П (4,
f/ag[2][3] f/ag[4][3]
3) (5) (6;
f/ag[6][3] NUMA 0 NUMA 1 t 0 1,2,3,...,7 (t)
Рис. 5. Турнирный алгоритм ф = 8, корневой процесс 1, снизу распределение процессов по двум КИМЛ-узлам)
Корневой процесс 0 ожидает установления флагов от \^2 р] дочерних процессов в дереве турнира, после чего уведомляет процессы о выходе из барьера:
t = 5 • tR log2 p + Tw = O (log2 P) •
4.7. Рассеивающий алгоритм
Рассеивающий алгоритм (dissemination) [9] относится к классу симметричных алгоритмов барьерной синхронизации - все процессы выполняют одинаковые шаги и не нуждаются в уведомлении о выходе из барьера (корень отсутствует, рис. 6). Каждый процесс выполняет flog2 р] шагов. На шаге i процесс rank увеличивает свой счетчик state[rank] на единицу и переходит к ожиданию установки счетчика удаленным процессом peer = (rank + 2г) % p. Начальное состояние: state[rank] = 0.
Время работы алгоритма логарифмически зависит от числа процессов, на каждом шаге процесс выполняет активное ожидание на счетчике другого процесса:
t = (% + ^ ■ tR ) 2 Р = O(logP)•
algorithm BarrierDissemination mask = 1
while mask < p do
peer = (rank + mask) % p state[rank]++
while state[peer] < state[rank] do mask = mask * 2 end while end algorithm
5. Алгоритм выбора корня операции
Авторами предложен алгоритм MinNumaDist динамического выбора корневого процесса, в памяти NUMA-узла которого размещаются совместно используемые флаги и счетчики алгоритмов барьерной синхронизации (в частности, флаг sense). Алгоритм MinNumaDist выбирает процесс, расстояние от NUMA-узла которого до NUMA-узлов всех остальных процессов минимально. Формально отыскивается процесс с минимальной степенью близости узла (closeness centrality, closeness) - мера центральности в графе, вычисляемая как обратная величина суммы длин кратчайших путей между узлом и всеми другими узлами графа:
p-1
С (;) = 1/ S d(i, j),
j=0
где d(i, j) - кратчайшее расстояние между NUMA-узлами процессов i и j. По сравнению с таким показателем, как центр графа, степень близости учитывает суммарное расстояние до всех вершин, а не ограничивается максимумом среди кратчайших расстояний. Ниже приведен псевдокод разработанного алгоритма, который реализован авторами в виде отдельного компонента coll/shm библиотеки Open MPI. Функция ProcNumaNode получает информацию о номере NUMA-узла процесса через интерфейс PMIx (pmix_locality_string), функция NumaDistance(a, b) возвращает расстояние между NUMA-узлами a и b (безразмерная величина, функция библиотеки GNU/Linux libnuma). Алгоритм выполняется только на этапе создания MPI-коммуникатора.
sfate[0]
state?] .-{fy; state[1]
state6]2y'^\ i|\ Wate[2]
NUMA 0 NUMA 1
ir^ii2)
' ¡1 l/ Г
state[3]
0
1,2,3,...,7
Ж
state[4]
Рис. 6. Рассеивающий алгоритм ^ = 8, корневой процесс 1, снизу распределение процессов по двум КиМЛ-узлам)
algorithm MinNumaDist dsum_min = от for i = 0 to p - 1 do
dsum = 0
for j = 0 to p - 1 do
dsum = dsum + NumaDistance(ProcNumaNode(i), ProcNumaNode(j))
end for
if dsum < dsum_min then dsum_min = dsum root = i
end if end for return root end algorithm
Сложность алгоритма квадратично зависит от числа процессов O(p2). На практике значение p редко превышает 128. Алгоритм легко распараллеливается - каждый процесс i самостоятельно вычисляет степень близости C(i) и выполняет глобальную редукцию (Allreduce) для определения номера процесса с минимальным значением C(i).
6. Результаты экспериментов
Эксперименты выполнены на двухпроцессорном сервере Intel Xeon Broadwell: 2 x Intel Xeon E5-2620 v4 (8 ядер, HyperThreading отключен); RAM 64 GB (2 NUMA-узла); ядро linux 4.18.0-80.11.2.el8_0.x86_64, gcc 8.2.1); использовалась master-ветвь пакета Open MPI 4 (параметры сборки: cflags="-o3 -g" --disable-debug). В качестве теста использовался пакет OSU Micro-Benchmarks. Для каждого числа процессов операция MPI_Barrier запускалась 1000 раз (параметры запуска теста: osu_barrier -x1 -i1000 -f). В каждом эксперименте тест OSU запускался 10 раз, отбрасывались наименьшее и наибольшее значения максимальной латентности (Max Latency), далее среди оставшихся значений отыскивалась медиана времени работы MPI_Barrier.
На рис. 7а показано время выполнения алгоритмов барьерной синхронизации для линейного распределения процессов по процессорным ядрам (наиболее распространенная политика распределения) - глобальные флаги и счетчики размещены в NUMA-узле процесса 0, а на рис. 7б - для распределения с изолированным процессом 0 (при p < 10 процесс 0 изолирован на NUMA-узле 0). При количестве процессов больше числа ядер одного процессора, а также на асимметричных подсистемах процессорных ядер (рис. 7б) наблюдается увеличение времени выполнения алгоритмов. Основная причина - активное использование межпроцессорного соединения, через которое осуществляется доступ к памяти другого процессора (NUMA-узла). В частности, на асимметричной подсистеме (рис. 7б) алгоритмы активно обращаются к разделяемым флагам и счетчикам, которые корневой процесс 0 разместил в памяти своего NUMA-узла, в то время как большая часть процессов находятся на NUMA-узле 1 (приp < 16).
2,0
О)
о. со
1,5
1,0
0,5
0,0
1 Socket
^NUMA0 NUMA 1
0,1,2,...,7 8,9,...,15
6 8 10 12 14 Количество процессов а)
GR
MCS CC
DS TR CT FT
16
2,0
1,5
1,0
0,5
CD CP CQ
0,0
...........................
6 8 10 12 Количество процессов
б)
14
Рис. 7. Время выполнения MPI_Barrier (корневой процесс 0): FT - flat tree; TR - tournament; DS dissemination; CC - central counter; MCS - MCS-барьер (fanin = 4; fanout = 2); CT - combining tree (k = 2); GR - flat tree gather/release; а) линейное распределение процессов; б) асимметричное распределение с изолированным процессом 0 (худший случай)
2
4
2
4
На рис. 8 приведено время работы MPI_Barrier на асимметричной подсистеме с выбором корневого процесса алгоритмом MinNumaDist. На рис. 8 приp < 16 алгоритмMinNumaDist выбирает корневым процесс 1. Это позволяет сократить время барьерной синхронизации в среднем на 10-35 % в зависимости от алгоритма.
2,0
1,5
1,0
0,5
0,0
6 8 10 12 14 Количество процессов
2,0
1,5
1,0
0,5
0,0
RootO
MinNumaDist
NUMA 0 NUMA 1
0,7,8,...,15 1,2,3,...,8
GR
FT
6 8 10 12 14 16 Количество процессов
Рис. 8. Время выполнения MPI_Barrier (корень выбран алгоритмом MinNumaDist, распределение с изолированным процессом 0): FT - flat tree; TR - tournament; DS - dissemination; CC - central counter; MCS - MCS-барьер (fanin = 4; fanout = 2); CT - combining tree (k = 2); GR - flat tree gather/release
2
4
Эксперименты подтверждают плохую масштабируемость алгоритма с глобальным счетчиком (CC). Это обусловлено тем, что использование атомарных операций приводит к интенсивному трафику на уровне протокола поддержания когерентности кеш-памяти - каждый процесс (ядро) захватывает владение строкой кеш-памяти, содержащей счетчик counter (отправляются запросы MESI RFO - request for ownership). В качестве корня для этого алгоритма рекомендуется использовать процесс 0.
Время выполнения алгоритма плоского дерева (FT) существенно зависит от выбора корневого процесса. Корневой процесс выполняет только чтение флагов и счетчиков других процессов, что не приводит к значительному трафику на уровне протокола поддержания когерентности кеш-памяти. Выбор корня операции алгоритмом MinNumaDist позволил в среднем сократить время операции MPI_Barrier на асимметричных подсистемах на 10-35 % (рис. 9). Аналогичные результаты наблюдаются для алгоритма плоского дерева Gather/Release (GR) -применение алгоритма MinNumaDist позволяет сократить время выполнения MPI_Barrier на 23-134 %. По сравнению с алгоритмом FT, в этом алгоритме время сокращено значительнее, так как в нем присутствует явная фаза release, на которой корень записывает данные в счетчики дочерних процессов, расстояние до которых оптимизированно алгоритмом MinNumaD-ist. В общем случае алгоритм MinNumaDist позволяет сократить время выполнения только на ассиметричных подсистемах процессорных ядер - разное число процессов на NUMA-узлах
или при использовании разного числа NUMA-узлов на нескольких процессорах. Для других конфигураций подсистем целесообразно в качестве корня использовать процесс 0.
Алгоритм MinNumaDist не обеспечивает сокращения времени алгоритмов объединяющего дерева, MCS-барьера и турнирного алгоритмов. В этих алгоритмах процесс 0 всегда, не зависимо от расстояния до других, собирает вектор признаков о входе процессов в барьер и уведомляет о возможности выхода из операции через запись в глобальный флаг sense. Возможна ситуация, при которой процесс 0 и флаг sense будут принадлежать разным NUMA-узлам, что негативно сказывается на времени выполнения операции.
Сокращение времени выполнения алгоритмов объединяющего дерева, MCS-барьера, турнирного и рассеивающего возможно путем динамического перестроения деревьев или изменения нумерации процессов так, чтобы максимально локализовать взаимодействия в пределах одного процессора (NUMA-узла) и минимизировать межпроцессорный трафик.
7. Заключение
Разработанный алгоритм MinNumaDist динамически выбирает процесс, в памяти NUMA-узла которого размещаются совместно используемые флаги и счетчики алгоритмов барьерной синхронизации. В качестве минимизируемого показателя используется суммарное расстояние до NUMA-узлов всех процессов (минимум степени близости). В общем случае алгоритм Min-NumaDist позволяет сократить время барьерной синхронизации только на ассиметричных подсистемах процессорных ядер, при разном числе процессов на NUMA-узлах или при использовании разного числа NUMA-узлов на нескольких процессорах. Наибольшее сокращение времени достигнуто на алгоритмах плоского дерева (flat tree) и плоского дерева с явными фазами gather/release. Оптимизация времени выполнения алгоритмов объединяющего дерева, MCS-барьера, турнирного и рассеивающего алгоритмов ограничена. Сокращение времени выполнения этих алгоритмов возможно путем динамического перестроения деревьев или изменения нумерации процессов с целью локализации взаимодействий процессов в пределах общей кеш-памяти последнего уровня (LLC), NUMA-узла, процессора.
Литература
1. Graham R., Gorentla M., Ladd J., Shami P., Rabinovitz I., Filipov V., Shainer G. Cheetah: A Framework for Scalable Hierarchical Collective Operations // Proc. IEEE/ACM International Symposium on Cluster, Cloud and Grid Computing (CCGRID11), 2011. P. 73-83.
2. Zhu H., GoodellD., Gropp W., Thakur R. Hierarchical Collectives in MPICH2 // Proc. European PVM/MPI, 2009. LNCS, V. 5759. P. 325-336.
3. Graham R L., Shipman G. MPI Support for Multi-core Architectures: Optimized Shared Memory Collectives // Proc. 15th European PVM/MPI Users' Group Meeting, 2008. P. 130-140.
4. Jain S., Kaleem R, Balmana M., Langer A., Durnov D., Sannikov A. and Garzaran M. Framework for Scalable Intra-Node Collective Operations using Shared Memory // Proc. International Conference for High Performance Computing, Networking, Storage, and Analysis (SC-2018), 2018. P. 374-385.
5. Yew P. C., TzengN. F., Lawrie D. H. Distributing Hot Spot Addressing in Large Scale Multiprocessors // IEEE Transactions on Computers. 1987. V. C-36, Is. 4. P. 388-395.
6. Mellor-Crummey J. M., ScottM. L. Algorithms for Scalable Synchronization on Shared-memory Multiprocessors // ACM Transactions on Computer Systems. 1991. V. 9 (1). P. 21-65.
7. Tzeng N.-F., Kongmunvattana A. Distributed Shared Memory Systems with Improved Barrier Synchronization and Data Transfer // Proc. 11th International Conference on Supercomputing, 1997. P.148-155.
8. Hengsen D., FinkelR., Manber U. Two Algorithms for Barrier Synchronization // Int. Journal of Parallel Programming. 1988. V. 17, Is. 1. P. 1-17.
9. BrooksE. The butterfly barrier // Journal of Parallel Programming. 1986. V. 15, Is. 4. P. 295-307.
Статья поступила в редакцию 20.11.2020; переработанный вариант - 20.12.2020.
Курносов Михаил Георгиевич
д.т.н., профессор, профессор кафедры вычислительных систем Сибирского государственного университета телекоммуникаций и информатики (630102, Новосибирск, ул. Кирова, 86), тел. (383) 269-83-82, e-mail: mkurnosov@sibguti.ru;
старший научный сотрудник федерального государственного бюджетного учреждения науки Института физики полупроводников им. А. В. Ржанова Сибирского отделения Российской академии наук (630090, Новосибирск, пр. Ак. Лаврентьева, 13), тел. (383) 330-56-26, e-mail: mkurnsov@isp .nsc. ru.
Токмашева Елизавета Ивановна
аспирант, старший преподаватель кафедры вычислительных систем Сибирского государственного университета телекоммуникаций и информатики (630102, Новосибирск, ул. Кирова, 86), тел. (383) 269-82-75, e-mail: eliz_tokmasheva@sibguti . ru.
Barrier Optimization on Asymmetrical NUMA Subsystems M. Kurnosov, E. Tokmasheva
Algorithm MinNumaDist for barrier's root selection is proposed. A root process allocates memory pages for shared counters and flags from its NUMA node. Total distance is minimized to all NUMA nodes (closeness centrality) by the algorithm. MinNumaDist reduces barrier's time by 1035% for asymmetrical NUMA subsystems - for different number of processes on NUMA nodes or different number of NUMA nodes used from each socket.
Keywords: barrier, shared memory, MPI, NUMA.