[CUDA RT Series 3] Adding Vectors

목차: Series 1 – Index + Methodology (Link)
이전 글: Series 2 –First Image (Link)
다음 글: Series 4 –Classing Up the GPU (Link)

이번 장에서는 “__global__”, “__host__”, “__device__” 사용에 관해서 설명한다. 앞에서 작성한 코드 중 GPU에서 실행될 Function에 “__global__”을 함수 이름 앞에 작성하였다. “__global__”의 경우 GPU에서 실행할 함수를 의미한다. 보통 CPU에서 처음 호출할 함수 앞에 작성한다. “__global__”로 작성된 함수를 호출하여서 연산이 수행하고 완료되는 구간을 Kernel이라고 한다. 여러 개의 “__global__” 함수를 호출하는 경우 여러 개의 Kernel 연산을 수행한 것이다. “__device__”의 경우 “__global__”과 거의 동일하게 GPU에서 연산을 수행할 함수 앞에 작성하게 된다. 하지만, “__global__” 과 달리 보통 GPU에서 호출할 함수 앞에 붙이게 된다. CPU에서 “__device__”로 작성된 함수를 호출하면 실행되지 않는다. “__host__”의 경우 CPU에서 동작할 함수 앞에 붙이게 된다. GPU에서 연산할 수 없으며 오직 CPU에서만 실행할 수 있다.

출처 2에서 Vec3 Class를 만들었다. 해당 Class는 그래픽 관련 연산을 효율적으로 하기 위해 필요한 연산을 포함한다. 이 부분을 CPU/GPU에서 모두 실행할 수 있도록 변경한다. 아래 코드 1은 Vec3 Class를 CPU/GPU에서 모두 사용할 수 있도록 변경한 코드이다.

코드 1: GPU에서 실행 가능하도록 Vec3 함수 변경 코드

#ifndef MKVEC3_H
#define MKVEC3_H
 
#include <math.h>
#include <stdlib.h>
#include <iostream>

//MK: (코드 1-1) __host__, __device__를 추가해서 CPU/GPU 모두 호출가능하도록 수정함
//MK: 그 이외 모든 부분을 기존 CPU코드와 동일함
class vec3{
    public:
      __host__ __device__ vec3(){}
      __host__ __device__ vec3(float e0, float e1, float e2){
        element[0] = e0;
        element[1] = e1;
        element[2] = e2;
      }
 
      __host__ __device__ inline float x() const{ return element[0];}
      __host__ __device__ inline float y() const{ return element[1];}
      __host__ __device__ inline float z() const{ return element[2];}
 
      __host__ __device__ inline float r() const{ return element[0];}
      __host__ __device__ inline float g() const{ return element[1];}
      __host__ __device__ inline float b() const{ return element[2];}
 
      __host__ __device__ inline const vec3& operator+() const{ return *this;}
      __host__ __device__ inline vec3 operator-() const {return vec3(-element[0], -element[1], -element[2]);}
      __host__ __device__ inline float operator[] (int i) const {return element[i];}
      __host__ __device__ inline float &operator[] (int i) {return element[i];}
 
      __host__ __device__ inline vec3& operator+=(const vec3 &v){
          element[0] += v.element[0];
          element[1] += v.element[1];
          element[2] += v.element[2];
          return *this;
      }

      __host__ __device__ inline vec3& operator-=(const vec3 &v){
          element[0] -= v.element[0];
          element[1] -= v.element[1];
          element[2] -= v.element[2];
          return *this;
      }

      __host__ __device__ inline vec3& operator*=(const vec3 &v){
          element[0] *= v.element[0];
          element[1] *= v.element[1];
          element[2] *= v.element[2];
          return *this;
      }

      __host__ __device__ inline vec3& operator/=(const vec3 &v){
          element[0] /= v.element[0];
          element[1] /= v.element[1];
          element[2] /= v.element[2];
          return *this;
      }

      __host__ __device__ inline vec3& operator*=(const float t){
          element[0] *= t;
          element[1] *= t;
          element[2] *= t;
          return *this;
      }

      __host__ __device__ inline vec3& operator/=(const float t){
          float k = 1.0/t;
          element[0] *= k;
          element[1] *= k;
          element[2] *= k;
          return *this;
      }
 
      __host__ __device__ inline float length() const{
          return sqrt(element[0] * element[0] + element[1] * element[1] + element[2] * element[2]);
      }

      __host__ __device__ inline float squared_length() const{
          return (element[0] * element[0] + element[1] * element[1] + element[2] * element[2]);
      }

	  __host__ __device__ inline void make_unit_vector(){
          float k = 1.0 / (sqrt(element[0] * element[0] + element[1] * element[1] + element[2] * element[2]));
          element[0] *= k;
          element[1] *= k;
          element[2] *= k;
      };
 
      float element[3];
};
 
inline std::istream& operator>>(std::istream &is, vec3 &t){
    is >> t.element[0] >> t.element[1] >> t.element[2];
    return is;
}
 
inline std::ostream& operator<<(std::ostream &os, const vec3 &t){
    os << t.element[0] << t.element[1] << t.element[2];
    return os;
}
 
__host__ __device__ inline vec3 operator+(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] + v2.element[0], v1.element[1] + v2.element[1], v1.element[2] + v2.element[2]);
}
 
__host__ __device__ inline vec3 operator-(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] - v2.element[0], v1.element[1] - v2.element[1], v1.element[2] - v2.element[2]);
}
 
__host__ __device__ inline vec3 operator*(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] * v2.element[0], v1.element[1] * v2.element[1], v1.element[2] * v2.element[2]);
}
 
__host__ __device__ inline vec3 operator/(const vec3 &v1, const vec3 &v2){
    return vec3(v1.element[0] / v2.element[0], v1.element[1] / v2.element[1], v1.element[2] / v2.element[2]);
}

__host__ __device__ inline vec3 operator*(const float t, const vec3 &v){
    return vec3(t * v.element[0], t * v.element[1], t * v.element[2]);
}
 
__host__ __device__ inline vec3 operator/(const vec3 &v, const float t){
    return vec3(v.element[0]/t, v.element[1]/t, v.element[2]/t);
}
 
__host__ __device__ inline vec3 operator*(const vec3 &v, const float t){
    return vec3(v.element[0] * t, v.element[1] * t, v.element[2] * t);
}
 
__host__ __device__ inline float dot(const vec3 &v1, const vec3 &v2){
    return (v1.element[0] * v2.element[0] + v1.element[1] * v2.element[1] + v1.element[2] * v2.element[2]);
}
 
__host__ __device__ inline vec3 cross(const vec3 &v1, const vec3 &v2){
    return vec3(
                (v1.element[1] * v2.element[2] - v1.element[2] * v2.element[1]),
                -(v1.element[0] * v2.element[2] - v1.element[2] * v2.element[0]),
                (v1.element[0] * v2.element[1] - v1.element[1] * v2.element[0])
            );
}
 
__host__ __device__ inline vec3 unitVector(vec3 v){
    return (v/v.length());
}
 
#endif

위 코드가 기존 CPU 코드와 다른 부분은 “__host__”와 “__device__”라는 문구가 모든 함수 이름 앞에 있는 것이다. “__host__”와 “__device__”를 모두 사용했기 때문에 모든 함수는 CPU와 GPU에서 실행 가능하다는 의미이다. 하지만, “__global__”이 없는 관계로 GPU에서 시작하는 함수는 아니다.

코드 2: Vec3 함수를 사용하기 위한 Main 코드 

#include <iostream>
#include <fstream>
#include <time.h>
#include "mkVec3.h"

using namespace std;

//MK: FB 사이즈
int nx = 1200;
int ny = 600;

// limited version of checkCudaErrors from helper_cuda.h in CUDA examples
//MK: #val은 val 전체를 String으로 Return 함
#define checkCudaErrors(val) check_cuda( (val), #val, __FILE__, __LINE__ )


//MK: Error 위치를 파악하기 위해서 사용
void check_cuda(cudaError_t result, char const *const func, const char *const file, int const line) {
    if (result) {
    	cerr << "MK: CUDA ERROR = " << static_cast<unsigned int>(result) << " at " << file << ":" << line << " '" << func << "' \n";
    	// Make sure we call CUDA Device Reset before exiting
       	cudaDeviceReset();
       	exit(99);
    }
}

//MK: (코드 2-1) Kernel 코드. GPU에서 수행할 함수
//MK: Vec3를 사용할 수 있도록 코드를 변경함
__global__ void mkRender(vec3 *fb, int max_x, int max_y) {
	//MK: Pixel 위치 계산을 위해 ThreadId, BlockId를 사용함
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    int j = threadIdx.y + blockIdx.y * blockDim.y;

	//MK: 계산된 Pixel 위치가 FB사이즈 보다 크면 연산을 수행하지 않음
	if((i >= max_x) || (j >= max_y)){
	   return;
	}

	//MK: FB Pixel 값 계산
    int pixel_index = j*max_x + i ;
	fb[pixel_index] = vec3(float(i)/max_x, float(j)/max_y, 0.2f);
}

int main() {
	//MK: Thread Block 사이즈
    int tx = 8;
    int ty = 8;

    cout << "MK: Rendering a " << nx << "x" << ny << " Image ";
    cout << "MK: in " << tx << "x" << ty << " Thread Blocks.\n";

    int num_pixels = nx*ny;
    size_t fb_size = num_pixels*sizeof(vec3);

    //MK: FB 메모리 할당 (cudaMallocManaged 는 Unitifed Memory를 사용 할 수 있도록 함)
	//MK: 필요에 따라 CPU/GPU에서 GPU/CPU로 데이터를 복사함
    //MK: (코드 2-2) Vec3를 사용할 수 있도록 코드를 변경함
    vec3 *fb;
    checkCudaErrors(cudaMallocManaged((void **)&fb, fb_size));

    clock_t start, stop;
    start = clock();
    //MK: GPU (CUDA) 연산을 위해서 Thread Block, Grid 사이즈 결정
    dim3 blocks(nx/tx+1,ny/ty+1);
    dim3 threads(tx,ty);
	//MK: CUDA 함수 호출
    mkRender<<<blocks, threads>>>(fb, nx, ny);
    checkCudaErrors(cudaGetLastError());
	//MK: CUDA 연산이 완료되길 기다림
    checkCudaErrors(cudaDeviceSynchronize());
	//MK: 연산 시간과 끝 부분을 계산하여서 연산 시간을 측정함 

    //MK: CPU 코드와 동일하게 결과를 파일에 작성
    //MK: (코드 2-3) Vec3를 사용할 수 있도록 코드를 변경함
	string fileName = "Ch3_gpu.ppm";
	ofstream writeFile(fileName.data());
	if(writeFile.is_open()){
		writeFile.flush();
		writeFile << "P3\n" << nx << " " << ny << "\n255\n";
    		for (int j = ny-1; j >= 0; j--) {
        		for (int i = 0; i < nx; i++) {
            			size_t pixel_index = j*nx + i;
            			int ir = int(255.99*fb[pixel_index].r());
            			int ig = int(255.99*fb[pixel_index].g());
            			int ib = int(255.99*fb[pixel_index].b());
            			writeFile  << ir << " " << ig << " " << ib << "\n";
        		}
    		}
		writeFile.close();
	}

	//MK: 메모리 Free
    checkCudaErrors(cudaFree(fb));

	//MK: 연산 시간과 끝 부분을 계산하여서 연산 시간을 측정함 
	stop = clock();
    double timer_seconds = ((double)(stop - start)) / CLOCKS_PER_SEC;
    cout << "MK: GPU (CUDA) Took " << timer_seconds << " Seconds.\n";
    
	return 0;
}

코드 2는 새로 작성한 Vec3 Class를 사용하기 위한 Main 코드이다. 기존 Float로 생성한 FrameBuffer (FB) 메모리를 Vec3으로 변경하였다. 추가로 “mkRender”함수 안에 Vec3 함수를 사용하는 것을 확인 할 수 있다.

그림 1: 결과 이미지

NVCC 컴파일러를 사용해서 컴파일 후 실행하면 위 그림 1을 확인 할 수 있다. 이전 글에서 작성한 코드와 동일한 결과이다. 혹시 코드를 원하시는 분이 있을 수도 있어서 아래 출처 3에 CPU/GPU 코드를 공유하였다.

 

성능 측정 결과

그림 2: CPU/GPU 실행 시간 결과

그림 2는 CPU/GPU 성능 측정 결과이다. 이전 글과 동일하게 대부분의 연산이 결과를 파일에 작성하는 부분이기 때문에 CPU/GPU성능에 큰 차이가 없다.

출처

  1. https://devblogs.nvidia.com/accelerated-ray-tracing-cuda/
  2. http://www.realtimerendering.com/raytracing/Ray%20Tracing%20in%20a%20Weekend.pdf
  3. https://github.com/mkblog-cokr/CudaRTSeries

Leave a Comment