junyeokk
Blog
Parallel Computing·2025. 10. 20

CUDA 에러 체크 방법

CUDA 함수의 에러 코드

모든 CUDA API 함수는 에러 코드를 리턴한다.

c
cudaError_t cudaMalloc(void** devPtr, size_t nbytes);
cudaError_t cudaFree(void* devPtr);
cudaError_t cudaMemcpy(void* dst, const void* src, size_t nbytes,
                       enum cudaMemcpyKind direction);

함수 이름 앞에 "cuda"가 붙으면 GPU 관련 함수다. 반환 타입은 cudaError_t다. 성공하면 cudaSuccess를 리턴한다.

c
if (cudaMalloc(&devPtr, SIZE) != cudaSuccess) {
    exit(1);
}

위 코드는 메모리 할당이 실패하면 프로그램을 종료한다.

cudaError_t 타입

cudaError_t는 열거형으로 정의된다.

c
typedef enum cudaError cudaError_t

가능한 값은 다음과 같다.

  • cudaSuccess: 성공
  • cudaErrorMissingConfiguration: 설정 누락
  • cudaErrorMemoryAllocation: 메모리 부족
  • cudaErrorLaunchFailure: 커널 실행 실패
  • cudaErrorInvalidDevicePointer: 유효하지 않은 device 포인터
  • cudaErrorInvalidValue: 유효하지 않은 값
  • cudaErrorUnknown: 알 수 없는 에러

에러 코드는 왜 필요한가?
GPU 연산은 비동기적으로 실행된다. 커널 실행 중 발생한 에러는 즉시 감지되지 않는다. 에러 코드를 체크해야 문제를 조기에 발견할 수 있다.

에러 코드를 문자열로 변환

에러 코드 숫자만으로는 의미를 파악하기 어렵다. CUDA는 두 가지 변환 함수를 제공한다.

c
const char* cudaGetErrorName(cudaError_t err);
const char* cudaGetErrorString(cudaError_t err);

cudaGetErrorName()은 짧은 이름을 리턴한다.

c
cout << cudaGetErrorName(cudaErrorMemoryAllocation) << endl;
// 출력: cudaErrorMemoryAllocation

cudaGetErrorString()은 설명 문자열을 리턴한다.

c
cout << cudaGetErrorString(cudaErrorMemoryAllocation) << endl;
// 출력: out of memory

두 함수 모두 NULL-terminated string을 리턴한다. 에러 코드가 유효하지 않으면 NULL 또는 nullptr을 리턴한다.

에러 체크 기본 패턴

CUDA 함수 호출 후 에러를 체크하는 패턴이다.

c
cudaError_t err = cudaMalloc(&dev_a, SIZE * sizeof(float));
if (cudaSuccess != err) {
    printf("CUDA: ERROR!\\n");
    exit(1);
}

위 코드는 cudaMalloc() 실행 후 즉시 에러를 체크한다. 실패하면 프로그램을 종료한다.

더 자세한 정보를 출력하려면 다음과 같이 작성한다.

c
cudaError_t err = cudaMalloc(&dev_a, SIZE * sizeof(float));
if (cudaSuccess != err) {
    printf("cuda failure \\"%s\\" at %s:%d\\n",
        cudaGetErrorString(err), __FILE__, __LINE__);
    exit(1);
}

__FILE____LINE__은 C 전처리기 매크로다. 현재 파일 이름과 줄 번호를 제공한다.

CUDA_CHECK_ERROR 매크로

매번 에러 체크 코드를 작성하면 코드가 길어진다. 매크로로 간단히 만들 수 있다.

c
#define CUDA_CHECK_ERROR() \\
    cudaError_t e = cudaGetLastError(); \\
    if (cudaSuccess != e) { \\
        printf("cuda failure \\"%s\\" at %s:%d\\n", \\
            cudaGetErrorString(e), __FILE__, __LINE__); \\
        exit(1); \\
    }

사용 예제다.

c
cudaMalloc(&dev_a, SIZE * sizeof(float));
CUDA_CHECK_ERROR();

cudaMemcpy(dev_a, a, SIZE * sizeof(float), cudaMemcpyHostToDevice);
CUDA_CHECK_ERROR();

하지만 세미콜론 문제가 있다. CUDA_CHECK_ERROR();는 정상이지만, if문 안에서 사용하면 문법 에러가 발생한다.

c
if (condition)
    CUDA_CHECK_ERROR();  // syntax error!

매크로가 전개되면 중괄호가 불완전해진다. 해결책은 do { ... } while (0) 패턴이다.

c
#define CUDA_CHECK_ERROR() do { \\
    cudaError_t e = cudaGetLastError(); \\
    if (cudaSuccess != e) { \\
        printf("cuda failure \\"%s\\" at %s:%d\\n", \\
            cudaGetErrorString(e), __FILE__, __LINE__); \\
        exit(1); \\
    } \\
} while (0)

이제 세미콜론과 함께 안전하게 사용할 수 있다.

Release vs Debug 모드

에러 체크는 성능 오버헤드를 유발한다. 개발 중에는 필요하지만, 릴리스 버전에서는 제거할 수 있다.

C/C++ 컴파일러는 두 가지 매크로를 제공한다.

  • _DEBUG: debug mode
  • NDEBUG: release mode

조건부 컴파일로 구분한다.

c
#if defined(NDEBUG)  // release mode
#define CUDA_CHECK_ERROR() 0
#else  // debug mode
#define CUDA_CHECK_ERROR() do { \\
    cudaError_t e = cudaGetLastError(); \\
    if (cudaSuccess != e) { \\
        printf("cuda failure \\"%s\\" at %s:%d\\n", \\
            cudaGetErrorString(e), __FILE__, __LINE__); \\
        exit(1); \\
    } \\
} while (0)
#endif

Release mode에서는 CUDA_CHECK_ERROR()가 0으로 치환된다. 아무 동작도 하지 않는다. Debug mode에서는 완전한 에러 체크를 수행한다.

이 매크로를 "./common.cpp"에 추가하면 모든 CUDA 프로그램에서 사용할 수 있다.

커널 실행 에러 체크

커널 실행은 에러 코드를 리턴하지 않는다.

c
kernel<<<1, SIZE>>>(dev_a, dev_b);  // 반환값 없음

커널 실행 중 발생한 에러는 내부 에러 플래그에 저장된다. cudaGetLastError()로 확인한다.

c
add_kernel<<<1, SIZE>>>(dev_c, dev_a, dev_b);
cudaDeviceSynchronize();
CUDA_CHECK_ERROR();

cudaDeviceSynchronize()는 커널 실행 완료를 기다린다. 그 후 에러를 체크한다.

cudaGetLastError()는 마지막 에러를 반환하고 에러 상태를 초기화한다. cudaPeekAtLastError()는 에러를 반환하지만 상태를 초기화하지 않는다. 연속된 CUDA 호출에서 에러가 어디서 발생했는지 추적할 때 이 차이가 중요하다.

에러 발생 예제

일부러 잘못된 코드를 작성해보자.

c
cudaMemcpy(b, dev_b, SIZE * sizeof(float), cudaMemcpyDeviceToDevice);

위 코드는 cudaMemcpyDeviceToDevice를 사용했지만, 실제로는 device → host 복사다. 방향이 잘못되었다.

실행 결과다.

plain
cuda failure "invalid argument" at 09d-error-detected.cu:53

CUDA_CHECK_ERROR() 매크로가 에러를 감지하고 파일 이름, 줄 번호와 함께 출력했다.

관련 문서