GPUs are specialized for enormous small tasks in parallel, while CPUs are optimized for few huge tasks sequentially. The typical procedure for a CUDA program includes: 1) allocating memory on the GPU, 2) copying data from CPU to GPU, 3) launching kernels on the GPU, and 4) copying results back to the CPU. Measuring GPU performance focuses on throughput or tasks processed per hour rather than latency of each task.
4. Why GPU?
T T
Core
T T
Core
T T
Core
T T
Core
T T
Core
T T
Core
…
3584 cores
Good for few huge tasks Good for enormous small tasks
3.6 GHz
1.531 MHz
5. Measuring Performance
CPU – Latency
How long does it take for a work
GPU - Throughput
How many tasks per hour
Data Size : 4.5[GB]
Assume that …
CPU can process 2 tasks at the time and each core processes 200 [MB/h]
GPU can process 40 tasks at the time and each core processes 50 [MB/h]
Latency
CPU : 4500/200 [MB/h] = 22.5 [Hours]
GPU : 4500/50 [MB/h] = 90 [Hours]
Throughput
CPU : 2[Tasks]/22.5[Hours] = 0.089[Tasks/Hour]
GPU : 40[Tasks]/90[Hours] = 0.445[Tasks/Hour]
Better !
Better !
6. CUDA Program Diagram
CPU GPU
Memory MemorycudaMemcpy()
cudaMalloc()
__global__ hello()
hello.cu
NVCC
Co-processor
7. Typical Procedure
CPU allocates memory on GPU
• cudaMalloc((void **)pointer, size);
CPU copies input data from CPU to GPU
• cudaMemcpy(dest, &src, size, cudaMemcpyHostToDevice)
CPU launches kernel on GPU
• Kernel<<<N_BLOCKS,N_THREADS>>>(args…)
CPU copies results back to CPU from GPU
• cudaMemcpy(dest, &src, size, cudaMemcpyDeviceToHost)
8. CUDA Example - Addiction
- Single Thread (1)
The pointers will be indicate GPU memory space
Allocate memory for each pointers
Copy from CPU -> GPU
9. CUDA Example - Addiction
- Single Thread (2)
Kernel : Will be executed in GPU
CPU GPU
d_a
d_b
d_out
h_a
h_b
h_out
1.Memcpy
sum
2.Kernal call
3.d_out updated
4.Memcpy
10. CUDA Example - Addiction
- Single Thread (3)
Compilation using NVCC
Execution result
11. CUDA Example – Cubic
- Multi Thread (1)
To be used for determining
the size of the memory space
Initialize the elements
with each array index.
12. CUDA Example - Cubic
- Multi Thread (2)
Kernel call with SIZE_ARRAY threads.
To acquire current thread index
21. Self-Check
Which kind of task do CPUs and GPUs specialized for?
Show the way to qualify for the CPUs and GPUs.
Describe the basic procedure of CUDA programs.
Describe the procedure how to measure elapsed time using cudaEvent object.
25. Thread Block and SM
Block Block Block Block
Threads
Kernel
Stream Multiprocessor (in Titan V)
Memory
Cores
Mapped for block (1 or more)
26. Memory Structure – Programmer’s View
Thread
1
Local
Thread
2
Local
Thread
3
Local
Thread
N
Local
Shared
Thread
Local
Thread
Local
Thread
Local
Thread
Local
Shared
Thread
Local
Thread
Local
Thread
Local
Thread
Local
Shared
Thread
Local
Thread
Local
Thread
Local
Thread
Local
Shared
Global Memory
Block 1 Block 2 Block 3 Block N
GPU
31. Synchronization
- Example
• Without Barrier : Only one element(thread) is filled with the index (Don’t wait for other threads)
• With Barrier : Each elements are filled with the index (Wait until other elements filled)
35. Atomicity
CUDA supports atomic operations.
atomicAdd(), atomicCAS(), atomicXor() … and so on
Limitations
Only certain data types, operations.
No ordering constraints.
May slow down.
36. Memory Management Strategies
Maximize arithmetic intensity
Maximize compute operations per thread
Minimize time spent on memory per thread
Move frequently-accessed data to fast memory
Local
Shared
Global
Host
[Access Speed from Core]
37. Coalesce Memory Access
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
T
h
r
e
a
d
Coalesce Stride (Not coalesced)
Global Memory
with single transaction with multiple transaction
39. Thread Divergence (Warp Divergence1))
Assume that a thread loops the code for its thread index.
Each threads wait until the other threads finished.
1) https://people.maths.ox.ac.uk/gilesm/cuda/lecs/lec3-2x2.pdf - Lecture note from Prof. Mike Giles (Oxford University)
T1
T2
T3
T4
1 2 3 40 0
Pre Loop Loop 1 Loop 2 Loop 3 Loop 4 Post Loop
40. Self Check
Communication Patterns
Memory Structure in CUDA
Synchronization
Atomicity
Memory Management Strategies
Editor's Notes
CPU 는 한번에 큰 작업을 빠르게 수행 할 수 있는 코어와 스레드로 구성되어 있음 -> 큰 작업 몇 개에 최적
GPU 는 작은 작업을 수행 할 수 있는 수 많은 CUDA 코어로 구성되어 있음. -> 수많은 작은 작업에 최적
앞서 다룬 특성에 따라 성능 체크를 달리함.
CPU -> 어떤 일을 수행하는데 얼마나 걸리는가?
GPU -> 시간당 얼마나 많은 일을 하는가?
cu 확장자로 작성한 소스를 컴파일 하면 CPU와 GPU 양측을 위한 코드가 동시 생성됨.
GPU는 CPU의 보조 프로세서 처럼 동작함.
각각의 Memory 공간 존재 (Host/Device)
__global__ 을 통해 GPU에서 실행할 커널 생선 가능.
cudaMalloc() 을 통해 GPU 메모리 공간 할당 가능.
cudaMemcpy() 를 통해 CPU와 GPU 메모리 간 복사 작업 가능.
GPU에 메모리를 할당
HOST 에서 보낼 데이터를 GPU 메모리로 복사.
커널 코드 실행, BLOCK 수와 THREAD 수 명시.
커널 코드 실행 후 GPU 메모리 공간에 갱신된 결과를 HOST 로 복사.
실수 변수가 호스트에 할당된 공간을 의미.
다음과 같은 포인터들이 디바이스에 할당될 공간을 의미. (Global – 2장에서 다룰 것)
cudaMalloc 을 통해 Device 에 메모리 공간 할당. 크기는 float 의 크기.
cudaMemcpy 를 통해 Host 의 메모리에 존재하는 값을 Device 에 할당된 메모리 공간에 복사.
커널은 다음과 같이 정의됨, 입력 받은 Parameter 들은 Device 의 Global Memory 공간.
Kernel 을 호출 할 때 이러한 메모리 공간에 대한 포인터를 전달받음.
다시 cudaMemcpy 를 통해 Host 의 메모리 공간에 Device 에서 계산을 마친 값을 복사해옴.
컴파일은 다음과 같은 전용 컴파일러인 nvcc 이용 사용법은 gcc와 유사
실행해보면 원하는 결과가 나오는 것을 확인할 수 있음.
스레드를 행렬의 각 요소를 계산하는데 사용하고자 함.
행렬의 SIZE 와 할당할 크기를 설정. 요소 하나가 int 크기이므로 다음과 같이 계산.
마찬가지로 입/출력 값에 대한 Host 변수을 정의
입력 변수 h_in 초기화, 각 요소의 값은 인덱스 번호와 같다.
Device 에 대한 메모리 공간을 지시하는 포인터 선언.`
여기선 3제곱을 구하는 커널을 작성,
커널에는 입 출력에 대한 Device 메모리 주소가 들어감.
현재 인덱스는 스레드의 인덱스와 같음, 이를 할당해줌.
해당 인덱스의 출력은 입력의 3제곱.
각 스레드마다 정상적으로 3제곱이 계산된 것을 확인할 수 있음.
작은 프로젝트 쿠쿠단을 작성해봄.
99x99 까지 계산하는데 각각의 곱셈은 하나하나의 스레드에서 계산되도록 만듦.
커널 내에선 단순히 두 값의 곱을 하나의 출력 인덱스에 저장하는 연산을 수행함.
d_out 엔 100000개의 원소가 있고 각각의 원소가 하나의 스레드를 가지게 된다.
앞자리와 뒷자리 각각은 100의 크기를 가지는 벡터로 정의되어 있고 그 값은 벡터의 인덱스로 초기화 한다.
결과가 저장될 결과 행렬의 크기는 100x100 이다.
이어서 cudaMalloc 을 수행해야 하나 앞에서 다루었으므로 생략
행렬의 크기만큼 스레드/블럭을 지정하여 커널을 실행시킨다. 커널 주위를 Cudaevent 객체의 Start 와 Stop 으로 감싸 구간 실행 시간을 측정한다.
커널 실행후 값이 채워져 나온 d_out 의 값을 Host 메모리 공간에 복사한다. 결과를 출력하고 Device에 할당된 메모리를 해제한다.
스레드 할당이 올바르게 이루어진다
소요시간은 약 7 마이크로초
결과또한 충돌없이 잘 계산되었다
메모리가 값을 참조하는 방식엔 여러 방식이 있으나 우선 4가지를 살펴본다.
*Stencil 잘 이해 안감
Transpose는 행렬의 전치를 메모리 공간으로 표현하는 방법이다.
강좌에선 SM 내부에 코어가 한 묶음 있지만, Titan V 에선 4개가 있음.
블록이 각각 SM에 할당됨.
*Titan X 나 그 외 하위 그래픽카드에선 SM 내에 블록이 1개만 존재
한 SM에 여러 블록 가질 수 있음.
단, 블록이 여러 SM에 걸칠 수 없음
- 블록 사이즈 한계가 있음 (SM 수가 한정적)ㄴ
프로그램 관점에서 보면 GPU의 내부의 구조는 이러함
GPU를 총괄하는 Global memory.
블록을 총괄하는 Shared Memory.
스레드마다 할당된 Local Memory.
실제 Titan V 의 구조는 이러함.
여러 스레드가 하나의 공유메모리를 접근하려다보면 동기화 문제가 생김.
__syncThread() 를 사용하여 모든 스레드의 공유메모리 읽기/쓰기가 완료될 때 까지 일시 대기. (공유메모리에 있는 값의 변경이 완료되는 것을 의미)
작업이 끝나면 재개.
배리어에 다 도착 해서 오퍼레이션이 끝날 때 까지 대기.
동기화를 이해하기 위해 행렬 Shift 예제를 수행함.
왼쪽과 같은 코드에서 문제가 되는 부분은 읽기/쓰기 가 발생하는 부분.
처음에 인덱스 번호를 공유메모리에 ‘쓰는’ 부분
공유메모리의 어떤 원소의 다음 원소를 ‘읽는’ 작업
읽은 값을 그 이전 원소에 ‘쓰는’ 작업
총 3개의 Barrier 가 필요하여 오른쪽 코드와 같이 수정.
// 블록 안에 있는 스레드만
Barrier 가 없으면 모든 스레드의 공유메모리 쓰기 작업을 기다리지 않기에 하나만 쓰고 끝나버려 다음과 같은 결과가 나옴.
Barrier 를 올바르게 적용하면 다음과 같이 제대로 된 결과가 나옴.
Local 메모리는 다음과 같이 어떤 커널 ‘스레드' 내에서만 쓰이는 메모리 영역.
Global Memory는 Device 에 Allocate 되어 GPU내의 모든 스레드와 블록에서 참조 가능한 메모리 공간이다.
Shared Memory는 다음과 같이 __shared__ 키워드를 사용하여 정의한다.
특정 연산, 데이터 타입에만 적용
순서를 제한할 수 없음 – 어떤 것이 먼저 수행될지 모름.
메모리 접근을 직렬화 하면 대기시간이 생겨 느려질수 있다.
하나의 sm 만이 아니라 전체 시스템에서의 동기화를 의미 (Global Memory 를 참조하기때문에 많이 느림)
// No ordering constraints : 둘중 어떤게 먼저 실행될지 모름 (대신 둘다 실행 되는건 확실히 방지)
효율적인 메모리 사용을 위해
스레드가 메모리에 사용하는 시간을 줄임
자주 쓰는 데이터를 빠른 메모리에 두기
스레드당 연산량을 늘림
Warp(Thread Divergence) 고려
Global Memory 는 Coalesce 된 형태로 저장하는게 효율적이다.
사이가 벌어지면 그만큼 Transaction 이 많이 필요하고 더군다나 벌어진 정도가 불규칙하다면 매우 비효율적이다.
한 블록 내의 스레드가 서로 다른일을 하는 것
조건문에 따라 같은 Warp 내의 Thread 의 수행시간이 달라질 수 있음.
그렇게 되면 먼저 끝난 Thread 는 대기하게 됨.
Warp 에 해당하는 스레드가 서로 하는일이 다르다면 어떤 스레드가 먼저 끝나도 다른 스레드가 모두 끝날때까지 기다려야함 (Warp Divergence)
그림은 서로 다른 시간동안 수행할 때의 예제 ( For loop )
조건문을 가능한한 줄여 스레드들이 비슷한 시간에 끝낼 수 있도록함.