Низкая производительность из-за гиперпоточности с OpenMP: как привязать потоки к ядрам

Я разрабатываю большой плотный матричный код умножения. Когда я профилирую код, он иногда получает около 75% пиковых провалов моей четырехъядерной системы, а иногда около 36%. Эффективность не меняется между выполнениями кода. Он либо начинается с 75% и продолжается с той же эффективностью, либо начинается с 36% и продолжается с той же эффективностью.

Я проследил проблему до гиперпоточности и того факта, что я установил количество потоков равным четырем вместо восьми по умолчанию. Когда я отключаю гиперпоточность в BIOS, я постоянно получаю около 75% эффективности (по крайней мере, я никогда не видел резкого падения до 36%).

Перед вызовом любого параллельного кода я делаю omp_set_num_threads(4). Я также пробовал export OMP_NUM_THREADS=4 перед запуском своего кода, но он кажется эквивалентным.

Я не хочу отключать гиперпоточность в биосе. Я думаю, мне нужно привязать четыре потока к четырем ядрам. Я тестировал несколько разных случаев GOMP_CPU_AFFINITY, но до сих пор у меня все еще есть проблема, что иногда эффективность составляет 36%. Какое сопоставление с гиперпоточностью и ядрами? Например. поток 0 и поток 1 соответствуют одному и тому же ядру, а поток 2 и поток 3 — другому ядру?

Как привязать потоки к каждому ядру без переноса потоков, чтобы мне не приходилось отключать гиперпоточность в BIOS? Возможно, мне нужно изучить возможность использования sched_setaffinity?

Некоторые детали моей текущей системы: ядро ​​​​Linux 3.13, GCC 4.8, Intel Xeon E5-1620 (четыре физических ядра, восемь гиперпотоков).

Изменить: пока это работает хорошо

export GOMP_CPU_AFFINITY="0 1 2 3 4 5 6 7"

or

export GOMP_CPU_AFFINITY="0-7"

Изменить: это, кажется, также хорошо работает

export OMP_PROC_BIND=true

Изменить: Эти параметры также работают хорошо (gemm — это имя моего исполняемого файла)

numactl -C 0,1,2,3 ./gemm

и

taskset -c 0,1,2,3 ./gemm

person Z boson    schedule 23.06.2014    source источник
comment
Поскольку экспорт GOMP_CPU_AFFINITY=0 1 2 3 4 5 6 7 дает хорошие результаты, я думаю, это означает, что потоки 0 и 4 являются ядром 0, потоки 1 и 5 являются ядром 2, ... т.е. потоки назначаются как электроны на орбитах. Сначала он заполняет каждое ядро ​​(поток 0-3), а когда все ядра имеют поток, он возвращается и назначает оставшиеся потоки одному и тому же ядру (потоки 4-7).   -  person Z boson    schedule 23.06.2014
comment
И hwloc-ls из библиотеки hwloc, и cpuinfo из Intel MPI предоставляют важную информацию о топологии машины, например. сопоставление логических номеров ЦП с физическими ядрами/потоками. Нумерация зависит от BIOS, но, по моему опыту, в большинстве случаев гиперпотоки зацикливаются во внешнем цикле. Кроме того, вы можете использовать сокращенное обозначение "0-7".   -  person Hristo Iliev    schedule 24.06.2014
comment
@HristoIliev, для переносимости кажется, что правильный способ сделать это - использовать OMP_PLACES, например. export OMP_PLACES=cores из OpenMP4.0. В системах AMD каждый модуль имеет только один FPU, но получает два потока, и я думаю, что он назначается линейно stackoverflow.com/questions/19780554/ поэтому я думаю, что GOMP_CPU_AFFINITY=0-7 не сработает. На самом деле, OMP_PROC_BIND=true тоже может подойти. Может быть, это лучшее решение.   -  person Z boson    schedule 24.06.2014
comment
Мой комментарий был только о том, что "0-7" совпадает с "0 1 2 3 4 5 6 7". С libgomp OMP_PROC_BIND=true практически не отличается от GOMP_CPU_AFFINITY="0-(#cpus-1)", т.е. нет понимания топологии, по крайней мере, для версий до 4.9.   -  person Hristo Iliev    schedule 24.06.2014
comment
@HristoIliev, о, я понимаю. В этом случае OMP_PROC_BIND=true на AMD может не работать. Возможно, мне придется выполнить GOMP_CPU_AFFINITY=0 2 4 6 1 3 5 7 с AMD (у меня нет системы для проверки). Единственным преимуществом OMP_PROC_BIND является то, что GOMP_CPU_AFFINITY зависит от GCC.   -  person Z boson    schedule 24.06.2014
comment
Предполагается, что OMP_PROC_BIND включает какую-то привязку, специфичную для реализации. Функция places в OpenMP 4.0 предоставляет пользователю возможность управлять этой привязкой абстрактным образом. В реализациях до версии 4.0 вы должны запустить hwloc-ls или cpuinfo, чтобы получить актуальную топологию (или проанализировать /proc/cpuinfo самостоятельно).   -  person Hristo Iliev    schedule 24.06.2014
comment
@HristoIliev, спасибо, кажется, теперь я понял. Я проанализировал /proc/cpuinfo в моей системе с одним сокетом и в моей системе с четырьмя сокетами NUMA. Похоже, топология эквивалентна KMP_AFFINITY=granularity=fine,scatter с ICC. Это то, что я хочу от процессоров Intel. Я не знаю, какова топология AMD, но я думаю, что ядра AMD на самом деле рассматриваются как отдельные ядра (они предназначены для целых чисел, но не для чисел с плавающей запятой) и не осведомлены о модулях. Это означает, что я должен сделать что-то другое для систем AMD. Это раздражает.   -  person Z boson    schedule 24.06.2014
comment
На процессорах AMD мне пришлось использовать GOMP_CPU_AFFINITY=0-24:2, чтобы получить достойную производительность. Ядра без FPU для меня в этом веке просто поддельные ядра.   -  person Vladimir F    schedule 28.06.2014
comment
@VladimirF, спасибо, это то, что я подозревал для AMD. Это означает, что я должен сделать что-то другое для AMD, а не для Intel.   -  person Z boson    schedule 30.06.2014


Ответы (1)


Это не прямой ответ на ваш вопрос, но, возможно, стоит посмотреть: очевидно, гиперпоточность может привести к перегрузке кеша. Вы пытались проверить valgrind, чтобы узнать, какая проблема вызывает вашу проблему? Может быть быстрое исправление путем размещения некоторого мусора в верхней части стека каждого потока, чтобы ваши потоки не выбрасывали строки кеша друг друга.

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

Для быстрого теста - если вы измените свои входные матрицы на что-то, что не кратно размеру вашего кеша (поэтому они больше не выровнены по границе), и ваши проблемы исчезнут, то есть большая вероятность, что вы имеете дело с конфликт пропускает.

person Patrick Collins    schedule 23.06.2014
comment
В какой-то момент я должен использовать valgrind (никогда его не использовал). Но тот факт, что гиперпоточность усугубляет ситуацию, в моем коде неудивителен. Гиперпоточность полезна для неоптимизированного кода. Кроме того, когда я запускаю GEMM в MKL, в моей системе используется четыре потока, а не восемь. На самом деле гиперпоточность высоко оптимизированного кода дает худшие результаты. - person Z boson; 23.06.2014