Report_CUDA_OpenCL_TBBx

advertisement
Министерство образования и науки
Государственное образовательное учреждение высшего профессионального
образования «Нижегородский государственный университет
им. Н.И. Лобачевского»
Факультет вычислительной математики и кибернетики
Кафедра: Математического обеспечения ЭВМ
Направление: Информационные технологии
Отчет по лабораторной работе
Тема:
«Умножение разреженных матриц»
Выполнили:
студенты группы 85М21
Леденцов Д. К.
Орлов Л. М.
Нижний Новгород
2011
Оглавление
Введение....................................................................................................................................................... 3
Постановка задачи....................................................................................................................................... 4
Описание алгоритма ................................................................................................................................... 5
Параллельная реализация ........................................................................................................................... 8
Реализация на CUDA ................................................................................................................................10
Реализация на TBB ....................................................................................................................................12
Реализация на OpenCL ..............................................................................................................................14
Полученные результаты ...........................................................................................................................16
Заключение ................................................................................................................................................18
Литература .................................................................................................................................................19
2
Введение
По мере того, как растут производительность и быстродействие вычислительных
машин, становится возможным обрабатывать все большего размера матрицы для
различного типа задач. Например, обработка матриц необходимо при решении систем
линейных уравнений. Но, несмотря на стремительное развитие вычислительной техники,
по-прежнему, как и несколько десятков лет назад, основными характеристиками остаются:
память, трудоемкость и быстродействие. С ростом порядка матричной задачи растет и
стоимость ее решения, становясь решающим фактором.
Разреженной матрицей называется матрица с большим числом ненулевых элементов.
Для такой матрицы неэффективно хранить и обрабатывать все элементы. Поэтому
существуют специальные форматы хранения данных матриц, позволяющие запоминать
только ненулевые элементы. Но для каждого такого нового способа хранения необходимо
разрабатывать методы для основных операций: умножение матрицы на число,
транспонирование, умножение двух матриц.
3
Постановка задачи
В ходе выполнения лабораторной работы необходимо реализовать умножение
разреженных матриц. При этом матрицы должны храниться в разреженном столбцовом
формате. Изначально требуется реализовать последовательный вариант алгоритма
умножения на языке С++. Далее необходимо разработать параллельную версию алгоритма
и реализовать ее с помощью трех технологий: CUDA, TBB, OpenCL. В завершении работы
должны быть приведены полученные результаты и заключение.
4
Описание алгоритма
Для хранения разреженных матриц используется так называемый разреженный
столбцовый формат, широко известным как CSS (Compressed Column Storage) или CSC
(Compressed Sparse Columns). В таком формате матрица хранится в виде трех массивов:

Массив значений Value (построчно, сверху вниз);

Массив номеров строк Row;

Массив индексов начала столбцов ColIndex.
При таком хранении ColIndex[j] указывает на начало j-го столбца в массивах Value и
Row, а элементы этого столбца находятся по индексам от ColIndex[j] до ColIndex[j+1]–1
включительно. При этом довольно просто обрабатываются пустые столбцы, для которых
ColIndex[j] = ColIndex[j+1]. Для последнего столбца полагается ColIndex[N+1] = NZ, где N
- размер матрицы, а NZ - число ненулевых элементов.
Для хранения матрицы N*N с NZ ненулевыми элементами в итоге нам потребуется
8NZ+4NZ+4(N+1) = 12NZ+4N+4 байт, если для хранения значений элементов мы будем
использовать 8 байт, а для индексов - 4 байта. Это конечно много меньше N^2 как если бы
мы использовали плотный формат.
Ниже представлен пример хранения для матрицы A (6*6).
Рис.1. Столбцовый формат хранения разреженной матрицы
5
Формат CSS предоставляет быстрый доступ к столбцам, при этом столбцы
рассматриваются по порядку, хотя внутри них элементы могут быть, как упорядочены, так
и нет. В первом случае достигается быстрый поиск элементов, хотя приходится “платить”
за поддержание такой упорядоченности. Во втором же случае ничего поддерживать не
надо, но зато поиск осуществляется лишь перебором. Существует также модификация
CSS с четырьмя массивами, где последний массив хранит индексы элементов, идущих в
конце столбца. Столбцы тогда могут не быть упорядочены, а их перестановка проводится
без перепаковки путем изменения индексов. Данная модификация в лабораторной работе
не используется.
Для реализации умножения нужно, прежде всего, реализовать транспонирование
матрицы. Пусть имеется разреженная матрица A(N*N) в формате CSS. Чтобы получить
AT в формате CSS необходимо:

сформировать N «целых» и N «вещественных» векторов;

в цикле просмотреть все столбцы исходной матрицы, для каждого столбца - все его
элементы;

если A[i][j] = v, тогда добавить числа i и v в j-ые «целый» и «вещественный»
вектор, тем самым в векторах сформируются столбцы транспонированной
матрицы;

скопировать данные из векторов в CSS-структуру транспонированной матрицы
(Row и Value), попутно формируя массив ColIndex.
Теперь
можно непосредственно выполнять
умножение. Алгоритм
умножение
разреженных матриц A и B в форматах CSS состоит в следующем:

транспонировать матрицу A, т.е. вычислить AT;

инициализировать структуру данных для матрицы C = A*B;

последовательно перемножить каждый столбец матрицы AT на каждую из строк
матрицы B, записывая в C полученные результаты и формируя ее структуру.
При умножении столбцов возникает задача сопоставления с целью выделения пар
ненулевых элементов. В простейшем варианте это решается простым перебором для
каждого элемента столбца матрицы AT элементов столбца матрицы B до тех пор, пока не
будет найден элемент с таким же значением в массиве Row или не закончится строка.
Хотя это излишне, ведь вектора упорядочены! Для решения проблемы достаточно:

встать на начало обоих векторов (ks= …, ls= …);

сравнить текущие элементы AT.Row[ks]и B.Row[ls];
6

если значения совпадают, просуммировать AT.Value[ks] * B.Value[ls] и увеличить
оба индекса, в противном случае – увеличить один из индексов, в зависимости от
того, какое значение больше.
Этого можно добиться, написав приблизительно следующее:
ks:=AT.ColIndex[i]; kf:=AT.ColIndex[i + 1] -1
ls:= B.ColIndex[j]; lf:= B.ColIndex[j + 1] -1
while ((ks <= kf) && (ls <= lf))
if AT.Row[ks]<B.Row[ls] then ks:=ks+1;
else if AT.Row[ks]>B.Row[ls] then ls:=ls+1;
else
sum:=sum + AT.Value[ks]*B.Value[ls];
ks:=ks+1;ls:=ls+1;
endif;
endif
endwile
7
Параллельная реализация
Предположим, что нам необходимо умножить две разреженные матрицы размера N*N,
которые хранятся в столбцовом формате.
C=A*B
Для этого сначала нужно транспонировать матрицу A, а затем перемножить столбцы
транспонированной матрицы A на столбцы матрицы B. Большую часть времени работы
алгоритма занимает умножение матриц. Поэтому данная часть будет распараллелена.
Транспонирование будет выполняться последовательно.
Умножение
После того, как выполнили транспонирование матрицы A, имеются следующие
исходные данные:

массив valueA – ненулевые элементы матрицы Ат;

массив rowA – массив номеров строк элементов матрицы Ат;

массив colIndexA – массив начала столбцов матрицы Ат;

массив valueB – ненулевые элементы матрицы B;

массив rowB – массив номеров строк элементов матрицы B;

массив colIndexB – массив начала столбцов матрицы B;
Умножение матриц выполняется в два прохода. Количество потоков равняется размеру
матриц (N). На первом проходе каждый поток подсчитывает количество ненулевых
элементов в соответствующем столбце матрицы C и записывает это значение в элемент
colIndexC[i]. После этого один поток пересчитывает массив colIndexC по следующему
алгоритму:
int t;
int sum = 0;
for (int i=0; i<=N; i++)
{
t = colIndexC[i];
colIndexC[i] = sum;
sum += t;
}
В результате будет сформирован массив colIndex для матрицы C. Последний элемент
данного массива будет содержать количество ненулевых элементов в матрице С.. Далее
будет выделена память под массивы: rowC и valueC.
8
На втором проходе каждый вычисляет ненулевые элементы матрицы С для своего
столбца и записывает их в массив valueC а номер строки в массив rowC. После этого
матрица C будет сформирована.
9
Реализация на CUDA
Для хранения структуры разреженной матрицы был создан класс SparseMatrix. Данный
класс содержит следующие поля:





int N – размер матрицы;
int size – количество ненулевых элементов;
float* Value – указатель на массив элементов матрицы;
int* Row – указатель на массив номеров строк;
int* colIndex – указатель на массив начала столбцов.
Также в данном классе реализованы два метода:


virtual SparseMatrix Transpose() - транспонировние матрицы;
virtual SparseMatrix Multiplication(SparseMatrix* m) – перемножение двух
разреженных матриц.
Для реализации алгоритма перемножения разреженных матриц с помощью технологии
CUDA был создан класс SparseMatrix_CUDA, который является наследником класса
SparseMatrix,и в нем переопределен метод Multiplication(). В данном методе выполняются
следующие действия:
1) Транспонирование матрицы A.
2) Выделение памяти на устройстве под массивы матриц A и B.
3) Вызов первого ядра. Данное ядро в массив colIndex матрицы C записывает
количество ненулевых элементов в каждом столбце.
4) Пересчет массива colIndex (выполняется в один поток) и выделение памяти под
массивы матрицы C.
5) Вызов второго ядра, которое заполняет массивы Value и Row матрицы C.
6) Создание матрицы C.
Функции ядра имеют следующий прототип:
__global__ void Mult_CalcColIndex(const int n, float* valueA, int* rowA, int* colIndexA,
float* valueB, int* rowB, int* colIndexB, int* colIndexC)
где n – размер мариц; valueA и valueB – указатели на область памяти, в которой хранятся
элементы матриц A и B; rowA и rowB – указатели на массивы номеров строк для элементов
матриц A и B; colIndexA и colIndexB – указатели на массивы начала столбцов матриц A и B;
colIndex – указатель на массив начала столбцов матрицы C. Данное ядро в массив colIndex
матрицы C записывает количество ненулевых элементов в каждом столбце.
__global__ void Mult_CalcValue(const int n, float* valueA, int* rowA, int* colIndexA,
float* valueB, int* rowB, int* colIndexB, float* valueC, int* rowC, int* colIndexC)
где n – размер мариц; valueA, valueB, valueC – указатели на область памяти, в которой
хранятся элементы матриц A, B, C; rowA, rowB, rowC – указатели на массивы номеров
строк для элементов матриц A, B, C; colIndexA, colIndexB, colIndexC – указатели на
10
массивы начала столбцов матриц A, B, C. Данное ядро заполняет массивы Value и Row
матрицы C.
Для выполнения ядер создается одномерное пространство индексов, размер которого
совпадает с размером матриц (N). Номер потока определяется следующим образом:
int j = blockIdx.x * blockDim.x + threadIdx.x;
11
Реализация на TBB
Для реализации алгоритма перемножения разреженных матриц с помощью технологии
TBB был создан класс SparseMatrix_TBB, который является наследником класса
SparseMatrix,и в нем был переопределен метод Multiplication().
Для распараллеливания алгоритма умножения матриц с помощью библиотеки TBB был
создан класс Multiplicator. Данный класс содержит следующие данные:



vector<int>* rows - массив векторов типа int из библиотеки STL. Данные
вектора будут содержать номера строк ненулевых элементов матрицы C.
vector<double>* values - массив векторов типа float из библиотеки STL.
Данные вектора будут содержать ненулевые элементы матрицы C.
int *col_index – указатель на массив colIndex матрицы C.
У класса Multiplicator перегружен оператор:
void operator()(const blocked_range<int> &r) const ;
В данном методе заполняются массивы векторов rows, values, и в массив colIndex
записывается количество ненулевых элементов для каждого столба. Реализация данного
метода представлена ниже.
void operator()(const blocked_range<int>& r) const
{
int begin = r.begin();
int end = r.end();
int N = A.N;
int i, j, k;
int *mas = new int[N];
for (i = begin; i < end; i++)
{
memset(mas, -1, N * sizeof(int));
int i1 = B.ColIndex[i], i2 = B.ColIndex[i + 1];
for (j = i1; j < i2; j++)
{
int row = B.Row[j];
mas[row] = j;
}
for (j = 0; j < N; j++)
{
double sum = 0;
int i3 = A.ColIndex[j], i4 = A.ColIndex[j + 1];
for (k = i3; k < i4; k++)
{
int arow = A.Row[k];
int bind = mas[arow];
if (bind != -1)
sum += B.Value[bind] * A.Value[k];
}
if (fabs(sum) > 0.000001)
{
rows[i].push_back(j);
values[i].push_back(sum);
12
col_index[i]++;
}
}
}
delete [] mas;
}
Таким образом, для выполнения умножения разреженных матриц с помощью
технологии TBB необходимо сделать:
1) Создать массивы rows и values размера N из векторов типа int и float.
2) Вызвать метод parallel_for, в который передать экземпляр класса Multiplicator.
После этого массив colIndex результируещей матрицы будет содержать
количество ненулевых элементов для каждого столбца.
3) Пересчитать массив colIndex.
4) Создать матрицу и передать в нее данные из массивов: colIndex, rows, values.
13
Реализация на OpenCL
Для реализации алгоритма перемножения разреженных матриц с помощью технологии
OpenCL был создан класс SparseMatrix_OpenCL, который является наследником класса
SparseMatrix, и в нем переопределен метод Multiplication(). В данном методе выбирается
платформа для вычислений, устройство, создается контекст, очередь команд, ядра,
буферы памяти. Сначала выполняется последовательный алгоритм транспонирования
матрицы A. Затем выполняются функции ядра для умножения матриц.
Для выполнения умножения матриц необходимы следующие буферы памяти:









cl_mem maValue – массив элементов матрицы Ат;
cl_mem maRow – массив номеров строк элементов матрицы Ат;
cl_mem maColIndex – массив начала столбцов матрицы Ат;
cl_mem mbValue – массив элементов матрицы B;
cl_mem mbRow - массив номеров строк элементов матрицы B;
cl_mem mbColIndex - массив начала столбцов матрицы B;
cl_mem mсValue – массив элементов матрицы С;
cl_mem mсRow - массив номеров строк элементов матрицы С;
cl_mem mсColIndex - массив начала столбцов матрицы С;
Первое ядро __kernel void Mult_CalcColIndex выполняет подсчет количества ненулевых
элементов для столбцов матрицы С. Эти данные записываются в буфер mcColIndex. Код
ядра приведен ниже:
__kernel void Mult_CalcColIndex(const int n,
__global float* valueA,
__global int* rowA,
__global int* colIndexA,
__global float* valueB,
__global int* rowB,
__global int* colIndexB,
__global int* colIndexC)
{
int j = get_global_id(0) ;
if (j >= n) return;
int ks, ls, kf, lf;
int countElem = 0;
for (int i=0; i<n; i++)
{
ks = colIndexA[i];
kf = colIndexA[i+1];
ls = colIndexB[j];
lf = colIndexB[j+1];
bool isFound = false;
while ((ks < kf) && (ls < lf))
{
if (rowA[ks] < rowB[ls]) ks++;
else
if (rowA[ks] > rowB[ls]) ls++;
else
{
isFound = true;
14
break;
}
}
if (isFound) countElem++;
}
colIndexC[j] = countElem;
}
Затем массив colIndexC считывается с устройства и пересчитывается на хосте и
запускается второе ядро __kernel void Mult_CalcValue. Оно вычисляет массивы valueC и
rowC.
__kernel void Mult_CalcValue(const int n,
__global float* valueA,
__global int* rowA,
__global int* colIndexA,
__global float* valueB,
__global int* rowB,
__global int* colIndexB,
__global float* valueC,
__global int* rowC,
__global int* colIndexC)
{
int j = get_global_id(0) ;
if (j >= n) return;
int ks, ls, kf, lf;
int idxCurElem = colIndexC[j];
for (int i=0; i<n; i++)
{
ks = colIndexA[i];
kf = colIndexA[i+1];
ls = colIndexB[j];
lf = colIndexB[j+1];
bool isFound = false;
float sum = 0;
while ((ks < kf) && (ls < lf))
{
if (rowA[ks] < rowB[ls]) ks++;
else
if (rowA[ks] > rowB[ls]) ls++;
else
{
isFound = true;
sum += valueA[ks] * valueB[ls];
ks++;
}
}
if (isFound)
{
valueC[idxCurElem] = sum;
rowC[idxCurElem] = i;
idxCurElem++;
}
}
}
После этого полученные данные считываются с устройства, и создается матрица C.
15
Полученные результаты
Для сравнения последовательной и параллельных реализаций алгоритма умножения
разреженных матриц была проведена серия экспериментов. Тестирование алгоритма
CUDA-версии проводилось на системе со следующими характеристиками:
1. CPU – Intel(R) Core(TM) 2 DUO E8400 3,00 GHz
2. RAM – 2 GB
3. GPU – NVidia GeForce 9500GT 512 Mb
4. OS – Windows XP SP3
В таблице приведено время работы алгоритмов и полученное ускорение в зависимости
от размера матриц. Необходимо отметить, что время работы CUDA-версии включает
время, затрачиваемое на выделение памяти и копирование данных.
Размер матриц
Последовательная
CUDA-версия (сек.)
Ускорение
версия (сек.)
128*128
0,013
0,084
0,15
256*256
0,101
0,160
0,63
512*512
0,785
0,630
1,25
1024*1024
6,219
4,560
1,36
1700*1700
28,428
20,020
1,42
Таблица 1. Результаты CUDA-версии
Тестирование алгоритмов TBB-версии и OpenCL-версии проводилось на системе со
следующими характеристиками:
1. CPU – AMD Turion(tm) 64 X2 2.1 GHz
2. RAM – 3 GB
3. GPU – Radeon HD 3470
4. OS – Windows Vista
Ниже в таблицах приведено время работы алгоритмов и полученное ускорение в
зависимости от размера матриц. Ядро алгоритма OpenCL-версии выполнялось на
центральном процессоре.
16
Размер матриц
Последовательная
OpenCL-версия
TBB-версия
версия (сек.)
128*128
0,018
0,552
0,005
256*256
0,148
0,596
0,03
512*512
1,182
1,001
0,202
1024*1024
9,046
4,138
1,624
2048*2048
56,017
26,283
18,108
Таблица 2. Время работы TBB-версии и OpenCL-версии
Размер матриц
128*128
Ускорение OpenCL-версии
0,033
Ускорение TBB-версии
3,6
256*256
0,248
4,93
512*512
1,18
5,85
1024*1024
2,19
5,57
2048*2048
2,13
3,09
Таблица 3. Ускорение TBB-версии и OpenCL-версии
17
Заключение
В ходе выполнения лабораторной работы был успешно реализован последовательный
вариант алгоритма умножения разреженных матриц на языке С++. Также разработаны
параллельные версии данного алгоритма, использующие технологии: CUDA, TBB,
OpenCL.
Проведенные эксперименты для CUDA-версии показали, что параллельная версия
алгоритма начинает работать быстрее при размере матрицы больше 512. Это связано с
тем, что вычисление на видеокарте требует накладных расходов из-за выделения памяти и
копирования данных. Таким образом, данный алгоритм эффективен для матриц больших
размеров.
Алгоритмы TBB-версии и OpenCL-версии выполнялись на центральном процессоре.
Как видно из полученных результатов, при небольшом размере матриц Open-CL версия
работает медленнее последовательной версии. Это связано с накладными расходами на
создание объектов библиотеки OpenCL, выделение и копирование памяти, компиляцию
ядер. Но с ростом размера матриц ускорение стремится к двум. Время работы TBB-версии
алгоритма в несколько раз превосходит время работы последовательного алгоритма.
Помимо выигрыша связанного с распараллеливанием, ускорение еще достигается
благодаря оптимизации алгоритма. Алгоритм TBB-версии выполняет умножение матриц
не в два прохода, а в один.
18
Литература
1.
2.
3.
Писсанецки С. Технология разреженных матриц.: Пер. с англ. - М.: Мир, 1988.
Тьюарсон Р. Разреженные матрицы.: Пер. с англ. М.: Мир, 1977.
Боресков, А.В. Основы работы с технологией CUDA / А.В. Боресков, А.А. Харламов.
– М.: ДМК Пресс, 2011. – 232 с
Интернет – ресурсы:
4.
5.
6.
7.
Разреженные матрицы [http://en.wikipedia.org/wiki/Sparse_matrix]
Технология CUDA [http://developer.nvidia.com/]
Технология OpenCL для AMD [http://www.amd.com/us/products/technologies/streamtechnology/opencl/Pages/opencl.aspx]
Технология TBB [www.threadingbuildingblocks.org]
19
Download