it-swarm.dev

CUDA: ¿cómo sumar todos los elementos de una matriz en un número dentro de la GPU?

En primer lugar, permítanme decir que soy plenamente consciente de que mi pregunta ya se ha formulado: Reducción de bloque en CUDA Sin embargo, como espero aclarar, mi pregunta es un seguimiento de eso y Tengo necesidades particulares que hacen que la solución encontrada por ese OP sea inadecuada.

Entonces, déjame explicarte. En mi código actual, ejecuto un kernel Cuda en cada iteración de un ciclo while para hacer algunos cálculos sobre los valores de una matriz. Como ejemplo, piénselo de la siguiente manera:

int max_iterations = 1000;
int iteration = 0;
while(iteration < max_iterations)
{
    __global__ void calcKernel(int* idata, int* odata)
    {
        int i = blockIdx.x*blockDim.x + threadIdx.x;
        if (i < n)
        {
            odata[i] = (idata[i] + 2) * 5;
        }
    }

    iteration++;
}

Sin embargo, a continuación tengo que realizar una tarea aparentemente difícil para la GPU. En cada iteración del ciclo while que llama al núcleo, tengo que sumar todos los valores generados dentro de odata y guardar el resultado en una matriz int llamada result, en una posición dentro de dicha matriz que corresponde a la iteración actual. Debe realizarse dentro del núcleo o al menos aún en la GPU porque debido a restricciones de rendimiento, solo puedo recuperar la matriz result al final una vez que se completan todas las iteraciones.

Un intento ingenuo equivocado se parecería a lo siguiente:

int max_iterations = 1000;
int iteration = 0;
while(iteration < max_iterations)
{
    __global__ void calcKernel(int* idata, int* odata, int* result)
    {
        int i = blockIdx.x*blockDim.x + threadIdx.x;
        if (i < n)
        {
            odata[i] = (idata[i] + 2) * 5;
        }
    }

    result[iteration] = 0;
    for(int j=0; j < max_iterations; j++)
    {
        result[iteration] += odata[j];            
    }

    iteration++;
}

Por supuesto, el código anterior no funciona debido a que la GPU distribuye el código a través de subprocesos. Para aprender cómo hacer eso correctamente, he estado leyendo otras preguntas aquí en el sitio sobre la reducción de matriz usando CUDA. En particular, encontré una mención a un muy buen pdf de NVIDIA sobre este tema, que también se discute en la pregunta anterior SO que mencioné al principio: http: // desarrollador .download.nvidia.com/compute/cuda/1.1-Beta/x86_website/projects/reduce/doc/reduce.pdf

Sin embargo, si bien entiendo completamente los pasos del código que se describen en dichas diapositivas, así como las optimizaciones generales, no entiendo cómo ese enfoque puede reducir una matriz a un número si el código realmente supera a una matriz completa (y uno de dimensiones poco claras). ¿Podría alguien arrojar algo de luz al respecto y mostrarme un ejemplo de cómo funcionaría (es decir, cómo sacar el número uno de la matriz de salida)?

Ahora, volviendo a esa pregunta que mencioné al principio ( Reducción de bloque en CUDA ). Tenga en cuenta que su respuesta aceptada simplemente sugiere leer el pdf que he vinculado anteriormente, que no habla sobre qué hacer con la matriz de salida generada por el código . En los comentarios, el OP menciona que él/ella pudo terminar el trabajo sumando la matriz de salida en la CPU, que es algo que no puedo hacer, ya que eso significaría descargar la matriz de salida cada iteración de mi ciclo while. Por último, la tercera respuesta en ese enlace sugiere el uso de una biblioteca para lograr esto, pero estoy interesado en aprender la forma nativa de hacerlo.

Alternativamente, también estaría muy interesado en cualquier otra propuesta sobre cómo implementar lo que se describe anteriormente.

6
AndrewSteer

Ya ha encontrado la información canónica sobre las reducciones paralelas de bloque, por lo que no repetiré eso. Si no desea escribir mucho código nuevo para hacerlo, le sugiero que busque en la biblioteca CUB block_reduce implementación , que proporciona una operación óptima de reducción en bloque con la adición de aproximadamente 4 líneas de código a su núcleo existente.

En la pregunta real aquí, puedes hacer lo que quieras si haces algo como esto:

__global__ void kernel(....., int* iter_result, int iter_num) {

    // Your calculations first so that each thread holds its result

    // Block wise reduction so that one thread in each block holds sum of thread results

    // The one thread holding the adds the block result to the global iteration result
    if (threadIdx.x == 0)
        atomicAdd(iter_result + iter_num, block_ressult);
}

La clave aquí es que un función atómica se usa para actualizar de manera segura el resultado de ejecución del kernel con los resultados de un bloque dado sin una carrera de memoria. Absolutamente debe inicializar iter_result antes de ejecutar el núcleo, de lo contrario el código no funcionará, pero ese es el patrón de diseño básico del núcleo.

4
talonmies

Si agrega 2 números contiguos y guarda el resultado, en cualquiera de las ranuras donde guarda esos números, solo tendrá que ejecutar, varias veces el mismo núcleo, para seguir reduciendo en 2 potencias las sumas de la matriz, como en este ejemplo :

Matriz para sumar valores:

[·1,·2,·3,·4,·5,·6,·7,·8,·9,·10]

Primero ejecute n/2 subprocesos, sume elementos de matriz contiguos y almacénelos a la "izquierda" de cada uno, la matriz ahora se verá así:

[·3,2,·7,4,·11,6,·15,8,·19,10]

Ejecute el mismo núcleo, ejecute n/4 subprocesos, ahora agregue cada 2 elementos y guárdelo en el elemento más a la izquierda, la matriz ahora se verá así:

[·10,2,7,4,·26,6,15,8,·19,10]

Ejecute el mismo kernel, ejecute n/8 subprocesos, ahora agregue cada 4 elementos y almacene en el elemento más a la izquierda en la matriz, para obtener:

[·36,2,7,4,26,6,15,8,·19,10]

Ejecute una última vez, un solo hilo para agregar cada 8 elementos, y almacene en el elemento más a la izquierda en la matriz, para obtener:

[55,2,7,4,26,6,15,8,19,10]

De esta manera, solo tiene que ejecutar su núcleo con algunos hilos como parámetros, para obtener el redux al final, en el primer elemento (55) mire los "puntos" (·) para ver qué elementos en la matriz están "activos" "para resumirlos, cada carrera.