gpumode · 강의 아카이브
《GPU Mode》 L001 2024 · JAN · 13 High priority transcript · 56분 · available

How to profile CUDA kernels in PyTorch

PyTorch 프로그램 안에 커스텀 CUDA 커널을 끼워넣는 가장 작은 길, 그리고 그게 실제로 빠른지 — 더 빠르게 만들 여지가 있는지 — 를 어떤 도구로 어떻게 확인하는가. Mark Saroufim 이 GPU Mode 첫 강의에서 깐 프로파일링 워크플로를 도구별 진입점의사결정 사다리 두 축으로 다시 정리한 학습 노트.

PyTorch profiler load_inline Triton torch.compile Nsight Compute PTX CUDA events fusion
M
Speaker
Mark Saroufim
Meta · PyTorch core · GPU Mode 운영진
강의 번호
L001
스피커
Mark Saroufim
학습 우선순위
High · 정독
다시 볼 때
직접 구현해본다
§ 01강의가 풀려는 문제· Why this lecture exists

“CUDA 튜토리얼 지옥” 을 건너뛰는 가장 짧은 길

대부분의 사람이 CUDA 를 시작할 때 부딪히는 문제는 문법이 아니라 경로의 문제다 — 어디서 시작해서 무엇으로 검증하고 어디까지 내려갈지 모른다는 것. 이 강의는 그 경로를 도구 단위로 깐다.

강의가 풀려고 하는 질문은 두 개로 압축된다.

  1. PyTorch 프로그램 안에 커스텀 CUDA 커널을 어떻게 끼워넣는가 — C/C++ 빌드 시스템 다루지 않고.
  2. 그 커널이 실제로 빠른지 어떻게 검증하는가 — 그리고 더 빠르게 만들 여지가 있는지 어떻게 보는가.

Mark 의 입장은 명시적으로 실용주의다. “GPU 가 어떻게 도는지 모든 디테일을 이해하지 않아도, 프로파일러로 black box 를 시각적으로 보면서 유용한 일을 할 수 있다.” 이 시각이 강의 전체를 끌고 간다 — 교과서식 bottom-up (메모리 모델 → warp → SM → kernel) 대신, top-down 으로 이미 도는 코드를 어디서 어떻게 보는지부터 깐다.

강의의 인지적 frame

이 강의의 모든 도구는 같은 질문에 답한다 — “방금 launch 한 그 커널, 실제로 GPU 위에서 무슨 일이 벌어지고 있는가?” 첫 도구는 그저 이름과 시간을 보여주고, 마지막 도구(NCU)는 특정 라인이 메모리 latency 때문에 막혔는지 register 부족 때문에 막혔는지까지 본다.

“프로파일링은 GPU 를 black box 로 두고도 유용하게 쓸 수 있게 해준다. 모든 디테일을 알 필요는 없다.”Mark Saroufim · 09:43

그래서 이 강의의 끝에 도착했을 때 손에 잡혀 있어야 하는 건 4개 도구1개의 의사결정 사다리다 — autograd profiler, PyTorch profiler(Chrome trace), Triton interpret 모드, NCU. 그리고 “언제 torch 에서 Triton 으로, Triton 에서 CUDA 로 내려갈지” 의 판단 기준.

§ 02CUDA 는 비동기다· The async-timing trap

가장 흔한 첫 실수 — time.time() 으로 GPU 코드를 재면 안 된다

강의에서 Mark 가 “CUDA 에 대해 가장 먼저 알아야 할 것 하나만 고르라면” 이라고 못 박은 사실 — CUDA 는 비동기다. 호스트가 kernel<<<...>>> 을 호출해도 GPU 가 끝나기를 기다리지 않는다. python 의 time.time() 으로 측정하는 건 launch overhead 일 뿐, 커널이 실제로 도는 시간이 아니다.

제대로 재려면 세 가지를 조합해야 한다.

  1. warmup — 첫 호출은 CUDA context 초기화가 섞이므로 버린다.
  2. torch.cuda.Event — GPU 타임라인 위에 직접 마커를 찍는다. start.record()end.record().
  3. torch.cuda.synchronize() — end 이벤트가 실제로 GPU 에서 기록될 때까지 기다린다.

이 셋이 빠진 측정은 거의 항상 잘못된 숫자를 준다 — 빨라 보이거나(launch 만 잰 경우) 들쭉날쭉(첫 호출의 context init 이 섞인 경우)이다.

Mark 의 헬퍼 — 강의에서 그대로 쓰는 패턴

torch.cuda.Event(enable_timing=True) 두 개를 만들어 start/end 로 쓰고, 사이를 GPU 가 워치하게 한 뒤 마지막에 synchronize(). start.elapsed_time(end) 가 ms 단위로 진짜 커널 시간을 돌려준다.

# Mark 의 time_pytorch_function — pytorch_square.py 그대로
def time_pytorch_function(func, input):
    # CUDA IS ASYNC — python time 모듈 쓰면 안 된다
    start = torch.cuda.Event(enable_timing=True)
    end   = torch.cuda.Event(enable_timing=True)

    # Warmup — 첫 호출에 context init 비용이 섞임
    for _ in range(5):
        func(input)

    start.record()
    func(input)
    end.record()
    torch.cuda.synchronize()
    return start.elapsed_time(end)
FIG · python time 으로 재면 무엇이 잡히는가HOST vs GPU 타임라인
잘못된 측정
python time.time()
launch
— python 은 여기서 이미 끝났다고 본다 —
~0.05 ms
실제 GPU 시간
cuda.Event + sync
launch
kernel 실행
sync
~12 ms
첫 호출(워밍 X)
context init 섞임
CUDA context init
kernel
~280 ms
위 두 줄의 차이가 200배. 같은 커널, 같은 입력. 측정 도구 하나로 결론이 뒤집힌다.

강의에서 Mark 가 보여준 첫 번째 작은 실험 — torch.square(b) vs b * b vs b ** 2 — 도 이 세팅 위에서만 의미가 있다. 결과: torch.squareb ** 2 는 똑같이 aten::pow 를 부르고, b * baten::mul 을 부르며 미세하게 더 빠르다. 같은 수학 연산이 백엔드에서 다른 op 로 디스패치된다는 사실이 이 첫 측정에서 이미 드러난다.

§ 03autograd profiler· torch.profiler.profile()

이름과 시간만 — 가장 작은 진입점

첫 프로파일러는 autograd profiler. context manager 안에 코드를 넣으면, GPU 위에서 어떤 op 들이 호출됐고 각자 얼마나 걸렸는지 표로 떨어진다. 코드 한 줄 더 추가하지 않아도 “내 함수는 aten::pow 를 부르는구나”, “aten::mul 이 더 빠르네” 같은 사실이 드러난다.

# torch.square / a*a / a**2 가 각자 어떤 op 를 부르는지 본다
with torch.profiler.profile() as prof:
    torch.square(b)

print(prof.key_averages().table(
    sort_by="cuda_time_total",
    row_limit=10,
))
--- 출력 (핵심 줄만) ---
Self CPU time total: 211.000us
Self CUDA time total: 162.001us

aten::pow                    7.66%   16.000us  ...   2  CUDA  pow_tensor_scalar
aten::result_type            0.00%    0.000us  ...   2
vectorized_elementwise_kernel
                           100.00%  162.001us  ...   2  CUDA

표에서 읽히는 사실 두 가지.

  • aten::pow 가 호출된다torch.square 라는 이름이 직접 op 가 아니라 pow(x, 2) 로 디스패치된다는 뜻. 즉 PyTorch 안에 “square” 라는 별도의 fused 커널은 없다.
  • 실제 GPU 시간은 vectorized_elementwise_kernel 안에서 다 잡힌다 — pointwise 연산은 거의 모두 이 templated 커널 하나로 모인다.

이 정도만으로도 첫 가설을 세울 수 있다 — “PyTorch 의 torch.square 는 사실상 elementwise pow 다. 내가 직접 짜는 square 커널이 더 빨라질 가능성은, fused 한 다른 연산과 함께 묶을 때나 의미가 있다.”

사용 패턴

autograd profiler 는 “이 함수가 어떤 op 들을 부르는가” 의 답을 가장 빠르게 준다. 항상 첫 도구로 쓴다.

§ 04Chrome trace 로 보는 PyTorch profiler· visual profiler · flow events

표가 끝나는 자리에서 시각화가 시작된다

표 위에서는 op 들이 같은 시간에 어떻게 겹치는지, memcpy 가 커널과 어떤 순서로 도는지가 안 보인다. PyTorch profiler 의 Chrome trace 가 그 자리를 채운다 — JSON 으로 export 해서 chrome://tracing 에 드래그하면 바로 timeline 뷰.

from torch.profiler import profile, ProfilerActivity

with profile(activities=[
    ProfilerActivity.CPU,
    ProfilerActivity.CUDA,
]) as prof:
    for _ in range(10):
        a = torch.square(torch.randn(10000, 10000).cuda())

prof.export_chrome_trace("trace.json")
# 그 다음 chrome://tracing 에 trace.json 을 드래그

실전에서는 schedule(wait=1, warmup=1, active=2, repeat=1)on_trace_ready 콜백을 함께 써서 학습 루프 일부 step 만 잡는 패턴이 표준이다. 강의의 pt_profiler.py 가 그 형태.

FIG · torch.square 한 번의 chrome traceCPU lane → GPU lane
CPU thread
aten::square
cudaLaunchKernel
aten::pow
cudaLaunchKernel
CUDA stream
memcpy H→D
vectorized_elementwise_kernel<4>
vectorized_elementwise_kernel<4>
flow event
CPU 가 launch API 를 부른 시점에서 flow arrow 가 GPU stream 위 실제 커널까지 그어진다. 둘이 같은 “일” 이라는 걸 시각적으로 묶는다.

이 뷰에서 처음 잡히는 새로운 정보가 두 가지 더 있다.

“표가 답을 빨리 주지만, 시각화는 어디서 갭이 생기는지를 답한다 — 갭이 곧 fusion 의 표적이다.”학습 노트
§ 05load_inline· torch.utils.cpp_extension

C++/CUDA 빌드 시스템 없이 커널을 끼워넣는 길

커스텀 CUDA 커널을 PyTorch 에 넣을 때 전통적으로 만나는 두 벽 — pybind11 보일러플레이트setup.py / CMake / make. torch.utils.cpp_extension.load_inline 은 둘 다 우회한다. C++ 소스와 CUDA 소스를 문자열로 그냥 넣으면, 안에서 ninja 가 build 해서 .so 로 import 가능한 모듈을 돌려준다.

FIG · load_inline 이 내부에서 하는 일string → callable op
L0
Python 안 문자열
cuda_source · cpp_source
L1
코드 생성
main.cpp · pybind glue
L2
build.ninja
자동 생성
L3
nvcc / g++
.o · .so
L4
torch op
module.square_matrix(t)
중요한 건 모든 중간 산출물(main.cpp, build.ninja)이 디스크에 남는다는 사실이다. build_directory='./load_inline_cuda' 로 위치를 고정해두면 — pybind 가 어떻게 자동 생성됐는지, ninja 가 어떤 nvcc flag 로 빌드했는지 직접 볼 수 있다. 학습 자료로도 쓰인다.
# load_inline.py — 강의 repo 그대로
cuda_source = '''
__global__ void square_matrix_kernel(
    const float* matrix, float* result,
    int width, int height) {
  int row = blockIdx.y * blockDim.y + threadIdx.y;
  int col = blockIdx.x * blockDim.x + threadIdx.x;
  if (row < height && col < width) {
    int idx = row * width + col;
    result[idx] = matrix[idx] * matrix[idx];
  }
}

torch::Tensor square_matrix(torch::Tensor m) {
  auto h = m.size(0); auto w = m.size(1);
  auto out = torch::empty_like(m);
  dim3 t(16,16);
  dim3 b((w+15)/16, (h+15)/16);
  square_matrix_kernel<<<b,t>>>(
    m.data_ptr<float>(),
    out.data_ptr<float>(), w, h);
  return out;
}
'''
cpp_source = "torch::Tensor square_matrix(torch::Tensor);"
from torch.utils.cpp_extension import load_inline

ext = load_inline(
    name='square_matrix_extension',
    cpp_sources=cpp_source,
    cuda_sources=cuda_source,
    functions=['square_matrix'],
    with_cuda=True,
    extra_cuda_cflags=["-O2"],
    build_directory='./load_inline_cuda',
)

# 이제 그냥 파이썬 함수처럼 부른다
a = torch.tensor([[1.,2.,3.],
                  [4.,5.,6.]],
                 device='cuda')
print(ext.square_matrix(a))

“C++ 파일을 손에 들지 않고 CUDA 커널을 PyTorch 에 박는다” — 이 한 문장이 강의 전반부의 핵심.

함정

처음 build 가 의외로 느리다. nvcc 가 한 번 다 돌아야 한다. flash attention 을 source 에서 build 할 때 시간이 오래 걸리는 이유와 같은 메커니즘 — 커널 하나하나가 nvcc 를 거친다. 매번 다시 컴파일되지는 않으니 두 번째부터는 빠르다. 캐싱이 자동이라는 점은 알아둘 것.

§ 06Triton· DSL · interpret · PTX

같은 일을 파이썬으로 — 그리고 launch 설정 한 줄에 성능이 뒤집힌다

Triton 은 파이썬 안 DSL 이지만 CUDA 코드를 만들지 않는다 — 직접 PTX 까지 내려간다. 통합은 trivial: 그냥 파이썬 함수를 부른다. 그런데 강의에서 Mark 가 보여준 첫 실험에서 torch.square 보다 자기 Triton 커널이 더 느렸다. 원인이 뭐였나 — 그게 이 섹션의 본론.

FIG · Triton lowering 사다리Python → SASS
L0 · DSL @triton.jit Pythontl.load / tl.store / tl.sum 같은 tile 단위 op 사람이 손으로 짜는 단계
L1 · Triton-IR tile-level IRBLOCK_SIZE, num_warps, num_stages 결정 반영 ~/.triton/cache/ 안 텍스트로 남음
L2 · MLIR / LLVM layout · pipeline · register 할당여기서 fast 와 느림이 갈린다 launch 설정 한 줄이 결과를 뒤집는 자리
L3 · PTX virtual ISAR8 = R0 * R0 같은 가상 register 명령 강의에서 직접 들여다보는 자료
L4 · SASS SM 별 머신코드실제 NVIDIA 칩 위에서 도는 명령 arch 별로 다름 (sm_80, sm_90…)
강의에서 Mark 가 직접 짠 square 커널의 PTX 를 까보니 register 8 개를 쓰고 있더라 — input 8 개 load → R0…R7, mul → R8…R15, store. 작은 커널의 register 사용량은 PTX 만 보고도 가늠할 수 있다.

강의의 시퀀스를 따라가 보자.

  1. Mark 가 Triton 의 fused softmax 튜토리얼을 베이스로 row-wise square 커널을 짠다. 코드는 짧다 — tl.load 로 한 row 를 SRAM 에 올리고 곱하고 tl.store 로 다시 쓴다.
  2. A100 / 4090 모두에서 torch.square 보다 느렸다. torch.compile 도 비슷하게 느렸다 — torch.compile 도 결국 Triton 으로 lowering 하니 자연스러운 결과.
  3. NCU 로 들어가서 본 hint — kernel grid is too small to fill available resources. 결국 BLOCK_SIZE 가 너무 작아서 SM 들이 비어 있었다.
  4. BLOCK_SIZE = 1024 로 고정하니 트렌드가 완전히 뒤집혀서 Triton 이 빨라졌다.
실전 교훈

Triton 커널의 성능은 소스 코드가 아니라 launch 설정(BLOCK_SIZE, num_warps, num_stages) 으로 결정되는 부분이 크다. 같은 코드가 한 설정에서 1ms, 다른 설정에서 10ms. 새 커널은 항상 이 셋을 sweep 하면서 본다.

interpret 모드 — Python breakpoint 가 박힌다

Triton 커널은 GPU 위에서 도는 동안 black box다. print 가 안 된다. 그래서 디버깅이 어렵다. 강의에서 Mark 가 강조한 게 @triton.jit(interpret=True) — 이 모드에서는 커널이 CPU 위에서 시뮬레이션 돌면서 breakpoint() 가 동작한다. “이 변수가 어떤 모양인지 step 마다 보고 싶다” 의 답.

환경변수 TRITON_INTERPRET=1 로도 켤 수 있다.

@triton.jit(interpret=True)
def square_kernel(out_ptr, in_ptr, n,
                  BLOCK: tl.constexpr):
    pid    = tl.program_id(0)
    offs   = pid * BLOCK + tl.arange(0, BLOCK)
    mask   = offs < n
    x      = tl.load(in_ptr + offs, mask=mask)
    breakpoint()                # ← 여기서 멈춘다
    tl.store(out_ptr + offs, x * x, mask=mask)

# pdb 안에서 print(x.tensor) 같이 wrapped tensor 의 내부를 본다
“GPU 커널을 디버깅하려고 변수를 일부러 글로벌 메모리에 적던 시절을 끝낸다 — interpret 모드는 GPU 커널 디버깅의 의미를 한 단계 바꾼다.”Andreas Köpf · 32:18
§ 07torch.compile + TORCH_LOGS· 코드 생성 트릭

Triton 커널을 직접 짜지 말고, torch 가 짜준 걸 출발점으로 삼는다

강의 후반부에서 Mark 가 가장 여러 번 추천한 트릭. 새 커널을 처음부터 짜지 말고, torch.compile 이 같은 연산에 대해 만들어내는 Triton 커널을 먼저 읽어본다. 그 코드가 보통 충분히 읽을 만 한 시작점이고, 변수명만 바꾸면 그대로 쓸 수 있다.

# square_compile.py
import torch

def square(a):
    return torch.square(a)

opt = torch.compile(square)
opt(torch.randn(10000, 10000).cuda())
# 실행 시 환경변수로 켜면 console 에 Triton 커널 dump
TORCH_LOGS=output_code python square_compile.py
# torch.compile 이 만들어낸 Triton 코드 (약식)
@triton.jit
def triton_(in_ptr0, out_ptr0, xnumel,
            XBLOCK: tl.constexpr):
    xoffset = tl.program_id(0) * XBLOCK
    xindex  = xoffset + tl.arange(0, XBLOCK)[:]
    xmask   = xindex < xnumel
    tmp0    = tl.load(in_ptr0 + xindex, xmask)
    tmp1    = tmp0 * tmp0       # ← elementwise mul
    tl.store(out_ptr0 + xindex, tmp1, xmask)

코드의 변수명은 tmp0, tmp1 같이 익명이지만, 구조는 명확하다 — load → 연산 → store. 그리고 강의의 다음 관찰이 진짜 흥미로운 지점.

fusion 의 증거를 코드에서 직접 본다

같은 입력에 torch.square 를 두 번 적용하는 함수를 컴파일하면 — 두 개의 커널이 launch 되는 게 아니라, 한 커널 안에 tmp1 = tmp0 * tmp0, tmp2 = tmp1 * tmp1 이 같이 들어간다. 이게 fusion 이다 — HBM 왕복 한 번을 절약. “fused 됐는지 안 됐는지” 가 코드 dump 만으로 검증된다는 사실이 강의의 큰 메시지.

“ML 코드의 가장 중요한 최적화는 결국 몇 개의 커널로 줄였는가. 그걸 가장 직관적으로 보여주는 도구가 TORCH_LOGS=output_code.”Mark Saroufim · 38:47
§ 08NCU· Nsight Compute

커널이 느린지까지 — actionable hints 가 박혀 있는 프로파일러

PyTorch profiler 까지가 “커널 이름과 시간” 을 본다면, NCU 는 한 단계 더 — 한 커널 안에서 메모리 latency 가 발목을 잡고 있는지, register 가 부족한지, occupancy 가 모자라서 SM 이 비어 있는지를 직접 알려준다. 사용법은 한 줄.

ncu python train.py

그리고 시각 모드는 --set full -o trace 를 붙여서 실행한 뒤 Nsight Compute UI 에 trace 를 연다.

FIG · NCU 가 직접 박아주는 hint 예시square 커널 · A100
METRIC / HINT현재가능한 개선
Achieved Occupancy38%→ 70%
Tail effect (last wave underfilled)0.4 wave+50% speedup
L1 cache throughput62%
L2 cache throughput71%
Long scoreboard stalls28%+20% speedup
NCU 가 구체적 다음 행동까지 제안한다 — “grid 를 키워라”, “padding 으로 last wave 를 채워라”, “coalesce 와 shared memory 를 직접 관리해라”. 단순한 metric dump 이상.

이 hint 들이 의미하는 바를 풀면 — 강의의 핵심 통찰이 여기 들어 있다.

  • Tail effect / Achieved occupancy 는 보통 padding 과 launch shape 으로 직접 통제할 수 있다. BLOCK_SIZE 한 줄, 입력 padding 한 줄.
  • Long scoreboard stalls메모리 latency 가 다른 명령이 도는 시간보다 길어서 생기는 stall. 이건 coalesced readsshared memory 의 직접 관리로 푼다.
  • 그리고 핵심 — Triton 은 padding 까지는 잡지 못 한다. coalescing/shared memory 관리도 Triton 의 결정 영역 안에 있어서 사용자가 직접 통제하기 어렵다.
의사결정 trigger

NCU 가 “tail effect 50%, scoreboard stall 20%” 같은 식으로 hint 를 주면 — 50% 는 Triton 위에서 padding 으로 잡고, 20% 는 직접 CUDA 로 내려가서 shared memory 를 짜야 잡힌다. 이 갈림길이 자연스럽게 다음 섹션의 “언제 어디까지 내려갈 것인가” 로 이어진다.

§ 09의사결정 사다리· torch → Triton → CUDA

NCU 가 “여기서부터 더 짜내려면 직접 내려가야 한다” 고 말해줄 때까지 미룬다

강의의 마지막 메시지 중 하나. CUDA 부터 시작하지 마라. torch → Triton → CUDA 의 사다리를 NCU 가 hint 를 주는 시점에 한 단계씩 내려간다. 그 hint 가 trigger 가 된다.

torch / torch.compile PyTorch op 그대로 + torch.compile wrap. 커널을 직접 안 짠다.
NCU 로 떠본다 — 만약 compute throughput 이 peak 의 70% 이상 이면 그대로 둔다.
여기서 멈출 신호 — peak 의 70%↑, 별다른 actionable hint 없음, fusion 이 이미 잘 일어남(output_code dump 로 확인).
Triton torch.compile 이 만들어준 Triton 커널을 시작점으로 한다. 변수명 바꾸고 BLOCK_SIZE, num_warps, num_stages 를 sweep 한다.
대부분의 ML 커널은 여기까지로 충분하다.
여기서 멈출 신호 — NCU 가 “tail effect / occupancy / launch shape” 만 hint 로 주고, padding 과 grid 조정으로 잡힘.
직접 CUDA NCU 가 long scoreboard stall, uncoalesced reads, shared memory bank conflict 를 hint 로 줄 때 비로소 내려간다. load_inline 으로 끼워넣는다 — 빌드 시스템 안 만진다. 여기서만 추가로 따는 것 — coalescing 직접 통제, shared memory 직접 stage, register 사용 조정, vector load (float4).
“CUDA 부터 시작해서 mucking around 하면 거의 항상 Triton autotuner 보다 느린 코드를 짠다.”Mark Saroufim · 43:00

이 사다리에서 한 가지 더 짚어야 하는 점 — Numba 는 강의에서 의도적으로 추천 경로에서 빠진다. 개념적으로는 CUDA 와 동일한 모델인데 (grid, block, threadIdx), 강의의 인지적 부담을 줄이기 위해 “이 클래스에서는 C++/CUDA 또는 Triton 만 쓴다” 고 못 박는다. 산업에서도 Triton 과 직접 CUDA 가 사실상 표준이 됐다.

§ 10기억할 메모와 코드· key takeaways · repo

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

강의에서 6개월 뒤에 다시 돌아왔을 때 가장 빨리 복원해야 하는 사실들과 — 직접 손에 박아야 하는 코드 자료들.

async timing pattern
CUDA Event start/end + warmup 5회 + cuda.synchronize(). time.time() 으로 GPU 코드 측정 절대 금지.
profiler 진입 순서
autograd profiler (표) → PyTorch profiler (Chrome trace) → NCU. 각 단계가 다른 차원의 정보를 준다.
load_inline
cuda_sourcescpp_sources 에 문자열로 넣고 build_directory 지정. main.cpp 와 build.ninja 가 디스크에 남아 학습 자료로도 쓰임.
launch 설정 sweep
Triton 새 커널은 BLOCK_SIZE · num_warps · num_stages 셋을 항상 sweep. 같은 소스가 설정 따라 10배 차이.
interpret = True
@triton.jit(interpret=True) 또는 TRITON_INTERPRET=1. CPU 시뮬레이션 + Python breakpoint() 로 GPU 커널 디버깅.
TORCH_LOGS=output_code
torch.compile 이 만든 Triton 커널을 dump. fusion 검증 + 새 커널의 시작점으로 활용.
NCU one-liner
ncu python train.py — 기본 hint 받기. 시각화는 ncu --set full -o trace python … 후 GUI 에서.
의사결정 trigger
NCU 가 “long scoreboard stall” 또는 “uncoalesced reads” hint 를 줄 때 비로소 직접 CUDA. 그 전엔 Triton 으로 충분.

손에 새기기 — 실습 시퀀스

  1. async timing 패턴 베이스라인pytorch_square.py 를 그대로 돌려 torch.square, a*a, a**2 의 GPU 시간을 측정한다. 작은 차이가 어떤 op 디스패치에서 오는지 표로 확인.
  2. load_inline 으로 첫 커널load_inline.py 를 돌리고 ./load_inline_cuda/ 안의 자동 생성된 main.cppbuild.ninja 를 직접 읽는다. pybind 가 어떻게 박혀 있는지 손에 잡힐 때까지.
  3. Chrome trace — 같은 코드에 torch.profiler.profile() 을 둘러 trace.json 을 export 하고 chrome://tracing 에 드래그한다. memcpy H→D 와 커널 사이의 갭을 직접 본다.
  4. Triton interpret 로 디버깅@triton.jit(interpret=True)breakpoint() 로 row-wise square 커널을 한 step 씩 들여다본다. tl.arange, tl.load 의 결과 모양을 직접 print.
  5. BLOCK_SIZE sweep — Triton square 커널을 BLOCK_SIZE ∈ {64, 128, 256, 512, 1024, 2048} 로 측정. torch.square 와 비교. 그래프가 U자나 단조 감소가 아니면 다른 변수가 섞인 것.
  6. torch.compile 의 Triton 코드 읽기TORCH_LOGS=output_codetorch.squaretorch.square(torch.square(x)) 두 케이스를 dump. fusion 이 일어났는지 코드 안에서 직접 확인.
  7. NCU 한번 돌려보기 — 자기 GPU 가 있다면 ncu python pytorch_square.py. 첫 hint 가 무엇인지 적어둔다. cloud 환경에서는 NCU 가 막혀 있을 수 있으니 자기 데스크탑이나 NCU 가 허용된 인스턴스에서.
§ 11다른 강의로 이어지는 길· connections

이 강의의 도구가 다음에 어디에 다시 등장하는지

L001 의 도구들이 시리즈 안에서 어떻게 다시 호출되는지를 묶어둔다 — 같은 도구가 다른 문맥에서 재등장하는 패턴이 GPU Mode 시리즈 전체의 학습 곡선이다.

§ 12열린 질문· open questions

다음에 다시 들었을 때 직접 검증해야 할 것들

학습 노트로 정리하면서 의도적으로 비워둔 자리들 — 강의 안에서 부분적으로만 등장한 주제, 또는 후속 강의에서 본격적으로 다뤄지는 주제.

검증 메모

이 노트의 모든 NCU 수치 예시(occupancy 38%, tail 0.4 wave 등)는 강의에서 Mark 가 보여준 화면을 재구성한 예시 값이다. 자기 GPU 에서 직접 NCU 를 돌려 보고 hint 의 실제 형태를 한 번 봐야 깊이 들어간다.

Lecture 002 → Ch1–3 PMPP book — Andreas Köpf 가 깐 grid/block/thread 의 정식 모델