PyTorch에서 프로파일링: torch.profiler 초보자 가이드
PyTorch에서 프로파일링: torch.profiler 초보자 가이드
성능 최적화를 위한 torch.profiler 이해
프로파일링은 대규모 언어 모델(LLM)과 딥러닝 파이프라인을 최적화하는 데 필수적인 첫 단계이며, 이를 통해 개발자는 모델이 오버헤드에 제한되는지 혹은 연산에 제한되는지를 파악할 수 있습니다. torch.profiler 모듈은 성능을 진단하기 위한 두 가지 주요 산출물을 제공합니다: profiler table은 가장 많은 시간을 차지하는 "무엇"에 대한 통계 요약을 제공하고, profiler trace는 CPU와 GPU 라인 전반에 걸쳐 연산이 언제, 왜 발생하는지를 시간 순으로 보여줍니다.
병목 현상 식별: 오버헤드 바인드 vs. 컴퓨트 바인드
성능 병목은 프로파일러 테이블에서 CPU와 GPU에 소요된 시간을 비교함으로써 드러납니다.
- Overhead-Bound:
Self CPU time total이Self CUDA time total보다 현저히 높을 때(예: CPU 시간은 밀리초 단위이고 GPU 시간은 마이크로초 단위) 알고리즘은 오버헤드 바인드입니다. 이는 CPU가 커널을 준비하고 실행시키는 데 GPU보다 더 많은 시간을 소비한다는 의미이며, 작은 행렬 곱셈에서 흔히 발생합니다. - Compute-Bound:
Self CPU time total과Self CUDA time total이 모두 밀리초 범위에 있으며 비슷한 수준일 때 알고리즘은 컴퓨트 바인드입니다. 이는 고성능 컴퓨팅에 이상적인 상태로, GPU가 주요 병목이 됩니다.
작업량을 늘리면(예: 64×64 행렬에서 4096×4096 행렬로 이동) 모델은 일반적으로 오버헤드 바인드 영역에서 컴퓨트 바인드 영역으로 전환됩니다.
CPU와 GPU 디스패치 체인 분석
PyTorch 연산은 Python 호출에서 CUDA 커널까지 특정 디스패치 체인을 따릅니다. 전형적인 행렬 곱셈 및 덧셈 순서는 다음과 같습니다:
ProfileStep → record_function (사용자 주석) → aten::matmul (ATen 수준 디스패치) → aten::mm (2D 행렬‑행렬 곱셈 백엔드) → cudaLaunchKernel.
주요 CPU 라인 지표
- Cold-Start Overhead: 첫 번째
ProfileStep은 워크스페이스 할당, cuBLAS 휴리스틱, 지연 모듈 로딩 등으로 인해 이후 단계보다 넓게 나타납니다. 이는 워밍업 반복을 통해 완화할 수 있습니다. - CUDA Occupancy Queries:
cudaLaunchKernel앞에cudaOccupancyMaxActiveBlocksPerMultiprocessor가 존재하면 이는 "무거운" 커널(예: GEMM 또는 컨볼루션)임을 나타냅니다. CPU는 하드웨어 용량에 기반해 최적 블록 크기를 결정하기 위해 드라이버에 질의합니다. - Resource-Light Kernels: 요소별 연산이나 축소 커널은 리소스 풋프린트가 고정되고 작기 때문에 보통 점유율 질의를 수행하지 않습니다.
- Synchronization: 트레이스 끝에 긴
cudaDeviceSynchronize가 나타나는 경우, 이는 프로파일러가 이벤트를 플러시하고 있음을 의미하며, 그 지속 시간은 CPU가 GPU가 남은 작업을 마치기를 기다린 시간을 반영합니다.
주요 GPU 라인 지표
- Activity Buffer Requests: 커널 사이의 간격이나 CPU와 GPU 라인 사이의 초기 오프셋은 종종 프로파일러가 자체 이벤트 버퍼를 할당하거나 재채우는 과정에서 발생합니다.
- Runtime Variance: 동일한 커널이라도 GPU 클럭 변동, 열 관리, 전원 관리, 드라이버 정리 등으로 인해 단계마다 실행 시간이 달라질 수 있습니다.
torch.compile의 영향
torch.compile을 사용하면 즉시 실행(eager) PyTorch 코드를 TorchDynamo, AOTAutograd, TorchInductor를 통해 최적화된 그래프로 변환합니다. 컴파일된 함수를 프로파일링하면 여러 구조적 변화가 드러납니다.
디스패처 수준 Fusion
torch.add(torch.matmul(x, w), b)와 같은 연산에 대해 torch.compile은 그래프 수준에서 연산자를 결합하여 별도의 aten::add와 aten::mm 호출을 하나의 aten::addmm 호출로 대체합니다. 그러나 이는 디스패처 수준의 Fusion이며, 커널 수준은 아닙니다. GPU는 여전히 바이어스를 목적 버퍼에 복사하는 Memcpy DtoD를 수행한 뒤, 바이어스‑덧셈 에필로그가 포함된 GEMM 커널을 실행합니다.
런타임 아키텍처와 오버헤드
컴파일된 영역은 특정 CPU 측 계층 구조를 도입합니다:
- TorchDynamo Cache Lookup: 입력 형태, dtype, 디바이스가 캐시된 컴파일과 일치하는지 확인합니다. 매 호출마다 수행됩니다.
- Torch-Compiled Region: 컴파일된 버전으로 진입하는 래퍼입니다.
- AOTDispatcher Runtime Wrapper Prologue: 텐서 메타데이터와 뷰 추적을 처리합니다.
- Call CompiledFxGraph: 생성된 코드를 실행합니다(내용 해시로 식별).
매우 작은 연산의 경우, torch.compile은 Dynamo → AOTAutograd → Inductor 스택을 탐색하는 비용이 Fusion으로 얻는 이득보다 커서 실제로 CPU 오버헤드를 증가시킬 수 있습니다. 이 오버헤드는 모델 내 연산 수가 늘어남에 따라 상쇄됩니다.
트레이스 읽기 요약 표
| 관찰 | 추정 의미 |
|---|---|
Self CPU $\gg$ Self CUDA |
오버헤드 바인드; 배치 크기를 늘리거나 연산을 결합하세요 |
cudaOccupancy... before launch |
무거운, 적응적으로 실행되는 커널 (GEMM/Conv) |
aten::matmul $\rightarrow$ aten::bmm |
3D 이상 텐서에 대한 배치 행렬 곱셈 |
Torch-Compiled Region |
torch.compile 최적화 블록 내에서 실행 |
Memcpy DtoD before GEMM |
컴파일 모드에서 addmm 에필로그를 위한 바이어스 복사 |
요약
Hugging Face는 torch.profiler를 활용해 병목을 식별하고, CPU‑GPU 디스패치 체인을 이해하며, torch.compile이 커널 실행에 미치는 영향을 분석하는 포괄적인 가이드를 제공합니다.
제목
Profiling in PyTorch: A Beginner's Guide to torch.profiler