OpenMP: В чем преимущество распараллеливания вложенности?

Из того, что я понимаю, #pragma omp parallel и его вариации в основном выполняют следующий блок в ряде параллельных потоков, что соответствует числу ЦП. Когда вложенные распараллеливания - параллельные для параллельных, параллельная функция внутри параллельной функции и т.д. - что происходит при внутренней распараллеливании?

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

Ответ 1

(1) Вложенный parallelism в OpenMP: http://docs.oracle.com/cd/E19205-01/819-5270/aewbc/index.html

Вам нужно включить вложенные parallelism, установив OMP_NESTED или omp_set_nested, потому что многие реализации отключили эту функцию по умолчанию, даже некоторые реализации полностью не поддерживали вложенный parallelism. Если включено, всякий раз, когда вы встречаете parallel for, OpenMP создаст количество потоков, как определено в OMP_NUM_THREADS. Итак, если 2-уровневое parallelism, общее число потоков будет N ^ 2, где N = OMP_NUM_THREADS.

Такой вложенный parallelism приведет к переподписке (т.е. количество занятых потоков больше, чем ядра), что может ухудшить ускорение. В крайнем случае, когда вложенный parallelism называется рекурсивно, потоки могут раздуваться (например, создавать потоки 1000s), а компьютер просто тратит время на переключение контекста. В этом случае вы можете управлять количеством потоков динамически, установив omp_set_dynamic.

(2) Пример умножения матрицы-вектора: код будет выглядеть так:

// Input:  A(N by M), B(M by 1)
// Output: C(N by 1)
for (int i = 0; i < N; ++i)
  for (int j = 0; j < M; ++j)
     C[i] += A[i][j] * B[j];

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

Ваша забота - это случай, когда N < # процессора. Да, правильно, в этом случае ускорение будет ограничено N, а разрешение вложенных parallelism будет иметь определенные преимущества.

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

  • Изменение структуры цикла так, чтобы существовал только 1-уровневый цикл. (Это выглядит выполнимо)
  • Специализирующий код: если N мало, тогда вложенный parallelism, иначе не делайте этого.
  • Вложенный parallelism с omp_set_dynamic. Но, пожалуйста, убедитесь, что omp_set_dynamic контролирует количество потоков и активность потоков. Реализации могут различаться.

Ответ 2

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

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

Ответ 3

На внешнем уровне используйте предложение NUM_THREADS (num_groups), чтобы установить количество потоков для использования. Если ваш внешний цикл имеет счет N, а число процессоров или ядер - num_cores, используйте num_groups = min (N, num_cores). На внутреннем уровне вам необходимо установить количество подпотоков для каждой группы потоков, чтобы общее количество субтитров равнялось числу ядер. Итак, если num_cores = 8, N = 4, то num_groups = 4. На нижнем уровне каждый подпоток должен использовать 2 потока (начиная с 2 + 2 + 2 + 2 = 8), поэтому используйте предложение NUM_THREADS (2). Вы можете собрать количество подпотоков в массив с одним элементом на поток внешнего региона (с элементами num_groups).

Эта стратегия всегда оптимально использует ваши ядра. Когда N < num_cores происходит некоторое вложенное распараллеливание. Когда N >= num_cores, массив подсчетов subthread содержит все 1s, и поэтому внутренний цикл является эффективным серийным.