최근 수정 시각 : 2026-04-30 08:11:59

CUDA/문법


파일:관련 문서 아이콘.svg   관련 문서: C++/문법
#!if 문서명2 != null
, [[]]
#!if 문서명3 != null
, [[]]
#!if 문서명4 != null
, [[]]
#!if 문서명5 != null
, [[]]
#!if 문서명6 != null
, [[]]

파일:상위 문서 아이콘.svg   상위 문서: CUDA
1. 개요2. 편집 지침3. 시작하기
3.1. 프로그래밍 모델3.2. 커널3.3. 메모리 모델
4. 기본 문법
4.1. 실행 지정자4.2. 메모리 지정자4.3. 메모리 할당4.4. 내장 자료형4.5. 내장 변수4.6. 동기화4.7. 오류 처리
5. 문법 상세
5.1. 워프 함수
5.1.1. __shfl_sync()5.1.2. __shfl_down_sync()5.1.3. __shfl_up_sync()5.1.4. __shfl_xor_sync()
5.2. 텐서 코어
6. 주의할 점7. 최적화8. 예제

1. 개요

CUDA 프로그래밍에 사용되는 C++ 확장인 CUDA C++의 문법에 대하여 전반적으로 설명하는 문서이다.

2. 편집 지침

소스 코드로 예시를 들 때 아래와 같이 문법을 활용하여 소스코드를 써 주시기 바랍니다.
{{{#!syntax cpp (소스코드)}}}

아래는 예시 코드입니다.
#!syntax cpp 
__global__ void vecAdd(float* A, float* B, float* C)
{
   int workIndex = threadIdx.x + blockDim.x * blockIdx.x

   C[workIndex] = A[workIndex] + B[workIndex];
}

int main()
{
    ...
    vecAdd<<<4, 256>>>(A, B, C);
    ...
}

3. 시작하기

CUDA C++의 문법은 기본적으로 C++ 문법을 바탕으로 하므로 용이한 이해를 위해서는 C++/문법 문서와 비교하여 참조하는 것이 좋다. 하지만 C++ 언어에 더해 CUDA 전용 문법을 아는 정도로는 CUDA C++를 충분히 이해했다고 보기 어렵다. CUDA C++를 제대로 활용하려면 호스트와 디바이스의 구분, 메모리 계층, 스레드/블록 구조, 동기화, 성능 최적화 등 CUDA 프로그래밍 모델 전반을 함께 이해해야 한다.

3.1. 프로그래밍 모델

CUDA 프로그래밍 모델은 CPUGPU가 함께 사용되는 이기종 컴퓨팅 환경을 전제로 한다. CPU와 CPU에 연결된 메모리를 각각 호스트(host), 호스트 메모리(host memory)라고 하며, GPU와 GPU에 연결된 메모리를 각각 디바이스(device), 디바이스 메모리(device memory)라고 한다.

CUDA 프로그램은 기본적으로 CPU에서 시작되며, 호스트 코드는 CUDA API를 통해 디바이스 메모리를 할당하거나, 호스트와 디바이스 사이의 데이터를 복사하거나, GPU에서 실행될 커널을 실행할 수 있다.

GPU에서 실행되는 코드는 디바이스 코드(device code)라고 하며, GPU에서 실행되도록 호출되는 함수를 커널(kernel)이라고 한다.

커널이 실행될 때 CUDA는 많은 수의 스레드를 생성하며, 이 스레드들은 블록(block)으로 묶이고, 블록들은 다시 하나의 그리드(grid)를 구성한다.

3.2. 커널

CUDA에서 커널(kernel)은 GPU에서 병렬로 실행되는 함수이다. 커널 함수는 __global__ 지정자를 사용하여 선언하며, 호스트 코드에서 <<< >>>로 실행 구성을 지정하여 호출한다.
#!syntax cpp
// 커널 정의
__global__ void VecAdd(float* A, float* B, float* C)
{
    int i = threadIdx.x;

    C[i] = A[i] + B[i];
}

int main()
{
    ...
    // N개의 스레드를 사용한 커널 호출
    VecAdd<<<1, N>>>(A, B, C);
    ...
}
이때 <<< >>>의 첫 번째 인자는 그리드 크기, 즉 그리드에 포함될 블록의 수이고, 두 번째 인자는 블록 크기, 즉 각 블록에 포함될 스레드의 수이다. 위 예시는 하나의 블록 안에서 N개의 스레드를 실행한다.

실제 프로그램에서는 처리할 원소 수가 블록 하나의 최대 스레드 수보다 클 수 있으므로, 여러 블록으로 나누어 커널을 실행하는 경우가 많다.
#!syntax cpp
__global__ void VecAdd(float* A, float* B, float* C, int N)
{
    int i = threadIdx.x + blockIdx.x * blockDim.x;

    if (i < N)
    {
        C[i] = A[i] + B[i];
    }
}

int main()
{
    ...
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;

    VecAdd<<<blocksPerGrid, threadsPerBlock>>>(A, B, C, N);
    ...
}

3.3. 메모리 모델

CUDA에서는 CPU가 주로 접근하는 호스트 메모리(host memory)와 GPU가 주로 접근하는 디바이스 메모리(device memory)가 구분된다. 일반적인 CUDA 프로그램에서는 호스트 코드가 디바이스 메모리를 할당하고, 호스트 메모리의 데이터를 디바이스 메모리로 복사한 뒤, 커널을 실행하고, 결과를 다시 호스트 메모리로 가져온다.
#!syntax cpp
float* d_A;

cudaMalloc(&d_A, N * sizeof(float));
cudaMemcpy(d_A, h_A, N * sizeof(float), cudaMemcpyHostToDevice);

Kernel<<<grid, block>>>(d_A, N);

cudaMemcpy(h_A, d_A, N * sizeof(float), cudaMemcpyDeviceToHost);
cudaFree(d_A);

4. 기본 문법

CUDA C++에서 자주 사용되는 기본 문법을 설명한다.

4.1. 실행 지정자

CUDA C++에서는 함수가 어디에서 호출되고 어디에서 실행되는지를 지정하기 위해 함수 실행 공간 지정자를 사용한다.
지정자 호출 위치 실행 위치 설명
__host__ host host CPU에서 실행되는 일반 함수이다. 생략 시 기본값이다.
__device__ device device GPU 코드에서 호출되어 GPU에서 실행되는 함수이다.
__global__ host device 호스트에서 호출되어 GPU에서 실행되는 커널 함수이다.
__host__ __device__ host/device host/device CPU와 GPU 양쪽에서 사용할 수 있도록 각각 컴파일된다.

각 지정자의 사용 예시는 아래 코드와 같다.
#!syntax cpp
__host__ void HostFunc()
{
    // CPU에서 실행
}

__device__ float DeviceFunc(float x)
{
    // GPU에서 실행
    return x * 2.0f;
}

__global__ void Kernel(float* data)
{
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    data[i] = DeviceFunc(data[i]);
}
지정자가 없는 함수는 기본적으로 __host__ 함수로 취급된다. 따라서 일반 C++ 함수는 호스트 코드에서 호출되어 CPU에서 실행된다.

4.2. 메모리 지정자

CUDA C++에서는 변수가 디바이스의 어느 메모리 공간에 배치되는지를 지정하기 위해 변수 메모리 공간 지정자를 사용한다.
지정자 메모리 공간 접근 범위 수명 설명
__device__ 전역 메모리(global memory) grid / CUDA Runtime API CUDA context 디바이스 전역 변수를 선언한다.
__constant__ 상수 메모리(constant memory) grid / CUDA Runtime API CUDA context 커널에서 읽기 전용으로 사용하는 상수 데이터를 선언한다.
__shared__ 공유 메모리(shared memory) block block 같은 블록 안의 스레드들이 공유하는 변수를 선언한다.
__managed__ 통합 메모리(unified memory) host/device program 호스트와 디바이스 양쪽에서 접근 가능한 관리 메모리 변수를 선언한다.

#!syntax cpp
__device__ int deviceValue;

__constant__ float coeff[4];

__global__ void Kernel(float* data)
{
    __shared__ float cache[256];

    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    cache[tid] = data[i];

    __syncthreads();

    data[i] = cache[tid] * coeff[0] + deviceValue;
}

__device____constant__ 전역 변수는 호스트 코드에서 직접 대입하는 것이 아니라, cudaMemcpyToSymbol(), cudaMemcpyFromSymbol(), cudaGetSymbolAddress(), cudaGetSymbolSize() 등의 CUDA Runtime API를 통해 접근한다.
#!syntax cpp
__constant__ float coeff[4];

int main()
{
    float h_coeff[4] = {1.0f, 2.0f, 3.0f, 4.0f};

    cudaMemcpyToSymbol(coeff, h_coeff, sizeof(h_coeff));
}

__shared__ 변수는 블록마다 별도로 생성된다. 따라서 서로 다른 블록의 스레드들은 같은 이름의 __shared__ 변수를 사용하더라도 실제로는 서로 다른 저장 공간을 사용한다.

4.3. 메모리 할당

CUDA에서 GPU가 사용할 전역 메모리는 보통 호스트 코드에서 CUDA Runtime API를 통해 할당한다. 가장 기본적인 방식은 cudaMalloc()으로 디바이스 메모리를 할당하고, cudaMemcpy()로 호스트와 디바이스 사이의 데이터를 복사한 뒤, cudaFree()로 해제하는 것이다.
#!syntax cpp
float* d_A = nullptr;
float* d_B = nullptr;
float* d_C = nullptr;

size_t size = N * sizeof(float);

cudaMalloc(&d_A, size);
cudaMalloc(&d_B, size);
cudaMalloc(&d_C, size);

cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);

cudaMemcpy()의 네 번째 인자는 복사 방향을 나타낸다.
의미
cudaMemcpyHostToDevice 호스트 메모리에서 디바이스 메모리로 복사
cudaMemcpyDeviceToHost 디바이스 메모리에서 호스트 메모리로 복사
cudaMemcpyDeviceToDevice 디바이스 메모리 사이에서 복사
cudaMemcpyHostToHost 호스트 메모리 사이에서 복사
cudaMemcpyDefault 포인터 값을 바탕으로 복사 방향을 추론

4.4. 내장 자료형

CUDA C++는 커널 실행 구성과 벡터형 데이터를 표현하기 위한 내장 자료형을 제공한다. 대표적으로 dim3는 그리드와 블록의 1차원, 2차원, 3차원 크기를 지정할 때 사용된다.
#!syntax cpp
dim3 block(16, 16);
dim3 grid(
    (width + block.x - 1) / block.x,
    (height + block.y - 1) / block.y
);

Kernel2D<<<grid, block>>>(data, width, height);

정수 하나를 사용한 실행 구성은 사실상 나머지 차원이 1인 dim3 값처럼 취급할 수 있다.
#!syntax cpp
Kernel<<<16, 256>>>();

// 개념적으로는 다음과 유사하다.
dim3 grid(16, 1, 1);
dim3 block(256, 1, 1);
Kernel<<<grid, block>>>();

또한 int2, int3, float2, float3, float4와 같은 벡터 자료형도 제공된다. 각 성분은 x, y, z, w로 접근할 수 있다.
#!syntax cpp
float3 pos = make_float3(1.0f, 2.0f, 3.0f);

float x = pos.x;
float y = pos.y;
float z = pos.z;

4.5. 내장 변수

커널 내부에서는 CUDA가 제공하는 내장 변수를 통해 현재 스레드와 블록의 위치를 알 수 있다.
변수 자료형 설명
threadIdx uint3 현재 스레드의 블록 내부 인덱스
blockIdx uint3 현재 블록의 그리드 내부 인덱스
blockDim dim3 블록 하나에 포함된 스레드 수
gridDim dim3 그리드에 포함된 블록 수
warpSize int 워프 하나에 포함되는 스레드 수. 일반적으로 32이다.

1차원 배열을 처리할 때는 보통 다음과 같이 전역 인덱스를 계산한다.
#!syntax cpp
int i = threadIdx.x + blockIdx.x * blockDim.x;

2차원 데이터에서는 x, y 성분을 함께 사용한다.
#!syntax cpp
__global__ void Kernel2D(float* data, int width, int height)
{
    int x = threadIdx.x + blockIdx.x * blockDim.x;
    int y = threadIdx.y + blockIdx.y * blockDim.y;

    if (x < width && y < height)
    {
        int i = x + y * width;
        data[i] = data[i] * 2.0f;
    }
}

threadIdxblockIdx는 0부터 시작한다. 예를 들어 threadIdx.x0부터 blockDim.x - 1까지의 값을 가진다.

4.6. 동기화

CUDA에서는 호스트와 디바이스 사이의 동기화, 블록 내부 스레드 사이의 동기화, 워프 내부 스레드 사이의 동기화를 구분해야 한다.

호스트 코드에서 커널 호출은 기본적으로 비동기적으로 수행된다. 커널 실행이 끝날 때까지 기다리려면 cudaDeviceSynchronize()를 사용한다.
#!syntax cpp
Kernel<<<grid, block>>>(data);

cudaDeviceSynchronize();

블록 내부의 모든 스레드를 동기화할 때는 __syncthreads()를 사용한다. 이 함수는 같은 블록 안의 모든 스레드가 해당 지점에 도달할 때까지 기다리게 하며, 공유 메모리를 사용한 협력 계산에서 자주 사용된다.
#!syntax cpp
__global__ void SharedExample(float* data)
{
    __shared__ float cache[256];

    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    cache[tid] = data[i];

    __syncthreads();

    data[i] = cache[tid] * 2.0f;
}

__syncthreads()는 같은 블록 안에서만 동작한다. 서로 다른 블록 사이의 스레드는 일반적인 커널 내부에서 직접 동기화할 수 없다.

조건 평가를 함께 수행하는 변형도 제공된다.
함수 설명
__syncthreads() 블록 내부 스레드 전체를 동기화한다.
__syncthreads_count(predicate) 조건을 만족한 스레드 수를 반환한다.
__syncthreads_and(predicate) 모든 스레드가 조건을 만족하면 참을 반환한다.
__syncthreads_or(predicate) 하나 이상의 스레드가 조건을 만족하면 참을 반환한다.
__syncwarp(mask) 지정된 워프 lane들을 동기화한다.

__syncthreads()를 조건문 안에서 사용할 때는 같은 블록의 모든 스레드가 동일하게 해당 호출에 도달해야 한다. 일부 스레드만 도달하는 비균일 조건에서는 교착 상태나 정의되지 않은 동작이 발생할 수 있다.

4.7. 오류 처리

5. 문법 상세

CUDA C++의 세부 문법과 고급 실행 기능을 설명한다.

5.1. 워프 함수

CUDA에서 워프(warp)는 같은 블록 안의 스레드 32개로 구성되는 실행 단위이다. CUDA는 워프 내부의 스레드들이 값을 교환하거나, 조건을 평가하거나, 동기화할 수 있도록 여러 워프 단위 함수를 제공한다. 이러한 함수들은 블록 전체가 아니라 하나의 워프 안에서 동작하므로, 블록 단위 동기화 함수인 __syncthreads()와 구분된다.

워프 함수는 공유 메모리를 사용하지 않고도 같은 워프 안에서 값을 교환할 수 있어 reduction, prefix sum, FFT 등에서 자주 사용된다. 다만 워프 단위 함수는 워프 내부의 실행과 활성 스레드 마스크에 의존하므로, 분기문 안에서 사용할 때는 참여하는 스레드 집합을 정확히 고려해야 한다.

대표적인 워프 함수는 다음과 같다.
함수 설명
__syncwarp() 같은 워프 안의 지정된 스레드들을 동기화한다.
__shfl_sync() 같은 워프 안의 다른 스레드가 가진 값을 가져온다.
__shfl_up_sync() 워프 안에서 더 낮은 lane의 값을 가져온다.
__shfl_down_sync() 워프 안에서 더 높은 lane의 값을 가져온다.
__shfl_xor_sync() lane 번호에 XOR 연산을 적용한 위치의 값을 가져온다.
__ballot_sync() 워프 안의 각 스레드가 조건을 만족하는지 비트마스크로 반환한다.
__all_sync() 워프 안의 모든 활성 스레드가 조건을 만족하는지 확인한다.
__any_sync() 워프 안의 하나 이상의 활성 스레드가 조건을 만족하는지 확인한다.

5.1.1. __shfl_sync()

__shfl_sync()는 같은 워프 안의 지정된 lane이 가진 값을 가져오는 함수이다. 기본 형태는 다음과 같다.
#!syntax cpp
T __shfl_sync(unsigned mask, T var, int srcLane, int width = warpSize);
여기서 mask는 연산에 참여하는 lane의 비트마스크이고, var는 각 lane이 가지고 있는 값이며, srcLane은 값을 가져올 source lane 번호이다. 모든 lane이 같은 srcLane을 지정하면 해당 lane의 값이 워프 전체로 전달되는 broadcast처럼 동작한다. 반대로 lane마다 서로 다른 srcLane을 지정하면 워프 내부 값들을 재배열하는 permutation처럼 사용할 수 있다.
#!syntax cpp
__global__ void ShuffleExample(int* output)
{
    int lane = threadIdx.x % warpSize;
    int value = threadIdx.x;

    int fromLane0 = __shfl_sync(0xffffffff, value, 0);

    output[threadIdx.x] = fromLane0 + lane;
}
예시에서 __shfl_sync(0xffffffff, value, 0)는 같은 워프 안의 lane 0 스레드가 가진 value 값을 워프의 다른 스레드들에게 전달한다. 0xffffffff는 워프의 32개 lane을 모두 참여 대상으로 지정하는 마스크이다.

다음 예시는 각 lane이 바로 오른쪽 lane의 값을 가져오도록 srcLane을 변수로 계산한 것이다.
#!syntax cpp
__global__ void ShufflePermutationExample(int* output)
{
    int lane = threadIdx.x % warpSize;
    int value = lane;

    int srcLane = (lane + 1) % warpSize;
    int fromNextLane = __shfl_sync(0xffffffff, value, srcLane);

    output[threadIdx.x] = fromNextLane;
}
이 경우 lane 0은 lane 1의 값을, lane 1은 lane 2의 값을 가져오며, lane 31은 lane 0의 값을 가져온다. 즉 워프 내부의 값이 lane 사이에서 순환 이동한 것과 같은 결과가 된다.

5.1.2. __shfl_down_sync()

__shfl_down_sync()는 같은 워프 안에서 현재 lane보다 delta만큼 높은 번호의 lane이 가진 값을 가져오는 함수이다.
#!syntax cpp
T __shfl_down_sync(unsigned mask, T var, unsigned int delta, int width = warpSize);
예를 들어 delta가 1이면 lane 0은 lane 1의 값을, lane 1은 lane 2의 값을 가져온다. delta는 반드시 2의 거듭제곱일 필요는 없지만, 워프 내부 reduction에서는 16, 8, 4, 2, 1처럼 절반씩 줄어드는 값이 자주 사용된다.
#!syntax cpp
__global__ void WarpReduceSum(const float* input, float* output)
{
    int lane = threadIdx.x % warpSize;
    int warpId = threadIdx.x / warpSize;
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    float value = input[i];

    for (int delta = warpSize / 2; delta > 0; delta /= 2)
    {
        value += __shfl_down_sync(0xffffffff, value, delta);
    }

    if (lane == 0)
    {
        output[blockIdx.x * (blockDim.x / warpSize) + warpId] = value;
    }
}
위 예시는 같은 워프 안의 값들을 합산하여 lane 0에 모으는 reduction 패턴이다. delta16, 8, 4, 2, 1로 줄어들면서 높은 lane의 값을 가져와 더한다.

5.1.3. __shfl_up_sync()

__shfl_up_sync()는 같은 워프 안에서 현재 lane보다 delta만큼 낮은 번호의 lane이 가진 값을 가져오는 함수이다.
#!syntax cpp
T __shfl_up_sync(unsigned mask, T var, unsigned int delta, int width = warpSize);
예를 들어 delta가 1이면 lane 1은 lane 0의 값을, lane 2는 lane 1의 값을 가져온다. 낮은 번호 쪽 경계를 벗어나는 lane은 유효한 source lane이 없으므로, 보통 조건문으로 결과 사용 여부를 제한한다.
#!syntax cpp
__global__ void WarpPrefixSum(const int* input, int* output)
{
    int lane = threadIdx.x % warpSize;
    int i = blockIdx.x * blockDim.x + threadIdx.x;

    int value = input[i];

    for (int delta = 1; delta < warpSize; delta *= 2)
    {
        int other = __shfl_up_sync(0xffffffff, value, delta);

        if (lane >= delta)
        {
            value += other;
        }
    }

    output[i] = value;
}
위 예시는 워프 내부 prefix sum, 즉 scan 패턴이다. 각 lane은 자신보다 낮은 번호의 lane들이 가진 값을 단계적으로 더해, 반복이 끝나면 자기 lane까지의 누적합을 갖는다.

5.1.4. __shfl_xor_sync()

__shfl_xor_sync()는 같은 워프 안에서 현재 lane 번호에 laneMask를 XOR 연산한 lane의 값을 가져오는 함수이다.
#!syntax cpp
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width = warpSize);
예를 들어 laneMask가 1이면 lane 0은 lane 1과, lane 1은 lane 0과, lane 2는 lane 3과 값을 교환하는 식으로 동작한다.

5.2. 텐서 코어

6. 주의할 점

7. 최적화

8. 예제

분류