A размера mxn и матрицы B размера nxl приводит к получению матрицы С размера mxl, каждый элемент
которой определяется в соответствии с выражением:$$c_{ij}=\sum_{k=0}^{n-1} a_{ik}\cdot b_{kj},\;
0 \le i < m, \; 0 \le j < l$$
Как следует из (7.1), каждый элемент результирующей матрицы С есть скалярное произведение соответствующих строки
матрицы A и столбца матрицы B:$$c_{ij}=(a_i,b_j^T), \; a_i = (a_{i0}, a_{i1} \ldots, a_{in-1}),
\; b_j^T = (b_{0j},b_{1j},\ldots,b_{n-1j})^T .$$
Этот алгоритм предполагает выполнение mxnxl операций умножения и столько же
операций сложения элементов исходных матриц. При умножении
квадратных матриц размера nxn количество
выполненных операций имеет порядок O(n3).
Известны последовательные алгоритмы nxn.
Последовательный алгоритм
Алгоритм 7.1. Последовательный алгоритм умножения двух квадратных матриц
// Алгоритм 7.1
// Последовательный алгоритм умножения матриц
double MatrixA[Size][Size];
double MatrixB[Size][Size];
double MatrixC[Size][Size];
int i,j,k;
...
for (i=0; i<Size; i++){
for (j=0; j<Size; j++){
MatrixC[i][j] = 0;
for (k=0; k<Size; k++){
MatrixC[i][j] = MatrixC[i][j] + MatrixA[i][k]*MatrixB[k][j];
}
}
}
Этот алгоритм является итеративным и ориентирован на
последовательное вычисление строк матрицы С.
Действительно, при выполнении одной итерации внешнего цикла
(цикла по переменной i ) вычисляется одна строка
результирующей матрицы (см. рис.
7.1).
(рис 7.1) На первой итерации цикла по переменной i используется первая строка матрицы A и все столбцы матрицы B для того, чтобы вычислить элементы первой строки результирующей матрицы СПоскольку каждый элемент результирующей матрицы есть скалярное
произведение строки и столбца исходных матриц, то для вычисления
всех элементов матрицы С размером nxn
необходимо выполнить n2x(2n–1)
скалярных операций и затратить время$$T_1 = n^2 \cdot (2n-1) \cdot \tau ,$$
где $$\tau$$ есть время выполнения одной элементарной скалярной
операции.
Рассмотрим два параллельных алгоритма A и B разбиваются на непрерывные
последовательности строк или столбцов (полосы).
Из определения операции матричного умножения следует, что
вычисление всех элементов матрицы С может быть
выполнено независимо друг от друга. Как результат, возможный
подход для организации параллельных вычислений состоит в
использовании в качестве базовой подзадачи процедуры определения
одного элемента результирующей матрицы С. Для
проведения всех необходимых вычислений каждая подзадача должна
содержать по одной строке матрицы А и одному столбцу
матрицы В. Общее количество получаемых при таком
подходе подзадач оказывается равным n2
(по числу элементов матрицы С ).
Рассмотрев предложенный подход, можно отметить, что
достигнутый уровень параллелизма является в большинстве случаев
избыточным. Обычно при проведении практических расчетов такое
количество сформированных подзадач превышает число имеющихся
процессоров и делает неизбежным этап укрупнения базовых задач. В
этом плане может оказаться полезной агрегация вычислений уже на
шаге выделения базовых подзадач. Возможное решение может состоять
в объединении в рамках одной подзадачи всех вычислений, связанных
не с одним, а с несколькими элементами результирующей матрицы С. Для дальнейшего рассмотрения определим базовую
задачу как процедуру вычисления всех элементов одной из строк
матрицы С. Такой подход приводит к снижению общего
количества подзадач до величины n.
Для выполнения всех необходимых вычислений базовой подзадаче
должны быть доступны одна из строк матрицы A и все
столбцы матрицы B. Простое решение этой проблемы –
дублирование матрицы B во всех подзадачах –
является, как правило, неприемлемым в силу больших затрат памяти
для хранения данных. Поэтому организация вычислений должна быть
построена таким образом, чтобы в каждый текущий момент времени
подзадачи содержали лишь часть данных, необходимых для проведения
расчетов, а доступ к остальной части данных обеспечивался бы при
помощи передачи данных между процессорами. Два возможных способа
выполнения параллельных вычислений подобного типа рассмотрены
далее в п. 7.3.2.
Для вычисления одной строки матрицы С необходимо,
чтобы в каждой подзадаче содержалась строка матрицы А и был обеспечен доступ ко всем столбцам матрицы B. Возможные способы организации параллельных
вычислений состоят в следующем.
1. Первый алгоритм. Алгоритм представляет собой
итерационную процедуру, количество итераций которой совпадает с
числом подзадач. На каждой итерации алгоритма каждая подзадача
содержит по одной строке матрицы А и одному столбцу матрицы В.
При выполнении итерации проводится С. По
завершении вычислений в конце каждой итерации столбцы матрицы В
должны быть переданы между подзадачами с тем, чтобы в каждой
подзадаче оказались новые столбцы матрицы В и могли быть
вычислены новые элементы матрицы C. При этом данная передача
столбцов между подзадачами должна быть организована таким
образом, чтобы после завершения итераций алгоритма в каждой
подзадаче последовательно оказались все столбцы матрицы В.
Возможная простая схема организации необходимой
последовательности передач столбцов матрицы В между подзадачами
состоит в представлении топологии информационных связей подзадач
в виде кольцевой структуры. В этом случае на каждой итерации
подзадача i, 0<=i<n, будет передавать свой столбец матрицы В
подзадаче с номером i+1 (в соответствии с кольцевой структурой
подзадача n-1 передает свои данные подзадаче с номером 0 ) – см.
рис. 7.2. После выполнения всех итераций алгоритма необходимое
условие будет обеспечено – в каждой подзадаче поочередно окажутся
все столбцы матрицы В.
На рис. 7.2 представлены итерации алгоритма матричного
умножения для случая, когда матрицы состоят из четырех строк и
четырех столбцов ( n=4 ). В начале вычислений в каждой подзадаче i,
0<=i<n, располагаются i -я строка матрицы A и i -й столбец матрицы B. В результате их перемножения подзадача получает элемент cii
результирующей матрицы С. Далее подзадачи осуществляют обмен
столбцами, в ходе которого каждая подзадача передает свой столбец
матрицы B следующей подзадаче в соответствии с кольцевой
структурой информационных взаимодействий. Далее выполнение
описанных действий повторяется до завершения всех итераций
параллельного алгоритма.
(рис 7.2) Общая схема передачи данных для первого параллельного алгоритма матричного умножения при ленточной схеме разделения данных2. Второй алгоритм. Отличие второго алгоритма состоит в том,
что в подзадачах располагаются не столбцы, а строки матрицы B.
Как результат, перемножение данных каждой подзадачи сводится не к
C.
При рассмотренном способе разделения данных для выполнения
операции матричного умножения нужно обеспечить последовательное
получение в подзадачах всех строк матрицы B, поэлементное
умножение данных и суммирование вновь получаемых значений с ранее
вычисленными результатами. Организация необходимой
последовательности передач строк матрицы B между подзадачами
также может быть выполнена с использованием кольцевой структуры
информационных связей (см. рис. 7.3).
На рис. 7.3 представлены
итерации алгоритма матричного умножения для случая, когда матрицы
состоят из четырех строк и четырех столбцов ( n=4 ). В начале
вычислений в каждой подзадаче i, 0<=i<n, располагаются i -е строки
матрицы A и матрицы B. В результате их перемножения подзадача
определяет i -ю строку частичных результатов искомой матрицы C.
Далее подзадачи осуществляют обмен строками, в ходе которого
каждая подзадача передает свою строку матрицы B следующей
подзадаче в соответствии с кольцевой структурой информационных
взаимодействий. Далее выполнение описанных действий повторяется
до завершения всех итераций параллельного алгоритма.
(рис 7.3) Общая схема передачи данных для второго параллельного алгорится матричного умножения при ленточной схеме разделения данных
Выделенные базовые подзадачи характеризуются одинаковой
вычислительной трудоемкостью и равным объемом передаваемых
данных. Когда размер матриц n оказывается больше, чем число
процессоров p, базовые подзадачи можно укрупнить, объединив в
рамках одной подзадачи несколько соседних строк и столбцов
перемножаемых матриц. В этом случае исходная матрица A
разбивается на ряд горизонтальных полос, а матрица B
представляется в виде набора вертикальных (для первого алгоритма)
или горизонтальных (для второго алгоритма) полос. Размер полос
при этом следует выбрать равным k=n/p (в предположении, что n
кратно p ), что позволит по-прежнему обеспечить равномерность
распределения вычислительной нагрузки по процессорам,
составляющим многопроцессорную вычислительную систему.
Для распределения подзадач между процессорами может быть использован любой способ, обеспечивающий эффективное представление кольцевой структуры информационного взаимодействия подзадач. Для этого достаточно, например, чтобы подзадачи, являющиеся соседними в кольцевой топологии, располагались на процессорах, между которыми имеются прямые линии передачи данных.
Выполним анализ эффективности первого параллельного алгоритма
Общая трудоемкость последовательного алгоритма, как уже
отмечалось ранее, является пропорциональной n3. Для параллельного
алгоритма на каждой итерации каждый процессор выполняет умножение
имеющихся на процессоре полос матрицы А и матрицы В (размер полос
равен n/p, и, как результат, общее количество выполняемых при
этом умножении операций равно n3/p2 ). Поскольку число итераций
алгоритма совпадает с количеством процессоров, сложность
параллельного алгоритма без учета затрат на передачу данных может
быть определена при помощи выражения$$T_p = (n^3/p^2)\cdot p = n^3/p .$$
С учетом этой оценки показатели ускорения и эффективности данного параллельного алгоритма матричного умножения принимают вид:$$S_p = \frac{n^3}{n^3/p} = p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$
Таким образом, общий анализ сложности дает идеальные
показатели эффективности параллельных вычислений. Для уточнения
полученных соотношений оценим более точно количество
вычислительных операций алгоритма и учтем затраты на выполнение
С учетом числа и длительности выполняемых операций время выполнения вычислений параллельного алгоритма может быть оценено следующим образом:$$T_p(calc)=(n^2/p)\cdot(2n-1)\cdot\tau$$ (здесь, как и ранее, $$\tau$$ есть время выполнения одной элементарной скалярной операции).
Для оценки коммуникационной сложности параллельных вычислений
будем предполагать, что все n/p строк или столбцов
длины n. Общее количество параллельных операций передачи
сообщений на единицу меньше числа итераций алгоритма (на
последней итерации передача данных не является обязательной). Тем
самым, оценка трудоемкости выполняемых w есть размер элемента матрицы в байтах.
С учетом полученных соотношений общее время выполнения параллельного алгоритма матричного умножения определяется следующим выражением:$$T_p=(n^2/p)(2n-1)\cdot\tau+(p-1)\cdot(\alpha + w\cdot n\cdot(n/p)/\beta).$$
Эксперименты проводились на вычислительном кластере на базе
процессоров Intel Xeon 4
Для оценки длительности $$\tau$$ базовой скалярной операции
проводилось решение задачи b соответственно 130 мкс и 53,29
Мбайт/с. Все вычисления производились над числовыми значениями
типа double, т.е. величина w равна 8 байт.
Результаты
| Размер матрицы | Последовательный алгоритм | Параллельный алгоритм | |||||
|---|---|---|---|---|---|---|---|
| 2 процессора | 4 процессора | 8 процессоров | |||||
| Время | Ускорение | Время | Ускорение | Время | Ускорение | ||
| 500 | 0,8752 | 0,3758 | 2,3287 | 0,1535 | 5,6982 | 0,0968 | 9,0371 |
| 1000 | 12,8787 | 5,4427 | 2,3662 | 2,2628 | 5,6912 | 0,6998 | 18,4014 |
| 1500 | 43,4731 | 20,9503 | 2,0750 | 11,0804 | 3,9234 | 5,1766 | 8,3978 |
| 2000 | 103,0561 | 45,7436 | 2,2529 | 21,6001 | 4,7710 | 9,4127 | 10,9485 |
| 2500 | 201,2915 | 99,5097 | 2,0228 | 56,9203 | 3,5363 | 18,3303 | 10,9813 |
| 3000 | 347,8434 | 171,9232 | 2,0232 | 111,9642 | 3,1067 | 45,5482 | 7,6368 |
(рис 7.4) Зависимость ускорения от количества процессоров при выполнении первого параллельного алгоритма матричного умножения при ленточной схеме распределения данныхСравнение экспериментального времени $$T^*_p$$ выполнения
эксперимента и теоретического времени Tp из формулы (7.8)
представлено в таблице 7.2 и на рис. 7.5.
(рис 7.5) График зависимости от объема исходных данных теоретического и экспериментального времени выполнения параллельного алгоритма на двух процессорах (ленточная схема разбиения данных)| Размер матрицы | 2 процессора | 4 процессора | 8 процессоров | |||
|---|---|---|---|---|---|---|
| $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | |
| 500 | 0,8243 | 0,3758 | 0,4313 | 0,1535 | 0,2353 | 0,0968 |
| 1000 | 6,51822 | 5,4427 | 3,3349 | 2,2628 | 1,7436 | 0,6998 |
| 1500 | 21,9137 | 20,9503 | 11,1270 | 11,0804 | 5,7340 | 5,1766 |
| 2000 | 51,8429 | 45,7436 | 26,2236 | 21,6001 | 13,4144 | 9,4127 |
| 2500 | 101,1377 | 99,5097 | 51,0408 | 56,9203 | 25,9928 | 18,3303 |
| 3000 | 174,6301 | 171,9232 | 87,9946 | 111,9642 | 44,6772 | 45,5482 |
При построении параллельных способов выполнения матричного умножения наряду с рассмотрением матриц в виде наборов строк и столбцов широко используется блочное представление матриц. Рассмотрим более подробно данный способ организации вычислений.
Блочная схема разбиения матриц подробно изложена в первом
разделе лекции 6. При таком способе разделения данных исходные
матрицы А, В и результирующая матрица С представляются в виде
наборов блоков. Для более простого изложения следующего материала
будем предполагать далее, что все матрицы являются квадратными
размера nxn, количество блоков по горизонтали и вертикали
одинаково и равно q (т.е. размер всех блоков равен kxk, k=n/q ).
При таком представлении данных операция матричного А и B в блочном виде может быть представлена так:$$\begin{pmatrix}
A_{00}A_{01}\ldots A_{0q-1} \\
\ldots \\
A_{q-10}A_{q-11}\ldots A_{q-1q-1}
\end{pmatrix}
\times
\begin{pmatrix}
B_{00}B_{01}\ldots B_{0q-1} \\
\ldots \\
B_{q-10}B_{q-11}\ldots B_{q-1q-1}
\end{pmatrix}
=
\begin{pmatrix}
C_{00}C_{01}\ldots C_{0q-1} \\
\ldots \\
C_{q-10}C_{q-11}\ldots C_{q-1q-1}
\end{pmatrix} ,$$
где каждый блок Cij матрицы C определяется в соответствии с выражением$$C_{ij}=\sum_{s=0}^{q-1} A_{is} B_{sj}.$$
При блочном разбиении данных для определения базовых подзадач
естественным представляется взять за основу вычисления,
выполняемые над матричными блоками. С учетом сказанного определим
базовую подзадачу как процедуру вычисления всех элементов одного
из блоков матрицы С.
Для выполнения всех необходимых вычислений базовым подзадачам
должны быть доступны соответствующие наборы строк матрицы A и
столбцов матрицы B. Размещение всех требуемых данных в каждой
подзадаче неизбежно приведет к дублированию и к значительному
росту объема используемой памяти. Как результат, вычисления
должны быть организованы таким образом, чтобы в каждый текущий
момент времени подзадачи содержали лишь часть необходимых для
проведения расчетов данных, а доступ к остальной части данных
обеспечивался бы при помощи передачи данных между процессорами.
Один из возможных подходов – алгоритм Фокса ( Fox ) – рассмотрен
далее в данном подразделе. Второй способ – алгоритм Кэннона
( Cannon ) – приводится в подразделе 7.5.
Итак, за основу параллельных вычислений для матричного
умножения при блочном разделении данных принят подход, при
котором базовые подзадачи отвечают за вычисления отдельных блоков
матрицы C и при этом в подзадачах на каждой итерации расчетов
располагается только по одному блоку исходных матриц A и B. Для
нумерации подзадач будем использовать индексы размещаемых в
подзадачах блоков матрицы C, т.е. подзадача (i,j) отвечает за
вычисление блока Cij – тем самым, набор подзадач образует
квадратную решетку, соответствующую структуре блочного
представления матрицы C.
Возможный способ организации вычислений при таких условиях состоит в применении широко известного алгоритма Фокса ( Fox ) — см., например, [, ].
В соответствии с алгоритмом Фокса в ходе вычислений на каждой
базовой подзадаче (i,j) располагается четыре матричных блока:
Cij матрицы C, вычисляемый подзадачей;Aij матрицы A, размещаемый в подзадаче перед началом вычислений;A'ij, B'ij матриц A и B, получаемые подзадачей в ходе выполнения вычислений.Выполнение параллельного метода включает:
(i,j)
передаются блоки Aij, Bij и обнуляются блоки Cij на всех
подзадачах;l, 0<=l<q, осуществляются следующие операции:i, 0<=i<q, блок Aij подзадачи (i,j)
пересылается на все подзадачи той же строки i решетки; индекс j,
определяющий положение подзадачи в строке, вычисляется в
соответствии с выражением$$j = ( i+l ) \bmod q,$$
где mod есть операция получения остатка от целочисленного деления;A'ij, B'ij каждой
подзадачи (i,j) перемножаются и прибавляются к блоку CijB'ij каждой подзадачи (i,j) пересылаются подзадачам,
являющимся соседями сверху в столбцах решетки подзадач (блоки
подзадач из первой строки решетки пересылаются подзадачам
последней строки решетки).
(рис 7.6) Состояние блоков в каждой подзадаче в ходе выполнения итераций алгоритма ФоксаДля пояснения этих правил параллельного метода на рис. 7.6
приведено состояние блоков в каждой подзадаче в ходе выполнения
итераций этапа вычислений (для решетки подзадач 2x2 ).
В рассмотренной схеме параллельных вычислений количество
блоков может варьироваться в зависимости от выбора размера блоков
– эти размеры могут быть подобраны таким образом, чтобы общее
количество базовых подзадач совпадало с числом процессоров p.
Так, например, в наиболее простом случае, когда число процессоров
представимо в виде $$p=\delta 2$$ (т.е. является полным квадратом), можно
выбрать количество блоков в матрицах по вертикали и горизонтали
равным $$\delta$$ (т.е. $$q=\delta$$ ). Такой способ определения количества блоков
приводит к тому, что объем вычислений в каждой подзадаче является
одинаковым и тем самым достигается полная балансировка
вычислительной нагрузки между процессорами. В более общем случае
при произвольных количестве процессоров и размерах матриц
Для эффективного выполнения алгоритма Фокса, в котором базовые
подзадачи представлены в виде квадратной решетки и в ходе
вычислений выполняются операции передачи блоков по строкам и
столбцам решетки подзадач, наиболее адекватным решением является
организация множества имеющихся процессоров также в виде
квадратной решетки. В этом случае можно осуществить
непосредственное отображение набора подзадач на множество
процессоров – базовую подзадачу (i,j) следует располагать на
процессоре Pi,j. Необходимая структура сети передачи данных может
быть обеспечена на физическом уровне, если топология
вычислительной системы имеет вид решетки или полного графа.
Определим вычислительную сложность данного алгоритма Фокса.
Построение оценок будет происходить при условии выполнения всех
ранее выдвинутых предположений: все матрицы являются квадратными
размера nxn, количество блоков по горизонтали и вертикали
являются одинаковым и равным q (т.е. размер всех блоков равен kxk, k=n/q ), процессоры образуют квадратную решетку и их
количество равно p=q2.
Как уже отмечалось, алгоритм Фокса требует для своего
выполнения q итераций, в ходе которых каждый процессор
перемножает свои текущие блоки матриц А и В и прибавляет
результаты умножения к текущему значению блока матрицы C. С
учетом выдвинутых предположений общее количество выполняемых при
этом операций будет иметь порядок n3/p. Как результат, показатели
ускорения и эффективности алгоритма имеют вид:$$S_p = \frac{n^3}{n^3/p} = p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$
Общий анализ сложности снова дает идеальные показатели
эффективности параллельных вычислений. Уточним полученные
соотношения — для этого укажем более точно количество
вычислительных операций алгоритма и учтем затраты на выполнение
Определим количество вычислительных операций. Сложность
выполнения A на столбец
блока матрицы В можно оценить как 2(n/q)-1. Количество строк и
столбцов в блоках равно n/q и, как результат, трудоемкость
операции блочного умножения оказывается равной (n2/p)(2n/q-1).
Для сложения блоков требуется n2/p операций. С учетом всех
перечисленных выражений время выполнения вычислительных операций
алгоритма Фокса может быть оценено следующим образом:$$T_p(calc)=q[(n^2 /p)\cdot(2n/q-1)+(n^2 /p)]\cdot\tau.$$
(напомним, что $$\tau$$ есть время выполнения одной элементарной скалярной операции).
Оценим затраты на выполнение A остальным процессорам своей строки. Как уже
отмечалось ранее, при топологии сети в виде гиперкуба или полного
графа выполнение этой операции может быть обеспечено за log2q
шагов, а объем передаваемых блоков равен n2/p. Как результат,
время выполнения операции передачи блоков матрицы A при
использовании модели Хокни может оцениваться как$$T_p^1(comm)=\log_2 q(\alpha +w(n^2/p)/ \beta),$$
где $$\alpha$$ – латентность, $$\beta$$ – пропускная способность сети
передачи данных, а w есть размер элемента матрицы в байтах. В
случае же когда топология строк процессорной решетки представляет
собой кольцо, выражение для оценки времени передачи блоков
матрицы A принимает вид:$$\widetilde{T}_p^1(comm)=(q/2)(\alpha+w(n^2/p)/ \beta).$$
Далее после умножения матричных блоков процессоры передают
свои блоки матрицы В предыдущим процессорам по столбцам
процессорной решетки (первые процессоры столбцов передают свои
данные последним процессорам в столбцах решетки). Эти операции
могут быть выполнены процессорами параллельно, и, тем самым,
длительность такой коммуникационной операции составляет:$$T-p^2(comm)=\alpha+w \cdot (n^2/p)/ \beta.$$
Просуммировав все полученные выражения, можно получить, что
общее время выполнения алгоритма Фокса может быть определено при
помощи следующих соотношений:$$\begin{aligned}
T_p = q[(n^2/p) \cdot (2n/q-1)+(n^2/p)] \cdot \tau+q \log_2 q(\alpha + w(n^2/p)/ \beta) + \\
+(q-1) \cdot (\alpha + w(n^2/p)/ \beta) = q[(n^2/p) \cdot (2n/q-1)+(n^2/p)] \cdot \tau+ \\
+(q\log_2 q +(q-1))( \alpha + w(n^2/p)/ \beta)
\end{aligned}$$
(напомним, что параметр q определяет размер процессорной решетки
и $$q=\sqrt{p}$$ ).
Представим возможный вариант
1. Главная функция программы. Определяет основную логику работы алгоритма, последовательно вызывает необходимые подпрограммы.
// Программа 7.1
// Алгоритм Фокса умножения матриц – блочное представление данных
// Условия выполнения программы: все матрицы квадратные,
// размер блоков и их количество по горизонтали и вертикали
// одинаково, процессы образуют квадратную решетку
int ProcNum = 0; // Количество доступных процессов
int ProcRank = 0; // Ранг текущего процесса
int GridSize; // Размер виртуальной решетки процессов
int GridCoords[2]; // Координаты текущего процесса в процессной
// решетке
MPI_Comm GridComm; // Коммуникатор в виде квадратной решетки
MPI_Comm ColComm; // коммуникатор – столбец решетки
MPI_Comm RowComm; // коммуникатор – строка решетки
void main ( int argc, char * argv[] ) {
double* pAMatrix; // Первый аргумент матричного умножения
double* pBMatrix; // Второй аргумент матричного умножения
double* pCMatrix; // Результирующая матрица
int Size; // Размер матриц
int BlockSize; // Размер матричных блоков, расположенных
// на процессах
double *pAblock; // Блок матрицы А на процессе
double *pBblock; // Блок матрицы В на процессе
double *pCblock; // Блок результирующей матрицы С на процессе
double *pMatrixAblock;
double Start, Finish, Duration;
setvbuf(stdout, 0, _IONBF, 0);
MPI_Init(argc, argv);
MPI_Comm_size(MPI_COMM_WORLD, ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
GridSize = sqrt((double)ProcNum);
if (ProcNum != GridSize*GridSize) {
if (ProcRank == 0) {
printf ("Number of processes must be a perfect square \n");
}
}
else {
if (ProcRank == 0)
printf("Parallel matrix multiplication program\n");
// Создание виртуальной решетки процессов и коммуникаторов
// строк и столбцов
CreateGridCommunicators();
// Выделение памяти и инициализация элементов матриц
ProcessInitialization ( pAMatrix, pBMatrix, pCMatrix, pAblock,
pBblock, pCblock, pMatrixAblock, Size, BlockSize );
// Блочное распределение матриц между процессами
DataDistribution(pAMatrix, pBMatrix, pMatrixAblock, pBblock, Size,
BlockSize);
// Выполнение параллельного метода Фокса
ParallelResultCalculation(pAblock, pMatrixAblock, pBblock,
pCblock, BlockSize);
// Сбор результирующей матрицы на ведущем процессе
ResultCollection(pCMatrix, pCblock, Size, BlockSize);
// Завершение процесса вычислений
ProcessTermination (pAMatrix, pBMatrix, pCMatrix, pAblock, pBblock,
pCblock, pMatrixAblock);
}
MPI_Finalize();
}
2. Функция CreateGridCommunicators. Данная функция создает
коммуникатор в виде двумерной квадратной решетки, определяет
координаты каждого процесса в этой решетке, а также создает
коммуникаторы отдельно для каждой строки и каждого столбца.
Создание решетки производится при помощи функции MPI_Cart_create (вектор Periodic определяет возможность передачи
сообщений между граничными процессами строк и столбцов
создаваемой решетки). После создания решетки каждый процесс
параллельной программы будет иметь координаты своего положения в
решетке; получение этих координат обеспечивается при помощи
функции MPI_Cart_coords.
Формирование топологий завершается созданием множества
коммуникаторов для каждой строки и каждого столбца решетки в
отдельности (функция MPI_Cart_sub ).
// Создание коммуникатора в виде двумерной квадратной решетки
// и коммуникаторов для каждой строки и каждого столбца решетки
void CreateGridCommunicators() {
int DimSize[2]; // Количество процессов в каждом измерении
// решетки
int Periodic[2]; // =1 для каждого измерения, являющегося
// периодическим
int Subdims[2]; // =1 для каждого измерения, оставляемого
// в подрешетке
DimSize[0] = GridSize;
DimSize[1] = GridSize;
Periodic[0] = 0;
Periodic[1] = 0;
// Создание коммуникатора в виде квадратной решетки
MPI_Cart_create(MPI_COMM_WORLD, 2, DimSize, Periodic, 1, GridComm);
// Определение координат процесса в решетке
MPI_Cart_coords(GridComm, ProcRank, 2, GridCoords);
// Создание коммуникаторов для строк процессной решетки
Subdims[0] = 0; // Фиксация измерения
Subdims[1] = 1; // Наличие данного измерения в подрешетке
MPI_Cart_sub(GridComm, Subdims, RowComm);
// Создание коммуникаторов для столбцов процессной решетки
Subdims[0] = 1;
Subdims[1] = 0;
MPI_Cart_sub(GridComm, Subdims, ColComm);
}
3. Функция ProcessInitialization. Данная функция определяет
параметры решаемой задачи (размеры матриц и их блоков), выделяет
память для хранения данных и осуществляет ввод исходных матриц
(или формирует их при помощи какого-либо датчика случайных
чисел). Всего в каждом процессе должна быть выделена память для
хранения четырех блоков – для указателей на выделенную память
используются переменные pAblock, pBblock, pCblock, pMatrixAblock.
Первые три указателя определяют блоки матриц A, B и C
соответственно. Следует отметить, что содержимое блоков pAblock и pBblock постоянно меняется в соответствии с пересылкой данных
между процессами, в то время как блок pMatrixAblock матрицы A
остается неизменным и применяется при рассылках блоков по строкам
решетки процессов (см. функцию AblockCommunication ).
Для определения элементов исходных матриц будем использовать
функцию RandomDataInitialization, реализацию которой читателю
предстоит выполнить самостоятельно.
// Функция для выделения памяти и инициализации исходных данных
void ProcessInitialization (double* pAMatrix, double* pBMatrix,
double* pCMatrix, double* pAblock, double* pBblock,
double* pCblock, double* pTemporaryAblock, int Size,
int BlockSize ) {
if (ProcRank == 0) {
do {
printf("\nВведите размер матриц: ");
scanf("%d", Size);
if (Size%GridSize != 0) {
printf ("Размер матриц должен быть кратен размеру сетки! \n");
}
}
while (Size%GridSize != 0);
}
MPI_Bcast(Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
BlockSize = Size/GridSize;
pAblock = new double [BlockSize*BlockSize];
pBblock = new double [BlockSize*BlockSize];
pCblock = new double [BlockSize*BlockSize];
pTemporaryAblock = new double [BlockSize*BlockSize];
for (int i=0; i<BlockSize*BlockSize; i++) {
pCblock[i] = 0;
}
if (ProcRank == 0) {
pAMatrix = new double [Size*Size];
pBMatrix = new double [Size*Size];
pCMatrix = new double [Size*Size];
RandomDataInitialization(pAMatrix, pBMatrix, Size);
}
}
4. Функции DataDistribution и ResultCollection. После задания
исходных матриц на нулевом процессе необходимо осуществить
распределение исходных данных. Для этого предназначена функция DataDistribution. Может быть предложено два способа выполнения
блочного разделения матриц между процессорами, организованными в
двумерную квадратную решетку. В первом из них для организации
передачи блоков в рамках одной и той же коммуникационной операции
можно сформировать средствами
Для выполнения сбора результирующей матрицы из блоков
предназначена функция ResultCollection. Сбор данных также можно
выполнить либо с использованием
Реализация функций DataDistribution и ResultCollection
представляет собой задание для самостоятельной работы.
5. Функция AblockCommunication. Функция выполняет рассылку
блоков матрицы A по строкам процессорной решетки. Для этого в
каждой строке решетки определяется ведущий процесс Pivot,
осуществляющий рассылку. Для рассылки используется блок pMatrixAblock, переданный в процесс в момент начального
распределения данных. Выполнение операции рассылки блоков
осуществляется при помощи функции MPI_Bcast. Следует отметить,
что данная операция является коллективной и ее локализация
пределами отдельных строк решетки обеспечивается за счет
использования коммуникаторов RowComm, определенных для набора
процессов каждой строки решетки в отдельности.
// Рассылка блоков матрицы А по строкам решетки процессов
void ABlockCommunication (int iter, double *pAblock,
double* pMatrixAblock, int BlockSize) {
// Определение ведущего процесса в строке процессной решетки
int Pivot = (GridCoords[0] + iter) % GridSize;
// Копирование передаваемого блока в отдельный буфер памяти
if (GridCoords[1] == Pivot) {
for (int i=0; i<BlockSize*BlockSize; i++)
pAblock[i] = pMatrixAblock[i];
}
// Рассылка блока
MPI_Bcast(pAblock, BlockSize*BlockSize, MPI_DOUBLE, Pivot,
RowComm);
}
6. Функция BlockMultiplication. Функция обеспечивает
перемножение блоков матриц A и B. Следует отметить, что для более
легкого понимания рассматриваемой программы приводится простой
вариант реализации функции – выполнение операции блочного
умножения может быть существенным образом оптимизировано для
сокращения времени вычислений. Данная оптимизация может быть
направлена, например, на повышение эффективности использования
кэша процессоров, векторизации выполняемых операций и т.п.
// Умножение матричных блоков
void BlockMultiplication (double *pAblock, double *pBblock,
double *pCblock, int BlockSize) {
// Вычисление произведения матричных блоков
for (int i=0; i<BlockSize; i++) {
for (int j=0; j<BlockSize; j++) {
double temp = 0;
for (int k=0; k<BlockSize; k++ )
temp += pAblock [i*BlockSize + k] * pBblock [k*BlockSize + j];
pCblock [i*BlockSize + j] += temp;
}
}
}
7. Функция BblockCommunication. Функция выполняет циклический
сдвиг блоков матрицы B по столбцам процессорной решетки. Каждый
процесс передает свой блок следующему процессу NextProc в столбце
процессов и получает блок, переданный из предыдущего процесса PrevProc в столбце решетки. Выполнение MPI_SendRecv_replace, которая
обеспечивает все необходимые пересылки блоков, используя при этом
один и тот же буфер памяти pBblock. Кроме того, эта функция
гарантирует отсутствие возможных тупиков, когда
// Циклический сдвиг блоков матрицы В вдоль столбца процессной
// решетки
void BblockCommunication (double *pBblock, int BlockSize) {
MPI_Status Status;
int NextProc = GridCoords[0] + 1;
if ( GridCoords[0] == GridSize-1 ) NextProc = 0;
int PrevProc = GridCoords[0] - 1;
if ( GridCoords[0] == 0 ) PrevProc = GridSize-1;
MPI_Sendrecv_replace( pBblock, BlockSize*BlockSize, MPI_DOUBLE,
NextProc, 0, PrevProc, 0, ColComm, Status);
}
8. Функция ParallelResultCalculation. Для непосредственного
выполнения параллельного алгоритма Фокса ParallelResultCalculation, которая
реализует логику работы алгоритма.
// Функция для параллельного умножения матриц
void ParallelResultCalculation(double* pAblock, double* pMatrixAblock,
double* pBblock, double* pCblock, int BlockSize) {
for (int iter = 0; iter < GridSize; iter ++) {
// Рассылка блоков матрицы A по строкам процессной решетки
ABlockCommunication (iter, pAblock, pMatrixAblock, BlockSize);
// Умножение блоков
BlockMultiplication(pAblock, pBblock, pCblock, BlockSize);
// Циклический сдвиг блоков матрицы B в столбцах процессной
// решетки
BblockCommunication(pBblock, BlockSize);
}
}
Вычислительные эксперименты для оценки эффективности параллельного алгоритма проводились при тех же условиях, что и ранее выполненные (см. п. 7.3.5). Результаты экспериментов с использованием четырех и девяти процессоров приведены в таблице 7.3.
| Размер матриц | Последовательный алгоритм | Параллельный алгоритм | |||
|---|---|---|---|---|---|
| 4 процессора | 9 процессоров | ||||
| Время | Ускорение | Время | Ускорение | ||
| 500 | 0,8527 | 0,2190 | 3,8925 | 0,1468 | 5,8079 |
| 1000 | 12,8787 | 3,0910 | 4,1664 | 2,1565 | 5,9719 |
| 1500 | 43,4731 | 10,8678 | 4,0001 | 7,2502 | 5,9960 |
| 2000 | 103,0561 | 24,1421 | 4,2687 | 21,4157 | 4,8121 |
| 2500 | 201,2915 | 51,4735 | 3,9105 | 41,2159 | 4,8838 |
| 3000 | 347,8434 | 87,0538 | 3,9957 | 58,2022 | 5,9764 |

(рис 7.8) Зависимость ускорения от размера матриц при выполнении параллельного алгоритма Фокса(рис 7.7) График зависимости экспериментального и теоретического времени выполнения алгоритма Фокса на четырех процессорах| Размер матриц | 4 процессора | 9 процессоров | ||
|---|---|---|---|---|
| $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | |
| 500 | 0,4217 | 0,2190 | 0,2200 | 0,1468 |
| 1000 | 3,2970 | 3,0910 | 1,5924 | 2,1565 |
| 1500 | 11,0419 | 10,8678 | 5,1920 | 7,2502 |
| 2000 | 26,0726 | 24,1421 | 12,0927 | 21,4157 |
| 2500 | 50,8049 | 51,4735 | 23,3682 | 41,2159 |
| 3000 | 87,6548 | 87,0538 | 40,0923 | 58,2022 |
Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретического
времени Tp, вычисленного в соответствии с выражением (7.13),
представлено в таблице 7.4 и на рис. 7.8.
Рассмотрим еще один параллельный алгоритм матричного умножения, основанный на блочном разбиении матриц, – алгоритм Кэннона ( Cannon ).
Как и при рассмотрении алгоритма Фокса, в качестве базовой
подзадачи выберем вычисления, связанные с определением одного из
блоков результирующей матрицы C. Как уже отмечалось ранее, для
вычисления элементов этого блока подзадача должна иметь доступ к
элементам горизонтальной полосы матрицы А и элементам
вертикальной полосы матрицы В.
Отличие алгоритма Кэннона от метода Фокса состоит в изменении схемы начального распределения блоков перемножаемых матриц между подзадачами вычислительной системы. Начальное расположение блоков в алгоритме Кэннона подбирается таким образом, чтобы блоки в подзадачах могли бы быть перемножены без каких-либо дополнительных передач данных. При этом подобное распределение блоков может быть организовано таким образом, что перемещение блоков между подзадачами в ходе вычислений может осуществляться с использованием более простых коммуникационных операций.
С учетом высказанных замечаний этап инициализации алгоритма Кэннона включает выполнение следующих операций передач данных:
(i,j) передаются блоки Aij, Bij ;i решетки подзадач блоки матрицы A сдвигаются на (i-1) позиций влево;j решетки подзадач блоки матрицы B сдвигаются на (j-1) позиций вверх.Выполняемые при перераспределении матричных блоков процедуры
передачи данных являются примером
(рис 7.9) Перераспределение блоков исходных матриц между процессорами при выполнении алгоритма КэннонаВ результате такого начального распределения в каждой базовой
подзадаче будут располагаться блоки, которые могут быть
перемножены без дополнительных A
должен быть передан предшествующей подзадаче влево по строкам
решетки подзадач, а каждый блок матрицы В – предшествующей
подзадаче вверх по столбцам решетки. Как можно показать,
последовательность таких циклических сдвигов и умножение
получаемых блоков исходных матриц A и B приведет к получению в
базовых подзадачах соответствующих блоков результирующей матрицы C.
Как и ранее в методе Фокса, для алгоритма Кэннона размер блоков может быть подобран таким образом, чтобы количество базовых подзадач совпадало с числом имеющихся процессоров. Поскольку объемы вычислений в каждой подзадаче являются равными, это обеспечивает полную балансировку вычислительной нагрузки между процессорами.
Для распределения подзадач между процессорами может быть
применен подход, использованный в алгоритме Фокса, – множество
имеющихся процессоров представляется в виде квадратной решетки и
размещение базовых подзадач (i,j) осуществляется на процессорах Pi,j соответствующих узлов процессорной решетки. Необходимая
структура сети передачи данных, как и ранее, может быть
обеспечена на физическом уровне при топологии вычислительной
системы в виде решетки или полного графа.
Перед проведением анализа эффективности следует отметить, что алгоритм Кэннона отличается от метода Фокса только видом выполняемых в ходе вычислений коммуникационных операций. Как результат, используя оценки времени выполнения вычислительных операций, приведенные в п. 7.4.4, проведем только анализ коммуникационной сложности алгоритма Кэннона.
В соответствии с правилами алгоритма на этапе инициализации
производится перераспределение блоков матриц А и B при помощи
циклического сдвига матричных блоков по строкам и столбцам
процессорной решетки. Трудоемкость выполнения такой log2q итераций. Для сети с кольцевой структурой связей
необходимое количество итераций оказывается равным q-1 – более
подробно методы выполнения n2/p определяет размер пересылаемых блоков, а
коэффициент 2 соответствует двум выполняемым операциям
циклического сдвига).
Оценим теперь затраты на передачу данных между процессорами
при выполнении основной части алгоритма Кэннона. На каждой
итерации алгоритма после умножения матричных блоков процессоры
передают свои блоки предыдущим процессорам по строкам (для блоков
матрицы A ) и столбцам (для блоков матрицы B ) процессорной
решетки. Эти операции также могут быть выполнены процессорами
параллельно, и, тем самым, длительность таких коммуникационных
действий составляет:$$T_p^2(comm)=2\cdot(\alpha+w\cdot(n^2/p)/ \beta)$$
Поскольку количество итераций алгоритма Кэннона равно q, то с
учетом оценки (7.10) общее время выполнения параллельных
вычислений может быть определено при помощи следующего
соотношения:$$T_p=q[(n^2/p)\cdot(2n/q-1)+(n^2/p)]\cdot\tau+(2q+2)(\alpha+w(n^2/p)/ \beta)$$
(в используемых выражениях параметр $$q=\sqrt{p}$$ определяет размер
процессорной решетки).
| Размер матриц | Последовательный алгоритм | Параллельный алгоритм | |||
|---|---|---|---|---|---|
| 4 процессора | 9 процессоров | ||||
| Время | Ускорение | Время | Ускорение | ||
| 1000 | 12,8787 | 3,0806 | 4,1805 | 1,1889 | 10,8324 |
| 1500 | 43,4731 | 11,1716 | 3,8913 | 4,6310 | 9,3872 |
| 2000 | 103,0561 | 24,0502 | 4,2850 | 14,4759 | 7,1191 |
| 2500 | 201,2915 | 53,1444 | 3,7876 | 23,5398 | 8,5511 |
| 3000 | 347,8434 | 88,2979 | 3,9394 | 36,3688 | 9,5643 |
Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретического
времени Tp, вычисленного в соответствии с выражением (7.16),
представлено в таблице 7.6 и на рис. 7.11.
| Размер матриц | 4 процессора | 9 процессоров | ||
|---|---|---|---|---|
| $$t_p$$ | $$t_p^*$$ | $$t_p$$ | $$t_p^*$$ | |
| 1000 | 3,4485 | 3,0806 | 1,5669 | 1,1889 |
| 1500 | 11,3821 | 11,1716 | 5,1348 | 4,6310 |
| 2000 | 26,6769 | 24,0502 | 11,9912 | 14,4759 |
| 2500 | 51,7488 | 53,1444 | 23,2098 | 23,5398 |
| 3000 | 89,0138 | 88,2979 | 39,8643 | 36,3688 |

(рис 7.11) Зависимость ускорения от размера матриц при выполнении параллельного алгоритма Кэннона(рис 7.10) График зависимости экспериментального и теоретического времени выполнения алгоритма Кэннона на четырех процессорах
В данной лекции рассмотрены три параллельных метода для
выполнения операции матричного умножения. Первый алгоритм основан
на ленточном разделении матриц между процессорами. В лекции
приведены два различных варианта этого алгоритма. Первый вариант
алгоритма основан на различном разделении перемножаемых матриц –
первая матрица (матрица A ) разбивается на горизонтальные полосы,
а вторая матрица (матрица B ) делится на вертикальные полосы.
Второй вариант ленточного алгоритма использует разбиение обеих
матриц на горизонтальные полосы.
Далее в лекции рассматриваются широко известные
Различие в способах разбиения данных приводит к разным топологиям коммуникационной сети, при которых выполнение параллельных алгоритмов является наиболее эффективным. Так, алгоритмы, основанные на ленточном разделении данных, ориентированы на топологию сети в виде гиперкуба или полного графа. Для реализации алгоритмов, основанных на блочном разделении данных, необходимо наличие топологии решетки.
На рис. 7.12 на общем графике представлены показатели
ускорения, полученные в результате выполнения
(рис 7.12) Ускорение трех параллельных алгоритмов при умножении матриц с использованием четырех процессоровЗадача
При рассмотрении вопросов ScaLAPACK.
A размера mxn и матрицы B размера nxl приводит к получению матрицы С размера mxl, каждый элемент
которой определяется в соответствии с выражением:$$c_{ij}=\sum_{k=0}^{n-1} a_{ik}\cdot b_{kj},\;
0 \le i < m, \; 0 \le j < l$$
Как следует из (7.1), каждый элемент результирующей матрицы С есть скалярное произведение соответствующих строки
матрицы A и столбца матрицы B:$$c_{ij}=(a_i,b_j^T), \; a_i = (a_{i0}, a_{i1} \ldots, a_{in-1}),
\; b_j^T = (b_{0j},b_{1j},\ldots,b_{n-1j})^T .$$
Этот алгоритм предполагает выполнение mxnxl операций умножения и столько же
операций сложения элементов исходных матриц. При умножении
квадратных матриц размера nxn количество
выполненных операций имеет порядок O(n3).
Известны последовательные алгоритмы nxn.
Последовательный алгоритм
Алгоритм 7.1. Последовательный алгоритм умножения двух квадратных матриц
// Алгоритм 7.1
// Последовательный алгоритм умножения матриц
double MatrixA[Size][Size];
double MatrixB[Size][Size];
double MatrixC[Size][Size];
int i,j,k;
...
for (i=0; i<Size; i++){
for (j=0; j<Size; j++){
MatrixC[i][j] = 0;
for (k=0; k<Size; k++){
MatrixC[i][j] = MatrixC[i][j] + MatrixA[i][k]*MatrixB[k][j];
}
}
}
Этот алгоритм является итеративным и ориентирован на
последовательное вычисление строк матрицы С.
Действительно, при выполнении одной итерации внешнего цикла
(цикла по переменной i ) вычисляется одна строка
результирующей матрицы (см. рис.
7.1).
(рис 7.1) На первой итерации цикла по переменной i используется первая строка матрицы A и все столбцы матрицы B для того, чтобы вычислить элементы первой строки результирующей матрицы СПоскольку каждый элемент результирующей матрицы есть скалярное
произведение строки и столбца исходных матриц, то для вычисления
всех элементов матрицы С размером nxn
необходимо выполнить n2x(2n–1)
скалярных операций и затратить время$$T_1 = n^2 \cdot (2n-1) \cdot \tau ,$$
где $$\tau$$ есть время выполнения одной элементарной скалярной
операции.
Рассмотрим два параллельных алгоритма A и B разбиваются на непрерывные
последовательности строк или столбцов (полосы).
Из определения операции матричного умножения следует, что
вычисление всех элементов матрицы С может быть
выполнено независимо друг от друга. Как результат, возможный
подход для организации параллельных вычислений состоит в
использовании в качестве базовой подзадачи процедуры определения
одного элемента результирующей матрицы С. Для
проведения всех необходимых вычислений каждая подзадача должна
содержать по одной строке матрицы А и одному столбцу
матрицы В. Общее количество получаемых при таком
подходе подзадач оказывается равным n2
(по числу элементов матрицы С ).
Рассмотрев предложенный подход, можно отметить, что
достигнутый уровень параллелизма является в большинстве случаев
избыточным. Обычно при проведении практических расчетов такое
количество сформированных подзадач превышает число имеющихся
процессоров и делает неизбежным этап укрупнения базовых задач. В
этом плане может оказаться полезной агрегация вычислений уже на
шаге выделения базовых подзадач. Возможное решение может состоять
в объединении в рамках одной подзадачи всех вычислений, связанных
не с одним, а с несколькими элементами результирующей матрицы С. Для дальнейшего рассмотрения определим базовую
задачу как процедуру вычисления всех элементов одной из строк
матрицы С. Такой подход приводит к снижению общего
количества подзадач до величины n.
Для выполнения всех необходимых вычислений базовой подзадаче
должны быть доступны одна из строк матрицы A и все
столбцы матрицы B. Простое решение этой проблемы –
дублирование матрицы B во всех подзадачах –
является, как правило, неприемлемым в силу больших затрат памяти
для хранения данных. Поэтому организация вычислений должна быть
построена таким образом, чтобы в каждый текущий момент времени
подзадачи содержали лишь часть данных, необходимых для проведения
расчетов, а доступ к остальной части данных обеспечивался бы при
помощи передачи данных между процессорами. Два возможных способа
выполнения параллельных вычислений подобного типа рассмотрены
далее в п. 7.3.2.
Для вычисления одной строки матрицы С необходимо,
чтобы в каждой подзадаче содержалась строка матрицы А и был обеспечен доступ ко всем столбцам матрицы B. Возможные способы организации параллельных
вычислений состоят в следующем.
1. Первый алгоритм. Алгоритм представляет собой
итерационную процедуру, количество итераций которой совпадает с
числом подзадач. На каждой итерации алгоритма каждая подзадача
содержит по одной строке матрицы А и одному столбцу матрицы В.
При выполнении итерации проводится С. По
завершении вычислений в конце каждой итерации столбцы матрицы В
должны быть переданы между подзадачами с тем, чтобы в каждой
подзадаче оказались новые столбцы матрицы В и могли быть
вычислены новые элементы матрицы C. При этом данная передача
столбцов между подзадачами должна быть организована таким
образом, чтобы после завершения итераций алгоритма в каждой
подзадаче последовательно оказались все столбцы матрицы В.
Возможная простая схема организации необходимой
последовательности передач столбцов матрицы В между подзадачами
состоит в представлении топологии информационных связей подзадач
в виде кольцевой структуры. В этом случае на каждой итерации
подзадача i, 0<=i<n, будет передавать свой столбец матрицы В
подзадаче с номером i+1 (в соответствии с кольцевой структурой
подзадача n-1 передает свои данные подзадаче с номером 0 ) – см.
рис. 7.2. После выполнения всех итераций алгоритма необходимое
условие будет обеспечено – в каждой подзадаче поочередно окажутся
все столбцы матрицы В.
На рис. 7.2 представлены итерации алгоритма матричного
умножения для случая, когда матрицы состоят из четырех строк и
четырех столбцов ( n=4 ). В начале вычислений в каждой подзадаче i,
0<=i<n, располагаются i -я строка матрицы A и i -й столбец матрицы B. В результате их перемножения подзадача получает элемент cii
результирующей матрицы С. Далее подзадачи осуществляют обмен
столбцами, в ходе которого каждая подзадача передает свой столбец
матрицы B следующей подзадаче в соответствии с кольцевой
структурой информационных взаимодействий. Далее выполнение
описанных действий повторяется до завершения всех итераций
параллельного алгоритма.
(рис 7.2) Общая схема передачи данных для первого параллельного алгоритма матричного умножения при ленточной схеме разделения данных2. Второй алгоритм. Отличие второго алгоритма состоит в том,
что в подзадачах располагаются не столбцы, а строки матрицы B.
Как результат, перемножение данных каждой подзадачи сводится не к
C.
При рассмотренном способе разделения данных для выполнения
операции матричного умножения нужно обеспечить последовательное
получение в подзадачах всех строк матрицы B, поэлементное
умножение данных и суммирование вновь получаемых значений с ранее
вычисленными результатами. Организация необходимой
последовательности передач строк матрицы B между подзадачами
также может быть выполнена с использованием кольцевой структуры
информационных связей (см. рис. 7.3).
На рис. 7.3 представлены
итерации алгоритма матричного умножения для случая, когда матрицы
состоят из четырех строк и четырех столбцов ( n=4 ). В начале
вычислений в каждой подзадаче i, 0<=i<n, располагаются i -е строки
матрицы A и матрицы B. В результате их перемножения подзадача
определяет i -ю строку частичных результатов искомой матрицы C.
Далее подзадачи осуществляют обмен строками, в ходе которого
каждая подзадача передает свою строку матрицы B следующей
подзадаче в соответствии с кольцевой структурой информационных
взаимодействий. Далее выполнение описанных действий повторяется
до завершения всех итераций параллельного алгоритма.
(рис 7.3) Общая схема передачи данных для второго параллельного алгорится матричного умножения при ленточной схеме разделения данных
Выделенные базовые подзадачи характеризуются одинаковой
вычислительной трудоемкостью и равным объемом передаваемых
данных. Когда размер матриц n оказывается больше, чем число
процессоров p, базовые подзадачи можно укрупнить, объединив в
рамках одной подзадачи несколько соседних строк и столбцов
перемножаемых матриц. В этом случае исходная матрица A
разбивается на ряд горизонтальных полос, а матрица B
представляется в виде набора вертикальных (для первого алгоритма)
или горизонтальных (для второго алгоритма) полос. Размер полос
при этом следует выбрать равным k=n/p (в предположении, что n
кратно p ), что позволит по-прежнему обеспечить равномерность
распределения вычислительной нагрузки по процессорам,
составляющим многопроцессорную вычислительную систему.
Для распределения подзадач между процессорами может быть использован любой способ, обеспечивающий эффективное представление кольцевой структуры информационного взаимодействия подзадач. Для этого достаточно, например, чтобы подзадачи, являющиеся соседними в кольцевой топологии, располагались на процессорах, между которыми имеются прямые линии передачи данных.
Выполним анализ эффективности первого параллельного алгоритма
Общая трудоемкость последовательного алгоритма, как уже
отмечалось ранее, является пропорциональной n3. Для параллельного
алгоритма на каждой итерации каждый процессор выполняет умножение
имеющихся на процессоре полос матрицы А и матрицы В (размер полос
равен n/p, и, как результат, общее количество выполняемых при
этом умножении операций равно n3/p2 ). Поскольку число итераций
алгоритма совпадает с количеством процессоров, сложность
параллельного алгоритма без учета затрат на передачу данных может
быть определена при помощи выражения$$T_p = (n^3/p^2)\cdot p = n^3/p .$$
С учетом этой оценки показатели ускорения и эффективности данного параллельного алгоритма матричного умножения принимают вид:$$S_p = \frac{n^3}{n^3/p} = p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$
Таким образом, общий анализ сложности дает идеальные
показатели эффективности параллельных вычислений. Для уточнения
полученных соотношений оценим более точно количество
вычислительных операций алгоритма и учтем затраты на выполнение
С учетом числа и длительности выполняемых операций время выполнения вычислений параллельного алгоритма может быть оценено следующим образом:$$T_p(calc)=(n^2/p)\cdot(2n-1)\cdot\tau$$ (здесь, как и ранее, $$\tau$$ есть время выполнения одной элементарной скалярной операции).
Для оценки коммуникационной сложности параллельных вычислений
будем предполагать, что все n/p строк или столбцов
длины n. Общее количество параллельных операций передачи
сообщений на единицу меньше числа итераций алгоритма (на
последней итерации передача данных не является обязательной). Тем
самым, оценка трудоемкости выполняемых w есть размер элемента матрицы в байтах.
С учетом полученных соотношений общее время выполнения параллельного алгоритма матричного умножения определяется следующим выражением:$$T_p=(n^2/p)(2n-1)\cdot\tau+(p-1)\cdot(\alpha + w\cdot n\cdot(n/p)/\beta).$$
Эксперименты проводились на вычислительном кластере на базе
процессоров Intel Xeon 4
Для оценки длительности $$\tau$$ базовой скалярной операции
проводилось решение задачи b соответственно 130 мкс и 53,29
Мбайт/с. Все вычисления производились над числовыми значениями
типа double, т.е. величина w равна 8 байт.
Результаты
| Размер матрицы | Последовательный алгоритм | Параллельный алгоритм | |||||
|---|---|---|---|---|---|---|---|
| 2 процессора | 4 процессора | 8 процессоров | |||||
| Время | Ускорение | Время | Ускорение | Время | Ускорение | ||
| 500 | 0,8752 | 0,3758 | 2,3287 | 0,1535 | 5,6982 | 0,0968 | 9,0371 |
| 1000 | 12,8787 | 5,4427 | 2,3662 | 2,2628 | 5,6912 | 0,6998 | 18,4014 |
| 1500 | 43,4731 | 20,9503 | 2,0750 | 11,0804 | 3,9234 | 5,1766 | 8,3978 |
| 2000 | 103,0561 | 45,7436 | 2,2529 | 21,6001 | 4,7710 | 9,4127 | 10,9485 |
| 2500 | 201,2915 | 99,5097 | 2,0228 | 56,9203 | 3,5363 | 18,3303 | 10,9813 |
| 3000 | 347,8434 | 171,9232 | 2,0232 | 111,9642 | 3,1067 | 45,5482 | 7,6368 |
(рис 7.4) Зависимость ускорения от количества процессоров при выполнении первого параллельного алгоритма матричного умножения при ленточной схеме распределения данныхСравнение экспериментального времени $$T^*_p$$ выполнения
эксперимента и теоретического времени Tp из формулы (7.8)
представлено в таблице 7.2 и на рис. 7.5.
(рис 7.5) График зависимости от объема исходных данных теоретического и экспериментального времени выполнения параллельного алгоритма на двух процессорах (ленточная схема разбиения данных)| Размер матрицы | 2 процессора | 4 процессора | 8 процессоров | |||
|---|---|---|---|---|---|---|
| $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | |
| 500 | 0,8243 | 0,3758 | 0,4313 | 0,1535 | 0,2353 | 0,0968 |
| 1000 | 6,51822 | 5,4427 | 3,3349 | 2,2628 | 1,7436 | 0,6998 |
| 1500 | 21,9137 | 20,9503 | 11,1270 | 11,0804 | 5,7340 | 5,1766 |
| 2000 | 51,8429 | 45,7436 | 26,2236 | 21,6001 | 13,4144 | 9,4127 |
| 2500 | 101,1377 | 99,5097 | 51,0408 | 56,9203 | 25,9928 | 18,3303 |
| 3000 | 174,6301 | 171,9232 | 87,9946 | 111,9642 | 44,6772 | 45,5482 |
При построении параллельных способов выполнения матричного умножения наряду с рассмотрением матриц в виде наборов строк и столбцов широко используется блочное представление матриц. Рассмотрим более подробно данный способ организации вычислений.
Блочная схема разбиения матриц подробно изложена в первом
разделе лекции 6. При таком способе разделения данных исходные
матрицы А, В и результирующая матрица С представляются в виде
наборов блоков. Для более простого изложения следующего материала
будем предполагать далее, что все матрицы являются квадратными
размера nxn, количество блоков по горизонтали и вертикали
одинаково и равно q (т.е. размер всех блоков равен kxk, k=n/q ).
При таком представлении данных операция матричного А и B в блочном виде может быть представлена так:$$\begin{pmatrix}
A_{00}A_{01}\ldots A_{0q-1} \\
\ldots \\
A_{q-10}A_{q-11}\ldots A_{q-1q-1}
\end{pmatrix}
\times
\begin{pmatrix}
B_{00}B_{01}\ldots B_{0q-1} \\
\ldots \\
B_{q-10}B_{q-11}\ldots B_{q-1q-1}
\end{pmatrix}
=
\begin{pmatrix}
C_{00}C_{01}\ldots C_{0q-1} \\
\ldots \\
C_{q-10}C_{q-11}\ldots C_{q-1q-1}
\end{pmatrix} ,$$
где каждый блок Cij матрицы C определяется в соответствии с выражением$$C_{ij}=\sum_{s=0}^{q-1} A_{is} B_{sj}.$$
При блочном разбиении данных для определения базовых подзадач
естественным представляется взять за основу вычисления,
выполняемые над матричными блоками. С учетом сказанного определим
базовую подзадачу как процедуру вычисления всех элементов одного
из блоков матрицы С.
Для выполнения всех необходимых вычислений базовым подзадачам
должны быть доступны соответствующие наборы строк матрицы A и
столбцов матрицы B. Размещение всех требуемых данных в каждой
подзадаче неизбежно приведет к дублированию и к значительному
росту объема используемой памяти. Как результат, вычисления
должны быть организованы таким образом, чтобы в каждый текущий
момент времени подзадачи содержали лишь часть необходимых для
проведения расчетов данных, а доступ к остальной части данных
обеспечивался бы при помощи передачи данных между процессорами.
Один из возможных подходов – алгоритм Фокса ( Fox ) – рассмотрен
далее в данном подразделе. Второй способ – алгоритм Кэннона
( Cannon ) – приводится в подразделе 7.5.
Итак, за основу параллельных вычислений для матричного
умножения при блочном разделении данных принят подход, при
котором базовые подзадачи отвечают за вычисления отдельных блоков
матрицы C и при этом в подзадачах на каждой итерации расчетов
располагается только по одному блоку исходных матриц A и B. Для
нумерации подзадач будем использовать индексы размещаемых в
подзадачах блоков матрицы C, т.е. подзадача (i,j) отвечает за
вычисление блока Cij – тем самым, набор подзадач образует
квадратную решетку, соответствующую структуре блочного
представления матрицы C.
Возможный способ организации вычислений при таких условиях состоит в применении широко известного алгоритма Фокса ( Fox ) — см., например, [, ].
В соответствии с алгоритмом Фокса в ходе вычислений на каждой
базовой подзадаче (i,j) располагается четыре матричных блока:
Cij матрицы C, вычисляемый подзадачей;Aij матрицы A, размещаемый в подзадаче перед началом вычислений;A'ij, B'ij матриц A и B, получаемые подзадачей в ходе выполнения вычислений.Выполнение параллельного метода включает:
(i,j)
передаются блоки Aij, Bij и обнуляются блоки Cij на всех
подзадачах;l, 0<=l<q, осуществляются следующие операции:i, 0<=i<q, блок Aij подзадачи (i,j)
пересылается на все подзадачи той же строки i решетки; индекс j,
определяющий положение подзадачи в строке, вычисляется в
соответствии с выражением$$j = ( i+l ) \bmod q,$$
где mod есть операция получения остатка от целочисленного деления;A'ij, B'ij каждой
подзадачи (i,j) перемножаются и прибавляются к блоку CijB'ij каждой подзадачи (i,j) пересылаются подзадачам,
являющимся соседями сверху в столбцах решетки подзадач (блоки
подзадач из первой строки решетки пересылаются подзадачам
последней строки решетки).
(рис 7.6) Состояние блоков в каждой подзадаче в ходе выполнения итераций алгоритма ФоксаДля пояснения этих правил параллельного метода на рис. 7.6
приведено состояние блоков в каждой подзадаче в ходе выполнения
итераций этапа вычислений (для решетки подзадач 2x2 ).
В рассмотренной схеме параллельных вычислений количество
блоков может варьироваться в зависимости от выбора размера блоков
– эти размеры могут быть подобраны таким образом, чтобы общее
количество базовых подзадач совпадало с числом процессоров p.
Так, например, в наиболее простом случае, когда число процессоров
представимо в виде $$p=\delta 2$$ (т.е. является полным квадратом), можно
выбрать количество блоков в матрицах по вертикали и горизонтали
равным $$\delta$$ (т.е. $$q=\delta$$ ). Такой способ определения количества блоков
приводит к тому, что объем вычислений в каждой подзадаче является
одинаковым и тем самым достигается полная балансировка
вычислительной нагрузки между процессорами. В более общем случае
при произвольных количестве процессоров и размерах матриц
Для эффективного выполнения алгоритма Фокса, в котором базовые
подзадачи представлены в виде квадратной решетки и в ходе
вычислений выполняются операции передачи блоков по строкам и
столбцам решетки подзадач, наиболее адекватным решением является
организация множества имеющихся процессоров также в виде
квадратной решетки. В этом случае можно осуществить
непосредственное отображение набора подзадач на множество
процессоров – базовую подзадачу (i,j) следует располагать на
процессоре Pi,j. Необходимая структура сети передачи данных может
быть обеспечена на физическом уровне, если топология
вычислительной системы имеет вид решетки или полного графа.
Определим вычислительную сложность данного алгоритма Фокса.
Построение оценок будет происходить при условии выполнения всех
ранее выдвинутых предположений: все матрицы являются квадратными
размера nxn, количество блоков по горизонтали и вертикали
являются одинаковым и равным q (т.е. размер всех блоков равен kxk, k=n/q ), процессоры образуют квадратную решетку и их
количество равно p=q2.
Как уже отмечалось, алгоритм Фокса требует для своего
выполнения q итераций, в ходе которых каждый процессор
перемножает свои текущие блоки матриц А и В и прибавляет
результаты умножения к текущему значению блока матрицы C. С
учетом выдвинутых предположений общее количество выполняемых при
этом операций будет иметь порядок n3/p. Как результат, показатели
ускорения и эффективности алгоритма имеют вид:$$S_p = \frac{n^3}{n^3/p} = p \quad \text{и} \quad E_p=\frac{n^3}{p\cdot(n^3/p)}=1.$$
Общий анализ сложности снова дает идеальные показатели
эффективности параллельных вычислений. Уточним полученные
соотношения — для этого укажем более точно количество
вычислительных операций алгоритма и учтем затраты на выполнение
Определим количество вычислительных операций. Сложность
выполнения A на столбец
блока матрицы В можно оценить как 2(n/q)-1. Количество строк и
столбцов в блоках равно n/q и, как результат, трудоемкость
операции блочного умножения оказывается равной (n2/p)(2n/q-1).
Для сложения блоков требуется n2/p операций. С учетом всех
перечисленных выражений время выполнения вычислительных операций
алгоритма Фокса может быть оценено следующим образом:$$T_p(calc)=q[(n^2 /p)\cdot(2n/q-1)+(n^2 /p)]\cdot\tau.$$
(напомним, что $$\tau$$ есть время выполнения одной элементарной скалярной операции).
Оценим затраты на выполнение A остальным процессорам своей строки. Как уже
отмечалось ранее, при топологии сети в виде гиперкуба или полного
графа выполнение этой операции может быть обеспечено за log2q
шагов, а объем передаваемых блоков равен n2/p. Как результат,
время выполнения операции передачи блоков матрицы A при
использовании модели Хокни может оцениваться как$$T_p^1(comm)=\log_2 q(\alpha +w(n^2/p)/ \beta),$$
где $$\alpha$$ – латентность, $$\beta$$ – пропускная способность сети
передачи данных, а w есть размер элемента матрицы в байтах. В
случае же когда топология строк процессорной решетки представляет
собой кольцо, выражение для оценки времени передачи блоков
матрицы A принимает вид:$$\widetilde{T}_p^1(comm)=(q/2)(\alpha+w(n^2/p)/ \beta).$$
Далее после умножения матричных блоков процессоры передают
свои блоки матрицы В предыдущим процессорам по столбцам
процессорной решетки (первые процессоры столбцов передают свои
данные последним процессорам в столбцах решетки). Эти операции
могут быть выполнены процессорами параллельно, и, тем самым,
длительность такой коммуникационной операции составляет:$$T-p^2(comm)=\alpha+w \cdot (n^2/p)/ \beta.$$
Просуммировав все полученные выражения, можно получить, что
общее время выполнения алгоритма Фокса может быть определено при
помощи следующих соотношений:$$\begin{aligned}
T_p = q[(n^2/p) \cdot (2n/q-1)+(n^2/p)] \cdot \tau+q \log_2 q(\alpha + w(n^2/p)/ \beta) + \\
+(q-1) \cdot (\alpha + w(n^2/p)/ \beta) = q[(n^2/p) \cdot (2n/q-1)+(n^2/p)] \cdot \tau+ \\
+(q\log_2 q +(q-1))( \alpha + w(n^2/p)/ \beta)
\end{aligned}$$
(напомним, что параметр q определяет размер процессорной решетки
и $$q=\sqrt{p}$$ ).
Представим возможный вариант
1. Главная функция программы. Определяет основную логику работы алгоритма, последовательно вызывает необходимые подпрограммы.
// Программа 7.1
// Алгоритм Фокса умножения матриц – блочное представление данных
// Условия выполнения программы: все матрицы квадратные,
// размер блоков и их количество по горизонтали и вертикали
// одинаково, процессы образуют квадратную решетку
int ProcNum = 0; // Количество доступных процессов
int ProcRank = 0; // Ранг текущего процесса
int GridSize; // Размер виртуальной решетки процессов
int GridCoords[2]; // Координаты текущего процесса в процессной
// решетке
MPI_Comm GridComm; // Коммуникатор в виде квадратной решетки
MPI_Comm ColComm; // коммуникатор – столбец решетки
MPI_Comm RowComm; // коммуникатор – строка решетки
void main ( int argc, char * argv[] ) {
double* pAMatrix; // Первый аргумент матричного умножения
double* pBMatrix; // Второй аргумент матричного умножения
double* pCMatrix; // Результирующая матрица
int Size; // Размер матриц
int BlockSize; // Размер матричных блоков, расположенных
// на процессах
double *pAblock; // Блок матрицы А на процессе
double *pBblock; // Блок матрицы В на процессе
double *pCblock; // Блок результирующей матрицы С на процессе
double *pMatrixAblock;
double Start, Finish, Duration;
setvbuf(stdout, 0, _IONBF, 0);
MPI_Init(argc, argv);
MPI_Comm_size(MPI_COMM_WORLD, ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, ProcRank);
GridSize = sqrt((double)ProcNum);
if (ProcNum != GridSize*GridSize) {
if (ProcRank == 0) {
printf ("Number of processes must be a perfect square \n");
}
}
else {
if (ProcRank == 0)
printf("Parallel matrix multiplication program\n");
// Создание виртуальной решетки процессов и коммуникаторов
// строк и столбцов
CreateGridCommunicators();
// Выделение памяти и инициализация элементов матриц
ProcessInitialization ( pAMatrix, pBMatrix, pCMatrix, pAblock,
pBblock, pCblock, pMatrixAblock, Size, BlockSize );
// Блочное распределение матриц между процессами
DataDistribution(pAMatrix, pBMatrix, pMatrixAblock, pBblock, Size,
BlockSize);
// Выполнение параллельного метода Фокса
ParallelResultCalculation(pAblock, pMatrixAblock, pBblock,
pCblock, BlockSize);
// Сбор результирующей матрицы на ведущем процессе
ResultCollection(pCMatrix, pCblock, Size, BlockSize);
// Завершение процесса вычислений
ProcessTermination (pAMatrix, pBMatrix, pCMatrix, pAblock, pBblock,
pCblock, pMatrixAblock);
}
MPI_Finalize();
}
2. Функция CreateGridCommunicators. Данная функция создает
коммуникатор в виде двумерной квадратной решетки, определяет
координаты каждого процесса в этой решетке, а также создает
коммуникаторы отдельно для каждой строки и каждого столбца.
Создание решетки производится при помощи функции MPI_Cart_create (вектор Periodic определяет возможность передачи
сообщений между граничными процессами строк и столбцов
создаваемой решетки). После создания решетки каждый процесс
параллельной программы будет иметь координаты своего положения в
решетке; получение этих координат обеспечивается при помощи
функции MPI_Cart_coords.
Формирование топологий завершается созданием множества
коммуникаторов для каждой строки и каждого столбца решетки в
отдельности (функция MPI_Cart_sub ).
// Создание коммуникатора в виде двумерной квадратной решетки
// и коммуникаторов для каждой строки и каждого столбца решетки
void CreateGridCommunicators() {
int DimSize[2]; // Количество процессов в каждом измерении
// решетки
int Periodic[2]; // =1 для каждого измерения, являющегося
// периодическим
int Subdims[2]; // =1 для каждого измерения, оставляемого
// в подрешетке
DimSize[0] = GridSize;
DimSize[1] = GridSize;
Periodic[0] = 0;
Periodic[1] = 0;
// Создание коммуникатора в виде квадратной решетки
MPI_Cart_create(MPI_COMM_WORLD, 2, DimSize, Periodic, 1, GridComm);
// Определение координат процесса в решетке
MPI_Cart_coords(GridComm, ProcRank, 2, GridCoords);
// Создание коммуникаторов для строк процессной решетки
Subdims[0] = 0; // Фиксация измерения
Subdims[1] = 1; // Наличие данного измерения в подрешетке
MPI_Cart_sub(GridComm, Subdims, RowComm);
// Создание коммуникаторов для столбцов процессной решетки
Subdims[0] = 1;
Subdims[1] = 0;
MPI_Cart_sub(GridComm, Subdims, ColComm);
}
3. Функция ProcessInitialization. Данная функция определяет
параметры решаемой задачи (размеры матриц и их блоков), выделяет
память для хранения данных и осуществляет ввод исходных матриц
(или формирует их при помощи какого-либо датчика случайных
чисел). Всего в каждом процессе должна быть выделена память для
хранения четырех блоков – для указателей на выделенную память
используются переменные pAblock, pBblock, pCblock, pMatrixAblock.
Первые три указателя определяют блоки матриц A, B и C
соответственно. Следует отметить, что содержимое блоков pAblock и pBblock постоянно меняется в соответствии с пересылкой данных
между процессами, в то время как блок pMatrixAblock матрицы A
остается неизменным и применяется при рассылках блоков по строкам
решетки процессов (см. функцию AblockCommunication ).
Для определения элементов исходных матриц будем использовать
функцию RandomDataInitialization, реализацию которой читателю
предстоит выполнить самостоятельно.
// Функция для выделения памяти и инициализации исходных данных
void ProcessInitialization (double* pAMatrix, double* pBMatrix,
double* pCMatrix, double* pAblock, double* pBblock,
double* pCblock, double* pTemporaryAblock, int Size,
int BlockSize ) {
if (ProcRank == 0) {
do {
printf("\nВведите размер матриц: ");
scanf("%d", Size);
if (Size%GridSize != 0) {
printf ("Размер матриц должен быть кратен размеру сетки! \n");
}
}
while (Size%GridSize != 0);
}
MPI_Bcast(Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
BlockSize = Size/GridSize;
pAblock = new double [BlockSize*BlockSize];
pBblock = new double [BlockSize*BlockSize];
pCblock = new double [BlockSize*BlockSize];
pTemporaryAblock = new double [BlockSize*BlockSize];
for (int i=0; i<BlockSize*BlockSize; i++) {
pCblock[i] = 0;
}
if (ProcRank == 0) {
pAMatrix = new double [Size*Size];
pBMatrix = new double [Size*Size];
pCMatrix = new double [Size*Size];
RandomDataInitialization(pAMatrix, pBMatrix, Size);
}
}
4. Функции DataDistribution и ResultCollection. После задания
исходных матриц на нулевом процессе необходимо осуществить
распределение исходных данных. Для этого предназначена функция DataDistribution. Может быть предложено два способа выполнения
блочного разделения матриц между процессорами, организованными в
двумерную квадратную решетку. В первом из них для организации
передачи блоков в рамках одной и той же коммуникационной операции
можно сформировать средствами
Для выполнения сбора результирующей матрицы из блоков
предназначена функция ResultCollection. Сбор данных также можно
выполнить либо с использованием
Реализация функций DataDistribution и ResultCollection
представляет собой задание для самостоятельной работы.
5. Функция AblockCommunication. Функция выполняет рассылку
блоков матрицы A по строкам процессорной решетки. Для этого в
каждой строке решетки определяется ведущий процесс Pivot,
осуществляющий рассылку. Для рассылки используется блок pMatrixAblock, переданный в процесс в момент начального
распределения данных. Выполнение операции рассылки блоков
осуществляется при помощи функции MPI_Bcast. Следует отметить,
что данная операция является коллективной и ее локализация
пределами отдельных строк решетки обеспечивается за счет
использования коммуникаторов RowComm, определенных для набора
процессов каждой строки решетки в отдельности.
// Рассылка блоков матрицы А по строкам решетки процессов
void ABlockCommunication (int iter, double *pAblock,
double* pMatrixAblock, int BlockSize) {
// Определение ведущего процесса в строке процессной решетки
int Pivot = (GridCoords[0] + iter) % GridSize;
// Копирование передаваемого блока в отдельный буфер памяти
if (GridCoords[1] == Pivot) {
for (int i=0; i<BlockSize*BlockSize; i++)
pAblock[i] = pMatrixAblock[i];
}
// Рассылка блока
MPI_Bcast(pAblock, BlockSize*BlockSize, MPI_DOUBLE, Pivot,
RowComm);
}
6. Функция BlockMultiplication. Функция обеспечивает
перемножение блоков матриц A и B. Следует отметить, что для более
легкого понимания рассматриваемой программы приводится простой
вариант реализации функции – выполнение операции блочного
умножения может быть существенным образом оптимизировано для
сокращения времени вычислений. Данная оптимизация может быть
направлена, например, на повышение эффективности использования
кэша процессоров, векторизации выполняемых операций и т.п.
// Умножение матричных блоков
void BlockMultiplication (double *pAblock, double *pBblock,
double *pCblock, int BlockSize) {
// Вычисление произведения матричных блоков
for (int i=0; i<BlockSize; i++) {
for (int j=0; j<BlockSize; j++) {
double temp = 0;
for (int k=0; k<BlockSize; k++ )
temp += pAblock [i*BlockSize + k] * pBblock [k*BlockSize + j];
pCblock [i*BlockSize + j] += temp;
}
}
}
7. Функция BblockCommunication. Функция выполняет циклический
сдвиг блоков матрицы B по столбцам процессорной решетки. Каждый
процесс передает свой блок следующему процессу NextProc в столбце
процессов и получает блок, переданный из предыдущего процесса PrevProc в столбце решетки. Выполнение MPI_SendRecv_replace, которая
обеспечивает все необходимые пересылки блоков, используя при этом
один и тот же буфер памяти pBblock. Кроме того, эта функция
гарантирует отсутствие возможных тупиков, когда
// Циклический сдвиг блоков матрицы В вдоль столбца процессной
// решетки
void BblockCommunication (double *pBblock, int BlockSize) {
MPI_Status Status;
int NextProc = GridCoords[0] + 1;
if ( GridCoords[0] == GridSize-1 ) NextProc = 0;
int PrevProc = GridCoords[0] - 1;
if ( GridCoords[0] == 0 ) PrevProc = GridSize-1;
MPI_Sendrecv_replace( pBblock, BlockSize*BlockSize, MPI_DOUBLE,
NextProc, 0, PrevProc, 0, ColComm, Status);
}
8. Функция ParallelResultCalculation. Для непосредственного
выполнения параллельного алгоритма Фокса ParallelResultCalculation, которая
реализует логику работы алгоритма.
// Функция для параллельного умножения матриц
void ParallelResultCalculation(double* pAblock, double* pMatrixAblock,
double* pBblock, double* pCblock, int BlockSize) {
for (int iter = 0; iter < GridSize; iter ++) {
// Рассылка блоков матрицы A по строкам процессной решетки
ABlockCommunication (iter, pAblock, pMatrixAblock, BlockSize);
// Умножение блоков
BlockMultiplication(pAblock, pBblock, pCblock, BlockSize);
// Циклический сдвиг блоков матрицы B в столбцах процессной
// решетки
BblockCommunication(pBblock, BlockSize);
}
}
Вычислительные эксперименты для оценки эффективности параллельного алгоритма проводились при тех же условиях, что и ранее выполненные (см. п. 7.3.5). Результаты экспериментов с использованием четырех и девяти процессоров приведены в таблице 7.3.
| Размер матриц | Последовательный алгоритм | Параллельный алгоритм | |||
|---|---|---|---|---|---|
| 4 процессора | 9 процессоров | ||||
| Время | Ускорение | Время | Ускорение | ||
| 500 | 0,8527 | 0,2190 | 3,8925 | 0,1468 | 5,8079 |
| 1000 | 12,8787 | 3,0910 | 4,1664 | 2,1565 | 5,9719 |
| 1500 | 43,4731 | 10,8678 | 4,0001 | 7,2502 | 5,9960 |
| 2000 | 103,0561 | 24,1421 | 4,2687 | 21,4157 | 4,8121 |
| 2500 | 201,2915 | 51,4735 | 3,9105 | 41,2159 | 4,8838 |
| 3000 | 347,8434 | 87,0538 | 3,9957 | 58,2022 | 5,9764 |

(рис 7.8) Зависимость ускорения от размера матриц при выполнении параллельного алгоритма Фокса(рис 7.7) График зависимости экспериментального и теоретического времени выполнения алгоритма Фокса на четырех процессорах| Размер матриц | 4 процессора | 9 процессоров | ||
|---|---|---|---|---|
| $$T_p$$ | $$T_p^*$$ | $$T_p$$ | $$T_p^*$$ | |
| 500 | 0,4217 | 0,2190 | 0,2200 | 0,1468 |
| 1000 | 3,2970 | 3,0910 | 1,5924 | 2,1565 |
| 1500 | 11,0419 | 10,8678 | 5,1920 | 7,2502 |
| 2000 | 26,0726 | 24,1421 | 12,0927 | 21,4157 |
| 2500 | 50,8049 | 51,4735 | 23,3682 | 41,2159 |
| 3000 | 87,6548 | 87,0538 | 40,0923 | 58,2022 |
Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретического
времени Tp, вычисленного в соответствии с выражением (7.13),
представлено в таблице 7.4 и на рис. 7.8.
Рассмотрим еще один параллельный алгоритм матричного умножения, основанный на блочном разбиении матриц, – алгоритм Кэннона ( Cannon ).
Как и при рассмотрении алгоритма Фокса, в качестве базовой
подзадачи выберем вычисления, связанные с определением одного из
блоков результирующей матрицы C. Как уже отмечалось ранее, для
вычисления элементов этого блока подзадача должна иметь доступ к
элементам горизонтальной полосы матрицы А и элементам
вертикальной полосы матрицы В.
Отличие алгоритма Кэннона от метода Фокса состоит в изменении схемы начального распределения блоков перемножаемых матриц между подзадачами вычислительной системы. Начальное расположение блоков в алгоритме Кэннона подбирается таким образом, чтобы блоки в подзадачах могли бы быть перемножены без каких-либо дополнительных передач данных. При этом подобное распределение блоков может быть организовано таким образом, что перемещение блоков между подзадачами в ходе вычислений может осуществляться с использованием более простых коммуникационных операций.
С учетом высказанных замечаний этап инициализации алгоритма Кэннона включает выполнение следующих операций передач данных:
(i,j) передаются блоки Aij, Bij ;i решетки подзадач блоки матрицы A сдвигаются на (i-1) позиций влево;j решетки подзадач блоки матрицы B сдвигаются на (j-1) позиций вверх.Выполняемые при перераспределении матричных блоков процедуры
передачи данных являются примером
(рис 7.9) Перераспределение блоков исходных матриц между процессорами при выполнении алгоритма КэннонаВ результате такого начального распределения в каждой базовой
подзадаче будут располагаться блоки, которые могут быть
перемножены без дополнительных A
должен быть передан предшествующей подзадаче влево по строкам
решетки подзадач, а каждый блок матрицы В – предшествующей
подзадаче вверх по столбцам решетки. Как можно показать,
последовательность таких циклических сдвигов и умножение
получаемых блоков исходных матриц A и B приведет к получению в
базовых подзадачах соответствующих блоков результирующей матрицы C.
Как и ранее в методе Фокса, для алгоритма Кэннона размер блоков может быть подобран таким образом, чтобы количество базовых подзадач совпадало с числом имеющихся процессоров. Поскольку объемы вычислений в каждой подзадаче являются равными, это обеспечивает полную балансировку вычислительной нагрузки между процессорами.
Для распределения подзадач между процессорами может быть
применен подход, использованный в алгоритме Фокса, – множество
имеющихся процессоров представляется в виде квадратной решетки и
размещение базовых подзадач (i,j) осуществляется на процессорах Pi,j соответствующих узлов процессорной решетки. Необходимая
структура сети передачи данных, как и ранее, может быть
обеспечена на физическом уровне при топологии вычислительной
системы в виде решетки или полного графа.
Перед проведением анализа эффективности следует отметить, что алгоритм Кэннона отличается от метода Фокса только видом выполняемых в ходе вычислений коммуникационных операций. Как результат, используя оценки времени выполнения вычислительных операций, приведенные в п. 7.4.4, проведем только анализ коммуникационной сложности алгоритма Кэннона.
В соответствии с правилами алгоритма на этапе инициализации
производится перераспределение блоков матриц А и B при помощи
циклического сдвига матричных блоков по строкам и столбцам
процессорной решетки. Трудоемкость выполнения такой log2q итераций. Для сети с кольцевой структурой связей
необходимое количество итераций оказывается равным q-1 – более
подробно методы выполнения n2/p определяет размер пересылаемых блоков, а
коэффициент 2 соответствует двум выполняемым операциям
циклического сдвига).
Оценим теперь затраты на передачу данных между процессорами
при выполнении основной части алгоритма Кэннона. На каждой
итерации алгоритма после умножения матричных блоков процессоры
передают свои блоки предыдущим процессорам по строкам (для блоков
матрицы A ) и столбцам (для блоков матрицы B ) процессорной
решетки. Эти операции также могут быть выполнены процессорами
параллельно, и, тем самым, длительность таких коммуникационных
действий составляет:$$T_p^2(comm)=2\cdot(\alpha+w\cdot(n^2/p)/ \beta)$$
Поскольку количество итераций алгоритма Кэннона равно q, то с
учетом оценки (7.10) общее время выполнения параллельных
вычислений может быть определено при помощи следующего
соотношения:$$T_p=q[(n^2/p)\cdot(2n/q-1)+(n^2/p)]\cdot\tau+(2q+2)(\alpha+w(n^2/p)/ \beta)$$
(в используемых выражениях параметр $$q=\sqrt{p}$$ определяет размер
процессорной решетки).
| Размер матриц | Последовательный алгоритм | Параллельный алгоритм | |||
|---|---|---|---|---|---|
| 4 процессора | 9 процессоров | ||||
| Время | Ускорение | Время | Ускорение | ||
| 1000 | 12,8787 | 3,0806 | 4,1805 | 1,1889 | 10,8324 |
| 1500 | 43,4731 | 11,1716 | 3,8913 | 4,6310 | 9,3872 |
| 2000 | 103,0561 | 24,0502 | 4,2850 | 14,4759 | 7,1191 |
| 2500 | 201,2915 | 53,1444 | 3,7876 | 23,5398 | 8,5511 |
| 3000 | 347,8434 | 88,2979 | 3,9394 | 36,3688 | 9,5643 |
Сравнение времени выполнения эксперимента $$T^*_p$$ и теоретического
времени Tp, вычисленного в соответствии с выражением (7.16),
представлено в таблице 7.6 и на рис. 7.11.
| Размер матриц | 4 процессора | 9 процессоров | ||
|---|---|---|---|---|
| $$t_p$$ | $$t_p^*$$ | $$t_p$$ | $$t_p^*$$ | |
| 1000 | 3,4485 | 3,0806 | 1,5669 | 1,1889 |
| 1500 | 11,3821 | 11,1716 | 5,1348 | 4,6310 |
| 2000 | 26,6769 | 24,0502 | 11,9912 | 14,4759 |
| 2500 | 51,7488 | 53,1444 | 23,2098 | 23,5398 |
| 3000 | 89,0138 | 88,2979 | 39,8643 | 36,3688 |

(рис 7.11) Зависимость ускорения от размера матриц при выполнении параллельного алгоритма Кэннона(рис 7.10) График зависимости экспериментального и теоретического времени выполнения алгоритма Кэннона на четырех процессорах
В данной лекции рассмотрены три параллельных метода для
выполнения операции матричного умножения. Первый алгоритм основан
на ленточном разделении матриц между процессорами. В лекции
приведены два различных варианта этого алгоритма. Первый вариант
алгоритма основан на различном разделении перемножаемых матриц –
первая матрица (матрица A ) разбивается на горизонтальные полосы,
а вторая матрица (матрица B ) делится на вертикальные полосы.
Второй вариант ленточного алгоритма использует разбиение обеих
матриц на горизонтальные полосы.
Далее в лекции рассматриваются широко известные
Различие в способах разбиения данных приводит к разным топологиям коммуникационной сети, при которых выполнение параллельных алгоритмов является наиболее эффективным. Так, алгоритмы, основанные на ленточном разделении данных, ориентированы на топологию сети в виде гиперкуба или полного графа. Для реализации алгоритмов, основанных на блочном разделении данных, необходимо наличие топологии решетки.
На рис. 7.12 на общем графике представлены показатели
ускорения, полученные в результате выполнения
(рис 7.12) Ускорение трех параллельных алгоритмов при умножении матриц с использованием четырех процессоровЗадача
При рассмотрении вопросов ScaLAPACK.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.