Profiling in PyTorch (Part 1): A Beginner's Guide to torch.profiler
Quick Summary
파이토치의 행렬 곱셈·덧셈 예제를 통해 torch.profiler 설정법, 표와 실행 추적을 읽는 법, CPU 오버헤드와 GPU 연산 시간의 차이, 워밍업이 프로파일 결과에 미치는 영향을 단계적으로 설명한다.
🖼️ 인포그래픽
🖼️ 4컷 인포그래픽
💡 한 줄 요약
파이토치의 행렬 곱셈·덧셈 예제를 통해 torch.profiler 설정법, 표와 실행 추적을 읽는 법, CPU 오버헤드와 GPU 연산 시간의 차이, 워밍업이 프로파일 결과에 미치는 영향을 단계적으로 설명한다.
📌 핵심 요약
- 프로파일링은 학습·추론 속도가 기대보다 느린 이유를 파악하고 병목을 찾는 핵심 수단이지만, 복잡한 이벤트 이름과 촘촘한 실행 추적 때문에 초보자가 접근하기 어렵다.
- torch.profiler.profile로 CPU와 CUDA 활동을 기록하고 record_function으로 관심 구간을 표시한 뒤, 프로파일러 표와 크롬 실행 추적 파일을 내보낼 수 있다.
- 프로파일러 표는 어떤 이벤트가 시간을 많이 소비하는지 통계적으로 보여 주며, Self 시간과 total 시간, 호출 횟수를 함께 살펴야 실제 병목을 올바르게 해석할 수 있다.
- 64×64 행렬에서는 GPU 커널 실행보다 준비·실행 요청·데이터 처리 같은 CPU 측 비용이 지배적이지만, 4096×4096 행렬에서는 GPU 계산 시간이 커지면서 오버헤드 중심에서 연산 중심으로 바뀐다.
- 실행 추적에서는 CPU 호출 계층, GPU 커널 실행, 두 영역 사이의 유휴 구간을 시간순으로 확인할 수 있으며, 초기 일회성 준비 비용은 충분한 워밍업을 통해 측정 구간에서 제외해야 한다.
🧩 주요 포인트
- 프로파일링은 학습·추론 속도가 기대보다 느린 이유를 파악하고 병목을 찾는 핵심 수단이지만, 복잡한 이벤트 이름과 촘촘한 실행 추적 때문에 초보자가 접근하기 어렵다.
- torch.profiler.profile로 CPU와 CUDA 활동을 기록하고 record_function으로 관심 구간을 표시한 뒤, 프로파일러 표와 크롬 실행 추적 파일을 내보낼 수 있다.
- 프로파일러 표는 어떤 이벤트가 시간을 많이 소비하는지 통계적으로 보여 주며, Self 시간과 total 시간, 호출 횟수를 함께 살펴야 실제 병목을 올바르게 해석할 수 있다.
- 64×64 행렬에서는 GPU 커널 실행보다 준비·실행 요청·데이터 처리 같은 CPU 측 비용이 지배적이지만, 4096×4096 행렬에서는 GPU 계산 시간이 커지면서 오버헤드 중심에서 연산 중심으로 바뀐다.
- 실행 추적에서는 CPU 호출 계층, GPU 커널 실행, 두 영역 사이의 유휴 구간을 시간순으로 확인할 수 있으며, 초기 일회성 준비 비용은 충분한 워밍업을 통해 측정 구간에서 제외해야 한다.
🧠 상세 정리
1. 프로파일링이 필요한 이유와 초보자의 진입 장벽
모델의 초당 처리 토큰 수를 높이거나 추론 시간을 줄이고, 학습 루프가 예상한 성능을 내지 못하는 이유를 이해하려면 결국 프로파일링이 필요하다. 그러나 프로파일러가 생성하는 실행 추적은 수많은 색상 막대와 생소한 이벤트 이름으로 구성되어 있어, 처음 접하는 사람에게는 해석 자체가 큰 부담이 된다. 원문은 이러한 진입 장벽을 낮추기 위해 기본적인 파이토치 지식만 가진 독자를 대상으로, 실행 추적을 열고 의문을 제기한 다음 원인을 따라가는 질문 중심의 설명 방식을 택한다. 이 글에서 다루는 목표는 torch.profiler의 설정과 출력물 이해, CPU·GPU 레인과 그 사이의 공백 읽기, 파이썬 호출에서 CUDA 커널 실행까지 이어지는 사건의 흐름을 익히는 것이다.
2. 행렬 곱셈과 덧셈을 이용한 실습 구성
실습 대상 함수는 입력 행렬과 가중치 행렬을 곱한 뒤 편향을 더하는 단순한 연산으로, 신경망에서 가중치와 편향이 결합되는 방식을 축약해 보여 준다. 원문은 GPU 커널을 GPU의 여러 스레드에서 병렬로 실행되는 프로그램으로 정의하고, 일반적인 파이토치 연산이 자동으로 하나 이상의 GPU 커널로 변환된다고 설명한다. 실험은 NVIDIA A100-SXM4-80GB GPU에서 수행되며, 전체 예제는 별도의 01_matmul_add.py 스크립트로 제공된다. 단순한 행렬 곱셈만 사용하지 않고 덧셈까지 포함한 이유는 실제 신경망 계산과의 연결성을 유지하면서, 이후 여러 연산의 실행 구조를 살펴볼 수 있는 기초 예제를 만들기 위해서다.
3. torch.profiler 설정과 두 가지 출력물
측정할 코드를 준비한 뒤 torch.profiler.record_function을 사용하면 해당 구간에 matmul_add 같은 알아보기 쉬운 이름을 붙일 수 있으며, 이 표시는 필수는 아니지만 실행 추적을 탐색하는 데 유용하다. 이어 torch.profiler.profile 컨텍스트 관리자에 CPU와 CUDA 활동을 지정하고, 같은 연산을 여러 번 실행하면서 prof.step을 호출해 측정 단계를 진행한다. 측정이 끝나면 key_averages의 표를 텍스트로 확인하고 export_chrome_trace를 통해 시간순 실행 정보가 담긴 추적 파일을 내보낼 수 있다. 프로파일러 표는 무엇이 가장 많은 시간을 소비했는지 알려 주는 통계적 요약이고, 실행 추적은 어떤 연산이 언제·왜 발생했는지와 CPU 및 GPU 활동이 어떻게 이어졌는지를 보여 주는 시간적 기록이다.
4. 프로파일러 표에서 시간과 호출 횟수 읽기
프로파일러 표의 첫 번째 열에는 측정 범위 안에서 발생한 이벤트가 나열되고, 나머지 열에는 CPU나 GPU 등 profile의 activities에 지정한 장치에서 각 이벤트가 소비한 시간이 표시된다. 병목을 찾을 때는 시간이 긴 이벤트만 보는 것이 아니라, 그 시간이 연산의 성격상 타당한지와 동일 이벤트가 몇 번 호출되었는지를 함께 확인해야 한다. Self CPU 또는 Self CUDA 시간은 자식 이벤트를 제외하고 해당 이벤트 자체에서 소비한 시간이며, CPU total 또는 CUDA total은 해당 이벤트와 그 아래에서 호출된 모든 자식 이벤트의 시간을 합친 값이다. 따라서 matmul_add의 total 시간은 단순히 표시 구간 자체의 비용만 의미하지 않고, 그 안에서 실행된 행렬 곱셈과 덧셈 및 관련 하위 이벤트의 시간까지 포함한다.
5. 64×64 행렬에서 드러난 CPU 오버헤드
64×64 행렬을 측정한 결과 Self CPU 전체 시간은 2.314밀리초였지만 Self CUDA 전체 시간은 23.104마이크로초에 불과해, GPU가 실제 계산에 사용된 시간은 CPU 측 시간의 1퍼센트보다도 작았다. 작은 행렬 곱셈은 GPU가 매우 빠르게 처리하므로, 전체 실행 시간에서는 커널을 준비하고 GPU에 실행을 요청하며 데이터를 전달하고 결과를 수집하는 과정이 상대적으로 큰 비중을 차지한다. 이처럼 실제 계산보다 실행 준비와 호출 비용이 지배하는 상태를 원문은 오버헤드 중심 알고리즘으로 설명한다. 프로파일러 표에서 GPU 커널 자체가 짧게 끝나고 GPU가 대부분의 시간 동안 유휴 상태라면, 커널 성능보다 CPU와 GPU 사이의 실행 준비 과정이 병목이라는 즉각적인 신호로 해석할 수 있다.
6. 4096×4096 행렬에서 연산 중심으로 전환
행렬 크기를 4096×4096으로 늘리자 Self CPU 전체 시간은 4.908밀리초, Self CUDA 전체 시간은 4.495밀리초로 나타나 두 값이 모두 밀리초 단위에 들어왔다. 행렬이 커지면서 GPU가 실제 곱셈을 수행하는 시간이 충분히 길어졌고, 프로파일러 표에서도 matmul_add를 시작한 CPU 이벤트가 아니라 실제 GPU 행렬 곱셈 커널이 CUDA 시간을 가장 많이 소비했다. 이는 작은 입력에서 지배적이던 고정적인 준비·실행 요청 비용의 상대적 비중이 줄고, 실제 수치 계산이 전체 시간을 좌우하는 상태로 이동했음을 보여 준다. 원문은 이를 오버헤드 중심에서 연산 중심으로 전환된 사례로 제시하며, 같은 코드라도 입력 규모에 따라 병목의 성격과 프로파일 결과의 해석이 달라질 수 있음을 강조한다.
7. 실행 추적의 CPU·GPU 레인과 측정 일정
JSON 형식의 실행 추적은 Perfetto UI에 업로드하거나 trace-util을 이용해 열 수 있으며, 막대의 너비는 이벤트 지속 시간을, 세로 방향의 중첩은 호출 계층을 나타낸다. CPU 레인에는 파이토치 호출과 관련 이벤트가 표시되고 GPU 레인에는 실제 커널 실행이 나타나며, 이벤트 사이의 빈 공간은 대기 또는 유휴 시간을 의미한다. 예제의 일정은 wait 1회, warmup 1회, active 3회로 구성되어 초기 단계인 ProfilerStep#0은 기록에서 제외하고 ProfilerStep#1은 프로파일러를 예열하는 데 사용하며, 이후 세 단계만 실행 추적에 남긴다. 따라서 실행 추적을 읽을 때는 보이는 단계가 전체 반복의 전부라고 간주하지 말고, 어떤 일정 설정에 의해 어느 구간이 기록되었는지를 먼저 확인해야 한다.
8. ProfilerStep#2의 지연과 워밍업의 의미
초기 실행 추적에서는 ProfilerStep#2와 그 안의 matmul_add 구간이 뒤따르는 단계보다 넓게 나타나며, record_function에 진입한 시점부터 aten::matmul이 실제로 시작되기까지 약 228마이크로초의 빈 구간이 관찰된다. 원문은 이 지연이 작업 공간 할당, cuBLAS의 실행 방식 선택, 지연된 모듈 로딩 등 초기 준비 작업으로 인해 발생할 수 있다고 설명한다. 이러한 일회성 비용을 일반적인 반복 실행 비용과 섞지 않으려면 프로파일러에 들어가기 전 연산을 여러 번 실행하고, 프로파일러 일정의 warmup 단계도 함께 사용하는 것이 표준적인 방법이다. 워밍업 후에는 각 프로파일 단계의 시간이 비슷해지지만, 이는 초기 오버헤드 자체를 최적화한 결과가 아니라 해당 준비 작업을 실제 기록 구간보다 앞에서 수행해 측정 대상에서 제외한 결과다.
🧾 핵심 주장 / 시사점
- 프로파일러 표는 병목의 크기를 찾는 데 적합하고 실행 추적은 그 병목이 발생한 순서와 CPU·GPU 사이의 관계를 설명하는 데 적합하므로, 두 출력물을 함께 읽어야 한다.
- 동일한 행렬 곱셈·덧셈 코드도 작은 입력에서는 실행 준비 비용이 지배하지만 큰 입력에서는 GPU 계산이 지배하므로, 성능 문제를 판단할 때 입력 규모를 분리해서 봐야 한다.
- 워밍업은 초기 일회성 비용을 제거하는 최적화가 아니라 반복 실행의 정상 상태를 측정하기 위해 해당 비용을 기록 구간 밖으로 옮기는 측정 절차다.
✅ 액션 아이템
- torch.profiler.profile로 CPU·CUDA 활동을 동시에 기록하고 record_function으로 분석 구간을 지정해 프로파일 표와 크롬 실행 추적 파일을 함께 생성한다.
- 프로파일 표 해석 시 Self 시간, total 시간, 호출 횟수를 함께 결합해 실제 병목 후보를 우선순위화한다.
- 대표 연산 규모(예: 64×64, 4096×4096)로 반복 실험해 워밍업 적용 전후 결과를 비교하고 오버헤드 지배 구간과 연산 지배 구간 이동을 점검한다.
❓ 열린 질문
- CPU 오버헤드가 지배적인 구간에서 GPU 연산으로 병목이 넘어가는 임계 입력 크기·연산량 기준은 어떻게 잡을 것인가?
- Self 시간·total 시간·호출 횟수 중 어떤 조합을 기준으로 실제 병목을 일관되게 정의할 것인가?
- 초기 일회성 준비 비용을 배제하려면 워밍업 횟수와 측정 구간 길이는 어떤 방식으로 정해 편향을 줄일 수 있는가?