[실습] PyTorch 프로파일러와 텐서보드 활용
실습 5 — PyTorch 프로파일러(PyTorch Profiler)와 텐서보드(TensorBoard)
Nsight Systems Profiling 실습은 CUDA API 호출, CPU 스레드, NVTX(NVIDIA Tools Extension, 코드 실행 구간에 사용자 정의 레이블을 추가해 CPU·GPU 작업을 프로파일링 타임라인에서 쉽게 구분하도록 하는 도구) 범위, 커널 실행과 같은 시스템 수준 관점(system view)을 제공한다. 분석 범위가 넓고 상세하지만 도구가 비교적 무거우며, 셸(shell) 환경에서 작업하고 CSV 파일을 분석해야 한다.
PyTorch 프로파일러(PyTorch Profiler)는 더 가벼운 프로파일링 도구다. Python 코드 몇 줄만으로 연산자별 실행 시간, 메모리 할당량, 크롬 트레이스(Chrome trace)를 확인할 수 있다. 특히 “어떤 PyTorch 연산자가 가장 많은 실행 시간을 사용하는가?”와 같은 모델 수준 최적화(model-level optimization)에 적합하다.
두 도구는 서로 대체하는 관계가 아니라 상호 보완적인 관계다.
분석 질문 적합한 도구
“현재 GPU가 유휴 상태인가, 작업 Nsight Systems 와 중인가?” 타임라인(timeline)
“모델에서 가장 느린 연산자는 PyTorch 프로파일러(PyTorch
무엇인가?” Profiler)와 key_averages
“각 계층(layer)이 얼마나 많은 profile_memory=True를 적용한
메모리를 할당하는가?” PyTorch 프로파일러
“마이크로초 단위에서 GPU가 어떤 Nsight Systems 또는 Nsight Compute 작업을 수행하는가?”
“지속적 통합(CI) 환경에서 사용할 PyTorch 프로파일러(PyTorch 수 있는가?” Profiler): 스크립트 실행과 결과 분석이 가능함 —————————————————
- 여기서 타임라인(timeline)은 CPU 스레드, CUDA API 호출, GPU 커널 실행, 메모리 전송 등의 동작을 시간 순서대로 시각화하여 GPU가 작업 중인지 대기 중인지 보여주는 분석 화면이다.
참고 자료
- PyTorch 프로파일러 예제 — 공식적으로 빠르게 시작할 수 있는 안내서다.
- PyTorch 프로파일러 API
문서 —
torch.profiler.profile, 프로파일링 활동 내역(activity), 스케줄(schedule)을 설명한다. - Chrome Trace Event
Format
— PyTorch 프로파일러가
chrome://tracing또는 퍼페토(Perfetto)용으로 내보내는 JSON 형식이다. - Horace He — Making Deep Learning Go
Brrrr — 연산자 수준 실행
시간을 해석하는 방법을 설명한다. 정확한 측정을 위해
torch.cuda.synchronize()로 측정 구간을 동기화해야 한다. - PyTorch TensorBoard Profiler
플러그인
—
torch_tb_profiler를 설치하면 그래픽 사용자 인터페이스(GUI)로 결과를 분석할 수 있다.
실습 목표
- 학습 단계(training step)를
torch.profiler.profile(...)로 감싸고 실행 시간이 긴 주요 연산자를 추출한다. key_averages()결과표를 출력하고 각 열의 의미를 이해한다.- 메모리 프로파일링(memory profiling)을 적용해 메모리 할당량이 큰 연산자를 찾는다.
- 크롬 트레이스(Chrome trace)를 내보내 스케줄러의 유휴 구간을 분석한다.
1단계 — 학습 단계 프로파일링 및 주요 연산자 추출
최소한으로 사용할 수 있는 프로파일링 코드는 다음과 같다.
with torch.profiler.profile(activities=[CPU, CUDA]) as prof:
for _ in range(5):
training_step()
prof.key_averages().table(sort_by='cuda_time_total', row_limit=20)
다음 두 가지 사항에 주의해야 한다.
- 프로파일러 외부에서 워밍업 단계(warmup step)를 먼저 실행한다. 최초 CUDA 실행에서는 커널 컴파일과 초기화가 발생하므로 측정값이 왜곡될 수 있다.
- 측정 마지막에
torch.cuda.synchronize()를 호출한다. CUDA 커널은 비동기식(asynchronous)으로 실행되므로, 동기화하지 않으면 일부 커널의 실행 시간이 집계되지 않을 수 있다.
프로파일러의 주요 시간 열은 다음과 같다.
열 측정 내용
CPU 시간(CPU time) CPU 스레드가 연산자 내부에서 디스패치(dispatch), 인자 바인딩(argument binding) 등을 수행한 시간
CUDA 시간(CUDA time) 해당 연산자가 실행한 GPU 커널의 전체 실행 시간
자체 CUDA 시간(Self CUDA time) 하위 연산자(child op)의 실행 시간을 제외한 해당 연산자 자체의 CUDA 실행 시간 ——————————————
개별 연산자 수준 최적화에는 자체 CUDA 시간(Self CUDA time)을 사용하고, 전체 모듈 수준 최적화에는 CUDA 시간(CUDA time)을 사용하는 것이 적절하다.
import torch
import torch.nn as nn
from torch.profiler import profile, ProfilerActivity, record_function
# CUDA GPU를 실행 장치로 설정한다.
device = torch.device('cuda')
# 재현 가능한 결과를 위해 난수 시드를 고정한다.
torch.manual_seed(0)
# 소규모 합성곱 신경망(CNN) 학습 환경을 구성한다.
model = nn.Sequential(
nn.Conv2d(3, 64, 7, stride=2, padding=3), nn.BatchNorm2d(64), nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
nn.Conv2d(128, 256, 3, stride=2, padding=1), nn.BatchNorm2d(256), nn.ReLU(),
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(256, 1000),
).to(device)
# 옵티마이저(optimizer)와 손실 함수(loss function)를 정의한다.
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
def training_step():
# 임의의 입력 배치와 클래스 레이블을 GPU에 생성한다.
x = torch.randn(64, 3, 224, 224, device=device)
y = torch.randint(0, 1000, (64,), device=device)
# 순전파(forward pass)를 수행하고 손실을 계산한다.
out = model(x)
loss = loss_fn(out, y)
# 기울기를 초기화한 뒤 역전파와 파라미터 갱신을 수행한다.
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
# 최초 CUDA 초기화 비용을 제외하기 위해 프로파일링 전에 워밍업한다.
for _ in range(3):
training_step()
# 비동기 CUDA 연산이 완료될 때까지 대기한다.
torch.cuda.synchronize()
print('워밍업 완료')
/usr/local/lib/python3.11/dist-packages/torch/cuda/__init__.py:65: FutureWarning: The pynvml package is deprecated. Please install nvidia-ml-py instead. If you did not install pynvml directly, please report this to the maintainers of the package that installed pynvml for you.
import pynvml # type: ignore[import]
warmed up
# 다섯 번의 학습 단계를 CPU와 CUDA 기준으로 프로파일링한다.
with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof:
for _ in range(5):
# Trace에서 학습 단계 구간을 식별할 수 있도록 레이블을 지정한다.
with record_function('training_step'):
training_step()
# 모든 비동기 CUDA 커널 실행이 끝날 때까지 대기한다.
torch.cuda.synchronize()
# 연산자별 누적 실행 시간과 호출 횟수를 추출한다.
top_ops = []
for evt in prof.key_averages():
top_ops.append({
'name': evt.key,
'cpu_time_us': evt.cpu_time_total,
'cuda_time_us': (
evt.device_time_total
if hasattr(evt, 'device_time_total')
else evt.cuda_time_total
),
'self_cuda_time_us': (
evt.self_device_time_total
if hasattr(evt, 'self_device_time_total')
else evt.self_cuda_time_total
),
'count': evt.count,
})
# 전체 CUDA 실행 시간이 긴 순서로 상위 15개 연산자를 선택한다.
top_ops.sort(key=lambda op: -op['cuda_time_us'])
top_ops = top_ops[:15]
print(f'CUDA 실행 시간 기준 상위 연산자 {len(top_ops)}개를 수집했다.\n')
print(f'{"연산자":<40s} {"CPU(ms)":>10s} {"CUDA(ms)":>10s} {"호출 수":>8s}')
for op in top_ops[:12]:
print(
f'{op["name"][:40]:<40s} '
f'{op["cpu_time_us"]/1000:>10.2f} '
f'{op["cuda_time_us"]/1000:>10.2f} '
f'{op["count"]:>8d}'
)
/usr/local/lib/python3.11/dist-packages/torch/profiler/profiler.py:217: UserWarning: Warning: Profiler clears events at the end of each cycle.Only events from the current cycle will be reported.To keep events across cycles, set acc_events=True.
_warn_once(
Captured 15 top ops (by CUDA time):
op CPU (ms) CUDA (ms) count
autograd::engine::evaluate_function: Con 3.98 28.51 15
ConvolutionBackward0 3.70 28.51 15
aten::convolution_backward 3.62 28.51 15
training_step 0.00 22.72 5
training_step 39.81 22.70 5
aten::conv2d 3.73 12.38 15
aten::convolution 3.67 12.38 15
aten::_convolution 3.39 12.38 15
autograd::engine::evaluate_function: Cud 1.82 10.45 15
CudnnBatchNormBackward0 1.43 10.45 15
aten::cudnn_batch_norm_backward 1.31 10.45 15
aten::cudnn_convolution 2.53 8.95 15
2단계 — 표준 key_averages().table() 요약표
prof.key_averages().table(...)은 프로파일링 결과를 사람이 읽을 수 있는
표 형식으로 출력한다. 성능 문제를 깃허브 이슈(GitHub issue)나 팀
채팅에서 공유할 때 사용할 수 있다.
주요 인자는 다음과 같다.
sort_by—cpu_time_total,cuda_time_total,self_cpu_time_total,self_cuda_time_total,count중 하나를 기준으로 정렬한다.row_limit— 출력할 최대 행 수를 지정한다. 기본값은 25다.group_by_input_shape=True— 입력 텐서 형태(input shape)별로 연산자를 구분한다. 동일한 연산자(예:aten::mm)라도 입력 형태가 다르면 별도의 행으로 표시되므로, 어떤 텐서 형태가 실행 시간을 가장 많이 차지하는지 확인할 수 있다.
🐛 일반적인 실수: 호출 횟수 기준 정렬
count를 기준으로 정렬하면 호출 횟수가 가장 많은 연산자가 표시된다.
일부 분석에는 유용하지만, 호출 횟수가 많다고 해서 전체 실행 시간을 가장
많이 차지하는 것은 아니다. 초기 분석에서는 일반적으로
cuda_time_total을 기준으로 정렬한다.
# CUDA 실행 시간을 기준으로 정렬한 연산자 요약표를 생성한다.
# PyTorch 버전 간 호환성을 위해 cuda_time_total을 사용한다.
table_str = prof.key_averages(
group_by_input_shape=False
).table(
sort_by='cuda_time_total',
row_limit=15,
)
print(table_str)
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg Self CUDA Self CUDA % CUDA total CUDA time avg # of Calls
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
autograd::engine::evaluate_function: ConvolutionBack... 0.33% 276.715us 4.75% 3.979ms 265.258us 0.000us 0.00% 28.514ms 1.901ms 15
ConvolutionBackward0 0.09% 78.921us 4.42% 3.702ms 246.811us 0.000us 0.00% 28.514ms 1.901ms 15
aten::convolution_backward 1.82% 1.524ms 4.32% 3.623ms 241.549us 26.521ms 39.49% 28.514ms 1.901ms 15
training_step 0.00% 0.000us 0.00% 0.000us 0.000us 22.718ms 33.83% 22.718ms 4.544ms 5
training_step 20.76% 17.404ms 47.49% 39.811ms 7.962ms 0.000us 0.00% 22.696ms 4.539ms 5
aten::conv2d 0.08% 63.111us 4.45% 3.729ms 248.632us 0.000us 0.00% 12.381ms 825.392us 15
aten::convolution 0.33% 274.614us 4.37% 3.666ms 244.425us 0.000us 0.00% 12.381ms 825.392us 15
aten::_convolution 0.35% 289.608us 4.05% 3.392ms 226.117us 0.000us 0.00% 12.381ms 825.392us 15
autograd::engine::evaluate_function: CudnnBatchNormB... 0.46% 388.718us 2.17% 1.819ms 121.256us 0.000us 0.00% 10.449ms 696.618us 15
CudnnBatchNormBackward0 0.14% 117.601us 1.71% 1.430ms 95.342us 0.000us 0.00% 10.449ms 696.618us 15
aten::cudnn_batch_norm_backward 0.75% 625.724us 1.57% 1.313ms 87.502us 10.449ms 15.56% 10.449ms 696.618us 15
aten::cudnn_convolution 1.68% 1.407ms 3.01% 2.525ms 168.358us 8.953ms 13.33% 8.953ms 596.836us 15
_5x_cudnn_ampere_scudnn_128x128_stridedB_splitK_medi... 0.00% 0.000us 0.00% 0.000us 0.000us 6.937ms 10.33% 6.937ms 1.387ms 5
void cudnn::bn_bw_1C11_kernel_new<float, float, floa... 0.00% 0.000us 0.00% 0.000us 0.000us 6.308ms 9.39% 6.308ms 1.262ms 5
aten::batch_norm 0.08% 63.860us 2.46% 2.059ms 137.291us 0.000us 0.00% 5.871ms 391.380us 15
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 83.825ms
Self CUDA time total: 67.152ms
3단계 — 메모리 프로파일링: 어떤 연산자가 가장 많은 GPU 메모리를 할당하는가?
메모리 단편화(memory fragmentation)와 최대 메모리 사용량 문제는 메모리
부족(OOM, Out of Memory)과 함께 AI 모델 학습 실패의 주요 원인이다.
profile_memory=True를 설정하면 학습 과정에서 발생하는 cudaMalloc과
cudaFree 이벤트를 기록하고, 메모리 할당을 발생시킨 연산자와 연결한다.
일반적으로 모델 가중치(weight)보다 활성값(activation)이 더 많은
메모리를 차지한다. 예를 들어, ResNet의 한 번의 순전파(forward pass)는
모델 파라미터 용량보다 5~10배 많은 메모리를 할당할 수 있다. 이러한
활성값은 역전파(backward pass) 과정에서 해제된다. 최대 메모리 할당량이
예상보다 크다면 캐시된 중간 출력이나 누락된 .detach() 호출로 인해
텐서가 계속 유지되는지 확인해야 한다.
구분해야 할 두 가지 지표
- 자체 CUDA 메모리 사용량(Self CUDA memory usage) — 해당 연산자가 순수하게 할당한 메모리다. 연산자 내부의 할당량에서 해제량을 뺀 값이다.
- 최대 메모리 사용량(Peak memory usage) — 해당 연산자의 실행 범위에서 기록된 최고 메모리 사용량이다.
메모리 부족(OOM)을 방지하려면 일반적으로 최대 메모리 사용량을 줄이는 것이 중요하다.
메모리 추적을 포함한 프로파일링
profile_memory=True를 설정하면 프로파일러가 연산자별 메모리 할당자
이벤트(allocator event)도 기록한다. 이를 통해 GPU 메모리(VRAM)를 많이
사용하는 연산자를 찾고, 활성값 체크포인팅(activation checkpointing,
순전파 중 일부 활성값만 저장하고 나머지는 역전파 시 다시 계산해 GPU
메모리 사용량을 줄이는 기법)이나 연산자 융합(operator fusion, 여러
연산을 하나의 커널로 결합해 메모리 접근과 커널 실행 오버헤드를 줄이는
최적화 기법)의 적용 대상을 검토할 수 있다.
record_shapes=True는 입력 텐서 형태 정보를 함께 저장한다. 따라서
보고서에서 linear-[32×1024]와 linear-[8×768]처럼 동일한 연산자라도
입력 형태가 다른 실행을 구분할 수 있다.
# 메모리 사용량과 입력 텐서 형태를 포함해 프로파일링한다.
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
profile_memory=True,
record_shapes=True,
) as prof_mem:
for _ in range(3):
training_step()
# 비동기 CUDA 연산을 완료한 후 측정을 종료한다.
torch.cuda.synchronize()
CUDA 메모리 사용량을 기준으로 연산자 순위 지정
key_averages()를 순회하면서 전체 메모리 할당량이 1 MiB를 초과하는
연산자만 수집한다. PyTorch 버전에 따라 메모리 속성 이름이 다를 수 있다.
일부 버전은 cuda_memory_usage를 사용하고, 최신 버전은
device_memory_usage를 사용하므로 코드는 두 속성을 순서대로 확인한다.
결과를 내림차순으로 정렬하면 가장 위에 표시되는 항목이 GPU 메모리를 가장 많이 사용하는 연산자다.
# 연산자별 CUDA 메모리 사용량을 수집한다.
mem_ops = []
for evt in prof_mem.key_averages():
# PyTorch 버전에 따라 메모리 속성 이름이 다를 수 있다.
cuda_mem = getattr(evt, 'device_memory_usage', None)
if cuda_mem is None:
cuda_mem = getattr(evt, 'cuda_memory_usage', 0)
self_cuda_mem = getattr(evt, 'self_device_memory_usage', None)
if self_cuda_mem is None:
self_cuda_mem = getattr(evt, 'self_cuda_memory_usage', 0)
# 메모리 사용량이 1 MiB를 초과하는 연산자만 유지한다.
if abs(cuda_mem) > 1024 * 1024:
mem_ops.append({
'name': evt.key,
'cuda_mem_mib': abs(cuda_mem) / (1024 * 1024),
'self_cuda_mem_mib': abs(self_cuda_mem) / (1024 * 1024),
'count': evt.count,
})
# 전체 CUDA 메모리 사용량이 큰 순서로 상위 10개를 선택한다.
mem_ops.sort(key=lambda m: -m['cuda_mem_mib'])
mem_ops = mem_ops[:10]
print('CUDA 메모리 할당량 기준 상위 연산자\n')
print(f'{"연산자":<40s} {"전체(MiB)":>12s} {"자체(MiB)":>12s} {"호출 수":>8s}')
for op in mem_ops:
print(
f'{op["name"][:40]:<40s} '
f'{op["cuda_mem_mib"]:>12.1f} '
f'{op["self_cuda_mem_mib"]:>12.1f} '
f'{op["count"]:>8d}'
)
Top memory-using ops (total CUDA memory allocated):
op total (MiB) self (MiB) count
aten::empty 2173.0 2173.0 99
[memory] 1140.0 1140.0 42
aten::batch_norm 1029.0 0.0 9
aten::_batch_norm_impl_index 1029.0 0.0 9
aten::cudnn_batch_norm 1029.0 0.0 9
CudnnBatchNormBackward0 1029.0 0.0 9
aten::cudnn_batch_norm_backward 1029.0 0.0 9
aten::empty_like 1029.0 0.0 12
aten::conv2d 1029.0 0.0 9
aten::convolution 1029.0 0.0 9
4단계 — 시각적 분석을 위한 크롬 트레이스(Chrome trace) 내보내기
표는 전체 결과를 요약하는 데 유용하지만, 병목 구간은 타임라인(timeline)에서 더 명확하게 확인할 수 있다. 잘 구성된 프로파일링 결과에서는 CUDA 스트림(stream)이 레인(lane)으로 표시되고, GPU 커널은 색상 막대로 표시되며, CPU 스레드 작업도 함께 나타난다.
타임라인에서는 다음 항목을 확인할 수 있다.
- 유휴 구간(idle gap) — CPU가 작업을 충분히 빠르게 전달하지 못해 GPU가 대기하는 시간이다.
- 커널 직렬화(kernel serialization) — 암시적 동기화 장벽(sync barrier)으로 인해 여러 커널이 동시에 실행되지 못하는 현상이다.
- 호스트-디바이스 전송(host-to-device transfer) — 데이터 전송과 연산이 중첩되는지 확인한다. 중첩되면 효율적이고, 중첩되지 않으면 최적화가 필요하다.
PyTorch의 크롬 트레이스는 표준 JSON 형식이다. 크롬 기반 브라우저의
chrome://tracing에서 열거나
ui.perfetto.dev에 업로드해 분석할 수 있다.
퍼페토(Perfetto)는 다양한 브라우저에서 사용할 수 있으며 분석
인터페이스가 더 편리하다.
추가 기능: torch.profiler 스케줄 API(schedule API)
장시간 학습에서는 모든 단계를 프로파일링하면 실행 속도가 느려지고
트레이스 파일이 지나치게 커질 수 있다. schedule 인자를 사용하면 특정
구간만 선택적으로 프로파일링할 수 있다. 다음 예시는 대기 5단계, 워밍업
5단계, 실제 측정 5단계를 수행한다.
prof = profile(
activities=[...],
schedule=torch.profiler.schedule(wait=5, warmup=5, active=5, repeat=1),
on_trace_ready=torch.profiler.tensorboard_trace_handler('./tb_logs'),
)
이 구성과 텐서보드(TensorBoard)를 함께 사용하는 방식은 대표적인 운영
환경 프로파일링 패턴이다. 텐서보드 플러그인을 사용하려면
torch_tb_profiler를 설치한다.
# 프로파일링 결과를 크롬 트레이스 JSON 파일로 저장한다.
chrome_trace_path = '/tmp/pytorch_trace.json'
prof.export_chrome_trace(chrome_trace_path)
import os
import json
# 트레이스 파일 크기와 이벤트 수를 확인한다.
size_kb = os.path.getsize(chrome_trace_path) / 1024
with open(chrome_trace_path, encoding='utf-8') as f:
trace = json.load(f)
events = trace.get('traceEvents') if isinstance(trace, dict) else trace
# 이벤트 범주(category)별 발생 횟수를 집계한다.
event_types = {}
for event in events:
category = event.get('cat', 'unknown')
event_types[category] = event_types.get(category, 0) + 1
print(
f'크롬 트레이스 저장 완료: {chrome_trace_path} '
f'({size_kb:.1f} KB, 이벤트 {len(events)}개)'
)
print('\n트레이스에 포함된 이벤트 범주:')
for category, count in sorted(
event_types.items(),
key=lambda item: -item[1],
)[:8]:
print(f' {category:<25s} {count:>6d}개')
print('\nchrome://tracing 또는 ui.perfetto.dev에서 타임라인을 분석할 수 있다.')
Chrome trace written: /tmp/pytorch_trace.json (994.9 KB, 4118 events)
Event categories (what's in the trace):
cpu_op 2085 events
ac2g 897 events
cuda_runtime 512 events
kernel 340 events
fwdbwd 150 events
unknown 62 events
gpu_memset 40 events
user_annotation 15 events
Open at chrome://tracing or ui.perfetto.dev to explore the timeline.
실습 결과
Python 코드만으로 실행 가능한 프로파일링 흐름을 구성했다. 여기에는 실행 시간 요약표, 연산자별 메모리 할당 순위, 크롬 트레이스 내보내기가 포함된다.
Nsight Systems Profiling 결과와 결합하면 시스템 전체 타임라인과 연산자 수준 세부 분석을 모두 수행할 수 있다. 이후의 성능 최적화 작업은 이 두 관점 중 하나 또는 두 관점을 함께 사용해 진행할 수 있다.
참고 자료
- PyTorch 프로파일러 자습서 — 단계별 프로파일링 예제를 제공한다.
- 프로파일러 API 참조
문서 — 인자,
스케줄,
record_function,record_shapes를 설명한다. - Kineto / TensorBoard 프로파일러
플러그인
—
pip install torch_tb_profiler로 대화형 브라우저 사용자 인터페이스를 사용할 수 있다. - Chrome Trace Event Format 명세 — 사용자 정의 트레이스를 생성할 때 활용할 수 있는 기본 JSON 형식이다.
- Making Deep Learning Go Brrrr — 프로파일링 데이터를 해석하기 위한 개념적 모델을 설명한다.
댓글남기기