Arquitecturas y programación de procesadores gráficos Nicolás Guil Mata Dpto. de Arquitectura de Computadores Universidad de Málaga Indice Arquitectura de las GPUs z z Arquitectura unificada Nvidia: GT200 Diagrama de bloques Memoria ALU Planificación de threads CUDA z z z z Jerarquía de threads Acceso a memoria global Control de flujo Herramientas para depurado y profiling Un ejemplo: multiplicación de matrices Nicolás Guil Mata Computación en GPUs Rendimiento pico 1000 800 600 GFlops 400 200 0 Intel Core 2 Extreme z z ATI Radeon HD 4800 Nvidia GTX 280 Gran número de unidades funcionales Capaces de ejecutar miles de threads de forma concurrente Ocultan latencias de acceso a memoria y riesgos z Adecuado para kernels de computación (SPMD) Acelerador gráfico y propósito general z Jerarquía de memoria para soportar altos rendimientos Nicolás Guil Mata Programación de GPUs Hardware de propósito específico para procesamiento gráfico vertex program z Rasterización, ROP Programación de la operaciones sobre vértices y píxeles geometry program z z z GLSL (3DLabs y OPenGL), Cg (Nvidia), HLSL (Microsoft) CUDA (NVidia) Sh (RapidMind), Brooks (Stanford) rasterizer fragment program texture unit output merger (ROP) Nicolás Guil Mata Evolución de las GPUs Arquitectura unificada ATI R520 (2005) ATI R600 (2007) Nicolás Guil Mata Evolución de las GPUs Nvidia G70 (2005) Nvidia G80 (2006) Nicolás Guil Mata Arquitectura G80/G90/GT200 Streaming Processor Array (SPA) Streaming multiprocessor (SM) Texture Processor Cluster (TPC) Instruction L1 TPC TPC Instruction Fetch/Dispatch Data L1 SM SM Register file Shared memory TPC SP SP SP SP SFU Texture units TPC Texture L1 SFU SP SP SP SP Nicolás Guil Mata Arquitectura G80 Número variable de TPCs y SMs/TPC en las implementaciones de la arquitectura Memoria de textura de los TPCs para aplicaciones gráficas Planificación y ejecución de instrucciones en SM z Número máximo de threads asignados a un SM: 768 Memoria cache de datos constantes 8192 registros (32 bits) partionados entre los threads asignados 16 KB de memoria local (más pequeña que el banco de registros) z Particionamiento explícito de la memoria local entre los bloques asignados a un SM La memoria global es particionada entre los bloques asignados a cada SM y cada bloque sólo accede a su partición Nicolás Guil Mata Bloques de threads Max. 8 bloques Bn · B1 · · ···· ······ · · · ·· · · · ······ ···· Max. 768 threads Instruction L1 Instruction Fetch/Dispatch Asignación a SM Data L1 Register file Shared memory SP SP SP SP SFU SFU SP SP SP SP A un SM se pueden asignar, como máximo, 768 threads. Los threads son asignados a los SMs en grupos llamados bloques (máximo 8 bloques por SM) z El bloque es la unidad de asignación de threads Primitivas de sincronización (barrier) para todos los threads de un bloque Todos los threads de un bloque pueden intercambiar información a través de la memoria compartida z Cada bloque puede contener 512 threads Nicolás Guil Mata Planificación de threads Warp j (32 threads) Instrucción x de warp j Bi ···· ······ ···· Instruction Fetch/Dispatch Data L1 Register file Shared memory SP SP SP SP SFU SFU SP SP SP SP SIMT (Single Instruction Multiple Thread): Cada instrucción del programa es ejecutada por 32 threads (warp) consecutivos del bloque z z El warp es la unidad de planificación Uso de round-robing/aging para seleccionar próximo warp a planificar de los disponibles en el pool de warps listos (operandos leidos) Ya que hay 8 SPs por SM, se tardan cuatro ciclos en ejecutar una instrucción de un warp. Nicolás Guil Mata Cambios de contexto SM Warp 5 Inst. 10 Warp 23 Inst. 17 Warp 5 Inst. 11 Warp 12 Warp 12 Warp 11 Warp 15 Inst. 3 Inst. 4 Inst. 8 Inst. 1 ciclos Cambio de contexto sin coste en ciclos z probablemente ocultado por la latencia de ejecución de un warp La selección del próximo warp a ejecutar se basa en análisis de dependencias de instrucciones z Uso de la técnica del marcador (scoreboarding) para evitar riesgos Nicolás Guil Mata Jerarquía de thread Grid: Grid z Bloque (0,0) Bloque (1,0) Bloque (2,0) Bloque (0,1) Bloque (1,1) Bloque (2,1) Explicitar número de bloques en el grid Bloque: z Bloque Un bloque está compuesto de threads Explicitar número de threads del bloque Thread Thread Thread Thread (0,0) (1,0) (2,0) (3,0) z Thread Thread Thread Thread (0,1) (1,1) (2,1) (3,1) z Thread Thread Thread Thread (0,2) (1,2) (2,2) (3,2) Cada kernel de ejecución del programa se mapea en un grid Threads pueden cooperar compartiendo datos de la shared memory Threads pueden sincronizarse (barrier) Warp: z z Grupo de 32 threads consecutivos de un bloque Tamaño de warp implícito Nicolás Guil Mata Memoria on chip Cada SM tiene z Instruction L1 Instruction L1 Instruction Fetch/Dispatch Instruction Fetch/Dispatch Data L1 Data L1 Register file Shared memo ry Register file Shared memory SP SP SP SP SP SP SP SP SFU z SFU SFU SFU SP SP SP SP SP SP SP SP TP TP TP TP TP TP 8192 registros de 32 bits 4 operandos por ciclo Manejo explícito (como cache de datos) 16 Kbytes No se garantiza compartición de datos entre threads de distintos bloques Memoria compartida que permite intercambio de datos entre threads ejecutándose en el mismo SM z TP Fichero de registros particionado entre los threads asignados (R/W) TP Cache de datos constante (R) Constantes leidas de la memoria constante Contenido de la cache es leido simultáneamente por los 8 SPs − Primer acceso lento Texture cache z Cache de texturas Nicolás Guil Mata Entrelazado de la SM 16 bancos de memoria z Entrelazado de orden inferior con palabra de 32 bits Acceso simultáneo de los threads de medio warp a memoria z z Sin conflictos siempre que los threads accedan a módulos diferentes (cualquier permutación) Si todos los threads acceden a la misma palabra de un banco específico Nicolás Guil Mata Unidades funcionales por SM 8 unidades funcionales segmentadas para operaciones de suma/multiplicación flotante (32 bits) y entera 2 super unidades funcionales segmentadas para operaciones flotantes complejas Instruction L1 Instruction Fetch/Dispatch Data L1 Register file z Rendimiento pico en SM z SP z SP SP SFU SFU SP SP SP SP G80 Shared memory SP Trigonométricas, inversa de raíz cuadrada, … 18 operaciones flotantes por ciclo (8 mul/add flotante y 2 operaciones complejas) En caso de 16 SM (1.35GHz)->388.8 GFlops − 388.8 = 16 x 18 x 1.35 GT200 24 OPF por ciclo (1mul/add y 1 mul por SM) 30 SM (1.35 Ghz) -> 972 GFlops Nicolás Guil Mata Memoria off-chip TPC TPC TPC TPC TPC Accessible a todos los SMs (threads) y al host z z Global memory Off-chip z Constant memory Texture memory I/O Contenidos son mantenidos entre ejecuciones de distintos grids z Arquitectura propósito general Host Memoria global (R/W) Memoria constante (R) Memoria de textura (R/W) Intercambio de información entre threads de grids diferentes Nicolás Guil Mata Coalescing en memoria global Instrucciones pueden leer/escribir palabras de 32, 64 o 128 bits Medio warp puede acceder de forma unificada (coalesced) para leer 32, 64 o 128 bytes a segmentos de memoria global z Segmento: Trozo contínuo de memoria de 32 , 64 o 128 bytes, alineado a estos tamaños K mod 32 K mod 64 K mod 128 32 bytes 64 bytes 128 bytes MG MG MG Nicolás Guil Mata Acceso a la memoria global Condiciones de coalescing de threads con Computing Capability 1.0 y 1.1 z z z Acceso a palabras de 32 bits (1 acceso de 64 bytes), de 64 bits (1 acceso de 128 bytes) o de 128 bits (2 accesos de 128 bytes) Las 16 palabras deben estar ubicadas en un segmento de memoria de igual tamaño al de la transacción (o del doble en el caso de 128 bits) Los threads deben acceder a las palabras en secuencia (k-ésimo thread a la k-ésima palabra) Si las condiciones no se cumplen se requiere un acceso independiente para cada thread Coalesced No-coalesced Nicolás Guil Mata Acceso a la memoria global Condiciones de coalescing para Computing Capability 1.2 o mayor z z z Palabras accedidas por lo threads están ubicada en un segmento de memoria de 32 bytes (palabras de 8 bits), 64 bytes (16 bits), 128 bytes (32 o 64 bits) El patrón de acceso puede ser cualquiera e incluso varios threads pueden acceder a la misma palabra Si el medio warp accede a n segmentos, el número de accesos será n Nicolás Guil Mata ¿ Qué es CUDA ? CUDA ejecuta sobre un device físico separado que opera como coprocesador de un host z z Extensión de C Biblioteca de runtime con tres tipos de componentes: Host: control y acceso a los devices Device: funciones específicas de los devices Comunes: tipos para vectores y un bibliotecas soportadas por el host y el device CPU Memoria principal HOST I/O GPU Memoria global DEVICE Nicolás Guil Mata Jerarquía de thread Grid: Grid z Bloque (0,0) Bloque (1,0) Bloque (2,0) Bloque (0,1) Bloque (1,1) Bloque (2,1) Explicitar número de bloques en el grid Bloque: z Bloque Un bloque está compuesto de threads Explicitar número de threads del bloque Thread Thread Thread Thread (0,0) (1,0) (2,0) (3,0) z Thread Thread Thread Thread (0,1) (1,1) (2,1) (3,1) z Thread Thread Thread Thread (0,2) (1,2) (2,2) (3,2) Cada kernel de ejecución del programa se mapea en un grid Threads pueden cooperar compartiendo datos de la shared memory Threads pueden sincronizarse (barrier) Warp: z z Grupo de 32 threads consecutivos de un bloque Tamaño de warp implícito Nicolás Guil Mata Modelo de programación Extiende lenguaje C con un nuevo tipo de función de tipo kernel z Código de esta función es ejecutado en paralelo por múltiples threads en un device (GPU) Ejecución __global__ kernelA(){···} __global__ kernelB(){···} int main() ··· kernelA <<<dimGridA, dimBlockA>>>; ··· kernelB <<<dimGridB, dimBlockB>>> ··· CPU GPU CPU GPU CPU Nicolás Guil Mata Identificación de threads Bloque z N=16 BlockIdx (built-in): vector (1D, 2D o 3D) que identifica el bloque dentro del grid. Thread z ThreadIdx(built-in): vector (1D, 2D o 3D) que identifica el thread dentro del bloque __global__ void matAdd (float A[N][N], float B[N][N], float C[N][N]) { int i = blockIdx.x * blockDim.x + threadIdx.x; int j = blockIdx.y * blockDim.y + threadIdx.y; C[i][j]=A[i][j]+B[i][j] { int main(){ dim3 dimBlock(4,4); dim3 dimGrid (N/ dimBlock.x, N/ dimBlock.y); matAdd<<<dimGrid, dimBlock)>>>(A, B, C); Matriz A blockIDX=(3,2) threadIdx=(3,3) BlockDim.x blockDim.y ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· ···· Grid } Nicolás Guil Mata Acceso a memoria Memoria local Thread Bloque On-chip Memoria compartida ··· ··· ··· ··· ··· ··· ··· Grid 0 Memoria Memoria global constante Grid 1 ··· ··· ··· ··· ··· ··· ··· ··· ··· (R/W) (R) Memoria textura Off-chip (R) Nicolás Guil Mata Control de flujo Cada SM ejecuta los 32 threads de un warp (SPMD) z Todos los threads ejecutan la misma instrucción Si bifurcaciones afectan de forma diferente a los threads de un warp (divergencia), se secuencializa la ejecución de los threads del warp z Pérdida de rendimiento if (ThreadIdx.x > 2) if (ThreadIdx / WARP_SIZE >2) { { } } Divergencia Sin Divergencia Nicolás Guil Mata Instrucciones predicadas Transformar la dependencia de control en una de datos z En el caso de la GPU evitamos el problema de la divergencia Idea interesante si hay pocas instrucciones en el ifelse ld r5, X if (x == 10) c=c+1; P1 <- r5 eq 10 P1: ld r1, c P1: add r1, r1, 1 P1: st r1, c Nicolás Guil Mata Predicados en CUDA La instrucciones predicadas permiten la escritura si la condición se cumple z Se evitan los accesos a memoria (loads y stores) de instrucciones que no cumplen condición El compilador analiza el posible grado de divergencia de warps z z Si deduce que no hay divergencia sólo aplica predicados si hay menos de 4 instrucciones Si no hay garantías predica con menos de 7 instrucciones Nicolás Guil Mata Coalescencing en 1-D arrays Acceso a arrays de estructuras z Si tamaño estructura mayor de 16 bytes, crear varios arrays de estructuras de este tamaño como máximo struct{ float var1; float var2; · · · float var8; } big; struct big[10000]; struct{ float var1; · · · float var4; } half1; struct{ float var5; · · · float var8; } half2 struct half1[10000]; struct half2[10000]; Nicolás Guil Mata Coalescencing en arrays 2-D En arrays de estructura 2-D con acceso a elementos cotigüos de la fila: z Ancho de la fila debe ser múltiplo de 16 (por alineamiento) float array[2][16] 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 Segmentos Nicolás Guil Mata Reducción sin conflictos en SM Acceso de cada thread activo a módulos diferentes en el mismo ciclo float A[64]; load r1, A[tid]; load r2, A[tid+32]; add r2,r2,r1 store r2, B[tid]; 0 1 2 3 4 5 6 7 8 9 16 17 18 19 20 21 22 23 24 25 32 33 34 35 36 37 38 39 40 41 48 49 50 51 52 53 54 55 56 57 10 26 42 58 11 27 43 59 12 28 44 60 13 29 45 61 14 30 46 62 15 31 47 63 // Sólo si tid <8 load r1, B[tid]; load r2, B[tid+8]; add r2,r2,r1 store r2, B[tid]; 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Sólo si tid <16 load r1, B[tid]; load r2, B[tid+16]; add r2,r2,r1 store r2, B[tid]; 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // Sólo si tid <4 load r1, B[tid]; load r2, B[tid+4]; add r2,r2,r1 store r2, B[tid]; 0 1 2 3 4 5 6 7 Nicolás Guil Mata Reducción y divergencia Divergencia presente cuando stride<=16 z Sólo threads con tid < 16, 8, 4, y 2 computan __shared__ float partialSum(); unsigned int t= threadIdx.x; for (unsigned int stride = blockDim.x; stride >1; stride >>1) { __syncthreads(); if (t < stride) partialSum[t] += partialSum[t+stride]; } z No hay conflictos en acceso a la SM Nicolás Guil Mata Compilando con NVcc NVcc genera dos tipos de código: z z Host: donde finalmente se usará un compilador de la CPU para generar el código final Device: código PTX que incluye los kernel y funciones llamadas por ellos Los dos códigos serán enlazados (binded) por el runtime z Comunicaciones entre estos códigos será realizada por el driver y la biblioteca de transferencia Nicolás Guil Mata PTX (Parallel thread execution) Código para máquina virtual NVCC z Código para el device z Código PTX Basado en ISA Expone todos los recursos computacionales Traductor Traductor C ··· z Replanifica y optimiza ligeramente el código en función de la arquitectura real durante el runtime G80 Nicolás Guil Mata Ejemplo Multiplicación de matrices Nicolás Guil Mata Arquitectura G80 388.8 GFlops GRID SM0 ·········· SM15 16 KB shared memory 32 KB register file 8 KB constant c. z Memoria global: z SP0 · · · · · · · · · · · · · · SP7 SFU0 16 SMs * 18 FLOPS/SM * 1.35 GHz z 364 bits 84.6 GB/s SFU1 756 MB global memory Nicolás Guil Mata Organización del computo Recomendable crear gran cantidad de threads para ocultar latencias con la memoria (evitando sincronizaciones) z z Usar un sólo grid con tantos threads como elementos haya en la matriz C Cada thread se encargará de realizar el cómputo de un elemento de la matriz C Grid · · · · Bloque ······ ······· ···· ···· ······ ···· ··············· · · · · Th(x,y) ······ ······· ···· ···· ······ ···· WidthA WidthB HeightA C(x, y) C = HeightA WidthB X A B dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE); dim3 dimGrid(WidthB/BLOCKSIZE, HeightA/BLOCKSIZE); Nicolás Guil Mata Versión simple GRID SM0 ·········· SM15 16KB shared memory 32 KB register file 8 KB constant c. SP0 · · · · · · · · · · · · · · SP7 SFU0 Ctemp=0; for (i=0; i<widthA; i++){ Ctemp += A[indexA] * B[indexB]; indexA++; indexB += width } C[indexC] = Ctemp ; Coalescing en acceso a matriz B SFU1 10 registros por thread ->768 threads: SM agrupados en tres bloques de 256 Rendimiento: 10.58 GFLOPS A B Memoria global Cuello de botella (mirar PTX) -> acceso a memoria global Nicolás Guil Mata Tiling en memoria compartida Cada thread carga un elemento de a y b del tile GRID ·········· SM0 SM15 As Bs ···· ···· ···· ···· ···· ···· 32 KB reg. file Shared Memory 8 KB constant c. SP0 · · · · · · · · · · · · · · SP7 SFU0 Ctemp=0; for (···){ __shared__ float As[16][16]; __shared__ float Bs[16][16]; SFU1 ···· ···· ···· ···· ···· ···· // Load tile (16x16) As[ty][tx] = A[indexA]; Bs[ty][tx] = B[indexB]; indexA += 16; indexB += 16 * widthB; __syncthreads(); // Compute results from tile for (i=0; i<16; i++) Ctemp+=As[ty][i]*Bs[i][tx]; __syncthreads(); } C[indexC] = Ctemp ; A B Memoria global Nicolás Guil Mata Desenrrolle Optimización automática del compilador Ctemp=0; for (···){ __shared__ float As[16][16]; __shared__ float Bs[16][16]; // Load tile (16x16) As[ty][tx] = A[indexA]; Bs[ty][tx] = B[indexB]; indexA += 16; indexB += 16 * widthB; __syncthreads(); // Compute results from tile Ctemp+=As[ty][0]*Bs[0][tx]; ···· Ctemp+=As[ty][15]*Bs[15][tx]; __syncthreads(); } C[indexC] = Ctemp ; Nicolás Guil Mata Tamaño del tile Rendimiento 100 90 80 70 60 GFlops 50 40 30 20 10 0 tiled only tiled & unrolled 4x4 8x8 12x12 16x16 Nicolás Guil Mata Conclusiones GPU puede ser usada como dispositivo acelerador de código basado en kernels z z Eficiencia basada en la ejecución concurrente de miles de threads Gran número de unidades funcionales Optimización de código compleja z z z Optimizar accesos a memoria global Gestión explícita de la memoria compartida Ajuste fino de tamaño de bloque Aspectos no visibles z Asignación y planificación de threads Nicolás Guil Mata