Chapter 09

GPU

화면이 120 Hz인 휴대폰에서 게임을 하면, 칩은 8.3 ms마다 화소 약 450만 개의 색을 새로 정해야 한다. 화소 하나마다 조명·그림자·텍스처를 계산하는 데 수백 번의 연산이 들어가니, 1초에 수천억 번의 계산이다. 4 GHz짜리 CPU 코어 몇 개로는 어림도 없다. 그런데 이 계산에는 특별한 성질이 있다. 화소 하나하나가 서로를 기다리지 않는다. 그렇다면 아주 빠른 일꾼 몇 명 대신, 느리지만 값싼 일꾼 수천 명을 고용하면 된다. 이것이 GPU(Graphics Processing Unit)의 출발점이다. 이 장에서는 삼각형 하나가 화소가 되는 과정에서 시작해, 수천 개의 스레드를 묶어 돌리는 SIMT, 메모리를 기다리는 시간을 숨기는 요령, 그리고 배터리로 도는 모바일 GPU만의 비결인 타일 기반 렌더링까지 차례로 만져 본다.

빠른 일꾼 몇 명, 느린 일꾼 수천 명

CPU 코어는 지연 시간(Latency)을 줄이도록 만들어졌다. 명령어 하나의 결과를 최대한 빨리 내기 위해 분기 예측기, 비순차 실행 엔진, 큰 캐시에 면적을 쏟는다(4장). 그 결과 큰 코어 하나가 수 mm²를 차지하고, 실제 계산을 하는 ALU는 그 면적의 작은 일부다.

GPU는 반대로 처리량(Throughput), 즉 단위 시간에 끝내는 일의 양을 키우도록 만들어졌다. 일꾼 하나하나는 느리고 단순하다. 분기 예측도, 비순차 실행도 거의 없다. 대신 같은 면적에 ALU를 수십 배 더 넣는다. 일이 충분히 많고 서로 독립적이면 이 전략이 압도적으로 이긴다. 반대로 일이 적거나 앞 결과를 기다려야 하면 느린 일꾼은 그냥 느리다. 아래에서 작업 개수와 종류를 바꿔 두 팀을 경주시켜 보자.

CPU 완료 시간—
GPU 완료 시간—
승자—
그림 9-1. 만져 보기CPU 팀은 작업 하나를 1틱에 끝내는 일꾼 8명, GPU 팀은 작업 하나에 6틱이 걸리는 일꾼 512명이다. GPU에는 일을 맡기는 준비 시간(커널 실행·데이터 전달) 3틱이 더 붙는다. 작업이 64개 남짓을 넘으면 GPU가 이기기 시작하고, 4,096개면 열 배 가까이 앞선다. 연쇄 작업으로 바꾸면 일꾼 수가 아무 소용이 없다.

그래픽은 GPU에 딱 맞는 일이다. 정점 수십만 개, 화소 수백만 개가 매 프레임 같은 프로그램(셰이더(Shader))을 각자의 데이터로 돌린다. 이런 성질을 데이터 병렬성(Data parallelism)이라 한다. 2007년 무렵부터는 이 구조를 그래픽이 아닌 일, 예컨대 행렬 곱이나 물리 시뮬레이션에도 쓰기 시작했다(GPGPU, CUDA). 오늘날 AI 학습이 GPU 위에서 도는 것도 같은 이유다.

GPU의 계약

GPU는 이렇게 약속한다. "일을 아주 많이, 서로 독립적으로 주면 엄청 빨리 끝내 주겠다. 대신 한 가지 일을 빨리 끝내 달라고는 하지 마라." 이 장의 모든 설계는 이 계약을 지키기 위한 장치다.

삼각형이 화소가 되기까지: 그래픽 파이프라인

3D 장면은 수많은 삼각형으로 이루어진다. GPU는 매 프레임 이 삼각형들을 화면 화소로 바꾸는데, 그 과정은 공장 컨베이어처럼 단계가 정해져 있다. 이를 그래픽 파이프라인(Graphics pipeline)이라 부른다.

정점 셰이더정점마다 좌표 변환(모델→카메라→화면)과 조명 값 계산.
삼각형 설정정점 셋을 묶고, 화면 밖·뒷면 삼각형을 버리고, 엣지 방정식을 준비.
래스터화삼각형이 덮는 화소(프래그먼트)를 찾아낸다. 고정 회로.
프래그먼트 셰이더화소마다 텍스처를 읽고 색을 계산.
깊이 테스트·블렌딩앞에 있는 것만 남기고, 반투명은 섞어서 프레임 버퍼에 쓴다.

이 중 래스터화(Rasterization)가 GPU 하드웨어의 핵심 트릭이다. 화소 중심 \(p\)가 삼각형 안에 있는지 판단하려고 GPU는 세 변마다 엣지 함수(Edge function)를 계산한다.

$$E_{ab}(p) = (b_x - a_x)(p_y - a_y) - (b_y - a_y)(p_x - a_x)$$

이 값은 변 \(ab\)와 점 \(p\)가 만드는 평행사변형의 넓이(부호 포함)다. 세 엣지 함수가 모두 같은 부호면 점은 삼각형 안이다. 곱셈 두 번과 뺄셈 몇 번이면 끝나고, 옆 화소로 한 칸 움직이면 값이 상수만큼만 변하므로 덧셈 하나로 갱신된다. 더 좋은 점은 세 값을 삼각형 넓이로 나누면 그대로 무게 중심 좌표(Barycentric coordinates)가 되어, 정점의 색·텍스처 좌표를 화소마다 보간하는 데 다시 쓰인다는 것이다. 아래에서 꼭짓점을 끌어 보자.

STEP

삼각형 래스터화 해부

검사한 화소 (경계 상자)—
덮인 화소—
검사 효율—
겹쳐 그린 화소—
꼭짓점(동그라미)을 끌어 삼각형 모양을 바꾸고, 화소 위에 마우스를 올리면(터치하면) 그 화소 중심의 엣지 함수 세 값이 나온다. ⑤ 단계에서는 반투명 삼각형이 하나 더 나타나며 그 꼭짓점도 끌 수 있다. 실제 GPU는 화소를 2×2 묶음(쿼드)이나 8×8 타일 단위로 한꺼번에 검사하고, 경계에 걸친 화소는 "왼쪽 위 규칙"으로 한쪽 삼각형에만 준다.

가늘고 긴 삼각형을 만들어 보면 경계 상자 안의 대부분 화소가 헛검사된다. 그래서 하드웨어는 경계 상자를 작은 타일로 나누고, 타일 네 모서리의 엣지 함수만 보고 "완전히 밖" 타일을 통째로 건너뛴다. ⑤ 단계에서는 같은 화소를 두 번 칠하는 오버드로(Overdraw)가 생긴다. 불투명 물체라면 뒤에 가려질 화소를 칠하는 일은 순전히 낭비다. 이 낭비를 줄이는 방법이 이 장 뒷부분의 주인공이다.

한때 GPU에는 정점 셰이더 전용 유닛과 화소 셰이더 전용 유닛이 따로 있었다. 문제는 장면마다 두 일의 비율이 크게 다르다는 것이다. 2005~2006년(Xbox 360의 Xenos, GeForce 8800)부터는 같은 셰이더 코어가 정점이든 화소든 계산이든 다 맡는 통합 셰이더(Unified shader) 구조가 표준이 되었다. 비율을 바꿔 가며 그 이유를 확인해 보자.

분리형 프레임 시간—
통합형 프레임 시간—
분리형 유닛 사용률—
그림 9-2. 만져 보기셰이더 유닛 16개, 프레임마다 16유닛·시간어치의 일. 분리형은 정점 전용 4개와 화소 전용 12개로 나뉘어 있다. 정점 비율이 정확히 25%일 때만 모두 바쁘고, 그 밖에서는 한쪽이 놀며 다른 쪽을 기다린다. 통합형은 비율과 상관없이 늘 1.0이다.

워프: 32개의 스레드가 한 몸처럼

GPU 셰이더 코어 안을 들여다보면 ALU가 하나씩 따로 움직이지 않는다. 명령어 인출·해독기 하나가 ALU 수십 개를 거느리고, 같은 명령을 동시에 내린다. NVIDIA는 이렇게 한 묶음으로 움직이는 스레드 32개를 워프(Warp)라 부른다(AMD는 웨이브프런트, 64 또는 32개. Arm Mali는 16개). 프로그래머는 스레드 하나의 코드만 쓰지만, 하드웨어는 32개를 한 명령으로 돌린다. 이 방식을 SIMT(Single Instruction, Multiple Threads)라 한다. 명령 인출·해독 비용을 32개 스레드가 나눠 내니 연산당 에너지가 크게 준다(1장의 효율 사다리).

문제는 if다. 같은 워프의 스레드들이 서로 다른 길로 가려 하면, 하드웨어는 두 길을 차례로 실행한다. 한쪽 길을 실행하는 동안 반대편 스레드는 활성 마스크(Active mask)로 꺼 둔다. 이를 분기 발산(Branch divergence)이라 한다. 조건을 만족하는 스레드의 분포를 바꿔 가며 워프가 몇 사이클을 쓰는지 보자.

워프가 쓴 사이클—
스레드당 실제 명령—
SIMD 효율—
그림 9-3. 만져 보기가로는 워프의 스레드(레인) 32개, 세로는 시간(명령 하나 = 한 줄)이다. 색칠된 칸은 그 레인이 실제로 일한 것, 빗금은 마스크로 꺼져 자리만 차지한 것이다. "짝·홀 번갈아"는 조건 비율이 50%로 고정된다. 참인 스레드가 0개나 32개면 한쪽 길을 통째로 건너뛰어 발산이 사라진다.
조건이 참인 스레드가 단 1개라면 효율은 어떻게 될까?

if 쪽 4개, else 쪽 3개 명령. 스레드 31개는 else로, 1개만 if로 간다.

답 보기

워프는 두 길을 모두 실행해야 하므로 사이클 수는 50% 비율일 때와 똑같다(2 + 4 + 3 + 2 = 11). 스레드 한 개를 위해 31개 레인이 4사이클 동안 논다. 효율은 비율이 아니라 "워프 안에 두 종류가 섞였는가"로 결정된다. 그래서 GPU 프로그래머는 데이터를 미리 정렬해 같은 길을 가는 스레드를 같은 워프에 모은다. "앞쪽 스레드" 패턴에서 참인 스레드 수를 32의 배수로 맞추는 것이 실전에서는 워프 단위 정렬에 해당한다.

스레드는 계층으로 묶인다. 워프 여러 개가 스레드 블록(Thread block, Workgroup)을 이루고, 한 블록은 한 셰이더 코어(NVIDIA의 SM(Streaming Multiprocessor), Arm의 셰이더 코어, Apple의 GPU 코어)에서만 돈다. 같은 블록의 스레드는 코어 안의 공유 메모리(Shared memory, Local memory)로 데이터를 주고받고 동기화할 수 있다. 블록 수천 개로 이루어진 격자(Grid) 전체가 GPU에 던지는 일 하나, 즉 커널(Kernel)이다.

기다림을 숨기는 법: 워프 교대와 점유율

GPU에는 CPU 같은 큰 캐시가 없다. 텍스처나 버퍼를 DRAM에서 읽으면 수백 사이클이 걸린다. CPU라면 비순차 실행과 큰 캐시로 이 기다림을 줄이려 하겠지만, GPU는 줄이지 않고 숨긴다. 한 워프가 메모리를 기다리는 동안 스케줄러가 다른 워프의 명령을 내보낸다. 워프마다 레지스터를 따로 갖고 있으므로 이 교대에는 비용이 0 사이클이다. CPU의 문맥 전환과 달리 레지스터를 저장하고 복원할 필요가 없다.

그렇다면 워프가 몇 개 있어야 기다림을 다 숨길 수 있을까? 워프 하나가 명령 \(c\)개를 내고 나서 지연 \(L\) 사이클짜리 메모리 접근을 한다면, 그동안 스케줄러를 바쁘게 하려면 다른 워프들이 \(L\) 사이클을 메워야 한다.

$$W_{\text{필요}} \;\approx\; 1 + \frac{L}{c+1} \qquad\text{(리틀의 법칙: 동시에 처리 중인 일 = 지연 × 처리량)}$$

그런데 워프를 무한정 올릴 수는 없다. 워프마다 레지스터 파일과 공유 메모리를 따로 떼어 줘야 하기 때문이다. 셰이더 코어 하나에 동시에 올라가 있는 워프 수를 최대치로 나눈 값을 점유율(Occupancy)이라 한다. 아래 시뮬레이터는 NVIDIA 계열 SM을 단순화했다. 레지스터 64K개(256 KB), 공유 메모리 96 KB, 워프 최대 64개, 블록 최대 16개, 스케줄러 4개. 커널의 자원 사용량을 바꿔 상주 워프 수를 정하고, 스케줄러 하나가 그 워프들을 어떻게 교대로 돌리는지 지켜보자.

SIMULATOR

점유율과 지연 숨기기

SM 자원 사용 (빨강 = 제한 요인)
상주 워프 / 점유율—
스케줄러당 워프—
필요 워프 (식)—
ALU 사용률—
위 그림의 각 줄이 스케줄러 하나에 배정된 워프다. 진한 칸 = 명령을 낸 사이클, 연한 칸 = 준비됐지만 차례를 기다림, 빈 칸 = 메모리 대기. 아래 곡선은 50사이클 이동 평균 ALU 사용률이다. 스케줄러는 매 사이클 준비된 워프 하나에서 명령 하나를 낸다(가장 오래 기다린 워프 우선). 워프들은 서로 다른 시점에 출발한다고 가정했다. 레지스터는 워프당 256개 단위로 할당한다. 실제 GPU는 한 워프 안에서도 독립 명령을 겹쳐 내고(ILP), 캐시 적중 시 지연이 훨씬 짧다.

몇 가지 실험을 해 보자. 레지스터를 32개에서 64개로 늘리면 상주 워프가 절반으로 줄고, 지연이 다 숨겨지지 않아 ALU가 놀기 시작한다. 반대로 로드 사이의 계산을 늘리면(산술 집약도를 높이면) 적은 워프로도 충분하다. 공유 메모리를 블록당 많이 쓰면 블록이 몇 개밖에 못 올라가는 것도 확인할 수 있다.

지연 400사이클, 로드 사이 명령 12개. 스케줄러 하나에 워프가 몇 개 있어야 ALU가 쉬지 않을까?

식에 넣어 계산해 보고, 시뮬레이터로 확인해 보자. 스케줄러당 워프는 최대 16개다.

답 보기

\(1 + 400/13 \approx 31.8\), 약 32개가 필요하다. 그런데 스케줄러 하나가 가질 수 있는 워프는 최대 16개다. 즉 이 커널은 점유율 100%로도 지연을 다 숨길 수 없고, ALU 사용률은 대략 \(16 \times 13 / 413 \approx 50\%\)에 머문다. 이것이 메모리 한계 커널이다. 해결책은 워프를 늘리는 것이 아니라 메모리 접근 자체를 줄이는 것, 즉 공유 메모리에 데이터를 올려 재사용하는 것이다(10장의 루프라인에서 다시 만난다).

점유율이 높을수록 늘 좋은가?

아니다. 레지스터를 적게 쓰게 강제하면 컴파일러가 값을 메모리로 내보내는 스필(Spill)이 생겨 오히려 느려질 수 있다. 워프당 독립 명령이 많으면(ILP) 점유율 30~50%로도 충분히 빠르다. 점유율은 지연을 숨기는 수단일 뿐 목표가 아니다.

메모리 병합: 32개의 요청을 한 번에

워프의 32개 스레드가 동시에 메모리를 읽으면 요청도 32개일까? GPU의 메모리 유닛은 같은 워프의 주소들을 모아 같은 32바이트 섹터에 들어가는 것끼리 한 번의 트랜잭션으로 합친다. 이를 메모리 병합(Memory coalescing)이라 한다. 스레드 \(i\)가 배열의 \(i\)번째 원소를 읽으면 4바이트 × 32 = 128바이트, 섹터 4개로 끝난다. 하지만 스레드 \(i\)가 \(i \times s\)번째 원소를 읽으면, 같은 일을 하는데도 섹터를 훨씬 많이 가져와야 한다. 가져온 바이트 중 쓰는 것은 일부뿐이다.

32 B 섹터 수—
가져온 바이트 / 쓴 바이트—
버스 효율—
그림 9-4. 만져 보기한 줄이 128바이트 캐시 라인, 굵은 테두리가 32바이트 섹터다. 색칠된 칸이 워프의 스레드 32개가 실제로 원하는 원소(색은 스레드 번호). 보폭 1이면 4섹터, 보폭 2면 8섹터, 보폭 8 이상이면 스레드마다 섹터 하나씩 32섹터가 필요하다. 시작 위치가 어긋나도 섹터가 하나 더 붙는다.

이 때문에 GPU 프로그래머는 데이터를 "구조체의 배열"(AoS)이 아니라 "배열의 구조체"(SoA)로 저장한다. 점의 x, y, z를 한 구조체로 묶으면 스레드들이 x만 읽을 때 보폭 3이 되지만, x만 모은 배열을 따로 두면 보폭 1이 된다. 행렬 전치처럼 피할 수 없는 경우에는 먼저 공유 메모리에 병합된 방식으로 읽어 들인 뒤 공유 메모리 안에서 순서를 바꾼다.

GPU의 메모리 계층을 정리하면 이렇다. 스레드마다 레지스터(가장 빠르고 가장 크다. SM 하나에 256 KB로 L1보다 크다), 블록이 함께 쓰는 공유 메모리/L1(수십~수백 KB), GPU 전체의 L2(모바일 수 MB), 그리고 SoC 전체가 함께 쓰는 시스템 캐시와 DRAM이다(5장, 7장). 모바일 SoC에서 GPU는 CPU와 같은 LPDDR을 나눠 쓰므로 데이터를 복사하지 않고 공유할 수 있지만, 대역폭 60~80 GB/s를 모두가 나눠 가져야 한다. 데스크톱 그래픽 카드가 GDDR로 1 TB/s를 쓰는 것과 비교하면 열 배 이상 적다. 그래서 모바일 GPU는 DRAM 접근을 줄이는 데 집착한다.

모바일 GPU의 비밀: 타일 기반 지연 렌더링

데스크톱 GPU의 전통적인 방식은 즉시 모드 렌더링(IMR, Immediate-mode rendering)이다. 삼각형이 들어오는 순서대로 래스터화하고, 화소마다 깊이 버퍼를 읽어 비교하고, 통과하면 색과 깊이를 프레임 버퍼에 쓴다. 프레임 버퍼는 수십 MB라 DRAM에 있다. 같은 화소를 여러 번 덮을수록(오버드로) DRAM 왕복이 늘어난다.

모바일 GPU 대부분(Arm Mali, Apple, Imagination PowerVR, Qualcomm Adreno의 binning 모드)은 다른 길을 택했다. 화면을 16×16이나 32×32 화소 크기의 타일로 나누고, 두 단계로 그린다.

비닝 단계모든 정점을 변환한 뒤, 삼각형마다 어느 타일에 걸치는지 목록(빈)을 만들어 DRAM에 쓴다.
타일 렌더링 단계타일 하나씩, 그 타일에 걸친 삼각형만 읽어 칩 안의 작은 타일 버퍼(SRAM)에서 깊이 테스트·셰이딩·블렌딩을 끝낸다.
타일 기록완성된 색만 DRAM 프레임 버퍼에 한 번 쓴다. 깊이와 MSAA 샘플은 칩 안에서 버린다.

여기에 "지연"(Deferred)이 붙으면, 타일 안의 모든 삼각형 깊이를 먼저 확정한 다음 보이는 프래그먼트만 셰이딩한다(은면 제거, HSR). 오버드로가 아무리 커도 화소당 셰이딩은 거의 한 번이다. 아래 시뮬레이터에서 두 방식이 한 프레임에 DRAM을 얼마나 오가는지 비교해 보자.

SIMULATOR

즉시 모드 vs 타일 기반 렌더링 대역폭

타일 크기
해상도 · 주사율
IMR DRAM 트래픽—
TBDR DRAM 트래픽—
DRAM 전력 차이 (약)—
타일 버퍼 SRAM—
교육용 모델. IMR: 프래그먼트 샘플마다 깊이 읽기 4 B, 앞에 있는 것(무작위 순서면 화소당 약 \(1+\frac12+\cdots+\frac1d\)개)은 깊이·색 쓰기 8 B, MSAA면 해소(resolve) 읽기·쓰기 추가. TBDR: 삼각형마다 변환된 위치 약 16 B와 걸친 타일마다 목록 항목 약 4 B를 쓰고 다시 읽음, 최종 색 4 B/화소만 기록. 두 방식 모두 정점 입력 32 B/삼각형, 텍스처 읽기는 같다고 보고 뺐다. 실제 GPU는 캐시와 프레임 버퍼 압축(AFBC, UBWC 등)으로 수치가 더 작다. DRAM 에너지는 비트당 약 15 pJ(PHY 포함)로 잡았다.

오버드로를 올리면 IMR 트래픽은 거의 비례해서 늘지만 TBDR은 거의 그대로다. MSAA를 켜면 차이는 더 벌어진다. 4×MSAA의 샘플 데이터가 IMR에서는 전부 DRAM을 오가지만 TBDR에서는 타일 버퍼 안에서 해소되고 사라지기 때문이다. 대신 TBDR은 삼각형이 많을수록 비닝 데이터가 늘고, 타일이 작을수록 큰 삼각형이 여러 타일에 걸쳐 목록이 길어진다. 타일을 키우면 그 반대로 타일 버퍼 SRAM이 커진다. 칩 면적과 DRAM 트래픽 사이의 전형적인 맞바꿈이다.

그럼 데스크톱 GPU는 왜 IMR을 쓸까?

삼각형이 수백만 개인 장면에서는 비닝 데이터 자체가 크고, 비닝 단계와 렌더 단계 사이에 프레임 하나만큼의 지연이 생긴다. 대역폭이 1 TB/s에 전력이 수백 W인 데스크톱에서는 IMR의 단순함이 더 낫다. 다만 최근 데스크톱 GPU도 내부적으로 화면을 작은 영역으로 나눠 캐시 안에서 처리하는 "타일 기반 래스터화"를 섞어 쓴다. 두 세계는 서로를 닮아 가고 있다.

숫자로 보는 모바일 GPU

GPU 성능을 말할 때 가장 흔히 쓰는 숫자는 초당 부동소수점 연산 수, FLOPS다. ALU 하나가 한 사이클에 곱셈-누산(FMA) 하나, 즉 연산 2개를 하므로

$$\text{최대 FLOPS} = 2 \times N_{\text{ALU}} \times f_{\text{clk}}$$

FP16(반정밀도)은 같은 ALU로 두 개씩 묶어 처리하는 경우가 많아 두 배가 된다. 아래에서 직접 GPU를 설계해 보자. 화면 화소 하나당 프레임마다 몇 번의 연산을 쓸 수 있는지도 함께 계산된다.

최대 성능—
QHD+ 120 Hz 화소당 연산—
DRAM 1 B당 연산 (70 GB/s)—
그림 9-5. 만져 보기막대는 아래 표의 대략값과 내 설계(강조색)를 로그 눈금으로 비교한다. 모바일 GPU는 ALU 수가 적은 대신 클럭을 낮게(1 GHz 안팎) 잡는다. 전압을 낮출 수 있어 같은 연산을 더 적은 에너지로 하기 때문이다(12장). 오른쪽 아래 숫자가 크다는 것은, DRAM에서 1바이트를 가져올 때마다 그만큼 계산을 해야 ALU가 놀지 않는다는 뜻이다.
GPU (연도)FP32 ALU클럭FP32 성능메모리 대역폭전력
Apple A17 Pro GPU, 6코어 (2023)약 768약 1.4 GHz약 2.1 TFLOPS약 51 GB/s (공유)수 W
Qualcomm Adreno 750 (2023)약 1,536약 0.9 GHz약 2.8 TFLOPS약 77 GB/s (공유)수 W
Apple M3 GPU, 10코어 (2023)약 1,280약 1.4 GHz약 3.6 TFLOPS약 100 GB/s (공유)약 10~20 W
NVIDIA GeForce RTX 4090 (2022)16,384약 2.5 GHz약 83 TFLOPS약 1,008 GB/s약 450 W
NVIDIA H100 SXM (2022)16,896약 2.0 GHz약 67 TFLOPS약 3,350 GB/s약 700 W

표의 모바일 수치는 제조사가 ALU 수를 공개하지 않는 경우가 많아 분석 자료를 바탕으로 한 추정값이다. 눈여겨볼 것은 비율이다. 휴대폰 GPU의 FP32 성능은 데스크톱 최고급의 1/30 정도지만, 전력은 1/100 이하다. 또 TFLOPS 대비 메모리 대역폭이 매우 적다. 그래서 모바일 게임은 화소당 연산을 아끼고(해상도를 낮춰 그린 뒤 업스케일링), 텍스처를 압축하고(ASTC), TBDR로 DRAM 왕복을 줄인다. GPU는 행렬 곱도 잘하므로 AI에도 쓰이지만, 휴대폰에서는 같은 일을 훨씬 적은 전력으로 하는 NPU가 따로 있다. 그 이야기는 10장에서 이어진다.

핵심 정리

  1. CPU는 지연 시간을, GPU는 처리량을 최적화한다. GPU는 서로 독립적인 일이 많을 때만 이긴다.
  2. 래스터화는 엣지 함수 세 개의 부호로 화소가 삼각형 안인지 판정하고, 같은 값으로 무게 중심 보간까지 한다.
  3. SIMT는 워프(32 스레드 등)에 명령 하나를 내려 제어 비용을 나눈다. 워프 안에서 분기가 갈리면 두 길을 차례로 실행해 효율이 떨어진다.
  4. GPU는 메모리 지연을 워프 교대로 숨긴다. 필요한 워프 수 ≈ 1 + L/(c+1). 상주 워프 수(점유율)는 레지스터·공유 메모리 사용량이 정한다.
  5. 워프의 주소가 연속이면 섹터 몇 개로 병합된다. 모바일 GPU는 타일 기반 렌더링으로 깊이·MSAA·오버드로 트래픽을 칩 안에 가둬 DRAM 대역폭과 전력을 아낀다.

확인 퀴즈

Q1. 다음 중 GPU보다 CPU에서 더 빨리 끝날 가능성이 가장 큰 일은?

연결 리스트 순회는 매 단계가 앞 결과(다음 포인터)를 기다리는 연쇄 작업이라 병렬성이 없다. 느린 일꾼 수천 명은 쓸모가 없고, 빠른 일꾼 한 명이 낫다.

Q2. 엣지 함수 세 값을 삼각형의 (부호 있는) 넓이의 두 배로 나누면 얻는 것은?

각 엣지 함수는 점과 한 변이 만드는 작은 삼각형 넓이의 두 배다. 전체로 나누면 합이 1인 무게 중심 좌표가 되고, 색·텍스처 좌표·깊이를 보간하는 가중치로 쓰인다.

Q3. 워프 32 스레드 중 8개만 if 쪽(명령 6개), 나머지가 else 쪽(명령 6개)을 실행한다. 분기 부분의 SIMD 효율은?

두 길을 차례로 실행하므로 12사이클 × 32레인 = 384 슬롯 중 실제 일은 8×6 + 24×6 = 192. 효율 50%다. 길이가 같으면 비율과 상관없이 50%가 된다.

Q4. 레지스터 64K개인 SM에서 스레드당 레지스터를 32개에서 128개로 늘리면, 다른 제한이 없을 때 상주 가능한 최대 워프 수는?

워프 하나가 128 × 32 = 4,096개 레지스터를 쓰므로 65,536 / 4,096 = 16개. 점유율이 1/4로 떨어져 메모리 지연을 숨길 워프가 줄어든다.

Q5. 워프의 스레드 i가 float 배열의 원소 [2i]를 읽는다(시작 정렬됨). 32바이트 섹터 몇 개를 가져와야 하는가?

주소 범위가 0~252바이트로 256바이트에 걸치므로 섹터 8개. 가져온 256바이트 중 128바이트만 쓰니 효율 50%다.

Q6. 타일 기반 렌더링이 4×MSAA에서 특히 유리한 이유는?

IMR에서는 샘플 단위 색·깊이 버퍼가 DRAM에 있어 트래픽이 샘플 수만큼 는다. TBDR은 타일 버퍼 안에서 해소한 뒤 화소당 4바이트만 내보낸다.