Grupo de cooperación CUDA para la traducción y el aprendizaje de documentos oficiales CUDA10.0

Tabla de contenido

antecedentes

Introducción

Grupo en bloque

Grupo de hilos y bloque de hilos

Particiones en mosaico

Azulejos de bloque de hilo

Función de reproducción aleatoria de pseudo hilos

Función de votación de pseudo hilo

Función de coincidencia de pseudo hilos

Fusionar grupo

Uso de grupos de cooperación dentro de un bloque

Plantilla de descubrimiento

Plantilla de código de sincronización de pseudohilo

combinación

Sincronización de cuadrícula

Sincronización multidispositivo

Conclusión

antecedentes

Hoy traducimos la última parte del documento oficial CUDA10.0 que merece nuestra atención: el grupo de cooperación

Introducción

Los grupos cooperativos son una extensión del modelo de programación CUDA introducido en CUDA 9 para organizar grupos de hilos de comunicación. El grupo cooperativo permite a los desarrolladores expresar la granularidad de la comunicación de subprocesos para expresar una deconstrucción paralela más rica y efectiva.

Antes de esto (consulte el artículo Modelo de programación CUDA ), el modelo de programación CUDA ha proporcionado una única estructura simple para sincronizar subprocesos cooperativos: una barrera a través de todos los subprocesos en el bloque de subprocesos implementado en la función __syncthreads (). Sin embargo, los programadores desean definir y sincronizar la sincronización de otros grupos de subprocesos granulares para admitir un mejor rendimiento, flexibilidad de diseño y reutilización de software de interfaces de funciones relacionadas a nivel de grupo. Para expresar una plantilla de interacción paralela más amplia, muchos programadores orientados al rendimiento han implementado sus propias funciones personalizadas individualmente pero inseguras para la sincronización de subprocesos en pseudo-subprocesos o diferentes bloques de subprocesos en una sola GPU. Aunque la mejora del rendimiento lograda es notable, esto ha llevado a la proliferación de código fragmentado, que se vuelve difícil de implementar, ajustar y mantener con el tiempo y las actualizaciones de la GPU. El equipo cooperativo resuelve este problema proporcionando un mecanismo seguro y preparado para el futuro para respaldar el código con un rendimiento superior.

La extensión del Modelo de programación de grupo cooperativo describe plantillas de sincronización en ambos bloques de subprocesos CUDA y entre bloques de subprocesos CUDA. Esto proporciona a las aplicaciones una forma de definir sus propios grupos de subprocesos y grupos de subprocesos de sincronización. También proporciona una nueva forma de hacer cumplir ciertas restricciones. Iniciar el API para garantizar que la sincronización funcione. Estas funciones aprovechan la nueva plantilla de paralelismo cooperativo en CUDA, incluido el paralelismo productor-consumidor, el paralelismo oportunista y la sincronización global dentro de toda la red.

Expresar el grupo como un objeto de programa de primer orden mejora la composición del software, porque la función relacionada puede recibir un objeto claro que representa la participación en el grupo de subprocesos. Este objeto también permite que la intención del programador sea clara, es decir, eliminar el código fragmentado y optimizaciones del compilador irrazonables Limite las suposiciones de arquitectura poco sólidas y adáptese mejor a la nueva versión de GPU.

El modelo de programación de grupos cooperativos consta de los siguientes elementos:

  • Representa el tipo de datos del subproceso cooperativo;
  • Obtenga la operación del grupo de nivel de instrucción definido por la API de inicio de CUDA;
  • La operación de dividir el grupo existente en un grupo nuevo;
  • Sincronizar el funcionamiento de la valla de un grupo determinado;
  • Ver atributos y operaciones de grupo para colecciones de grupo específicas

Grupo en bloque

En esta sección, describimos las funciones disponibles para crear un grupo de subprocesos que puede sincronizarse y cooperar dentro de un bloque. Tenga en cuenta que la sincronización de un grupo cooperativo entre bloques de subprocesos o dispositivos requiere algunas consideraciones adicionales, que se describirán más adelante.

El grupo cooperativo requiere la versión CUDA> = 9.0. Para usar esta función, debe agregar el archivo de encabezado: #include <cooperative_groups.h>, y usar el espacio de nombres del grupo cooperativo: using namespace cooperative_groups ;, y luego incluir el código de la función de grupo cooperativo en cualquier bloque Puede utilizar nvcc para compilar de la forma habitual.

Grupo de hilos y bloque de hilos

Cualquier programador CUDA ya debe estar familiarizado con un conjunto de bloques subprocesos (ver artículo Modelo de programación CUDA ). La expansión del grupo cooperativo introduce un nuevo tipo de datos-thread_block para expresar claramente este concepto en la función del kernel. El grupo se puede inicializar así: thread_block g = this_thread_block ();. El tipo de datos thread_block proviene de los datos más generales de thread_group tipo. thread_group se puede utilizar para representar una gama más amplia de grupos y proporciona las siguientes funciones:

void sync(); // 同步组内线程
unsigned size(); // 组内线程数
unsigned thread_rank(); // 调用线程的组内序号,值域为[0, size]
bool is_valid(); // 组是否违反了任何API约束

Y thread_block proporciona las siguientes funciones adicionales basadas en bloques:

dim3 group_index(); // 网格内的块索引,三维
dim3 thread_index(); // 块内的线程索引,三维

Por ejemplo, si el grupo g se ha inicializado como se indicó anteriormente, g.sync (); sincronizará todos los subprocesos del bloque, lo que equivale a __syncthreads () ;. Tenga en cuenta que todos los subprocesos del grupo deben realizar operaciones uniformes; de lo contrario, se producirá un comportamiento indefinido.

Particiones en mosaico

La función tile_partition () se puede utilizar para descomponer bloques de subprocesos en varios grupos de subprocesos cooperativos más pequeños. Por ejemplo, si primero creamos un grupo que contiene todos los hilos del bloque:

thread_block wholeBlock = this_thread_block();

Luego, podemos dividirlo en grupos más pequeños, como 32 subprocesos por grupo:

thread_group tile32 = tiled_partition(wholeBlock, 32);

Además, podemos dividir cada grupo de 32 subprocesos en grupos más pequeños, como 4 subprocesos por grupo:

thread_group tile4 = tiled_partition(tile32, 4);

Entonces, si agregamos el siguiente código:

if (tile4.thread_rank() == 0) printf(“Hello from tile4 rank 0\n”);

Luego, se imprimirá un párrafo cada cuatro hilos: saldrá el hilo 0 de cada grupo tile4 y los hilos 0, 4, 8 y 12 del grupo wholeBlock. Tenga en cuenta que el tamaño de corte actual solo puede ser una potencia de 2 y no puede exceder 32

Azulejos de bloque de hilo

También se puede utilizar una versión con plantilla de la función tiled_partition, donde el parámetro de plantilla se utiliza para especificar el tamaño del segmento, que se determina en el momento de la compilación, por lo que hay más espacio para la optimización de la ejecución. Al igual que en la sección anterior, el siguiente código creará dos conjuntos de sectores con tamaños 32 y 4:

thread_block_tile<32> tile32 = tiled_partition<32>(this_thread_block());
thread_block_tile<4> tile4 = tiled_partition<4>(this_thread_block());

Tenga en cuenta que la estructura de datos de la plantilla thread_block_tile se usa aquí, y el tamaño del grupo se pasa a la función tiled_partition () como un parámetro de plantilla en lugar de un parámetro de función.

La fragmentación de bloques de subprocesos también proporciona las siguientes funciones adicionales:

.shfl()
.shfl_down()
.shfl_up()
.shfl_xor()
.any()
.all()
.ballot()
.match_any()
.match_all()

Estas operaciones de sincronización cooperativa son similares a las funciones de mezcla de pseudo-subprocesos, funciones de votación de pseudo-subprocesos y funciones de coincidencia de pseudo-subprocesos.

Función de reproducción aleatoria de pseudo hilos

La función de mezcla de pseudo-subprocesos se utiliza para difundir datos entre subprocesos en el pseudo-subproceso sin utilizar memoria compartida. El prototipo de la función es el siguiente:

T __shfl_sync(unsigned mask, T var, int srcLane, int width=warpSize);
T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width=warpSize);
T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int
width=warpSize);
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize);

Donde T es el tipo de datos que se transmitirá, que puede ser int, unsigned int, long, unsigned long, long long, unsigned long long, float o double. Si se incluye el archivo de encabezado cuda_fp16.h, T también puede ser __half o __half2; La máscara se usa para marcar el hilo de destino que realiza el intercambio; srcLane representa el hilo de origen que envía la transmisión. Si el id del hilo de origen es mayor que el ancho, entonces el id del hilo de origen real es igual a srcLane% width; el ancho representa el tamaño del paquete para realizar la transmisión, que debe ser 2. Si es una potencia total y no excede de 32, el valor se transmitirá en el grupo de tamaño especificado; la función devuelve la palabra de cuatro bytes especificada por valor en la fuente hilo;

La identificación del hilo de origen de la función __shfl_sync () es srcLane; la identificación del hilo de origen de la función __shfl_up_sync () es srcLane-delta; la identificación del hilo de origen de la función __shfl_down_sync () es srcLane + delta; la identificación del hilo de origen de la __shfl_xor_sync () la función es srcLane xor laneMask

Función de votación de pseudo hilo

Las funciones de votación de pseudo-subprocesos permiten que los subprocesos en pseudo-subprocesos realicen operaciones de difusión de reducción. Los prototipos de estas funciones son los siguientes:

int __all_sync(unsigned mask, int predicate);
int __any_sync(unsigned mask, int predicate);
unsigned __ballot_sync(unsigned mask, int predicate);
unsigned __activemask();

El predicado representa el predicado de juicio y la máscara representa el hilo que participa en la votación. La función lee predicados enteros de cada subproceso en el pseudo-subproceso, y si el valor de estos predicados es 0, y transmite el valor de retorno a cada subproceso participante. La lógica de ejecución de la función se muestra en la siguiente tabla

función

Lógica de ejecución

__all_sync ()

Evalúe el valor del predicado para todos los subprocesos especificados por la máscara que no han salido y devuelva un valor distinto de cero solo si el valor del predicado de todos los subprocesos es distinto de cero

__any_sync ()

Evaluar el valor del predicado para todos los subprocesos especificados por la máscara que no han salido. Si el valor del predicado de cualquier subproceso es distinto de cero, devuelve un valor distinto de cero

__ballot_sync ()

Evalúe el valor del predicado para todos los subprocesos especificados por la máscara que no han salido y devuelva un número entero. Si y solo si el N-ésimo hilo del pseudo-hilo está activo y el valor del predicado no es 0, el N-ésimo bit del entero es 1

__activemask ()

Devuelve la máscara de 4 bytes de todos los subprocesos actualmente activos en el pseudoproceso. Si el N-ésimo subproceso en el pseudo-subproceso está activo cuando se llama a esta función, el N-ésimo bit de la máscara es 1 y el bit de código correspondiente al subproceso salido o inactivo es 0. Tenga en cuenta que el hilo que converge cuando se llama a esta función no puede garantizar que seguirá convergiendo en las instrucciones posteriores, a menos que estas instrucciones sean funciones de sincronización integradas de pseudohilos.

Función de coincidencia de pseudo hilos

La función de coincidencia de pseudo-subproceso realizará una operación de comparación de transmisión sincronizada entre subprocesos en el pseudo-subproceso. Admite dispositivos con potencia de cálculo> = 7.X. El prototipo de la función es el siguiente:

unsigned int __match_any_sync(unsigned mask, T value);
unsigned int __match_all_sync(unsigned mask, T value, int *pred);

T puede ser int, unsigned int, long, unsigned long, long long, unsigned long long, float o double. El valor indica el valor que se va a comparar por difusión y la máscara especifica el hilo en el que participar. La lógica de retorno de estas dos funciones es diferente, como se muestra en la siguiente tabla

función

Lógica de retorno

__match_any_sync ()

Devuelve la máscara de hilo del hilo especificado por máscara que tiene un valor igual a valor

__match_all_sync ()

Si todos los subprocesos especificados por máscara tienen el mismo valor que valor, se devuelve máscara y pred es verdadero; de lo contrario, devuelve 0 y pred es falso

Volviendo a la sección de corte de bloques de subprocesos de subprocesos de grupo cooperativo, estas funciones se utilizan en el contexto de grupos de subprocesos definidos por el usuario y proporcionan una mejor flexibilidad y productividad.

Fusionar grupo

En la arquitectura SIMT de CUDA (ver artículo Implementación de hardware de CUDA ), a nivel de hardware, los multiprocesadores usan 32 subprocesos como un grupo (pseudo-subproceso) para ejecutar subprocesos. Si hay una rama condicional dependiente de datos en el código de la aplicación, los subprocesos en el pseudo-subproceso están dispersos, entonces el pseudo-subproceso recorrerá cada rama y bloqueará los subprocesos que no están en esa ruta, pero que están activos en la actual. ruta de ejecución La ejecución de subprocesos se denomina ejecución combinada. El grupo cooperativo tiene la función de descubrir o crear todos los grupos de hilos fusionados: coalesced_group active = coalesced_threads () ;. Por ejemplo, considere un escenario: hay ramas en el código donde solo el segundo, cuarto y octavo subproceso de cada pseudo-subproceso permanecen activos. La declaración ejecutada en esta rama creará un nombre para cada pseudo-subproceso El grupo activo contiene los tres hilos activos (los identificadores en el grupo son 0, 1, 2 respectivamente)

Uso de grupos de cooperación dentro de un bloque

En esta sección, la función del grupo cooperativo se ilustra a través de algunos ejemplos.

Plantilla de descubrimiento

En circunstancias normales, los desarrolladores deben trabajar con el conjunto de subprocesos activo. No podemos suponer ni especificar qué subprocesos están disponibles actualmente, pero solo podemos trabajar con subprocesos que estén allí. Visto en "Adición atómica agregada de subprocesos cruzados dentro de un subproceso "(escrito usando la función CUDA 9.0 correcta):

{
    unsigned int writemask = __activemask();
    unsigned int total = __popc(writemask); // 活跃的线程数
    unsigned int prefix = __popc(writemask & __lanemask_lt()); // 当前活跃线程前缀,比如活跃线程掩码为01010,那么对于第2个线程,__lanemask_lt()为00001,那么prefix就是0(第4个活跃线程对应的就是1)。因此前缀为0就表示当前为第一个活跃的线程

    int elected_lane = __ffs(writemask) - 1; // id最小的活跃线程
    int base_offset = 0;
    if (prefix == 0) {
        base_offset = atomicAdd(p, total);
    }

    base_offset = __shfl_sync(writemask, base_offset, elected_lane); // 把elected_lane中原子加前的值广播到所有的活跃线程中
    int thread_offset = prefix + base_offset;

    return thread_offset;
}

Si usa la API del grupo cooperativo para reescribir, obtendrá el siguiente código:

{
    cg::coalesced_group g = cg::coalesced_threads(); // 活跃线程组

    int prev;
    if (g.thread_rank() == 0) { // 第一个活跃线程
        prev = atomicAdd(p, g.size()); // 原子加
    }

    prev = g.thread_rank() + g.shfl(prev, 0); // 最小的活跃线程id + 老的值
    return prev;
}

Plantilla de código de sincronización de pseudohilo

Los desarrolladores pueden tener un código de sincronización de pseudo-subproceso e hicieron una suposición implícita del tamaño del pseudo-subproceso y codificado de acuerdo con este tamaño. Ahora, el tamaño del pseudo hilo debe especificarse explícitamente:

auto g = tiled_partition<16>(this_thread_block());

Sin embargo, los usuarios pueden querer particionar mejor el algoritmo y no usar pseudo-subprocesos para sincronizar los parámetros de plantilla incorporados:

auto g = tiled_partition(this_thread_block(), 8);

En este caso, el grupo g aún se puede sincronizar y aún podemos construir múltiples algoritmos paralelos basados ​​en él, pero funciones como shfl () no se pueden usar:

__global__ void cooperative_kernel(...) {
    // 获取默认的块线程组
    thread_group my_block = this_thread_block();

    // 分组成32个线程一组的线程组(片),线程片将线程组平均瓜分,每个片内的线程都是连续的
    thread_group my_tile = tiled_partition(my_block, 32);

    // 只在块内前32个线程中执行操作
    if (my_block.thread_rank() < 32) {
        // ...
        my_tile.sync();
    }
}

combinación

En el pasado, al escribir código, había algunas restricciones implícitas en la implementación, como el siguiente código:

__device__ int sum(int *x, int n) {
    // ...
    __syncthreads();
    return total;
}

__global__ void parallel_kernel(float *x){
    // ...
    // 所有的线程块都要调用sum()
    sum(x, n);
}

Los subprocesos dentro del bloque de subprocesos deben alcanzar la barrera __syncthreads (), pero esta restricción es invisible para el desarrollador que llama a sum (). Luego, utilizando un grupo cooperativo, una mejor manera de lograrlo puede ser:

__device__ int sum(const thread_group& g, int *x, int n)
{
    // ...
    g.sync()
    return total;
}

__global__ void parallel_kernel(...)
{
    // ...
    sum(this_thread_block(), x, n);
    // ...
}

Sincronización de cuadrícula

Antes de la introducción de la sincronización de grupos cooperativos, el modelo de programación CUDA solo permitía la sincronización entre bloques de subprocesos cuando se completaba la función del kernel, y el límite de la función del kernel tenía un estado no válido implícito y un impacto potencial en el rendimiento. Por ejemplo, en un caso de uso específico, la aplicación tiene una gran cantidad de funciones de kernel pequeñas, y cada función de kernel representa una etapa del proceso. El modelo de programación actual de CUDA requiere que estas funciones del núcleo produzcan datos antes de que el bloque de subprocesos en la etapa de canalización inferior esté listo para consumir datos. En este caso, la capacidad de proporcionar sincronización entre bloques de subprocesos globales permitirá que la aplicación reconstruya estos bloques de subprocesos para sincronizar dispositivos cuando se complete una fase determinada.

Para sincronizar la cuadrícula dentro de una función del kernel, puede usar el grupo: grid_group grid = this_grid (); y luego llamar a grid.sync () ;. Para admitir la sincronización de celdas, es necesario usar cudaLaunchCooperativeKernel (), la API de inicio de tiempo de ejecución de CUDA al iniciar la función del kernel, en lugar de la sintaxis de configuración de ejecución <<< >>>:

cudaLaunchCooperativeKernel(const T *func, dim3 gridDim, dim3 blockDim, void **args, size_t sharedMem = 0, cudaStream_t stream = 0)
// 或者CUDA驱动API的对应函数,这种核函数不能使用附录A中的动态并行功能

Para garantizar la coexistencia de bloques de subprocesos en la GPU, se debe considerar cuidadosamente el número de bloques iniciados. Por ejemplo, podemos comenzar de la siguiente manera:

cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
// 初始化,而后启动
cudaLaunchCooperativeKernel((void*)my_kernel, deviceProp.multiProcessorCount, numThreads, args);

O podemos usar la calculadora de ocupación de la siguiente manera para calcular cuántos bloques de subprocesos pueden existir en un multiprocesador al mismo tiempo:

cudaOccupancyMaxActiveBlocksPerMultiprocessor(&numBlocksPerSm, my_kernel, numThreads, 0));
// 初始化,而后启动
cudaLaunchCooperativeKernel((void*)my_kernel, numBlocksPerSm, numThreads, args);

También tenga en cuenta que para usar la sincronización de cuadrícula, el código del dispositivo debe compilarse por separado y luego vincularse cuando el dispositivo se está ejecutando (para obtener más detalles, consulte el documento NVCC del controlador del compilador CUDA utilizando el capítulo de compilación independiente), el ejemplo más simple es se muestra a continuación:

nvcc -arch=sm_61 -rdc=true mytestfile.cu -o mytest

También debemos asegurarnos de que el dispositivo admita atributos de inicio cooperativos, que se pueden ver utilizando la API del controlador CUDA cuDeviceAttribute ():

int pi=0;
cuDevice dev;
cuDeviceGet(&dev,0) // 查询设备0
cuDeviceGetAttribute(&pi, CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH, dev);

Si pi es 1, significa que este atributo es compatible con el dispositivo 0, y solo los dispositivos con capacidad informática ≥ 6.0 pueden admitir el atributo de inicio cooperativo. Además, debemos ejecutar el programa con función de inicio cooperativo en plataforma Linux sin plataforma MPS o Windows con equipo en modo TCC

Sincronización multidispositivo

Para admitir la sincronización entre múltiples dispositivos usando un grupo cooperativo, necesitamos usar la api CUDA cuLaunchCooperativeKernelMultiDevice (), que es una extensión importante de la API CUDA existente y admitirá un hilo de host para iniciar funciones del kernel en múltiples dispositivos. Además de las restricciones y garantías distintas de la función cuLaunchCooperativeKernel (), la función cuLaunchCooperativeKernelMultiDevice () tiene la siguiente semántica:

  • Esta API garantiza que el inicio es atómico, lo que significa que si se llama correctamente a la API, se iniciará un número específico de bloques de subprocesos en todos los dispositivos especificados;
  • Las funciones del kernel lanzadas a través de esta API deben ser las mismas, esta parte del controlador no hará una verificación explícita, ya que esta verificación es básicamente inviable en el controlador, por lo que la aplicación debería asegurarlo;
  • Los dos elementos del parámetro launchParamsList no se pueden asignar a un dispositivo;
  • El dispositivo de destino para este tipo de inicio debe tener la misma potencia informática: la versión principal o la versión secundaria deben ser iguales;
  • El tamaño del bloque de subprocesos, el tamaño de la cuadrícula y la cantidad de memoria compartida utilizada por cada celda deben ser iguales para todos los dispositivos. Tenga en cuenta que esto significa que el número máximo de bloques de subprocesos iniciados por cada dispositivo depende del dispositivo con el menor número de multiprocesadores;
  • Cualquier variable global de dispositivo __device__, __constant__ o __managed__ personalizada que exista en el módulo que llama a la función cu para iniciarse se inicializará de forma independiente en cada dispositivo, y el usuario debe garantizar la inicialización correcta de dichas variables globales de dispositivo.

Los parámetros de inicio deben definirse con la siguiente estructura:

typedef struct CUDA_LAUNCH_PARAMS_st {
    CUfunction function;
    unsigned int gridDimX;
    unsigned int gridDimY;
    unsigned int gridDimZ;
    unsigned int blockDimX;
    unsigned int blockDimY;
    unsigned int blockDimZ;
    unsigned int sharedMemBytes;
    CUstream hStream;
    void **kernelParams;
} CUDA_LAUNCH_PARAMS;

Luego páselo a la API de inicio:

cudaLaunchCooperativeKernelMultiDevice(CUDA_LAUNCH_PARAMS *launchParamsList, unsigned int numDevices, unsigned int flags = 0);

Este método de inicio es similar al inicio de la sincronización de la red anterior, y también hay un método de sincronización que también es similar:

multi_grid_group multi_grid = this_multi_grid();
multi_grid.sync();

También es necesario utilizar la compilación independiente.

Debemos asegurarnos de que el dispositivo admita el atributo de inicio de múltiples dispositivos de la misma manera que se describe en la sección anterior. Solo necesita cambiar el parámetro a CU_DEVICE_ATTRIBUTE_COOPERATIVE_MULTI_DEVICE_LAUNCH. Solo los dispositivos con capacidad informática ≥ 6.0 pueden admitir el atributo de inicio cooperativo. Además, debemos ejecutar el programa con función de inicio cooperativo en plataforma Linux sin plataforma MPS o Windows con equipo en modo TCC

Conclusión

En este punto, he terminado de compartir la traducción del documento oficial CUDA10.0. Yo personalmente traduje todo el proceso, pero mi nivel de inglés es limitado. Si hay algo inapropiado, por favor haga sugerencias en el área de comentarios. Disculpe.

Supongo que te gusta

Origin blog.csdn.net/qq_37475168/article/details/112388296
Recomendado
Clasificación