Уровень алгоритма

Участник:Blizn/Хранение ненулевых элементов разреженной матрицы. Умножение разреженной матрицы на вектор.: различия между версиями

Материал из Алговики
Перейти к навигации Перейти к поиску
м (Blizn переименовал страницу [[Участник:Иванов Иван/Хранение ненулевых элементов разреженной матрицы. Умножение разреженной матрицы на век…)
 
(не показана 41 промежуточная версия 3 участников)
Строка 1: Строка 1:
ыыы
+
{{Assignment|ASA|Evgeny Mortikov}}
 +
 
 +
 
 +
{{algorithm
 +
| name              = Умножение разреженной матрицы на вектор
 +
| serial_complexity = <math>O(l)</math>
 +
| pf_height        = <math>O(m)</math>
 +
| pf_width          = <math>O(n)</math>
 +
| input_data        = <math>2l+m+n+1</math>
 +
| output_data      = <math>n</math>
 +
}}
 +
Выполнила: [[Участник:Blizn|И.В. Близнякова]] (611 группа).
 +
 
 +
= Свойства и структура алгоритма =
 +
 
 +
== Общее описание алгоритма<ref>С.Писсанецки. Технология разреженных матриц. - М.: Мир, 1988.</ref> ==
 +
'''Разрежённая матрица''' — это матрица с преимущественно нулевыми элементами. В противном случае, если бо́льшая часть элементов матрицы ненулевые, матрица считается '''''плотной'''''.
 +
 
 +
Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разрежённой. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов:
 +
* есть <math>O(n)</math>. Такое определение подходит разве что для теоретического анализа асимптотических свойств матричных алгоритмов;
 +
* в каждой строке не превышает 10 в типичном случае;
 +
* ограничено <math>n^{1+\gamma}</math>, где <math>\gamma < 1</math>.
 +
* таково, что для данного алгоритма и вычислительной системы имеет смысл извлекать выгоду из наличия в ней нулей.
 +
 
 +
=== Хранение разреженной матрицы ===
 +
==== Формат 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> - на втором.
 +
 
 +
==== Формат 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 ]
 +
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными (например, операции вставки и удаления новых коэффициентов), а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными.
 +
 
 +
==== Замечания ====
 +
 
 +
Несколько замечаний по поводу рассмотренных форматов представления:
 +
# Очевидно, что представление матрицы в формате '''RR(C)O''' так же является и представлением в формате '''RR(C)U''', но не наоборот.
 +
# Из представления матрицы в формате '''RR(C)''' нельзя получить информацию о точном количестве столбцов исходной матрицы.
 +
# Целесообразно (в вопросе экономии памяти) использовать представления '''RR(C)''' в случае, если матрица содержит значительное число нулевых элементов.
 +
 
 +
=== Умножение разреженной матрицы на вектор ===
 +
 
 +
Важным приложением этих алгоритмов является вычисление векторов Ланцоша, необходимое при итерационном решении линейных уравнений методом сопряженных градиентов, а также при вычислении собственных значений и собственных векторов матрицы. Достоинство этих процедур, с вычислительной точки зрения, состоит в том, что единственная требуемая матричная операция - это повторное умножение матрицы на последовательность заполненных векторов; сама матрица не меняется.
 +
 
 +
Мы рассмотрим умножение разреженной матрицы общего вида, хранимой в форме '''RR(C)U''' посредством массивов <math>IA</math>, <math>JA</math>, <math>AN</math> на заполненный вектор-столбец.
 +
 
 +
== Математическое описание алгоритма ==
 +
 
 +
Исходные данные: разреженная матрица общего вида <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>.
 +
 
 +
== Вычислительное ядро алгоритма ==
 +
Вычислительное ядро последовательной версии умножения разреженной матрицы на вектор можно составить из множественных (всего их <math>n</math>) вычислений скалярных произведений строк матрицы:
 +
 
 +
<math>c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}</math>.
 +
 
 +
== Макроструктура алгоритма ==
 +
 
 +
Как записано и в [[#Вычислительное ядро алгоритма|описании ядра алгоритма]], основную часть метода составляют множественные (всего <math>n</math>) вычисления сумм:
 +
 
 +
<math>c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}</math>,
 +
 
 +
которые могут вычисляться в произвольном порядке.
 +
 
 +
== Схема реализации последовательного алгоритма ==
 +
 
 +
Далее предполагаем, что разреженная матрица общего вида <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>.
 +
 
 +
== Последовательная сложность алгоритма ==
 +
 
 +
Для умножения разреженной матрицы общего вида, хранимой в форме '''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>.
 +
 
 +
== Информационный граф ==
 +
 
 +
Опишем [[глоссарий#Граф алгоритма|граф алгоритма]]<ref>Воеводин В.В.  Математические основы параллельных вычислений// М.: Изд. Моск. ун-та, 1991. 345 с.</ref><ref>Воеводин В.В., Воеводин Вл.В. Параллельные вычисления. – СПб.: БХВ - Петербург, 2002. – 608 с.</ref><ref>Фролов А.В.. Принципы построения и описание языка Сигма. Препринт ОВМ АН N 236. М.: ОВМ АН СССР, 1989.</ref> как аналитически, так и в виде рисунка.
 +
 
 +
Граф алгоритма состоит из двух групп вершин, расположенных в целочисленных узлах двух областей одной размерности.
 +
 
 +
Первая группа вершин расположена в двумерной области, соответствующая ей операция вычисляет функцию <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 > 1</math> - результат срабатывания операции, соответствующей вершине из второй группы, с координатами <math>(i, j - 1)</math>;
 +
* <math>b</math> — результат срабатывания операции, соответствующей вершине из первой группы, с координатами <math>(i, j + 1)</math>.
 +
 
 +
Результат срабатывания операции:
 +
* при <math>j < k - 1</math> является ''промежуточным данным'' алгоритма;
 +
* при <math>j = k - 1</math> является ''выходным данным'' <math>c_{i}</math>.
 +
 
 +
Описанный граф можно посмотреть на рис.1. Здесь вершины первой группы обозначены красным цветом и отмечены знаком умножения, вершины второй - зелёным цветом и знаком сложения. Вершины, соответствующие входным данным обозначены белым цветом и выходным - синим.
 +
[[file:Bliznalg.png|thumb|center|1000px|Рисунок 1. Граф алгоритма умножения разреженной матрицы на вектор]]
 +
 
 +
== Ресурс параллелизма алгоритма ==
 +
 
 +
Для умножения разреженной матрицы общего вида, хранимой в форме '''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>.
 +
 
 +
== Входные и выходные данные алгоритма ==
 +
 
 +
'''Входные данные''': разреженная матрица общего вида <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>.
 +
 
 +
== Свойства алгоритма ==
 +
 
 +
Соотношение последовательной и параллельной сложности в случае неограниченных ресурсов, как хорошо видно, является ''линейной''.
 +
 
 +
При этом вычислительная мощность алгоритма, как отношение числа операций к суммарному объему входных и выходных данных – ''константа''.
 +
 
 +
Пусть <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 > \frac{xnm - y(n+1)}{x+y}</math>.
 +
 
 +
= Программная реализация алгоритма =
 +
 
 +
== Особенности реализации последовательного алгоритма ==
 +
 
 +
== Локальность данных и вычислений ==
 +
 
 +
== Возможные способы и особенности параллельной реализации алгоритма ==
 +
 
 +
== Масштабируемость алгоритма и его реализации ==
 +
 
 +
Параллельная реализация алгоритма заключается в следующем:
 +
 
 +
# Каждый процессор считывает входные данные из файла, зависящего от его номера. Данные имеют вид: вектор <math>b</math> и набор определённых строк матрицы <math>A</math> в формате '''RR(C)U''' в зависимости от номера процессора и их числа;
 +
# Процессоры проводят вычисления значений вектора <math>c</math> в зависимости от имеющихся данных;
 +
# Все процессоры отправляют результаты вычисления процессору, выбранному главным, и главный процессор выводит результат вычислений.
 +
 
 +
<div class="mw-collapsible mw-collapsed" style="width:100%">
 +
Текст использованной в экспериментах реализации (язык ''С++''):
 +
<div class="mw-collapsible-content">
 +
<source lang="c++">
 +
#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;
 +
}
 +
</source>
 +
</div></div>
 +
 
 +
<div class="mw-collapsible mw-collapsed" style="width:100%">
 +
Для формирования входных данных использовался следующий алгоритм (язык ''Python''):
 +
<div class="mw-collapsible-content">
 +
<source lang="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)
 +
</source>
 +
</div></div>
 +
 
 +
Проведём исследование масштабируемости параллельной реализации алгоритма умножения разреженной матрицы на вектор. Исследование проводилось на суперкомпьютере "Ломоносов"<ref name="Lom">Воеводин Вл., Жуматий С., Соболев С., Антонов А., Брызгалов П., Никитенко Д., Стефанов К., Воеводин Вад. Практика суперкомпьютера «Ломоносов» // Открытые системы, 2012, N 7, С. 36-39.</ref> [http://parallel.ru/cluster Суперкомпьютерного комплекса Московского университета]. Входные данные формировались на персональном компьютере в силу того, что интерпретатор на суперкомпьютере "Ломоносов" не удовлетворял нужным требованиям, и потом отправлялись на суперкомпьютер для последующей обработки.
 +
 
 +
Программа была собрана на узле компиляции суперкомпьютера "Ломоносов", с помощью компилятора ''intel/15.0.090'' и компилятора ''openmpi/1.6.5-icc'', на языке ''С++11''. 
 +
 
 +
<div class="mw-collapsible mw-collapsed" style="width:100%">
 +
Сборка производилась с помощью ''CMakeLists.txt'', его содержание прилагается:
 +
<div class="mw-collapsible-content">
 +
<pre>
 +
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)
 +
</pre>
 +
</div></div>
 +
 
 +
Запуск производился на вычислительных узлах суперкомпьютера "Ломоносов".
 +
 
 +
Набор и границы значений изменяемых параметров запуска реализации алгоритма:
 +
 
 +
* число процессоров [1 : 128] по степеням 2;
 +
* размер матрицы [1000 : 10000] с шагом 1000.
 +
 
 +
Распределение ненулевых и нулевых коэффициентов: 1:1. Однако стоит обратить внимание, что матрица генерировалась случайным образом, а значит, это лишь распределение, а не точное значение.
 +
 
 +
[[file:Blizntime.png|thumb|center|1000px|Рисунок 2. Время работы программы в зависимости от количества процессоров и размера матрицы]]
 +
 
 +
Можно заметить, что при небольшом количестве входных данных, и большом количестве процессоров эффективность распараллеливания падает до нуля, поскольку время вычисления элементов вектора <math>c</math> становится несоизмеримо малым по сравнению с временем, затраченным на пересылку в пункте (3).
 +
 
 +
[[file:Efficiency(paral).png|thumb|center|1000px|Рисунок 4. Эффективность распараллеливания в зависимости от количества процессоров и размера матрицы]]
 +
 
 +
== Динамические характеристики и эффективность реализации алгоритма ==
 +
 
 +
== Выводы для классов архитектур ==
 +
 
 +
== Существующие реализации алгоритма ==
 +
 
 +
Существующие реализации:
 +
 
 +
SciPy [https://docs.scipy.org/doc/scipy-0.18.1/reference/index.html],
 +
 
 +
MatLab [https://www.mathworks.com/help/matlab/math/sparse-matrix-operations.html],
 +
 
 +
Intel MKL [https://software.intel.com/en-us/articles/intel-math-kernel-library-inspector-executor-sparse-blas-routines].
 +
 
 +
= Литература =

Текущая версия на 06:13, 3 декабря 2016

Symbol confirmed.svgЭта работа успешно выполнена
Преподавателю: в основное пространство, в подстраницу

Данное задание было проверено и зачтено.
Проверено Evgeny Mortikov и ASA.




Умножение разреженной матрицы на вектор
Последовательный алгоритм
Последовательная сложность O(l)
Объём входных данных 2l+m+n+1
Объём выходных данных n
Параллельный алгоритм
Высота ярусно-параллельной формы O(m)
Ширина ярусно-параллельной формы O(n)

Выполнила: И.В. Близнякова (611 группа).

Содержание

1 Свойства и структура алгоритма

1.1 Общее описание алгоритма[1]

Разрежённая матрица — это матрица с преимущественно нулевыми элементами. В противном случае, если бо́льшая часть элементов матрицы ненулевые, матрица считается плотной.

Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разрежённой. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов:

  • есть O(n). Такое определение подходит разве что для теоретического анализа асимптотических свойств матричных алгоритмов;
  • в каждой строке не превышает 10 в типичном случае;
  • ограничено n^{1+\gamma}, где \gamma \lt 1.
  • таково, что для данного алгоритма и вычислительной системы имеет смысл извлекать выгоду из наличия в ней нулей.

1.1.1 Хранение разреженной матрицы

1.1.1.1 Формат RR(C)O

Рассмотрим сначала формат RR(C)O. Сокращенное название данного формата происходит от английского словосочетания "Row - wise Representation Complete and Ordered" (строчное представление, полное и упорядоченное). В данном формате вместо одного двумерного массива, используются три одномерных. Значения ненулевых элементов матрицы и соответствующие им столбцовые индексы хранятся в этом формате по строкам в двух массивах AN и JA. Массив указателей IA, используется для ссылки на компоненты массивов AN и JA, с которых начинается описание очередной строки. Последняя компонента массива IA содержит указатель первой свободной компоненты в массивах AN и JA, т.е. равна числу ненулевых элементов матрицы, увеличенному на единицу. Здесь уместно привести пример.

Рассмотрим матрицу A:

\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},

тогда ее представление в формате RR(C)O будет иметь вид:

  IA = [ 1 2 4 4 5 6 ]
  JA = [ 4 1 3 2 3 ]
  AN = [ 2 1 3 6 4 ]

Т.е. массив AN содержит все не нулевые элементы исходной матрицы A, массив JA номер столбца в котором находится соответствующий элемент из AN и наконец массив IA содержит номер с которого начинается описание элементов в массивах JA и AN. Таким образом информация об элементах 2-ой строки матрицы хранится в элементах с IA[2] = 2 по IA[3] - 1 = 3 включительно массивов JA и AN. Можно обратить внимание, что IA[3] = IA[4] = 4, а это означает, что 3-я строка матрицы A нулевая.

В общем случае описание r-й строки матрицы A хранится в компонентах с IA[r] до IA[r + 1] - 1 включительно массивов AN и JA. Если IA[r + 1] = IA[r], то это означает, что r-я строка нулевая. Количество элементов в массиве IA на единицу больше, чем число строк исходной матрицы, а количество элементов в массивах JA и AN равно числу ненулевых элементов исходной матрицы.

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

Массивы IA и JA представляют портрет (структуру) матрицы A, задаваемый как множество списков смежности ассоциированного с A графа. Если алгоритм, реализующий какую-либо операцию над разреженными матрицами, разбит на этапы символической обработки, на котором определяется портрет результирующей матрицы, и численной обработки, на котором определяются значения элементов результирующей матрицы, то массивы IA и JA заполняются на первом этапе, а массив AN - на втором.

1.1.1.2 Формат RR(C)U

Рассмотрим теперь формат RR(C)U.

Сокращенное название данного формата происходит от английского словосочетания "Row - wise Representation Complete and Unordered" (строчное представление, полное, но неупорядоченное). Формат RR(C)U отличается от RR(C)O тем, что в данном случае соблюдается упорядоченность строк, но внутри каждой строки элементы исходных матриц могут храниться в произвольном порядке. Для матрицы A нашего примера вполне можно было бы использовать и строчное представление, полное, но неупорядоченное такое:

  IA = [ 1 2 4 4 5 6 ]
  JA = [ 4 3 1 2 3 ]
  AN = [ 2 1 3 6 4 ]

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

1.1.1.3 Замечания

Несколько замечаний по поводу рассмотренных форматов представления:

  1. Очевидно, что представление матрицы в формате RR(C)O так же является и представлением в формате RR(C)U, но не наоборот.
  2. Из представления матрицы в формате RR(C) нельзя получить информацию о точном количестве столбцов исходной матрицы.
  3. Целесообразно (в вопросе экономии памяти) использовать представления RR(C) в случае, если матрица содержит значительное число нулевых элементов.

1.1.2 Умножение разреженной матрицы на вектор

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

Мы рассмотрим умножение разреженной матрицы общего вида, хранимой в форме RR(C)U посредством массивов IA, JA, AN на заполненный вектор-столбец.

1.2 Математическое описание алгоритма

Исходные данные: разреженная матрица общего вида A с элементами a_{ij} (i = 1,...,n и j = 1,...,m). Заполненный вектор-столбец b с элементами b_{j} (j =1,...,m).

Вычисляемые данные: заполненный вектор-столбец c с элементами c_{i} (i = 1,...,n).

Формулы метода:

c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)},

где l_{i} - количество ненулевых элементов строки i матрицы A, j(k) - индекс k-го ненулевого элемента матрицы A.

1.3 Вычислительное ядро алгоритма

Вычислительное ядро последовательной версии умножения разреженной матрицы на вектор можно составить из множественных (всего их n) вычислений скалярных произведений строк матрицы:

c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}.

1.4 Макроструктура алгоритма

Как записано и в описании ядра алгоритма, основную часть метода составляют множественные (всего n) вычисления сумм:

c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)},

которые могут вычисляться в произвольном порядке.

1.5 Схема реализации последовательного алгоритма

Далее предполагаем, что разреженная матрица общего вида A хранится в форме RR(C)U посредством массивов IA, JA, AN. Последовательность исполнения метода следующая:

Выполнять для i от 1 до n

  1. c_{i} = 0
  2. IAA = IA[i]
  3. IAB = IA[i + 1] - 1
  4. c_{i} = \sum\limits_{k = IAA}^{IAB} AN[k]b_{JA[k]}.

После этого (если i \le n) происходит переход к шагу 1 с бо́льшим i.

1.6 Последовательная сложность алгоритма

Для умножения разреженной матрицы общего вида, хранимой в форме RR(C)U, размером n \times m на заполненный вектор m \times 1 в последовательном (наиболее быстром) варианте требуется:

  • O(l) сложений,
  • O(l) умножений.

Умножения и сложения составляют основную часть алгоритма.

При классификации по последовательной сложности, таким образом, алгоритм умножения разреженной матрицы на вектор относится к алгоритмам O(l).

1.7 Информационный граф

Опишем граф алгоритма[2][3][4] как аналитически, так и в виде рисунка.

Граф алгоритма состоит из двух групп вершин, расположенных в целочисленных узлах двух областей одной размерности.

Первая группа вершин расположена в двумерной области, соответствующая ей операция вычисляет функцию a \cdot b. Естественно введённые координаты области таковы:

  • i — меняется в диапазоне от 1 до n, принимая все целочисленные значения;
  • j — меняется в диапазоне от 1 до k, принимая все целочисленные значения,

где k = k(i) = IA[i+1]-IA[i].

Аргументы операции следующие:

  • a - элемент входных данных, а именно AN[IA_{i}+j-1].
  • b - элемент входных данных, а именно b[IA_{i}+j-1].

Результат срабатывания операции является промежуточным данным алгоритма.

Вторая группа вершин расположена в двумерной области, соответствующая ей операция a + b. Естественно введённые координаты области таковы:

  • i - меняется в диапазоне от 1 до n, принимая все целочисленные значения;
  • j - меняется в диапазоне от 1 до k-1, принимая все целочисленные значения,

где k = k(i) = IA[i+1]-IA[i].

Аргументы операции следующие:

  • a:
    • при j = 1 - результат срабатывания операции, соответствующей вершине из первой группы, с координатами (i, j);
    • при j \gt 1 - результат срабатывания операции, соответствующей вершине из второй группы, с координатами (i, j - 1);
  • b — результат срабатывания операции, соответствующей вершине из первой группы, с координатами (i, j + 1).

Результат срабатывания операции:

  • при j \lt k - 1 является промежуточным данным алгоритма;
  • при j = k - 1 является выходным данным c_{i}.

Описанный граф можно посмотреть на рис.1. Здесь вершины первой группы обозначены красным цветом и отмечены знаком умножения, вершины второй - зелёным цветом и знаком сложения. Вершины, соответствующие входным данным обозначены белым цветом и выходным - синим.

Рисунок 1. Граф алгоритма умножения разреженной матрицы на вектор

1.8 Ресурс параллелизма алгоритма

Для умножения разреженной матрицы общего вида, хранимой в форме RR(C)U, размерности n \times m на заполненный вектор m \times 1 в параллельном варианте требуется последовательно выполнить следующие ярусы:

  • Не более чем m сложений и умножений (n вычислений в каждом из ярусов)

При классификации по высоте ЯПФ, таким образом, алгоритм умножения разреженной матрицы на вектор относится к алгоритмам со сложностью O(m). При классификации по ширине ЯПФ его сложность будет O(n).

1.9 Входные и выходные данные алгоритма

Входные данные: разреженная матрица общего вида A размерности n \times m, хранимая в форме RR(C)U посредством массивов IA, JA, AN. Заполненный вектор-столбец b с элементами b_{j} размерности m \times 1.

Объём входных данных: 2l+m+n+1, где l - количество ненулевых элементов в матрице A.

Выходные данные: заполненный вектор-столбец c с элементами c_{i} (i = 1,...,n).

Объём выходных данных: n.

1.10 Свойства алгоритма

Соотношение последовательной и параллельной сложности в случае неограниченных ресурсов, как хорошо видно, является линейной.

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

Пусть l - количество ненулевых элементов матрицы A размерности n \times m. Пусть x - объем памяти, используемый для хранения значения элемента матрицы, y - объём памяти, используемый для хранения номера столбца или строки. В таком случае для хранения матрицы в стандартном представлении нам потребуется объем памяти, равный x \cdot n \cdot m, для хранения в формате RR(C) - y(n + 1) + (x + y)l. Таким образом хранение в формате RR(C) не является эффективным (в вопросе используемой памяти) для матриц, в которых l \gt \frac{xnm - y(n+1)}{x+y}.

2 Программная реализация алгоритма

2.1 Особенности реализации последовательного алгоритма

2.2 Локальность данных и вычислений

2.3 Возможные способы и особенности параллельной реализации алгоритма

2.4 Масштабируемость алгоритма и его реализации

Параллельная реализация алгоритма заключается в следующем:

  1. Каждый процессор считывает входные данные из файла, зависящего от его номера. Данные имеют вид: вектор b и набор определённых строк матрицы A в формате RR(C)U в зависимости от номера процессора и их числа;
  2. Процессоры проводят вычисления значений вектора c в зависимости от имеющихся данных;
  3. Все процессоры отправляют результаты вычисления процессору, выбранному главным, и главный процессор выводит результат вычислений.

Текст использованной в экспериментах реализации (язык С++):

Для формирования входных данных использовался следующий алгоритм (язык Python):

Проведём исследование масштабируемости параллельной реализации алгоритма умножения разреженной матрицы на вектор. Исследование проводилось на суперкомпьютере "Ломоносов"[5] Суперкомпьютерного комплекса Московского университета. Входные данные формировались на персональном компьютере в силу того, что интерпретатор на суперкомпьютере "Ломоносов" не удовлетворял нужным требованиям, и потом отправлялись на суперкомпьютер для последующей обработки.

Программа была собрана на узле компиляции суперкомпьютера "Ломоносов", с помощью компилятора intel/15.0.090 и компилятора openmpi/1.6.5-icc, на языке С++11.

Сборка производилась с помощью CMakeLists.txt, его содержание прилагается:

Запуск производился на вычислительных узлах суперкомпьютера "Ломоносов".

Набор и границы значений изменяемых параметров запуска реализации алгоритма:

  • число процессоров [1 : 128] по степеням 2;
  • размер матрицы [1000 : 10000] с шагом 1000.

Распределение ненулевых и нулевых коэффициентов: 1:1. Однако стоит обратить внимание, что матрица генерировалась случайным образом, а значит, это лишь распределение, а не точное значение.

Рисунок 2. Время работы программы в зависимости от количества процессоров и размера матрицы

Можно заметить, что при небольшом количестве входных данных, и большом количестве процессоров эффективность распараллеливания падает до нуля, поскольку время вычисления элементов вектора c становится несоизмеримо малым по сравнению с временем, затраченным на пересылку в пункте (3).

Рисунок 4. Эффективность распараллеливания в зависимости от количества процессоров и размера матрицы

2.5 Динамические характеристики и эффективность реализации алгоритма

2.6 Выводы для классов архитектур

2.7 Существующие реализации алгоритма

Существующие реализации:

SciPy [1],

MatLab [2],

Intel MKL [3].

3 Литература

  1. С.Писсанецки. Технология разреженных матриц. - М.: Мир, 1988.
  2. Воеводин В.В. Математические основы параллельных вычислений// М.: Изд. Моск. ун-та, 1991. 345 с.
  3. Воеводин В.В., Воеводин Вл.В. Параллельные вычисления. – СПб.: БХВ - Петербург, 2002. – 608 с.
  4. Фролов А.В.. Принципы построения и описание языка Сигма. Препринт ОВМ АН N 236. М.: ОВМ АН СССР, 1989.
  5. Воеводин Вл., Жуматий С., Соболев С., Антонов А., Брызгалов П., Никитенко Д., Стефанов К., Воеводин Вад. Практика суперкомпьютера «Ломоносов» // Открытые системы, 2012, N 7, С. 36-39.