목표는 약 1억개 bytes 의 점을 Matrix 한 개로 변환할 예정.
/* CUDA_Util.h */
#ifndef __CUDA_UTIL_H__
#define __CUDA_UTIL_H__
typedef unsigned long DWORD;
typedef unsigned long long UINT64;
typedef wchar_t WCHAR;
const DWORD VERTEX_COUNT = 1024 * 1024 * 100;
struct MATRIX4
{
union
{
struct
{
float _11;
float _12;
float _13;
float _14;
float _21;
float _22;
float _23;
float _24;
float _31;
float _32;
float _33;
float _34;
float _41;
float _42;
float _43;
float _44;
};
struct
{
float f[4][4];
};
};
};
MATRIX4* pMatHost = nullptr;
MATRIX4* pMatDev = nullptr;
cudaMallocHost(&pMatHost, sizeof(MATRIX4));
void FillMatrix(MATRIX4* pOutMat)
{
SetIdentityMatrix(pOutMat);
for (DWORD y = 0; y < 4; y++){
for(DWORD x = 0; x < 4; x++){
pOutMat->f[y][x] = (float)((rand() % 10) + 1)/10.0f;
}
}
}
FillMatrix(pMatHost);
cudaMalloc(&pMatDev, sizeof(MATRIX4));
// CPU -> GPU : Matrix 를 copy 해준다.
cudaMemcpy(pMatDev, pMatHost, sizeof(MATRIX4), cudaMemcpyHostToDevice);
NAME
memcpy - copy memory area
SYNOPSIS
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
1st param 이 destination. 2nd param 이 source.
// cuda_runtime_api.h
/**
...
* \param dst - Destination memory address
* \param src - Source memory address
* \param count - Size in bytes to copy
* \param kind - Type of transfer
...
**/
extern __host__ cudaError_t CUDARTAPI cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind);
// driver_types.h
/**
* CUDA memory copy types
*/
enum __device_builtin__ cudaMemcpyKind
{
cudaMemcpyHostToHost = 0, /**< Host -> Host */
cudaMemcpyHostToDevice = 1, /**< Host -> Device */
cudaMemcpyDeviceToHost = 2, /**< Device -> Host */
cudaMemcpyDeviceToDevice = 3, /**< Device -> Device */
cudaMemcpyDefault = 4 /**< Direction of the transfer is inferred from the pointer values. Requires unified virtual addressing */
};
1억개의 점을 모두 random 으로 채우면 시간이 오래걸리므로, Sampling 할 점을 1024 개 만든 다음에 그 1024 개를 돌려가면서 사용.
const DWORD SAMPLE_VERTEX_COUNT = 1024;
float4* pSampleVertexList = new float4[SAMPLE_VERTEX_COUNT];
for (DWORD i = 0; i < SAMPLE_VERTEX_COUNT; i++)
{
FillVector4(pSampleVertexList + i);
}
void FillVector4(float4* pf4Out)
{
float* pf = &pf4Out->x;
for (DWORD i = 0; i < 4; i++) {
pf[i] = (float(rand() % 10) + 1) / 10.0f;
}
}
Random 하게 숫자를 채워가능 함수. 이와 같이 1024 개의 점을 채워 넣은 다음 데이터를 변환.
float4* pSrcVertexListHost = nullptr;
float4* pDestVertexListHost = nullptr;
float4* pSrcVertexListDev = nullptr;
float4* pDestVertexListDev = nullptr;
// vector_types.h
struct __device_builtin__ __builtin_align__(16) float4
{
float x, y, z, w;
};
size : 16byte VERTEX_COUNT
cudaMallocHost(&pSrcVertexListHost, sizeof(float4) * VERTEX_COUNT);
cudaMallocHost(&pDestVertexListHost, sizeof(float4) * VERTEX_COUNT);
cudaMalloc(&pSrcVertexListDev, sizeof(float4) * VERTEX_COUNT);
cudaMalloc(&pDestVertexListDev, sizeof(float4) * VERTEX_COUNT);
for (DWORD i = 0; i < VERTEX_COUNT; i++){
pSrcVertexListHost[i] = pSampleVertexList[i % SAMPLE_VERTEX_COUNT];
}
// CPU -> GPU : 입력으로 들어갈 Vertex 배열, source 배열을 copy 해준다.
cudaMemcpy(pSrcVertexListDev, pSrcVertexListHost, sizeof(float4) * VERTEX_COUNT, cudaMemcpyHostToDevice);
sizeof(MATRIX4)
sizeof(float4) * VERTEX_COUNT
이제 Data 가 준비 되었다.
지금부터 나오는 code 는 kernel.cu 파일 안에 있는 코드이다.
LaunchKernelConstMemory(pDestVertexListDev, pSrcVertexListDev, pMatDev, VERTEX_COUNT);
현재 이 함수는
각각의 Thread 는 src vertex 하나를 읽어서 matrix 에 곱한 다음, dest 에 써놓은 것.
const DWORD MAX_VERTEX_NUM_PER_ONCE = 65536 * 10;
while 로 loop 을 돌면서, 제한 갯수인 65만개씩 잘라서 쓰고 있다.
while (dwVertexNum)
{
DWORD dwVertexNumPerOnce = dwVertexNum;
if ( dwVertexNumPerOnce > MAX_VERTEX_NUM_PER_ONCE ) {
dwVertexNumPerOnce = MAX_VERTEX_NUM_PER_ONCE;
}
block 당 thread 개수, 여기서는 max 가 1024 개 이다.
const DWORD THREAD_NUM_PER_BLOCK = 1024; // 32 - 1024 threads.
Memory type 이 Global, Constant, Texture memory 으로 구성되어 있다. (물론 더 있지만..)
이들 memory 가 물리적으로 다르지는 않지만
constamt memory 에 넣기 위해서 API 를 호출한다. (VerifyCudaError 는 나중에..)
VerifyCudaError(cudaMemcpyToSymbolAsync(g_mat, pMatDev, sizeof(MATRIX4), 0, cudaMemcpyDeviceToDevice));
constant memory 에 넣을 데이터는 constant 로 선언을 한다.
// kernel.cu
__constant__ MATRIX4 g_mat;
while (dwVertexNum)
{
DWORD dwVertexNumPerOnce = dwVertexNum;
if ( dwVertexNumPerOnce > MAX_VERTEX_NUM_PER_ONCE ) {
dwVertexNumPerOnce = MAX_VERTEX_NUM_PER_ONCE;
}
dim3 threadPerBlock(1, 1);
dim3 blockPerGrid(1, 1, 1);
reference
// vector_types.h
struct __device_builtin__ dim3
{
unsigned int x, y, z;
#if defined(__cplusplus)
#if __cplusplus >= 201103L
__host__ __device__ constexpr dim3(unsigned int vx = 1, unsigned int vy = 1, unsigned int vz = 1) : x(vx), y(vy), z(vz) {}
__host__ __device__ constexpr dim3(uint3 v) : x(v.x), y(v.y), z(v.z) {}
__host__ __device__ constexpr operator uint3(void) const { return uint3{x, y, z}; }
#else
__host__ __device__ dim3(unsigned int vx = 1, unsigned int vy = 1, unsigned int vz = 1) : x(vx), y(vy), z(vz) {}
__host__ __device__ dim3(uint3 v) : x(v.x), y(v.y), z(v.z) {}
__host__ __device__ operator uint3(void) const { uint3 t; t.x = x; t.y = y; t.z = z; return t; }
#endif
#endif /* __cplusplus */
};
물론, 현재 RTX 40 series 는 block 수가.. 엄청 많겠지... 만..
thread 가 1024 개 쓸것이라 정하고, 약 65만개를 처리해야 하므로, 640 개의 block 이 정해진다. 정확히는, 이라는 계산이 나온다.
threadPerBlock.x = THREAD_NUM_PER_BLOCK;
threadPerBlock.y = 1;
blockPerGrid.x = (dwVertexNumPerOnce / THREAD_NUM_PER_BLOCK) + ((dwVertexNumPerOnce % THREAD_NUM_PER_BLOCK) != 0);
blockPerGrid.y = 1;
인데, 실제 개수는 이다. 여기서는 총 개수가 중요하다. 프로그램 짜기 편하라고 3차원으로 되어 있다.
Kernel 함수 호출은 비동기적으로 호출 되어서, 엄밀히 따지면 호출하고 나서 끝난 것이 아니다. 실행 조차 안되어 있을 수 있다. Kernel 함수 호출이 끝난 것을 어떻게 알 수 있을까?
KernelTransformVector4ConstMemory <<< blockPerGrid, threadPerBlock, 0 >>> (pf4DestDev, pf4SrcDev, dwVertexNumPerOnce);
VerifyCudaError(cudaDeviceSynchronize());
reference
// driver_types.h
/**
* CUDA error types
*/
enum __device_builtin__ cudaError
{
/**
* The API call returned with no errors. In the case of query calls, this
* also means that the operation being queried is complete (see
* ::cudaEventQuery() and ::cudaStreamQuery()).
*/
cudaSuccess = 0,
/**
* This indicates that one or more of the parameters passed to the API call
* is not within an acceptable range of values.
*/
cudaErrorInvalidValue = 1,
/**
* The API call failed because it was unable to allocate enough memory or
* other resources to perform the requested operation.
*/
cudaErrorMemoryAllocation = 2,
// (.. 중략 ..)
이 enum error 선언을 handling 하는 함수를 구현.
void VerifyCudaError(cudaError _err)
{
std::string wchErr = "Unknown";
switch(_err) {
case cudaErrorMemoryAllocation:
wchErr = "cudaErrorMemoryAllocation";
break;
case cudaErrorLaunchFailure:
wchErr = "cudaErrorLaunchFailure";
break;
case cudaErrorLaunchTimeout:
wchErr = "cudaErrorLaunchTimeout";
break;
case cudaErrorMisalignedAddress:
wchErr = "cudaErrorMisalignedAddress";
break;
case cudaErrorInvalidValue:
wchErr = "cudaErrorInvalidValue";
break;
case cudaSuccess:
wchErr = "cudaSuccess";
break;
}
if (cudaSuccess != _err) {
printf("cuda error = %s(%d)\n", wchErr.c_str(), _err);
}
}
문제없이 돌아가면 멈추지 않고 연산을 하면서,
pDestDev += dwVertexNumPerOnce; // 써 놓을 vertex 위치를 뒤로 옮긴다.
pSrcDev += dwVertexNumPerOnce; // 읽을 vertex 위치를 뒤로 옮긴다.
dwVertexNum -= dwVertexNumPerOnce; // 처리 할 vertex 갯수도 줄여나간다.
1억개의 vertex 갯수 완료 될 때 까지 loop 이 돈다.
다시 돌아와서 문제 없을 시 Kernel 관련 코드를 보자.
KernelTransformVector4ConstMemory <<< blockPerGrid, threadPerBlock, 0 >>> (pf4DestDev, pf4SrcDev, dwVertexNumPerOnce);
"<<<" 3 개를 붙여서 KernelTransformVector4ConstMemory 함수에 parameter 를 집어 넣는다.
__global__ void KernelTransformVector4ConstMemory(float4* pf4Dest, float4* pf4Src, DWORD dwVertexNum)
{
DWORD ThreadIndex = blockIdx.x * blockDim.x + threadIdx.x;
if (ThreadIndex >= dwVertexNum){
return;
}
cuTransformVector4(pf4Dest + ThreadIndex, pf4Src + ThreadIndex, &gMat);
}
blockIdx, blockDim, threadIdx 등은
// device_launch_parameters.h
uint3 __device_builtin__ __STORAGE__ threadIdx;
uint3 __device_builtin__ __STORAGE__ blockIdx;
dim3 __device_builtin__ __STORAGE__ blockDim;
에 정의 되어 있으며,
수치는 위에서 정의하였다.
DWORD ThreadIndex = blockIdx.x * blockDim.x + threadIdx.x;
cuTransformVector4(pDest + ThreadIndex, pSrc + ThreadIndex, &g_mat);
__host__ __device__ __inline__ void cuTransformVector4(float4* pf4Dest, float4* pf4Src, MATRIX4* pMat)
{
float4 r;
r.x = pf4Src->x * pMat->_11 + pf4Src->y * pMat->_21 + pf4Src->z * pMat->_31 + pf4Src->w * pMat->_41;
r.y = pf4Src->x * pMat->_12 + pf4Src->y * pMat->_22 + pf4Src->z * pMat->_32 + pf4Src->w * pMat->_42;
r.z = pf4Src->x * pMat->_13 + pf4Src->y * pMat->_23 + pf4Src->z * pMat->_33 + pf4Src->w * pMat->_43;
r.w = pf4Src->x * pMat->_14 + pf4Src->y * pMat->_24 + pf4Src->z * pMat->_34 + pf4Src->w * pMat->_44;
*pf4Dest = r;
}
LaunchKernelConstMemory(pDestVertexListDev, pSrcVertexListDev, pMatDev, VERTEX_COUNT);
cudaMemcpy(pDestVertexListHost, pDestVertexListDev, sizeof(float4) * VERTEX_COUNT, cudaMemcpyDeviceToHost);
자료 구조 (예를 들어 Tree 등) 를 GPU 상으로 구현할 때가 있다.
통합 Address system (아직 안해봐서 잘 모른다.. 해볼 예정.)