Эффективное распараллеливание линейной алгебраической функции в C++ OpenMP

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

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

Ниже мой код. Обратите внимание, что этот код работает. Матрицы, с которыми я работаю, имеют размер примерно 700x700 [см. int s ниже] или 700x30 [int n].

Кроме того, я использую библиотеку броненосца для своего последовательного кода. Может случиться так, что распараллеливание с использованием openMP, но с сохранением матричных классов броненосца будет медленнее, чем стандартная библиотека по умолчанию; есть ли у кого-нибудь мнение по этому поводу (прежде чем я потрачу часы на капитальный ремонт!)?

double start, end, dif;

int i,j,k;      // iteration counters
int s,n;        // matrix dimensions

mat B; B.load(...location of stored s*n matrix...) // input objects loaded from file
mat I; I.load(...s*s matrix...);
mat R; R.load(...s*n matrix...);
mat D; D.load(...n*n matrix...);

double e = 0.1; // scalar parameter

s = B.n_rows; n = B.n_cols;

mat dBdt; dBdt.zeros(s,n); // object for storing output of function

// 100X sequential computation using Armadillo linear algebraic functionality

start = omp_get_wtime();

for (int r=0; r<100; r++) {
    dBdt = B % (R - (I * B)) + (B * D) - (B * e);
}

end = omp_get_wtime();
dif = end - strt;
cout << "Seq computation: " << dBdt(0,0) << endl;
printf("relaxation time = %f", dif);
cout << endl;

// 100 * parallel computation using OpenMP

omp_set_num_threads(8);


for (int r=0; r<100; r++) {

//          parallel computation of I * B 
#pragma omp parallel for default(none) shared(dBdt, B, I, R, D, e, s, n) private(i, j, k) schedule(static)
    for (i = 0; i < s; i++) {
        for (j = 0; j < n; j++) {
            for (k = 0; k < s; k++) {
                dBdt(i, j) += I(i, k) * B(k, j);
            }
        }
     }

//          parallel computation of B % (R - (I * B)) 
#pragma omp parallel for default(none) shared(dBdt, B, I, R, D, e, s, n) private(i, j) schedule(static)
    for (i = 0; i < s; i++) {
        for (j = 0; j < n; j++) {
            dBdt(i, j)  = R(i, j) - dBdt(i, j);
            dBdt(i, j) *= B(i, j);
            dBdt(i, j) -= B(i, j) * e;
        }
    }

//          parallel computation of B * D 
#pragma omp parallel for default(none) shared(dBdt, B, I, R, D, e, s, n) private(i, j, k) schedule(static)
   for (i = 0; i < s; i++) {
        for (j = 0; j < n; j++) {
            for (k = 0; k < n; k++) {
                dBdt(i, j) += B(i, k) * D(k, j);
            }
        }
    }    
}

end = omp_get_wtime();
dif = end - strt;
cout << "OMP computation: " << dBdt(0,0) << endl;
printf("relaxation time = %f", dif);
cout << endl;

Если я использую Hyper-Threading 4 ядра, я получаю следующий результат:

Seq computation: 5.54926e-10
relaxation time = 0.130031
OMP computation: 5.54926e-10
relaxation time = 2.611040

Это говорит о том, что хотя оба метода дают одинаковый результат, параллельная формулировка примерно в 20 раз медленнее, чем последовательная.

Возможно, что для матриц такого размера накладные расходы, связанные с этой проблемой «переменной размерности», перевешивают преимущества распараллеливания. Любые идеи будут высоко оценены.

Заранее спасибо,

Джек


person jack.l    schedule 06.09.2017    source источник
comment
Мне непонятно, что именно вы пытаетесь сделать? Итак, код броненосца работает и делает то, что должен делать? Тогда используйте его. Если вы правильно настроите броненосец, он будет максимально быстрым (кеш, simd, многопоточность; как при использовании BLAS внутренне), возможно, отсутствует переупорядочивание матриц (не уверен, что это сделано или поддерживается).   -  person sascha    schedule 06.09.2017
comment
после каждого параллельного цикла идет забор, т.е. все потоки ждут, пока последний завершит цикл. У вас есть 3 отдельных параллельных раздела, которые пахнут большой эффективной непараллельной частью времени выполнения. Так же 700х700 не очень большая матрица, может нужно увеличить размер, чтобы увидеть пользу от параллельного запуска   -  person 463035818_is_not_a_number    schedule 06.09.2017
comment
Я надеюсь увеличить скорость вычислений, запустив симуляцию на HPC с более чем 4 ядрами. Использует ли внутренняя реализация BLAS все доступные ядра автоматически?   -  person jack.l    schedule 06.09.2017
comment
В реальной симуляции, которую я запускаю, мне нужно повторить это вычисление много тысяч раз, я хотел бы увеличить масштаб, но примерно 700x700 уже занимает несколько дней, чтобы выполнить один запуск, поэтому даже если бы я выиграл от распараллеливания, это все равно было бы довольно утомительный процесс. Кстати, спасибо за ваши комментарии!   -  person jack.l    schedule 06.09.2017
comment
BLAS — это скорее стандарт со многими реализациями. Вы читали вики-ссылку? Да, конечно, он также может использовать 32 ядра (в зависимости от используемой реализации), но кто знает, какое масштабирование/ускорение будет достигнуто (зависит от размера). Но, возможно, что-то лучшее, как вы бы добились (как высокооптимизированное). Возможно, проверьте также умножение цепочки матриц (я уже упоминал об этом; обычно это хорошая идея с большими данными , не уверен, что этап настройки окупится в вашем случае)   -  person sascha    schedule 06.09.2017
comment
Не могли бы вы представить свое понимание области HPC, зачем пытаться использовать 8 потоков OMP в ситуации, когда ваше оборудование имеет 4 процессорных ядра?   -  person user3666197    schedule 07.09.2017
comment
Спасибо @sascha, я посмотрю на умножение цепочки матриц.   -  person jack.l    schedule 07.09.2017
comment
@ jack.l, при всем уважении, должно быть ясно, что в этом процессе нет никакой Matrix-цепочки. Ни BD, ни IB не являются Матричными цепочками. Лучше бы сосредоточились скорее на основных проблемах.   -  person user3666197    schedule 07.09.2017
comment
@ user3666197 Хорошо. Это на мне, так как я не внимательно изучил его код и дважды упомянул об этом.   -  person sascha    schedule 07.09.2017


Ответы (2)


Если вы используете компилятор, который исправляет ваши неверные вложения циклов и плавные циклы, чтобы улучшить локальность памяти для непараллельных сборок, openmp, скорее всего, отключит эти оптимизации. Как рекомендуют другие, вы должны рассмотреть оптимизированную библиотеку, такую ​​​​как mkl или acml. Gfortran blas по умолчанию, обычно поставляемый с дистрибутивами, не является многопоточным.

person user8566534    schedule 06.09.2017
comment
Извините, мне пришлось искать некоторые из этих терминов. Верно ли, что Armadillo BLAS поддерживает функциональность MKL? (software.intel.com/en-us /articles/) Как вы думаете, в каком случае наиболее оптимальным решением будет просто использовать встроенный функционал arma? - person jack.l; 07.09.2017

Искусство высокопроизводительных вычислений заключается в эффективности(бедные гранты никогда не получают квоту кластера высокопроизводительных вычислений)

  • так что сначала надеюсь, что ваш процесс никогда не будет перечитан из файла

Почему? Это будет HPC-убийца:

Мне нужно повторить это вычисление много тысяч раз

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

И последнее, но не менее важное: планирование [PARALLEL] не требуется, поскольку здесь вполне достаточно планирования "просто"-[CONCURRENT]-процесса. Нет необходимости организовывать какую-либо явную межпроцессную синхронизацию или какой-либо обмен сообщениями, и процесс может быть просто организован для достижения наилучшей возможной производительности.


Никакие "...быстрый взгляд на код..." не помогут

Сначала вам нужно понять как весь процесс, так и аппаратные ресурсы, на которых он будет выполняться.

Тип процессора сообщит вам о доступных расширениях набора инструкций для продвинутых трюков, размеры кэша L3- / L2- / L1 + размеры строки кэша помогут вам решить, какое повторное использование наиболее удобного для кэширования дешевого доступа к данным ( не < strong>платить сотни [ns], если вместо этого можно работать эффективнее, используя всего лишь несколько [ns], используя еще не удаленную копию NUMA-core-local )


Сначала математика, потом реализация:

Как указано dBdt = B % ( R - (I * B) ) + ( B * D ) - ( B * e )

При ближайшем рассмотрении любой должен быть готов осознать приоритеты HPC/выравнивания кэша и ловушки неправильного зацикливания:

dBdt = B % ( R - ( I * B ) )   ELEMENT-WISE OP B[s,n]-COLUMN-WISE
     +               ( B * D ) SUM.PRODUCT  OP B[s,n].ROW-WISE MUL-BY-D[n,n].COL
     -               ( B * e ) ELEMENT-WISE OP B[s,n].ROW-WISE MUL-BY-SCALAR

 ROW/COL-SUM.PRODUCT OP -----------------------------------------+++++++++++++++++++++++++++++++++++++++++++++
 ELEMENT-WISE OP ---------------------------------------------+  |||||||||||||||||||||||||||||||||||||||||||||
 ELEMENT-WISE OP ----------------------+                      |  |||||||||||||||||||||||||||||||||||||||||||||
                                       |                      |  |||||||||||||||||||||||||||||||||||||||||||||
                                       v                      v  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
 dBdt[s,n]        =   B[s,n]           % /   R[s,n]           - /  I[s,s]                  . B[s,n]           \ \
     _________[n]         _________[n]   |       _________[n]   |      ________________[s]       _________[n]  | |
    |_|       |          |_|       |     |      |_|       |     |     |________________|        | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
    | .       |   =      | .       |   % |      | .       |   - |     |                |   .    | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
    | .       |          | .       |     |      | .       |     |     |                |        | |       |    | |
 [s]|_________|       [s]|_________|     |   [s]|_________|     |  [s]|________________|     [s]|_|_______|    | |
                                         \                      \                                             / /

                      B[s,n]              D[n,n]          
                          _________[n]        _________[n]
                         |_________|         | |       |   
                         | .       |         | |       |  
                         | .       |         | |       |  
                         | .       |         | |       |  
                  +      | .       |   .  [n]|_|_______|   
                         | .       |      
                         | .       |      
                         | .       |             
                      [s]|_________|      


                      B[s,n]                
                          _________[n]      
                         |_| . . . |        
                         | .       |        
                         | .       |        
                         | .       |        
                  -      | .       |    *  REGISTER_e
                         | .       |        
                         | .       |        
                         | .       |        
                      [s]|_________|        

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

В зависимости от реального кэша ЦП
цикл может очень эффективно совместно обрабатывать B-выровненные по строкам ( B * D ) - ( B * e )
в одной фазе, а также часть самого длинного поэлементного конвейера, основанная на максимальной эффективности повторного использования, B % ( R - ( I * B ) ) здесь имеет возможность повторно использовать ~ 1000 x ( n - 1 ) кэш-попаданий B. strong>-column-aligned, что вполне должно вписаться в размеры L1-DATA-кэша, поэтому экономия порядка секунд достигается только за счет циклов, выровненных по кешу.


После завершения этого удобного для кэширования цикла выравнивания,

дальше может помочь распределенная обработка, не раньше.

Итак, план эксперимента:

Шаг 0: Истина: ~ 0.13 [s] для dBdt[700,30] с использованием броненосца в 100-тестовых циклах

Шаг 1. Ручной серийный: - проверьте вознаграждение за лучший код, выровненный по кешу (не опубликованный, а математический эквивалент, оптимизированный для повторного использования строки кеша - где есть должно быть не более 4x for(){...} кодовых блоков, вложенных по 2, с оставшимися 2 внутри, чтобы соответствовать правилам линейной алгебры без разрушительных преимуществ выравнивания строк кэша ( с некоторым остаточным потенциалом, чтобы получить еще немного пользы в [PTIME] от использования дублированного макета данных [PSPACE] (как FORTRAN-порядка, так и C-порядка для соответствующих пере- стратегии чтения), так как матрицы настолько миниатюрны по размерам, а L2-/L1-DATA-кэш, доступный для каждого ядра ЦП, наслаждаются размерами кеша, хорошо выросшими в масштабе)

Шаг 2: руководство-omp( <= NUMA_cores - 1 ): - проверьте, действительно ли omp может дать какой-либо "положительный" < strong>Закон Амдала ускорение (помимо накладных расходов на установку omp). Тщательное процесс-2-CPU_core affinity-mapping может помочь избежать любого возможного вытеснения из кэша, вызванного любым потоком, не относящимся к высокопроизводительным вычислениям, который портит дружественную к кэшу компоновку в наборе конфигураций-"зарезервированных"-наборов ( NUMA_cores - 1 ), где все остальные (процессы, не относящиеся к высокопроизводительным вычислениям) должны быть привязаны к последнему (общему) ядру ЦП, что помогает предотвратить сохранение этих ядер процессов высокопроизводительных вычислений их строк кэша, не вытесняемых каким-либо ядром/планировщиком. вводится не-HPC-поток.

(Как видно из (2), существуют механизмы, основанные на лучших практиках HPC, которые ни один компилятор (даже оснащенный волшебной палочкой) никогда не сможет реализовать, поэтому не стесняйтесь спрашивать своего наставника PhD за руку помощи, если ваша диссертация нуждается в некотором опыте в области высокопроизводительных вычислений, так как не так просто построить метод проб и ошибок в этой довольно дорогой экспериментальной области, и ваша основная область не является линейной алгеброй и/или конечной CS-теоретикой / HW оптимизация стратегии кэширования. )


Эпилог:

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

person user3666197    schedule 07.09.2017