Половинкин А.Н. Вычисления общего назначения на GPU Архитектура GPU Программная модель выполнения на CUDA Программирование с использованием CUDA Настольная вычислительная суперкомпьютерая система Nvidia Tesla D870 для ◦ параллельных по данным: одна и та же операция выполняется над многими данными параллельно (SIMD) ◦ в которых отношение вычислительных операций к числу операций по доступу к памяти велико CPU GPU GPU предназначен вычислений, Вместо кэша и сложных элементов управления на кристалле размещено большее число вычислительных элементов 8 SP (Streaming Processor) потоковые скалярные процессоры 2 SFU (Super Functions Unit) – предназначен для вычисления сложных математических функций RFn (Register File) Shared Memory – разделяемая память ядро (kernel) – функция, выполняемая решеткой (grid) блоков потоков (threads block) блок потоков (threads block) – набор потоков, выполняющих одну функцию (kernel) на одном мультипроцессоре, способных общаться между собой посредством: ◦ разделяемой памяти memory) ◦ точек синхронизации (shared два потока из двух различных блоков не взаимодействуют между собой каждый поток и блок потоков имеют идентификаторы ◦ каждый поток может определить, с какими данными он должен работать Block ID (1D или 2D) Thread ID (1D, 2D или 3D) данный подход упрощает адресацию памяти при обработке многомерных данных registers (чтение/запись, одним SP*) local (чтение/запись, одним SP) shared (чтение/запись, всеми SP, входящими в состав MP**) constant cache texture cache (только чтение, всеми SP, входящими в состав MP) (только чтение, всеми SP, входящими в состав MP) device (global) (чтение/запись, всеми SP, входящими в состав всех MP) *SP – scalar processor **MP – multiprocessor данные, расположенные в глобальной памяти, реально располагаются в памяти устройства (доступ к device memory много медленнее доступа к shared memory) общий подход к ускорению вычислений заключается в следующем: ◦ разбить множество обрабатываемых данных на подмножества, убирающиеся в shared memory ◦ обрабатывать каждое подмножество данных одним блоком потоков: загрузить подмножество данных из global memory в shared memory выполнить вычисления над элементами данных из подмножества скопировать результаты из shared memory в global memory host = CPU device = GPU = набор мультипроцессоров device memory = собственная память GPU kernel (ядро) – подпрограмма, выполняемая на GPU grid (решетка) – массив блоков потоков, которые выполняют одно и то же ядро thread block (блок потоков) – набор потоков, которые выполняют ядро и могут взаимодействовать, используя общую память (shared memory) Стандартный язык C для разработки параллельных приложений на GPU Библиотеки FFT (Fast Fourier Transform) и BLAS (Basic Linear Algebra Subroutine) Специализированный драйвер для вычислений, обеспечивающий быструю передачу данных между CPU и GPU Драйвер CUDA, обеспечивающий взаимодействие с OpenGL и DirectX Поддержка видеокарт NVidia >= G80, Tesla, Quadro Поддержка Windows XP 32/64bit, Windows Vista 32/64 bit, Linux 32/64bit, Mac OS Комплект поставки ◦ CUDA driver ◦ CUDA toolkit ◦ CUDA SDK API представляет собой расширение языка C Состав CUDA API: ◦ расширения языка C ◦ библиотека времени выполнения (runtime library): общий компонент, обеспечивающий встроенные векторные типы и подмножество C runtime library поддерживающее как host, так и device код host component, обеспечивающий управление и доступ к одному или нескольким устройствам с хоста device component, обеспечивающий функции, специфичные для устройства Квалификатор Выполняется на: Вызывается с: __device__ device device __host__ host host __global__ device host __global__ - определяет функцию ядро (kernel) должна возвращать результат типа void __device__ и __host__ могут использоваться совместно невозможно взять адрес __device__ функции функции, выполняемые на устройстве, не допускают: рекурсию объявление статических переменных внутри функции переменное число аргументов Пример: __global__ void KernelFunc(float arg); __device__ объявляет переменную, размещаемую на GPU __constant__ объявляет переменную, которая __shared__ объявляет переменную, которая ◦ размещается в глобальном пространстве памяти; ◦ время жизни переменной совпадает со временем жизни приложения; ◦ доступ к переменной может быть осуществлен из всех потоков, выполняемых на устройстве, а также с хоста через библиотеки времени выполнения. ◦ размещается в константном пространстве памяти; ◦ время жизни переменной совпадает со временем жизни приложения; ◦ доступ к переменной может быть осуществлен из всех потоков, выполняемых на устройстве, а также с хоста через библиотеки времени выполнения. ◦ размещается в пространстве общей памяти блока потоков; ◦ время жизни переменной совпадает со временем жизни блока потоков; ◦ доступ к переменной может быть осуществлен из потоков, принадлежащих блоку потоков. [u]char[1..4] [u]int[1..4] [u]long[1..4] float[1..4] double2 gridDim – переменная типа dim3, содержит текущую размерность решетки; blockIdx – переменная типа uint3, содержит индекс блока потоков внутри решетки; blockDim – переменная типа dim3, содержит размерность блока потоков; threadIdx – переменная типа uint3, содержит индекс потока внутри блока потоков; warpSize – переменная типа int, содержит размер «свёртки» (warp) в потоках. Замечание: ◦ данные переменные предназначены только для чтения и не могут быть изменены из вызывающий программы перечисление устройств: выбор устройства: ◦ cudaError_t cudaGetDeviceCount(int* count) – возвращает число доступных устройств; ◦ cudaError_t cudaGetDevice (int* dev) – возвращает используемое устройство ◦ cudaError_t cudaGetDeviceProperties(struct cudaDeviceProp* prop, int dev) – возвращает структуру, содержащую свойства устройства ◦ cudaError_t cudaChooseDevice(int* dev, const struct cudaDeviceProp* prop) – устанавливает устройство, на котором выполняется device код, в наибольшей степени соответствующее конфигурации ◦ cudaError_t cudaSetDevice(int dev) – устанавливает устройство, на котором выполняется device код; Замечание: Nvidia Tesla D870 представляется в виде двух устройств выделение и устройстве: освобождение памяти на ◦ cudaError_t cudaMalloc(void** devPtr, size_t count) – выделяет память на устройстве и возвращает указатель на нее ◦ cudaError_t cudaFree(void* devPtr) – освобождает память на устройстве копирование устройством: данных между хостом и ◦ cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, enum cudaMemcpyKind kind) – копирует данные между хостом и устройством Функция ядра должна быть вызвана с указанием конфигурации исполнения Конфигурация определяется использованием выражения специального вида <<< Dg, Db, Ns>>> между именем функции и списком ее аргументов, где: ◦ Dg – определяет размерность и размер сетки, так что Dg.x * Dg.y равно числу блоков потоков, которые будут запущены, Dg.z не используется. ◦ Db – определяет размерность и размер каждого блока потоков, Db.x * Db.y * Db.z равно числу потоков на блок. ◦ Ns – переменная типа size_t, определяет число байт в разделяемой памяти, которое дополнительно выделяется на блок добавление к автоматически выделенной компилятором памяти. __global__ void KernelFunc() …. dim3 DimGrid(100, 50); dim3 DimBlock (8, 8, 8); size_t SMSize = 64; KernelFunc<<<DimGrid, DimBlock, SMSize>>>(); void __syncthreads() – синхронизирует все потоки внутри блока потоков; как только все потоки достигли данной точки, они продолжают свое выполнение используется, чтобы избежать RAW/WAR/ WAW конфликтов при доступе к shared или global памяти #include <stdio.h> #include <stdlib.h> #include <cutil.h> const int N = 256; const int DATA_SZ = N * sizeof(float); float RandFloat(float low, float high) { float t = (float)rand() / (float)RAND_MAX; return (1.0f - t) * low + t * high; } // ядро, каждый поток вычисляет сумму элементов массивов A и B с индексами, // соответствующими индексу потока __global__ void vecAdd(float* A, float* B, float* C) { int i = threadIdx.x; C[i] = A[i] + B[i]; } int main(int argc, char **argv) { float *h_A, *h_B, *h_C; float *d_A, *d_B, *d_C; int i; h_A = (float *)malloc(DATA_SZ); h_B = (float *)malloc(DATA_SZ); h_C = (float *)malloc(DATA_SZ); for(i = 0; i < N; i++) { h_A[i] = RandFloat(0.0f, 1.0f); h_B[i] = RandFloat(0.0f, 1.0f); } // инициализация GPU CUT_DEVICE_INIT(argc, argv); // выделение памяти на GPU CUDA_SAFE_CALL( cudaMalloc((void **)&d_A, DATA_SZ) ); CUDA_SAFE_CALL( cudaMalloc((void **)&d_B, DATA_SZ) ); CUDA_SAFE_CALL( cudaMalloc((void **)&d_C, DATA_SZ) ); // копирование данных с хоста на GPU CUDA_SAFE_CALL( cudaMemcpy(d_A, h_A, DATA_SZ, cudaMemcpyHostToDevice) ); CUDA_SAFE_CALL( cudaMemcpy(d_B, h_B, DATA_SZ, cudaMemcpyHostToDevice) ); // вызов ядра vecAdd<<<1, N>>>(d_A, d_B, d_C); // копирование вектора, содержащего сумму A и B с GPU на хост CUDA_SAFE_CALL( cudaMemcpy(h_C, d_C, DATA_SZ, cudaMemcpyDeviceToHost) ); // освобождение памяти на GPU CUDA_SAFE_CALL( cudaFree(d_C) ); CUDA_SAFE_CALL( cudaFree(d_B) ); CUDA_SAFE_CALL( cudaFree(d_A) ); free(h_C); free(h_B); free(h_A); // освобождение ресурсов устройства СUT_EXIT(argc, argv); } Для каждого потока в свертке: ◦ чтение операндов ◦ выполнение инструкции ◦ запись результата Для повышения производительности необходимо: ◦ уменьшить число арифметических инструкций с низким throughput ◦ максимизировать использование доступной пропускной способности для каждого типа памяти 4 clock cycles: ◦ floating point сложение, умножение, умножениесложение (multiply-add) ◦ integer сложение ◦ 24-bit integer умножение (__mul24) ◦ побитовые операции, сравнение, минимум, максимум, преобразование типов 16 clock cycles: ◦ вычисление обратного числа, 1 / sqrt(x), __logf(x) ◦ умножение 32-bit integers Целочисленное деление и взятие остатка по модулю следует заменять битовыми операциями везде, где это возможно: ◦ если n=2^p, тогда i/n ~ i>>log2(n), i%n ~ i&(n-1) 32 clock cycles: ◦ __sinf(x), __cosf(x), __expf(x) (доступны только из кода, выполняемого на устройстве) Рекомендуется использовать везде, где это возможно, floating point данные и floating point версии арифметических функций условные операторы и операторы циклов (if, switch, do, while, for) влияют на производительность приложений потоки в свёртке потоки в свёртке ... ... condition == false code 1 idle idle code2 idle code2 condition == true code 1 idle idle code2 code 1 idle if (condition) { code1; } else { code2; } Включают в себя операции чтения/записи глобальной, локальной и shared памяти. Выполнение одной инструкции доступа к памяти требует 4 clock cycles. Глобальная память обладает латентностью 400-600 clock cycles __shared__ float shared[32]; __device__ float device[32]; shared[threadIdx.x] = device[threadIdx.x]; Существуют атомарные инструкции для чтения 32-bit, 64-bit и 128-bit машинных слов. __device__ type device[32]; type data = device[tid]; sizeof(type) должен быть равен 4, 8 или 16 данные должны быть выровнены по sizeof(type) Выравнивание обеспечивается компилятором автоматически для встроенных типов данных (float2, float4, …) Размер и выравнивание структур обеспечивается директивой компилятора __align__ struct __align__(8) { float a; float b; }; struct __align__(16) { float a; float b; float c; }; 1 64-bit load instruction 1 128-bit load instruction Структуры размера больше 16 байт следует определять, используя __align__(16) struct { float float float float float }; a; b; c; d; e; 5 32-bit load instructions struct __align__(16) { float a; float b; float c; float d; float e; }; 2 128-bit load instructions доступ к глобальной памяти всеми потоками половины свертки (half warp), объединяется в 1 или 2 инструкции, при выполнении условий: ◦ потоки совершают доступ к 32-bit, 64-bit или 128-bit words. ◦ все 16 машинных слов должны лежать в одном и том же сегменте, размер которого равен размеру memory transaction size ◦ k-ый поток совершает доступ к k-му слову shared memory состоит из блоков памяти равного размера, доступ к которым может быть осуществлен одновременно, - банков памяти банки в shared memory организованы таким образом, что последовательно идущие 32-bit words относятся к последовательно идущим банкам памяти Bank 0 Address 0 Address 16 Bank 1 Address 1 Address 17 Bank 2 Address 2 Address 18 Bank 3 Address 3 Address 19 Bank 4 Address 4 Address 20 Bank 5 Address 5 Address 21 Bank 6 Address 6 Address 22 Bank 7 Address 7 Address 23 Bank 8 Address 8 Address 24 Bank 9 Address 9 Address 25 Bank 10 Address 10 Address 26 Bank 11 Address 11 Address 27 Bank 12 Address 12 Address 28 Bank 13 Address 13 Address 29 Bank 14 Address 14 Address 30 Bank 15 Address 15 Address 31 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... __shared__ float shared[32]; float data = shared[BaseIndex + s * tid]; tid – thread ID, s – шаг доступа к элементам массива потоки с ID tid и tid+n вызовут bank conflict, если sn кратно числу банков m пусть d = НОД(m, s), тогда для того, чтобы избежать bank conflicts, необходимо, чтобы d=1 BLAS (Basic Linear Algebra Subroutines) – набор базисных подпрограмм линейной алгебры. Данный набор является основой для функций из пакета LAPACK. Состоит из 3 уровней: 1.Операции над векторами (vector-vector) 2.Вектор-матричные операции (matrix-vector) 3.Операции над матрицами (matrix-matrix) Имя любой процедуры BLAS имеет следующую структуру: <character code><name><mod>() <character code> : символ, описывающий тип данных, с которым работает процедура. s вещественный, одинарной точности с комплексный, одинарной точности d вещественный, двойной точности z комплексный, двойной точности Для некоторых процедур и функций данные символы могут комбинироваться. Например, функция scasum принимает на вход массив комплексных чисел и возвращает вещественное значение. <name> : для BLAS Level 1 определяет тип операции (например, dot – скалярное произведение, swap – перестановка элементов векторов местами), для BLAS Level 2 и 3 определяет тип матричного аргумента. Некоторые функции BLAS возвращают индекс элемента массива. Независимо от того, какая версия библиотеки (Fortran или C) используется, элементы массива нумеруются с 1. Следовательно, при использовании C-версии из результата, который вернула функция, следует вычесть 1. #include <stdio.h> #include <mkl.h> void main() { int n = 4; float x[4] = {1., 2., 3., -4.}; float y[4] = {2., 2., -1., 10}; int incx = 1, incy = 1; float alpha = 1.0; int imax = 0; saxpy(&n, &alpha, x, &incx, y, &incy); // y := alpha*x + y } cublasInit() - инициализация CUBLAS (должна быть вызвана перед использованием любой другой CUBLAS функции) cublasShutdown() - освобождает ресурсы, используемые библиотекой CUBLAS на стороне хоста cublasAlloc(int n, int elemSize, void **devicePtr) – создает объект в пространстве памяти GPU, содержащий массив из n элементов размера elemSize. Указатель на созданный объект размещается в devicePtr (данный указатель не должен впоследствии изменяться в коде, выполняемом на хосте) cublasFree(const void *devicePtr) – освобождает память, выделенную на GPU cublasSetVector (int n, int elemSize, const void *x, int incx, void *y, int incy) – копирует n элементов вектора x (пространство памяти CPU) в вектор y (пространство памяти GPU) cublasGetVector (int n, int elemSize, const void *x, int incx, void *y, int incy) - копирует n элементов вектора x (пространство памяти GPU) в вектор y (пространство памяти CPU) #include <stdio.h> #include <cublas.h> void main() { int n = 4; float x_H[4] = {1., 2., 3., -4.}; float y_H[4] = {2., 2., -1., 10}; float* x_D = 0; float* y_D = 0; int incx = 1, incy = 1; float alpha = 1.0; cublasInit(); cublasAlloc(n, sizeof(float), &x_D); cublasAlloc(n, sizeof(float), &y_D); cublasSetVector(n, sizeof(float), x_H, incx, x_D, incx); cublasSetVector(n, sizeof(float), y_H, incy, y_D, incy); // y_D := alpha*x_D + y_D cublasSaxpy(n, alpha, x_D, &incx, y_D, &incy); cublasGetVector(n, sizeof(float), y_D, incy, y_H, incy); cublasFree(x_D); cublasFree(y_D); cublasShutdown(); } = + Настольная вычислительная суперкомпьютерная система Tesla D870 2 GPU Tesla C870 32 (2x16) мультипроцессора 256 (2x128) потоковых процессорных ядер Рабочая частота ядер – 1.35 ГГц Пропускная способность шины памяти (пиковая) – 76.8 Гб/с Пиковая производительность – 1 Tflop/s Максимальная потребляемая мощность – 520 Вт Физическое моделирование Вычислительная биология (задачи молекулярной динамики) Вычислительная химия Томография Финансовая математика Компьютерное зрение … http://www.nvidia.com/object/cuda_home.html Умножение квадратных матриц Аппаратное обеспечение ◦ NVidia Tesla C870 ◦ Intel Core2 Quad Q6600 Используемое ПО: ◦ Windows XP 32bit; ◦ Microsoft Visual Studio 2005 ◦ CUBLAS 2.0 (включен в состав CUDA Toolkit 2.0) (для Tesla C870) ◦ Intel MKL 10.0.3 (для Core2 Quad 6600) Замечание: при измерении общего времени работы алгоритма на видеокарте учитывается время загрузки данных на карту и получения данных с карты Производительность, GFlops 350 300 250 200 Core2 Quad Q6600 150 Tesla C870 Tesla D870 100 50 0 N 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 (на русском языке) ?