GPGPU 아키텍처와 CUDA 프로그래밍의 핵심: 워프(Warp) 스케줄링과 메모리 트랜잭션 최적화
팀 내 세미나를 준비하면서 여러 논문과 레퍼런스를 뒤져봤는데, 한글로 속 시원하게 정리된 곳이 없더군요. 그래서 제 경험을 녹여 직접 작성해 봤습니다.
데이터 사이언스와 인공지능의 황금기를 견인한 1등 공신은 수학 알고리즘을 현실로 끌어낸 그래픽 처리 장치, 즉 GPU입니다. 초기에 단지 픽셀 렌더링에만 사용되던 일방향 파이프라인 형태의 GPU를 범용 연산기(GPGPU)로 환골탈태시킨 결정적 전환점이 바로 NVIDIA의 CUDA(Compute Unified Device Architecture) 플랫폼의 등장입니다. CPU가 소수의 코어에 거대한 L3 캐시와 복잡한 분기 예측기(Branch Predictor)를 집약하여 개별 스레드의 지연 시간을 극도로 줄이는 철학(Low Latency)을 추구한다면, GPU는 캐시 따위의 사치를 버리고 대신 수천 개의 아주 단순한 ALU(산술 논리 장치)를 때려 박아 초당 엄청난 처리량(High Throughput)으로 데이터를 갈아버리는 철학을 채택했습니다. 이 병렬성의 단위를 CUDA 프로그래밍에서는 스레드 계층으로 추상화하며, 스레드 블록 단위로 할당되는 스트리밍 멀티프로세서(SM)가 GPU의 심장 역할을 수행합니다.
GPU 연산 효율성의 절대적인 열쇠는 하드웨어 인스트럭션 실행 단위인 32개의 스레드 그룹, 즉 워프(Warp)에 있습니다. GPU는 SIMT(Single Instruction Multiple Thread) 아키텍처이므로 워프 안의 32개 스레드는 무조건 정확히 동일한 기계어 명령어를 각자의 고유 데이터에 병렬적으로 실행해야 합니다. 만약 C/C++ 소스코드에서 IF-ELSE 조건문이 등장하여 워프 내의 16개 스레드는 IF 문을 타고 나머지 16개는 ELSE 문을 타는 분기 발산(Warp Divergence)이 일어난다면 끔찍한 성능 붕괴가 발생합니다. 하드웨어적으로 동시에 두 흐름을 실행할 물리적 분배기가 없으므로, GPU는 어쩔 수 없이 IF 로직을 실행할 때 16개 스레드를 정지(Masking)시켜 놓고 사이클을 허비하며, 그 후 다시 앞선 스레드를 멈추고 직렬 형태로 ELSE 문을 실행해야만 합니다. 따라서 최고 수준의 쿠다 엔지니어는 데이터의 편중된 조건을 설계 단계에서 비트 연산 마스킹이나 수식으로 우회하여 알고리즘 내의 모든 브랜치를 의도적으로 말살시키는 극단적인 최적화를 구사합니다.
연산 낭비 이상으로 치명적인 것은 글로벌 메모리 병목입니다. 디바이스의 VRAM과 연산기 사이는 아주 넓은 대역폭을 가지지만 반대로 매우 긴 왕복 지연 시간을 동반합니다. 32개의 스레드가 메모리를 요청할 때 그들이 요구하는 물리적 메모리 주소들이 연속적이게 배열되어 있다면, 하드웨어는 이를 단일 트랜잭션인 합체 메모리 호출(Coalesced Memory Access)로 묶어 파이프라인 지연 없이 한방에 처리합니다. 반대로 데이터가 연속되지 않고 난수처럼 흩뿌려져 있다면 32번의 개별적인 순차 I/O가 발생하여 병렬 처리의 이점이 통째로 증발합니다. 이를 해결하기 위해 온칩 고속 메모리인 공유 메모리(Shared Memory)를 L1 캐시처럼 프로그래머가 코드 상에서 수동으로 명시적 할당 제어하고, 행렬의 좌표를 수열 맵핑을 통해 물리 메모리의 순서에 강제로 맞춤으로써 성능을 수십 배 끌어올리는 하드웨어 의존적 프로그래밍이야말로 하드웨어 엑셀러레이팅의 극의라 할 수 있습니다.