GPU
화면이 120 Hz인 휴대폰에서 게임을 하면, 칩은 8.3 ms마다 화소 약 450만 개의 색을 새로 정해야 한다. 화소 하나마다 조명·그림자·텍스처를 계산하는 데 수백 번의 연산이 들어가니, 1초에 수천억 번의 계산이다. 4 GHz짜리 CPU 코어 몇 개로는 어림도 없다. 그런데 이 계산에는 특별한 성질이 있다. 화소 하나하나가 서로를 기다리지 않는다. 그렇다면 아주 빠른 일꾼 몇 명 대신, 느리지만 값싼 일꾼 수천 명을 고용하면 된다. 이것이 GPU(Graphics Processing Unit)의 출발점이다. 이 장에서는 삼각형 하나가 화소가 되는 과정에서 시작해, 수천 개의 스레드를 묶어 돌리는 SIMT, 메모리를 기다리는 시간을 숨기는 요령, 그리고 배터리로 도는 모바일 GPU만의 비결인 타일 기반 렌더링까지 차례로 만져 본다.
- 지연 시간 중심(CPU)과 처리량 중심(GPU) 설계의 차이를 작업 수와 의존성으로 설명할 수 있다.
- 그래픽 파이프라인(정점 → 삼각형 설정 → 래스터화 → 프래그먼트 → 블렌딩)과 엣지 함수 래스터화를 이해한다.
- 워프·SIMT 실행과 분기 발산이 효율을 떨어뜨리는 원리를 마스크로 따져 본다.
- 레지스터·공유 메모리가 점유율을 정하고, 점유율이 메모리 지연을 숨기는 원리를 리틀의 법칙으로 계산한다.
- 메모리 병합과 타일 기반 지연 렌더링(TBDR)이 DRAM 대역폭과 전력을 아끼는 이유를 수치로 비교한다.
빠른 일꾼 몇 명, 느린 일꾼 수천 명
CPU 코어는 지연 시간(Latency)을 줄이도록 만들어졌다. 명령어 하나의 결과를 최대한 빨리 내기 위해 분기 예측기, 비순차 실행 엔진, 큰 캐시에 면적을 쏟는다(4장). 그 결과 큰 코어 하나가 수 mm²를 차지하고, 실제 계산을 하는 ALU는 그 면적의 작은 일부다.
GPU는 반대로 처리량(Throughput), 즉 단위 시간에 끝내는 일의 양을 키우도록 만들어졌다. 일꾼 하나하나는 느리고 단순하다. 분기 예측도, 비순차 실행도 거의 없다. 대신 같은 면적에 ALU를 수십 배 더 넣는다. 일이 충분히 많고 서로 독립적이면 이 전략이 압도적으로 이긴다. 반대로 일이 적거나 앞 결과를 기다려야 하면 느린 일꾼은 그냥 느리다. 아래에서 작업 개수와 종류를 바꿔 두 팀을 경주시켜 보자.
그래픽은 GPU에 딱 맞는 일이다. 정점 수십만 개, 화소 수백만 개가 매 프레임 같은 프로그램(셰이더(Shader))을 각자의 데이터로 돌린다. 이런 성질을 데이터 병렬성(Data parallelism)이라 한다. 2007년 무렵부터는 이 구조를 그래픽이 아닌 일, 예컨대 행렬 곱이나 물리 시뮬레이션에도 쓰기 시작했다(GPGPU, CUDA). 오늘날 AI 학습이 GPU 위에서 도는 것도 같은 이유다.
GPU는 이렇게 약속한다. "일을 아주 많이, 서로 독립적으로 주면 엄청 빨리 끝내 주겠다. 대신 한 가지 일을 빨리 끝내 달라고는 하지 마라." 이 장의 모든 설계는 이 계약을 지키기 위한 장치다.
삼각형이 화소가 되기까지: 그래픽 파이프라인
3D 장면은 수많은 삼각형으로 이루어진다. GPU는 매 프레임 이 삼각형들을 화면 화소로 바꾸는데, 그 과정은 공장 컨베이어처럼 단계가 정해져 있다. 이를 그래픽 파이프라인(Graphics pipeline)이라 부른다.
이 중 래스터화(Rasterization)가 GPU 하드웨어의 핵심 트릭이다. 화소 중심 \(p\)가 삼각형 안에 있는지 판단하려고 GPU는 세 변마다 엣지 함수(Edge function)를 계산한다.
이 값은 변 \(ab\)와 점 \(p\)가 만드는 평행사변형의 넓이(부호 포함)다. 세 엣지 함수가 모두 같은 부호면 점은 삼각형 안이다. 곱셈 두 번과 뺄셈 몇 번이면 끝나고, 옆 화소로 한 칸 움직이면 값이 상수만큼만 변하므로 덧셈 하나로 갱신된다. 더 좋은 점은 세 값을 삼각형 넓이로 나누면 그대로 무게 중심 좌표(Barycentric coordinates)가 되어, 정점의 색·텍스처 좌표를 화소마다 보간하는 데 다시 쓰인다는 것이다. 아래에서 꼭짓점을 끌어 보자.
삼각형 래스터화 해부
가늘고 긴 삼각형을 만들어 보면 경계 상자 안의 대부분 화소가 헛검사된다. 그래서 하드웨어는 경계 상자를 작은 타일로 나누고, 타일 네 모서리의 엣지 함수만 보고 "완전히 밖" 타일을 통째로 건너뛴다. ⑤ 단계에서는 같은 화소를 두 번 칠하는 오버드로(Overdraw)가 생긴다. 불투명 물체라면 뒤에 가려질 화소를 칠하는 일은 순전히 낭비다. 이 낭비를 줄이는 방법이 이 장 뒷부분의 주인공이다.
한때 GPU에는 정점 셰이더 전용 유닛과 화소 셰이더 전용 유닛이 따로 있었다. 문제는 장면마다 두 일의 비율이 크게 다르다는 것이다. 2005~2006년(Xbox 360의 Xenos, GeForce 8800)부터는 같은 셰이더 코어가 정점이든 화소든 계산이든 다 맡는 통합 셰이더(Unified shader) 구조가 표준이 되었다. 비율을 바꿔 가며 그 이유를 확인해 보자.
워프: 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)이라 한다. 조건을 만족하는 스레드의 분포를 바꿔 가며 워프가 몇 사이클을 쓰는지 보자.
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\) 사이클을 메워야 한다.
그런데 워프를 무한정 올릴 수는 없다. 워프마다 레지스터 파일과 공유 메모리를 따로 떼어 줘야 하기 때문이다. 셰이더 코어 하나에 동시에 올라가 있는 워프 수를 최대치로 나눈 값을 점유율(Occupancy)이라 한다. 아래 시뮬레이터는 NVIDIA 계열 SM을 단순화했다. 레지스터 64K개(256 KB), 공유 메모리 96 KB, 워프 최대 64개, 블록 최대 16개, 스케줄러 4개. 커널의 자원 사용량을 바꿔 상주 워프 수를 정하고, 스케줄러 하나가 그 워프들을 어떻게 교대로 돌리는지 지켜보자.
점유율과 지연 숨기기
몇 가지 실험을 해 보자. 레지스터를 32개에서 64개로 늘리면 상주 워프가 절반으로 줄고, 지연이 다 숨겨지지 않아 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\)번째 원소를 읽으면, 같은 일을 하는데도 섹터를 훨씬 많이 가져와야 한다. 가져온 바이트 중 쓰는 것은 일부뿐이다.
이 때문에 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 화소 크기의 타일로 나누고, 두 단계로 그린다.
여기에 "지연"(Deferred)이 붙으면, 타일 안의 모든 삼각형 깊이를 먼저 확정한 다음 보이는 프래그먼트만 셰이딩한다(은면 제거, HSR). 오버드로가 아무리 커도 화소당 셰이딩은 거의 한 번이다. 아래 시뮬레이터에서 두 방식이 한 프레임에 DRAM을 얼마나 오가는지 비교해 보자.
즉시 모드 vs 타일 기반 렌더링 대역폭
오버드로를 올리면 IMR 트래픽은 거의 비례해서 늘지만 TBDR은 거의 그대로다. MSAA를 켜면 차이는 더 벌어진다. 4×MSAA의 샘플 데이터가 IMR에서는 전부 DRAM을 오가지만 TBDR에서는 타일 버퍼 안에서 해소되고 사라지기 때문이다. 대신 TBDR은 삼각형이 많을수록 비닝 데이터가 늘고, 타일이 작을수록 큰 삼각형이 여러 타일에 걸쳐 목록이 길어진다. 타일을 키우면 그 반대로 타일 버퍼 SRAM이 커진다. 칩 면적과 DRAM 트래픽 사이의 전형적인 맞바꿈이다.
삼각형이 수백만 개인 장면에서는 비닝 데이터 자체가 크고, 비닝 단계와 렌더 단계 사이에 프레임 하나만큼의 지연이 생긴다. 대역폭이 1 TB/s에 전력이 수백 W인 데스크톱에서는 IMR의 단순함이 더 낫다. 다만 최근 데스크톱 GPU도 내부적으로 화면을 작은 영역으로 나눠 캐시 안에서 처리하는 "타일 기반 래스터화"를 섞어 쓴다. 두 세계는 서로를 닮아 가고 있다.
숫자로 보는 모바일 GPU
GPU 성능을 말할 때 가장 흔히 쓰는 숫자는 초당 부동소수점 연산 수, FLOPS다. ALU 하나가 한 사이클에 곱셈-누산(FMA) 하나, 즉 연산 2개를 하므로
FP16(반정밀도)은 같은 ALU로 두 개씩 묶어 처리하는 경우가 많아 두 배가 된다. 아래에서 직접 GPU를 설계해 보자. 화면 화소 하나당 프레임마다 몇 번의 연산을 쓸 수 있는지도 함께 계산된다.
| 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장에서 이어진다.
핵심 정리
- CPU는 지연 시간을, GPU는 처리량을 최적화한다. GPU는 서로 독립적인 일이 많을 때만 이긴다.
- 래스터화는 엣지 함수 세 개의 부호로 화소가 삼각형 안인지 판정하고, 같은 값으로 무게 중심 보간까지 한다.
- SIMT는 워프(32 스레드 등)에 명령 하나를 내려 제어 비용을 나눈다. 워프 안에서 분기가 갈리면 두 길을 차례로 실행해 효율이 떨어진다.
- GPU는 메모리 지연을 워프 교대로 숨긴다. 필요한 워프 수 ≈ 1 + L/(c+1). 상주 워프 수(점유율)는 레지스터·공유 메모리 사용량이 정한다.
- 워프의 주소가 연속이면 섹터 몇 개로 병합된다. 모바일 GPU는 타일 기반 렌더링으로 깊이·MSAA·오버드로 트래픽을 칩 안에 가둬 DRAM 대역폭과 전력을 아낀다.
확인 퀴즈
Q1. 다음 중 GPU보다 CPU에서 더 빨리 끝날 가능성이 가장 큰 일은?
Q2. 엣지 함수 세 값을 삼각형의 (부호 있는) 넓이의 두 배로 나누면 얻는 것은?
Q3. 워프 32 스레드 중 8개만 if 쪽(명령 6개), 나머지가 else 쪽(명령 6개)을 실행한다. 분기 부분의 SIMD 효율은?
Q4. 레지스터 64K개인 SM에서 스레드당 레지스터를 32개에서 128개로 늘리면, 다른 제한이 없을 때 상주 가능한 최대 워프 수는?
Q5. 워프의 스레드 i가 float 배열의 원소 [2i]를 읽는다(시작 정렬됨). 32바이트 섹터 몇 개를 가져와야 하는가?
Q6. 타일 기반 렌더링이 4×MSAA에서 특히 유리한 이유는?