Эффективная параллелизация с учетом NUMA

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

В обычном сценарии до инициализации данных (чтение из файлов, распределение по узлам, создание структур данных) руки не доходят. Обычная политика NUMA, касающаяся выделения памяти на NUMA-узлах работает по алгоритму ‘first touch’ — кто первый запросил и инициализировал память, на сокете того процесса и будет выделена память. Подразумевается что тот процесс, который память инициализирует и будет с ней работать. Однако в случае «точечного» распараллеливания на OpenMP поток-мастер проводит всю инициализацию, а в вычислительной и распараллеленой части потоки вне сокета мастер-потока будут передавать данные через QPI — межпроцессорную шину, которая довольно быстрая, но всё равно медленнее локальной памяти.

Чтобы избежать такого эффекта следует распараллеливать код инициализации таким же образом как и вычислительную часть. К примеру, если внутри вычислительного участка есть тройной цикл с обращением к двум массивам, то и в иницилизации этих массивов должен быть тройной цикл записи начальных значений. Настройки OpenMP планировщика так же должны быть идентичны. В этом случае массив расположится по NUMA-узлам так же как и в вычислительном коде, минимизируя количество доступа в дальнюю память.

Следует обратить внимание что присваивание страниц памяти NUMA-узлу происходит не в момент выделения (malloc) а только в момент инициализации, т.е. первой записи.

Для изменения политики NUMA существует утилита numactl и две ее опции: —membind (задают на каких узлах выделять память) и —interleave, на котором остановимся чуть подробнее. Второй флаг задает режим «размазывания» памяти по NUMA-узлам, и полезен для случая когда несколько вычислительных участков в одной программе имеют различный шаблон доступак памяти. В этом случае невозможно получить идентичную инициализацию, но можно уменьшить частоту доступа в дальнюю память «в среднем» работая в режиме —interleave=all.

dmitry

Добавить комментарий