stringtranslate.com

Bloque de subprocesos (programación CUDA)

Un bloque de subprocesos es una abstracción de programación que representa un grupo de subprocesos que se pueden ejecutar en serie o en paralelo. Para un mejor mapeo de procesos y datos, los subprocesos se agrupan en bloques de subprocesos. Anteriormente, la arquitectura limitaba el número de subprocesos en un bloque de subprocesos a un total de 512 subprocesos por bloque, pero a partir de marzo de 2010, con capacidad de cálculo 2.x y superior, los bloques pueden contener hasta 1024 subprocesos. Los subprocesos del mismo bloque de subprocesos se ejecutan en el mismo procesador de flujo. [1] Los subprocesos en el mismo bloque pueden comunicarse entre sí a través de memoria compartida , sincronización de barrera u otras primitivas de sincronización, como operaciones atómicas.

Se combinan varios bloques para formar una cuadrícula. Todos los bloques de la misma cuadrícula contienen el mismo número de hilos. La cantidad de subprocesos en un bloque es limitada, pero las cuadrículas se pueden usar para cálculos que requieren una gran cantidad de bloques de subprocesos para operar en paralelo y usar todos los multiprocesadores disponibles.

CUDA es una plataforma informática paralela y un modelo de programación que los lenguajes de nivel superior pueden utilizar para explotar el paralelismo. En CUDA, el kernel se ejecuta con la ayuda de subprocesos. El hilo es una entidad abstracta que representa la ejecución del kernel. Un kernel es una función que se compila para ejecutarse en un dispositivo especial. Las aplicaciones de subprocesos múltiples utilizan muchos de estos subprocesos que se ejecutan al mismo tiempo para organizar el cálculo paralelo. Cada hilo tiene un índice, que se utiliza para calcular las ubicaciones de las direcciones de memoria y también para tomar decisiones de control.

Dimensiones

CUDA opera en un modelo de programación heterogéneo que se utiliza para ejecutar programas de aplicaciones del dispositivo host. Tiene un modelo de ejecución similar a OpenCL . En este modelo, comenzamos a ejecutar una aplicación en el dispositivo host, que suele ser un núcleo de CPU . El dispositivo es un dispositivo orientado al rendimiento, es decir, un núcleo GPU que realiza cálculos paralelos. Las funciones del kernel se utilizan para realizar estas ejecuciones paralelas. Una vez que se ejecutan estas funciones del kernel, el control se devuelve al dispositivo host que reanuda la ejecución en serie.

Como muchas aplicaciones paralelas involucran datos multidimensionales, es conveniente organizar bloques de subprocesos en matrices de subprocesos 1D, 2D o 3D. Los bloques de una cuadrícula deben poder ejecutarse de forma independiente, ya que la comunicación o cooperación entre bloques de una cuadrícula no es posible. 'Cuando se inicia un kernel, se especifica el número de subprocesos por bloque de subprocesos y el número de bloques de subprocesos, esto, a su vez, define el número total de subprocesos CUDA iniciados. [2] ' Las dimensiones máximas x, y y z de un bloque son 1024, 1024 y 64, y deben asignarse de modo que x × y × z ≤ 1024, que es el número máximo de subprocesos por bloque. [3] Los bloques se pueden organizar en cuadrículas de una, dos o tres dimensiones de hasta 2 31 -1, 65,535 y 65,535 bloques en las dimensiones x, y y z respectivamente. [3] A diferencia del máximo de hilos por bloque, no existe un límite de bloques por cuadrícula distinto de las dimensiones máximas de la cuadrícula.

Indexación

indexación 1D

Cada hilo en CUDA está asociado con un índice particular para que pueda calcular y acceder a ubicaciones de memoria en una matriz.

Considere un ejemplo en el que hay una matriz de 512 elementos. Una de las estructuras de la organización es tomar una cuadrícula con un solo bloque que tiene 512 subprocesos. Considere que hay una matriz C de 512 elementos que está hecha de la multiplicación por elementos de dos matrices A y B, que tienen 512 elementos cada una. Cada hilo tiene un índice i y realiza la multiplicación del i ésimo elemento de A y B y luego almacena el resultado en el i ésimo elemento de C. i se calcula usando blockIdx (que es 0 en este caso ya que solo hay uno block), blockDim (512 en este caso ya que el bloque tiene 512 elementos) y threadIdx que varía de 0 a 511 para cada bloque.

Jerarquía de subprocesos en la programación CUDA [4]

El índice del hilo i se calcula mediante la siguiente fórmula:

blockIdx.x es el identificador del bloque de dimensión x

blockDim.x es la dimensión x de la dimensión del bloque

threadIdx.x es la dimensión x del identificador del hilo

Por lo tanto, 'i' tendrá valores que oscilan entre 0 y 511 y cubren toda la matriz.

Si queremos considerar cálculos para una matriz mayor que 1024, podemos tener varios bloques con 1024 subprocesos cada uno. Considere un ejemplo con 2048 elementos de matriz. En este caso tenemos 2 bloques de hilos con 1024 hilos cada uno. Por lo tanto, los valores de los identificadores de subprocesos variarán de 0 a 1023, el identificador de bloque variará de 0 a 1 y la dimensión del bloque será 1024. Por lo tanto, el primer bloque obtendrá valores de índice de 0 a 1023 y el último tendrá valores de índice. de 1024 a 2047.

Así, cada hilo calculará primero el índice de memoria al que tiene que acceder y luego procederá con el cálculo. Considere un ejemplo en el que los elementos de las matrices A y B se agregan en paralelo mediante el uso de subprocesos y los resultados se almacenan en una matriz C. El código correspondiente en un subproceso se muestra a continuación: [5]

__global__ void vecAddKernel ( flotante * A , flotante * B , flotante * C , int n ) { int index = blockIdx . x * bloqueDim . x + hiloIdx . X ; si ( índice < n ) { C [ índice ] = A [ índice ] + B [ índice ] ; } }                                 

indexación 2D

De la misma manera, en grillas particularmente complejas, cada hilo debe calcular el blockId y el threadId dependiendo de la geometría de la grilla. Considere una cuadrícula bidimensional con bloques bidimensionales. El threadId y el blockId se calcularán mediante las siguientes fórmulas:

[6]

Perspectiva del hardware

Aunque hemos establecido la jerarquía de subprocesos, debemos tener en cuenta que los subprocesos, los bloques de subprocesos y la cuadrícula son esencialmente la perspectiva de un programador. Para obtener una idea completa del bloque de subprocesos, es fundamental conocerlo desde la perspectiva del hardware. El hardware agrupa subprocesos que ejecutan la misma instrucción en deformaciones. Varias urdimbres constituyen un bloque de hilo. Se asignan varios bloques de subprocesos a un Streaming Multiprocessor (SM). Varios SM constituyen toda la unidad GPU (que ejecuta todo el Kernel Grid). [ cita necesaria ]

Una correlación gráfica de la perspectiva de un programador versus una perspectiva de hardware de un bloque de subprocesos en GPU [7]

Transmisión de multiprocesadores

Cada arquitectura en GPU (digamos Kepler o Fermi ) consta de varios SM o multiprocesadores de streaming. Estos son procesadores de uso general con un objetivo de frecuencia de reloj baja y un caché pequeño. Un SM es capaz de ejecutar varios bloques de subprocesos en paralelo. Tan pronto como uno de sus bloques de subprocesos haya completado su ejecución, ocupará el siguiente bloque de subprocesos en serie. En general, los SM admiten el paralelismo a nivel de instrucción pero no la predicción de bifurcaciones . [8]

Una ilustración de un multiprocesador de streaming y sus recursos [9]

Para lograr este propósito, un SM contiene lo siguiente: [8]

  1. Caché L1 . (para reducir la latencia de acceso a la memoria).
  2. Memoria compartida . (para datos compartidos entre subprocesos).
  3. Caché constante (para transmisión de lecturas desde una memoria de solo lectura).
  4. Caché de textura . (para agregar ancho de banda desde la memoria de texturas).

El hardware programa bloques de subprocesos en un SM. En general, un SM puede manejar múltiples bloques de subprocesos al mismo tiempo. Un SM puede contener hasta 8 bloques de subprocesos en total. Un ID de subproceso es asignado a un subproceso por su SM respectivo.

Siempre que un SM ejecuta un bloque de subprocesos, todos los subprocesos dentro del bloque de subprocesos se ejecutan al mismo tiempo. Por lo tanto, para liberar la memoria de un bloque de subprocesos dentro del SM, es fundamental que todo el conjunto de subprocesos del bloque haya concluido su ejecución. Cada bloque de hilo se divide en unidades programadas conocidas como urdimbre. Estos se analizan en detalle en la siguiente sección.

Una ilustración de un programador de doble deformación implementado en la microarquitectura Fermi de Nvidia [10]

El programador warp de SM decide cuál de los warp tiene prioridad durante la emisión de instrucciones. [11] Algunas de las políticas de priorización de warp también se han discutido en las siguientes secciones.

Deformaciones

En el lado del hardware, un bloque de hilos se compone de 'urdimbres'. (Este término proviene del tejido . [12] ) Una urdimbre es un conjunto de 32 hilos dentro de un bloque de hilos de manera que todos los hilos en una urdimbre ejecutan la misma instrucción. Estos hilos son seleccionados en serie por el SM. [13]

Una vez que se lanza un bloque de subprocesos en un multiprocesador (SM), todos sus warps permanecen residentes hasta que finaliza su ejecución. Por lo tanto, no se lanza un nuevo bloque en un SM hasta que haya suficiente número de registros libres para todos los warps del nuevo bloque y hasta que haya suficiente memoria compartida libre para el nuevo bloque.

Considere una urdimbre de 32 hilos ejecutando una instrucción. Si uno o ambos operandos no están listos (por ejemplo, aún no han sido recuperados de la memoria global), tiene lugar un proceso llamado ' cambio de contexto ' que transfiere el control a otro warp. [14] Al salir de una deformación en particular, todos los datos de esa deformación permanecen en el archivo de registro para que pueda reanudarse rápidamente cuando sus operandos estén listos. Cuando una instrucción no tiene dependencias de datos pendientes, es decir, ambos operandos están listos, se considera que el warp respectivo está listo para su ejecución. Si más de un warp es elegible para ejecución, el SM principal usa una política de programación de warp para decidir qué warp recibe la siguiente instrucción recuperada.

A continuación se analizan diferentes políticas para programar warps que son elegibles para ejecución: [15]

  1. Round Robin (RR): las instrucciones se obtienen en forma round robin. RR se asegura de que los SM se mantengan ocupados y no se desperdicien ciclos de reloj en latencias de memoria.
  2. Obtenido menos recientemente (LRF): en esta política, el warp para el cual la instrucción no se ha recuperado durante más tiempo tiene prioridad en la recuperación de una instrucción.
  3. Justo (JUSTO) [15] - En esta política, el programador se asegura de que todos los warps tengan una oportunidad "justa" en la cantidad de instrucciones obtenidas para ellos. Obtiene instrucciones para un warp para el cual se ha obtenido un número mínimo de instrucciones.
  4. CAWS basado en bloques de subprocesos [16] (programación de deformación consciente de la criticidad): el énfasis de esta política de programación está en mejorar el tiempo de ejecución de los bloques de subprocesos. Asignó más recursos de tiempo a la deformación que tardará más en ejecutarse. Al dar prioridad a la deformación más crítica, esta política permite que los bloques de subprocesos finalicen más rápido, de modo que los recursos estén disponibles más rápidamente.

El "cambio" de contexto de subproceso de CPU tradicional requiere guardar y restaurar los valores de registro asignados y el contador del programa en la memoria fuera del chip (o caché) y, por lo tanto, es una operación mucho más pesada que con el cambio de contexto warp. Todos los valores de registro de un warp (incluido su contador de programa) permanecen en el archivo de registro, y la memoria compartida (y el caché) también permanecen en su lugar, ya que se comparten entre todos los warps en el bloque de subprocesos.

Para aprovechar la arquitectura warp, los lenguajes de programación y los desarrolladores deben comprender cómo fusionar los accesos a la memoria y cómo gestionar la divergencia del flujo de control. Si cada hilo en un warp toma una ruta de ejecución diferente o si cada hilo accede a una memoria significativamente divergente, entonces los beneficios de la arquitectura warp se pierden y el rendimiento se degradará significativamente.

Referencias

  1. ^ "Capítulo 4. Implementación de hardware. Los subprocesos de un bloque de subprocesos se ejecutan simultáneamente en un multiprocesador, y varios bloques de subprocesos se pueden ejecutar simultáneamente en un multiprocesador".
  2. ^ "Modelo de hilo CUDA". www.olcf.ornl.gov . Archivado desde el original el 23 de septiembre de 2016 . Consultado el 21 de septiembre de 2016 .
  3. ^ ab "Documentación del kit de herramientas CUDA: características y especificaciones técnicas". docs.nvidia.com . Consultado el 24 de mayo de 2022 .
  4. ^ "Jerarquía de subprocesos en programación CUDA" . Consultado el 21 de septiembre de 2016 .
  5. ^ Kirk, David; Hwu, Wen-mei W (28 de enero de 2010). Programación de procesadores masivamente paralelos: un enfoque práctico .
  6. ^ "Hoja de referencia de indexación de subprocesos" (PDF) . Consultado el 21 de septiembre de 2016 .
  7. ^ "Optimizaciones de subprocesos (Universidad de Mayland)" (PDF) .
  8. ^ ab Wilt, Nicholas (2013). El manual CUDA: una guía completa para la programación de GPU .
  9. ^ "Optimizaciones de subprocesos (Universidad de Mayland)" (PDF) .
  10. ^ "Optimizaciones de subprocesos (Universidad de Mayland)" (PDF) .
  11. ^ "Computación GPU con CUDA Lecture 2 - Memorias CUDA" (PDF) .
  12. ^ "Ejecución de subprocesos paralelos ISA versión 6.0". Zona de desarrolladores: documentación del kit de herramientas CUDA . Corporación NVIDIA. 22 de septiembre de 2017. Archivado desde el original el 28 de octubre de 2017 . Consultado el 27 de octubre de 2017 .
  13. ^ "Uso de primitivas de nivel de deformación CUDA". NVIDIA . 2018-01-15 . Consultado el 8 de abril de 2020 . Las GPU NVIDIA ejecutan grupos de subprocesos conocidos como warps en forma SIMT (Instrucción única, subprocesos múltiples)
  14. ^ "Problemas de memoria en CUDA y programación de ejecución en CUDA" (PDF) .
  15. ^ ab "Efecto de la búsqueda de instrucciones y la programación de la memoria en el rendimiento de la GPU" (PDF) .
  16. ^ "CAWS: programación Warp consciente de la criticidad para cargas de trabajo GPGPU" (PDF) .