Buscar..


Introducción

Este tema presenta algunos de los mecanismos básicos subyacentes de la computación paralela que son necesarios para comprender completamente y utilizar OpenCL.

Hilos y Ejecución

La clave del paralelismo es usar varios subprocesos para resolver un problema (duh.) Pero hay algunas diferencias con la programación clásica de subprocesos múltiples en cómo se organizan los subprocesos.

Primero hablemos de su GPU típica, por simplicidad, me centraré en

Una GPU tiene muchos núcleos de procesamiento, lo que la hace ideal para ejecutar muchos subprocesos en paralelo. Esos núcleos están organizados en Streaming Processors (SM, término de NVidia), de los cuales una GPU tiene un número dado.

Todos los subprocesos que se ejecutan dentro de un SM se denominan "bloque de subprocesos". Puede haber más hilos en un SM que tiene núcleos. El número de núcleos define el llamado 'Tamaño de deformación' (término NVidia). Los hilos que se encuentran dentro de un bloque de hilos están programados en las denominadas "deformaciones".

Un rápido ejemplo de seguimiento: un NVidia SM típico tiene 32 núcleos de procesamiento, por lo tanto, su tamaño de deformación es 32. Si mi bloque de hilos ahora tiene 128 hilos para ejecutar, se procesarán en 4 deformaciones (4 deformaciones * 32 tamaño de deformación = 128 trapos).

El tamaño de la urdimbre es bastante importante al elegir el número de subprocesos más adelante.

Todos los hilos dentro de una sola urdimbre comparten un solo contador de instrucciones. Eso significa que esos 32 subprocesos están verdaderamente sincronizados, ya que cada subproceso ejecuta todos los comandos al mismo tiempo. Aquí hay un error de rendimiento: ¡Esto también se aplica a las declaraciones de ramificación en su núcleo!

Ejemplo: tengo un kernel que tiene una sentencia if y dos ramas. 16 de mis hilos dentro de una urdimbre ejecutarán la rama uno, los otros 16 la rama dos. Hasta la instrucción if, todos los hilos dentro de la deformación están sincronizados. Ahora la mitad de ellos elige una rama diferente. Lo que sucede es que la otra mitad permanecerá inactiva hasta que la declaración incorrecta haya terminado de ejecutarse en los primeros 16 subprocesos. Entonces esos hilos estarán inactivos hasta que los otros 16 hilos terminen su rama.

Como puede ver, los malos hábitos de bifurcación pueden ralentizar considerablemente su código paralelo, porque ambas declaraciones se ejecutan en el peor de los casos. Si todos los hilos dentro de una deformación deciden que solo necesitan una de las declaraciones, la otra se omite por completo y no se produce ningún retraso.

Sincronizar hilos tampoco es un asunto simple. Sólo puede sincronizar los hilos withing un único SM. Todo lo que se encuentra fuera del SM es inescrutable desde el interior del núcleo. Tendrás que escribir núcleos separados y lanzarlos uno tras otro.

Memoria de la GPU

La GPU ofrece seis regiones de memoria diferentes. Se diferencian en su latencia, tamaño y accesibilidad de diferentes hilos.

  • Memoria global: la memoria más grande disponible y una de las pocas para intercambiar datos con el host. Esta memoria tiene la mayor latencia y está disponible para todos los subprocesos.
  • Memoria constante: una parte de solo lectura de la memoria global, que solo puede ser leída por otros hilos. Su ventaja es la menor latencia en comparación con la memoria global.
  • Memoria de textura: también una parte de la memoria constante, específicamente diseñada para texturas
  • Memoria compartida: esta región de memoria se coloca cerca del SM y solo se puede acceder a ella mediante un único bloque de subproceso. Ofrece una latencia mucho menor que la memoria global y un poco menos de latencia que la memoria constante.
  • Registros: Solo accesible por un solo hilo y la memoria más rápida de todos. Pero si el compilador detecta que no hay suficientes registros para las necesidades del núcleo, subcontratará las variables a la memoria local.
  • Memoria local: una parte de la memoria accesible solo mediante subprocesos en la región de memoria global. Se usa como respaldo para los registros, para evitarlos si es posible.

Acceso a la memoria

El escenario típico para el uso de la memoria es almacenar los datos de origen y los datos procesados ​​en la memoria global. Cuando se inicia un bloqueo de subprocesos, primero copia todas las partes relevantes en la memoria compartida antes de obtener sus partes en los registros.

La latencia de acceso a la memoria también depende de su estrategia de memoria. Si accede ciegamente a los datos, obtendrá el peor rendimiento posible.

Los diferentes recuerdos están organizados en los llamados 'bancos'. Cada solicitud de memoria para un banco se puede manejar en un solo ciclo de reloj. El número de bancos en la memoria compartida es igual al tamaño de la deformación. La velocidad de la memoria se puede aumentar evitando el acceso al banco en conflicto dentro de una sola deformación.

Para copiar la memoria compartida desde o hacia la memoria global, la forma más rápida es "alinear" sus llamadas de memoria. Esto significa que el primer hilo en una deformación debe acceder al primer elemento en el banco de la memoria compartida y global. El segundo hilo del segundo elemento y así sucesivamente. Esta llamada se optimizará en una sola instrucción de transferencia de memoria que copia todo el banco a la memoria de destino de una sola vez.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow