
- •ВВЕДЕНИЕ
- •1. ПРЕДПОСЫЛКИ ВОЗНИКНОВЕНИЯ ЯЗЫКА OPENCL
- •2. ДИЗАЙН OPENCL
- •2.1. Модель платформы
- •2.2. Модель вычислений
- •2.3. Модель памяти
- •2.4. Модель программирования
- •3. АППАРАТНЫЕ СРЕДСТВА, ПОДДЕРЖИВАЮЩИЕ ПАРАЛЛЕЛЬНЫЕ ВЫЧИСЛЕНИЯ
- •4. ЛАБОРАТОРНЫЕ РАБОТЫ
- •4.1. Поток проектирования при работе с языком OpenCL
- •4.1.1. Задание
- •4.1.2. Программное и аппаратное обеспечение
- •4.1.3. Последовательность выполнения работы
- •4.1.4. Заключение по практическому эксперименту
- •4.1.5. Содержание отчета
- •4.2. Создание аппаратно-программной системы с ОС Linux. Подключение к Ethernet. Работа с Web-сервером
- •4.2.1. Задание
- •4.2.2. Последовательность выполнения работы
- •4.2.3. Содержание отчета
- •4.3. Оптимизация умножения матриц в OpenCL
- •4.3.1. Базовый алгоритм умножения матриц
- •4.3.2. Использование локальной памяти
- •4.3.3. Увеличение числа одновременно исполняемых рабочих элементов
- •4.3.4. Содержание отчета
- •СПИСОК ЛИТЕРАТУРЫ
- •ИНТЕРНЕТ-РЕСУРСЫ
- •ПРИЛОЖЕНИЯ
- •П1. Код хост-программы для сложения двух векторов
- •П2. Исходный код хост-программы для умножения двух матриц

Рис. 4.13. Задание задержки мигания светодиодов
С помощью кнопки «Blink» применить введенное значение (рис. 4.13). Кнопки «ON» и « OFF», соответственно, включают и выключают светодиод на плате.
4.2.3. Содержание отчета
Отчет должен быть выполнен по требованиям УСПД и содержать следующую информацию:
1.Задание на проектирование.
2.Описание основных этапов проделанной работы с результатами их исполнения.
3.Выводы по проделанной работе и полученным результатам.
4.3. Оптимизация умножения матриц в OpenCL
Цель работы – познакомиться с различными методиками оптимизации вычислений в OpenCL на примере умножения матриц [11].
Умножение матриц часто используется для демонстрации оптимизации программ, требующих большого количества вычислений. Хорошо оптимизированная программа умножения матриц может практически на 100 % задействовать доступные аппаратные ресурсы. Для простоты рассмотрим умножение квадратных матриц, в которых число нулевых элементов пренебрежимо мало по сравнению с числом ненулевых. В этом случае не имеет смысла отслеживать нули в попытках сэкономить используемую память или упростить вычисления.
4.3.1. Базовый алгоритм умножения матриц
Рассмотрим три матрицы: А[M][K], B[K][N] и С[M][N]. Так как данные матрицы квадратные, то M = K = N. При программной реализации алгоритмов умножения условимся рассматривать матрицы как одномерные массивы с соответствующим преобразованием пар индексов в один. Это распространенная техника при работе с многомерными массивами.
25

Операция умножения матрицы А на матрицу В с сохранением результата в матрицу С представлена в листинге 1.
КаждыйэлементматрицыСявляетсярезультатомумножениястрокиматрицы Ана столбец матрицы В (рис. 4.14). Для прохода по всем элементам матрицыСпотребуется двойнойвложенныйцикл.Третийвложенныйциклнужен для вычисления произведения строки на столбец. Таким образом, прямая (последовательная) реализация этого алгоритма на языке С потребует тройного вложенного цикла.
Рис. 4.14. Иллюстрация умножения двух матриц
Листинг 1. Код функции умножения двух матриц на языке С:
for (int m=0; m<M; m++) { for (int n=0; n<N; n++) {
float acc = 0.0f;
for (int k=0; k<K; k++) {
acc += A[k*M + m] * B[n*K + k];} C[n*M + m] = acc;
}
}
Для преобразования приведенной ранее функции в OpenCL-ядро избавимся от циклов по m и n, используя функцию get_global_id() для получения глобального id рабочего элемента в двух измерениях. NDRange в данном случае будет иметь размерность 2 (M×N).
Листинг 2. Код OpenCL-ядра для умножения двух матриц:
__kernel void mmul( __global int *restrict C, __global int *restrict A,__global int *restrict B, int M, int N, int K){
26

|
|
|
|
|
|
|
|
const int |
globalRow=get_global_id(0);//индекс строки С(0..М) |
|
|
||
|
const int |
globalCol=get_global_id(1);//индекс столбца С(0..N) |
|
|||
|
int acc = |
0; // вычислить один элемент матрицы С |
|
|
||
|
for (int |
k=0; k<K; k++) { |
k] * B[k*M + globalRow]; |
|||
|
acc += |
A[globalCol*K + |
}
C[globalCol*M + globalRow] = acc;//записать результат
}
Исходный код соответствующей хост-программы приведен в приложении 2. Будем использовать одну и ту же программу для всех случаев оптимизации. Она имеет традиционный функционал хост-программы OpenCL и принимает на вход два необязательных аргумента: размер блока вычислений (понадобится в следующих вариантах оптимизации) и множитель, задающий размеры вычисляемых матриц путем умножения на размер блока. Для обоих аргументов заданы значения по умолчанию. Для первой реализации умножения матриц эти параметры не имеют значения.
При запуске хост-программы без аргументов размеры перемножаемых матриц будут составлять 1024×1024 элемента. Полученная при этом пропускная способность составляет 0,25 ГФ.
root@socfpga:~/matrix_mul# ./mmul
Launching for device 0 (global size: 1024, 1024) Time: 8527.772 ms
Kernel time (device 0): 8527.397 ms Throughput: 0.25 GFLOPS
4.3.2. Использование локальной памяти
Умножение матриц состоит из операций сложения и умножения. Практическивсесовременныепроцессорыимеютдостаточнуюширинушиныкарифметически логическому устройству, чтобы выполнять эти операции с максимальной производительностью при условии минимальных накладных расходов на перемещение данных. Таким образом, при оптимизации умножения матриц рано или поздно приходится заняться минимизацией трафика данных. Рассмотренные ранее реализации матричного умножения подразумевают нахождение всех трех матриц в глобальной памяти устройства. Это означает, что при вычислениях данные постоянно перемещаются из глобальной памяти в приватную для каждой операции умножения «строка – столбец». Можно снизить расходы на перемещение, вычисляя матрицу С поблочно (рис. 4.15).
Для того чтобы вычислить блок матрицы С, понадобятся соответствующие строки матрицы А и столбцы матрицы В. При этом наблюдается повторное использование одних и тех же элементов внутри одного блока (рис. 4.16).
27

Рис. 4.15. Иллюстрация блочного умножения матриц
Рис. 4.16. Использование данных внутри одного блока
Таким образом, нет смысла постоянно копировать одни и те же данные из глобальной памяти для разных рабочих элементов. Вместо этого мы можем загрузить по одному блоку матрицы А и В в локальную память рабочей группы, выполнить вычисления и перейти к следующему блоку.
Код OpenCL-ядра, выполняющего эти операции, представлен в листинге 3. Обратите внимание на директиву #define BLOCK_SIZE, задающую размер вычисляемого блока. Потенциально больший размер блока должен обеспечиватьбольшуюпроизводительность.Крометого,посколькулокальная память доступна рабочим элементам внутри одной рабочей группы, имеет смысл поручить вычисление одного блока матрицы С одной рабочей группе. Для этого принудительно задается размер рабочей группы, равный размеру одного блока, с помощью атрибута _attribute((reqd_work_ group_size)). Для максимально эффективного использования локальной памяти будем осуществлять размотку цикла, соответствующего вычислению одного элемента матрицы С. С помощью директивы #pragma unroll перед циклом указываем
28

компилятору, что цикл должен быть размотан. Это приведет к ускорению вычисления цикла за счет исключения необходимости проверять граничные условия цикла и увеличения количества инструкций, которые могут выполняться параллельно, в обмен на увеличение аппаратных затрат на реализацию.
Листинг 3. Код OpenCL-ядра для блочного умножения матриц:
#define BLOCK_SIZE 16 // размер блока __kernel
__attribute((reqd_work_group_size(BLOCK_SIZE,BLOCK_SIZE,1))) void mmul( // Input and output matrices
__global int *restrict C, __global int *restrict A, __global int *restrict B, int M,int N, int K)
{
//локальная память для одного блока матриц А и В
__local int A_local[BLOCK_SIZE][BLOCK_SIZE]; __local int B_local[BLOCK_SIZE][BLOCK_SIZE]; int block_x = get_group_id(0); // индекс блока int block_y = get_group_id(1);
//локальный индекс (отступ внутри блока)
int local_x = get_local_id(0); int local_y = get_local_id(1);
// границы вычислений
int a_start = M * BLOCK_SIZE * block_y; int a_end = a_start + M - 1;
int b_start = BLOCK_SIZE * block_x; int running_sum = 0;
//начать вычисление выходной матрицы С
//каждая итерация цикла соответствует одному блоку матрицы for (int a = a_start, b = b_start; a <= a_end;
a += BLOCK_SIZE, b += (BLOCK_SIZE * K))
{
// копировать входные матрицы в локальную память
A_local[local_x][local_y] = A[a + M * local_y + local_x]; B_local[local_x][local_y] = B[b + K * local_y + local_x];
// дождаться окончания копирования barrier(CLK_LOCAL_MEM_FENCE);
//вычислить один элемент матрицы С
//осуществить полную размотку цикла
#pragma unroll
for(int k = 0; k < BLOCK_SIZE; ++k)
{
running_sum += A_local[k][local_y] * B_local[local_x][k];
}
//окончание обработки блока
barrier(CLK_LOCAL_MEM_FENCE);
}
// записать результат вычислений
29