gpumode · 강의 아카이브
《GPU Mode》 L029 2024 · AUG · 31 High priority transcript · available

Triton Internals

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 이 어디서 일어나는가 를 다섯 페이지 안에 잡는다.

Triton compiler MLIR TritonGPU IR software pipeline layout PTX cubin ~/.triton/cache jit
K
Speaker
Kapil Sharma
Meta · Triton compiler internals 블로그 시리즈 저자
강의 번호
L029
스피커
Kapil Sharma
학습 우선순위
High · 정독
다시 볼 때
cache 직접 들여다본다
§ 01강의가 풀려는 문제· 왜 Triton 이 빠른가

BLOCK_SIZE 한 줄로 perf 가 10배 차이 나는” 자리의 내부

Triton 사용자라면 누구나 안다 — 같은 커널 소스가 launch 설정만으로 perf 가 한 자리수 차이가 난다. 왜? 이 강의는 그 “왜” 를 컴파일러 단계로 분해한다. Kapil 의 두 편짜리 블로그 시리즈를 영상 형태로 옮긴 것.

강의가 답하려는 두 줄 —

  1. @triton.jit 데코레이터 안에서 무엇이 일어나는가 — 첫 호출, 두 번째 호출, cache hit 이 각각 어떤 작업을 한다.
  2. BLOCK_SIZE, num_warps, num_stages 가 어느 단계에서 어떤 결정으로 바뀌는가 — 사용자가 주는 hint 가 컴파일러의 어디에 흘러가는가.

강의의 frame 은 명시적으로 — “CUDA 의 nvcc lowering 은 cuc → PTX → SASS 의 3 단계로 잘 알려져 있다. Triton 은 그 위에 세 단계가 더 얹혀 있을 뿐이다”. 그 다섯 단계가 강의 전체를 끌고 간다.

강의의 frame

Triton 은 “Python 으로 짜는 GPU 커널” 이라는 사용자 시점이 있고, “MLIR 위에 얹힌 multi-IR 컴파일러” 라는 시스템 시점이 있다. Kapil 은 두 시점을 한 줄로 잇는다 — “같은 vector_add 가 다섯 가지 IR 형태로 어떻게 변하는가”. 이 다섯 IR 을 직접 보면 perf 의 결정자가 어디에 있는지 손에 잡힌다.

“Triton 은 black box 가 아닙니다. ~/.triton/cache 에 모든 IR 이 텍스트로 떨어져 있어요. 한 번이라도 직접 열어보면 컴파일러를 더 잘 쓰게 됩니다.”Kapil Sharma · 02:41

강의 끝에 손에 잡혀야 하는 건 — 5 단계의 이름과 입출력, 각 단계에서 일어나는 핵심 결정(layout, pipeline, allocation), cache 디렉터리의 구조, 그리고 perf 디버깅을 어디부터 시작해야 하는지 의 한 페이지 mental model.

§ 02컴파일 5 단계 사다리· Python → PTX

같은 add_kernel 이 다섯 가지 IR 형태로 변하는 길

강의의 본론이 시작되는 자리. Kapil 이 같은 vector_add 커널을 가지고 5 단계를 한 단계씩 따라간다. 각 단계가 정확히 무엇을 하고, 무엇을 결정하고, 무엇을 다음 단계로 넘기는가.

FIG · Triton 컴파일 사다리5 stages · 1 add_kernel
L0 · DSL @triton.jit Pythontl.load · tl.store · tl.dot 같은 tile-level op. BLOCK_SIZE 같은 tl.constexpr 가 specialization key 가 됨. 사용자가 짜는 단계
~50 lines
L1 · Triton IR tile-level IR (TTIR)scalar 가 아닌 tile 이 일급 객체. tensor type 이 명시적. 아직 GPU 와 무관 — 어떤 GPU 인지는 다음 단계가 결정. vendor-neutral
~80 lines MLIR
L2 · TritonGPU IR GPU-specific dialect (TTGIR)여기서 layout(blocked / shared / mma) 가 텐서마다 박힌다. num_warps, num_stages 의 효과가 표현됨. ssoftware pipeline pass, layout conversion pass 등이 여기서 적용. perf 결정의 핵심 자리
~200 lines
L3 · LLVM IR NVVM (LLVM + NVIDIA intrinsic)tile 단위 op 가 vector + scalar 명령으로 분해됨. llvm.nvvm.read.ptx.sreg.tid.x 같은 intrinsic. register 할당이 LLVM 의 backend 단계에서 일어남. ~700 lines
(같은 add 기준)
L4 · PTX → cubin virtual ISA · 그리고 binaryNVIDIA ptxas 가 PTX → SASS → cubin. cubin 이 ~/.triton/cache 에 저장되고 같은 specialization 에 대해 재사용. ~150 lines PTX
+ binary
사용자가 보는 입력은 L0. 첫 호출 시 L0→L4 전체가 돈다. 같은 (kernel, dtypes, BLOCK_SIZE) 조합이 다시 호출되면 L4 의 cubin 이 즉시 reuse. 이게 @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
}
§ 03layout 결정 단계· blocked · shared · mma layout

L2 에서 일어나는 진짜 perf 의 결정 — “이 텐서가 어떻게 쓰레드에 흩어지는가”

Triton IR (L1) 에서는 텐서가 그냥 tensor<1024xf32>. 어느 thread 가 어느 element 를 들고 있는지 안 정해져 있다. L2 의 TritonGPU IR 에서 그 mapping 이 박힌다 — 그게 layout. perf 의 결정자가 거의 다 여기서 정해진다.

Triton 의 4 가지 핵심 layout —

  • BlockedLayout — 일반 elementwise. (sizePerThread, threadsPerWarp, warpsPerCTA) 의 곱이 tensor shape. tl.load/store 의 기본.
  • SharedLayout — shared memory 위 텐서. swizzle pattern (XOR-based) 이 박혀서 bank conflict 회피.
  • MmaLayout / WMMA Layout — Tensor Core 가 요구하는 매우 특수한 layout. tl.dot 의 입력/출력에 자동 적용. Hopper 에서는 WgmmaLayout.
  • DotOperandLayout — mma 의 입력 — load → DotOperand → dot → mma_result 의 흐름.

이 layout 들 사이의 전환을 layout conversion이라 부른다. Triton 컴파일러의 큰 일 중 하나가 layout conversion 의 비용을 최소화 — 가능하면 shared memory 를 거치지 않고 register-to-register shuffle.

FIG · matmul 의 layout 전환 흐름load → dot → store
L
load
BlockedLayout
C
conversion
→ SharedLayout
D
load to op
DotOperand
M
mma
MmaLayout 결과
S
store
→ BlockedLayout
단순한 GEMM 한 번도 5 번의 layout 사이를 오간다. 컴파일러가 잘 짜면 SharedLayout 을 통해 swizzle 로 bank conflict 0. 잘못 짜면 round trip 이 늘어 register pressure.
num_warps 가 layout 에 박히는 자리

num_warps=4 는 BlockedLayout 의 warpsPerCTA = [4, 1] 같이 박힌다. 한 텐서를 4 warp 가 분담. num_warps=8 이면 [8,1] 또는 [4,2] — shape 별로 컴파일러가 자동 결정. 이게 launch 설정이 layout 으로 흘러가는 자리다.

§ 04software pipeline· num_stages 의 의미

num_stages=4 가 실제로 어떤 코드 변환을 하는가

L2 의 가장 흥미로운 pass — software pipelining. matmul 의 K-loop 처럼 load → compute → load → compute 가 반복되는 패턴에서, load 와 compute 를 시간적으로 겹치는 변환. num_stages 가 이 변환의 stage 수를 결정.

FIG · num_stages=2 의 코드 변환load 와 compute 의 시간 겹침
변환 전 (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
Hopper 의 cp.async.bulk + barrier 가 이 변환의 hardware enabler. num_stages 가 클수록 latency hiding 이 강해지지만 — shared memory 를 더 쓴다(stage 마다 buffer 한 벌). 균형점이 모델/shape 마다 다름.

software pipeline 의 효과를 결정짓는 변수들 —

  • K_BLOCKS 가 충분히 많아야 효과. K 가 작으면 prologue/epilogue 의 비중이 커져 손해.
  • shared memory budget. num_stages × (BLOCK_M + BLOCK_N) × dtype 가 한 SM 의 shared mem 안에 들어와야.
  • register pressure. stage 가 많으면 register 도 더 — spill 발생 가능.
  • cp.async.bulk / TMA. Hopper 부터는 async copy 가 hardware level. pipeline 의 수율이 한 자리수 좋아진다.
num_stages 의 sweet spot

일반적인 GEMM-like 커널 — A100 에서 3 또는 4, H100 에서 4 또는 5. flash attention 의 경우 2 가 최적일 때도 — K loop 가 짧고 shared memory 가 이미 많이 쓰이니까. autotune 으로 sweep 해야 답이 나온다는 게 정석.

§ 05register allocation· spill 의 자리

L3 LLVM 단계 — 텐서 한 element 가 어느 register 에 살게 되는가

L2 까지는 “tile 의 element 가 thread 의 어디에 있다” 라는 abstract level. L3 의 LLVM IR 에서 그 element 가 구체적인 virtual register에 박힌다. 그 다음 LLVM 의 register allocator 가 physical register 에 mapping. 이 자리에서 spill 이 발생할 수 있다.

register pressure 가 높은 자리들 —

  • 큰 BLOCK_SIZE. BLOCK_M × BLOCK_K = 256 × 64 의 fp32 fragment 면 thread 당 ~64 element 의 partial sum. register 가 빠르게 차오름.
  • 여러 fragment 동시 보관. flash attention 의 Q, K, V, O, l, m 동시 보관 — register pressure 의 핵심.
  • num_stages 의 부산물. pipelined load 가 register-resident 면 stage 만큼 추가.

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 가 분해된 것이 흔한 원인.

§ 06~/.triton/cache 들여다보기· artifact 의 모양

한 번이라도 직접 열어보면 컴파일러를 더 잘 쓰게 된다

강의의 가장 실용적인 부분 — Kapil 이 터미널에서 ~/.triton/cache 디렉터리를 열어 안의 파일들을 한 줄씩 보여준다. 각 specialization 마다 한 디렉터리, 그 안에 모든 IR 단계의 텍스트가 떨어져 있다.

FIG · 한 specialization 디렉터리의 내용물~/.triton/cache/<hash>/
add_kernel.ttirL1 — Triton IR. vendor neutral, scalar/tile 명령
add_kernel.ttgirL2 — TritonGPU IR. layout 이 박혀 있음. 핵심 자료
add_kernel.llirL3 — LLVM NVVM IR. virtual register 가 보임
add_kernel.ptxL4 — virtual ISA 텍스트. ptxas 의 입력
add_kernel.cubinL4 — binary. cuobjdump --dump-sass 으로 SASS 도 볼 수 있음
add_kernel.jsonmetadata — num_warps, shared mem usage, register count, spill count 등
__grp__add_kernel.jsongrouped metadata — autotune 결과
디렉터리 이름이 hash. 같은 (kernel, dtype, BLOCK_SIZE) 가 같은 hash → cache hit. repo 에 강의의 add_kernel.9969bdda_0123.{c,h} 가 그 hash 의 한 사례.
# 직접 열어보는 명령 — 강의 데모 그대로
$ 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:52
§ 07디버깅 트릭· interpret · MLIR_ENABLE_DUMP

커널이 도는 동안 — 그리고 컴파일러가 도는 동안 — 안을 들여다본다

Triton 의 두 가지 검사 도구 — 커널 실행 검사(interpret 모드)와 컴파일러 단계 검사(MLIR dump). 강의에서 Kapil 이 두 가지 모두 데모.

1. interpret 모드 — 커널을 CPU 위에서

@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 별로 보는 데 결정적.

2. MLIR dump — 컴파일러 단계 출력

# 모든 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 추적. 사다리에서 위로 갈수록 빈도가 낮아짐.

§ 08흔한 함정· layout mismatch · pointer math

“코드는 맞는 것 같은데 컴파일러가 이상한 짓을 한다” 의 단골 원인

layout mismatch — silent slowdown
두 텐서의 layout 이 안 맞으면 컴파일러가 silent layout conversion 을 끼워넣는다. 결과는 정확하지만 perf 가 떨어짐. ~/.triton/cache 의 .ttgir 에서 convert_layout op 가 보이는지 확인.
pointer 산술의 type
x_ptr + offs 에서 offs 가 int64 면 — H100 에서 32-bit indexing 보다 느리다. 큰 인덱스가 필요 없으면 int32 를 명시. flash attention 의 코드도 64bit indexing 을 별도 분기로 처리.
tl.where 의 컴파일
tl.where(mask, a, b) 가 어떤 단계에서는 select 명령으로 lower, 다른 단계에서는 branch 로. predicated load (mask 인자) 와 명시적 if 는 다른 코드를 만든다.
non-contiguous tensor
PyTorch 에서 .transpose() 후 바로 Triton 에 넣으면 — Triton 은 physical view 만 본다. logical 한 transpose 는 일어나지 않은 것처럼 동작. ".contiguous() 한 번 더" 가 흔한 fix.
BLOCK_SIZE 가 power of 2 가 아닐 때
tl.arange 가 power of 2 만 받음. 32, 64, 128 같이 쓴다. hidden_size 가 power of 2 가 아닐 때 padding 또는 mask 로 처리.
tl.constexpr 의 retrigger
constexpr 인자가 바뀔 때마다 새 specialization. autotune 의 모든 config 가 별도 컴파일. cache 의 디렉터리 수가 수십 개로 늘 수 있음.
§ 09Triton 3.x 의 변화· Hopper · Blackwell 지원

spec 이 H100/H200 에서 무엇을 새로 받는가

Triton 3.x (강의 시점에 베타) 가 가져오는 큰 변화들. 아키텍처가 새 명령을 내놓을 때마다 컴파일러가 따라가야 한다 — H100 의 wgmma, TMA, async barrier 가 그 핵심.

wgmma (warp-group mma)
Hopper 의 새 mma 명령. 4 warp 가 한 group 으로 행동. MmaLayout 이 WgmmaLayout 으로 확장.
TMA (Tensor Memory Accelerator)
async bulk copy 의 hardware. tl.make_block_ptrtl.advance 가 TMA 로 lower. software pipeline 의 수율이 크게 개선.
async barrier
cp.async.bulk.commit_group, wait_group. pipeline stage 사이의 동기화가 hardware level.
FP8 type
tl.float8e4m3, tl.float8e5m2. mma 가 native 로 받음. Transformer Engine 과의 통합.
cluster mode
여러 SM 이 한 cluster 로 묶여서 distributed shared memory 공유. flash attention 의 추가 최적화 자리.
AMD / Intel backend
Triton 3.x 가 NVIDIA 외 backend 를 정식으로. AMD MI300 의 mfma, Intel XMX 의 joint_matrix 가 같은 dialect 로.
“Triton 의 매력은 NVIDIA 가 새 명령을 내놓을 때마다 — 사용자 코드를 안 바꿔도 그 명령이 자동으로 들어온다는 것입니다. flash attention v3 가 wgmma + TMA 를 쓴다는 건 컴파일러가 그것들을 lowering 할 때만 가능합니다.”Kapil Sharma · 1:14:07
§ 10기억할 메모와 코드· key takeaways

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

5 단계 사다리
L0 Python → L1 Triton IR → L2 TritonGPU IR → L3 LLVM NVVM → L4 PTX/cubin. 각 단계의 입출력과 핵심 결정.
L2 가 perf 의 자리
layout, software pipeline, register hint 가 박힘. num_warps, num_stages, BLOCK_SIZE 가 모두 여기서 효과.
layout 4 가지
BlockedLayout(elementwise), SharedLayout(swizzle), MmaLayout(Tensor Core), DotOperandLayout(mma input). 사이의 전환이 perf cost.
software pipeline
load 와 compute 의 시간 겹침. num_stages 가 stage 수. shared memory + register pressure 와의 균형.
~/.triton/cache
specialization 별 디렉터리. .ttir/.ttgir/.llir/.ptx/.cubin/.json. 한 번 직접 열어볼 것.
디버깅 사다리
logical bug → interpret. perf bug → cache 의 .ttgir 와 .json. compiler bug → MLIR_ENABLE_DUMP.
흔한 함정
silent layout conversion · int64 indexing · non-contiguous · constexpr 의 cache 폭발.
Triton 3.x 변화
wgmma, TMA, async barrier, FP8, cluster mode, AMD/Intel backend. 사용자 코드 거의 그대로 새 명령을 받는다.

손에 새기기 — 실습 시퀀스

  1. vector_add 한 번 실행 — 강의 repo 의 vector_add.py. ~/.triton/cache 안에 새 디렉터리가 생기는 것 확인.
  2. 5 단계 IR 직접 열기 — 그 디렉터리 안의 .ttir, .ttgir, .llir, .ptx 를 열어 같은 vector_add 가 어떻게 변하는지 본다.
  3. BLOCK_SIZE sweep + cache — BLOCK_SIZE 를 다섯 가지로. 새 디렉터리 다섯 개. 각 디렉터리의 .ttgir 의 layout 차이 확인.
  4. num_warps 의 효과 — num_warps=1, 4, 8 로 같은 커널. .ttgir 에서 BlockedLayout 의 warpsPerCTA 가 어떻게 박히는지.
  5. num_stages 의 효과 — matmul 커널로. num_stages=1 vs 3 의 .llir 비교 — pipelined load 의 prologue/epilogue 가 보이는지.
  6. spill 발생시키기 — 매우 큰 BLOCK 으로 register 를 일부러 압박. .json 의 spill_count > 0 인지 확인. SASS 에서 LDL(local memory load) 명령이 보이는지.
  7. MLIR pass 추적MLIR_ENABLE_DUMP=1 으로 한 컴파일 전체 dump. layout conversion pass 가 무엇을 변환하는지 본다.
  8. SASS 까지 내려가기cuobjdump --dump-sass. 자기 GPU(A100/H100/RTX) 별 명령 차이 확인. wgmma 명령이 보이는 자리.
§ 11다른 강의로 이어지는 길· connections

이 강의의 사다리가 시리즈 안에 어떻게 다시 등장하는지

§ 12열린 질문· open questions

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

검증 메모

이 노트의 IR snippet 들은 강의에서 Kapil 이 보여준 버전을 단순화한 것. 실제 .ttgir 는 훨씬 더 길고 attribute 가 많이 박혀 있다. 자기 환경에서 직접 cache 디렉터리를 열어 비교해야 한다.

← Lecture 028 Liger Kernel — Byron Hsu 의 Triton LLM 학습 커널 Lecture 030 → Quantized Training — Thien Tran 의 양자화 학습 커널