Heterogeneous Computing
- Host
- The CPU and its memory (host memory)
- Device
- The GPU and its memory (device memory)
- 요즘에는 ARM, RISC-V를 GPU에 넣는 경우도 있는데, 그럴때는 GPU가 마치 peripheral device처럼 동작하는 것이다.
Programs
- Host program
- 커널 실행 담당
- Kernels
- Compute device에서 실행가능한 코드의 최소단위
- 한번 실행되면 많은 동일한 인스턴스들이 생성되어 data parallelism을 exploit할 수 있다
- Host program과 kernel은 아래 그림처럼 병렬로 실행된다
Execution process
- CPU 메모리에서 input data를 복사하여 GPU 메모리로 옮긴다
- GPU 코드를 로드해서 실행한다 (요때는 On-chip cache를 쓴다)
- GPU memory에서 CPU memory로 결과를 복사한다
“Hello World! in CUDA”
- A program with no device code
- NVIDIA compiler (nvcc) 를 이용해 컴파일
- Host code와 Device code를 분리하여 Host code는 gcc, Device code는 NVIDIA 컴파일러를 사용해 컴파일한다
- Host code와 Device code를 분리하여 Host code는 gcc, Device code는 NVIDIA 컴파일러를 사용해 컴파일한다
“Hello World!” with Device Code
__global__
은 CUDA C/C++ 에서 사용하는 키워드로, host code 에서 부르는 device의 함수<<<>>>
은 host code에서 device code로의 call을 의미- Kernel launch 라고도 함
- 1, 1은 블록 갯수로 1개, 블록 크기로 스레드 1개를 사용한다는 의미
- 위 코드에서는 device가 하는건 아무것도 없다
A Simple Kernel to Add Two Integers
Device kernel code
- Device에서 동작
Host code
cudaMalloc
GPU를 위한 메모리(*a, *b, *c) 할당- 메모리를 할당하여 세개의 포인터를 CUDA의 디바이스에게 넘겨줌
- 중요한 것은 실제로 주소공간 자체가 호스트와 디바이스가 따로 있기 때문에, 메모리 allocation을 할 때 양쪽을 할당한 다음 호스트 쪽에 값을 할당하고 디바이스쪽으로 copy(
cudaMemcpy()
)를 해야 한다.- 여기서 alloc된 메모리는 pagable한데, sync 방식이라 괜찮다
- 아니라면 pinned memory 할당을 위해
cudaMallocHost
혹은cudaHostAlloc
을 쓰고, 해제를 위해cudaFreeHost()
를 써야 함
- *a, *b, *c를 GPU에게 넘겨줄 때는 디바이스쪽 주소를 넘겨야 한다 (d_a, d_b, d_c)
- 디바이스 포인터 vs 호스트 포인터
- 디바이스 포인터
- GPU 메모리를 가리킴
- 호스트 코드에서 전달 가능하지만, 직접 참조(dereference는 불가능하다)
- 호스트 포인터
- CPU 메모리를 가리킴
- 장치 코드에서 전달 가능하지만, 직접 참조는 불가능
- 디바이스 포인터
- 메모리 관리 함수:
cudaMalloc(),cudaFree(),cudaMemcpy()
- cudaMemcpy의 플래그
- cudaMemcpyHostToDevice
- 호스트 → 디바이스 메모리 복사
- cudaMemcpyDeviceToHost
- 디바이스 → 호스트 메모리 복사
- cudaMemcpyHostToDevice
- add<<1, 1>> : 그리드(커널 인덱스 스페이스) 내에 블록 및 쓰레드가 각각 하나
- 그리드가 전체 인덱스 스페이스
- 블록은 쓰레드의 집합으로, 워크 그룹과 같은 것
- 쓰레드는 하나의 워크 아이템 같은 것
- 즉 본 예제에서는 쓰레드가 하나인 하나의 블록만 실행하는 예제
- cudaMemcpy의 플래그
Running add()
in Parallel
add()
를 N개 병렬적으로 돌린다고 생각해 보자add()
의 parallel invocation은 block이라고 불림
-
blockIdx 는 약속된 변수로, 1차원 변수의 경우 x, 2차원 변수의 경우 x, y, 3차원 변수의 경우 x, y, z … 를 사용한다
-
blockIdx == get_group_id()
-
threadIdx == get_local_id()
-
blockIdx.x * blockDim.x + threadIdx.x == get_global_id()
-
여기서는 그냥 group id 랑 global id가 같아서 관계 없음
-
SPMD를 사용한다
-
실제 코드
CUDA Execution Model
- Thread (Work item)
- Sequential execution unit
- 병렬로 실행되고, 모든 쓰레드가 동일한 sequential program을 실행함
- 실제로는 stack이 없기에 아키텍처적인 쓰레드라고 보기는 어렵다
- Threads Block (Work group)
- A group of threads
- 동일한 SM 내에서 실행됨
- 같은 쓰레드 블록의 경우 협력 가능
- 경량 동기화: 동기화 배리어를 이용
- 데이터 교환 (아마도 shared memory를 이용해서)
- Grid
- 여러 스레드 블록으로 구성
- 여러 SM에 걸쳐 실행됨
- 글로벌 메모리를 통해 블록 간 통신
- 비용이 많이듬
매핑
- Grid → GPU : 하나의 그리드가 하나의 GPU에 대응됨
- Block → SM : 각 블록은 SM에 매핑되어 처리됨 (OpenCL에서 워크그룹 단위로 SM에 일을 assign하는 것과 마찬가지)
- Thread → SP : 블록 내 스레드는 SM의 SP(Scalar processor, PE인듯)에서 실행됨
- Warp: 각 블록의 스레드는 Warp 단위로 실행(하나의 SM이 여러 warp를 돌림)
- Warp는 32개의 스레드로 구성
- Warp는 32개의 스레드로 구성
커널 실행
- 다음과 같은 구조로 호출
myKernel<<<B, T>>>(arg1, ... )
- 차원에 따라 다르게 호출 가능. 예를 들어 2차원인 경우,
dim3 grid(4, 4);
dim3 block(16, 16);
myKernel<<<grid, block>>>();
- 이런식으로 호출 가능하다
- 그리드는 4 * 4 = 16개 블록의 그리드
- 블록은 16 * 16 = 256 스레드
- 총 스레드는 16 * 256 = 4096 스레드
Built in variables
- blockIdx.x, blockIdx.y, blockIdx.z : 그룹 아이디
- threadIdx.x, threadIdx.y, threadIdx.z : 로컬 아이디 (블록 내에서의 offset)
- blockDim.x, blockDim.y, blockDim.z : 블록을 구성하는 스레드 수
Running add() in Multiple Threads
- 커널코드가 동일할때, 아래와 같이 바꿔주면 된다. 아까와 다르게 하나의 SM에서 실행된다.
- 쓰레드 개수가 적을 때는 성능이 비슷할 텐데, 쓰레드 갯수가 많으면 SM을 놀게하는거기 때문에 별로 좋지 않다
- 어차피 하나의 블록(SM)이 한번에 실행하는 쓰레드의 수에는 한계(32개)가 있기 때문이다. 다른 SM이 동시실행을 해줘야 함
- 그러나 블록 간 동기화나 통신이 필요할 때에는 유리할 수 있겠음. 블록 내의 스레드는 shared memory를 공유하기 때문
Multiple Threads and Multiple Blocks
-
CUDA에서는 글로벌 인덱스라는게 없기 때문에, 아래와 같이 계산해야 한다
-
즉 SM에 공평하게 로드를 나눠주고 싶으면 위 코드에 아래와 같은 호스트 코드를 붙여줌
Handling Arbitrary Vector size
- vector가 1023 같은 식이면 어케할 것인가 → 실제로 개수가 1023이 아니라 1024가 되면, 0~ 1023 이 index일 것. 이걸 다루는 코드로 1021개 백터를 가진 애를 핸들링한다고 해보자. 1021, 1022, 1023 을 접근할 때는 seg fault가 나니 보호해 줘야 한다. 따라서 n을 아규먼트로 넘겨줘서 보호해 주는 코드
- 문제는 블록이 가득 차 있고, 마지막에 저 세개를 담당하는 애가 있으면 걔는 전부 if의 false가 나올테니, branch divergence가 생기게 된다. 아주 약간(마지막 블록만 손해보는 거니)의 손해가 발생한다.
- Branch divergence를 줄이는 것이 참 중요하다
- 모든 함수들은 기본적으로 blocking으로 동작한다 (memcpy)
- 이상한데? 왜냐하면 내가 찾아봤을 때는 비동기도 있다고 나왔기 때문
- 의문 해결 ⇒ SHPC 18 - CUDA Streams