Лихогруд Николай n.lihogrud@gmail.com Часть вторая.

Презентация:



Advertisements
Похожие презентации
Лихогруд Николай Задание. Постановка.
Advertisements

Технологии высокопроизводительных вычислений на GPU и гибридных вычислительных системах Аксёнов Сергей Владимирович к.т.н., доцент каф.ОСУ ТПУ Томский.
Лихогруд Николай Часть первая.
Лекторы: Боресков А.В. (ВМиК МГУ) Харламов А.А. (NVidia) CUDA Stream. Иерархия памяти в CUDA. Глобальная память.
Лекторы: Боресков А.В. (ВМиК МГУ) Харламов А.А. (NVidia) CUDA Stream. Иерархия памяти в CUDA. Глобальная память.
Лекторы: Боресков А.В. (ВМиК МГУ) Харламов А.А. (NVidia) CUDA Stream. Иерархия памяти в CUDA. Глобальная память.
Лихогруд Николай Часть седьмая.
Лихогруд Николай Часть третья.
Структура ПО CUDA Приложения, использующие возможности CUDA для параллельной обработки данных, взаимодействуют с GPU через несколько разных программных.
Лекторы: Боресков А.В. (ВМиК МГУ) Харламов А.А. (NVidia) CUDA Stream. Иерархия памяти в CUDA. Глобальная память.
Лекция 14 Динамические данные. Виды памяти Существует три вида памяти: статическая, стековая и динамическая. Статическая память выделяется еще до начала.
Лихогруд Николай Часть шестая.
Лекция 9 Функции. Массивы-параметры функции Передача массива в функцию Пример: void array_enter(int a[], int size) { int i; for (i = 0; i < size; i++)
Половинкин А.Н.. Постановка задачи Алгоритм умножения матриц на GPU Программная реализация.
МОСКОВСКИЙ АВИАЦИОННЫЙ ИНСТИТУТ (национальный исследовательский университет) (национальный исследовательский университет) Факультет прикладной математики.
Двумерные динамические массивы. Двумерный массив - это одномерный массив, элементами которого являются одномерные массивы. Другими словами, это набор.
Основы информатики Массивы. Указатели. Заикин Олег Сергеевич
Лекция 8 Область видимости Время жизни. Область видимости Область видимости – характеристика именованного объекта Область видимости - часть текста программы,
Основы информатики Лекция. Массивы. Указатели. Заикин Олег Сергеевич
Половинкин А.Н.. Постановка задачи Алгоритм вычисления функции axpy на GPU Программная реализация.
Транксрипт:

Лихогруд Николай Часть вторая

Fermi Streaming Multiprocessor (SM) Потоковый мультипроцессор «Единица» построения устройства (как ядро в CPU): 32 скалярных ядра CUDA Core, ~1.5ГГц 2 Warp Scheduler-а Файл регистров, 128KB 3 Кэша – текстурный, глобальный (L1), константный(uniform) PolyMorphEngine – графический конвейер Текстурные юниты 16 x Special Function Unit (SFU) – интерполяция и трансцендентная математика одинарной точности 16 x Load/Store

GigaThread engine Блоки программы Варп Виртуальный блок нитей Warp Scheduler Резидентные блоки на мультипроцессоре Максимальное число блоков на SM– 8 Максимальное число варпов на SM– 48 = 1536 нитей Модель выполнения

SIMT нить варп блок программа Nvidia SIMT-все нити из одного варпа одновременно выполняют одну инструкцию, варпы выполняются независимо SIMD – все нити одновременно выполняют одну инструкцию MIMD – каждая нить выполняется независимо от других, SMP – все нити имеют равные возможности для доступа к памяти MIMD SIMD

Утилизация латентности памяти GPU: Много нитей, покрывать обращения одних нитей в память вычислениями в других за счёт быстрого переключения контекста За счёт наличия сотен ядер и поддержки миллионов нитей (потребителей) на GPU легче утилизировать всю полосу пропускания

CUDA Kernel («Ядро») Специальная функция, являющая входной точкой для кода на GPU Нет возвращаемого значения (void) Выделена атрибутом __global__ Объявления параметров и их использование такое же, как и для обычных функций Хост запускает именно «ядра», устройство их выполняет __global__ void kernel (int * ptr) { ptr = ptr + 1; ptr[0] = 100; ….; //other code for GPU }

CUDA Grid Двумерный грид из трёхмерных блоков Логический индекс по переменной z у всех блоков равен нулю Каждый блок состоит из трёх «слоёв» нитей, соответствующих z=0,1,2

Пример ядра __global__ void sum_kernel( int *A, int *B, int *C ) { int threadLinearIdx = blockIdx.x * blockDim.x + threadIdx.x; //определить свой индекс int elemA = A[threadLinearIdx]; //считать нужный элемент A int elemB = B[threadLinearIdx]; // считать нужный элемент B C[threadLinearIdx] = elemA + elemB; //записать результат суммирования }

cudaMalloc( (void**)& resultOnDevice, nb) ; // выделить память cudaMemcpy(inputDataOnDevice, inputDataOnHost, …); // переслать на устройство входные данные dim3 blockDim = dim3(512); dim3 gridDim = dim3((n – 1) / ); // рассчитать грид kernel >> (inputDataOnDevice,…); // запустить ядро На хосте

Выбор устройства, обработка ошибок, вычисление времени выполнения

Выбор устройства struct cudaDeviceProp структура с параметрами устройства. Полный список параметром см. в документации cudaError_t cudaGetDeviceCount ( int* count ) записывает в *count число доступных устройств в системе cudaError_t cudaGetDeviceProperties ( cudaDeviceProp * prop, int device ) записывает параметры устройства с индексом device в *prop cudaError_t cudaSetDevice ( int device ) выбрать устройство c индексом device для проведения вычислений

Выбор устройства int deviceCount=0, suitableDevice=-1; cudaDeviceProp devProp; // структура с параметрами устройства cudaGetDeviceCount( &deviceCount ); // число доступных устройств printf ( "Found %d devices\n", deviceCount ); for ( int device = 0; device < deviceCount; device++ ) { cudaGetDeviceProperties ( &devProp, device ); // получить параметры устройства с заданным номером printf( "Device %d\n", device ); printf( "Compute capability : %d.%d\n", devProp.major, devProp.minor); pintf( "Name : %s\n", devProp.name ); printf("Total Global Memory : %d\n", devProp.totalGlobalMem ); if (ourRequirementsPassed(devProp)) // ищем устройство с нужными параметрами suitableDevice = device ; } assert(suitableDevice != -1); cudaSetDevice(suitableDevice); // Выбрать для работы заданное устройство

Асинхронность в CUDA Чтобы GPU больше времени работало в фоновом режиме, параллельно с CPU, некоторые вызовы являются асинхронными Отправляют команду на устройство и сразу возвращают управление хосту К таким вызовам относятся: Запуски ядер ( если CUDA_LAUNCH_BLOCKING не установлена на 1) Копирование между двумя областями памяти на устройстве Копирование с хоста на устройство менее 64KB Копирования, выполняемые функциями с окончанием *Async cudaMemSet – присваивает всем байтам области памяти на устройстве одинаковое значение (чаще всего используется для обнуления)

Асинхронность в CUDA Почему тогда верно работает код? //запуск ядра (асинхронно) sum_kernel >>(aDev, bDev, cDev); //переслать результаты обратно на хост cudaMemcpy(cHost, cDev, nb, cudaMemcpyDeviceToHost); Ведь хост вызывает cudaMemcpy до завершения выполнения ядра!

cudaStream Последовательность команд для GPU (запуски ядер, копирования памяти и т.д.), исполняемая строго последовательно, следующая выполняется после завершения предыдущей Команды из разных потоков могут выполняться параллельно, независимо от исполнения команд в других потоках Пользователь сам объединяет команды в потоки. Результаты взаимодействия команд из разных потоков непредсказуемы По умолчанию, все команды помещаются в «Default Stream», равный нулю

Асинхронность в CUDA Почему тогда верно работает код? // запуск ядра (асинхронно) sum_kernel >>(aDev, bDev, cDev); //переслать результаты обратно на хост cudaMemcpy(cHost, cDev, nb, cudaMemcpyDeviceToHost); Вызов ядра и cudaMemcpy попадают в один поток (поток по умолчанию) Устройство гарантирует их последовательное выполнение

Коды всех возникающих ошибок автоматически записываются в единственную специальную хостовую переменную типа enum cudaError_t Эта переменная в каждый момент времени равна коду последней ошибки, произошедшей в системе cudaError_t cudaPeekAtLastError() – возвращает текущее значение этой переменной cudaError_t cudaGetLastError() - возвращает текущее значение этой переменной и присваивает ей cudaSuccess const char* cudaGetErrorString (cudaError_t error ) –по коду ошибки возвращает её текстовое описание Обработка ошибок

Простейший способ быть уверенным, что в программе не произошло CUDA-ошибки: В конце main вставить std::cout

События Маркеры, приписываемые точкам программы Можно проверить произошло событие или нет Можно замерить время между двумя произошедшими событиями Событие происходит когда завершаются все команды, помещённые в поток, к которому приписано событие, до последнего вызова cudaEventRecord для него Если событие приписано потоку по умолчанию (stream = 0), то оно происходит в момент завершения всех команд, помещённых во все потоки до последнего вызова cudaEventRecord для него

Измерение времени выполнения Расчёт времени выполнения ядра Записать одно событие до вызова ядра, другое – сразу после: cudaEvent_t start, stop; float time; cudaEventCreate(&start); cudaEventCreate(&stop); cudaEventRecord( start, 0 ); // В потоке могут быть некоторые команды // приписываем этой точке событие start kernel >> ( params); cudaEventRecord( stop, 0 ); // К данному моменту в поток отправлена команда вызова ядра, приписываем этой точке событие stop cudaEventSynchronize( stop ); // Дождаться событие stop, т.е. завершения выполнения всех команд, в т.ч. запуска ядра cudaEventElapsedTime( &time, start, stop ); // Время между двумя событиями cudaEventDestroy( start ); cudaEventDestroy( stop );

Как скомпилировать и запустить программу с использованием CUDA

Особое отношение к.cu При работе с CUDA используются расширения Си++: Конструкция запуска ядра >> Встроенные переменные threadIdx, blockIdx Квалификаторы __global__ __device__ и т.д. …. Эти расширения могут быть обработаны только в *.cu файлах! cudafe не запускается для файлов с другим расширением В этих файлах можно не делать #include Вызовы библиотечных функций вида cuda* можно располагать в *.cpp файлах Они будут слинкованы обычным линковщиком из библиотеки libcudart.so

Компиляция хост-кода В файле test.cpp : Основной хост-код. Т.к. конструкцию запуска ядра в *.cpp применять нельзя, вынесем её в отдельную функцию, определённую в каком-нибудь *.cu #include // здесь объявления функций тулкита void launchKernel(params); // определить эту функцию в каком-нибудь *.cu int main() { … // обычный хост-код cudaSetDevice(0); // Можно! Обычная функция, потом линкуется … // обычный хост-код, kernel >>(params) здесь нельзя, только в *.cu! launchKernel(params); // Внутри этой функции будет вызвано ядро // Определена в некотором *.cu … // обычный хост-код }

Компиляция хост-кода В файле test.cpp : Основной хост-код. Т.к. конструкцию запуска ядра в *.cpp применять нельзя, вынесем её в отдельную функцию, определённую в каком-нибудь *.cu Компиляция: $g++ -I /toolkit_install_dir/include test.cpp –c –o test.o Указали путь, по которому следует искать инклюд-файлы для CUDA Попросили сделать объект ник Или же через nvcc, без указания директории с инклюдами: $nvcc test.cpp –c –o test.o

Компиляция device-кода В файле kernel.cu Определяем ядро и функцию для его запуска. В функции запуска рассчитывается конфигурация и дополнительно выводится на экран время работы ядра __global __ void kernel(params) { ….; // код ядра } void launchKernel(params) { …;// расчёт конфигурации запуска в зависимости от параметров, создание событий float time; cudaEventRecord( start, 0 ); kernel >> (params); // запуск ядра cudaEventRecord( stop, 0 ); cudaEventSynchronize( stop ); cudaEventElapsedTime( &time, start, stop );// Время между двумя событиями printf(%4.4f, time); }

Компиляция device-кода В файле kernel.cu Определяем ядро и функцию для его запуска. В функции запуска рассчитывается конфигурация и дополнительно выводится на экран время работы ядра Компиляция: $nvcc –arch=sm_20 –Xptxas –v kernel.cu -c –o kernel.o

Линковка проекта $g++ -L/toolkit_install_dir/lib64 –lcudart test.o kernel.o –o test Попросили линковаться с libcudart.so, указали где она может лежать $nvcc test.o kernel.o –o test nvcc –v test.o kernel.o –o test покажет какая конкретно команда вызвалась Также можно расположить весь код в *.cu файле и не пользоваться *.cpp вообще

Запуск В результате компиляции и линковки получаем обычный исполняемый файл Запускаем из командной строки обычным способом $./test 1024

Глобальная память Расположена в DRAM GPU Объём до 6Gb Параметр устройства totalGlobalMem Кешируется в: L2 – на устройстве максимальный размер 1536 KB Параметр устройства l2CacheSize L1 (только на Fermi) – на каждом мультипроцессоре максимальный размер 48KB минимальный размер 16KB Device Memory L2 cache Device SM L1 cache SM L1 cache

Выделение Динамически с хоста через cudaMalloc() Динамически из ядер через malloc() Статически - глобальная переменная с атрибутом __device__

Динамически с хоста __global__ void kernel(int *arrayOnDevice) { arrayOnDevice[threaIdx.x] = threaIdx.x; } int main() { size_t size = 0; void *devicePtr = NULL; int hostMem[512]; cudaMalloc(&devicePtr, sizeof(hostMem)); cudaMemcpy(devicePtr, hostMem, size, cudaMemcpyHostToDevice); kernel >>(devicePtr); }

Статически __device__ int arrayOnDevice[512] __global__ void kernel() { arrayOnDevice[threaIdx.x] = threaIdx.x; } int main() { size_t size = 0; void *devicePtr = NULL; int hostMem[512]; cudaGetSymbolSize(&size, arrayOnDevice); cudaMemcpyToSymbol(arrayOnDevice, localMem, size); kernel >>(devicePtr); }

Статически __device__ int arrayOnDevice[512] __global__ void kernel() { arrayOnDevice[threaIdx.x] = threaIdx.x; } int main() { size_t size = 0; int hostMem[512]; void *devicePtr = NULL; cudaGetSymbolSize(&size, arrayOnDevice); cudaGetSymbolAddress(&devicePtr, arrayOnDevice); cudaMemcpy(devicePtr, hostMem, size, cudaMemcpyHostToDevice); kernel >>(); }

сuda*Symbol* Переменные с атрибутами __device__ и __constant__ находятся в глобальной области видимости и хранятся объектном модуле как отдельные символы Память под них выделяется статически при старте приложения, как и под обычные глобальные переменные Работать с ними на хосте можно через функции cudaMemcpyToSymbol, cudaMemcpyToSymbolAsync, cudaGetSymbolAddress, cudaMemcpyFromSymbol, cudaMemcpyFromSymbolAsynс, cudaGetSymbolSize

Динамически из ядер #include __global__ void kernel() { size_t size = 1024 * sizeof(int); int *ptr = (int *)malloc(size); memset(ptr, 0, size) free(ptr) } int main() { cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128*1024*1024); kernel >>(); }

malloc() из ядра выделяет память в куче Не освобождается между запусками ядер Освобождение по free() только с устройства Компилировать с –arch=sm_20 Доступны memcpy(), memset() Динамически из ядер

Память под кучу выделяется на устройстве при инициализации CUDA runtime и освобождается при завершении программы После создания размер кучи не может быть изменен По-умолчанию 8 мб Можно задать до первого вызова ядра c malloc через cudaDeviceSetLimit(cudaLimitMallocHeapSize, N) Динамически из ядер

Режимы работы кеша L1 Кеш может работать в двух режимах: 48KB и 16KB Переключение режимов: cudaDeviceSetCacheConfig(cudaFuncCache cacheConfig) Устанавливает режим работы кеша cacheConfig для всего устройства cudaFuncSetCacheConfig ( const void* func, cudaFuncCache cacheConfig ) Устанавливает режим работы кеша cacheConfig для всего отдельного ядра

Режимы работы кеша L1 cudaDeviceSetCacheConfig(cudaFuncCache cacheConfig) Возможные режимы: cudaFuncCachePreferNone – без предпочтений(по умолчанию). Выбирается последняя использованная конфигурация. Начальная конфигурация – 16KB L1 cudaFuncCachePreferShared: 16КB L1 cudaFuncCachePreferL1: 48KB L1 cudaFuncSetCacheConfig ( const void* func, cudaFuncCache cacheConfig ) По умолчанию - cudaFuncCachePreferNone - запускать с режимом устройства

Транзакции Глобальная память оптимизирована с целью увеличения полосы пропускания Отдать максимум данных за одно обращение

Транзакции Транзакция – выполнение загрузки из глобальной памяти сплошного отрезка в 128 байт, с началом кратным 128 (naturally aligned) Инструкция обращения в память выполняется одновременно для всех нитей варпа (SIMT) Выполняется столько транзакций, сколько нужно для покрытия обращений всех нитей варпа Если нужен один байт – все равно загрузится 128

Шаблоны доступа Обращения нитей варпа … Все обращения умещаются в одну транзакцию Обращения нитей варпа … Порядок не важен, главное, чтобы попадали в одну кеш-линию

Обращения нитей варпа … Запрашивается 128 байт, но обращения разбросаны в пределах трёх кеш-линий - 3 транзакции – 384 байта Обращения нитей варпа … Запрашивается 128 байт, но с не выровненного адреса 2 транзакции байт 512 Шаблоны доступа

Кеш-линии Ядра взаимодействуют не с памятью напрямую, а с кешами Транзакция – выполнение загрузки кеш-линии У кеша L1 кеш-линии 128 байт, у L байта, naturally aligned Кеш грузит из памяти всю кеш-линию, даже если нужен один байт Можно обращаться в память минуя кеш L1 Транзакции будут по 32 байта

Транзакции: L1 включен L1 L2 Варп

Транзакции: L1 выключен L1 L2 Варп

Включение \ отключение L1 Кеширование в L1 можно отключить при компиляции nvcc -Xptxas -dlcm=ca с кешированием в L1 (по умолчанию) nvcc -Xptxas -dlcm=cg В бинарном коде обращения в глобальную память будут транслированы в инструкции, не использующие кеш L1 при выполнении Различия именно на уровне бинарного кода – другие инструкции ассемблера

Обращения нитей варпа … Запрашивается 128 байт, но обращения разбросаны по пяти кеш-линиям L2 5 транзакций по 32 – 160 байт Обращения нитей варпа … Запрашивается 128 байт, но с невыровненного адреса. Умещаются в четыре кеш-линии L2 4 транзакции по 32 байта – 128 байт Шаблоны доступа: L1 выключен

Вывод Если в ядре не используется общая память (см. далее), то заведомо стоит включить cudaFuncCachePreferL1 Если разреженный доступ – кеширование в L1 отключаем В общем случае, стоит проверить производительность работы всех 4-х вариантов: (-dlcm=ca, -clcm=cg)x(16KB, 48KB)

Матрицы и глобальная память Пусть транзакция – 8*4=32 байта, адрес транзакции выровнен по 32 байта Если ширина матрицы не кратна 32 байтам – большая часть строк не выровнена

Матрицы и глобальная память Обращения варпа 2 транзакции!

Матрицы и глобальная память Матрицы хранятся в линейном виде, по строкам Пусть длина строки матрицы – 480 байт (120 float) обращение – matrix[idy*120 + idx] Строка 0 Строка 1Строка 2 Строка 3 Адрес начала каждой строки, кроме первой, не выровнен по 128 – обращения варпов в память будут выполняться за 2 транзакции

Матрицы и глобальная память Дополним каждую строку до размера, кратного 128 байтам – в нашем случае, = 512, это наш pitch – фактическая ширина в байтах Эти байты никак не будут использоваться, т.е. 32/512=6% лишней памяти будет выделено (Но для больших матриц эта доля будет существенно меньше) Зато каждая строка будет выровнена по 128 байт Обращение matrix[idy*128+ idx] Строка 0 Строка 1Строка 2Строка 3 …. Адрес начала каждой строки выровнен по 128! Обращения варпов в память выполняются за одну транзакцию Pitch in bytes Padding (набивка)

Матрицы и глобальная память Обращения варпа 1 транзакция! 15

Выделение памяти с «паддингом» cudaError_t cudaMallocPitch (void ** devPtr, size_t * pitch, size_t width, size_t height) width – логическая ширина матрицы в байтах Выделяет не менее width * height байтов, может добавить в конец строк набивку, с целью соблюдения выравнивания начала строк сохраняет указатель на память в (*devPtr) сохраняет фактическую ширину строк в байтах в (*pitch)

Выделение памяти с «паддингом» cudaError_t cudaMallocPitch (void ** devPtr, size_t * pitch, size_t width, size_t height) Адрес элемента (Row, Column) матрицы, выделенной при помощи cudaMallocPitch: T* pElement = (T*)((char*) devPtr + Row * pitch) + Column

Копирование в матрицу с padding-ом cudaError_t cudaMemcpy2D ( void* dst, size_t dpitch, const void* src, size_t spitch, size_t width, size_t height, cudaMemcpyKind kind ) dst - указатель на матрицу, в которую нужно копировать, dpitch – фактическая ширина её строк в байтах src - указатель на матрицу из которой нужно копировать, spitch – фактическая ширина её строк в байтах width – сколько байтов каждой строки нужно копировать height – число строк kind – направление копирования (как в обычном cudaMemcpy)

Копирование в матрицу с padding-ом cudaError_t cudaMemcpy2D ( void* dst, size_t dpitch, const void* src, size_t spitch, size_t width, size_t height, cudaMemcpyKind kind ) Из начала каждой строки исходной матрицы копируется по width байтов. Всего копируется width*height байтов, при этом Адрес строки с индексом Row определяется по фактической ширине: (char*)src + Row* spitch – в матрице-источнике (char*)dst + Row* dpitch – в матрице-получателе

Обращение к матрице по столбцам? Матрица расположена по строкам, а обращение идёт по столбцам Обращения нитей варпа Каждая нить варпа обращается в свою строку к элементу в столбце j j 1024+j 1536+j2048+j Если матрица имеет размер больше 128 байт, то эти обращения ни за что не «влезут» в одну транзакцию!

Транспонировать ! Решение – хранить матрицу в транспонированном виде! В этом случае обращения по столбцам превратятся в обращения к последовательным адресам Выделять память под транспонированную матрицу также через cudaMallocPitch

Массивы структур? struct example { int a; int b; int c; } __global__ void kernel(example * arrayOfExamples) { int idx = threadIdx.x + blockIdx.x * blockDim.x; arrayOfExamples[idx].c = arrayOfExamples[idx].b + arrayOfExamples[idx].a; } a a b b Обращения нитей варпа …. Обращение варпа в память будет выполняться в три транзакции лишних байтов c c a a b b c c a a b b c c a a b b c c a a b b c c … a a b b c c a a b b c c a a b b c c

Структура с массивами! struct example { int *a; int *b; int *c; } __global__ void kernel(example arrayOfExamples) { int idx = threadIdx.x + blockIdx.x * blockDim.x; arrayOfExamples.c[idx] = arrayOfExamples.b[idx] + arrayOfExamples.a[idx]; } a a Обращения нитей варпа Обращение варпа в память будет выполняться за одну транзакцию a a a a a a a a a a a a … b b b b b b b b b b b b b b c c c c c c c c c c c c c c …

Косвенная адресация Требует двух чтений из памяти сначала A[i], потом A[i][j] При первом чтении варпу нужно всего 4 байта, а скачается 128 float **A; A[i][j] = 1;

Выводы Обращения нитей варпа в память должны быть пространственно- локальными Начала строк матрицы должны быть выровнены Массивы структур -> структура с массивами 16КB vs 48KB L1 Избегаем косвенной адресации Избегаем обращений нитей варпа к столбцу матрицы В случае сильно разреженного доступа проверяем работу с отключенным кешем

end