CUDA 벡터 셈: 스레드 수에 따른 구현 전략

CUDA에서 벡터 덧셈을 구현할 때, 데이터 개수와 할당된 스레드 수의 관계에 따라 접근 방식이 달라진다. 스레드가 충분한 경우와 부족한 경우를 각각 살펴다.

스레드가 충분할 때: 1:1 매핑

데이터 개수만큼 블록을 할당하면 각 요소를 독립적인 스레드가 처리한다.

#include <iostream>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#define TOTAL_ELEMS 10

__global__ void vecAddKernel(const int* __restrict__ srcA,
                             const int* __restrict__ srcB,
                             int* __restrict__ dst)
{
    int idx = blockIdx.x;
    dst[idx] = srcA[idx] + srcB[idx];
}

int main()
{
    int hostA[TOTAL_ELEMS], hostB[TOTAL_ELEMS], hostC[TOTAL_ELEMS];
    int *gpuA, *gpuB, *gpuC;

    cudaMalloc((void**)&gpuA, TOTAL_ELEMS * sizeof(int));
    cudaMalloc((void**)&gpuB, TOTAL_ELEMS * sizeof(int));
    cudaMalloc((void**)&gpuC, TOTAL_ELEMS * sizeof(int));

    for (int k = 0; k < TOTAL_ELEMS; k++) {
        hostA[k] = k;
        hostB[k] = k + 1;
    }

    cudaMemcpy(gpuA, hostA, TOTAL_ELEMS * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(gpuB, hostB, TOTAL_ELEMS * sizeof(int), cudaMemcpyHostToDevice);

    vecAddKernel<<<TOTAL_ELEMS, 1>>>(gpuA, gpuB, gpuC);

    cudaMemcpy(hostC, gpuC, TOTAL_ELEMS * sizeof(int), cudaMemcpyDeviceToHost);

    for (int k = 0; k < TOTAL_ELEMS; k++) {
        std::cout << hostC[k] << " ";
    }

    cudaFree(gpuA);
    cudaFree(gpuB);
    cudaFree(gpuC);

    return 0;
}

위 예시에서는 TOTAL_ELEMS개의 블록에 각각 1개의 스레드를 배치하여, 전체 요소를 동시에 처리한다.

스레드가 부족할 때: 순환 처리(Strided Loop)

32개의 요소를 더해야 하는데 GPU에 2개 블록 × 4개 스레드 = 8개 스레드만 할당된 상황을 가정한다. 8개 스레드가 32개 데이터를 번갈아가며 처리해야 한다.

각 스레드는 자신의 고유 인덱스에서 시작해, 전체 스레드 수(gridDim.x × blockDim.x)만큼 건너며 데이터를 처리한다.

#include <iostream>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#define DATA_COUNT 32

__global__ void stridedVecAdd(const int* __restrict__ inX,
                              const int* __restrict__ inY,
                              int* __restrict__ outZ)
{
    int pos = blockIdx.x * blockDim.x + threadIdx.x;
    int leap = gridDim.x * blockDim.x;

    for (; pos < DATA_COUNT; pos += leap) {
        outZ[pos] = inX[pos] + inY[pos];
    }
}

int main(int argc, char** argv)
{
    int cpuBufX[DATA_COUNT], cpuBufY[DATA_COUNT], cpuBufZ[DATA_COUNT];
    int *gpuBufX, *gpuBufY, *gpuBufZ;

    cudaMalloc((void**)&gpuBufX, DATA_COUNT * sizeof(int));
    cudaMalloc((void**)&gpuBufY, DATA_COUNT * sizeof(int));
    cudaMalloc((void**)&gpuBufZ, DATA_COUNT * sizeof(int));

    for (int j = 0; j < DATA_COUNT; j++) {
        cpuBufX[j] = j;
        cpuBufY[j] = j;
    }

    cudaMemcpy(gpuBufX, cpuBufX, DATA_COUNT * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(gpuBufY, cpuBufY, DATA_COUNT * sizeof(int), cudaMemcpyHostToDevice);

    stridedVecAdd<<<2, 4>>>(gpuBufX, gpuBufY, gpuBufZ);

    cudaMemcpy(cpuBufZ, gpuBufZ, DATA_COUNT * sizeof(int), cudaMemcpyDeviceToHost);

    for (int j = 0; j < DATA_COUNT; j++) {
        std::cout << cpuBufZ[j] << " ";
    }

    cudaFree(gpuBufX);
    cudaFree(gpuBufY);
    cudaFree(gpuBufZ);

    return 0;
}

0번 스레드는 0, 8, 16, 24번 인덱스를, 1번 스레드는 1, 9, 17, 25번 인덱스를 담당한다. 이 패턴은 leap 간격의 등차수열 형태로 전개되며, 제한된 스레드 자원으로 대용량 데이터를 처리할 수 있게 한다.

태그: CUDA GPU Programming parallel computing Vector Addition Strided Access

9월 26일 02:58에 게시됨