Comprensión de la programación NVIDIA GPU SM y CUDA

Fundamentos de la arquitectura de hardware SM

Para cambios en diferentes arquitecturas, consulte:

​​​​​​Una revisión de los cambios en la arquitectura de la GPU desde la perspectiva de los sistemas de IA: de Fermi a Ampere (V1.2) bazyd

La arquitectura GPU de Nvidia ha evolucionado durante casi una década, de Fermi a Ampere: se busca programador

Volta GV100 Streaming Multiprocesador (SM)

Multiprocesador de transmisión GA100 (SM)

Multiprocesador de transmisión GA102 (SM)

Lo anterior muestra las diferencias entre varios SM de arquitectura diferente, y es necesario señalar algunas similitudes y diferencias notables:

Cada SM se divide en 4 subbloques, preste atención a qué partes comparten estos 4 subbloques y qué partes son independientes de estos 4 subbloques.

Por ejemplo, la memoria compartida y el caché L1 son compartidos por todos los subbloques SM4, mientras que el archivo de registro, el núcleo cuda, etc. son independientes para cada subbloque. Estos son instructivos para la práctica y comprensión de la programación CUDA.

Preste atención a la cantidad de núcleos cuda en cada subbloque. Por ejemplo, cada subbloque de GV100 GA100 tiene 16 núcleos cuda INT32 y FP32, 8 núcleos cuda FP64 y 4 SFU, mientras que GA102 no tiene núcleos cuda FP64. La última arquitectura de tolva no tiene 16 sino 32 núcleos cuda FP32 por subbloque.

Preste atención a la cantidad de TensorCores para cada subbloque y sus especificaciones de parámetros específicos.

Fundamentos del modelo de programación CUDA SIMT

Algunas cuestiones que deben aclararse:

¿Qué tiene que ver el significado de CUDA core con los hilos?

¿La relación entre warp, thread block y SM?

¿Diferente entendimiento de cambio de deformación?

Los programas escritos por la programación de la CPU generalmente se ejecutan en serie por un solo hilo. En SIMD (datos múltiples de instrucción única), una instrucción se aplica a muchos elementos de datos al mismo tiempo. Las GPU Nvidia, por otro lado, utilizan el modelo SIMT (instrucción única, múltiples subprocesos) para la computación paralela.

Primero necesitamos escribir una función del kernel, y eventualmente se crearán miles de subprocesos. Cada subproceso ejecuta de forma independiente las mismas instrucciones del kernel, pero procesa datos diferentes:

Aunque cada subproceso ejecuta la misma instrucción, todos los subprocesos se gestionan según dos niveles de bloque y cuadrícula:

add<<<tamaño_cuadrícula, tamaño_bloque>>> (a, b, c); 

Cada bloque de bloque de subprocesos contiene docenas o cientos de subprocesos (generalmente un múltiplo entero de 32), y los subprocesos dentro del bloque de subprocesos se ejecutan como una deformación compuesta por 32 subprocesos. Al mismo tiempo, los 32 subprocesos dentro de un warp se ejecutan estrictamente sincrónicamente (cada subproceso ejecuta la misma instrucción al mismo tiempo). Finalmente, varios bloques forman una cuadrícula completa.

Por qué esta organización corresponde directamente a la arquitectura de hardware:

En comparación con la arquitectura de hardware SM anterior, todos los subprocesos de cada bloque de subprocesos se ejecutan en el mismo SM (ejecutado por 4 subbloques), y un SM puede residir y ejecutar varios bloques de subprocesos al mismo tiempo. La GPU completa generalmente tiene docenas o cientos de SM que se pueden ejecutar, según las especificaciones específicas del hardware.

Al mismo tiempo, se ejecutan 32 subprocesos de cada deformación en el mismo subbloque de SM, y se pueden distribuir múltiples deformaciones del mismo bloque de subprocesos en múltiples subbloques de SM para su ejecución.

Cambio de deformación dentro del mismo subbloque: una de las características que diferencian a la GPU de la CPU es que el cambio de subprocesos es extremadamente rápido. Esto se debe a que los recursos utilizados por cada subproceso y bloque de subprocesos se guardan directamente en función de los recursos de hardware, en lugar de guardar primero la memoria de registro en la memoria y luego cargar la información del nuevo subproceso de la memoria al registro y luego ejecutarlo. . Aquí hay algunos puntos a tener en cuenta: 1. Cuando el warp actual está esperando algo (por ejemplo, el subproceso en el warp actual está leyendo mem global, lo que requiere cientos de relojes), cambiará a un warp nuevo para la ejecución. , que puede mejorar significativamente la utilización del hardware y el rendimiento de ejecución. 2. Por lo tanto, aunque SM solo puede ejecutar 4 warps al mismo tiempo, debe haber suficientes warps residentes para cambiar para garantizar el rendimiento. El número de subprocesos por bloque de subprocesos y el límite superior del número de bloques de subprocesos que cada SM puede ejecutar simultáneamente se pueden consultar a través de la interfaz proporcionada por CUDA. Sin embargo, dado que cada subproceso y bloque de subprocesos utiliza registros reales y recursos de hardware de memoria compartidos, los recursos de hardware son limitados. Por lo tanto, el uso de recursos de cada subproceso y bloque de subprocesos determina el número real de subprocesos contenidos en cada bloque de subprocesos y el número de bloques de subprocesos que cada SM puede ejecutar simultáneamente. Por lo tanto, el programa real debe planificar cuidadosamente la cantidad de subprocesos en cada bloque de subprocesos, los registros y los recursos de memoria compartidos utilizados por cada subproceso y bloque de subprocesos, para garantizar que el SM tenga suficientes deformaciones para ejecutar al mismo tiempo. debe haber de 4 a 8 warps que realmente se puedan ejecutar, más del doble.

El contexto de ejecución de cada warp (contador de programa, registros, etc.) se mantiene en el chip durante la vida útil del warp. Los archivos de registro, las cachés de datos y la memoria compartida se dividen entre bloques de subprocesos. Por lo tanto, no hay una penalización de costos por cambiar a otra deformación en el siguiente paso de tiempo en comparación con otros cambios de contexto. Pero la cantidad máxima predefinida de bloques de subprocesos y deformaciones que pueden residir en un SM está limitada por la capacidad de la GPU. Esta instrucción se puede seleccionar desde el mismo warp sin depender de la última instrucción, o más a menudo una instrucción de otro warp El tiempo de ejecución para muchas instrucciones aritméticas será de 2 ciclos de reloj.

La lógica de ejecución específica de cada instrucción de subproceso:

¿Cuál es la relación entre las decenas de miles de ejecución de subprocesos del programa SIMT de CUDA y el núcleo de CUDA?

Al principio, es más fácil malinterpretar a la gente como si cada hilo se ejecutara en cada CUDA CORE, lo cual no es el caso.

Podemos pensar en un núcleo como una serie de instrucciones. Suponga que la siguiente instrucción es una operación INT32. La GPU Nvidia envía un warp de 32 subprocesos a 16 unidades aritméticas INT32 para ejecutar instrucciones simultáneamente (o a 16 unidades FP32 para operaciones FP32).

Tenga en cuenta que las instrucciones de un warp 32 subprocesos se envían a 16 núcleos en lugar de 32, porque del diagrama SM anterior, se puede ver que cada subbloque SM tiene solo 16 núcleos cuda FP32 e INT32, lo que también hace que el warp se ejecute cada instrucción A FP32/INT32 en realidad requiere 2 relojes para completarse (a excepción de la tolva, porque ya tiene 32 CUDA CORE por subbloque).

De manera similar, si la siguiente instrucción es FP64, las instrucciones de 32 subprocesos en el mismo warp deben enviarse a 8 núcleos cuda FP64 para su ejecución, por lo que se requiere un ciclo más largo.

Los núcleos CUDA en Fermi brindan operaciones FP e INT (multiplexadas en el tiempo), pero de manera similar a las GPU V100 y Turing, Ampere las separa en unidades INT32, FP32 y FP64 separadas. Al separar los núcleos FP32 e INT32, permite la ejecución simultánea de operaciones FP32 e INT32 y aumenta el rendimiento de emisión de instrucciones. Muchas aplicaciones tienen bucles internos que realizan aritmética de punteros (cálculos de direcciones de memoria enteras) combinados con cálculos de coma flotante que se benefician de la ejecución simultánea de instrucciones FP32 e INT32. Cada iteración del bucle canalizado puede actualizar direcciones (aritmética de puntero INT32) y cargar datos para la siguiente iteración mientras se procesa la iteración actual en FP32.

Aquí hay otra vista de la emisión de instrucciones y la ejecución en la arquitectura Volta en un bloque de procesamiento (subnúcleo).

Algunas consideraciones y puntos de optimización de los programas CUDA

El principio básico es utilizar la GPU: un SM puede ejecutar varios bloques de subprocesos al mismo tiempo. Al mismo tiempo, una cuadrícula debe tener suficientes bloques de hilos.

Un SM puede ejecutar varios bloques de subprocesos al mismo tiempo:

Porque un SM debe tener suficientes deformaciones para poder realizar concurrencia y cambiar deformaciones para garantizar el rendimiento. El límite superior del número de warps que cada SM puede ejecutar al mismo tiempo depende del mínimo de los dos:

1. Limitaciones de hardware y configuración de parámetros del kernel (la cantidad de subprocesos y bloques de subprocesos de cada bloque de subprocesos y cada SM es fijo y se puede consultar a través de la interfaz). Cuando los recursos son suficientes y mayores que los recursos utilizados por los hilos y los bloques de hilos, la cantidad de deformaciones ejecutadas por cada SM está limitada por los parámetros establecidos por el kernel. Limitado, lo que conduce a una cantidad insuficiente de hilos ejecutados por SM al mismo tiempo. tiempo. En general, el número de subprocesos en un bloque de subprocesos debe llegar a 128 o 256 para utilizar completamente el SM. Este parámetro se puede ajustar para encontrar un valor óptimo.

2. El uso de recursos de subprocesos y bloques de subprocesos da como resultado un límite en el número que realmente se puede ejecutar. Si la memoria compartida de un bloque de subprocesos se usa demasiado, por ejemplo, un bloque de subprocesos utiliza más de la mitad de toda la memoria compartida, dicho SM solo puede ejecutar un bloque de subprocesos como máximo. Para garantizar que un SM pueda ejecutar múltiples bloques de subprocesos al mismo tiempo, obviamente cada bloque de subprocesos solo puede usar una fracción de la memoria compartida total de cada SM. Lo mismo es cierto para el uso de registros, cuando el uso de registros es razonable, un warp puede ejecutar 32 hilos al mismo tiempo, y al mismo tiempo, los recursos de un sub-bloque pueden satisfacer múltiples warps que residen en el mismo. Mismo tiempo. Sin embargo, los registros usan demasiados subbloques para residir en múltiples warps, e incluso en casos extremos, todos los recursos de un warp no son suficientes para 32 hilos.

Una cuadrícula debe tener suficientes bloques de hilos

Un kenel corresponde a una cuadrícula, y debe haber suficientes bloques de subprocesos para hacer un uso completo de todos los SM de toda la GPU. Por un lado, un SM en sí mismo debe residir en varios bloques de subprocesos, por lo que la cantidad de bloques de subprocesos utilizados por docenas o cientos de SM en toda la GPU debe multiplicarse por un múltiplo relativamente grande.

Aquí hay un ejemplo de un cálculo real de reduce/layer_norm en aprendizaje profundo. Si calculamos la media de reducción de cada fila en la dimensión más interna de un tensor [200, 768], si la idea ingenua calcula una fila por hilo, entonces el total número de hilos será de 200. De esta manera, solo se pueden generar uno o dos bloques de subprocesos y solo se pueden usar uno o dos SM. Obviamente, el rendimiento es extremadamente bajo. Y si usamos una deformación para calcular una fila, entonces hay 200 deformaciones, y si un bloque de hilos tiene 4 deformaciones, hay 50 bloques de hilos, que pueden usar la mayor parte del SM. Por supuesto, un bloque de subprocesos también se puede usar para calcular una fila, luego tenemos 200 bloques de subprocesos y la tasa de utilización de SM es mayor.

Por supuesto, hay algunas otras compensaciones en este ejemplo de reduce: debido a que reduce necesita intercambiar datos entre subprocesos, cuando se usa warp para calcular una línea, como se mencionó anteriormente, los registros de cada subproceso se guardan directamente en el hardware y la misma deformación está en Las mismas ejecuciones de subbloques SM, estos subbloques comparten el archivo de registro, y los datos compartidos de diferentes subbloques solo pueden pasar a través de la memoria compartida a la mayor velocidad. Por lo tanto, el intercambio de datos entre diferentes hilos en el mismo warp puede intercambiar datos de registro directamente a través de warp shuffle (Warp-Level Primitives), que es más rápido. Y un bloque de subprocesos necesita intercambiar datos a través de una memoria compartida primero para calcular una línea. Cómo equilibrar esta compensación depende de la cantidad de trabajo (número de elementos por fila).

Alguna información de recursos de la arquitectura de amperios:

Guía de ajuste de la arquitectura de GPU NVIDIA Ampere :: Documentación de CUDA Toolkit

1.4.1.1. Ocupación
El número máximo de warps simultáneos por SM sigue siendo el mismo que en Volta (es decir, 64) y otros factores que influyen en la ocupación de warps son:
‣ El tamaño del archivo de registro es de 64 000 registros de 32 bits por SM.
‣ El número máximo de registros por subproceso es 255.
El número máximo de bloques de subprocesos por SM es 32 para dispositivos con capacidad de cómputo 8.0 (es decir, GPU A100) y 16 para GPU con capacidad de cómputo 8.6 (GA102/104,如RTX3060等).
‣ Para dispositivos con capacidad de cómputo 8.0 (es decir, GPU A100), la capacidad de memoria compartida por SM es de 164 KB, un aumento del 71 % en comparación con la capacidad de V100 de 96 KB. Para GPU con capacidad de cómputo 8.6, la capacidad de memoria compartida por SM es de 100 KB.
‣ Para dispositivos con capacidad de cómputo 8.0 (es decir, GPU A100), la memoria compartida máxima por bloque de subprocesos es de 163 KB. Para GPU con capacidad de cómputo 8.6, la memoria compartida máxima por bloque de subprocesos es de 99 KB.

Consideraciones y jerarquía de almacenamiento de CUDA

registro

¿Qué datos se utilizarán automáticamente como registros?

El uso de la memoria compartida suele estar claramente definido y conocido por el usuario, pero ¿cómo determinar el uso de los registros?

¿Cuántos registros hay disponibles para cada subproceso? ¿Cómo evitar el uso excesivo de registros?

A diferencia de la CPU, la GPU usa diferentes registros de hardware para cada subproceso.Al cambiar de subprocesos, el proceso de guardar registros en la memoria y cargar registros desde la memoria no ocurre, por lo que el cambio de subprocesos es muy eficiente. Pero los recursos de registro totales del SM y la cantidad de registros utilizados por cada subproceso determinan la cantidad de subprocesos que se pueden ejecutar simultáneamente. La memoria compartida también tiene esta limitación.

A diferencia de la CPU, cada subproceso de GPU tiene diferentes registros de hardware independientes, por lo que el cambio de subprocesos no necesita guardar y cargar la memoria de registro, y el costo del cambio de subprocesos es bajo.

El registro y el uso de memoria compartida de cada bloque de subprocesos limita la cantidad de bloques de subprocesos que cada SM puede ejecutar al mismo tiempo. Al mismo tiempo, el uso excesivo de registros puede causar un desbordamiento de registros y el almacenamiento de datos en la memoria, lo que resulta en una caída.

https://developer.download.nvidia.com/CUDA/training/register_spilling.pdf

Configuración de CMakeLists.txt para ver registros y otros usos: 

set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --ptxas-options=-v")

¿La matriz en el kernel usa registros o mem local?

En la programación CUDA, ¿el hilo asigna la matriz en el registro o en la memoria local? _Blog de internauta común-Blog de CSDN

Indexación dinámica rápida de arreglos privados en CUDA | Blog técnico de NVIDIA

Es decir, solo cuando el compilador puede determinar estáticamente el índice del índice de la matriz, la matriz se colocará en el registro.

Deformación aleatoria

https://people.maths.ox.ac.uk/gilesm/cuda/lecs/lec4.pdf

Primitivas de nivel Warp usando CUDA - NVIDIA Tech Blog

Dado que todos los subprocesos en el mismo warp se ejecutan en el mismo bloque SM, estos subprocesos usan el mismo espacio de registro, lo que permite que los subprocesos en el mismo warp intercambien datos de manera eficiente a través de registros. El comando warp shuffle proporciona tal función. La única forma de intercambiar datos de manera eficiente entre diferentes deformaciones en el mismo bloque de subprocesos es a través de memorias compartidas.

El código para acumular todos los datos en un mismo warp al primer hilo basado en warp suffle:

template <typename T>
__inline__ __device__ T WarpReduceSum(T data) {
#pragma unroll 5 // for warp_size = 32
    for (int offset = 16; offset > 0; offset >>= 1) {
        data += __shfl_down_sync(0xFFFFFFFF, data, offset);
    }
    // optional broadcast value of the first thread to all threads in warp
    data = __shfl_sync(0xFFFFFFFF, data, 0);
    return data;
}

Memoria compartida

¿El principio del conflicto bancario y cómo evitarlo? 

https://developer.nvidia.com/blog/usando-memoria-compartida-cuda-cc/

https://on-demand.gputechconf.com/gtc/2018/presentation/s81006-volta-architecture-and-performance-optimization.pdf

Cuando ocurre un conflicto de banco, la deformación no se cambiará para reducir la demora, por lo que el impacto en el rendimiento es relativamente grande.

El método comúnmente utilizado para evitar conflictos bancarios es el relleno, es decir, alargar la longitud de la fila de la matriz original, de modo que la matriz real sea una submatriz de la matriz de almacenamiento de memoria compartida. La siguiente figura muestra que una matriz con un ancho de 32 puede evitar conflictos de banco cuando diferentes subprocesos acceden a la misma columna a través del relleno de + 1. d_id y b_id son los id de los datos y el banco, respectivamente. De hecho, también puede agregar otro relleno, como +4 o +8, que también puede cumplir con los requisitos de alineación de 128/256 bytes de cada fila de datos. El d_id en la figura es el id de los datos, y b_id es el id del banco. 

¿Cómo implementar doble búfer/búfer previo?

El búfer doble es para usar dos búferes para realizar el cálculo de tubería de lectura/escritura y cálculo, y mantener la unidad de cálculo siempre en un estado ocupado. La implementación de doble búfer requiere una ejecución asíncrona para realizar la superposición del cálculo y la copia de datos.

La carga desde la memoria global a un registro en sí es asíncrona (no bloquea las instrucciones subsiguientes a menos que el registro se use más tarde). Sin embargo, la arquitectura anterior de Ampere debe cargarse directamente desde la memoria global a la compartida a través de la transferencia de registro. Dado que la escritura en la memoria compartida depende de que el registro esté listo, debe esperar a que se complete la memoria global. Para realizar la asincronía de global a compartida, la arquitectura anterior de Ampere puede transferirse manualmente en función de los registros. Es decir, primero lea manualmente el registro global en el registro, luego ejecute otras instrucciones de cálculo irrelevantes y luego copie la memoria del registro en la compartida, ocultando así la espera de lectura de la memoria global.

Apmere presenta una nueva instrucción LDGSTS de copia asíncrona que no requiere la transferencia de registros para leer de la memoria global a la memoria compartida, lo que reduce la presión sobre los registros y la transferencia de datos innecesaria, y ahorra aún más el consumo de energía. Y debido a la naturaleza asíncrona de esta instrucción, se puede ejecutar como una superposición entre la operación en segundo plano y la instrucción informática en primer plano, lo que mejora aún más la eficiencia informática general.
Un código de demostración simple para doble búfer:

prefetch data block0
for loop:
    prefetch next block
    compute cur block

CUDA 11.0 presenta una función de copia asíncrona que se puede usar dentro del código del dispositivo para
administrar explícitamente la copia asíncrona de datos de la memoria global a la memoria compartida. Esta
función permite que los kernels de CUDA superpongan la copia de datos de la memoria global a la compartida con
el cálculo. También evita un acceso al archivo de registro intermediario tradicionalmente presente entre
la lectura de la memoria global y la escritura de la memoria compartida.
https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#async_data_operations

memoria mundial

Preguntas a aclarar:

¿Comprensión y práctica de los accesos a la memoria coalesce?

Acceso a la memoria fusionada El acceso a la memoria fusionada es uno de los puntos importantes de atención en la optimización del rendimiento.

Acceso combinado de memoria: las direcciones de memoria leídas por 32 subprocesos en el mismo warp al mismo tiempo son n * 128 bytes consecutivos (cada subproceso lee 4 bytes, y la identificación de la dirección leída por cada subproceso no tiene que ser necesariamente la misma que la id de subproceso Consistente, pero toda la perspectiva warp lee una memoria continua), y se requiere alineación de memoria (la primera dirección de la memoria leída continuamente es un múltiplo entero de 128 bytes).

Algunos puntos a tener en cuenta son que cuando se trata de tipos de datos como int8 fp16, si cada subproceso lee 1 elemento, entonces un warp puede leer menos de bytes 128. En este momento, los tipos de datos como float2 float4 se pueden usar para leer y luego en distribución.

Fusionar accesos a la memoria: los subprocesos adyacentes preferiblemente leen elementos de datos adyacentes.

Carga/almacenamiento vectorizado:  sin embargo, los elementos de datos pueden ser de varios tipos de datos, y la cantidad de bytes de cada tipo de datos es diferente, por lo que es mejor combinar la carga/almacenamiento de memoria fusionada con la carga/almacenamiento vectorizado, es decir, cada subproceso lee tipos de datos vectoriales, como float2, float4, half4, etc., pero un subproceso no debe leer tipos de datos demasiado grandes; por lo general, es mejor leer al menos 4 bytes correspondientes a float/half2, pero normalmente no debe exceder los 16 correspondiente a float4/half8 byte.

cuda-samples/helper_math.h en maestro · NVIDIA/cuda-samples · GitHub

Si la forma de la matriz no es un múltiplo entero de 4, 8 o 32, se puede considerar el relleno; de lo contrario, la primera dirección de la fila distinta de cero no está alineada en 128 bytes.
Este ejemplo de transposición de matriz es una buena ilustración de cómo usar la memoria compartida para realizar simultáneamente el acceso combinado al contenido de leer y escribir dos matrices, y evitar el conflicto de banco de la memoria compartida: aprendizaje CUDA (2) transposición y optimización de matriz ( Combined acceso, memoria compartida, conflicto bancario) bzdww

La idea es que cada warp lea un bloque de datos de 32x32, y los 32 subprocesos de cada warp lean cada fila de 32x32, de modo que la lectura de entrada se combine con el acceso a la memoria. La transposición se puede implementar fácilmente en función de la transformación de índice idx al leer o escribir memorias compartidas. Luego, en función de la memoria compartida transpuesta, cada fila de la matriz de salida se vuelve a escribir a su vez para realizar el acceso a la memoria combinada de la salida. Dado que siempre hay 32 subprocesos que acceden a la misma fila o columna al mismo tiempo cuando se lee o se reescribe una memoria compartida de 32x32, si solo se crea un tamaño de memoria compartida de 32x32, se producirá un conflicto bancario al leer o escribir de nuevo. Pero si crea un bloque de datos de memoria compartida de [32,33], escribirá en la memoria compartida [i,0:32] cada vez que la lea y leerá la memoria compartida [0:32,i] cuando la escriba. Aparece un conflicto bancario.

memoria constante, memoria de textura

Generalmente, solo se usa en escenarios de aplicación específicos.¿Hay espacio para la aplicación en IA?

Primitivas de nivel Warp de CUDA

Uso de primitivas de nivel Warp de CUDA | Blog técnico de NVIDIA

Caché de registro: almacenamiento en caché para programas CUDA centrados en Warp | Blog técnico de NVIDIA

Grupos cooperativos: Programación flexible de subprocesos CUDA | Blog técnico de NVIDIA

Los bloques de subprocesos son programados y ejecutados automáticamente por SM en unidades warp.Este proceso es básicamente imperceptible para los programadores, pero también puede operarse explícitamente en el nivel warp. Por ejemplo, el intercambio de intraregistros a nivel Warp, porque la ejecución del subproceso y el contenido del registro del mismo warp están en el mismo bloque sm, por lo que el mismo subproceso warp tiene la posibilidad de intercambiar datos de registro entre sí (registro-shuffle), y pueden haber diferentes deformaciones en Se ejecutan diferentes bloques sm, y los datos solo se pueden intercambiar a través de la memoria compartida.

CUDA 9 introdujo tres categorías de primitivas de nivel warp nuevas o actualizadas.

  1. Intercambio de datos sincronizado: intercambio de datos entre hilos en warp.
    • __all_sync__any_sync__uni_sync__ballot_sync
    • __shfl_sync__shfl_up_sync__shfl_down_sync__shfl_xor_sync
    • __match_any_sync__match_all_sync
  2. Consulta de máscara activa: devuelve una máscara de 32 bits que indica qué subprocesos en un warp están activos con el subproceso en ejecución actual.
    • __activemask
  3. Sincronización de subprocesos: sincronice subprocesos en una deformación y proporcione una valla de memoria.
    • __syncwarp

Consulte la   Guía de programación de CUDA  para obtener descripciones detalladas de estas primitivas.

Aquí hay un ejemplo del uso de cada warp para calcular el promedio de cada fila de un tensor bidimensional basado en warp shuffle:

__global__ void reduce_mean_row_warp(const float* __restrict__ A,
                                     float* __restrict__ B,
                                     int row, int col) {
    int tid = blockDim.x * blockIdx.x + threadIdx.x;
    int cur_row = tid / warpSize;
    int start_col = tid % warpSize;

    if (cur_row < row) {
        float ratio = 1.0f / col;
        int addr_offset = cur_row * col;

        float mean_val = 0;
        for (int i = start_col; i < col; i += warpSize) {
            mean_val += ratio * A[addr_offset + i]; // method 1
        }
        // use warp shuffle to get correct mean for thread 0 from all threads in a warp
        mean_val += __shfl_down_sync(0xFFFFFFFF, mean_val, 16);
        mean_val += __shfl_down_sync(0xFFFFFFFF, mean_val, 8);
        mean_val += __shfl_down_sync(0xFFFFFFFF, mean_val, 4);
        mean_val += __shfl_down_sync(0xFFFFFFFF, mean_val, 2);
        mean_val += __shfl_down_sync(0xFFFFFFFF, mean_val, 1);

        if (start_col == 0) {
            B[cur_row] = mean_val;
        }
    }
}

TensorCore

¿Cómo los hilos en una urdimbre usan TensorCore juntos?

hacer

Otras consideraciones comunes

La divergencia de deformación causada por las ramas debe evitarse tanto como sea posible, como permitir que la misma deformación procese la misma rama tanto como sea posible.

__restrict__ La palabra clave puede generar algunos efectos de optimización y  restrictbásicamente tiene la misma semántica que la palabra clave C99.

Lo más importante en la optimización del rendimiento es saber dónde está el cuello de botella

1. ¿Dónde está el cuello de botella de todo el modelo? ¿La asignación de memoria es copia de datos? ¿O algunos operadores consumen mucho tiempo?

2. ¿Dónde está el cuello de botella en un solo operador? cálculo de datos? ¿Lectura y escritura de datos? Cálculo de sesgo?

Algunas características nuevas de CUDA

Asignación de memoria asíncrona

La asignación y reutilización de memoria es una parte extremadamente importante del motor de inferencia, ya que cada reasignación y liberación de memoria es un proceso que lleva mucho tiempo. Por lo general, es necesario implementar un grupo de memoria, asignar memoria por adelantado y luego reutilizar la memoria según en el grupo de memoria para mejorar el rendimiento. y

Sin embargo, cuda11.2 introduce nuevas funciones e implementa automáticamente dichas funciones en la capa inferior, eliminando la necesidad de que los usuarios implementen algoritmos complejos de reutilización de memoria por sí mismos.

cudaMallocAsync(&ptr, size, stream); // Allocates physical memory
kernel<<<...,stream>>>(ptr);
cudaFreeAsync(ptr, stream);          // releases memory back into a pool
cudaMallocAsync(&ptr, size, stream); // Reuses previously freed pointer
kernel<<<...,stream>>>(ptr);
cudaFreeAsync(ptr, stream);          // releases memory back into a pool
....                                 // Executes other work in the stream

herramienta de perfilado

nvprof

nvprof python xx.py

nvprof xx_bin

Sistemas NVIDIA Nsight

Sistemas NVIDIA Nsight | Desarrollador NVIDIA

Descargue Linux Host .run Installer en el lado de Linux, descargue Windows Host en el lado de Windows e instálelos por separado

nsys profile xx se ejecuta para generar un archivo qdrep y abrirlo con el host de Windows para ver el gráfico de línea de tiempo visualizado

Para usar nsys en Windows, primero agregue la ruta nsys a la variable de entorno del sistema: C:\Program Files\NVIDIA Corporation\Nsight Systems 2022.3.4\target-windows-x64

Ejecute en la línea de comandos de anaconda:

perfil nsys D:\ProgramData\Anaconda3\python.exe matmul_tf.py

Es posible que no se encuentre python directo xx.py después de actualizar python, puede usar la ruta completa anterior.

árbitro

​​​​​​Una revisión de los cambios en la arquitectura de la GPU desde la perspectiva de los sistemas de IA: de Fermi a Ampere (V1.2) bazyd

La arquitectura GPU de Nvidia ha evolucionado durante casi una década, de Fermi a Ampere: se busca programador

Arquitectura GPU y renderizado - Zhihu

"Guía de programación de GPU de programación paralela CUDA"

《PROFESIONALCUDA C Programación》

Profundizando en la arquitectura de GPU Nvidia Ampere

Disección de la arquitectura de GPU NVIDIA Volta a través de Microbenchmarking

Arquitectura de GPU NVIDIA A100 Tensor Core

Profundizando en la GPU "Pascal" de Nvidia

Diseño general del proceso (1) - Estructura jerárquica de los programas CUDA - Conoce Quora

https://jonathan-hui.medium.com/ai-chips-a100-gpu-with-nvidia-ampere-architecture-3034ed685e6e

Análisis en profundidad de la arquitectura Nvidia Ampere - Se busca programador

https://developer.nvidia.com/blog/nvidia-ampere-architecture-in-depth/

Microarquitectura CUDA y conjunto de instrucciones (4) - emisión de instrucciones y programación warp- Saber sobre

Guía de programación :: Documentación del kit de herramientas CUDA

¿Cómo evaluar la nueva GPU H100 lanzada por Nvidia el 22 de marzo? - saber casi

Arquitectura NVIDIA Hopper en profundidad | Blog técnico de NVIDIA

declaración:

Parte del contenido de este artículo utiliza el contenido de los documentos y páginas web citados en el artículo.

Supongo que te gusta

Origin blog.csdn.net/u013701860/article/details/121311135
Recomendado
Clasificación