Upload
-
View
3.319
Download
5
Embed Size (px)
DESCRIPTION
NDC 2012 발표자료GPGPU(CUDA)를 이용한 MMOG캐릭터 충돌처리
Citation preview
목표• GPU 기반 병렬 프로그래밍의 소개• GPGPU 기술 습득의 동기부여• 자세한 프로그래밍 기법은 생략한다 .
서버에서 충돌처리를 하면 좋은 점• 위치 , 속도 Hack 의 원천 봉쇄• 서버측에서 능동적으로 이것저것 하기 좋음• 정밀한 위치 추척 가능 (30,60 프레임 단위로 처리
가능 )• 클라이언트에서 플레이어 제어하듯 서버에서도
플레이어 ,NPC, 몬스터의 이동을 프로그래밍 가능 .
걸림돌• 동기화 맞추는게 까다롭다 .• 서버측 CPU 부하가 심하다 .
동기화의 문제• 여기서 다룰 주제는 아닙니다• 그냥 열심히 하면 됩니다 ( 될겁니다 ?)
CPU 부하의 문제• 요새 CPU 는 기본 4Core• 충돌처리 코드를 멀티스레드로 작동하도록 만든다
그래도 감당이 안되면 (1)?
• CPU 를 더 꽂는다 .• 일반 데스크탑 CPU 는 멀티 CPU 구성 불가• 2 개 이상 꽂을 수 있는 Xeon CPU 를 사용한다 .
참고 )
프로젝트 엡실론에선 플레이어 4000 명을 커버하기 위해 6core Xeon 2 개를 사용했음 .
그래도 감당이 안되면 (2)?
• 4 개 이상 꽂을 수 있는 Xeon CPU 를 사용한다• 여기서부턴 조립불가 . 가격이 미친듯이 뛰기
시작함• 10 Core Xeon 을 8 개 꽂으면 확실 !
Intel® Xeon® Processor E7-8870
4616$ x 8 = 36928$(CPU 만 )
서버머신 가격은 더 비쌈 .
저렴하게 갑시다 .
• CPU 가 계산할 충돌처리 코드를 GPU 에서 돌린다 .
GPGPU(General-Purpose computing on GPU
• GPU 를 사용하여 CPU 가 전통적으로 취급했던 응용 프로그램들의 계산을 수행하는 기술
• GPU 코어 1 개의 효율은 CPU 코어 1 개에 비해 많이 떨어지지만 코어의 개수가 엄청나게 많다 .
• 많은 수의 코어를 사용하면 병렬 산술 연산의 성능은 CPU 의 성능을 압도한다 .
GPU vs CPU
GM < ν Gundam - 택도없는 승부 . 이것은 학살 .
GPU vs CPU
GM <= ν Gundam – 그래도 혹시 모르지 않나 ?
GPU vs CPU
GM > ν Gundam – 한판 붙자 !!!
GPU vs CPU
GM > ν Gundam – 줄 서서 1:1 로 붙으면 자살행위
GPGPU 의 특징
강점• 엄청난 수의 스레드를 사용할
수 있다 .• 부동소수점 연산이 엄청
빠르다 .• 컨텍스트 스위칭이 엄청
빠르다 .• 그래서 충분히 병렬화된
산술처리에는 짱 .
약점• 흐름제어 기능 자체가
빈약하다 .• 프로그래밍 기법의 제약 (
재귀호출불가 )• Core 당 클럭이 CPU 에 비해
많이 느리다 (1/3 – 1/2 수준 )
CUDA (Compute Unified Device Architecture)
• C 언어 등 산업 표준 언어를 사용하여 GPU 에서 작동하는 병렬처리 코드를 작성할 수 있도록 하는 GPGPU 기술
• nVidia 가 개발 , 배포 . 그래서 nVidia GPU 만 가능 .• 비슷한 기술로 OpenCL, Direct Compute Shader 가
있음 .
From nVidia CUDA_C Programming Guide
GPU 구조의 이해 (GF110)*CPU 의 Core 에 해당하는 것은 GPU 의 SM 이다 .
따라서 흐름제어의 측면으로 보면 512 Core CPU 가 아니라 16 Core CPU 에 상응한다 .
Thread
• CPU 의 Thread 와 비슷…하다 ( 같지 않다 ).• GPU 의 Core( 혹은 SP) 에 맵핑• 독립적인 컨텍스트를 가지고 있다 ( 라고 해봐야
연산에 필요한 컨텍스트 뿐 ).• 컨텍스트 스위칭이 겁나 빠르다 .• 독립적인 흐름제어는 불가능하다 (32 개 단위로
묶여서 움직임 ).
Block
• Block -> N 개의 Thread 집합 . SM 에 맵핑• 동일 Block 의 스레드는 L1 캐쉬와 Shared Memory 를
공유한다 .• 스레드만 잔뜩 있으면 될것 같은데 왜 이런게 있냐면…
그것은 아마도 CPU 의 core ! GPU≒ 의 core
CPU 의 core GPU≒ 의 SM
이기 때문일 것이다 .
Grid
• Block 들의 집합• 그냥 쉽게 생각해서 그래픽 카드 -> Grid
Thread, Block, Grid• Thread -> Core• Block -> SM• Grid -> GPU
Kernel
• GPU 에서 돌아가는 C 함수• CPU 멀티스레드 프로그래밍에서의 Thread
Function과 같다 .• 복수의 Thread 에 의해 실행된다 .• CPU 측 (host) 에서 호출되며 GPU 측 (device)
코드를 호출할 수 있다 .
N x N 매트릭스의 합을 구하는 CUDA 커널함수
CUDA메모리
• Global Memory – 보통 말하는 Video Memory. 크다 . 느리다 .• Register – CPU 의 레지스터와 같다 . 로컬변수 , 인덱싱하지
않고 크기가 작은 로컬배열은 레지스터에 맵핑된다 . 작다 . 빠르다 .
• Shared Memory – Block 의 스레드들이 공유하는 메모리 . 블럭당 48KB. 작다 . 빠르다 .
참고 )
SM 에 하나씩 존재하는 64KB 의 캐쉬를 16KB L1 캐쉬와 48KB Shared Memory 로 사용
메모리 억세스 범위
• Thread ->
Local Memory ,
Shared Memory , Global Memory
• Block ->
Shared Memory, Global Memory
• Grid -> Global Memory
CUDA 이미지 프로세싱 예제
성능 비교2885x4102 크기의 이미지에 5x5 Gaussian Blur 를 3회 적용
1Threads - Intel i7 2600K @3.8GHz
-> 10054.3ms
8Threads - Intel Intel i7 2600K @3.7GHz
-> 2247.8ms
CUDA - GTX480 (15 SM x 32 Core = 480 Core)
->203.7ms
참고 )
이미지 필터링 코드는 CUDA, CPU 완전 동일 .
알고리즘상의 최적화는 없음 .
병렬처리 효율의 비교를 위한 테스트임 .
단순포팅예제
CUDA 로 충돌처리하기
구현전략
• 싱글스레드 C 코드로 작성한다• 자료구조상으로 최적화한다 .• 멀티스레드로 바꾼다 .• CUDA 로 ( 단순 ) 포팅한다 .• 퍼포먼스를 측정한다 .• 절망한다 .• CUDA 에 적합하도록 일부의 설계를 변경 한다 .• 최적화한다 .
충돌처리코드개요
• 움직이는 타원체의 궤적에 걸칠만한 삼각형과 다른 타원체를 수집
• 루프를 돌며 각각의 삼각형과 타원체에 충돌체크• 가장 가까운 충돌점을 찾는다• 반사 , 또는 미끄러짐 벡터를 구한다 . • 충돌 후 벡터의 스칼라량이 0 에 근접하면 탈출• 그렇지 않으면 다시 1 부터 반복
CPU 코드로 작성
• CUDA 로 포팅할 것을 염두해 두고 작성한다 (재귀호출 사용불가 )
• 주변 삼각형과 타원체를 골라내기 위해 공간분할이 중요하다 . (BSP Tree, KD Tree, Grid 등 ) BSPTree나 KDTree 는 CUDA 로 포팅하기도 어렵고 CUDA로 돌리려면 성능이 많이 떨어진다 . GRID 와 같이 단순한 자료구조를 권장 .
CPU 코드로 작성
• 일단 CPU 상에서 멀쩡히 돌아가는게 가장 중요하다 .
• SSE,AVX 등 SIMD최적화는 나중에 생각하자 .CUDA 포팅에 걸림돌이 된다 .
Multi-Thread 코드 작성
• 잘 돌아가는 Single-Thread 코드를 작성했으면 Multi-Thread 버젼을 만든다 .
• 충돌처리할 오브젝트들을 Thread 개수로 나눠서 처리 .
Multi-Thread 코드 작성 (Tip)
• 4000obj / 8 thread = 500, 1 thread 당 500 개처리 -> OK? ->NO!!!!
오브젝트마다 처리시간이 다르기 때문에 탱자탱자 노는 thread 가 생긴다 .
• Thread 들이 경쟁적으로 큐에서 충돌처리할 오브젝트를 가져간다 . -> OK!!!
거의 노는 thread 없이 CPU 자원을 꽉 채워서 사용할 수 있다 .
CUDA 코드로 포팅
• 표준적인 문법만 사용했다면 별로 수정할게 없다 .• Multi-Thread 코드와 유사하다 . CPU Thread ->
CUDA Thread 로 바로 맵핑하면 된다 .
단순포팅
• 처리하고 하는 [ 원소 1 개 > thread 1 개 ] 직접 맵핑
• 1 pixel -> 1 thread, 1 오브젝트 -> 1 스레드• C 로 짠 코드는 거의 100% 그대로 돌릴 수 있다 .• 이미지 프로세싱 매트릭스 연산 등에선 충분히
효과가 있다 .
단순포팅의 결과
• 충돌처리에선 CPU 코드 ( 멀티스레드 ) 보다 느리게 작동한다 !
• HW 스펙만으로 동시 처리 가능한 오브젝트 수를 짐작해보면…
CPU(i7 2600) : 4Core x 2(HT) = 8 Thread,
CUDA(GF110) : 32Core x 16SM = 512 Thread
단순하게 생각하면 동시에 처리하는 오브젝트 수가 훨씬 많은데 왜 느릴까 .
SIMT(Single Instruction Multiple Threads)
• 동일 명령어를 여러개의 Thread 가 동시 실행• GPU 는 기본적으로 SIMT 로 작동• N차원 벡터를 다루는 산술처리에선 적합• Thread중 일부가 분기를 하거나 루프를 하면 나머지
Thread 들은 대기 -> 병렬처리의 의미가 없어짐 .
WARP 의 이해
• Thread 들을 묶어서 스케쥴링하는 단위• 현재까지는 모든 nVidia GPU 의 1 Warp 는 32
Thread. 즉 32 Thread 는 늘 같은 같은 코드 어드레스를 지나감 .
• 동시 수행 가능한 Warp 수는 SM 의 Warp 스케쥴러 개수에 의존 . SM 당 2 개의 Warp 를 동시에 실행가능 (GF100)
WARP 의 이해
• 32 Thread중 1 개의 Thread 만이 분기해서 Loop를 돌고 있으면 31 개의 Thread 는 1 개의 Thread가 Loop 를 마치고 같은 코드 어드레스를 수행할 수 있을 때가지 대기 .
• 각 Thread 가 다른 루프 , 다른 흐름을 타는 것은 엄청난 성능 저하를 부른다 .
Worst CaseDWORD ThreadIndexInBlock = threadIdx.y * blockDim.x + threadIdx.x;
DWORD UniqueThreadIndex = UniqueBlockIndex * ThreadNumPerBlock + ThreadIndexInBlock;
int CountList[32] = {0,1,2,3,...,31};
// 32 개의 스레드가 동시에 루프를 돈다 .
for (int i=0; i<CountList[UniqueThreadIndex]; i++)
{
...
}
// CountList[UniqueThreadIndex] == 0 인 스레드도 CountList[UniqueThreadIndex] == 31 인 스레드를 대기// 모든 스레드가 가장 오래 걸리는 스레드에 맞춰서 움직인다 .즉 // 모든 스레드가 31번 루프를 도는 것과 같다 .
SIMT 를 고려한 설계
• 가장 시간을 많이 잡아먹는 코드는 어디인가 ?• 병렬화 하기 좋은 코드는 어디인가 ?• 가장 시간을 많이 잡아먹고 병렬화 가능한 코드를
병렬화한다 .
충돌처리 코드에서 SIMT 를 고려한 설계
• 충돌처리의 경우 Loop 를 돌며 인접한 삼각형과 오브젝트 ( 타원체 ) 를 찾는 코드에서 가장 많은 시간을 잡아먹는다 .
• WARP 단위로 명령어를 실행하기 때문에 삼각형과 타원체를 찾는 루프에서 대부분의 코어가 논다 .
• [1 개의 Thread 가 N번 Loop -> M 개의 Thread 가 N/M번의 Loop] 로 수정
충돌처리 단순포팅에서의 병목Thread
Object
Thread
Object
Thread
Object
Tim
e
Tim
e
Tim
e
Wai
t
Wai
t
루프개선( 단순화된 모델에서의 루프 개선 )
• 충돌처리 코드에서 가장 병목이 걸리는 루프를 단순화 시켜보자 .
A. 오브젝트 한 개당 N 개의 타원체와 삼각형에 대해 충돌체크를 하고 가장 가까운 충돌점을 찾는다 .
B. 오브젝트 한개당 N 개의 숫자를 들고 있고 그 중 가장 작은 수 , 혹은 가장 큰 수를 찾는다 .
루프개선 ( 단순화된 모델에서의 루프 개선 )
• 8192 개의 CELL 이 있다 .• 각 CELL 은 랜덤하게 1 ~ 8192범위의 숫자를 1 ~
8192 개 가지고 있다 .• 각 CELL 에 대해서 최대값을 구한다 .
1) CPU Single Thread
2) CUDA , 1 Cell -> 1 Thread
3) CUDA , 1 Cell -> 1 Block
1
2
3
.
.
8191
8192
1
2
3
.
.
99
100
1
2
3
.
.
9
10
1
2
3
.
.
49
50
8192 100 10 50
1
2
3
4
5
6
7
1
2
3
4
5
1
2
3
1
7 5 3 1
1 Obj -> 1 ThreadThread
Tim
e
Wai
t
Wai
t
Wai
t
Thread
Tim
e
Thread
Tim
e
Thread
Tim
e
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
Thread
1
2
3
4
5
6
7
1
2
3
4
5
1
2
3
1
7
5
3
1
1 Obj -> 1 BlockBlock
Tim
e
Block
Tim
e
Block
Tim
e
Block
Tim
e
1
2
3
4
5
5
1
2
3
4
5
6
1
2
3
1 Cell -> 1 Thread
1 Cell -> 1 Block
1 Cell -> 1 Block
단순화된 모델에서의 Loop 개선 결과
• CPU Single-Thread
35.68ms• CUDA ,1 Cell - 1 Thread
11.63ms• CUDA , 1 Cell - 1 Block
1.24ms
• 같은 방식으로 CUDA 충돌처리 코드에 적용
1 Object -> 1 Thread
Thread
Object
Thread
Object
Thread
Object
Tim
e
Tim
e
Tim
e
Wai
t
Wai
t
BlockObject
Tim
eThread
Thread
Thread
Thread
Thread
Thread
Thread
BlockObject
Tim
e
Thread
Thread
Thread
Thread
BlockObject
Tim
e
Thread
Thread
Thread
1 Object -> 1 Block
충돌처리 루프 수정 후
• 빨라졌다 !• 그러나 생각보다는 많이 안빠르다 .(CPU대비 2-3
배 성능 )
Occupancy
• SM 당 활성화 Warp 수 / SM 당 최대 Warp 수• GPU 의 처리능력을 얼마나 사용하는지 척도• 대부분의 경우 Occupancy 가 증가하면 효율 (
시간당 처리량 ) 도 증가함• Global Memory 억세스로 latency 가 발생할때 유휴
Warp 를 수행함 .• GPU 디바이스의 스펙에 상관없이 WARP 를 많이
유지할 수 있으면 처리성능이 높아진다 .
Occupancy Calculator
Occupancy늘리기
• 많은 워프를 사용하여 Latency 숨기기• 레지스터 수를 줄이도록 노력한다 .• Shared Memory 사용을 아낀다 .( 줄이는 것이
아니다 )• Shared Memory 사용량 때문에 Occupancy 를
떨어뜨린다면 Shared Memory대신 Global Memory를 사용하는 것이 나을수 있다 .(L2 캐쉬 때문에 생각보단 느리지 않다 )
Shared Memory 아껴쓰기
• 자주 억세스할 경우 Shared Memory 에 올려놓으면 빠르다 .
• 무작정 마구 쓰면 SM 에 맵핑할 수 있는 블럭 수가 크게 떨어진다 .
• Shared Memory절약 ->Active Block 수 증가 -> 성능향상
Register 사용량 줄이기
• 사실 뾰족한 방법이 있는건 아니다 .• 알고리즘 수준에서의 최적화 -> 결과적으로 작은
코드 .• 레지스터 개수를 컴파일 타임에 제한할 수는 있다
(경험상 별로 안빨라짐 ).
추가적인 최적화
• Unrolling• PCI-E 버스 병목 줄이기• CDUA Stream 이용 ( 이번에는 안다룸 )
Unrolling
• 루프를 풀면 컨트롤 명령어 수를 줄일 수 있다 . 산술 명령어를 더 실행할 수 있다 .
• Occupancy증가만큼은 아니지만 꽤 효과가 있다 .• 아직까지는 nvcc 가 능동적으로 Unrolling 을
해주지는 못하는 것 같다 . 수동으로 해주면 효과가 있다 .
PCI-E 버스 병목 줄이기
• 한번에 몰아서 전송한다 .• 퍼포먼스를 떨어뜨리지 않는 선에 압축할 수 있으면
압축한다 . 이 경우 커널 호출을 여러번 해야한다 .• 처리량의 향상에는 큰 도움을 주지 않지만 응답성
면에선 차이가 크다 ( 게임에선 처리량보다 응답성이 중요 ).
충돌처리 성능 테스트
• 테스트 조건 타원체 7657 개 (최대 8192 개 ) 삼각형 77760 개 타원체 VS 타원체 , 타원체 VS 삼각형 유휴시간 없이 최대프레임으로 충돌처리를 수행 .
한번 충돌처리에 걸리는 시간 측정 .
CPU i7 2600K (8Threads @3.7GHz) – 326msCUDA GTX480 – 40ms (CPU대비 8.15 배 )
게임 클라이언트에서 사용
• 하나의 GPU 로 렌더링과 CUDA 처리를 겸하게 되면 퍼포먼스 하락 .
• PCI-E 버스 병목도 원인 .• GPU 가 두개 꽂힌 시스템이라면 게임
클라이언트에서도 프레임향상에 도움이 된다 .• 두번째 GPU 로 충돌처리를 하면 응답시간이 더
빠르다 . ( 캐릭터 100 마리일 때 약 3~4 배 차이 )
CUDA 적용의 장점
• 가격 대비 성능이 매우 뛰어나다 .• CPU 자원의 여유를 확보할 수 있다 ..• 같은 기능의 코드를 CPU 와 GPU 양쪽에서 동시
처리해도 좋다 . 성능 2x 이상 향상 .• 확장성에는 큰 도움이 된다 .• 새로운 GPU 가 나올때마다 성능이 크게 향상된다 .• 기술 덕후의 호기심을 충족시킨다 .
CUDA 적용의 단점 or 걸림돌
• 단순 포팅은 의미가 없으므로 코드를 상당 부분 수정해야한다 .
• GPU 에 맞춰서 최적화 하고나면 코드가 아스트랄해진다 . 유지보수가 어렵다 .
• 디버깅이 지랄같다 .
(희소식 ! Parallel Nsight 2.2 에서 드디어 싱글 GPU 디버깅이 가능해졌다 !!!)
결론
• 게임서버 용도이고 돈이 많다면 굳이 CUDA 안써도 됨 .
하지만 이걸 보면 쓰고 싶을걸 ?
32core 이상의 Xeon 서버Dell Power Edge 910 (8 core x 4) - 27989$
HP ProLiant DL980 (10 core * 4) 55229$
Tesla 장비 (PCI-E 카드 )
Tesla m2070(16 SM , 512 core) - 2699$
GeForce 그래픽카드GTX580 (16 SM , 512 core) - 400$ ~ 500$
Tesla내장 서버IBM HS23 m2070 Server - 6999$
참고문헌
• Programming Massively Parallel Processors (David B.Kirk , Wen-mei W. Hwu)
• CUDA 병렬프로그래밍 ( 정영훈 )• nVidia GPU Computing SDK 4.1 Documents
Q/A