Ручная синхронизация в OpenMP while loop

Недавно я начал работать с OpenMP, чтобы провести небольшое «исследование» для проекта в университете. У меня есть прямоугольная и равномерно распределенная сетка, на которой я решаю уравнение в частных производных с итерационной схемой. Таким образом, у меня в основном есть два цикла for (по одному в направлении x и y сетки каждый), обернутых циклом while для итераций.

Теперь я хочу исследовать различные схемы распараллеливания для этого. Первый (очевидный) подход заключался в том, чтобы сделать пространственное распараллеливание на циклах for. Тоже нормально работает.

Подход, с которым у меня возникли проблемы, - идея более сложная. Каждый поток вычисляет все точки сетки. Первый поток начинает решение уравнения в первой строке сетки (y = 0). По завершении поток переходит к следующему ряду (y = 1) и так далее. При этом поток №2 может начинаться уже при y = 0, потому что вся необходимая информация уже доступна. Мне просто нужно выполнить своего рода ручную синхронизацию между потоками, чтобы они не могли обогнать друг друга.

Поэтому я использовал массив с именем check. Он содержит идентификатор потока, которому в настоящее время разрешено работать с каждой строкой сетки. Когда следующая строка не «готова» (значение в check[j] неверно), поток переходит в пустой цикл while, пока он не станет готовым.

С MWE все станет яснее:

#include <stdio.h>
#include <math.h>
#include <omp.h>

int main()
{
    // initialize variables
    int iter = 0;                       // iteration step counter
    int check[100] = { 0 };             // initialize all rows for thread #0

    #pragma omp parallel num_threads(2)
    {
        int ID, num_threads, nextID;
        double u[100 * 300] = { 0 };

        // get parallelization info
        ID = omp_get_thread_num();
        num_threads = omp_get_num_threads();

        // determine next valid id
        if (ID == num_threads - 1) nextID = 0;
        else nextID = ID + 1;

        // iteration loop until abort criteria (HERE: SIMPLIFIED) are valid 
        while (iter<1000)
        {
            // rows (j=0 and j=99 are boundary conditions and don't have to be calculated)
            for (int j = 1; j < (100 - 1); j++)
            {
                // manual sychronization: wait until previous thread completed enough rows
                while (check[j + 1] != ID)
                {
                    //printf("Thread #%d is waiting!\n", ID);
                }

                // gridpoints in row j
                for (int i = 1; i < (300 - 1); i++)
                {
                    // solve PDE on gridpoint
                    // replaced by random operation to consume time
                    double ignore = pow(8.39804,10.02938) - pow(12.72036,5.00983);
                }

                // update of check array in atomic to avoid race condition
                #pragma omp atomic write
                {
                    check[j] = nextID;
                }
            }// for j

            #pragma omp atomic write
            check[100 - 1] = nextID;

            #pragma omp atomic
            iter++;

            #pragma omp single
            {
                printf("Iteration step: %d\n\n", iter);
            }
        }//while
    }// omp parallel
}//main

Дело в том, что этот MWE действительно работает на моей машине. Но если я скопирую это в свой проект, этого не произойдет. Кроме того, результат всегда разный: он останавливается либо после первой итерации, либо после третьей.

Еще одна странность: когда я удаляю косую черту в комментарии во внутреннем цикле while, он работает! Вывод содержит некоторые

"Thread #1 is waiting!"

но это разумно. Мне кажется, что я каким-то образом создал состояние гонки, но я не знаю где.

Есть ли у кого-нибудь идеи, в чем может быть проблема? Или подсказка, как реализовать такую ​​синхронизацию?


person topper91    schedule 29.12.2016    source источник
comment
Почему iter делится? Я не уверен, что так должно быть, но если так, его нужно обновить в atomic, не так ли?   -  person Gilles    schedule 29.12.2016
comment
@Gilles: он должен быть общим, поскольку каждый поток вычисляет всю итерацию и должен иметь возможность увеличивать iter. Итак, вы правы, я вставил это в atomic, но это не повлияло на результат.   -  person topper91    schedule 29.12.2016


Ответы (1)


Я думаю, вы смешиваете атомарность и согласованность памяти. Стандарт OpenMP на самом деле очень хорошо описывает его в

1.4 Модель памяти (выделено мной):

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

1.4.3 Операция промывки

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

Чтобы этого избежать, вам также следует прочитать check[] atomic и указать предложение seq_cst в ваших atomic конструкциях. Это предложение вызывает неявную очистку операции. (Это называется последовательно согласованной атомарной конструкцией)

int c;
// manual sychronization: wait until previous thread completed enough rows
do
{
    #pragma omp atomic read
    c = check[j + 1];
} while (c != ID);

Отказ от ответственности: я не могу сейчас попробовать код.

Примечания Furhter:

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

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

Похоже, есть несоответствие между вашим кодом (с использованием check[j + 1]) и вашим описанием «В то же время поток №2 уже может начинаться с y = 0»

person Zulan    schedule 29.12.2016
comment
И ты прав. Это не мои настоящие критерии остановки, есть еще один блок с операциями по вычислению остатка. и да есть это несоответствие. Мне действительно нужно, чтобы следующая строка была закончена, чтобы вычислить конечные разности. Я думал, что оба будут слишком запутанными, поскольку это не влияет на реальную проблему. - person topper91; 29.12.2016
comment
@JanDrendel Рад помочь. Удалось ли вам увидеть некоторые характеристики различных подходов к распараллеливанию? BTW: На какой архитектуре и компиляторе вы работаете? Это немного удивительно, потому что x86 имеет очень сильную согласованность памяти, и можно подумать, что OpenMP просто отображается непосредственно на модель собственной памяти, но я предполагаю, что здесь может быть задействована некоторая оптимизация компилятора. - person Zulan; 30.12.2016
comment
Извините, я не видел вашего комментария. Как вы уже догадались, итеративное распараллеливание кажется не очень эффективным: оно занимает в 5 раз больше времени, чем фактическая последовательная версия. Я использую компилятор Intel C ++ в системе x64 с процессором i3-3110m. - person topper91; 06.03.2017