GPU 아키텍처
SM: 스트리밍 멀티프로세서
하나의 GPU는 SM이라는 물리적 구조를 여러 개 포함한다. SM은 여러 CUDA 코어를 가진 연산 장치다. Fermi 아키텍처는 하나의 SM에 32개의 CUDA 코어를 가지고 있다. SM에는 CUDA 코어말고 레지스터, 공유 메모리, L1 캐시 등이 포함된다.
CUDA 코어
CUDA 코어는 GPU의 가장 기본이 되는 프로세싱 유닛이다. 코어 안에는 FP 연산기, INT 연산기 등이 있으며, CUDA 프로그램의 동작 단위가 스레드이므로, 스레드 1개에 CUDA 코어 1개가 할당된다.
CUDA 스레드 계층과 GPU 하드웨어
간단한 요약
- 1 스레드 = 1 코어
- 32개 스레드가 모여 1개의 워프이다. 또한 워프가 모여서 스레드 블록을 이룬다.
- 블록 내에 스레드가 있다는 것은, 스레드 묶음인 워프로 이루어져 있다는 것이다.
그리드에서 GPU
- 1개의 GPU는 여러 개의 그리드를 처리할 수 있다.
- 1개의 그리드가 여러 개의 GPU를 동시에 사용하거나 옮겨가며 실행할 수 없다.
스레드 블록에서 SM
- 그리드의 스레드 블록은 그리드가 배정된 GPU 속 SM에서 처리한다.
- 스레드 블록을 처리하는 물리적 단위는 SM이다. 따라서 스레드 블록을 적절히 SM에 분배해야 한다.
- 스레드 블록들은 SM에 순차적으로 균등하게 분배되어 처리된다. (하나의 SM에 여러 개의 블록이 할당될 수 있다.)
- SM이 갖는 자원 양과 스레드 블록을 처리하기 위해, 필요한 자원의 양에 따라 한 SM이 동시에 처리할 수 있는 스레드 블록 수를 결정
워프 & 스레드 -> SM 속의 CUDA 코어
- 스레드 블록의 스레드는 워프로 묶을 수 있다.
- 워프는 32개 스레드로 구성되며, 스레드 각각 CUDA 코어 하나에서 처리한다.
- 워프는 하나의 명령어에 의해 움직인다. GPU가 SIMT 아키텍처라는 말이 나온 이유이다.
- CUDA 코어 그룹마다 워프 스케줄러와 명령어 전달 유닛이 1개씩 있다.
스레드의 실행 문맥
워프 내 스레드들은 하나의 명령어에 의해 움직이지만, 각 스레드는 독립적으로 처리될 수 있다. (스레드만의 실행 문맥) 실행 문맥은 작업 상태의 기록이다. GPU에서 각 스레드는 자신만의 작업 상황을 저장한다. 실제 스레드의 실행 문맥은 레지스터로 관리되며, 중요한 GPU 아키텍처 특징은 스레드 블록 내 모든 워프가 SM 내부 레지스터 파일을 나누어 사용한다.
예시:
- 스레드 블록 내 512개 스레드가 있다.
- 레지스터 파일 내, 레지스터 갯수가 5,120개이다.
- 이 경우는 각 스레드는 10개의 레지스터를 사용한다. 이러한 실행 구조는 워프 처리에 대해 무비용 문맥 교환과 워프 분기라는 2가지 특성이 있다.
무비용 문맥 교환
- 현재 코어를 사용 중인 프로세스와, 다음 차례를 기다리는 프로세스
- CPU: CPU에서 문맥 교환이 발생할 때, 현재 실행 중인 프로세스의 레지스터 값과 프로그램 카운터를 저장하고, 다음에 실행할 프로세스 값을 로드한다. 이는 메모리 접근을 해야하기에, 시간이 소요된다.
- GPU: GPU에서는 각 SM 안에 다수의 워프가 존재한다. 워프는 다수의 스레드로 고성되며, 이 스레드들이 병렬로 실행된다.
- 문맥 교환 과정
- 레지스터 값 저장 및 로드: 문맥 교환에서는 현재 레지스터 값을 메모리에 저장하고, 다음 프로세스의 레지스터 값을 메모리에서 읽어와 레지스터에 저장하는 작업이 필요합니다. CPU에서는 이 과정이 상당히 비용이 많이 든다.
- GPU의 경우: SM (Streaming Multiprocessors): GPU의 SM은 다수의 워프를 포함하고 있으며, 여러 워프가 동시에 존재한다.
- 레지스터 파일: 각 워프는 고유의 레지스터 파일을 가지며, 레지스터 값은 각 워프 내에서 유지한다.
- 빠른 전환: GPU에서는 한 워프가 연산을 수행하는 동안 다른 워프가 메모리 접근을 하거나 대기 상태일 수 있습니다. 이렇게 다른 워프로 전환하는 과정이 매우 빠르게 이루어진다.
- 하드웨어 스케줄링: GPU의 하드웨어 스케줄러는 워프 간의 전환을 효율적으로 관리하여, 문맥 교환에 필요한 비용을 최소화한다.
워프 분기
GPU의 워프 내 스레드는 동일한 명령어로 동작하기 때문에 스레드들이 독립적으로 움직일 수 없다. 커널 내에서 분기가 발생할 경우, GPU는 각 분기를 순차적으로 처리한다. 이 과정에서 특정 분기를 따르는 스레드들을 먼저 처리하고, 다른 분기를 따르는 스레드들을 그 다음에 처리한다.
이때 한쪽 분기를 처리하는 동안 다른 분기를 따르는 스레드들은 idle 상태로 대기하게 되어 연산 자원이 낭비된다. 예를 들어, 분기가 두 가지 경로로 나뉘는 경우, 하나의 워프를 두 번 나누어 처리하므로 연산이 2배 느려지게 된다. 따라서 CUDA 알고리즘을 설계할 때에는 분기를 최대한 제거하거나 피하는 것이 좋다.
메모리 액세스 대기 시간 숨기기
데이터 처리는 “데이터 접근 - 데이터 연산"이라는 일련의 과정을 반복하는 것이다. 처리할 데이터를 메모리에서 Load/Store하기 위해 접근하는 동안 CUDA 코어는 아무 작업을 하지 않는다. 이 메모리 접근 작업이 끝날 때까지 연산 코어가 대기하는 시간을 메모리 접근 지연(Memory Access Latency)이라고 한다.
GPU의 병렬 처리 능력을 최대화하려면 이 메모리 접근 지연을 최소화해야 한다. 주요한 전략은 “CUDA 코어 수보다 많은 수의 스레드를 사용하는 것"이다. 실제로 CUDA 프로그램은 CUDA 코어 수에 비해 매우 많은 스레드를 사용하도록 정의한다. 이러한 전략은 한 스레드가 메모리 접근으로 대기하는 동안, 다른 스레드가 CUDA 코어를 사용하도록 처리하는 것이다. 이것이 가능한 이유는 GPU의 문맥 교환 비용이 거의 없기 때문이다.
알고리즘에 따라 메모리 접근 시간과 전환의 빈도는 다를 수 있다.
- I/O bounded: 스레드 수를 늘려, 데이터 I/O가 많아도 스레드들이 계속 작업할 수 있도록 한다.
- Compute-bounded: 스레드가 너무 많으면, 계산에서 병목이 생긴다.