NVIDIA 사의 공식 튜토리얼 Fundamentals of Accelerated Computing C/C++을 학습하며 정리한 내용.
Accelerating Applications with CUDA C/C++
GPU-accelerated Vs. CPU-only Applications
Writing Application Code for the GPU
1 | |
__global__ void GPUFunction()__global__이라는 키워드가 GPU에서 돌아간다는 사실을 명시해준다.
-
GPUFunction<<1,1>>();- GPU에서 작동하는 이러한 함수를 kernel이라 부르며, thread hierarchy를 명시해준다고 한다.
- 인자 중 앞의
1은 실행될 쓰레드 그룹의 개수를 명시하며,block이라고 부른다. - 인자 중 뒤의
1은 각block내에 몇 개의 쓰레드가 실행될 것인지를 명시한다.
좀 더 자세한 내용은 뒤에서..
-
cudaDeviceSynchronize();- 이후 계산된 값을 CPU와 synchronize하여 작동하게 하기 위해서는, 이 함수를 사용하여야 한다.
Compiling and Running Accelerated CUDA Code
.c 파일을 gcc로 컴파일하는 것처럼, .cu 파일은 nvcc라는 NVIDIA CUDA Compiler로 컴파일한다. 다음과 같이 쓸 수 있다.
nvcc -arch=sm_70 -o out some-CUDA.cu -run
옵션은 다음과 같다.
-arch: 컴파일되는 환경의 GPU 아키텍쳐를 명시해준다.sm_70의 경우 Volta 아키텍쳐를 명시해준다.-o: 아웃풋 파일의 이름을 명시해준다.-run: 편의를 위한 옵션. 이 옵션을 쓰면 컴파일한 바이너리 파일을 실행해준다.
CUDA Thread Hierarchy
CUDA Thread Hierarchy에서의 용어 좀 더 자세히 설명
kernel: GPU function을 부르는 용어.kernel은execution configuration에 따라 실행된다.thread: GPU 작업의 기본 단위. 여러thread가 병렬적으로 작동한다.block:thread의 모임을block이라 한다.grid: 주어진kernel의execution configuration에서block들의 모임, 그러니까 전체를grid라 부른다.
CUDA-Provided Thread Hierarchy Variables
CUDA Thread Hierarchy에서는 미리 정해진 변수를 통해 각 block과 thread에 접근할 수 있다.
gridDim.x:grid내에 있는block의 개수. performWork«<2,4»>()와 같은kernel의 경우 2가 된다.blockIdx.x:grid내에 있는block들 중 해당block의 위치 인덱스. performWork«<2,4»>()와 같은kernel을 실행한다면0,1이 될 수 있다.blockDim.x:block내에 있는thread의 개수. performWork«<2,4»>()와 같은kernel의 경우4가 된다. 한grid내에 있는 모든block은 같은 수의thread를 가진다.threadIdx.x:block내에 있는thread중 해당thread의 위치 인덱스. performWork«<2,4»>()와 같은kernel의 경우0,1,2,3중 하나가 된다.
Accelerating For Loops
반복문을 가속화하는 법.
Exercise: Accelerating a For Loop with a Single Block of Threads
1 | |
이런 반복문을 어떻게 GPU로 가속할 수 있을까? 병렬화를 하기 위해서는 2가지 단계를 꼭 거쳐야 한다:
kernel은 해당 반복문에서 딱 한 번의 반복 작업만 하도록 설계되어야 한다.kernel이 다른kernel에 대해서 알지 못하기 때문에,execution configuration이 해당 반복문에서 반복되는 작업의 수에 맞춰 선언되어야 한다.
우리가 위에서 배운 Thread Hierarchy Variable을 활용하면 이를 달성할 수 있다.
Solution)
1 | |
Coordinating Parallel Threads
각 block에 존재할 수 있는 thread의 개수는 최대 1024개로 한계가 있다. 따라서 병렬처리의 효과를 더 크게 누리기 위해서는 여러 block들 간의 coordinate를 잘 해야 한다.
GPU thread에 data를 할당하기 위해, 각 thread의 인덱스를 활용한 데이터 분배 접근 전략을 활용한다.
각 block의 사이즈는 blockDim.x로 알 수 있고, 인덱스는 blockIdx.x로 접근할 수 있다. 또 각 thread의 인덱스는 threadIdx.x로 접근할 수 있다.
따라서 threadIdx.x + blockIdx.x * blockDim.x라는 공식을 활용하여 데이터를 thread에 매핑할 수 있다.
Exercise: Accelerating a For Loop with Multiple Blocks of Threads
아까 위에서 병렬화 한 반복문을, 이번에는 최소 2개 이상의 block을 활용하여 병렬화시켜보자.
Allocating Memory to be accessed on the GPU and the CPU
CPU-only application에서는 C가 malloc과 free를 사용해 메모리를 할당하고 해제하지만, GPU 가속을 할 때는 대신 cudaMallocManaged와 cudaFree를 사용한다. 사용 예시는 다음과 같으니 비교해보자.
1 | |
Exercise: Array Manipulation on both the Host and Device
1 | |
위 코드를 배열 포인터 a가 CPU와 GPU 코드에서 모두 쓰일 수 있게, 또 a를 정확히 메모리 해제해야 한다는 점에 유의해서 고쳐보자.
Solution)
1 | |
Grid Size Work Amount Mismatch
우리가 사용하려는 데이터가 grid 사이즈에 딱 맞으면 상관 없지만, 만약 그것보다 부족한 경우 사이즈가 맞지 않는다는 문제가 발생한다. e.g.) grid 내에 thread 개수가 8개인데 사용할 데이터는 5개밖에 없으면 threadIdx.x + blockIdx.x * blockDim.x 공식으로 할당할 때 5,6,7번은 문제가 생긴다.
1 | |
그래서 위와 같이 some_kernel 함수의 if문처럼 인덱스가 데이터의 크기보다 작을 때만 특정 기능을 실행하도록 설정해주면 된다.
Exercise: Accelerating a For Loop with a Mismatched Execution Configuration
1 | |
이 코드는 1000개의 integer를 cudaMallocManaged를 활용해 메모리 할당하고 있고, thread_per_blocks 라는 이름의 변수로 block 당 최대 thread 개수를 정의하고 있다. 이에 따라 number_of_blocks 변수에 필요한 block의 개수를 구해 할당해주고, initializeElementsTo 함수에다 데이터 수보다 인덱스가 넘치는 경우 예외를 처리해주는 내용의 코드를 추가해주자.
Solution)
1 | |
Grid-Stride Loops
이번에는 데이터의 개수가 grid 내에 존재하는 thread의 개수보다 많은 경우이다. 이 경우 남은 데이터들을 처리해줄 수 없기 때문에 grid-stride loops라는 방법을 사용한다.
이 방법은 kernel 내에서 반복문을 사용하는 것인데, 매 반복마다 grid 내의 thread의 개수인 gridDim.x * blockDim.x만큼 인덱스에 더해주면서 모든 데이터를 처리하는 방법이다.
Exercise: Use a Grid-Stride Loop to Manipulate an Array Larger than the Grid
1 | |
grid 내의 thread 개수는 256*32 = 8192개지만 전체 데이터는 10000개로 이를 초과한다. doubleElements 커널을 grid-stride loops 방식으로 수정하여 모든 데이터를 연산할 수 있게 해보자.
Solution)
1 | |
Error Handling
어느 어플리케이션에서나 Error Handling은 중요하다. 대부분의 CUDA 함수는 cudaError_t라는 자료형으로 결과값을 반환하는데, 이걸 확인하면 에러가 발생했는지 아닌지를 확인할 수 있다. cudaMallocManaged 함수의 경우 다음과 같다.
1 | |
그런데 kernel 함수는 void 함수로 짜서 cudaError_t를 반환하지 않는데, 이 경우 CUDA는 cudaGetLastError 함수를 제공한다.
1 | |
만약 asynchronous하게 발생하는 에러를 잡고 싶다면(예를 들면 asynchronous하게 작동하는 kernel) synchronize하게 하는 CUDA runtime API call (예를 들면 cudaDeviceSynchronize)을 잘 체크해서 잡아내야 한다.
Exercise: Add Error Handling
1 | |
Solution)
1 | |
CUDA Error Handling Function
다음은 에러를 확인하기 편하게 해주는 매크로 함수다. 다른 exercise를 풀 때 편하게 사용하면 된다.
1 | |
요약
Accelerating Applications with CUDA C/C++에서 배운 내용 요약.
- Write, compile, and run C/C++ programs that both call CPU functions and launch GPU kernels.
- Control parallel thread hierarchy using execution configuration.
- Refactor serial loops to execute their iterations in parallel on a GPU.
- Allocate and free memory available to both CPUs and GPUs.
- Handle errors generated by CUDA code.
Final Exercise: Accelerate Vector Addition Application
1 | |
위 코드는 CPU로만 작동하는 벡터 더하기 어플리케이션이다. 여기 있는 addVectorsInto 함수를 CUDA kernel로 만들어 GPU 병렬 연산을 할 수 있게 만들어보자. 다음을 유의해서 코드를 짜보자.
addVectorsInto를 CUDAkernel로 만들기addVectorsInto가 CUDAkernel로 작동하는 적절한 execution configuration을 찾고 실행하기- 메모리 할당과 해제를 적절히 해서
a,b,result벡터가 CPU/GPU에서 모두 접근 가능하도록 하기 addVectorsInto를 리팩토링하자: it will be launched inside of a single thread, and only needs to do one thread’s worth of work on the input vectors. Be certain the thread will never try to access elements outside the range of the input vectors, and take care to note whether or not the thread needs to do work on more than one element of the input vectors.- CUDA 코드가 잘못될 수 있는 부분에 적절히 error handling을 하자.
Solution)
1 | |
Advanced Content
The following exercises provide additional challenge for those with time and interest. They require the use of more advanced techniques, and provide less scaffolding. They are difficult and excellent for your development.
Grids and Blocks of 2 and 3 Dimensions
Grids and blocks can be defined to have up to 3 dimensions. Defining them with multiple dimensions does not impact their performance in any way, but can be very helpful when dealing with data that has multiple dimensions, for example, 2d matrices. To define either grids or blocks with two or 3 dimensions, use CUDA’s dim3 type as such:
1 | |
Given the example just above, the variables gridDim.x, gridDim.y, blockDim.x, and blockDim.y inside of someKernel, would all be equal to 16.
Exercise: Accelerate 2D Matrix Multiply Application
The file 01-matrix-multiply-2d.cu contains a host function matrixMulCPU which is fully functional. Your task is to build out the matrixMulGPUCUDA kernel. The source code will execute the matrix multiplication with both functions, and compare their answers to verify the correctness of the CUDA kernel you will be writing. Use the following guidelines to support your work and refer to the solution if you get stuck:
- You will need to create an execution configuration whose arguments are both
dim3values with thexandydimensions set to greater than1. - Inside the body of the kernel, you will need to establish the running thread’s unique index within the grid per usual, but you should establish two indices for the thread: one for the x axis of the grid, and one for the y axis of the grid.
1 | |
Solution)
1 | |
Exercise: Accelerate A Thermal Conductivity Application
In the following exercise, you will be accelerating an application that simulates the thermal conduction of silver in 2 dimensional space.
Convert the step_kernel_mod function inside 01-heat-conduction.cu to execute on the GPU, and modify the main function to properly allocate data for use on CPU and GPU. The step_kernel_ref function executes on the CPU and is used for error checking. Because this code involves floating point calculations, different processors, or even simply reording operations on the same processor, can result in slightly different results. For this reason the error checking code uses an error threshold, instead of looking for an exact match. Refer to the solution if you get stuck.
1 | |
Solution)
1 | |