gpumode · 강의 아카이브
《GPU Mode》 L053 2024 High priority transcript · failed

torch.compile Q&A — Richard Zou

PyTorch core 의 Richard Zou 가 깐 torch.compile 의 내부 — TorchDynamo 의 graph 캡처, AOT autograd, FakeTensor, dynamic shape, Inductor 의 lowering, cudagraph 의 위치. “왜 graph break 가 일어나나, dynamic shape 가 왜 잘 안 되나, 어디서 디버깅을 시작하나” 의 실전 질문에 대한 답. 본 페이지는 transcript 가 실패해 PyTorch 공식 문서, dev-discuss 토론, Richard 의 공개 talk 으로 재구성됐다.

torch.compile TorchDynamo AOT autograd Inductor FakeTensor dynamic shape graph break cudagraph
R
Speaker
Richard Zou
Meta · PyTorch core · functorch / torch.compile / vmap
강의 번호
L053
스피커
Richard Zou
Transcript
failed · 본 노트는 재구성
학습 우선순위
High · 정독
§ 01강의가 풀려는 문제· why this lecture exists

“torch.compile 한 줄 추가했더니 왜 안 빨라지는가” 의 실전 답

PyTorch 2.0 의 torch.compile() 은 한 줄 wrapping 으로 fusion 을 잡아주는 기적적인 도구로 광고되지만 — 실전에서 학습 코드를 감싸면 graph break 가 줄줄이 터지거나, recompile 이 매 step 일어나거나, dynamic shape 에서 행이 멈춘다. 이 강의는 왜 그런 일이 일어나는지 의 내부 답을 깐다.

강의의 출발 질문 셋.

  1. graph break 가 정확히 무엇이고 어디서 일어나는가?
  2. dynamic shape 가 잡힐 때와 안 잡힐 때의 차이는?
  3. backward pass 까지 어떻게 같이 컴파일되는가? (AOT autograd 의 의미)

본 노트는 transcript 실패로 — PyTorch 공식 문서, dev-discuss 의 Dynamo/Inductor 디자인 RFC, Richard Zou 의 다른 talk (PyTorch Conference, EuroPython) 을 종합한 재구성.

강의의 frame

torch.compile 을 한 줄짜리 마법으로 보면 안 된다. 안에는 4개의 거의 독립적인 시스템 이 있다 — Dynamo (graph 캡처), AOT autograd (backward 합성), FakeTensor (shape 추론), Inductor (lowering). 각자 다른 자리에서 다른 이유로 깨진다.

“torch.compile 의 디버깅은 — 어느 시스템에서 깨졌는지를 먼저 식별하는 것. 그 식별이 가능해지는 순간 90% 의 문제가 풀린다.” 학습 노트 · 재구성
§ 02graph break 가 일어나는 자리· Dynamo 의 한계

“이 코드 라인을 graph 로 못 잡는다” — Dynamo 의 거부 사유들

TorchDynamo 는 Python bytecode 위에서 동작한다 — 함수가 호출되면 bytecode 를 walk 하면서 “이 op 가 graph 에 들어갈 수 있나” 를 체크한다. 못 들어가는 자리에서 graph break — 거기서 graph 를 끊고, 그 라인은 일반 Python 으로 돌고, 그 후 다시 graph 캡처 시작.

FIG · torch.compile 의 4-stage 파이프라인Python → Triton
L0 · capture
Dynamo
Python bytecode 위에서 graph 캡처. graph break 가 일어나는 자리.
L1 · trace
FakeTensor
실제 Tensor 안 만들고 shape/dtype 만 추적. AOT trace 의 substrate.
L2 · backward
AOT autograd
forward graph 에서 backward graph 를 같이 만든다. 두 graph 가 짝이 되어 lowering.
L3 · lower
Inductor
FX graph → Triton/C++/cpp_wrapper. fusion · scheduling · vectorization 결정.
L4 · run
Triton kernel
생성된 코드가 GPU 위에서 실행. cudagraph 가 따로 wrapping 가능.
L0 가 graph break 의 자리, L1 이 dynamic shape 의 자리, L2 가 backward 의 자리, L3 가 fusion 의 자리. 각자 다른 디버깅 도구.

graph break 를 일으키는 흔한 패턴.

패턴왜 깨지는가회피
if x.item() > 0: tensor 의 값을 Python 조건문에 쓰면 — Dynamo 가 값을 안 만들고 graph 만 짜기 때문에 즉시 break. data-dependent control flow 라고 부름. torch.where
print(tensor) side effect. 디버깅 print 가 학습 코드에 남으면 그 자리에서 break. 로깅 분리
.numpy() / .tolist() tensor 를 Python 객체로 변환. graph 영역 밖으로 나간다. tensor 유지
unsupported op Dynamo 가 아직 lowering 못 하는 ATen op 또는 custom op. 점점 줄지만 매 PyTorch 버전마다 변동. torch.compile.allow_in_graph
try/except 예외 처리 자체는 가능하지만 except 안의 logic 이 어렵다. 예외 분리
.cpu() in middle device transition. 의도한 거면 OK 지만, 의도 안 한 경우 보통 .item() 의 우회. 의도 확인
graph break 가 무조건 나쁜 건 아니다

graph break 가 한 번 나도 — 그 전후의 두 sub-graph 는 각자 컴파일된다. 단, fusion 이 break 경계를 넘지 못 한다는 점이 손해. “이 break 가 fusion 을 깨고 있는지” 가 진짜 질문. 학습 루프 시작/끝 의 break 는 거의 무해.

“graph break 자체보다 — break 가 hot loop 안에 있느냐가 중요하다. step 마다 한 번 break 면 큰 손해, epoch 의 begin/end 면 무해.” 학습 노트 · 재구성
§ 03dynamic shape 처리· guard · symbolic shape

같은 함수가 다른 shape 으로 들어올 때 무엇이 일어나는가

torch.compile 은 처음 호출될 때 그 입력의 shape 으로 graph 를 만든다. 그 다음 호출 — 다른 shape 이면? 두 가지 모드. (1) static (default) — recompile. (2) dynamic — symbolic shape 으로 graph 를 만들어 두고 다음 호출에서 재사용.

static 모드의 함정 — 매 step batch size 가 다른 학습 (가변 길이 sequence, dynamic batching) 에서 매번 recompile. compile 비용이 1~5초인데, 매 step 이 100ms 면 — compile 비용이 학습 비용보다 큼.

dynamic 모드의 함정 — 모든 차원이 dynamic 이라고 처리되면 fusion 이 잘 안 됨 (Inductor 가 shape 을 모르면 vectorization 결정 어려움). 보통 “batch dim 만 dynamic, 나머지 static” 이 좋음.

guard 의 의미

compile 된 graph 에는 guard 가 같이 박힌다 — “이 graph 는 batch=32, seq=512, dtype=fp16 일 때만 valid” 같은 조건. 호출 시 guard 가 false 면 recompile. 너무 많은 guard 가 박히면 recompile 폭주.

# dynamic shape 의 세 모드
torch.compile(fn)                           # default · static
torch.compile(fn, dynamic=True)             # 모든 차원 symbolic
torch.compile(fn, dynamic=None)             # automatic — 첫 recompile 후 dynamic

# 차원별 마킹
from torch import _dynamo as dynamo
dynamo.mark_dynamic(x, 0)                # dim 0 만 dynamic

# compile log 보기
import os
os.environ["TORCH_LOGS"] = "recompiles"
FIG · 같은 함수가 두 번 호출될 때의 결정 트리schematic
호출 시퀀스일어나는 일cost
f(x; B=32) (1st) cold compile. Dynamo trace + AOT + Inductor + Triton compile + first run. ~3 sec
f(x; B=32) (2nd) guard 통과 — graph 재사용. 거의 raw kernel 비용만. ~10 ms
f(x; B=64) static guard 실패 — recompile. 새 graph 생성. cache 에 둘 다 보관. ~3 sec
f(x; B=64) dynamic guard 통과 (B 가 symbolic). 같은 graph 재사용. ~10 ms
f(x; B=128) static 또 recompile. cache 에 셋. 16번 누적되면 recompile limit 도달 — “torch._dynamo.config.cache_size_limit” warning. ~3 sec ↑
cache_size_limit 가 default 8. 그 이상이면 자동 disable 되고 Dynamo 가 eager 로 fallback. 학습 코드에서 “이상하게 느려졌다” 의 흔한 원인.
§ 04FakeTensor 의 역할· shape inference

실제 메모리 안 잡고 graph 를 trace 하는 방법

graph 를 짜려면 — 매 op 의 출력 shape, dtype, device 를 알아야 한다. 그런데 실제 Tensor 를 만들면 메모리가 잡히고 GPU op 가 실행된다. compile 단계에서는 그게 싫다. 그래서 FakeTensor — “shape, dtype, device 만 들고 있고 storage 는 없는 Tensor”.

FakeTensor 가 풀어주는 것들.

  • shape inferencetorch.matmul(a, b) 의 출력 shape 을 실제 곱셈 안 하고 알 수 있음. metadata 만 다룸.
  • device 일관성 검증 — 입력 cuda 면 출력도 cuda 인지 trace 시점에 확인.
  • fast trace — 큰 모델의 graph 를 trace 할 때 실제 forward 1번 안 돌려도 됨. 메모리 0.
  • conditional 분석 어려움 — fake tensor 는 “값” 을 안 들고 있어서 if x.sum() > 0 같은 검사를 trace 시점에 못 함. 그래서 graph break.
from torch._subclasses import FakeTensorMode

with FakeTensorMode():
    a = torch.randn(1024, 1024, device="cuda")
    b = torch.randn(1024, 1024, device="cuda")
    c = a @ b
    print(c.shape)        # torch.Size([1024, 1024])
    print(c.dtype)        # torch.float32
    print(c.device)       # cuda:0
    # 실제 메모리 0 — c 는 metadata 만
FakeTensor 가 깨지는 자리

(1) data-dependent optorch.unique, torch.nonzero, torch.repeat_interleave(x, n) 같이 출력 shape 이 input value 에 의존. fake tensor 가 추정 못 함. (2) custom op 의 fake impl 미등록 — 사용자 정의 op 이 fake tensor mode 에서 어떻게 동작하는지 별도 등록 필요 (@register_fake).

“FakeTensor 는 PyTorch 의 새 substrate 다. AOT autograd, Dynamo, export 가 모두 같은 FakeTensor 위에서 trace 한다.” 학습 노트 · 재구성
§ 05AOT autograd· forward + backward 같이

backward graph 를 미리 만들어 둔다 — 그 의미와 함정

PyTorch 의 default autograd 는 tape-based — forward 가 실행되면서 동시에 grad function 을 tape 에 쌓고, backward 호출 시 그걸 거꾸로 실행. AOT autograd 는 다르다 — forward 가 실행되기 전 단계에서 backward graph 를 같이 만들어 둔다 (Ahead-Of-Time).

왜 AOT 가 필요한가.

  • fusion 의 기회 — backward 도 graph 로 잡혀 있어야 forward 와 같이 fusion 할 수 있음. tape 위에서는 op 별로 흩어져 있어 fusion 어려움.
  • Inductor 의 optimization — Inductor 는 graph 를 받아서 lowering. tape 는 못 받음.
  • scheduling 자유도 — “이 backward op 을 어디 위치에 둘까” 의 결정. AOT 는 이 자유도를 활용 가능.
AOT 의 trick — recompute

backward graph 안에 forward op 을 다시 넣어 둔다 (activation checkpointing 의 정형화된 형태). 메모리 절약. 단, 그 결정을 자동으로 하는 게 어려움 — 강의에서 “min-cut partitioning” 이라는 알고리즘으로 forward 와 backward 사이의 “저장” 과 “재계산” 의 경계를 결정한다고 알려져 있다.

# AOT autograd 의 의사 흐름
joint_graph = trace_forward_and_backward(fn, inputs)
# joint_graph: forward + backward 가 한 graph 안에

fwd_graph, bwd_graph = partition(joint_graph)
# forward 가 무엇을 backward 에 넘겨줄지 결정
# (saved tensors vs recomputed)

fwd_compiled = inductor_compile(fwd_graph)
bwd_compiled = inductor_compile(bwd_graph)

# 호출 시 forward 만 실행, backward 는 backward() 시
def compiled_fn(*args):
    out, saved = fwd_compiled(*args)
    register_for_backward(saved, bwd_compiled)
    return out
FIG · joint graph 의 partitionmin-cut
forward x → linear → gelu → linear → norm → out (fusion 가능)
save x, gelu_out, norm_input — backward 에 필요. 메모리 부담.
recompute gelu 는 cheap 하고, gelu_input 만 있으면 된다. saved 에서 빼고 backward 에서 재계산. 메모리 ↓.
backward recomputed gelu + saved norm_input + saved x → grad_x. 그래프 위에서 fusion 가능.
min-cut partitioning 은 — forward 의 어느 op 의 출력을 saved 로 두고, 어느 op 은 backward 에서 재계산할지를 결정. 메모리 vs compute 의 자동 trade-off.
§ 06Inductor 와 Triton 의 분리· backend 의 선택

graph 가 어떻게 GPU 코드가 되는가

Inductor 는 torch.compile 의 default backend. FX graph 를 받아서 Triton kernel(GPU) 또는 C++/cpp_wrapper(CPU) 로 lowering. 사용자가 거의 보지 않지만 fusion / scheduling / vectorization 의 결정을 여기서 한다.

backend역할출력
inductor (default) FX graph → Triton (GPU) / C++ (CPU). fusion · loop tiling · vectorization. Triton / C++
aot_eager AOT autograd 만 — Inductor 안 거치고 ATen op 그대로. 디버깅용. eager
eager Dynamo 가 graph 를 잡지만 그대로 eager 실행. graph break 검출만. eager
cudagraphs cudagraph wrapping 만. fusion 안 함. cudagraph
tensorrt third-party — NVIDIA TensorRT 로 lowering. TRT engine
Inductor 의 fusion 결정

Inductor 는 graph 의 op 들을 — pointwise (elementwise), reduction (sum/max), tile (matmul/conv) 의 세 카테고리로 분류. 같은 카테고리는 잘 fuse 되고 (특히 pointwise), reduction 다음의 pointwise 도 fuse 가능. tile 연산은 보통 자기 kernel 로 — Inductor 는 이 결정을 자동으로.

실전에서 Inductor 의 출력 코드를 보고 싶다면 — TORCH_LOGS=output_code 환경변수. 생성된 Triton 코드가 console 에 dump 된다. L001 에서 Mark 가 같은 트릭으로 새 커널의 “시작점” 을 얻는 방법을 깐다.

“Inductor 의 출력 코드를 한 번 읽어보면 — fusion 이 어떻게 일어나는지의 직관이 잡힌다. 그 직관 위에서 자기 손으로 Triton 을 짤 때 시작점이 된다.” 학습 노트 · 재구성
§ 07cudagraph 와의 관계· 언제 둘이 같이

Inductor 가 만든 코드를 cudagraph 가 한 번 더 wrapping

cudagraph 는 PyTorch 의 별도 기능 — 같은 sequence 의 GPU 호출을 한 번 캡처해서 “하나의 단위” 로 launch. launch overhead 가 크게 줄어든다 (특히 작은 op 이 많이 있는 경우).

cudagraph 가 도움 되는 자리.

  • 작은 batch size 의 학습 — kernel 한 개 실행이 너무 짧아서 launch overhead 가 dominant.
  • LLM inference 의 generate 단계 — token 마다 같은 sequence of ops. cudagraph 로 capture 하면 launch overhead 없음.
  • RL 환경의 small policy — 환경 step 마다 작은 forward.

cudagraph 와 torch.compile 은 서로 다른 layer 에서 작동한다. compile 이 op 들을 fuse 해서 큰 kernel 로 만들고, cudagraph 가 그 kernel 들의 sequence 를 한 번에 launch. 같이 쓸 수 있고 보통 같이 쓰는 게 best.

cudagraph 의 함정

(1) shape 이 고정이어야 함 — graph 캡처 시점의 shape 으로 lock. (2) memory address 도 고정 — pre-allocated buffer 사용. (3) conditional 안 됨 — capture 한 sequence 만 그대로. (4) capture 시 “warmup” 한 번 필요 — 첫 호출은 캡처용.

# torch.compile + cudagraph 같이
fn = torch.compile(model, mode="reduce-overhead")
# reduce-overhead = inductor + cudagraph
언제 cudagraph 가 안 좋은가

큰 batch / 큰 모델에서는 — kernel 자체가 충분히 무거워서 launch overhead 가 무의미. cudagraph 가 잡는 메모리 (pre-allocated buffer) 가 부담일 수 있음. 그리고 dynamic shape 이 있으면 cudagraph 가 매번 capture 다시 — 오히려 손해.

§ 08디버깅 워크플로· TORCH_LOGS · TORCH_COMPILE_DEBUG

“왜 안 빨라지나” 의 답을 찾는 표준 시퀀스

torch.compile 디버깅의 가장 큰 함정은 — 어디서 깨졌는지가 안 보임. 4-stage 파이프라인 어디든 깨질 수 있음. 표준 시퀀스 를 거치며 layer 별로 좁혀간다.

  1. graph break 가 있나? torch._dynamo.explain(fn, *args) — graph break 의 횟수와 자리를 알려준다. 0 이 목표.
  2. recompile 이 자주 일어나나? TORCH_LOGS="recompiles". 학습 step 마다 recompile 하면 cache_size_limit 도달. dynamic 모드 활성화 또는 shape 고정.
  3. output code 가 어떤 모양인가? TORCH_LOGS="output_code". Inductor 의 Triton 출력. fusion 이 의도대로 됐는지 확인.
  4. 전체 trace 보고 싶으면 TORCH_COMPILE_DEBUG=1. ./torch_compile_debug/ 디렉토리에 파일 dump.
  5. backend 만 바꿔보기backend="aot_eager" 로 두면 AOT 까지만, Inductor 안 함. 둘 중 어느 단계가 문제인지 식별.
step 1 torch._dynamo.explain(fn, x) — graph break 위치 확인. get_graph_break_reasons()
step 2 TORCH_LOGS="recompiles" python train.py — recompile 자주 나면 dynamic 모드.
step 3 TORCH_LOGS="output_code" — Triton 코드 dump. fusion 검증.
step 4 TORCH_COMPILE_DEBUG=1 — full trace 디렉토리 dump.
step 5 backend 바꾸기 — aot_eager, eager — 어느 단계 문제인지 식별.
Richard 의 자주 인용되는 조언

“처음 torch.compile 을 쓸 때 — 먼저 graph break 0 을 만든다. fusion 최적화는 그 다음.” graph break 가 있으면 fusion 도 깨지니까 둘이 동시에 풀려야 함. 원본 영상 확인 필요 — 강의에서 Richard 가 같은 조언을 했는지.

“torch.compile 의 디버깅은 — 정상 작동하는 사람의 워크플로를 그대로 모방하는 것에서 시작. step 1, 2, 3, 4, 5 를 정해진 순서로.” 학습 노트 · 재구성
§ 09흔한 회피 패턴· graph break 를 피하는 코드

compile 친화적인 PyTorch 코드를 짜는 법

강의의 실전 답 — “이런 패턴은 compile 가 잡는다”, “이런 패턴은 깨진다”. 코딩 습관 단위.

.item() 안 쓰기
tensor 의 scalar 값을 Python 으로 꺼내면 break. 비교가 필요하면 torch.where, mask 처리.
control flow 는 tensor op 으로
if cond: x else: y 가 cond 가 tensor 면 break. torch.where(cond, x, y).
print, breakpoint 분리
학습 hot loop 에 디버깅 print 두지 말 것. logging 은 graph 밖에서.
in-place op 주의
x += y 도 가능하지만, autograd 와 잘 안 맞아서 일부 케이스에서 break. 가능하면 x = x + y.
custom op 은 register
사용자 정의 cuda 함수는 torch.library.custom_op 로 등록. fake impl 도 같이.
data-dependent 패턴 분리
torch.unique, torch.nonzero 같이 출력 shape 이 입력 값에 의존하는 op 은 graph 밖으로.
dynamic shape 명시
batch dim 만 dynamic. torch._dynamo.mark_dynamic(x, 0).
함수 분리
학습 step 의 forward + backward 를 한 함수로 묶고 그것만 compile. data loading, logging 은 밖.
전략 정리

compile 친화적 코드의 황금률 — “tensor 가 graph 안에서 끝까지 살아 있게”. tensor → Python value → tensor 로 왔다갔다 하면 break 폭주. 모든 logic 을 tensor op 으로 표현.

§ 10기억할 메모와 자료· key takeaways

다시 열었을 때 5분 안에 손에 잡혀야 할 것

4-stage 파이프라인
Dynamo (capture) → FakeTensor (trace) → AOT autograd (backward) → Inductor (lower → Triton).
graph break
.item(), data-dependent control flow, print, custom op 미등록. hot loop 안의 break 가 가장 큰 손해.
dynamic shape
static (default) 은 매 shape 마다 recompile. dynamic=True 또는 mark_dynamic 으로 symbolic shape.
cache_size_limit
default 8. 그 이상 recompile 누적 시 자동 disable. 학습 코드에서 “이상하게 느려졌다” 의 흔한 원인.
FakeTensor
shape/dtype/device 만 들고 storage 없음. graph trace 의 substrate. 메모리 0.
AOT autograd
backward graph 를 미리 합성. min-cut partitioning 으로 saved vs recomputed 자동 결정.
Inductor backend
FX → Triton (GPU). pointwise/reduction/tile 카테고리로 fusion. TORCH_LOGS=output_code 로 코드 dump.
cudagraph (mode=reduce-overhead)
launch overhead 줄임. shape 고정 필수. 작은 batch / generate 에서 큰 효과.
§ 11다른 강의로 이어지는 길· connections

같은 자리를 다른 각도에서 다루는 강의들

§ 12열린 질문· open questions

원본 자막 실패로 비워둔 자리들

검증 메모

본 노트의 모든 코드 스니펫과 동작 설명은 PyTorch 2.0+ 공식 문서와 dev-discuss RFC 를 토대로 한 재구성. PyTorch 의 발전 속도가 빠르므로 — 실제 사용 시 자기 PyTorch 버전의 docs 를 직접 확인. 특히 backend 옵션과 mode 이름은 자주 변경됨.

← Lecture 052 Scaling Laws for Low Precision Lecture 054 → Small RL Models with LeanRL