Acerca de la memoria unificada en CUDA

¿Qué es la memoria unificada?

En CUDA 6, NVIDIA introdujo una de las mejoras de modelo de programación más importantes en la historia de CUDA, la memoria unificada (en lo sucesivo, UM). En una PC típica actual, la memoria de la CPU y la GPU son físicamente independientes y están conectadas y comunicadas a través del bus PCI-E. De hecho, antes de CUDA 6.0, los programadores tenían que ser conscientes de esto durante la programación y reflejarlo en el código. La asignación de memoria debe realizarse en ambos extremos de la CPU y la GPU, y la copia manual se realiza constantemente.

void sortfile(FILE *fp, int N)                       void sortfile(FILE *fp, int N)                   
{
    
                                                        {
    
    
    char *data;                                          char *data; 
    data = (char*)malloc(N);                             cudaMallocManaged(data, N);

    fread(data, 1, N, fp);                               fread(data, 1, N, fp);

    qsort(data, N, 1, compare);                          qsort<<<...>>>(data, N, 1, compare);
                                                         cudaDeviceSynchronize();

    usedata(data);                                       usedata(data);
    free(data);                                          free(data);
}

Se puede ver claramente que las dos piezas de código son sorprendentemente similares.

La única diferencia es:

Versión de GPU:
1. Utilice cudaMallocManaged para asignar memoria en lugar de malloc
2. Dado que la CPU y la GPU se ejecutan de forma asíncrona, es necesario llamar a cudaDeviceSynchronize para la sincronización después de iniciar el kernel.
3. Antes de CUDA 6.0, para realizar las funciones anteriores, es posible que se requiera el siguiente código:

void sortfile(FILE *fp, int N)    
{
    
    
    char *h_data, *d_data;                                        
    h_data= (char*)malloc(N); 
    cudaMalloc(&d_data, N);

    fread(h_data, 1, N, fp);  

    cudaMemcpy(d_data, h_data, N, cudaMemcpyHostToDevice);

    qsort<<<...>>>(data, N, 1, compare);

    cudaMemcpy(h_data, h_data, N, cudaMemcpyDeviceToHost);  //不需要手动进行同步,该函数内部会在传输数据前进行同步
    
    usedata(data);
    free(data); 
}

Hasta el momento, se puede apreciar que las principales ventajas son las siguientes:

1. Simplifica la escritura de código y el modelo de memoria
2. Puede compartir un puntero en el lado de la CPU y el lado de la GPU, sin asignar espacio por separado. Es conveniente para la gestión y reduce la cantidad de código.
3. Los idiomas están más integrados, reduciendo las diferencias gramaticales con idiomas compatibles.
4. Migración de código más conveniente.

Copia profunda

etc. . . A juzgar por la descripción anterior, parece que la cantidad de código no se ha reducido mucho. . Entonces consideremos una situación muy común. Cuando tenemos una estructura como esta:

struct dataElem {
    
    
    int data1;
    int data2;
    char *text;
}

Podríamos hacer algo como esto:

void launch(dataElem *elem) 
{
    
    
    dataElem *d_elem;
    char *d_text;

    int textlen = strlen(elem->text);

    // 在GPU端为g_elem分配空间
    cudaMalloc(&d_elem, sizeof(dataElem));
    cudaMalloc(&d_text, textlen);
    // 将数据拷贝到CPU端
    cudaMemcpy(d_elem, elem, sizeof(dataElem));
    cudaMemcpy(d_text, elem->text, textlen);
    // 根据gpu端分配的新text空间,更新gpu端的text指针
    cudaMemcpy(&(d_elem->text), &d_text, sizeof(g_text));

    // 最终CPU和GPU端拥有不同的elem的拷贝
    kernel<<< ... >>>(g_elem);
}

Pero después de CUDA 6.0, debido a la referencia de UM, puede ser así:

void launch(dataElem *elem) 
{
    
      
    kernel<<< ... >>>(elem); 
} 

Obviamente, en el caso de la copia profunda, la mensajería unificada reduce considerablemente la cantidad de código. Antes de la aparición de la mensajería unificada, debido a que los espacios de direcciones en ambos extremos no estaban sincronizados, era necesario asignar y copiar la memoria manualmente varias veces. Especialmente para los programadores que no son de CUDA, es muy incómodo y muy engorroso. Cuando la estructura de datos real es más compleja, la brecha entre los dos será más significativa.

En cuanto a la lista enlazada de estructura de datos común, es esencialmente una estructura de datos anidada compuesta de punteros Sin UM, la lista enlazada compartida entre CPU y GPU es muy difícil de manejar, y la transferencia de espacio de memoria es muy complicada.

Usar UM en este momento puede tener las siguientes ventajas:

1. Transfiere directamente los elementos de la lista enlazada entre la CPU y la GPU.
2. Modifique los elementos de la lista enlazada en cualquiera de los extremos de la CPU o la GPU.
3. Evita problemas complicados de sincronización.

Pero, de hecho, antes de la aparición de UM, la memoria de copia cero (memoria de host anclada) se puede utilizar para resolver este complicado problema. Pero aun así, la existencia de UM sigue siendo significativa, porque la adquisición de datos de la memoria del host anclada está sujeta al rendimiento de PCI-express, y se puede obtener un mejor rendimiento mediante el uso de UM. Este tema no será discutido en profundidad en este trabajo.

Cómo usar la memoria unificada en C++

Dado que el C++ moderno intenta evitar llamar explícitamente a funciones de asignación de memoria como malloc, new se usa para envolver. Por lo tanto, la mensajería unificada se puede utilizar a través de la nueva función de invalidación.

class Managed {
    
    
    void *operator new(size_t len) 
   {
    
    
        void *ptr;
         cudaMallocManaged(&ptr, len);
         return ptr;
   }
    void operator delete(void *ptr) 
   {
    
    
        cudaFree(ptr);
   }
};

Al heredar esta clase, la clase personalizada de C++ puede realizar el paso por referencia de mensajería unificada. En el constructor, llame a cudaMallocManaged para realizar el paso por valor en UM. La siguiente es una clase de cadena para ilustrar:

// 通过继承来实现 pass-by-reference
class String : public Managed {
    
    
    int length;
    char *data;
    // 通过copy constructor实现pass-by-value
    String (const String &s) {
    
    
        length = s.length;
        cudaMallocManaged(&data, length);
        memcpy(data, s.data, length);
    }
};

Comparación de las ventajas y desventajas de la memoria unificada y el direccionamiento virtual unificado

De hecho, CUDA 4.0 comenzó a admitir el direccionamiento virtual unificado (en lo sucesivo, UVA), no lo confunda con la memoria unificada. Aunque la UM depende de los rayos UVA, en realidad no son lo mismo. Para aclarar este tema, primero necesitamos saber el tipo de memoria que le importa a UVA

1.Memoria del dispositivo (posiblemente en una GPU diferente)
2.Memoria compartida en chip
3.Memoria del host

En cuanto a la memoria relacionada con subprocesos, como la memoria local y el registro en SM, obviamente no está dentro del alcance de la atención de UVA. Por lo tanto, UVA en realidad proporciona un espacio de direcciones unificado para estas memorias. Por esta razón, UVA habilita la tecnología de copia cero, asigna memoria en el lado de la CPU, le asigna CUDA VA y realiza cada operación a través de PCI-E. También tenga en cuenta que UVA nunca hará la migración de memoria por usted.

Una comparación más profunda y un análisis de rendimiento entre los dos está más allá del alcance de este artículo y puede discutirse en un artículo de seguimiento.

duda:

1. P: ¿La mensajería unificada eliminará la copia entre la memoria del sistema y la memoria de la GPU?
Respuesta: No, es solo que esta parte del trabajo de copia se entrega a CUDA para que la ejecute durante el tiempo de ejecución, lo cual solo es transparente para los programadores. La sobrecarga de la copia de la memoria aún existe, y el problema de las condiciones de carrera aún debe considerarse, para garantizar que los datos en los lados de la GPU y la CPU sean consistentes. En pocas palabras, si tiene una excelente capacidad para administrar la memoria manualmente, la mensajería unificada no puede brindarle un mejor rendimiento, sino que solo reduce su carga de trabajo.
2. Pregunta: Dado que no se ha eliminado la copia entre datos, parece que esto es solo una cuestión de tiempo del compilador. ¿Por qué todavía necesitamos calcular más de 3.0? ¿Es para engañar a todos para que compren una tarjeta?
espera... De hecho, hemos omitido muchos detalles de implementación hasta ahora. Debido a que en principio es imposible eliminar la copia, es imposible obtener todos los mensajes durante la compilación. Y lo que es más importante, en la arquitectura de GPU después de Pascal, se proporcionan funciones de migración de página bajo demanda y direccionamiento de memoria virtual de 49 bits. La longitud de direccionamiento de 49 bits es suficiente para que la GPU cubra toda la memoria del sistema y toda la memoria de la GPU. El motor de migración de páginas migra la memoria en cualquier rango direccionable a la memoria de la GPU a través de la memoria, de modo que los subprocesos de la GPU puedan acceder a la memoria no residente.
En resumen, la nueva tarjeta de arquitectura permite físicamente que la GPU acceda a la memoria "excesiva", sin modificar el código del programa, para que la GPU pueda manejar operaciones fuera del núcleo (es decir, operaciones en las que los datos a procesar exceden el local). memoria física).
Además, Pascal y Volta incluso admiten operaciones de memoria atómica en todo el sistema, que pueden abarcar múltiples GPU.Bajo múltiples GPU, la complejidad del código se puede simplificar enormemente.
Al mismo tiempo, para programas con datos dispersos, la función de migración de página bajo demanda puede cargar memoria con una granularidad más fina a través de errores de página en lugar de cargar toda la memoria, lo que ahorra más costos de migración de datos. (De hecho, la CPU tiene algo similar durante mucho tiempo y el principio es muy similar).

Supongo que te gusta

Origin blog.csdn.net/daijingxin/article/details/122462167
Recomendado
Clasificación