28.35 GPU 기반 텐서 연산의 병렬화 원리와 CUDA 커널 매핑
1. 텐서 연산이 GPU에 적합한 이유
딥러닝의 텐서 연산은 본질적으로 대량의 동일한 산술 연산을 서로 독립적인 데이터 원소들에 대해 적용하는 형태이다. 이러한 계산 패턴은 단일 명령어 다중 데이터(Single Instruction, Multiple Data, SIMD) 또는 단일 명령어 다중 스레드(Single Instruction, Multiple Threads, SIMT) 모형의 실행 환경에서 가장 효율적으로 처리되며, 그래픽 처리 장치(Graphics Processing Unit, GPU)는 정확히 이러한 모형을 대규모로 구현한 장치이다. 일반 중앙 처리 장치(CPU)가 소수의 강력한 코어로 복잡한 제어 흐름을 처리하는 데 최적화되어 있다면, GPU는 수천 개의 단순한 산술 코어를 활용하여 동일한 연산을 거대한 데이터 집합에 동시에 적용하는 데 최적화되어 있다.
행렬 곱셈, 합성곱, 원소별 연산, 축소(Reduction) 연산, 활성 함수 적용 등 딥러닝의 모든 표준 연산은 데이터 의존성이 국소적이거나 결합 법칙을 만족하는 합산으로 환원될 수 있으므로, 이론적으로 매우 높은 병렬성을 가진다. 따라서 GPU 기반 가속은 단순한 공학적 선택이 아니라, 텐서 연산의 본질적 구조와 GPU 하드웨어의 실행 모형이 깊은 수준에서 일치한다는 사실에 기반한 필연적 결과이다.
2. SIMT 실행 모형과 워프
NVIDIA의 CUDA 환경에서 GPU의 기본 실행 단위는 워프(Warp)이며, 한 개의 워프는 32개의 스레드로 구성된다. 동일한 워프에 속한 스레드들은 동일한 명령어를 동시에 실행하지만 각자 다른 데이터를 다루며, 이러한 모형이 SIMT라고 불리는 이유이다. 워프 내에서 분기(Branch)가 발생하여 일부 스레드만 한쪽 경로를 따르는 경우, 두 경로가 순차적으로 실행되는 분기 발산(Branch Divergence)이 일어나며 이는 성능 저하의 주요 원인이 된다. 따라서 텐서 연산을 GPU 커널로 매핑할 때는 워프 단위의 분기 발산을 최소화하도록 알고리즘을 설계하는 것이 핵심이다.
3. CUDA 실행 계층 구조
CUDA 프로그래밍 모형은 계산을 그리드(Grid), 블록(Block), 스레드(Thread)의 세 계층으로 조직한다. 그리드는 한 번의 커널 호출 전체에 해당하며, 다수의 블록으로 구성된다. 블록은 동일한 스트리밍 멀티프로세서(Streaming Multiprocessor, SM) 위에서 실행되는 스레드의 집합이며, 블록 내 스레드들은 공유 메모리(Shared Memory)와 동기화 원시(Synchronization Primitive)를 통해 협력할 수 있다. 스레드는 가장 기본적인 실행 단위이며, 각 스레드는 자신만의 레지스터(Register)와 지역 메모리를 가진다.
이러한 3계층 모형은 텐서의 다차원 인덱스 공간과 자연스럽게 대응된다. 예를 들어 형상이 (N, C, H, W)인 4차 텐서의 모든 원소에 동일한 원소별 연산을 적용하는 경우, 전체 인덱스 공간 N \times C \times H \times W를 그리드와 블록의 좌표로 분할하여 스레드별로 한 개의 원소를 처리하도록 매핑할 수 있다. 이때 텐서의 인덱스와 CUDA 스레드의 좌표 사이의 매핑을 어떻게 설계하는가가 메모리 접근 패턴과 성능을 결정한다.
4. 메모리 계층과 결합 접근
GPU 메모리는 속도와 용량의 절충에 따라 여러 계층으로 구성된다. 가장 큰 용량을 가지지만 가장 느린 전역 메모리(Global Memory), 블록 내에서 공유되는 빠른 공유 메모리, 단일 스레드가 사용하는 매우 빠른 레지스터, 읽기 전용으로 활용되는 상수 메모리(Constant Memory)와 텍스처 메모리(Texture Memory) 등이 있다. 텐서 연산의 성능은 산술 연산 자체의 속도가 아니라, 전역 메모리로부터 데이터를 얼마나 효율적으로 읽어 오는가에 의해 사실상 결정된다.
이러한 맥락에서 가장 중요한 개념이 결합 메모리 접근(Coalesced Memory Access)이다. 동일한 워프에 속한 32개 스레드가 인접한 전역 메모리 주소에 동시에 접근하면, 하드웨어는 이를 한 번의 메모리 트랜잭션으로 합쳐서 처리할 수 있으며, 이 경우 메모리 대역폭이 거의 최대치까지 활용된다. 반대로 스레드들이 서로 떨어진 주소에 접근하면 트랜잭션이 분리되어 대역폭이 급격히 저하된다. 따라서 텐서의 메모리 레이아웃과 스레드-인덱스 매핑은, 가장 안쪽 차원이 워프의 스레드 차원에 정렬되도록 설계되어야 한다.
5. 행렬 곱셈의 GPU 매핑
가장 대표적이고 가장 자주 다루어지는 사례는 행렬 곱셈 C = AB이다. 가장 단순한 매핑은 출력 행렬 C의 원소 C(i, j)를 한 개의 스레드에 할당하는 것이며, 각 스레드는 한 개의 행과 한 개의 열을 따라 합산을 수행한다. 그러나 이 단순한 매핑은 동일한 행 또는 열을 여러 스레드가 중복적으로 전역 메모리에서 읽어 오므로 매우 비효율적이다.
이를 해결하기 위해 도입된 표준 기법이 타일링(Tiling)이다. 타일링은 행렬을 작은 정방형 부분 행렬, 즉 타일로 분할하고, 한 블록의 스레드들이 협력하여 두 입력 행렬의 한 쌍의 타일을 공유 메모리로 적재한 뒤, 적재된 타일에 대해 모든 스레드가 자신의 출력 원소에 필요한 부분합을 계산하는 절차를 반복한다. 공유 메모리는 전역 메모리보다 수십 배 빠르므로, 한 번 적재된 데이터를 여러 번 재사용함으로써 메모리 대역폭의 부담을 결정적으로 감소시킬 수 있다. 이러한 접근은 cuBLAS와 같은 고성능 선형대수 라이브러리의 설계 원리이며, 현대의 행렬 곱셈 커널은 이를 더욱 정교화하여 다층 타일링, 레지스터 블로킹(Register Blocking), 이중 버퍼링(Double Buffering) 등을 결합한다.
6. 합성곱의 GPU 매핑
합성곱 연산은 행렬 곱셈으로 환원되거나, 직접 합성곱 커널로 구현되거나, 빠른 푸리에 변환(FFT) 기반으로 가속되거나, 위노그라드(Winograd) 알고리즘 기반으로 가속될 수 있다. 가장 보편적으로 사용되는 방법은 입력 텐서를 im2col 변환을 통해 행렬로 펼친 후, 가중치 행렬과의 일반 행렬 곱셈으로 환원하는 방식이다. 이 방법은 메모리 사용량이 크다는 단점이 있지만, 매우 성숙한 GEMM(GEneral Matrix Multiplication) 커널을 그대로 활용할 수 있다는 결정적 장점이 있다. 직접 구현 방식은 메모리 사용량이 적지만 데이터 접근 패턴이 복잡하므로, cuDNN과 같은 라이브러리는 입력 형상, 채널 수, 커널 크기, 보폭 등 매개변수에 따라 여러 알고리즘 가운데 가장 빠른 것을 선택하는 자동 알고리즘 선택(Auto-Tuning) 기능을 제공한다.
7. 축소 연산의 병렬화
합산, 평균, 최댓값, 노름 등 축소 연산은 수학적으로 결합 법칙을 만족하므로 병렬화가 가능하지만, 단순한 순차 합산과 달리 트리 형태의 분할을 통해 수행되어야 한다. GPU에서의 표준 구현은 블록 내에서 공유 메모리를 활용한 트리 축소(Tree Reduction)를 수행하고, 블록별 부분합을 다시 두 번째 커널 호출에서 합산하는 두 단계 절차를 따른다. 또한 워프 셔플(Warp Shuffle) 명령은 동일한 워프 내에서 공유 메모리를 거치지 않고 직접 데이터를 교환할 수 있게 하여, 축소의 마지막 단계를 더욱 효율적으로 수행할 수 있게 한다.
8. 텐서 코어와 혼합 정밀도
볼타(Volta) 세대 이후의 NVIDIA GPU에는 텐서 코어(Tensor Core)라 불리는 전용 산술 단위가 도입되었다. 텐서 코어는 작은 크기의 행렬-행렬 곱셈을 단일 명령어로 수행하며, 일반적으로 입력은 반정밀도(FP16) 또는 그 변형(BF16, FP8 등)이고 누적은 단정밀도(FP32)로 수행된다. 이러한 혼합 정밀도(Mixed Precision) 산술은 행렬 곱셈과 합성곱의 처리량을 일반 부동소수점 연산기에 비해 수 배에서 수십 배까지 증가시킨다. 텐서 코어를 효과적으로 활용하기 위해서는 텐서의 형상과 메모리 정렬이 텐서 코어가 요구하는 단위에 맞추어져 있어야 하며, 이는 cuBLAS와 cuDNN의 내부에서 자동으로 처리된다. 마이클리시브(P. Micikevicius) 등의 2018년 논문 “Mixed Precision Training“은 이러한 정밀도 혼합 학습의 안정성과 정확성에 관한 표준 분석을 제시하였다.
9. 점유율과 지연 은닉
GPU의 처리량은 단순히 명령어 처리 속도가 아니라, 여러 워프를 동시에 SM 위에 적재함으로써 메모리 접근 지연을 산술 연산으로 가리는 지연 은닉(Latency Hiding) 능력에 크게 의존한다. 한 SM 위에 동시에 적재 가능한 워프의 수에 대한 비율을 점유율(Occupancy)이라 부르며, 점유율이 높을수록 일부 워프가 메모리 접근을 기다리는 동안 다른 워프가 계산을 수행할 수 있어 전체 처리량이 향상된다. 그러나 점유율이 절대적인 성능 지표는 아니며, 각 스레드가 사용하는 레지스터 수, 공유 메모리 사용량, 명령어 수준 병렬성(Instruction-Level Parallelism) 등 여러 요인 사이의 균형이 중요하다.
10. 호스트-디바이스 데이터 전송과 스트림
CPU(호스트)와 GPU(디바이스) 사이의 데이터 전송은 PCI Express 또는 NVLink 등의 인터커넥트를 통해 이루어지며, 그 대역폭은 GPU 내부 메모리 대역폭에 비해 훨씬 작다. 따라서 학습 파이프라인에서는 데이터 전송과 계산을 가능한 한 중첩(Overlap)시키는 것이 중요하다. CUDA는 이를 위해 스트림(Stream)이라는 추상화를 제공하며, 서로 다른 스트림 위의 연산은 비동기적으로 동시에 진행될 수 있다. 페이지 잠금 메모리(Page-Locked Memory)와 비동기 복사(Asynchronous Copy)를 결합하면, 한 미니배치를 GPU에서 학습하는 동안 다음 미니배치를 미리 GPU 메모리로 전송할 수 있다.
11. 추상화와 기계 매핑의 분리
현대 딥러닝 프레임워크는 사용자가 텐서 연산을 고수준의 수학적 표현으로 기술하면, 그 표현을 자동으로 적절한 CUDA 커널로 매핑하는 컴파일러 또는 디스패처를 내장하고 있다. PyTorch의 ATen, TensorFlow의 XLA(Accelerated Linear Algebra), JAX의 XLA 백엔드 등이 그러한 사례이며, 이들은 텐서 연산 그래프를 분석하여 연산 융합(Operation Fusion), 메모리 재사용, 커널 자동 생성을 수행한다. 이러한 컴파일러는 사용자가 직접 CUDA 코드를 작성하지 않고도 손으로 작성된 커널에 근접하거나 그를 능가하는 성능을 얻을 수 있게 한다. 결국 GPU 기반 텐서 연산의 핵심 원리는, 텐서의 다차원 인덱스 공간을 SIMT 실행 모형의 그리드-블록-스레드 좌표로 매핑하고, 메모리 계층 구조와 결합 접근 규칙에 맞추어 데이터 이동을 최소화함으로써, 산술 연산기의 처리량을 한계까지 끌어올리는 데 있다.