Hugging Face가 2026년 5월 29일 PyTorch profiling 시리즈의 첫 글을 공개했습니다. 주제는 torch.profiler입니다. 글은 matrix multiplication 뒤에 bias add를 붙인 아주 작은 예제에서 시작해, profiler table과 Chrome trace를 어떻게 읽는지 설명합니다. 실무 개발자에게 이 주제가 중요한 이유는 간단합니다. LLM inference, fine-tuning, embedding batch job, vision 모델 학습에서 느린 지점을 감으로 고치면 대부분 실패합니다.
“GPU를 쓰는데 왜 느리지?”라는 질문은 거의 매주 나옵니다. 답은 모델이 커서일 수도 있지만, CPU launch overhead, warmup 누락, 작은 batch, 데이터 로딩, kernel gap, compile graph break, synchronization, memory allocation 때문일 수도 있습니다. torch.profiler는 이 문제를 숫자와 시간축으로 나눠 보여줍니다.
초보자가 가장 자주 하는 실수는 GPU가 놀고 있으면 “GPU가 나쁘다”거나 “PyTorch가 느리다”고 결론 내리는 것입니다. 실제로는 작은 연산을 너무 자주 던져서 CPU가 kernel launch를 준비하는 시간이 대부분일 수 있습니다. Hugging Face 예제에서도 64x64 matmul에서는 GPU kernel 시간보다 CPU 쪽 준비 시간이 훨씬 크게 보입니다. 반대로 4096x4096으로 키우면 GPU 시간이 의미 있게 늘면서 compute-bound에 가까워집니다.
이 차이를 모르면 엉뚱한 최적화를 합니다. 모델을 compile하거나 dtype을 바꾸기 전에, 지금 문제가 overhead-bound인지 compute-bound인지부터 봐야 합니다. 같은 코드라도 batch size, sequence length, matrix size에 따라 병목이 완전히 달라집니다.
torch.profiler를 쓸 때 가장 먼저 익힐 것은 두 출력물의 역할 차이입니다. profiler table은 “무엇이 시간을 많이 쓰는가”를 보여줍니다. trace는 “언제, 왜, 어떤 순서로 실행됐는가”를 보여줍니다.
기본 흐름은 다음과 같습니다.
with torch.profiler.profile(
activities=[
torch.profiler.ProfilerActivity.CPU,
torch.profiler.ProfilerActivity.CUDA,
],
) as prof:
for _ in range(5):
step()
prof.step()
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=15))
prof.export_chrome_trace("trace.json")
그리고 중요한 함수에는 record_function으로 이름을 붙입니다.
def step():
with torch.profiler.record_function("matmul_add"):
return fn(x, w, b)
이 작은 annotation이 trace를 읽을 때 큰 차이를 만듭니다. 실제 프로젝트에서는 forward_pass, tokenize_batch, rerank_topk, decode_step, loss_backward처럼 업무 단위 이름을 붙여야 합니다.
profiler table에는 Self CPU, CPU total, Self CUDA, CUDA total 같은 열이 나옵니다. Self는 해당 event 자체에서 쓴 시간이고, total은 자식 event까지 포함한 시간입니다. 예를 들어 matmul_add의 total time은 그 안에서 호출된 aten::matmul, aten::add, CUDA runtime call까지 포함할 수 있습니다.
실무에서 이 구분을 안 하면 병목을 잘못 잡습니다. wrapper 함수의 total이 크다고 wrapper 자체가 문제는 아닙니다. 반대로 작은 helper 함수가 수천 번 호출되면서 Self CPU time을 크게 먹을 수도 있습니다.
추천 절차는 이렇습니다.
cuda_time_total 기준으로 가장 큰 GPU event를 봅니다.self_cpu_time_total 기준으로 CPU overhead가 큰 event를 봅니다.# of Calls가 비정상적으로 많은 event를 찾습니다.이 순서로 보면 “연산이 무거운 것”과 “실행 준비가 비싼 것”을 분리할 수 있습니다.
Perfetto나 Chrome trace를 열면 CPU lane과 GPU lane이 보입니다. 막대는 실행 시간을 의미하고, 빈 공간은 대기나 idle time입니다. Hugging Face 글에서는 record_function("matmul_add") 진입 후 aten::matmul dispatch 전까지 약 228µs의 dead window가 보이는 예시를 설명합니다. 이런 gap은 workspace allocation, cuBLAS heuristics, lazy module loading 같은 초기화 작업에서 나올 수 있습니다.
또 CPU가 CUDA kernel을 submit한 뒤 GPU lane에서 실제 실행이 늦게 시작되는 offset도 보일 수 있습니다. 글에서는 약 2.5ms offset과 activity buffer request 사례를 다룹니다. 중요한 것은 gap이 보인다고 바로 최적화 코드를 쓰지 않는 것입니다. 먼저 warmup, profiler schedule, iteration 수를 바꿔 재현되는 gap인지 확인해야 합니다.
trace를 읽을 때는 다음 질문을 던지면 좋습니다.
GPU profiling에서 warmup은 선택이 아닙니다. 첫 실행에는 lazy initialization, kernel selection, cache 준비, memory allocation 같은 일회성 비용이 섞입니다. 이 비용까지 최적화 대상으로 보면 잘못된 결론을 냅니다.
torch.profiler.schedule을 쓰면 wait, warmup, active 구간을 나눌 수 있습니다.
schedule = torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1)
wait는 초기 노이즈를 건너뛰고, warmup은 profiler를 켜되 기록하지 않는 구간이며, active가 실제 분석 대상입니다. 실무에서는 별도 warmup loop도 같이 두는 편이 안전합니다. 특히 LLM inference 서버는 model load 직후 첫 요청과 steady-state 요청의 latency가 다릅니다. 운영 지표는 steady-state와 cold-start를 따로 봐야 합니다.
많은 팀이 성능 문제를 만나면 바로 torch.compile을 붙입니다. 물론 도움이 될 수 있습니다. 하지만 baseline trace 없이 compile 결과만 보면 무엇이 좋아졌는지 모릅니다. kernel fusion으로 add가 합쳐졌는지, graph break 때문에 효과가 없는지, compile overhead 때문에 첫 요청이 느려졌는지 구분해야 합니다.
권장 순서는 다음입니다.
torch.compile을 적용합니다.LLM 서비스에서는 평균 latency보다 p95, p99가 더 중요할 수 있습니다. compile이 평균은 줄여도 shape 변화에서 graph break가 생기면 tail latency가 튈 수 있습니다.
LLM inference에서 profiler를 쓸 때는 전체 요청을 한 덩어리로 보지 말고 단계별로 나누세요.
각 단계에 record_function을 붙이면 어느 구간이 CPU bound인지 GPU bound인지 보입니다. 특히 streaming 응답에서는 decode step이 작고 반복적이라 launch overhead와 synchronization이 문제가 될 수 있습니다. embedding batch job에서는 batch size가 작아 GPU가 놀 수 있고, RAG pipeline에서는 retrieval이나 reranking이 모델보다 느릴 수 있습니다.
성능 개선은 profiler 결과에 맞춰야 합니다. overhead-bound면 batch를 키우거나 operation을 fuse하거나 compile을 검토합니다. compute-bound면 dtype, kernel, model architecture, quantization을 봅니다. CPU preprocessing이 병목이면 DataLoader, tokenization parallelism, cache를 봅니다.
torch.profiler.record_function 이름을 붙입니다.torch.compile 적용 전후를 같은 조건으로 비교합니다.torch.profiler의 목적은 예쁜 trace 이미지를 만드는 것이 아닙니다. 병목을 추측에서 증거로 바꾸는 것입니다. GPU가 느린지, CPU가 바쁜지, launch overhead가 큰지, 첫 step만 느린지 숫자로 확인한 뒤에야 최적화가 시작됩니다.
참고: Hugging Face Blog, “Profiling in PyTorch (Part 1): A Beginner's Guide to torch.profiler”, 2026-05-29.