스터디 발표 · 2026-09
| 파트 | 핵심 질문 |
|---|---|
| 00 · 배경 | PyTorch와 CUDA는 어떻게 연결되는가? 그래프는 무엇인가? |
| 01 · 전체 구조 | 한 함수는 각 단계에서 어떻게 표현되는가? |
| 02 · Dynamo | 무엇을 캡처하고, 어디에서 그래프가 끊기는가? |
| 03 · AOTAutograd | 변이·별칭·복합 연산을 어떻게 다루는가? |
| 04 · Inductor | 어떤 실행 코드가 생기며, 비용은 어디서 줄어드는가? |
| 실측 · 정리 | CPU 생성 코드·실행 시간·확장성 |
| 부록 A–F | 실제 그래프 / IR / 심화 수식 / 출처 / 재현 자료 |
CUDA 생태계는 왜 단단한가 · 그래프란 무엇인가 · FX 그래프란 무엇인가
| 관점 | eager 실행 | compile 실행 |
|---|---|---|
| 사용자 코드 | torch 연산으로 모델 작성 | 같은 함수를 torch.compile로 감쌈 |
| 연산 처리 | 호출된 연산을 그때그때 실행 | 캡처한 그래프를 분석·최적화 |
| GPU 실행 | 기존 CUDA 커널·라이브러리 구현 활용 | 생성 커널 + 기존 라이브러리 구현 활용 |
| CPU 실행 | CPU 연산 구현 활용 | CPU용 코드 생성·라이브러리 활용 |
PyTorch는 모델을 작성하고 실행하는 프레임워크, CUDA는 NVIDIA GPU용 소프트웨어 플랫폼이다. CUDA core는 GPU 안의 연산 장치 명칭으로, CUDA 플랫폼 자체와 구분한다.
참고 · Compiler
| 층 | 이식할 때 확인할 것 | torch.compile이 돕는 부분 |
|---|---|---|
| 모델 코드 | CUDA 전용 코드·확장 연산·지원 연산 | 일반적인 tensor 코드를 그래프로 캡처 |
| 연산·커널 | 라이브러리 품질과 생성 커널 성능 | fusion·코드 생성·기존 커널 선택 |
| 장치·런타임 | 메모리 할당·실행·동기화·드라이버 | 백엔드 밖의 장치 통합도 필요 |
| 생태계 | 도구·분산 통신·개발 경험 | 컴파일러만으로 모두 해결되지는 않음 |
CUDA의 강점은 라이브러리와 도구, 개발 경험이 함께 쌓인 생태계다. torch.compile은 공통 그래프 캡처를 재사용하게 해 새 백엔드의 구현 부담을 줄인다.
x ──▶ floor ──▶ y ──▶ add(1) ──▶ z # 노드: floor, add # 간선: x→floor, floor→add(y), add→z
# FX 그래프를 코드로 되찍은 형태 (graph_code 로그의 모양)
def forward(self, L_x_ : torch.Tensor):
l_x_ = L_x_
floor = torch.floor(l_x_); l_x_ = None
add = floor + 1; floor = None
return (add,)TorchDynamo → AOTAutograd → TorchInductor, 세 단계 파이프라인
| 요소 | 입력 | 출력 |
|---|---|---|
| TorchDynamo | 파이썬 함수 | FX 그래프 + guard + 잔여 바이트코드 |
| AOTAutograd | FX 그래프 | 정규화된 ATen 그래프 |
| TorchInductor | ATen 그래프 | 생성 커널 + 기존 커널 호출 코드 |
def inspect_backend(gm, example_inputs):
print(gm.code)
return gm.forward
compiled = torch.compile(f, backend=inspect_backend)
compiled(x)새 가속기에는 그래프 변환 외에도 장치·런타임·메모리·연산 지원·성능 최적화가 필요하다.
참고 · Backends
파이썬 코드를 그래프로 — 그리고 graph break
def a_dynamo_break(x):
y = x * 2
if y.sum() > 0: # ← 여기서 graph break
return y + 1 # 그래프 1: x*2, sum / 그래프 2: y+1 또는 y-1
return y - 1
torch._dynamo.explain(a_dynamo_break)(x)
# Graph Count / Graph Break Count / Break Reasons 를 알려 준다CPU 실측: Graph Count = 2, Graph Break Count = 1. 입력은 torch.ones(4). 실제 사유·그래프는 부록 A에 수록.
그래프 정규화 — functionalization · decomposition · 디스패처 아래에서 캡처
# 사용자 코드 y = x.clone() y.add_(1) # 기존 y의 값을 변경 y = y.view(-1) # 같은 저장 공간을 볼 수 있음
# 개념 예시: mutation 제거 clone = aten.clone(x) add = aten.add(clone, 1) view = aten.view(add, [-1]) # view가 남았다고 독립 메모리가 된 것은 아님
참고 · Functionalize
class <lambda>(torch.nn.Module):
def forward(self, arg0_1: "f32[4]"):
add: "f32[4]" = torch.ops.aten.add.Tensor(arg0_1, 1); arg0_1 = None
neg: "f32[4]" = torch.ops.aten.neg.default(add)
exp: "f32[4]" = torch.ops.aten.exp.default(neg); neg = None
add_1: "f32[4]" = torch.ops.aten.add.Tensor(exp, 1); exp = None
div: "f32[4]" = torch.ops.aten.div.Tensor(add, add_1); add = add_1 = None
return (div,)참고 · Backends
| 사용자 표현 | 정규화의 목적 |
|---|---|
| torch.relu(x) | 같은 의미를 갖는 연산을 공통 ATen 표현으로 다룸 |
| F.relu(x) | Python API 표기의 차이를 줄임 |
| nn.ReLU()(x) | 모듈 호출 내부의 tensor 연산을 추적 |
커널 코드 생성 — 원소 단위 · fusion · FLOPs 와 bytes · 문자열 기반 codegen
| 단계 | 하는 일 | 결과 |
|---|---|---|
| lowering | 텐서 단위 연산을 원소 단위 수식으로 내린다 | 루프 수준 IR |
| scheduling | 의존 관계를 보고 합칠 노드를 묶는다 (fusion 결정) | 커널 단위 그룹 |
| codegen | IR 의 기본 동작을 코드 생성 함수로 갈아끼운다 | Triton(GPU) · C++(CPU) 소스 |
# FX / ATen 그래프 — 텐서 단위 y = floor(x) z = y + 1
# Inductor 루프 수준 IR — 원소 단위
for i in range(N):
y[i] = floor(x[i])
z[i] = y[i] + 1A: [N, P] B: [P, M] C: [N, M] 계산량 ≈ 2 × N × P × M FLOPs 이상적인 최소 데이터 이동량 ≈ (NP + PM + NM) × s bytes s = 원소당 바이트 수 # 캐시·재사용·추가 버퍼에 따라 실측량은 달라짐
참고 · Roofline · Transformer FLOPs
| 구분 | eager: floor → add | fused: floor + add |
|---|---|---|
| 수학적 계산 | 원소마다 floor, add | 원소마다 floor, add |
| 커널 수: 이 예제 | 2 | 1 |
| 읽기·쓰기 | x 읽기 / y 쓰기 / y 읽기 / z 쓰기 | x 읽기 / z 쓰기 |
| 이상적 이동량 | 4Ns bytes | 2Ns bytes |
| FP32, N=1,048,576 | 16 MiB | 8 MiB |
메모리 접근량 계산 예시이지, DRAM 트래픽 실측값은 아니다. 캐시와 커널 실행 비용도 영향을 준다. 이동량이 절반이라고 실행 시간이 반드시 절반이 되지는 않는다.
참고 · Roofline
계산 시간 하한 ≈ FLOPs / 연산 처리량 이동 시간 하한 ≈ bytes / 메모리 대역폭 연산 집약도 = FLOPs / bytes 단순 Roofline 관점: 시간 ≳ max(계산 시간, 이동 시간)
참고 · Roofline
auto tmp0 = at::vec::Vectorized<float>::loadu(in_ptr0 + static_cast<int64_t>(x0), static_cast<int64_t>(16)); auto tmp1 = tmp0.floor(); auto tmp2 = static_cast<float>(1.0); auto tmp3 = at::vec::Vectorized<float>(tmp2); auto tmp4 = tmp1 + tmp3; tmp4.store(out_ptr0 + static_cast<int64_t>(x0));
참고 · Compiler
| 측정 항목 | 결과 | 해석 |
|---|---|---|
| compile 첫 호출 | 19.150 s | 컴파일·로그·첫 실행을 포함 |
| eager 반복 중앙값 | 1.6775 ms / call | 20개 라운드의 라운드당 평균에서 중앙값 |
| compiled 반복 중앙값 | 0.1615 ms / call | 이 측정의 eager/compiled 비율: 10.39× |
| 결과 검증 | 정확히 일치 · max abs diff = 0 | 고정 입력에서 rtol=0, atol=0로 비교 |
각 경로 10회 워밍업 후, 20회 호출 × 20라운드. 측정 순서를 번갈아 적용했다. 공유 CPU 환경의 예제 측정으로 GPU·LLM 전체 성능을 대표하지 않는다. 컴파일 캐시는 새 경로를 사용했지만 OS·컴파일러 캐시까지 초기화한 cold-start 측정은 아니다.
| 질문 | 답 |
|---|---|
| 왜 그래프가 필요한가? | 여러 연산의 의존 관계를 보고 함께 최적화하기 위해 |
| 각 단계는 무엇을 맡나? | Dynamo: 캡처 / AOTAutograd: 정규화·학습 그래프 / Inductor: 실행 코드 |
| 왜 빨라질 수 있나? | 중간 메모리 접근·호출 오버헤드 감소와 커널 최적화 |
| 언제 효과가 달라지나? | 컴파일 비용·graph break·재컴파일·shape·장치·커널 구현에 따라 |
| 이번에 직접 확인한 것은? | CPU 실제 그래프·C++ 코드·결과 일치·반복 실행 시간 |
다음 실험: 같은 재현 스크립트를 바탕으로 GPU 버전을 별도로 구성하고, 실제 모델의 병목과 compile 전후 시간을 비교한다.
Graph Count: 2
Graph Break Count: 1
def forward(self, L_x_ : torch.Tensor):
l_x_ = L_x_
y = l_x_ * 2; l_x_ = None
sum_1 = y.sum()
gt = sum_1 > 0; sum_1 = None
return (gt, y)def forward(self, L_y_ : torch.Tensor):
l_y_ = L_y_
add = l_y_ + 1; l_y_ = None
return (add,)실제 사유 발췌: generic_jump TensorVariable()
이 입력은 양수 분기를 실행했다. 음수 분기까지 하나의 그래프로 캡처했다는 뜻은 아니다.
# remove="mutations"
def forward(self, x_1):
clone = torch.ops.aten.clone.default(x_1); x_1 = None
add = torch.ops.aten.add.Tensor(clone, 1); clone = None
view = torch.ops.aten.view.default(add, [-1]); view = None
view_1 = torch.ops.aten.view.default(add, [-1]); add = None
silu = torch.ops.aten.silu.default(view_1); view_1 = None
return silu# remove="mutations_and_views"
def forward(self, x_1):
clone = torch.ops.aten.clone.default(x_1); x_1 = None
add = torch.ops.aten.add.Tensor(clone, 1); clone = None
view_copy = torch.ops.aten.view_copy.default(add, [-1]); view_copy = None
view_copy_1 = torch.ops.aten.view_copy.default(add, [-1]); add = None
silu = torch.ops.aten.silu.default(view_copy_1); view_copy_1 = None
return silu왼쪽과 오른쪽의 view / view_copy를 비교한다. 이 별도 실험은 설정 차이를 보여주기 위한 것이며, 오른쪽이 기본 torch.compile 파이프라인이라는 뜻은 아니다.
# 실제 ATen (파일 경로 주석 생략)
class <lambda>(torch.nn.Module):
def forward(self, arg0_1: "f32[1024, 1024]"):
floor: "f32[1024, 1024]" = torch.ops.aten.floor.default(arg0_1); arg0_1 = None
add: "f32[1024, 1024]" = torch.ops.aten.add.Tensor(floor, 1); floor = None
return (add,)class op0_loop_body:
var_ranges = {p0: 1048576}
index0 = p0
def body(self, ops):
get_index = self.get_index('index0')
load = ops.load('arg0_1', get_index)
floor = ops.floor(load)
constant = ops.constant(1.0, torch.float32)
add = ops.add(floor, constant)
get_index_1 = self.get_index('index0')
store = ops.store('buf0', get_index_1, add, None)
return storepre/post 파일에서 노드 수가 반드시 2→1이 되는 것은 아니다. 더 이른 단계에서 이미 같은 루프 표현으로 묶일 수 있다. 생성 코드·중간 버퍼·호출을 함께 확인한다. 원문 IR 두 파일은 내려받기 자료에 포함했다.
참고 · Transformer FLOPs · Roofline
| 항목 | 조건 |
|---|---|
| PyTorch / Python | 2.14.0+cpu / 3.12.14 |
| 장치 / dtype / threads | CPU / float32 / 1 |
| 입력 / seed | [1024, 1024] / 42 |
| 검증 | eager와 compiled 결과 정확히 일치 |
| 한계 | CPU 공유 실행 환경 · GPU 미측정 · 한 예제에 대한 단일 세션 결과 |
참고 · 원문 · Compiler · Functionalize · Backends · Dynamic shapes · Roofline · Transformer FLOPs
python -m venv .venv # 가상환경 활성화 후 CPU 빌드 설치 (Linux 예시) python -m pip install torch==2.14.0 --index-url https://download.pytorch.org/whl/cpu python dump_torch_compile.py > run-summary.log 2> compiler.log # evidence/results.json : 실제 값과 그래프 # evidence/debug/ : 생성 코드·IR # GPU 실험은 입력 device와 측정 동기화 등을 별도 수정해야 함
원문 로그에는 실행 환경의 로컬 경로가 포함된다. 실제 출력은 그대로 보존했으며, 본문의 코드 발췌는 실측 파일의 연속 구간이다.