Половинкин А.Н. Постановка задачи Алгоритм умножения матриц на GPU Программная реализация C = A*B A – прямоугольная матрица ◦ число строк – hA ◦ число столбцов – wA B – прямоугольная матрица ◦ число строк – hB ◦ число столбцов - wB Bsub Asub Каждый блок потоков занимается вычислением одной подматрицы Csub матрицы С Каждый поток внутри блока потоков занимается вычислением одного элемента подматрицы Csub Csub = Asub * Bsub ◦ Asub размерности (block_size, wA) ◦ Bsub размерности (wA, block_size) ◦ Матрицы Asub и Bsub в общем случае могут не помещаться в общей памяти устройства, что приведёт к потере производительности ◦ Решение: разбить Asub и Bsub на блоки Asub,i и Bsub,i размерности (block_size, block_size), вычислять Csub как сумму произведений этих блоков: Csub Asub,i Bsub,i i for i = 1 to wA/block_size ◦ загрузить Asub,i и Bsub,i из глобальной (device) памяти в общую (shared) память блока потоков. Каждый поток загружает «свой» элемент! ◦ каждый поток вычисляет «свой» элемент в произведении Asub,i *Bsub,i и сохраняет аккумулированное значение end каждый поток загружает «свой» вычисленный элемент в глобальную (device) память matrixMul.h – содержит определения (через define) размера блока и размеров матриц matrixMul_gold.cpp ◦ computeGold matrixMul.cu ◦ ◦ ◦ ◦ main randomInit printDiff runMultiplication matrixMul_kernel.cu ◦ matrixMul (kernel) Используется схема хранения матриц по строкам в виде одномерного массива __global__ void matrixMul( float* C, float* A, float* B, int wA, int wB) ◦ вычисляем координаты текущего блока потоков и сохраняем их в переменные bx, by int bx = blockIdx.x int by = blockIdx.y ◦ вычисляем координаты текущего потока в блоке потоков и сохраняем их в переменные tx, ty вычисляем индекс элемента в массиве, хранящем исходную матрицу A, который соответствует первому элементу первой обрабатываемой подматрицы Asub,1 int aBegin = wA * BLOCK_SIZE * by; вычисляем шаг для перехода к первому элементу следующей обрабатываемой подматрицы int aStep = BLOCK_SIZE; вычисляем индекс (условие останова перебора подматриц Asub,i) int aEnd = aBegin + wA - 1; вычисляем индекс элемента в массиве, хранящем исходную матрицу B, который соответствует первому элементу первой обрабатываемой подматрицы Bsub,1 int bBegin = … вычисляем шаг для перехода к первому элементу следующей обрабатываемой подматрицы int bStep = … объявляем переменную, в которой хранится элемент произведения, вычисляемый текущим потоком float Csub = 0.f; В цикле по всем подматрицам Asub,i, Bsub,i выполняем следующие действия for (int a = aBegin, b = bBegin; a <= aEnd; a += aStep, b += bStep) { … } ◦ объявляем в общей (shared) памяти подматрицы Asub,i и Bsub,i __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; ◦ копируем из глобальной памяти в общую элементы соответствующих подматриц (каждый поток копирует «свой» элемент) ◦ синхронизируем все потоки в блоке потоков __syncthreads(); ◦ каждый поток вычисляет свой элемент Csub в произведении подматриц Asub,i*Bsub,i ◦ синхронизируем потоки в блоке потоков конец цикла загрузить вычисленный элемент Csub в соответствующий элемент матрицы С (в глобальную память) int c = wB * BLOCK_SIZE * by + BLOCK_SIZE * bx; C[c + wB * ty + tx] = Csub; void runMultiplication(int argc, char** argv) инициализируем устройство (device) CUT_DEVICE_INIT(argc, argv); выделяем память на хосте для хранения матриц A и B unsigned int unsigned int float* h_A = unsigned int unsigned int float* h_B = size_A = WA * HA; mem_size_A = sizeof(float) * size_A; (float*)malloc(mem_size_A); size_B = WB * HB; mem_size_B = sizeof(float) * size_B; (float*)malloc(mem_size_B); инициализируем матрицы A и B случайными значениями randomInit(h_A, size_A); randomInit(h_B, size_B); выделяем память под матрицы A и B на устройстве, копируем данные с хоста на устройство float* d_A; CUDA_SAFE_CALL(cudaMalloc((void**)&d_A, mem_size_A)); float* d_B; CUDA_SAFE_CALL(cudaMalloc((void**)&d_B, mem_size_B)); CUDA_SAFE_CALL(cudaMemcpy(d_A, h_A, mem_size_A, cudaMemcpyHostToDevice) ); CUDA_SAFE_CALL(cudaMemcpy(d_B, h_B, mem_size_B, cudaMemcpyHostToDevice) ); выделяем память под матрицу C на хосте и на устройстве (имена соответствующих переменных h_C, d_C, size_C, mem_size_C) создаем и инициализируем таймер unsigned int timer = 0; CUT_SAFE_CALL(cutCreateTimer(&timer)); CUT_SAFE_CALL(cutStartTimer(timer)); определяем конфигурацию выполнения ядра (размер решетки блоков потоков и блока потоков) dim3 threads(BLOCK_SIZE, BLOCK_SIZE); dim3 grid(WC / threads.x, HC / threads.y); запускаем ядро копируем вычисленную матрицу С с устройства на хост останавливаем таймер, выводим время вычислений, освобождаем ресурсы таймера CUT_SAFE_CALL(cutStopTimer(timer)); printf("Processing time: %f (ms) \n", cutGetTimerValue(timer)); CUT_SAFE_CALL(cutDeleteTimer(timer)); вычисляем то же самое произведение на CPU float* reference = (float*) malloc(mem_size_C); computeGold(reference, h_A, h_B, HA, WA, WB); сравниваем результат, полученный на GPU, с результатом, полученным на CPU (по евклидовой норме) CUTBoolean res = cutCompareL2fe(reference, h_C, size_C, 1e-6f); printf("Test %s \n", (1 == res) ? "PASSED" : "FAILED"); if (res!=1) printDiff(reference, h_C, WC, HC); освобождаем память Nvidia CUDA Programming Guide Многочисленные курсы по CUDA: ◦ http://courses.ece.uiuc.edu/ece498/al1/Syllabus.html ◦ http://www.nvidia.ru/object/cuda_state_university_courses_ru.html (на русском языке) ?