CPU가 운영체제와 같은 컴퓨터 시스템의 기본 연산 장치이며, GPU등 다른 연산 장치를 사용하기 위해서는 호스트 코드에서 커널을 호출해야함.
CPU와 GPU는 서로 독립된 장치
: 사용하는 메모리 영역도 다르다.
모든 데이터는 기본적으로 호스트 메모리에 저장
: GPU를 이용해서 데이터를 처리하기 이해서는 호스트 메모리에 있는 데이터를 디바이스 메모리로 복사해주어야 함.
디바이스 메모리 공간 할당.
cudaError_t cudaMalloc (void ** ptr, size_t size)
: 첫 번째 인자인 ptr은 할당된 디바이스 메모리 공간의 시작 주소를 담을 포인터 변수의 주소
: 두 번째 인자인 size는 할당할 공간의 크기로 단위는 byte.
: size_t 형에 대해 다시 짚고 넘어가자면...
size_t can store the maximum size of a theoretically possible object of any type (C99 원문)
: 반환값은 cudaError_t enumeration 으로, API 호출에 성공하면 cudaSuccess (=0)가 반환.
: 실패시 대표적인 에러 코드는 cudaErrorMemoryAllocation (=2)
디바이스 메모리 공간에 int형 데이터 32개를 담을 공간이 필요하다면,
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
int main(void)
{
int *dDataPtr;
coudaMalloc(&dDataPtr, sizeof(int)*32);
}
: 할당된 메모리 공간의 시작 주소가 dDataPtr 포인터 변수에 저장. -> dDataPtr 포인터 변수를 통해 할당된 메모리 영역에 접근
: 디바이스 메모리상의 주소는 호스트 코드에서 직접 접근할 수 없다.
디바이스 메모리 해제
cudaError_t cudaFree (void* ptr)
: 인자 ptr은 해제할 메모리 공간을 가리키는 포인터 변수
: 반환값은 cudaMalloc()과 같이 cudaError_t enumeration
cudaMalloc() 예제에서 할당한 디바이스 멤리 공간을 해제하려면
cudaFree(dDataPtr);
디바이스 메모리 초기화
: cudaMalloc()을 통해 메모리 공간을 할당 -> 해당 메모리 공간에 남아 있던 garbage value가 그대로 남아있기 때문에, 디바이스 메모리 공간을 특정 값으로 초기화하는 cudaMemset() 함수가 필요하다.
cudaError_t cudaMemset (void *ptr, int value, size_t size)
: 첫 번째 인자 ptr은 값을 초기화 할 메모리 공간의 시작 주소
: 마지막 인자 size는 초기화 할 메모리 공간의 크기
: 두 번째 인자 value는 해당 공간의 각 바이트를 초기화 할 값을 의미.
cudaMalloc() 예제에서 할당된 메모리 공간의 값을 모두 0으로 초기화하고 싶다면,
cudaMemset(dDataPtr, 0, sizeof(int)*32)
에러 코드 확인
__host__ __device__ const char* cudaGetErrorName() (cudaError_t error)
: 원형 앞에 __host__ 와 __device__ 가 모두 붙어있는데, 이는 호스트와 디바이스 코드 모두에서 사용 가능함을 의미
%%cu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
void checkDeviceMemory(void)
{
size_t free, total;
cudaMemGetInfo(&free, &total);
printf("Device memory (free / total) = %lld / %lld bytes\n", free, total);
}
int main(void)
{
int* dDataPtr;
cudaError_t errorCode;
checkDeviceMemory();
errorCode = cudaMalloc(&dDataPtr, sizeof(int)* 1024 * 1024);
printf("cudaMalloc - %s\n", cudaGetErrorName(errorCode));
checkDeviceMemory();
errorCode = cudaMemset(dDataPtr, 0, sizeof(int) * 1024 * 1024);
printf("cudaMemset - %s\n", cudaGetErrorName(errorCode));
errorCode = cudaFree(dDataPtr);
printf("cudaFree -%s\n", cudaGetErrorName(errorCode));
checkDeviceMemory();
}
Device memory (free / total) = 15727394816 / 15835398144 bytes
cudaMalloc - cudaSuccess
Device memory (free / total) = 15723200512 / 15835398144 bytes
cudaMemset - cudaSuccess
cudaFree -cudaSuccess
Device memory (free / total) = 15727394816 / 15835398144 bytes
: checkDeviceMemory() 함수는 현재 디바이스 메모리 상태를 출력하는 함수.
: 디바이스 메모리의 총 크기와 현재 사용 가능한 공간의 크기를 반환해주는 cudaMemGetInfo() 함수를 사용하였다. 가용 메모리 크기는 free에, 사용하는 GPU가 가진 총 디바이스 메모리 크기는 total에 바이트 단위로 반환.
: 코드에서 메모리 할당 크기를 디바이스 메모리 크기보다 크게 설정하면 에러가 발생한다.
장치 간 데이터 복사
cudaError_t cudaMemcpy (void* dst, const void* src, size_t size, enum cudaMemcpyKind kind)
: 첫 번째 인자 dst는 데이터가 복사될 메모리 공간의 시작 주소 (destination)
: 두 번째 인자 src는 복사할 원본 데이터가 들어있는 메모리 공간의 시작 주소 (sorce)
: 세 번째 인자 size는 복사할 데이터의 크기 (byte단위)
: 마지막 인자 kind는 cudaMemcpyKind열거형 변수, 데이터 복사의 방향을 설정. 다음은 cudaMemcpyKind 열거형의 항목들이다.
cudaMemcpyHostToHost Host -> Host
cudaMemcpyHostToDevice Host -> Device
cudaMemcpyDeviceToHost Device -> Host
cudaMemcpyDeviceToDevice Device -> Device
cudaMemcpyDefault dst와 src의 포인터 값에 의해 결정
: cudaMemcpyDefault 는 호스트 메모리와 디바이스 메모리를 하나의 메모리 공간처럼 가상화 해주는 unified virtual addressing을 지원하는 시스템에서만 사용가능.
: 복사 방향을 명시적으로 지정하는 것을 추천.
%%cu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
__global__ void printData(int* _dDataPtr)
{
printf("%d", _dDataPtr[threadIdx.x]);
}
__global__ void setData(int* _dDataPtr)
{
_dDataPtr[threadIdx.x] = 2;
}
int main(void)
{
int data[10] = { 0 };
for (int i = 0; i < 10; i++)
data[i] = 1;
int* dDataPtr;
cudaMalloc(&dDataPtr, sizeof(int)*10);
cudaMemset(dDataPtr, 0, sizeof(int)*10);
printf("Data in device: ");
printData<<<1, 10>>>(dDataPtr);
cudaMemcpy(dDataPtr, data, sizeof(int)*10, cudaMemcpyHostToDevice);
printf("\nHost -> Device: ");
printData<<<1, 10>>>(dDataPtr);
setData<<<1, 10>>> (dDataPtr);
cudaMemcpy(data, dDataPtr, sizeof(int)*10, cudaMemcpyDeviceToHost);
printf("\nDevice -> Host: ");
for (int i = 0; i < 10 ; i++)
printf("%d", data[i]);
cudaFree(dDataPtr);
}
Data in device: 0000000000
Host -> Device: 1111111111
Device -> Host: 2222222222
cudaMemcpy2D()와 cudaMemcpy3D()cudaMemcpyAsync() (동기화의 개념 및 비동기적 데이터 복사 등에 대한 내용은 뒤에서 다룬다.)int *da, *db, *dc
cudaMalloc(&da, memSize); cudaMemset(da, 0, memSize);
cudaMalloc(&db, memSize); cudaMemset(db, 0, memSize);
cudaMalloc(&dc, memSize); cudaMemset(dc, 0, memSize);
cudaMemcpy(da, a, memSize, cudaMemcpyHostToDevice);
cudaMemcpy(db, b, memSize, cudaMemcpyHostToDevice);
__global__ void vecAdd(int* _a, int* _b, int* _c)
{
int tID = threadIdx.x;
_c[tID] = _a[tID] + _b[tID];
}
// 호스트 코드에서 커널 호출
vecAdd <<<1, NUM_DATA>>> (da, db, dc);
cudaMemcpy(c, dc, memSize, cudaMemcpyDeviceToHost);
cudaFree(da);
cudaFree(db);
cudaFree(dc);
%%cu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// The size of the vector
#define NUM_DATA 1024
// Simple vector sum kernel
__global__ void vecAdd(int* _a, int* _b, int* _c)
{
int tID = threadIdx.x;
_c[tID] = _a[tID] + _b[tID];
}
int main(void)
{
int* a, * b, * c, * hc;
int *da, *db, *dc;
int memSize = sizeof(int) * NUM_DATA;
printf("%d elements, memSize = %d bytes\n", NUM_DATA, memSize);
// memory allocation on the host_side
a = new int[NUM_DATA]; memset(a, 0, memSize);
b = new int[NUM_DATA]; memset(b, 0, memSize);
c = new int[NUM_DATA]; memset(c, 0, memSize);
hc = new int[NUM_DATA]; memset(hc, 0, memSize);
// Data generation
for (int i = 0 ; i < NUM_DATA; i++)
{
a[i] = rand() % 10;
b[i] = rand() % 10;
}
//Vector sum on the host
for (int i = 0 ; i < NUM_DATA ; i++)
hc[i] = a[i] + b[i];
// Memory allocation on te device-side
cudaMalloc(&da, memSize); cudaMemset(da, 0, memSize);
cudaMalloc(&db, memSize); cudaMemset(db, 0, memSize);
cudaMalloc(&dc, memSize); cudaMemset(dc, 0, memSize);
// Data copy : Host -> Device
cudaMemcpy(da, a, memSize, cudaMemcpyHostToDevice);
cudaMemcpy(db, b, memSize, cudaMemcpyHostToDevice);
// Kernel call
vecAdd <<<1, NUM_DATA>>> (da, db, dc);
// Copy results : Device -> Host
cudaMemcpy(c, dc, memSize, cudaMemcpyDeviceToHost);
// Release device Memory
cudaFree(da);
cudaFree(db);
cudaFree(dc);
// Check results
bool result = true;
for (int i = 0 ; i < NUM_DATA ; i++)
{
if (hc[i] != c[i])
{
printf("[%d] The result is not matched! (%d, %d)\n", i, hc[i], c[i]);
result = false;
}
}
if (result)
printf("GPU works well!\n");
// Release host memory
delete[] a;
delete[] b;
delete[] c;
return 0;
}
1024 elements, memSize = 4096 bytes
GPU works well!
벡터의 크기를 늘려보면, 검증 과정에서 결과가 일치하지 않는다는 메시지가 출력된다. 커널이 연산을 제대로 수행하지 못하는 것이다.
: 4장의 스레드 계층 구조 및 실행모델, 5장의 스레드 인덱싱을 학습하면서 이를 해결해 보자.
병렬 처리 알고리즘의 설계 및 구현은 논리적 버그가 발생할 가능성이 높으며, 디버그 난이도가 훨씬 높기 때문에, 호스트 코드와 같이 검증된 코드의 연산 결과와 비교해보기를 추천한다.
GPU 연산은 커널 호출을 통해 진행.
: 성능 측정의 주요 지점 중 하나는 커널 부분이다.
커널 호출 시 디바이스에게 명령을 전달한 후 프로그램 흐름의 제어권을 바로 호스트에게 반환한다.
: 즉, 호스트는 디바이스에게 커널 수행을 요청하고 바로 다음 작업으로 진행한다.
: 이는 호스트와 디바이스의 Asynchronous한 수행이 가능하게 하는 특징으로, 두 장치를 동시에 사용 가능하다는 의미.
정확한 시간 측정을 위해서는, 커널 연산이 종료될 때 까지 대기한 후 시간 측정을 종료해야한다.
: 디바이스가 수행 죽인 작업이 끝날 때까지 대기하는 CUDA Synchronous 함수는 cudaDeviceSynchronize()
CUDA API는 기본적으로 순차적으로 진행된다.
: 호스트 코드를 디바이스 코드 제어에만 사용하는 경우에는 cudaDeviceSynchronize()와 같은 동기화 함수를 사용하지 않아도 된다.
CUDA 알고리즘의 성능을 판단할 때는 반드시 데이터 전송시간도 함께 고려해야 한다. (실제로 컴퓨터 구조에서 메모리 접근 시간은 상당한 성능 저하를 일으킨다. 이를 보완하기 위해 Cache를 사용하며, Cache 실패시 메모리 접근에 의한 성능 저하를 줄이기 위한 기법들도 있다.)
: cudaMemcpy() 함수는 호스트 코드와 동기적으로 수행된다.
이 외에도 CUDA를 통해 GPU를 사용하기 위해 데이터 변환 등과 같은 추가 작업이 있다면 해당 작업에 소요되는 시간도 포함해 성능을 측정해야 한다.