CUDA 에러 체크 방법
CUDA 함수의 에러 코드
모든 CUDA API 함수는 에러 코드를 리턴한다.
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를 리턴한다.
if (cudaMalloc(&devPtr, SIZE) != cudaSuccess) {
exit(1);
}
위 코드는 메모리 할당이 실패하면 프로그램을 종료한다.
cudaError_t 타입
cudaError_t는 열거형으로 정의된다.
typedef enum cudaError cudaError_t
가능한 값은 다음과 같다.
cudaSuccess: 성공cudaErrorMissingConfiguration: 설정 누락cudaErrorMemoryAllocation: 메모리 부족cudaErrorLaunchFailure: 커널 실행 실패cudaErrorInvalidDevicePointer: 유효하지 않은 device 포인터cudaErrorInvalidValue: 유효하지 않은 값cudaErrorUnknown: 알 수 없는 에러
에러 코드는 왜 필요한가?
GPU 연산은 비동기적으로 실행된다. 커널 실행 중 발생한 에러는 즉시 감지되지 않는다. 에러 코드를 체크해야 문제를 조기에 발견할 수 있다.
에러 코드를 문자열로 변환
에러 코드 숫자만으로는 의미를 파악하기 어렵다. CUDA는 두 가지 변환 함수를 제공한다.
const char* cudaGetErrorName(cudaError_t err);
const char* cudaGetErrorString(cudaError_t err);
cudaGetErrorName()은 짧은 이름을 리턴한다.
cout << cudaGetErrorName(cudaErrorMemoryAllocation) << endl;
// 출력: cudaErrorMemoryAllocation
cudaGetErrorString()은 설명 문자열을 리턴한다.
cout << cudaGetErrorString(cudaErrorMemoryAllocation) << endl;
// 출력: out of memory
두 함수 모두 NULL-terminated string을 리턴한다. 에러 코드가 유효하지 않으면 NULL 또는 nullptr을 리턴한다.
에러 체크 기본 패턴
CUDA 함수 호출 후 에러를 체크하는 패턴이다.
cudaError_t err = cudaMalloc(&dev_a, SIZE * sizeof(float));
if (cudaSuccess != err) {
printf("CUDA: ERROR!\\n");
exit(1);
}
위 코드는 cudaMalloc() 실행 후 즉시 에러를 체크한다. 실패하면 프로그램을 종료한다.
더 자세한 정보를 출력하려면 다음과 같이 작성한다.
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 매크로
매번 에러 체크 코드를 작성하면 코드가 길어진다. 매크로로 간단히 만들 수 있다.
#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); \\
}
사용 예제다.
cudaMalloc(&dev_a, SIZE * sizeof(float));
CUDA_CHECK_ERROR();
cudaMemcpy(dev_a, a, SIZE * sizeof(float), cudaMemcpyHostToDevice);
CUDA_CHECK_ERROR();
하지만 세미콜론 문제가 있다. CUDA_CHECK_ERROR();는 정상이지만, if문 안에서 사용하면 문법 에러가 발생한다.
if (condition)
CUDA_CHECK_ERROR(); // syntax error!
매크로가 전개되면 중괄호가 불완전해진다. 해결책은 do { ... } while (0) 패턴이다.
#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 modeNDEBUG: release mode
조건부 컴파일로 구분한다.
#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 프로그램에서 사용할 수 있다.
커널 실행 에러 체크
커널 실행은 에러 코드를 리턴하지 않는다.
kernel<<<1, SIZE>>>(dev_a, dev_b); // 반환값 없음
커널 실행 중 발생한 에러는 내부 에러 플래그에 저장된다. cudaGetLastError()로 확인한다.
add_kernel<<<1, SIZE>>>(dev_c, dev_a, dev_b);
cudaDeviceSynchronize();
CUDA_CHECK_ERROR();
cudaDeviceSynchronize()는 커널 실행 완료를 기다린다. 그 후 에러를 체크한다.
cudaGetLastError()는 마지막 에러를 반환하고 에러 상태를 초기화한다. cudaPeekAtLastError()는 에러를 반환하지만 상태를 초기화하지 않는다. 연속된 CUDA 호출에서 에러가 어디서 발생했는지 추적할 때 이 차이가 중요하다.
에러 발생 예제
일부러 잘못된 코드를 작성해보자.
cudaMemcpy(b, dev_b, SIZE * sizeof(float), cudaMemcpyDeviceToDevice);
위 코드는 cudaMemcpyDeviceToDevice를 사용했지만, 실제로는 device → host 복사다. 방향이 잘못되었다.
실행 결과다.
cuda failure "invalid argument" at 09d-error-detected.cu:53
CUDA_CHECK_ERROR() 매크로가 에러를 감지하고 파일 이름, 줄 번호와 함께 출력했다.
관련 문서
- CUDA Runtime API - Error Handling - CUDA 에러 처리 API 전체
- CUDA Runtime API - cudaGetErrorName - 에러 이름 가져오기
- CUDA Runtime API - cudaGetErrorString - 에러 설명 문자열 가져오기
- CUDA C Best Practices Guide - Error Handling - 에러 처리 모범 사례
- C Preprocessor - Wikipedia - C 전처리기 매크로
- CUDA C Programming Guide - Error Checking - 에러 체크 방법