Поведение OpenMP - вложенная многопоточность - PullRequest
0 голосов
/ 16 апреля 2020

Мой вопрос касается вложенного параллелизма и OpenMP. Давайте начнем со следующего фрагмента однопоточного кода:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

Теперь предположим, что мы хотим сделать наши вызовы на performAnotherTask параллельно, используя OpenMP.

Итак, мы получаем следующий код:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

Насколько я понимаю, вызовы performAnotherTask будут выполняться параллельно, и по умолчанию openMP попытается использовать все доступные потоки на вашем компьютере (возможно, это предположение неверно).

Допустим, теперь мы также хотим распараллелить вызовы к performTask так, чтобы мы получили следующий код:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

Как это будет работать? Будут ли оба цикла for многопоточными? Можем ли мы что-нибудь сказать о количестве потоков, которые будет использовать каждый l oop? Есть ли способ заставить внутренний для l oop (в пределах performTask) использовать только один поток, в то время как внешний для l oop использует все доступные потоки?

1 Ответ

3 голосов
/ 16 апреля 2020

В последнем примере поведение выполнения зависит от нескольких параметров среды.

Во-первых, OpenMP действительно поддерживает такие шаблоны, но по умолчанию отключает параллельное выполнение во вложенной параллельной области. Чтобы включить его, вы должны установить OMP_NESTED=true или вызвать omp_set_nested(1) в своем коде. Затем включается поддержка вложенного параллельного выполнения.

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel for
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

Во-вторых, когда OpenMP достигает внешней области parallel, он может захватить все доступные ядра и предположить, что он может выполнить поток на них, поэтому Возможно, вы захотите уменьшить количество потоков для внешнего уровня, чтобы некоторые ядра были доступны для вложенной области. Скажем, если у вас 32 ядра, вы можете сделать это:

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp parallel for num_threads(8)
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel for num_threads(4)
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

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

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

void performAnotherTask() {
    // DO something here
}

void performTask() {
    // Do other stuff here
#pragma omp taskloop
    for (size_t i = 0; i < 100; ++i) {
        performAnotherTask();
    }
}


int main() {
    omp_set_nested(1);
#pragma omp parallel 
#pragma omp single
#pragma omp taskloop
    for (size_t i = 0; i < 100; ++i) {
        performTask();
    }   

    return 0;
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...