Участник:Blizn/Хранение ненулевых элементов разреженной матрицы. Умножение разреженной матрицы на вектор.
Эта работа успешно выполнена Преподавателю: в основное пространство, в подстраницу Данное задание было проверено и зачтено. Проверено Evgeny Mortikov и ASA. |
Умножение разреженной матрицы на вектор | |
Последовательный алгоритм | |
Последовательная сложность | [math]O(l)[/math] |
Объём входных данных | [math]2l+m+n+1[/math] |
Объём выходных данных | [math]n[/math] |
Параллельный алгоритм | |
Высота ярусно-параллельной формы | [math]O(m)[/math] |
Ширина ярусно-параллельной формы | [math]O(n)[/math] |
Выполнила: И.В. Близнякова (611 группа).
Содержание
- 1 Свойства и структура алгоритма
- 1.1 Общее описание алгоритма[1]
- 1.2 Математическое описание алгоритма
- 1.3 Вычислительное ядро алгоритма
- 1.4 Макроструктура алгоритма
- 1.5 Схема реализации последовательного алгоритма
- 1.6 Последовательная сложность алгоритма
- 1.7 Информационный граф
- 1.8 Ресурс параллелизма алгоритма
- 1.9 Входные и выходные данные алгоритма
- 1.10 Свойства алгоритма
- 2 Программная реализация алгоритма
- 2.1 Особенности реализации последовательного алгоритма
- 2.2 Локальность данных и вычислений
- 2.3 Возможные способы и особенности параллельной реализации алгоритма
- 2.4 Масштабируемость алгоритма и его реализации
- 2.5 Динамические характеристики и эффективность реализации алгоритма
- 2.6 Выводы для классов архитектур
- 2.7 Существующие реализации алгоритма
- 3 Литература
1 Свойства и структура алгоритма
1.1 Общее описание алгоритма[1]
Разрежённая матрица — это матрица с преимущественно нулевыми элементами. В противном случае, если бо́льшая часть элементов матрицы ненулевые, матрица считается плотной.
Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разрежённой. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов:
- есть [math]O(n)[/math]. Такое определение подходит разве что для теоретического анализа асимптотических свойств матричных алгоритмов;
- в каждой строке не превышает 10 в типичном случае;
- ограничено [math]n^{1+\gamma}[/math], где [math]\gamma \lt 1[/math].
- таково, что для данного алгоритма и вычислительной системы имеет смысл извлекать выгоду из наличия в ней нулей.
1.1.1 Хранение разреженной матрицы
1.1.1.1 Формат RR(C)O
Рассмотрим сначала формат RR(C)O. Сокращенное название данного формата происходит от английского словосочетания "Row - wise Representation Complete and Ordered" (строчное представление, полное и упорядоченное). В данном формате вместо одного двумерного массива, используются три одномерных. Значения ненулевых элементов матрицы и соответствующие им столбцовые индексы хранятся в этом формате по строкам в двух массивах [math]AN[/math] и [math]JA[/math]. Массив указателей [math]IA[/math], используется для ссылки на компоненты массивов [math]AN[/math] и [math]JA[/math], с которых начинается описание очередной строки. Последняя компонента массива [math]IA[/math] содержит указатель первой свободной компоненты в массивах [math]AN[/math] и [math]JA[/math], т.е. равна числу ненулевых элементов матрицы, увеличенному на единицу. Здесь уместно привести пример.
Рассмотрим матрицу [math]A[/math]:
- [math]\begin{pmatrix} 0 & 0 & 0 & 2 & 0 \\ 1 & 0 & 3 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 6 & 0 & 0 & 0 \\ 0 & 0 & 4 & 0 & 0 \\ \end{pmatrix}[/math],
тогда ее представление в формате RR(C)O будет иметь вид:
IA = [ 1 2 4 4 5 6 ] JA = [ 4 1 3 2 3 ] AN = [ 2 1 3 6 4 ]
Т.е. массив [math]AN[/math] содержит все не нулевые элементы исходной матрицы [math]A[/math], массив [math]JA[/math] номер столбца в котором находится соответствующий элемент из [math]AN[/math] и наконец массив [math]IA[/math] содержит номер с которого начинается описание элементов в массивах [math]JA[/math] и [math]AN[/math]. Таким образом информация об элементах 2-ой строки матрицы хранится в элементах с [math]IA[2] = 2[/math] по [math]IA[3] - 1 = 3[/math] включительно массивов [math]JA[/math] и [math]AN[/math]. Можно обратить внимание, что [math]IA[3] = IA[4] = 4[/math], а это означает, что 3-я строка матрицы [math]A[/math] нулевая.
В общем случае описание [math]r[/math]-й строки матрицы A хранится в компонентах с [math]IA[r][/math] до [math]IA[r + 1] - 1[/math] включительно массивов [math]AN[/math] и [math]JA[/math]. Если [math]IA[r + 1] = IA[r][/math], то это означает, что [math]r[/math]-я строка нулевая. Количество элементов в массиве [math]IA[/math] на единицу больше, чем число строк исходной матрицы, а количество элементов в массивах [math]JA[/math] и [math]AN[/math] равно числу ненулевых элементов исходной матрицы.
Данный способ представления называют полным, поскольку представлена вся матрица [math]A[/math], упорядоченным, поскольку элементы каждой строки матрицы [math]A[/math] хранятся в соответствии с возрастанием столбцовых индексов, и строчным, поскольку информация о матрице [math]A[/math] указывается по строкам.
Массивы [math]IA[/math] и [math]JA[/math] представляют портрет (структуру) матрицы [math]A[/math], задаваемый как множество списков смежности ассоциированного с [math]A[/math] графа. Если алгоритм, реализующий какую-либо операцию над разреженными матрицами, разбит на этапы символической обработки, на котором определяется портрет результирующей матрицы, и численной обработки, на котором определяются значения элементов результирующей матрицы, то массивы [math]IA[/math] и [math]JA[/math] заполняются на первом этапе, а массив [math]AN[/math] - на втором.
1.1.1.2 Формат RR(C)U
Рассмотрим теперь формат RR(C)U.
Сокращенное название данного формата происходит от английского словосочетания "Row - wise Representation Complete and Unordered" (строчное представление, полное, но неупорядоченное). Формат RR(C)U отличается от RR(C)O тем, что в данном случае соблюдается упорядоченность строк, но внутри каждой строки элементы исходных матриц могут храниться в произвольном порядке. Для матрицы [math]A[/math] нашего примера вполне можно было бы использовать и строчное представление, полное, но неупорядоченное такое:
IA = [ 1 2 4 4 5 6 ] JA = [ 4 3 1 2 3 ] AN = [ 2 1 3 6 4 ]
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными (например, операции вставки и удаления новых коэффициентов), а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными.
1.1.1.3 Замечания
Несколько замечаний по поводу рассмотренных форматов представления:
- Очевидно, что представление матрицы в формате RR(C)O так же является и представлением в формате RR(C)U, но не наоборот.
- Из представления матрицы в формате RR(C) нельзя получить информацию о точном количестве столбцов исходной матрицы.
- Целесообразно (в вопросе экономии памяти) использовать представления RR(C) в случае, если матрица содержит значительное число нулевых элементов.
1.1.2 Умножение разреженной матрицы на вектор
Важным приложением этих алгоритмов является вычисление векторов Ланцоша, необходимое при итерационном решении линейных уравнений методом сопряженных градиентов, а также при вычислении собственных значений и собственных векторов матрицы. Достоинство этих процедур, с вычислительной точки зрения, состоит в том, что единственная требуемая матричная операция - это повторное умножение матрицы на последовательность заполненных векторов; сама матрица не меняется.
Мы рассмотрим умножение разреженной матрицы общего вида, хранимой в форме RR(C)U посредством массивов [math]IA[/math], [math]JA[/math], [math]AN[/math] на заполненный вектор-столбец.
1.2 Математическое описание алгоритма
Исходные данные: разреженная матрица общего вида [math]A[/math] с элементами [math]a_{ij}[/math] ([math]i = 1,...,n[/math] и [math]j = 1,...,m[/math]). Заполненный вектор-столбец [math]b[/math] с элементами [math]b_{j}[/math] ([math]j =1,...,m[/math]).
Вычисляемые данные: заполненный вектор-столбец [math]c[/math] с элементами [math]c_{i}[/math] ([math]i = 1,...,n[/math]).
Формулы метода:
[math]c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}[/math],
где [math]l_{i}[/math] - количество ненулевых элементов строки [math]i[/math] матрицы [math]A[/math], [math]j(k)[/math] - индекс [math]k[/math]-го ненулевого элемента матрицы [math]A[/math].
1.3 Вычислительное ядро алгоритма
Вычислительное ядро последовательной версии умножения разреженной матрицы на вектор можно составить из множественных (всего их [math]n[/math]) вычислений скалярных произведений строк матрицы:
[math]c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}[/math].
1.4 Макроструктура алгоритма
Как записано и в описании ядра алгоритма, основную часть метода составляют множественные (всего [math]n[/math]) вычисления сумм:
[math]c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}[/math],
которые могут вычисляться в произвольном порядке.
1.5 Схема реализации последовательного алгоритма
Далее предполагаем, что разреженная матрица общего вида [math]A[/math] хранится в форме RR(C)U посредством массивов [math]IA[/math], [math]JA[/math], [math]AN[/math]. Последовательность исполнения метода следующая:
Выполнять для [math]i[/math] от [math]1[/math] до [math]n[/math]
- [math]c_{i} = 0[/math]
- [math]IAA = IA[i][/math]
- [math]IAB = IA[i + 1] - 1[/math]
- [math]c_{i} = \sum\limits_{k = IAA}^{IAB} AN[k]b_{JA[k]}[/math].
После этого (если [math]i \le n[/math]) происходит переход к шагу 1 с бо́льшим [math]i[/math].
1.6 Последовательная сложность алгоритма
Для умножения разреженной матрицы общего вида, хранимой в форме RR(C)U, размером [math]n \times m[/math] на заполненный вектор [math]m \times 1[/math] в последовательном (наиболее быстром) варианте требуется:
- [math]O(l)[/math] сложений,
- [math]O(l)[/math] умножений.
Умножения и сложения составляют основную часть алгоритма.
При классификации по последовательной сложности, таким образом, алгоритм умножения разреженной матрицы на вектор относится к алгоритмам [math]O(l)[/math].
1.7 Информационный граф
Опишем граф алгоритма[2][3][4] как аналитически, так и в виде рисунка.
Граф алгоритма состоит из двух групп вершин, расположенных в целочисленных узлах двух областей одной размерности.
Первая группа вершин расположена в двумерной области, соответствующая ей операция вычисляет функцию [math]a \cdot b[/math]. Естественно введённые координаты области таковы:
- [math]i[/math] — меняется в диапазоне от [math]1[/math] до [math]n[/math], принимая все целочисленные значения;
- [math]j[/math] — меняется в диапазоне от [math]1[/math] до [math]k[/math], принимая все целочисленные значения,
где [math]k = k(i) = IA[i+1]-IA[i][/math].
Аргументы операции следующие:
- [math]a[/math] - элемент входных данных, а именно [math]AN[IA_{i}+j-1][/math].
- [math]b[/math] - элемент входных данных, а именно [math]b[IA_{i}+j-1][/math].
Результат срабатывания операции является промежуточным данным алгоритма.
Вторая группа вершин расположена в двумерной области, соответствующая ей операция [math]a + b[/math]. Естественно введённые координаты области таковы:
- [math]i[/math] - меняется в диапазоне от [math]1[/math] до [math]n[/math], принимая все целочисленные значения;
- [math]j[/math] - меняется в диапазоне от [math]1[/math] до [math]k-1[/math], принимая все целочисленные значения,
где [math]k = k(i) = IA[i+1]-IA[i][/math].
Аргументы операции следующие:
- [math]a[/math]:
- при [math]j = 1[/math] - результат срабатывания операции, соответствующей вершине из первой группы, с координатами [math](i, j)[/math];
- при [math]j \gt 1[/math] - результат срабатывания операции, соответствующей вершине из второй группы, с координатами [math](i, j - 1)[/math];
- [math]b[/math] — результат срабатывания операции, соответствующей вершине из первой группы, с координатами [math](i, j + 1)[/math].
Результат срабатывания операции:
- при [math]j \lt k - 1[/math] является промежуточным данным алгоритма;
- при [math]j = k - 1[/math] является выходным данным [math]c_{i}[/math].
Описанный граф можно посмотреть на рис.1. Здесь вершины первой группы обозначены красным цветом и отмечены знаком умножения, вершины второй - зелёным цветом и знаком сложения. Вершины, соответствующие входным данным обозначены белым цветом и выходным - синим.
1.8 Ресурс параллелизма алгоритма
Для умножения разреженной матрицы общего вида, хранимой в форме RR(C)U, размерности [math]n \times m[/math] на заполненный вектор [math]m \times 1[/math] в параллельном варианте требуется последовательно выполнить следующие ярусы:
- Не более чем [math]m[/math] сложений и умножений ([math]n[/math] вычислений в каждом из ярусов)
При классификации по высоте ЯПФ, таким образом, алгоритм умножения разреженной матрицы на вектор относится к алгоритмам со сложностью [math]O(m)[/math]. При классификации по ширине ЯПФ его сложность будет [math]O(n)[/math].
1.9 Входные и выходные данные алгоритма
Входные данные: разреженная матрица общего вида [math]A[/math] размерности [math]n \times m[/math], хранимая в форме RR(C)U посредством массивов [math]IA[/math], [math]JA[/math], [math]AN[/math]. Заполненный вектор-столбец [math]b[/math] с элементами [math]b_{j}[/math] размерности [math]m \times 1[/math].
Объём входных данных: [math]2l+m+n+1[/math], где [math]l[/math] - количество ненулевых элементов в матрице [math]A[/math].
Выходные данные: заполненный вектор-столбец [math]c[/math] с элементами [math]c_{i}[/math] ([math]i = 1,...,n[/math]).
Объём выходных данных: [math]n[/math].
1.10 Свойства алгоритма
Соотношение последовательной и параллельной сложности в случае неограниченных ресурсов, как хорошо видно, является линейной.
При этом вычислительная мощность алгоритма, как отношение числа операций к суммарному объему входных и выходных данных – константа.
Пусть [math]l[/math] - количество ненулевых элементов матрицы [math]A[/math] размерности [math]n \times m[/math]. Пусть [math]x[/math] - объем памяти, используемый для хранения значения элемента матрицы, [math]y[/math] - объём памяти, используемый для хранения номера столбца или строки. В таком случае для хранения матрицы в стандартном представлении нам потребуется объем памяти, равный [math]x \cdot n \cdot m[/math], для хранения в формате RR(C) - [math]y(n + 1) + (x + y)l[/math]. Таким образом хранение в формате RR(C) не является эффективным (в вопросе используемой памяти) для матриц, в которых [math]l \gt \frac{xnm - y(n+1)}{x+y}[/math].
2 Программная реализация алгоритма
2.1 Особенности реализации последовательного алгоритма
2.2 Локальность данных и вычислений
2.3 Возможные способы и особенности параллельной реализации алгоритма
2.4 Масштабируемость алгоритма и его реализации
Параллельная реализация алгоритма заключается в следующем:
- Каждый процессор считывает входные данные из файла, зависящего от его номера. Данные имеют вид: вектор [math]b[/math] и набор определённых строк матрицы [math]A[/math] в формате RR(C)U в зависимости от номера процессора и их числа;
- Процессоры проводят вычисления значений вектора [math]c[/math] в зависимости от имеющихся данных;
- Все процессоры отправляют результаты вычисления процессору, выбранному главным, и главный процессор выводит результат вычислений.
Текст использованной в экспериментах реализации (язык С++):
#include <mpi.h>
#include <memory>
#include <iostream>
#include <vector>
#include <sstream>
#include <fstream>
#include <string>
#include <iterator>
typedef std::shared_ptr<std::vector<std::vector<int> > > MyData;
MyData ReadData(const std::string& data_path) {
std::ifstream file_stream;
MyData retval(new std::vector<std::vector<int> >());
file_stream.open(data_path);
while (!file_stream.eof()) {
std::string str;
std::getline(file_stream, str);
std::istringstream iss(str);
auto temp_1 = (std::istream_iterator<std::string>(iss));
auto temp_2 = (std::istream_iterator<std::string>());
std::vector<std::string> coordinates{ temp_1, temp_2};
retval->push_back(std::vector<int>());
for (auto& e : coordinates) {
retval->back().push_back(std::stoi(e));
}
}
return retval;
}
// input: vector b, vector ia, vector ja, vector an
int main(int argc, char** argv){
MPI_Init(&argc, &argv);
int pid, np;
MPI_Status *s = new MPI_Status;
MPI_Comm_rank(MPI_COMM_WORLD, &pid);
MPI_Comm_size(MPI_COMM_WORLD, &np);
if (argc == 2){
int n;
int indx = 0;
int stp;
double strttime = MPI_Wtime();
std::vector<int> b;
std::vector<int> ia;
std::vector<int> ja;
std::vector<int> an;
auto data = ReadData("data_" + std::to_string(static_cast<long long>(pid)) + ".txt");
b = (*data)[0];
ia = (*data)[1];
ja = (*data)[2];
an = (*data)[3];
n = (*data)[0].size();
std::vector<int> c;
stp = n / np + ((n % np > pid) ? 1 : 0);
if (pid == 0){
c.resize(n, 0);
}
else{
c.resize(stp, 0);
}
for (int i = 0; i < stp; i++){
int strt = ia[i] - ia[0];
int lstr = ia[i + 1] - ia[i];
for (int j = 0; j < lstr; j++){
c[i] += an[strt + j] * b[ja[strt + j]];
}
}
if (pid == 0){
for (int i = 1; i < np; i++){
indx += n / np + ((n % np > i - 1) ? 1 : 0);
stp = n / np + ((n % np > i) ? 1 : 0);
MPI_Recv(&c[indx], stp, MPI_INT, i, 0, MPI_COMM_WORLD, s);
}
}
else{
MPI_Send(&c[0], stp, MPI_INT, 0, 0, MPI_COMM_WORLD);
}
if (pid == 0){
for (int e : c) {
std::cout << e << "\n";
}
double endtime = MPI_Wtime();
std::cout << "Success! Time = " << endtime - strttime << "\n";
}
}
MPI_Finalize();
return 0;
}
Для формирования входных данных использовался следующий алгоритм (язык Python):
from scipy import sparse as sp
from numpy.random import randint
from os import mkdir
from os.path import join as jn
n = 10000
np = [1, 2, 4, 8, 16, 32, 64, 128]
a = sp.csr_matrix(randint(0, 2, (n, n)))
b = randint(1, 5, n)
i = 0
for j in np:
mkdir('data_' + str(j))
indx = 0
for i in range(0, j):
stp = n / j + (1 if (n % j > i) else 0)
ael = a.indptr[indx + stp] - a.indptr[indx]
strt = a.indptr[indx]
with open(jn('data_' + str(j), 'data_' + str(i) + '.txt'), 'w') as fout:
for e in b:
fout.write('{0} '.format(e))
fout.write('\n')
s = a.indptr[indx:indx + stp + 1]
for e in s:
fout.write('{0} '.format(e))
fout.write('\n')
s = a.indices[strt:strt + ael]
for e in s:
fout.write('{0} '.format(e))
fout.write('\n')
s = a.data[strt:strt + ael]
for e in s:
fout.write('{0} '.format(e))
fout.write('\n')
indx = indx + n / j + (1 if (n % j > i) else 0)
Проведём исследование масштабируемости параллельной реализации алгоритма умножения разреженной матрицы на вектор. Исследование проводилось на суперкомпьютере "Ломоносов"[5] Суперкомпьютерного комплекса Московского университета. Входные данные формировались на персональном компьютере в силу того, что интерпретатор на суперкомпьютере "Ломоносов" не удовлетворял нужным требованиям, и потом отправлялись на суперкомпьютер для последующей обработки.
Программа была собрана на узле компиляции суперкомпьютера "Ломоносов", с помощью компилятора intel/15.0.090 и компилятора openmpi/1.6.5-icc, на языке С++11.
Сборка производилась с помощью CMakeLists.txt, его содержание прилагается:
cmake_minimum_required(VERSION 2.8) project(task1) set(CMAKE_CXX_STANDARD 11) set(CXX_STANDARD_REQUIRED) set(CMAKE_C_COMPILER mpicc) set(CMAKE_CXX_COMPILER mpicxx) find_package(MPI REQUIRED) include_directories(${MPI_INCLUDE_PATH}) SET(CMAKE_CXX_COMPILER mpicc) if(MPI_COMPILE_FLAGS) set_target_properties(task1 PROPERTIES COMPILE_FLAGS "${MPI_COMPILE_FLAGS}") endif() set(CMAKE_CXX_FLAGS "-std=c++11 -fpermissive " ${MPI_COMPILE_FLAGS}) set(SOURCE_EXE task1.cpp) add_executable(task1 ${SOURCE_EXE}) target_link_libraries(task1)
Запуск производился на вычислительных узлах суперкомпьютера "Ломоносов".
Набор и границы значений изменяемых параметров запуска реализации алгоритма:
- число процессоров [1 : 128] по степеням 2;
- размер матрицы [1000 : 10000] с шагом 1000.
Распределение ненулевых и нулевых коэффициентов: 1:1. Однако стоит обратить внимание, что матрица генерировалась случайным образом, а значит, это лишь распределение, а не точное значение.
Можно заметить, что при небольшом количестве входных данных, и большом количестве процессоров эффективность распараллеливания падает до нуля, поскольку время вычисления элементов вектора [math]c[/math] становится несоизмеримо малым по сравнению с временем, затраченным на пересылку в пункте (3).
2.5 Динамические характеристики и эффективность реализации алгоритма
2.6 Выводы для классов архитектур
2.7 Существующие реализации алгоритма
Существующие реализации:
SciPy [1],
MatLab [2],
Intel MKL [3].
3 Литература
- ↑ С.Писсанецки. Технология разреженных матриц. - М.: Мир, 1988.
- ↑ Воеводин В.В. Математические основы параллельных вычислений// М.: Изд. Моск. ун-та, 1991. 345 с.
- ↑ Воеводин В.В., Воеводин Вл.В. Параллельные вычисления. – СПб.: БХВ - Петербург, 2002. – 608 с.
- ↑ Фролов А.В.. Принципы построения и описание языка Сигма. Препринт ОВМ АН N 236. М.: ОВМ АН СССР, 1989.
- ↑ Воеводин Вл., Жуматий С., Соболев С., Антонов А., Брызгалов П., Никитенко Д., Стефанов К., Воеводин Вад. Практика суперкомпьютера «Ломоносов» // Открытые системы, 2012, N 7, С. 36-39.