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

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

Материал из Алговики
Перейти к навигации Перейти к поиску
Нет описания правки
Нет описания правки
 
(не показано 13 промежуточных версий 3 участников)
Строка 1: Строка 1:
{{Assignment}}
{{Assignment|ASA|Evgeny Mortikov}}
 


{{algorithm
{{algorithm
Строка 54: Строка 55:
   JA = [ 4 3 1 2 3 ]
   JA = [ 4 3 1 2 3 ]
   AN = [ 2 1 3 6 4 ]
   AN = [ 2 1 3 6 4 ]
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными, а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными.
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными (например, операции вставки и удаления новых коэффициентов), а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными.


==== Замечания ====
==== Замечания ====
Строка 110: Строка 111:
Для умножения разреженной матрицы общего вида, хранимой в форме '''RR(C)U''', размером <math>n \times m</math> на заполненный вектор <math>m \times 1</math> в последовательном (наиболее быстром) варианте требуется:
Для умножения разреженной матрицы общего вида, хранимой в форме '''RR(C)U''', размером <math>n \times m</math> на заполненный вектор <math>m \times 1</math> в последовательном (наиболее быстром) варианте требуется:


* <math>l</math> сложений,
* <math>O(l)</math> сложений,
* <math>l</math> умножений.
* <math>O(l)</math> умножений.


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


Пусть <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>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>.


= Программная реализация алгоритма =
= Программная реализация алгоритма =
Строка 196: Строка 197:
# Все процессоры отправляют результаты вычисления процессору, выбранному главным, и главный процессор выводит результат вычислений.
# Все процессоры отправляют результаты вычисления процессору, выбранному главным, и главный процессор выводит результат вычислений.


Проведём исследование масштабируемости параллельной реализации алгоритма умножения разреженной матрицы на вектор. Исследование проводилось на суперкомпьютере "Ломоносов"<ref name="Lom">Воеводин Вл., Жуматий С., Соболев С., Антонов А., Брызгалов П., Никитенко Д., Стефанов К., Воеводин Вад. Практика суперкомпьютера «Ломоносов» // Открытые системы, 2012, N 7, С. 36-39.</ref> [http://parallel.ru/cluster Суперкомпьютерного комплекса Московского университета].
<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>
 
Запуск производился на вычислительных узлах суперкомпьютера "Ломоносов".  


Набор и границы значений изменяемых параметров запуска реализации алгоритма:
Набор и границы значений изменяемых параметров запуска реализации алгоритма:
Строка 202: Строка 391:
* число процессоров [1 : 128] по степеням 2;
* число процессоров [1 : 128] по степеням 2;
* размер матрицы [1000 : 10000] с шагом 1000.
* размер матрицы [1000 : 10000] с шагом 1000.
Распределение ненулевых и нулевых коэффициентов: 1:1. Однако стоит обратить внимание, что матрица генерировалась случайным образом, а значит, это лишь распределение, а не точное значение.


[[file:Blizntime.png|thumb|center|1000px|Рисунок 2. Время работы программы в зависимости от количества процессоров и размера матрицы]]
[[file:Blizntime.png|thumb|center|1000px|Рисунок 2. Время работы программы в зависимости от количества процессоров и размера матрицы]]

Текущая версия от 03:13, 3 декабря 2016

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

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




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

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

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

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

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

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

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

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

Формат RR(C)O

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

Рассмотрим матрицу [math]\displaystyle{ A }[/math]:

[math]\displaystyle{ \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]\displaystyle{ AN }[/math] содержит все не нулевые элементы исходной матрицы [math]\displaystyle{ A }[/math], массив [math]\displaystyle{ JA }[/math] номер столбца в котором находится соответствующий элемент из [math]\displaystyle{ AN }[/math] и наконец массив [math]\displaystyle{ IA }[/math] содержит номер с которого начинается описание элементов в массивах [math]\displaystyle{ JA }[/math] и [math]\displaystyle{ AN }[/math]. Таким образом информация об элементах 2-ой строки матрицы хранится в элементах с [math]\displaystyle{ IA[2] = 2 }[/math] по [math]\displaystyle{ IA[3] - 1 = 3 }[/math] включительно массивов [math]\displaystyle{ JA }[/math] и [math]\displaystyle{ AN }[/math]. Можно обратить внимание, что [math]\displaystyle{ IA[3] = IA[4] = 4 }[/math], а это означает, что 3-я строка матрицы [math]\displaystyle{ A }[/math] нулевая.

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

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

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

Формат RR(C)U

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

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

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

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

Замечания

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

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

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

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

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

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

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

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

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

[math]\displaystyle{ c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)} }[/math],

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

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

Вычислительное ядро последовательной версии умножения разреженной матрицы на вектор можно составить из множественных (всего их [math]\displaystyle{ n }[/math]) вычислений скалярных произведений строк матрицы:

[math]\displaystyle{ c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)} }[/math].

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

Как записано и в описании ядра алгоритма, основную часть метода составляют множественные (всего [math]\displaystyle{ n }[/math]) вычисления сумм:

[math]\displaystyle{ c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)} }[/math],

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

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

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

Выполнять для [math]\displaystyle{ i }[/math] от [math]\displaystyle{ 1 }[/math] до [math]\displaystyle{ n }[/math]

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

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

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

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

  • [math]\displaystyle{ O(l) }[/math] сложений,
  • [math]\displaystyle{ O(l) }[/math] умножений.

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

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

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

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

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

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

  • [math]\displaystyle{ i }[/math] — меняется в диапазоне от [math]\displaystyle{ 1 }[/math] до [math]\displaystyle{ n }[/math], принимая все целочисленные значения;
  • [math]\displaystyle{ j }[/math] — меняется в диапазоне от [math]\displaystyle{ 1 }[/math] до [math]\displaystyle{ k }[/math], принимая все целочисленные значения,

где [math]\displaystyle{ k = k(i) = IA[i+1]-IA[i] }[/math].

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

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

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

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

  • [math]\displaystyle{ i }[/math] - меняется в диапазоне от [math]\displaystyle{ 1 }[/math] до [math]\displaystyle{ n }[/math], принимая все целочисленные значения;
  • [math]\displaystyle{ j }[/math] - меняется в диапазоне от [math]\displaystyle{ 1 }[/math] до [math]\displaystyle{ k-1 }[/math], принимая все целочисленные значения,

где [math]\displaystyle{ k = k(i) = IA[i+1]-IA[i] }[/math].

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

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

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

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

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

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

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

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

  • Не более чем [math]\displaystyle{ m }[/math] сложений и умножений ([math]\displaystyle{ n }[/math] вычислений в каждом из ярусов)

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

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

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

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

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

Объём выходных данных: [math]\displaystyle{ n }[/math].

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

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

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

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

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

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

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

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

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

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

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

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

#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. Однако стоит обратить внимание, что матрица генерировалась случайным образом, а значит, это лишь распределение, а не точное значение.

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

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

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

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

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

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

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

SciPy [1],

MatLab [2],

Intel MKL [3].

Литература

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