Making Deep Learning Go Brrrr from First Principles
개요
딥러닝 모델의 성능 향상을 위한 접근 방식은 종종 임의적인 기법에 의존하지만, 첫 원칙에 기반한 이해는 최적화 가능 영역을 명확히 하여 문제를 단순화할 수 있습니다.
주요 내용
* 성능 병목 현상 분석: 딥러닝 시스템의 효율성은 크게 세 가지 요소, 즉 컴퓨트(Compute), 메모리(Memory), 오버헤드(Overhead)로 구성됩니다. 현재 시스템이 어느 요소에 시간을 가장 많이 소모하는지 파악하는 것이 중요합니다.
* 컴퓨트(Compute): GPU가 실제 부동소수점 연산(FLOPS)을 수행하는 데 소요되는 시간입니다. 현대 GPU는 행렬 곱셈에 특화된 Tensor Core와 같은 하드웨어를 갖추고 있어, 행렬 곱셈이 아닌 연산은 훨씬 낮은 FLOPS를 달성합니다. BERT와 같은 모델에서도 행렬 곱셈이 전체 FLOPS의 대부분을 차지합니다.
* 메모리(Memory): GPU 내부에서 텐서를 전송하는 데 소요되는 시간으로, 주로 GPU의 DRAM에서 연산 장치로 데이터를 이동시키는 데 발생하는 비용을 의미합니다. 연산량이 적은 유니터리 연산(unary operation)의 경우, 데이터 전송 시간이 실제 연산 시간보다 훨씬 많이 소요될 수 있습니다.
* 연산자 융합(Operator Fusion): 메모리 대역폭 병목 현상을 해결하는 핵심 최적화 기법으로, 여러 연산을 하나의 커널로 묶어 전역 메모리(global memory) 읽기/쓰기 횟수를 줄입니다. 예를 들어, x.cos().cos() 연산은 두 번의 전역 메모리 접근이 필요하지만, 융합을 통해 한 번으로 줄여 속도를 2배 향상시킬 수 있습니다.
* 컴퓨트 집약도(Compute Intensity): 특정 연산에서 메모리 접근 횟수 대비 연산 횟수를 높이는 것을 의미합니다. Comput Intensity를 높이면 메모리 대역폭의 한계를 극복하고 GPU의 최대 FLOPS에 가까워질 수 있습니다.
* 오버헤드(Overhead): 텐서 전송이나 연산이 아닌, 코드 실행 중 발생하는 모든 추가적인 시간입니다. Python 인터프리터, PyTorch 프레임워크 자체, CUDA 커널 실행 대기 시간 등이 오버헤드에 포함됩니다. GPU가 매우 빠르기 때문에 Python과 같은 언어는 상대적으로 매우 느립니다.
* 오버헤드 은닉: PyTorch와 같은 프레임워크는 비동기 실행을 통해 GPU 커널 실행 중에 더 많은 작업을 큐에 넣어 오버헤드를 은닉할 수 있습니다. GPU 연산이 충분히 크면 CPU 오버헤드는 무시할 수 있습니다.
* 병목 현상 진단: 데이터 크기를 늘렸을 때 런타임이 비례적으로 증가하지 않으면 오버헤드 병목 현상일 가능성이 높습니다. 또한, nvidia-smi의 "GPU-Util"은 GPU 커널이 실제로 실행되는 시간의 비율을 나타내므로 오버헤드를 파악하는 데 유용합니다.
* 유연성 vs. 효율성: PyTorch의 유연성은 오버헤드를 발생시키는 주요 원인이며, 이를 줄이기 위해 JIT 컴파일 (jit.trace, FX, jax.jit) 또는 CUDA Graphs와 같은 기법을 사용할 수 있습니다. TorchDynamo와 같은 VM 레벨에서의 동작은 유연성과 효율성을 모두 얻을 수 있는 대안으로 제시됩니다.
시사점
딥러닝 모델의 성능을 극대화하기 위해서는 현재 시스템의 병목 현상이 컴퓨트, 메모리, 또는 오버헤드 중 어디에 있는지 정확히 진단하고, 그에 맞는 최적화 전략(예: 연산자 융합, JIT 컴파일, Tensor Core 활용)을 적용하는 것이 필수적입니다.
댓글
GitHub Discussions