Triton 의 한 줄 @triton.jit 안에서 실제로 무엇이 벌어지는가 — Kapil Sharma 가 직접 컴파일러 소스를 짚어가며 깐 5 단계 lowering pipeline(Python AST → Triton IR → TritonGPU IR → LLVM IR → PTX) 의 학습 노트. 같은 vector_add 를 6 개 IR 형태로 따라가면서 — layout 결정, software pipeline, register allocation 이 어디서 일어나는가 를 다섯 페이지 안에 잡는다.
BLOCK_SIZE 한 줄로 perf 가 10배 차이 나는” 자리의 내부Triton 사용자라면 누구나 안다 — 같은 커널 소스가 launch 설정만으로 perf 가 한 자리수 차이가 난다. 왜? 이 강의는 그 “왜” 를 컴파일러 단계로 분해한다. Kapil 의 두 편짜리 블로그 시리즈를 영상 형태로 옮긴 것.
강의가 답하려는 두 줄 —
@triton.jit 데코레이터 안에서 무엇이 일어나는가 — 첫 호출, 두 번째 호출, cache hit 이 각각 어떤 작업을 한다.BLOCK_SIZE, num_warps, num_stages 가 어느 단계에서 어떤 결정으로 바뀌는가 — 사용자가 주는 hint 가 컴파일러의 어디에 흘러가는가.강의의 frame 은 명시적으로 — “CUDA 의 nvcc lowering 은 cuc → PTX → SASS 의 3 단계로 잘 알려져 있다. Triton 은 그 위에 세 단계가 더 얹혀 있을 뿐이다”. 그 다섯 단계가 강의 전체를 끌고 간다.
Triton 은 “Python 으로 짜는 GPU 커널” 이라는 사용자 시점이 있고, “MLIR 위에 얹힌 multi-IR 컴파일러” 라는 시스템 시점이 있다. Kapil 은 두 시점을 한 줄로 잇는다 — “같은 vector_add 가 다섯 가지 IR 형태로 어떻게 변하는가”. 이 다섯 IR 을 직접 보면 perf 의 결정자가 어디에 있는지 손에 잡힌다.
~/.triton/cache 에 모든 IR 이 텍스트로 떨어져 있어요. 한 번이라도 직접 열어보면 컴파일러를 더 잘 쓰게 됩니다.”Kapil Sharma · 02:41강의 끝에 손에 잡혀야 하는 건 — 5 단계의 이름과 입출력, 각 단계에서 일어나는 핵심 결정(layout, pipeline, allocation), cache 디렉터리의 구조, 그리고 perf 디버깅을 어디부터 시작해야 하는지 의 한 페이지 mental model.
add_kernel 이 다섯 가지 IR 형태로 변하는 길강의의 본론이 시작되는 자리. Kapil 이 같은 vector_add 커널을 가지고 5 단계를 한 단계씩 따라간다. 각 단계가 정확히 무엇을 하고, 무엇을 결정하고, 무엇을 다음 단계로 넘기는가.
BLOCK_SIZE 같은 tl.constexpr 가 specialization key 가 됨.
사용자가 짜는 단계llvm.nvvm.read.ptx.sreg.tid.x 같은 intrinsic. register 할당이 LLVM 의 backend 단계에서 일어남.
~700 lines~/.triton/cache 에 저장되고 같은 specialization 에 대해 재사용.
~150 lines PTX@triton.jit 의 specialization 메커니즘.# L0 — 우리가 짜는 코드
@triton.jit
def add_kernel(x_ptr, y_ptr, out_ptr, n,
BLOCK: tl.constexpr):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
mask = offs < n
x = tl.load(x_ptr + offs, mask=mask)
y = tl.load(y_ptr + offs, mask=mask)
tl.store(out_ptr + offs, x + y, mask=mask)
// L1 — Triton IR (간략)
tt.func @add_kernel(%x: !tt.ptr<f32>, ...) {
%pid = tt.get_program_id x : i32
%offs = tt.make_range {start=0, end=1024}
%addrs = tt.addptr %x_ptr, %offs
%x = tt.load %addrs, %mask
%y = tt.load %y_addrs, %mask
%sum = arith.addf %x, %y : tensor<1024xf32>
tt.store %out_addrs, %sum, %mask
tt.return
}
Triton IR (L1) 에서는 텐서가 그냥 tensor<1024xf32>. 어느 thread 가 어느 element 를 들고 있는지 안 정해져 있다. L2 의 TritonGPU IR 에서 그 mapping 이 박힌다 — 그게 layout. perf 의 결정자가 거의 다 여기서 정해진다.
Triton 의 4 가지 핵심 layout —
tl.load/store 의 기본.tl.dot 의 입력/출력에 자동 적용. Hopper 에서는 WgmmaLayout.이 layout 들 사이의 전환을 layout conversion이라 부른다. Triton 컴파일러의 큰 일 중 하나가 layout conversion 의 비용을 최소화 — 가능하면 shared memory 를 거치지 않고 register-to-register shuffle.
num_warps=4 는 BlockedLayout 의 warpsPerCTA = [4, 1] 같이 박힌다. 한 텐서를 4 warp 가 분담. num_warps=8 이면 [8,1] 또는 [4,2] — shape 별로 컴파일러가 자동 결정. 이게 launch 설정이 layout 으로 흘러가는 자리다.
num_stages=4 가 실제로 어떤 코드 변환을 하는가L2 의 가장 흥미로운 pass — software pipelining. matmul 의 K-loop 처럼 load → compute → load → compute 가 반복되는 패턴에서, load 와 compute 를 시간적으로 겹치는 변환. num_stages 가 이 변환의 stage 수를 결정.
변환 전 (num_stages=1):
for k in K_BLOCKS:
a = load(A[k]) ┐
b = load(B[k]) ├ memory stall — load latency 다 노출
acc += dot(a, b) ┘
변환 후 (num_stages=2 — pipelined):
a0 = load(A[0]) ┐
b0 = load(B[0]) │ prologue
for k in K_BLOCKS[1:]:
a_next = load(A[k]) │ next 의 load 가
b_next = load(B[k]) │ 현재 iter 의 dot 와 겹침
acc += dot(a, b) │
a = a_next; b = b_next │
acc += dot(a, b) │ epilogue
num_stages 가 클수록 latency hiding 이 강해지지만 — shared memory 를 더 쓴다(stage 마다 buffer 한 벌). 균형점이 모델/shape 마다 다름.software pipeline 의 효과를 결정짓는 변수들 —
일반적인 GEMM-like 커널 — A100 에서 3 또는 4, H100 에서 4 또는 5. flash attention 의 경우 2 가 최적일 때도 — K loop 가 짧고 shared memory 가 이미 많이 쓰이니까. autotune 으로 sweep 해야 답이 나온다는 게 정석.
L2 까지는 “tile 의 element 가 thread 의 어디에 있다” 라는 abstract level. L3 의 LLVM IR 에서 그 element 가 구체적인 virtual register에 박힌다. 그 다음 LLVM 의 register allocator 가 physical register 에 mapping. 이 자리에서 spill 이 발생할 수 있다.
register pressure 가 높은 자리들 —
BLOCK_M × BLOCK_K = 256 × 64 의 fp32 fragment 면 thread 당 ~64 element 의 partial sum. register 가 빠르게 차오름.spill 이 일어나면 — register 에 못 들어가는 값을 local memory(off-chip) 으로 내려보낸다. local memory 는 사실상 device memory 의 한 segment 라 latency 가 높다. spill 이 한 줄이라도 있으면 perf 가 한 자리수 떨어질 수 있다.
// L3 — LLVM NVVM (간략)
define void @add_kernel(...) {
%tid = call i32 @llvm.nvvm.read.ptx.sreg.tid.x()
%bid = call i32 @llvm.nvvm.read.ptx.sreg.ctaid.x()
%off = ... // pid * BLOCK + tid
// 8 element vector load — coalesced
%vec_x = load <8 x float>, <8 x float>* %addr
%vec_y = load <8 x float>, <8 x float>* %addr
%vec_s = fadd <8 x float> %vec_x, %vec_y
store <8 x float> %vec_s, <8 x float>* %out
ret void
}
L4 의 PTX 에 도착하면 — ld.global.v4.f32 같은 vectorized load 명령. NCU 가 “uncoalesced” 라고 hint 를 주면 — L2 에서 layout 이 잘못 박혀서 vector load 가 분해된 것이 흔한 원인.
강의의 가장 실용적인 부분 — Kapil 이 터미널에서 ~/.triton/cache 디렉터리를 열어 안의 파일들을 한 줄씩 보여준다. 각 specialization 마다 한 디렉터리, 그 안에 모든 IR 단계의 텍스트가 떨어져 있다.
cuobjdump --dump-sass 으로 SASS 도 볼 수 있음# 직접 열어보는 명령 — 강의 데모 그대로
$ ls ~/.triton/cache/
8a3b...c4d/ # add_kernel BLOCK=1024 · fp32
9912...abc/ # add_kernel BLOCK=1024 · fp16
$ cd 8a3b...c4d/
$ ls
add_kernel.ttir add_kernel.ptx
add_kernel.ttgir add_kernel.cubin
add_kernel.llir add_kernel.json
# shared memory 와 register 사용량 한 줄에 — JSON 안
$ jq '.shared, .num_warps, .num_ctas' add_kernel.json
24576
4
1
# SASS 까지 보고 싶으면 — cuobjdump
$ cuobjdump --dump-sass add_kernel.cubin | head -30
~/.triton/cache 에 한 번 들어가서 .ttgir 파일을 열어보세요. 자기가 짠 커널의 layout 이 어떻게 박혔는지 그 텍스트만으로 파악됩니다.”Kapil Sharma · 41:52Triton 의 두 가지 검사 도구 — 커널 실행 검사(interpret 모드)와 컴파일러 단계 검사(MLIR dump). 강의에서 Kapil 이 두 가지 모두 데모.
@triton.jit(interpret=True)
def add_kernel(...):
pid = tl.program_id(0)
offs = pid * BLOCK + tl.arange(0, BLOCK)
breakpoint() # pdb 가 동작
x = tl.load(x_ptr + offs)
# pdb 안에서 print(x.tensor) 가능
# 환경변수로도 가능 — 커널 변경 없이
# TRITON_INTERPRET=1 python script.py
CPU simulation 이라 매우 느리다. 그래도 tl.arange, tl.load 의 결과 모양을 step 별로 보는 데 결정적.
# 모든 pass 의 in/out IR 을 stdout 에
$ MLIR_ENABLE_DUMP=1 python script.py 2> trace.log
# 특정 pass 만
$ MLIR_ENABLE_DUMP=1 \
TRITON_REPRODUCER_PATH=/tmp/repro.mlir \
python script.py
# 한 IR 파일을 input 으로 직접 컴파일
$ triton-opt --convert-triton-to-tritongpu in.ttir
컴파일러 자체를 디버깅하거나, “이 pass 가 무슨 일을 했는가” 를 확인할 때. compiler engineer 의 도구지만, perf 디버깅에 결정적인 자리가 있다.
(1) logical bug — 결과가 틀림. interpret 모드 + breakpoint. (2) perf bug — 결과는 맞지만 느림. ~/.triton/cache 의 .ttgir 와 .json 의 register/shared 보기. (3) compiler bug — 컴파일러가 잘못된 코드를 만든 것. MLIR_ENABLE_DUMP 로 pass 추적. 사다리에서 위로 갈수록 빈도가 낮아짐.
convert_layout op 가 보이는지 확인.x_ptr + offs 에서 offs 가 int64 면 — H100 에서 32-bit indexing 보다 느리다. 큰 인덱스가 필요 없으면 int32 를 명시. flash attention 의 코드도 64bit indexing 을 별도 분기로 처리.tl.where(mask, a, b) 가 어떤 단계에서는 select 명령으로 lower, 다른 단계에서는 branch 로. predicated load (mask 인자) 와 명시적 if 는 다른 코드를 만든다..transpose() 후 바로 Triton 에 넣으면 — Triton 은 physical view 만 본다. logical 한 transpose 는 일어나지 않은 것처럼 동작. ".contiguous() 한 번 더" 가 흔한 fix.Triton 3.x (강의 시점에 베타) 가 가져오는 큰 변화들. 아키텍처가 새 명령을 내놓을 때마다 컴파일러가 따라가야 한다 — H100 의 wgmma, TMA, async barrier 가 그 핵심.
tl.make_block_ptr 와 tl.advance 가 TMA 로 lower. software pipeline 의 수율이 크게 개선.cp.async.bulk.commit_group, wait_group. pipeline stage 사이의 동기화가 hardware level.tl.float8e4m3, tl.float8e5m2. mma 가 native 로 받음. Transformer Engine 과의 통합.num_warps, num_stages, BLOCK_SIZE 가 모두 여기서 효과.num_stages 가 stage 수. shared memory + register pressure 와의 균형.vector_add.py. ~/.triton/cache 안에 새 디렉터리가 생기는 것 확인.LDL(local memory load) 명령이 보이는지.MLIR_ENABLE_DUMP=1 으로 한 컴파일 전체 dump. layout conversion pass 가 무엇을 변환하는지 본다.cuobjdump --dump-sass. 자기 GPU(A100/H100/RTX) 별 명령 차이 확인. wgmma 명령이 보이는 자리.tl.dot 의 backward 를 어떻게 수동으로 짜는가의 표준 패턴.이 노트의 IR snippet 들은 강의에서 Kapil 이 보여준 버전을 단순화한 것. 실제 .ttgir 는 훨씬 더 길고 attribute 가 많이 박혀 있다. 자기 환경에서 직접 cache 디렉터리를 열어 비교해야 한다.