Распределение памяти OpenMP на процессоре NUMA

В настоящее время я пытаюсь ускорить простой тест вычитания матриц с помощью OpenMP на процессоре Maestro, который имеет архитектуру NUMA и основан на процессоре Tilera Tile64. Плата Maestro имеет 49 процессоров, расположенных в двумерном массиве в конфигурации 7x7. Каждое ядро ​​имеет свой кэш L1 и L2. Макет платы можно увидеть здесь: https://i.imgur.com/naCWTuK.png

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

Для этого теста вычитания матрицы (C[i] = A[i] - B[i]) я подумал, что было бы неплохо выделить каждому потоку свои собственные частные массивы A, B и C с размером, равным сумме размер работы делится на количество потоков. Например, если общий размер массивов был 6000*6000, и я пытался распараллелить его на 20 потоков, я бы выделил частные массивы размером (6000*6000)/20. Каждый поток делал бы это вычитание на своем собственном частном массиве, а затем я собирал результаты обратно в окончательный массив с общим размером 6000*6000. Например (без сбора результатов каждого потока в окончательный массив):

int threads = 20;
int size = 6000;
uint8_t *C_final = malloc(sizeof(uint8_t)*(size*size));
#pragma omp parallel num_threads(threads) private(j)
{
     uint8_t *A_priv = malloc(sizeof(uint8_t)*((size*size)/threads));
     uint8_t *B_priv = malloc(sizeof(uint8_t)*((size*size)/threads));
     uint8_t *C_priv = malloc(sizeof(uint8_t)*((size*size)/threads));

     for(j=0; j<((size*size)/threads); j++)
       {
            A_priv[j]=100;
            B_priv[j]=omp_get_thread_num();
            C_priv[j]=0;
       }

     for(j=0; j<((size*size)/threads); j++)
       {
           C_priv[j] = A_priv[j]-B_priv[j];
       }
}

Начальные значения для массивов являются произвольными, у меня просто есть omp_get_thread_num(), поэтому я получаю разные значения в C_priv из каждого потока. В настоящее время я экспериментирую с пользовательской динамической сетью, которая есть на плате, которая обеспечивает аппаратное обеспечение для маршрутизации пакетов между процессорами, чтобы накапливать все результаты отдельных потоков в окончательный результирующий массив.

Я добился ускорения, выполнив это таким образом, а также закрепив потоки с помощью OMP_PROC_BIND=true, но я обеспокоен тем, что накопление отдельных результатов в окончательный массив может привести к издержкам, которые сведут на нет ускорение.

Это правильный способ решения проблемы такого типа? Какие методы следует использовать для ускорения работы архитектуры NUMA для решения проблемы, подобной этой, в которой используется OpenMP?

Редактировать:

Для пояснения, это то, что я изначально пробовал, и где я заметил более медленное время выполнения, чем если бы я просто запускал код последовательно:

     int threads = 20;
     int size = 6000;
     uint8_t *A_priv = malloc(sizeof(uint8_t)*(size*size));
     uint8_t *B_priv = malloc(sizeof(uint8_t)*(size*size));
     uint8_t *C_priv = malloc(sizeof(uint8_t)*(size*size));

     int i;
     for(i=0; i<(size*size); i++)
     {
       A[i] = 10;
       B[i] = 5;
       C[i] = 0;
     }

     #pragma omp parallel for num_threads(threads)
     for(i=0; i<(size*size); i++)
     {
       C[i] = A[i] - B[i];
     }

Увидев, что при использовании OpenMP я получаю более медленное время выполнения, я попытался выяснить, почему это так. Казалось, что проблема заключается в локальности данных. Это предположение основано на том, что я прочитал об архитектурах NUMA.

Я с трудом пытаюсь понять, как устранить узкие места, которые замедляют его. Я нашел некоторую помощь по схожим вопросам, таким как: OpenMP: для расписания, в котором говорится о распределении данных для каждого потока, чтобы каждый поток работал со своими локальными данными.

Я просто чувствую, что что-то столь же простое, как вычитание матрицы, не должно быть трудным для получения повышенной производительности при использовании OpenMP. Я не уверен, как понять, что такое узкое место и как его устранить.

0 ответов

При быстром поиске и сканировании таблицы данных TILE64 не похоже, что архитектура предоставляет счетчики производительности, как те, которые вы бы использовали на x86 с помощью таких инструментов, как oprofile, VTune или xperf. Без них вам придется разработать несколько собственных экспериментов, чтобы итеративно сузить круг вопросов, какая часть кода является горячей и почему - в отсутствие документации по микроархитектуре и инструментов, показывающих, как ваш код использует оборудование, немного задачи обратного проектирования.

Некоторые идеи о том, с чего начать:

  1. Проведите несколько экспериментов с масштабированием. Есть ли изгиб на кривой, когда превышение определенного размера проблемы или количества потоков оказывает большое влияние на общую производительность? Указывает ли это число на какую-то четкую связь с размером определенного уровня в иерархии памяти, размером сетки процессоров или чем-то подобным?
  2. Запишите время выполнения в нескольких точках программы. Вероятно, было бы полезно знать, например, на высоком уровне, сколько времени тратится на маллоки по сравнению с первым циклом по сравнению со вторым.
  3. "Я добился ускорения, сделав это таким образом вместе с закреплением потоков с помощью OMP_PROC_BIND=true, но меня беспокоит, что накопление отдельных результатов в окончательном массиве может вызвать накладные расходы, которые свели бы на нет ускорение". - это беспокойство также можно проверить эмпирически, особенно если вы работаете с проблемой достаточно большого размера, так что точность вашего таймера, как в (2), не является проблемой для изолирования времени, затраченного на шаг сбора, от части, которая полностью распараллеливается.
  4. Попробуйте выполнить другую операцию - скажем, сложение или поэлементное деление вместо вычитания и посмотрите, изменит ли это результат. На многих архитектурах разные арифметические операции имеют разную задержку и пропускную способность. Если вы посмотрели и обнаружили, что это было в случае с TILE64, внесение подобных изменений и инструментовка времени выполнения вашего второго примера может сказать вам кое-что полезное о том, сколько времени, потраченного на его серийный запуск, на самом деле связано с данными. Проблемы с локализацией по сравнению со временем запуска или другими накладными расходами, связанными со средой выполнения OpenMP, которые могут иметь большее значение для общих результатов в связи с их отношением к небольшому размеру проблемы, чем с должным образом параллельной частью параллельной реализации, фактически работающей медленнее.
  5. Вы можете изучить сгенерированную сборку. Предположение, что компилятор будет делать в основном те же вещи, что и в опубликованных вами примерах, кажется разумным, но не обязательно так сильно, как вам хотелось бы, если смотреть на нечетную производительность. Может быть, есть что-то в размере кода или макете, которые меняются с / без OpenMP или при переходе от одного параллельного подхода к другому, например, использование кеша инструкций, доступность станции резервирования или записи ROB (если в TILE64 есть эти вещи)...? Кто знает, пока не посмотрите.
Другие вопросы по тегам