《GPU Mode》L0042024 · FEB · 03High prioritytranscript · available
Lecture 4 · Compute and Memory Basics
PMPP 의 Ch.4–5 를 한 시간으로 — 커널이 왜 빠르거나 느린가 의 첫 정량 모델. compute/SM/warp/occupancy 의 회계, memory hierarchy 의 latency 와 bandwidth, roofline 으로 보는 compute-bound vs memory-bound 의 갈림길, 그리고 fusion 이 왜 중요한가의 첫 그림. Thomas Viehmann 이 직접 짠 GELU 커널과 batchnorm 이식 일화로 “정량을 모르고 짜면 어디까지 헛발을 짚는가” 를 보여주는 강의.
PMPP Ch.1–3 (L002) 까지는 “GPU 위에 일을 어떻게 분배하는가” 의 framing. 거기서 짠 커널이 빠른지 느린지는 측정해 볼 수는 있어도 왜 그런지 는 답할 수 없다. L004 가 그 자리를 메운다 — Ch.4(GPU 아키텍처)와 Ch.5(메모리 모델 + 성능)를 묶어 “이 커널은 compute 가 모자란가, memory bandwidth 가 모자란가, occupancy 가 모자란가” 의 첫 정량 진단을 가르친다.
강의가 답하는 질문은 셋.
SM 안에서 무엇이 무엇을 도는가 — FP32 unit, Tensor Core, warp scheduler, register file, shared memory.
같은 데이터에 어떤 메모리 계층이 적용되는가 — 그리고 각 계층의 latency · bandwidth 가 대략 얼마인가.
roofline 위에서 내 커널이 어디 있는가 — peak FLOPs 의 70% 인지, peak bandwidth 의 70% 인지, 아니면 둘 다 못 채우고 있는지.
시리즈 안의 위치
이 강의는 나머지 모든 GPU Mode 강의의 진단 도구다. L008 CUDA Performance Checklist 가 이 진단의 actionable 항목들이고, L001 의 NCU hint 가 사실상 이 강의의 한 단어들 (occupancy, scoreboard stall) 을 사용한다. 손에 안 잡히면 그 다음 강의들이 “왜” 를 설명하는 자리에서 멈춘다.
“많은 커널은 메모리에 의해 제한된다 — 그리고 그게 빠른 커널의 첫 번째 사실이다.”Thomas Viehmann · 학습 노트
§ 02SM 의 해부· FP32 / Tensor Core / warp scheduler
한 SM 안에 무엇이 들어 있고, 한 cycle 에 무엇이 도는가
강의의 첫 정량 — 한 SM 안에 32개의 FP32 unit, 그 중 절반은 INT32 도 가능, 그리고 별도의 Tensor Core unit. 한 cycle 에 처리되는 thread 의 단위가 warp = 32 threads 이고, 한 SM 은 여러 warp 를 동시에 schedule 한다 (Ampere 기준 4 warp scheduler).
Ampere SM (A100 기준)
FP32 unit64 (lane)
FP64 unit32 (lane)
INT32FP32 의 절반과 공유
Tensor Core4 unit · MMA 큰 단위
warp scheduler4
register file256 KB
shared / L1192 KB total · split
max threads/SM2048
실행 단위로 보면
warp32 threads · lockstep
block1+ warps · shared 공유
gridblock 들의 집합
한 cyclewarp 단위 instruction issue
divergencewarp 안 분기 → serialize
__syncthreadsblock 안 동기화 barrier
block sync없음 — kernel boundary 만
streamkernel 들의 시퀀스
이 표의 두 가지 사실이 이 강의의 모든 디테일에 반복적으로 등장한다.
warp = 32 threads, 한 cycle 에 한 instruction 을 lockstep 으로 실행 — 그래서 warp 안에서 분기 (if-else) 가 갈라지면 두 path 가 직렬로 돌면서 절반은 idle. 이게 warp divergence.
register file 이 SM 당 256 KB — 한 thread 당 몇 register 를 쓰는지가 SM 위에 동시에 둘 수 있는 thread 수를 직접 제한. occupancy (§06) 의 출발점.
Tensor Core 는 의도적으로 빠진다
Thomas 가 강의에서 명시 — “today we're not going to look at Tensor Core in great detail”. 첫 정량 모델은 일반 FP32 unit 만으로 깐다. Tensor Core 는 후속 (L023 CUTLASS, L036 CUTLASS Cute) 의 영역.
§ 03block 이 SM 에 배정되는 방식· 실행 순서를 가정하지 마라
block 들 사이에는 어떤 동기화도 없다 — 그래서 분배가 자유롭다
강의에서 Thomas 가 가장 못 박는 사실 — “CUDA 는 block 을 SM 들에 임의로 배정한다. 우리는 실행 순서를 모른다.” 이 한 문장이 GPU 의 확장성의 비밀이고, 동시에 학습자가 가장 자주 잊는 사실이다.
이 사실의 결과 두 가지.
block 사이의 통신은 kernel boundary 가 유일 — block A 가 block B 의 결과를 보려면 두 개의 kernel launch 가 필요하다. “이 block 이 다른 block 의 결과를 기다린다” 같은 코드는 deadlock 의 길.
block 사이의 atomic 은 동기화가 아닌 update 만 — atomicAdd 가 race-free 하게 update 를 보장하지만, 순서를 보장하진 않는다. 이게 L009 Reductions 의 핵심 함정.
FIG · block 의 임의 배정108 SM 에 256 block 을 던지면
“wave” = 한 SM 당 한 block 씩 돌아가는 한 묶음. 마지막 wave 가 절반만 차면 나머지 SM 들이 idle. 이걸 L001 에서 NCU 가 “tail effect” 로 hint 줬던 그 자리.
강의에서 Thomas 가 던지는 두 번째 정량. 한 cycle 안에 register 를 읽는 비용과 HBM 을 읽는 비용은 100~200배 차이다. on-chip 위계를 register → shared/L1 → L2 → HBM 으로 깔고 각 단계의 대략적 latency 와 bandwidth 를 외워둔다.
registerthread 별 사적, on-chip. 가장 빠름. compiler 가 할당 — 너무 많이 쓰면 spill1 cycle— per thread —
shared / L1block 안 thread 끼리 공유. __shared__. 같은 SM 의 SRAM 에 위치~30 cycle~19 TB/s (A100)
L2전체 SM 공유. global memory 의 캐시. 자동 관리.~200 cycle~5 TB/s
HBM (global)device DRAM. cudaMalloc 으로 잡는 곳. 주된 bandwidth bottleneck~400-800 cycle~1.5 TB/s (A100)
PCIe / NVLinkCPU↔GPU 또는 GPU↔GPU. cudaMemcpy 의 길.매우 큼~32 GB/s · 600 GB/s
local memoryregister spill 시 “local” 이라 부르지만 실제로는 HBM 에 저장. 최대한 피해야 함.HBM 과 동급— 회피 대상 —
register spill 의 함정
강의에서 Thomas 가 자기 일화로 짚은 자리 — “pyo bnom 커널을 옛날 torch 에서 새 PyTorch 로 옮기는데 indexing 을 실수로 int64 로 했더니 register 가 부족해져서 local memory(=HBM)로 spill 되어 느려졌다.” register 사용량은 nvcc -Xptxas=-v 로 빌드 시 dump 가능. 한 thread 가 64 register 를 넘으면 occupancy 가 떨어지기 시작 (Ampere 기준).
“local memory 라는 이름에 속지 마라 — 실제로는 HBM 에 spill 되는 register 의 다른 이름이다.”Thomas Viehmann · L004
§ 05arithmetic intensity 와 roofline· FLOPs / byte
한 byte 를 가져와서 몇 번의 연산을 하는가 — 이 비율이 모든 것을 결정한다
강의의 가장 중요한 한 그림. arithmetic intensity = FLOPs / byte. 같은 알고리즘이라도 이 값이 작으면 (memory-bound) bandwidth 가 천장이고, 크면 (compute-bound) FLOPs 가 천장이다. 두 천장이 만나는 “ridge point” 가 GPU 마다 정해져 있다 — A100 의 경우 약 13 FLOP/byte (FP32) 또는 ~140 FLOP/byte (Tensor Core BF16).
왼쪽 사선의 기울기는 peak HBM bandwidth. 오른쪽 평평한 선은 peak FP32 throughput (또는 Tensor Core 면 더 위로). 내 커널이 어디 점인지에 따라 “더 빠르게 만들려면 어디를 풀어야 하는가” 가 정해진다.
roofline 위에서 자주 보이는 패턴들.
elementwise op (add, relu, sigmoid) 는 거의 항상 far left — bandwidth 가 천장이다. fusion 의 가장 큰 표적 (§08).
matmul 은 어느 사이즈에서는 ridge 를 넘어 compute-bound. 하지만 tall-skinny matmul (예: M=2, K=1024, N=8192) 은 reuse 가 적어 memory-bound 로 떨어진다.
attention 의 score 계산 (Q·Kᵀ) 은 큰 matmul 처럼 보이지만, softmax 와 V·attn 까지 다 묶어 계산할 때 HBM 왕복이 dominant. L012 FlashAttention 의 본론.
같은 모델의 같은 레이어가 training 시 bound 가 다르다 — backward 의 reduction step 이 종종 memory-bound. 한 화살표가 아니다.
“더 빨리 가게 하려면 — 천장이 어딘지 먼저 보라. 잘못된 천장을 깎으려고 하면 노력이 0.”학습 노트 · L004 §05
§ 06occupancy 의 회계· register · shared · launch shape 의 삼각형
SM 에 동시에 살 수 있는 warp 수 — 세 자원의 가장 빡빡한 한 자리가 결정
occupancy = SM 의 active warp 수 / max warp 수. 이 비율이 100% 면 “SM 이 가득 찼다”. 그런데 이 비율을 결정하는 건 세 자원 중 가장 빡빡한 자리다.
occupancy 를 깎는 세 자원
register/thread256 KB / SM
shared mem/block~100 KB / SM (split)
block sizeblock 수가 너무 많으면
max threads2048 / SM
실제 occupancy셋 중 가장 빡빡한 자리
예시 — 64 reg, 256 thread/block
block 당 register64 × 256 = 16 KB
SM 당 가능 block256 KB / 16 KB = 16
하지만 max 2048 thread2048 / 256 = 8 block
→ 실제8 block × 256 = 2048 thread
occupancy100%
강의에서 Thomas 가 깐 직관 — “higher occupancy 가 항상 좋은 건 아니다.” 이게 흔한 오해다. occupancy 는 memory latency 를 hide 하는 도구다. 한 warp 가 HBM 을 기다리는 동안 다른 warp 가 일을 하면 latency 가 hide 된다. 그래서 memory-bound 커널에서는 occupancy 가 중요하지만, 이미 compute-bound 면 occupancy 가 80% → 60% 떨어져도 거의 차이 없을 수 있다.
occupancy calculator
강의에서 Thomas 가 짚은 도구 — 옛날엔 Excel sheet, 이제는 NCU 가 직접 보여준다. ncu --set full 의 occupancy section. 또는 CUDA Toolkit 의 cudaOccupancyMaxActiveBlocksPerMultiprocessor API. 자기 커널의 register 와 shared mem 사용을 넣으면 occupancy 의 상한을 계산.
§ 07launch overhead· empty kernel benchmark
비어 있는 커널을 띄우면 얼마나 걸리는가 — “고정 비용” 의 수치화
강의의 작은 실험. __global__ void empty() {} 같은 빈 커널을 launch 하면 — host → device 의 launch dispatch 만큼의 시간이 걸린다. 이게 “fixed cost per kernel launch”. 대략 5~20 μs (system, driver, GPU 종류에 따라).
이 수치가 의미 있는 자리는 — 커널이 작을수록이다.
빈 커널 (n=0)
~10 μs
elementwise (n=1024)
~12 μs (대부분 launch)
elementwise (n=10⁶)
~50 μs (kernel 우세)
elementwise (n=10⁸)
~5 ms (kernel 만)
10000개 작은 kernel 직렬
~100 ms (launch only)
마지막 줄이 L006 Optimizing Optimizers 의 핵심 동기다 — 10000 개의 작은 tensor 에 같은 op 를 따로 launch 하면 launch overhead 만 100 ms. foreach / multi_tensor_apply 가 이걸 한 launch 로 합쳐서 거의 0 으로 줄인다.
실측 시작 — 자기 GPU 에서
강의의 notebook 에 empty 커널을 직접 launch 해서 시간을 재는 셀이 있다. 자기 GPU 에서 5 μs 인지 20 μs 인지를 한 번 측정해 두면 — “이 커널은 너무 작아서 launch 가 dominant 이겠다” 가 직관적으로 잡힌다. CUDA Graphs 또는 fusion 으로 가야 할 신호.
§ 08왜 fusion 인가· 매번 HBM 을 다시 왕복하지 마라
EAGER 의 가장 흔한 비효율 — 같은 데이터를 여러 번 읽고 쓴다
강의에서 Thomas 가 가장 명료하게 한 줄로 정리한 fusion 의 동기. “PyTorch eager 는 op 마다 input 을 read, 결과를 write 한다. 두 op 를 합치면 중간을 register 에 두고 한 번만 write.”
FIG · fusion 의 메모리 트래픽 절감x → relu(x) → x*2 의 두 형태
unfused (eager)
R xreluW tmp→R tmp×2W out
fused
R xrelu×2W out
unfused 면 HBM read 2회 + write 2회. fused 면 HBM read 1회 + write 1회. elementwise op 라면 거의 50% bandwidth 절약. arithmetic intensity 는 그대로지만 절대 시간은 절반에 가까워진다.
fusion 의 자동화는 두 자리에서 일어난다.
torch.compile — Inductor 가 elementwise chain 을 자동 fuse. L001 §07 에서 TORCH_LOGS=output_code 로 검증.
강의에서 Thomas 가 짠 GELU 커널이 좋은 예 — 0.5f * x * (1.0f + tanhf(sqrt(2/pi) * (x + 0.044715 * x³))) 안에 곱셈/덧셈/tanh 가 다 들어 있다. 이걸 PyTorch eager 처럼 풀면 5~6개의 elementwise launch + HBM 왕복이 되지만, 한 커널 안에서는 한 번의 read 와 한 번의 write 로 끝난다.
§ 09batchnorm 이식 일화· int32 → int64 의 함정
“실수 한 줄” 이 register pressure 를 통해 200% 의 시간 차이로
강의에서 Thomas 가 자기 디버깅 일화로 깐 가장 인상적인 자리. 옛날 Torch 에서 새 PyTorch 로 batchnorm 커널을 옮기다가 indexing 을 무심코 int64 로 했더니 새 커널이 더 느려졌다. 원인 추적에 한참 걸렸고 — register 사용량이 늘어나서 spill 이 생겼다는 게 진단이었다.
왜 int64 가 register 를 더 먹나
32-bit register 가 GPU 의 기본 단위. int32 한 변수는 1 register, int64 는 2 register. 큰 인덱스 산술이 많이 들어간 커널에서 모든 indexing 변수가 한 번에 register 두 배가 된다. 한 thread 가 64 → 80 register 가 되면 occupancy 가 한 단 떨어지고, 더 늘면 local memory 로 spill — 즉 HBM 으로 보내짐. 결과는 “int64 한 줄” 의 시간 차가 200%.
이 일화의 메시지가 두 줄로 압축된다.
indexing 의 dtype 도 성능 결정 요소. int 면 충분한 자리에 long 을 쓰지 마라. CUDA 의 blockIdx.x 등은 unsigned int.
“느려진 커널” 의 원인이 코드의 한 글자에 있을 수 있다 — 그래서 NCU 의 register/local memory 카운터를 항상 본다.
“정량을 모르면 같은 코드가 왜 느려졌는지 추측만 해야 한다. 정량을 알면 카운터 한 줄에서 답이 나온다.”학습 노트 · L004 §09
강의의 마지막 메시지가 이 일화에서 자연스럽게 따라 나온다 — “성능 일은 추측이 아니라 측정이다.” 그리고 측정하려면 roofline · occupancy · register pressure 의 단어들이 손에 잡혀 있어야 한다. 그게 이 강의의 모든 것.
§ 10기억할 메모와 코드· key takeaways · repo
다시 열었을 때 5분 안에 손으로 잡혀야 할 것
PMPP Ch.4–5 의 정량 모델을 다시 펼치기 전에 머릿속에 남아 있어야 하는 사실들과 자료.
arithmetic intensity
FLOPs / byte. 이 비율이 ridge 보다 작으면 memory-bound, 크면 compute-bound.
roofline
왼쪽 사선(BW) + 오른쪽 평선(FLOPs) 의 두 천장. 내 커널이 어느 점인지 보고 무엇을 풀지 결정.
강의의 framing 안에서 의도적으로 비워둔 자리들과, 자기 GPU 위에서 측정해야 손에 박히는 사실들.
Tensor Core 의 roofline 위치 — 강의는 FP32 unit 만 본다. Tensor Core 가 들어가면 ridge point 가 ~10x 오른쪽으로 이동. L023 CUTLASS 강의에서.
arithmetic intensity 의 정확한 정의 — “FLOP / byte” 의 byte 가 무엇을 셀까? HBM read 만? L2 hit 까지 포함? 강의에서는 흐릿. NVIDIA 공식 정의는 “DRAM bytes” — 즉 HBM 만.
occupancy 와 latency hiding 의 정량 — 100% occupancy 가 75% 보다 얼마나 빠른가? bound 에 따라 다름. 강의에서 정량적인 비교는 안 함.
register pressure 의 정확한 측정 — nvcc -Xptxas=-v 가 보여주는 “registers per thread” 와 NCU 의 “registers per thread” 가 일치하는가? compiler 의 inline 결정 이후의 실제 사용량인가, 정의된 값인가?
L2 cache 의 영향 — 강의는 L1/shared 와 HBM 만 깐다. L2 의 hit 률이 실제 bandwidth 에 어떻게 기여하는지는 빠진다. L008 와 NCU 의 “L2 cache throughput” 카운터에서.
Hopper (H100) 의 SM 변화 — 강의는 Ampere 기준. H100 은 SM 당 더 많은 register, 더 큰 L2, async copy 의 등장 (cp.async). 같은 정량 모델이지만 숫자가 다르다.
GELU 커널의 직접 측정 — Thomas 가 강의에서 GELU 커널을 짜고 PyTorch eager 와 비교했지만, 측정 결과를 정확히 재구성하지 못했다. (확인 필요) notebook 에서 직접 돌려 시간 차이 확인.
검증 메모
이 노트의 모든 latency/bandwidth 숫자(register 1 cycle, shared 30 cycle, HBM 400 cycle 등) 는 Ampere 기준의 대략적 값이다. 정확한 측정은 micro-benchmark 가 필요. Demystifying Latency and Bandwidth (Mei et al., 2017) 같은 논문이 PTX 단위 측정을 한다.