Участник:Blizn/Хранение ненулевых элементов разреженной матрицы. Умножение разреженной матрицы на вектор.: различия между версиями
Blizn (обсуждение | вклад) Нет описания правки |
Нет описания правки |
||
| (не показаны 32 промежуточные версии 3 участников) | |||
| Строка 1: | Строка 1: | ||
{{Assignment|ASA|Evgeny Mortikov}} | |||
{{algorithm | {{algorithm | ||
| name = Умножение разреженной матрицы на вектор | | name = Умножение разреженной матрицы на вектор | ||
| Строка 11: | Строка 14: | ||
= Свойства и структура алгоритма = | = Свойства и структура алгоритма = | ||
== Общее описание алгоритма == | == Общее описание алгоритма<ref>С.Писсанецки. Технология разреженных матриц. - М.: Мир, 1988.</ref> == | ||
'''Разрежённая матрица''' — это матрица с преимущественно нулевыми элементами. В противном случае, если бо́льшая часть элементов матрицы ненулевые, матрица считается '''''плотной'''''. | '''Разрежённая матрица''' — это матрица с преимущественно нулевыми элементами. В противном случае, если бо́льшая часть элементов матрицы ненулевые, матрица считается '''''плотной'''''. | ||
Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разрежённой. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов: | Среди специалистов нет единства в определении того, какое именно количество ненулевых элементов делает матрицу разрежённой. Разные авторы предлагают различные варианты. Для матрицы порядка n число ненулевых элементов: | ||
| Строка 52: | Строка 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 ] | ||
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными, а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными. | Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными (например, операции вставки и удаления новых коэффициентов), а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными. | ||
==== Замечания ==== | ==== Замечания ==== | ||
| Строка 88: | Строка 91: | ||
Как записано и в [[#Вычислительное ядро алгоритма|описании ядра алгоритма]], основную часть метода составляют множественные (всего <math>n</math>) вычисления сумм: | Как записано и в [[#Вычислительное ядро алгоритма|описании ядра алгоритма]], основную часть метода составляют множественные (всего <math>n</math>) вычисления сумм: | ||
<math>c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}</math>. | <math>c_{i} = \sum\limits_{k = 1}^{l_{i}} a_{i,j=j(k)}b_{j=j(k)}</math>, | ||
которые могут вычисляться в произвольном порядке. | |||
== Схема реализации последовательного алгоритма == | == Схема реализации последовательного алгоритма == | ||
| Строка 99: | Строка 104: | ||
# <math>IAA = IA[i]</math> | # <math>IAA = IA[i]</math> | ||
# <math>IAB = IA[i + 1] - 1</math> | # <math>IAB = IA[i + 1] - 1</math> | ||
# <math>c_{i} = \sum\limits_{k = IAA}^{IAB} AN | # <math>c_{i} = \sum\limits_{k = IAA}^{IAB} AN[k]b_{JA[k]}</math>. | ||
После этого (если <math>i \le n</math>) происходит переход к шагу 1 с бо́льшим <math>i</math>. | После этого (если <math>i \le n</math>) происходит переход к шагу 1 с бо́льшим <math>i</math>. | ||
| Строка 106: | Строка 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> умножений. | ||
Умножения и сложения составляют ''основную часть алгоритма''. | Умножения и сложения составляют ''основную часть алгоритма''. | ||
| Строка 119: | Строка 124: | ||
Граф алгоритма состоит из двух групп вершин, расположенных в целочисленных узлах двух областей одной размерности. | Граф алгоритма состоит из двух групп вершин, расположенных в целочисленных узлах двух областей одной размерности. | ||
Первая группа вершин расположена в двумерной области, соответствующая ей операция вычисляет функцию <math>a | Первая группа вершин расположена в двумерной области, соответствующая ей операция вычисляет функцию <math>a \cdot b</math>. Естественно введённые координаты области таковы: | ||
* <math>i</math> — меняется в диапазоне от <math>1</math> до <math>n | * <math>i</math> — меняется в диапазоне от <math>1</math> до <math>n</math>, принимая все целочисленные значения; | ||
* <math>j</math> — меняется в диапазоне от <math>i+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>a</math>: | ||
** при <math> | ** при <math>j = 1</math> - результат срабатывания операции, соответствующей вершине из первой группы, с координатами <math>(i, j)</math>; | ||
** при <math> | ** при <math>j > 1</math> - результат срабатывания операции, соответствующей вершине из второй группы, с координатами <math>(i, j - 1)</math>; | ||
* <math>b</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''', | Для умножения разреженной матрицы общего вида, хранимой в форме '''RR(C)U''', размерности <math>n \times m</math> на заполненный вектор <math>m \times 1</math> в параллельном варианте требуется последовательно выполнить следующие ярусы: | ||
* Не более чем <math>m</math> сложений и умножений (<math>n</math> вычислений в каждом из ярусов) | * Не более чем <math>m</math> сложений и умножений (<math>n</math> вычислений в каждом из ярусов) | ||
| Строка 142: | Строка 165: | ||
== Входные и выходные данные алгоритма == | == Входные и выходные данные алгоритма == | ||
'''Входные данные''': разреженная матрица общего вида <math>A</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>2l+m+n+1</math>, где <math>l</math> - количество ненулевых элементов в матрице <math>A</math>. | ||
| Строка 155: | Строка 178: | ||
При этом вычислительная мощность алгоритма, как отношение числа операций к суммарному объему входных и выходных данных – ''константа''. | При этом вычислительная мощность алгоритма, как отношение числа операций к суммарному объему входных и выходных данных – ''константа''. | ||
Пусть <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>. | |||
= Программная реализация алгоритма = | = Программная реализация алгоритма = | ||
| Строка 165: | Строка 190: | ||
== Масштабируемость алгоритма и его реализации == | == Масштабируемость алгоритма и его реализации == | ||
Параллельная реализация алгоритма заключается в следующем: | |||
# Каждый процессор считывает входные данные из файла, зависящего от его номера. Данные имеют вид: вектор <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. Эффективность распараллеливания в зависимости от количества процессоров и размера матрицы]] | |||
== Динамические характеристики и эффективность реализации алгоритма == | == Динамические характеристики и эффективность реализации алгоритма == | ||
| Строка 171: | Строка 405: | ||
== Существующие реализации алгоритма == | == Существующие реализации алгоритма == | ||
Существующие реализации: | |||
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]. | |||
= Литература = | = Литература = | ||
Текущая версия от 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 ]
Такие неупорядоченные представления могут быть очень удобны в практических вычислениях. Результаты большинства матричных операций получаются неупорядоченными (например, операции вставки и удаления новых коэффициентов), а их упорядочение стоило бы значительных затрат машинного времени. В то же время, за немногими исключениями, алгоритмы для разреженных матриц не требуют, чтобы их представления были упорядоченными.
Замечания
Несколько замечаний по поводу рассмотренных форматов представления:
- Очевидно, что представление матрицы в формате RR(C)O так же является и представлением в формате RR(C)U, но не наоборот.
- Из представления матрицы в формате RR(C) нельзя получить информацию о точном количестве столбцов исходной матрицы.
- Целесообразно (в вопросе экономии памяти) использовать представления 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]
- [math]\displaystyle{ c_{i} = 0 }[/math]
- [math]\displaystyle{ IAA = IA[i] }[/math]
- [math]\displaystyle{ IAB = IA[i + 1] - 1 }[/math]
- [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. Здесь вершины первой группы обозначены красным цветом и отмечены знаком умножения, вершины второй - зелёным цветом и знаком сложения. Вершины, соответствующие входным данным обозначены белым цветом и выходным - синим.

Ресурс параллелизма алгоритма
Для умножения разреженной матрицы общего вида, хранимой в форме 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].
Программная реализация алгоритма
Особенности реализации последовательного алгоритма
Локальность данных и вычислений
Возможные способы и особенности параллельной реализации алгоритма
Масштабируемость алгоритма и его реализации
Параллельная реализация алгоритма заключается в следующем:
- Каждый процессор считывает входные данные из файла, зависящего от его номера. Данные имеют вид: вектор [math]\displaystyle{ b }[/math] и набор определённых строк матрицы [math]\displaystyle{ A }[/math] в формате RR(C)U в зависимости от номера процессора и их числа;
- Процессоры проводят вычисления значений вектора [math]\displaystyle{ 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]\displaystyle{ c }[/math] становится несоизмеримо малым по сравнению с временем, затраченным на пересылку в пункте (3).

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