GPGPU(CUDA)를 이용한 MMOG 캐릭터 충돌처리

유영천
http://yuchi.net
Twitter: @dgtman
목표
• GPU기반 병렬 프로그래밍의 소개
• GPGPU기술 습득의 동기부여
• 자세한 프로그래밍 기법은 생략한다.
어따 쓸까…
http://youtu.be/3sl63XhuuWc

         MIKU 데모(타원체)
서버에서 충돌처리를 하면 좋은 점
• 위치,속도 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 Case
DWORD 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        Thread   Thread

Object        Object   Object
루프개선
       (단순화된 모델에서의 루프 개선)

• 충돌처리 코드에서 가장 병목이 걸리는 루프를
  단순화 시켜보자.
 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     1     1    1
 2     2     2    2
 3     3     3    3
 .      .    .    .
 .      .    .    .
8191   99    9    49
8192   100   10   50


8192   100   10   50
1 Obj -> 1 Thread
Thread     Thread    Thread   Thread

1         1          1        1
2         2          2
3         3          3
4         4
5         5
6
7


7         5          3        1
1 Obj -> 1 Block
             Block                 Block              Block          Block

Thread   1           Thread    1           Thread 1           Thread 1
Thread   2           Thread    2           Thread 2
Thread   3           Thread    3           Thread 3                 1
Thread   4           Thread    4
                                                  3           Thread 1
Thread   5           Thread    5
                                                              Thread 2
Thread   6
                               5           Thread 1           Thread 3
Thread   7
                                           Thread 2           Thread 4

                     Thread    1           Thread 3           Thread 5
         7
                     Thread    2           Thread 4
                                           Thread 5                 5
                     Thread    3
                                           Thread 6
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         Thread      Thread

Object         Object       Object
1 Object -> 1 Block

         Block               Block
Object   Thread     Object   Thread

         Thread              Thread

         Thread              Thread

         Thread              Thread

         Thread

         Thread

         Thread              Block
                    Object   Thread

                             Thread

                             Thread
충돌처리 루프 수정 후

• 빨라졌다!
• 그러나 생각보다는 많이 안빠르다.(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) – 326ms
CUDA 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

GPGPU(CUDA)를 이용한 MMOG 캐릭터 충돌처리

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