Lightning AI 의 Taylor Robbie 가 — “장표보다 직접 손에 잡히는 게 낫다” 는 입장으로 — 실제 모델을 nsys/Nsight Compute/PyTorch profiler 위에 펼쳐 어디가 비는지, 어떤 kernel 이 dominant 한지, 무엇이 진짜 actionable hint 인지를 보여주는 라이브 워크숍. NVTX 마커 추가, dynamic shape 의 함정, memory profiler 까지.
Profiling 강의의 흔한 함정 — 도구의 사용법은 가르치지만 “진단 결과를 받았을 때 무엇을 해야 하는가” 까지는 깊게 안 다룬다. Taylor 의 강의는 정확히 그 자리를 푼다. 실제 production 모델을 화면에 띄우고 — 어떤 metric 이 actionable 하고, 어떤 metric 이 그저 정보일 뿐인지를 라이브로 시연.
강의가 답하는 세 질문.
Taylor 의 입장이 명시적으로 “prescriptive” — “이 결과를 보면 다음에 무엇을 해라” 의 처방. profiling 은 도구가 아니라 의사결정 입력. 같은 nsys 화면을 두 사람이 보는데 한 명만 다음 step 을 안다면 — 도구의 문제가 아니라 frame 의 문제.
Taylor 가 강의 초반에 명확히 정리하는 자리 — 세 도구를 같은 “profiler” 라는 이름으로 묶으면 안 된다. 각자 다른 질문을 답한다.
NCU 는 대부분의 cloud 환경에서 막혀 있다 — root 권한 또는 NVIDIA 의 “PerfWorks Compatibility Mode” 가 필요. 그래서 production 환경에서는 nsys + pytorch profiler 만 쓰는 경우가 많다. 자기 데스크탑이나 dedicated dev box 에서 NCU 로 다시 봐야 함.
Taylor 가 “라이브 워크숍의 첫 trick” 으로 보여준 것 — 어떤 trace 든 처음엔 의미가 안 잡힌다. NVTX 마커로 사용자 코드의 의미 단위를 timeline 위에 직접 그린다.
import torch.cuda.nvtx as nvtx
for i, batch in enumerate(loader):
nvtx.range_push(f"step_{i}")
nvtx.range_push("forward")
out = model(batch)
loss = criterion(out, target)
nvtx.range_pop()
nvtx.range_push("backward")
loss.backward()
nvtx.range_pop()
nvtx.range_push("optimizer")
optimizer.step()
optimizer.zero_grad()
nvtx.range_pop()
nvtx.range_pop() # step
nsys 로 launch:
nsys profile -t cuda,nvtx,osrt \
--capture-range=cudaProfilerApi \
-o trace python train.py
결과 — timeline 위에 “step_0”, “forward”, “backward”, “optimizer” 가 nested range 로 표시. iteration 안에서 어디가 비는지가 명확해진다.
PyTorch 가 자동으로 추가하는 NVTX 마커도 있다 — autograd 의 op 이름들. 하지만 사용자가 자기 모듈 단위로 한 단계 추가하는 게 가독성을 비약적으로 높인다.
Taylor 가 가장 자주 마주치는 오해 — “warp occupancy 가 낮으니까 이게 병목” 이라고 단정 짓는 것. 사실 occupancy 와 throughput 은 직교한다. 낮은 occupancy 로도 peak throughput 이 나는 kernel 이 있고, 높은 occupancy 인데 throughput 이 안 나는 kernel 도 있다.
Taylor 의 처방 — “occupancy 와 함께 항상 throughput 을 본다”. NCU 의 “SM throughput” 또는 “Compute throughput” metric. 그 둘이 모두 낮으면 launch shape 문제, occupancy 만 낮고 throughput 은 충분하면 register 가 일하는 중 (FA 처럼), throughput 만 낮으면 memory-bound — 다른 처방.
trace 위에 뜨는 kernel 이름이 길고 흉하다 — 하지만 각 부분이 정보다. Taylor 가 각 부분을 어떻게 읽는지 라이브로 시연.
# 예 — fp16 GEMM kernel 한 자리
void cutlass::Kernel<
cutlass_80_tensorop_f16_s16816gemm_f16_128x128_32x4_tn_align8
>(...);
각 부분의 의미.
cutlass — CUTLASS template 위에서 만들어진 kernel.80 — Ampere (SM 8.0) 용. 이게 자기 GPU 와 맞아야 한다. H100 위에서 80 prefix 면 fallback — 차선.tensorop — Tensor Core 사용. (없으면 CUDA core 만 — 큰 손해)f16_s16816 — fp16 input, MMA 모양 16×16×16.128x128_32x4 — CTA tile 128×128, K 축 32, num_stages 4.tn — A 가 transposed (T), B 가 normal (N).align8 — 8-element aligned load (8×16-bit = 128-bit vector).이 한 줄에서 즉시 진단 가능한 것.
contiguous() 추가.indexSelectLargeIndex, SmallIndexFunctor 같은 이름은 — embedding lookup 의 batch size 분기. “Small” 인지 “Large” 인지가 cross-over 점. Taylor 의 § 06 사례.
강의의 가장 인상적인 사례. Taylor 가 production 모델에서 “이 자리는 fast embedding kernel 을 쓰면 50 µs 정도다” 라고 baseline 을 알고 있었는데 — 자기 모델에서 쟀더니 100 µs. 같은 kernel 이 2× 느렸다. 어떻게 풀었나.
SmallIndexFunctor 이 아니라 indexSelectLargeIndex 가 떠 있었다. “이 size 에는 small 이 빨라야 하는데 왜 large 가 떠 있나”.“fast” 라는 라벨이 붙은 kernel 도 입력 분포에 따라 잘못된 path 로 dispatch될 수 있다. 그게 actionable hint — “현재 kernel 이름” + “기대 kernel 이름” 의 차이가 진단의 단서.
대형 모델의 inference 또는 일부 학습에서 — input shape 가 매번 다르다. Taylor 가 이런 dynamic shape workload 에서 자주 만나는 두 cost.
cost 1 — torch.compile 의 recompile. compile 의 cache 가 shape 별로 잡혀 있어서, 새 shape 가 들어오면 recompile. 1-2 초 stall. 처음 N iteration 만 그렇고 그 다음엔 cache hit — 하지만 production 의 처음 N 번이 critical.
해결 — torch.compile(dynamic=True) 또는 mark_dynamic 으로 일부 dim 을 dynamic 으로 표시. 컴파일러가 단일 generic 커널 생성. 속도는 약간 떨어지지만 recompile 안 일어남.
cost 2 — cudaMalloc 의 비용. 매 iteration 마다 새 size 의 buffer 가 필요. CUDA caching allocator (PyTorch 의 default) 가 이전 size 의 buffer 를 재사용 못 하면 새 cudaMalloc — 0.1-1 ms stall.
해결 — pre-allocated pool, torch.cuda.set_per_process_memory_fraction, 또는 caching_allocator_warmup. PyTorch profiler 의 memory section 으로 확인.
PyTorch profiler 에 profile_memory=True 와 record_shapes=True. 그 trace 안에 같은 op 가 다른 shape 로 매 iteration 마다 다시 나타나는지 확인. 같은 op 가 cache 된 형태면 OK.
Memory 진단은 보통 두 형태 — steady state (학습이 도는 동안 일관된 점유) 와 peak (가장 높은 자리). PyTorch profiler 의 memory snapshot 기능이 둘을 분리해서 본다.
import torch.profiler as profiler
from torch.profiler import profile, record_function
# memory profile 켜기
with profile(profile_memory=True,
record_shapes=True) as prof:
for _ in range(5):
out = model(batch)
loss = criterion(out, label)
loss.backward()
optimizer.step()
# peak vs allocated 분리
print(prof.key_averages().table(
sort_by="self_cuda_memory_usage",
row_limit=10))
Taylor 가 강조한 디테일.
처방. activation 이 큰 자리 → gradient checkpointing (recompute). optimizer 가 큰 자리 → ZeRO 또는 8-bit Adam. fragment 가 큰 자리 → allocator 의 fragmentation 분석.
PyTorch 2.1+ 의 torch.cuda.memory._record_memory_history 로 전체 memory trace 를 dump 한 뒤 memory viz 에 넣으면 — 시간별 alloc/free 를 시각적으로. peak 가 어떤 op 위에 있는지 정확히 보임.
Taylor 가 강의에서 짚은 디테일 — chrome trace 의 flame chart 와 icicle plot 의 차이. 두 시각이 같은 정보를 거꾸로 보여준다.
flame chart — 함수 호출 stack 을 위로 쌓는다. 가장 많이 시간 쓰는 함수가 가장 넓은 막대로. CPU profiler 의 표준.
icicle plot — 같은 stack 을 위에서 아래로. CPU 코드와 GPU 코드를 같은 화면에 놓을 때 GPU 의 “이 kernel 이 어떤 CPU op 가 launch 한 결과인가” 의 부모-자식 관계를 표현.
chrome://tracing 의 PyTorch profiler 가 icicle 을 쓴다. 위쪽 lane 이 CPU, 아래쪽이 GPU. flow event 화살표가 CPU op 와 그것이 launch 한 GPU kernel 을 잇는다.
icicle 위에서 — (1) GPU lane 의 큰 kernel 부터 본다, (2) 그 kernel 의 flow source CPU op 를 따라 올라간다, (3) 그 CPU op 의 부모 stack 을 본다. “이 GPU 일은 어디서 왔는가”의 답을 한 번에. profiler 의 가장 강력한 사용 자리.
GPU lane 은 “하나의 SM 의 timeline” 이 아니다. 모든 SM 이 한 kernel 을 같이 돌면 그 kernel 이 한 줄에 그려진다. multiple stream 이면 다른 줄. nsys 도 같은 방식.
mark_dynamic 또는 pre-allocated pool 로 풀음.--capture-range 활용.mark_dynamic 적용 후 비교.torch.cuda.memory._record_memory_history 로 dump 후 web viz. peak 가 어떤 op 위인지 확인.record_function) 와 비교. overhead 차이.이 노트의 § 04 timing 수치 (50 µs, 100 µs 등) 는 강의 paraphrase. 자기 GPU + 자기 모델에서 직접 측정해야 baseline. nsys 와 NCU 를 자기 데스크탑에서 한번씩 직접 돌리는 게 강의 학습의 마무리.