[GPU] Tutorial avanzado de programación Nvidia CUDA - Modelo de memoria NVSHMEM

El blogger no ha autorizado a ninguna persona u organización a reimprimir ningún artículo original del blogger, ¡gracias por su apoyo al original!
enlace de blogger

Trabajo para un fabricante de terminales de renombre internacional y soy responsable de la investigación y el desarrollo de chips de módem.
En los primeros días de 5G, fue responsable del desarrollo de la capa de servicio de datos de terminales y la red central. Actualmente, lidera la investigación sobre estándares técnicos para redes de potencia informática 6G.


El contenido del blog gira principalmente en torno a:
       Explicación del protocolo 5G/6G
       Explicación de la red de potencia informática (computación en la nube, computación de borde, computación final)
       Explicación del lenguaje C avanzado Explicación
       del lenguaje Rust



Modelo de memoria NVSHMEM

inserte la descripción de la imagen aquí

PE: unidad de procesamiento (entidad de proceso)

memoria simétrica

       La API de asignación de memoria de NVSHMEM, nvshmem_malloc(), funciona de manera similar al cudaMalloc() estándar, pero cudaMalloc() devuelve una dirección privada de la GPU1 local . Los objetos asignados mediante nvshmem_malloc() se denominan objetos de datos simétricos . Cada objeto de datos simétricos tiene un objeto de datos correspondiente del mismo nombre, tipo y tamaño en todos los PE. La dirección virtual correspondiente al puntero devuelto por nvshmem_malloc() se denomina dirección simétrica . Es legal usar direcciones simétricas para el acceso remoto a otros PE en las rutinas de comunicación NVSHMEM (las direcciones simétricas también se pueden usar directamente para acceder a la memoria local del PE). Podemos manipular direcciones virtuales como direcciones locales normales. Para acceder a una copia de un objeto de datos simétricos en un PE remoto mediante la API de NVSHMEM, podemos indexar el almacenamiento como de costumbre con un puntero y usar la ubicación correspondiente en el PE de destino remoto . Por ejemplo,

       Si ejecutamos la siguiente sentencia:

int* a = (int*) nvshmem_malloc(sizeof(int));

Luego podemos 本地 PErealizar el acceso a la memoria local o 远程 PEel acceso a la memoria remota para obtener el valor de a[0]. Una forma de pensar en esta operación es que, dados los MPE, distribuimos los elementos de datos en una matriz de longitud M de manera uniforme en todos los PE, de modo que cada PE tenga solo un elemento. Dado que el objeto de datos simétricos tiene una longitud de 1 en este ejemplo, solo necesitamos acceder a [0] en cualquier PE.

Por favor agregue una descripción de la imagen
       En NVSHMEM, la asignación de memoria dinámica para objetos de datos simétricos proviene de un área de memoria especial llamada ,对称堆(symmetric heap) creada por NVSHMEM durante la ejecución del programa2 y luego utilizada para la asignación de memoria dinámica posterior.

ejercicio 1

       A continuación reemplazamos la llamada a cudaMalloc() con una llamada a nvshmem_malloc(). Todavía podemos usar atomicAdd() en datos asignados localmente, por lo que la copia simétrica del objeto en cada PE obtendrá el mismo resultado que antes.

       En segundo lugar, sumamos los resultados de todos los PE. Esta es una operación de unión, que es una operación de reducción global. En NVSHMEM podemos usar nvshmem_int_sum_reduce(team, dest, source, nreduce)para sumar todas las instancias de un objeto simétrico.

  • fuente: es la dirección simétrica que queremos sumar;
  • destino: donde se almacenan los resultados;
  • nreduce: es el número de elementos a reducir (solo uno para nosotros, ya que nuestros datos son escalares);
  • team: es el grupo de PEs a sumar 3 (usaremos el grupo por defecto NVSHMEM_TEAM_WORLD, que es el conjunto de todos los PEs);

En resumen, lo que tenemos que hacer es:

// 累积所有 PE 的结果
int* d_hits_total = (int*) nvshmem_malloc(sizeof(int));
nvshmem_int_sum_reduce(NVSHMEM_TEAM_WORLD, d_hits_total, d_hits, 1);

Por favor agregue una descripción de la imagen
Ahora, todos los PE tienen la suma de los conteos, por lo que el tercer cambio que debemos hacer es imprimir solo el resultado en un solo PE . Por convención, generalmente imprimimos en PE0.

if (my_pe == 0) {
    
    
    // 将最终结果复制回主机
    ...

    // 计算 pi 的最终值
    ...

    // 打印结果
    ...
}

El código completo es el siguiente (nombre de archivo: nvshmem_pi_step3.cpp):

#include <iostream>
#include <curand_kernel.h>

#include <nvshmem.h>
#include <nvshmemx.h>

inline void CUDA_CHECK (cudaError_t err) {
    
    
    if (err != cudaSuccess) {
    
    
        fprintf(stderr, "CUDA error: %s\n", cudaGetErrorString(err));
        exit(-1);
    }
}

#define N 1024*1024

__global__ void calculate_pi(int* hits, int seed) {
    
    
    int idx = threadIdx.x + blockIdx.x * blockDim.x;

    // 初始化随机数状态(网格中的每个线程不得重复)
    int offset = 0;
    curandState_t curand_state;
    curand_init(seed, idx, offset, &curand_state);

    // 在 (0.0, 1.0] 内生成随机坐标
    float x = curand_uniform(&curand_state);
    float y = curand_uniform(&curand_state);

    // 如果这一点在圈内,增加点击计数器
    if (x * x + y * y <= 1.0f) {
    
    
        atomicAdd(hits, 1);
    }
}


int main(int argc, char** argv) {
    
    
    // 初始化 NVSHMEM
    nvshmem_init();

    // 获取 NVSHMEM 处理元素 ID 和 PE 数量
    int my_pe = nvshmem_my_pe();
    int n_pes = nvshmem_n_pes();

    // 每个 PE(任意)选择与其 ID 对应的 GPU
    int device = my_pe;
    CUDA_CHECK(cudaSetDevice(device));

    // 分配主机和设备值
    int* hits = (int*) malloc(sizeof(int));
    int* d_hits = (int*) nvshmem_malloc(sizeof(int));

    // 初始化点击次数并复制到设备
    *hits = 0;
    CUDA_CHECK(cudaMemcpy(d_hits, hits, sizeof(int), cudaMemcpyHostToDevice));

    // 启动核函数进行计算
    int threads_per_block = 256;
    int blocks = (N / n_pes + threads_per_block - 1) / threads_per_block;

    int seed = my_pe;
    calculate_pi<<<blocks, threads_per_block>>>(d_hits, seed);
    CUDA_CHECK(cudaDeviceSynchronize());

    // 累积所有 PE 的结果
    int* d_hits_total = (int*) nvshmem_malloc(sizeof(int));
    nvshmem_int_sum_reduce(NVSHMEM_TEAM_WORLD, d_hits_total, d_hits, 1);

    if (my_pe == 0) {
    
    
        // 将最终结果复制回主机
        CUDA_CHECK(cudaMemcpy(hits, d_hits_total, sizeof(int), cudaMemcpyDeviceToHost));

        // 计算 pi 的最终值
        float pi_est = (float) *hits / (float) (N) * 4.0f;

        // 打印结果
        std::cout << "Estimated value of pi averaged over all PEs = " << pi_est << std::endl;
        std::cout << "Relative error averaged over all PEs = " << std::abs((M_PI - pi_est) / pi_est) << std::endl;
    }

    free(hits);
    nvshmem_free(d_hits);
    nvshmem_free(d_hits_total);

    // 最终确定 nvshmem
    nvshmem_finalize();

    return 0;
}

Las instrucciones de compilación y ejecución son las siguientes:

nvcc -x cu -arch=sm_70 -rdc=true -I $NVSHMEM_HOME/include -L $NVSHMEM_HOME/lib -lnvshmem -lcuda -o nvshmem_pi_step3 exercises/nvshmem_pi_step3.cpp
nvshmrun -np $NUM_DEVICES ./nvshmem_pi_step3

El resultado es el siguiente:

Estimated value of pi averaged over all PEs = 3.14072
Relative error averaged over all PEs = 0.000277734


inserte la descripción de la imagen aquí


  1. La excepción es que en los sistemas donde las GPU están conectadas mediante NVLink, el mecanismo CUDA IPC se puede usar para permitir que las GPU accedan directamente a la memoria de las demás. ↩︎

  2. El tamaño predeterminado del almacenamiento dinámico simétrico es de 1 GB, que se puede controlar mediante la variable de entorno NVSHMEM_SYMMETRIC_SIZE. ↩︎

  3. Basado en la especificación OpenSHMEM 1.5, el uso del equipo para especificar operaciones que involucran múltiples grupos de PE es una nueva función de NVSHMEM 2.0. ↩︎

Supongo que te gusta

Origin blog.csdn.net/qq_31985307/article/details/128594791
Recomendado
Clasificación