Skip to content
GPU Pro Tip
Source: developer.nvidia.com

CUDA Consejo profesional: Aumenta el rendimiento con acceso a memoria vectorizada

Sources: https://developer.nvidia.com/blog/cuda-pro-tip-increase-performance-with-vectorized-memory-access, developer.nvidia.com

TL;DR

  • Las cargas y almacenes de memoria vectorizados pueden aumentar la utilización del ancho de banda y reducir la cantidad de instrucciones.
  • Use tipos de datos vectoriales (p. ej., int2, int4, float2, float4) mediante casting para generar instrucciones LDG.E.{64,128} y STG.E.{64,128} que procesan 64 o 128 bits por operación.
  • La alineación es obligatoria: los datos deben estar alineados al tamaño del tipo vectorial; offsets desalineados invalidan el acceso vectorizado.
  • Los accesos vectorizados pueden aumentar la presión de registros y reducir el paralelismo; los kernels con limitaciones de paralelismo pueden preferir cargas escalares.
  • Se esperan reducciones significativas de instrucciones y posibles mejoras de rendimiento, con trade-offs según la forma del kernel y la arquitectura de la GPU.

Contexto y antecedentes

Muchos kernels CUDA son limitados por la ancho de banda, y la relación entre operaciones de coma flotante y ancho de banda está aumentando en el hardware nuevo, lo que da como resultado más kernels limitados por el ancho de banda. NVIDIA describe cómo las cargas y almacenes vectorizados en CUDA C++ pueden ayudar a aumentar la utilización de la banda ancha mientras se reducen las instrucciones ejecutadas. Un kernel simple de copia de memoria y su análisis ilustran los posibles beneficios. Puedes inspeccionar el ensamblado con la herramienta cuobjdump (incluida en el CUDA Toolkit) para ver las instrucciones generadas. El kernel escalar de copia usa LDG.E y STG.E para cargar y almacenar 32 bits desde memoria global. Al cambiar a cargas vectorizadas, las operaciones se realizan en anchos de 64 o 128 bits (LDG.E.{64,128}, STG.E.{64,128}). Estas operaciones cargan y almacenan los mismos datos, pero en bloques más anchos, lo que reduce el número total de instrucciones, disminuye la latencia y mejora la utilización de la ancho de banda. Este es un recordatorio práctico de que el compilador genera instrucciones vectorizadas cuando trabajas con tipos de datos vectoriales. Por ejemplo, convertir un puntero d_in a int2* mediante reinterpret_cast (d_in) hace que el kernel trate dos enteros como una unidad. En C99, lo mismo es (int2*)(d_in). Desreferenciar esos punteros hace que el compilador genere instrucciones vectorizadas. Sin embargo, hay advertencias: los datos deben estar alineados. Si el tamaño de la estructura no es una potencia de dos, podría haber padding y desalineación. El tamaño debe ser potencia de dos para evitar estos problemas. Consideremos ahora modificar el kernel de copia de memoria para usar loads vectorizados. El bucle ahora se ejecuta N/2 veces, ya que cada iteración procesa dos elementos. Usamos el casting descrito y manejamos cualquier resto si N no es divisible por 2. Por último, se lanzan la mitad de hilos que en la versión escalar. Al inspeccionar el SASS, vemos LDG.E.64 y STG.E.64 en la versión vectorizada. Las demás instrucciones permanecen iguales; sin embargo, habrá la mitad de instrucciones ejecutadas ya que el bucle se ejecuta la mitad de veces. Este incremento de 2x en el recuento de instrucciones es muy importante en kernels limitados por instrucciones o latencia. También podemos escribir una versión vector4 del kernel de copia. En esa versión, se generan LDG.E.128 y STG.E.128. Esta versión reduce el recuento de instrucciones en un factor de cuatro. Puedes ver los resultados para los tres kernels en la figura correspondiente. En casi todos los casos, las cargas vectorizadas son preferibles a las cargas escalares. Sin embargo, las cargas vectorizadas aumentan la presión de los registros y reducen el paralelismo. Si tu kernel ya está limitado por registros o tiene parálisis muy bajo, quizá quieras mantener las cargas escalares. Además, si tu puntero no está alineado o el tamaño de tu tipo de datos no es una potencia de dos, no puedes usar cargas vectorizadas. Las cargas vectorizadas son una optimización fundamental de CUDA que aumenta el ancho de banda, reduce la cantidad de instrucciones y la latencia cuando se usan donde corresponde. Este artículo muestra cómo incorporar fácilmente cargas vectorizadas en kernels existentes con relativamente pocos cambios. Una versión de este blog apareció el 4 de diciembre de 2013 y se actualizó para reflejar el comportamiento en GPUs actuales.

Novedades

La idea central es reemplazar cargas/almacenamientos escalares por operaciones vectorizadas manejando varios elementos como una única unidad y convirtiendo punteros a tipos vectoriales como int2, int4, float2 o float4. Pasos prácticos:

  • Defina tipos de datos vectoriales y convierta punteros a estos tipos para permitir transacciones de 64 o 128 bits.
  • Garantice alineamiento: la dirección base debe estar alineada con el ancho del vector; los offsets también deben estar alineados.
  • Reemplace loads escalares por loads vectorizados: el compilador generará LDG.E.{64,128} y STG.E.{64,128} para transacciones de memoria más anchas.
  • Ajuste el bucle y la configuración de hilos: para dos elementos por iteración, el bucle es N/2; para cuatro elementos, N/4 con LDG.E.128/STG.E.128 y manejo de resto si es necesario.
  • Considere el costo de registros: los loads vectorizados pueden incrementar la presión de registros; evalúe si la ganancia compensa.
  • Verifique y compare rendimientos: inspecte el SASS para confirmar las instrucciones vectorizadas y compare el rendimiento con la versión escalar. Resultados prácticos: las versiones de dos y cuatro elementos muestran reducciones en la cantidad de instrucciones y mejoras en el rendimiento cuando las condiciones de alineación y tamaño de datos permiten su uso.

Importancia (impacto para desarrolladores/empresas)

Los kernels limitados por el ancho de banda pueden obtener beneficios significativos al hacer las transacciones de memoria más anchas. A medida que el hardware avanza hacia mayores tasas de cómputo por ancho de banda, mitigar cuellos de botella de ancho de banda se vuelve más crucial. Las cargas vectorizadas ofrecen una ruta práctica para aumentar el rendimiento de aplicaciones CUDA sin cambiar algoritmos. Para desarrolladores y empresas, esto puede significar kernels más rápidos y mejor uso de la banda de memoria. Pero la técnica no es universal: si el kernel está fuertemente limitado por registros o tiene poco paralelismo, los beneficios pueden ser modestos o incluso negativos debido a mayor presión de registros. La recomendación es evaluar caso por caso y usar cargas vectorizadas cuando el alineamiento y las características del kernel lo permiten.

Detalles técnicos o implementación

  • Identifique patrones de acceso de memoria que cargan/almacenan de forma escalar y determine si pueden tolerar transacciones más anchas.
  • Defina tipos vectoriales como int2, int4, float2, float4 (o estructuras cuyo tamaño sea potencia de dos). Realice un cast de punteros a estos tipos con reinterpret_cast para que el compilador genere loads vectorizados.
  • Garantice alineamiento: la dirección base debe estar alineada con la anchura del vector y los offsets deben estar alineados. Offsets desalineados deshabilitan los loads vectorizados.
  • Reemplace loads escalares con loads vectorizados: el compilador generará LDG.E.{64,128} y STG.E.{64,128} para transacciones más anchas.
  • Ajuste el bucle y el número de hilos: para dos elementos por iteración, el bucle es N/2 y se ejecutan la mitad de hilos; para cuatro elementos, N/4 y LDG.E.128/STG.E.128 con manejo de resto.
  • Considere el uso de registros: los loads vectorizados pueden incrementar la presión de registros y reducir el paralelismo; evalúe el impacto.
  • Validación y rendimiento: compare con la versión escalar para cuantificar ganancias y verificar corrección. Resultados prácticos esperados: las variantes vectorizadas suelen mostrar mejoras en ancho de banda y latencia cuando el alineamiento y el tamaño de datos son apropiados.

Conclusiones clave

  • Los accesos de memoria vectorizados pueden aumentar el uso del ancho de banda y reducir el número de instrucciones cuando el alineamiento y el tamaño de datos lo permiten.
  • Use tipos vectoriales (int2, int4, float2, float4) para transacciones de 64 o 128 bits vía LDG.E.{64,128} y STG.E.{64,128}.
  • El alineamiento es un requisito crucial; offsets desalineados o tamaños que no son potencias de dos pueden anular los beneficios.
  • Existen compensaciones: mayor presión de registradores y menor paralelismo; evalúe cada kernel individualmente.
  • En muchos escenarios, las cargas vectorizadas ofrecen ganancias reales de ancho de banda y latencia.

Preguntas Frecuentes

Referencias

More news