gpumode · 강의 아카이브
《GPU Mode》 L004 2024 · FEB · 03 High priority transcript · 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 이식 일화로 “정량을 모르고 짜면 어디까지 헛발을 짚는가” 를 보여주는 강의.

roofline arithmetic intensity occupancy memory hierarchy SM · warp register pressure launch overhead fusion PMPP Ch.4–5
T
Speaker
Thomas Viehmann
MathInf · 전 PyTorch core · The Incredible PyTorch 책 저자
강의 번호
L004
스피커
Thomas Viehmann
학습 우선순위
High · 정독
다시 볼 때
자기 GPU 의 roofline 을 그린다
§ 01강의가 풀려는 문제· Why this lecture exists

커널이 왜 빠른지 / 왜 느린지를 첫 정량 모델로

PMPP Ch.1–3 (L002) 까지는 “GPU 위에 일을 어떻게 분배하는가” 의 framing. 거기서 짠 커널이 빠른지 느린지는 측정해 볼 수는 있어도 왜 그런지 는 답할 수 없다. L004 가 그 자리를 메운다 — Ch.4(GPU 아키텍처)와 Ch.5(메모리 모델 + 성능)를 묶어 “이 커널은 compute 가 모자란가, memory bandwidth 가 모자란가, occupancy 가 모자란가” 의 첫 정량 진단을 가르친다.

강의가 답하는 질문은 셋.

  1. SM 안에서 무엇이 무엇을 도는가 — FP32 unit, Tensor Core, warp scheduler, register file, shared memory.
  2. 같은 데이터에 어떤 메모리 계층이 적용되는가 — 그리고 각 계층의 latency · bandwidth 가 대략 얼마인가.
  3. 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 들의 시퀀스

이 표의 두 가지 사실이 이 강의의 모든 디테일에 반복적으로 등장한다.

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 의 확장성의 비밀이고, 동시에 학습자가 가장 자주 잊는 사실이다.

이 사실의 결과 두 가지.

FIG · block 의 임의 배정108 SM 에 256 block 을 던지면
grid (256 blocks) b0 b1 b2 b255 SM 0..107 (108 SM) SM 0 b0 b8 SM 1 b1 b9 SM 2 b3 b11 SM 107 b107 b215 wave 1: 108 block · wave 2: 108 block · wave 3: 40 block (under-filled — tail effect) → 256 / 108 = 2.37 wave. 0.37 wave 가 비어 있는 게 “tail effect”.
“wave” = 한 SM 당 한 block 씩 돌아가는 한 묶음. 마지막 wave 가 절반만 차면 나머지 SM 들이 idle. 이걸 L001 에서 NCU 가 “tail effect” 로 hint 줬던 그 자리.
§ 04memory hierarchy 의 latency· register · shared · L1/L2 · HBM

같은 데이터, 어디서 읽느냐에 따라 200배 차이

강의에서 Thomas 가 던지는 두 번째 정량. 한 cycle 안에 register 를 읽는 비용과 HBM 을 읽는 비용은 100~200배 차이다. on-chip 위계를 register → shared/L1 → L2 → HBM 으로 깔고 각 단계의 대략적 latency 와 bandwidth 를 외워둔다.

register thread 별 사적, on-chip. 가장 빠름. compiler 가 할당 — 너무 많이 쓰면 spill 1 cycle — per thread —
shared / L1 block 안 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 / NVLink CPU↔GPU 또는 GPU↔GPU. cudaMemcpy 의 길. 매우 큼 ~32 GB/s · 600 GB/s
local memory register 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).

arithmetic intensity (FLOPs / byte) throughput (FLOPs/s) ~13 FLOP/byte (ridge) memory-bound slope = peak BW compute-bound peak FLOPs ceiling elementwise (add, relu) — 0.25 FLOP/byte attention softmax (per-row) — ~3 FLOP/byte large matmul (fp16 + Tensor Core) — > ridge conv (small batch, fp32) — ~6 FLOP/byte peak compute 0
왼쪽 사선의 기울기는 peak HBM bandwidth. 오른쪽 평평한 선은 peak FP32 throughput (또는 Tensor Core 면 더 위로). 내 커널이 어디 점인지에 따라 “더 빠르게 만들려면 어디를 풀어야 하는가” 가 정해진다.

roofline 위에서 자주 보이는 패턴들.

“더 빨리 가게 하려면 — 천장이 어딘지 먼저 보라. 잘못된 천장을 깎으려고 하면 노력이 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 x relu W tmp R tmp ×2 W out
fused
R x relu ×2 W out
unfused 면 HBM read 2회 + write 2회. fused 면 HBM read 1회 + write 1회. elementwise op 라면 거의 50% bandwidth 절약. arithmetic intensity 는 그대로지만 절대 시간은 절반에 가까워진다.

fusion 의 자동화는 두 자리에서 일어난다.

강의에서 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%.

이 일화의 메시지가 두 줄로 압축된다.

  1. indexing 의 dtype 도 성능 결정 요소. int 면 충분한 자리에 long 을 쓰지 마라. CUDA 의 blockIdx.x 등은 unsigned int.
  2. “느려진 커널” 의 원인이 코드의 한 글자에 있을 수 있다 — 그래서 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) 의 두 천장. 내 커널이 어느 점인지 보고 무엇을 풀지 결정.
memory hierarchy
register(1 cycle) → shared(30) → L2(200) → HBM(400+). 200 배 차이.
occupancy
register/thread, shared/block, max threads 셋 중 가장 빡빡한 자리가 결정. memory-bound 면 중요, compute-bound 면 80→60% 차이 작음.
block 의 임의 배정
block 사이 통신은 kernel boundary 만. atomic 은 update 만 보장.
launch overhead
빈 커널 ~10 μs. 작은 커널이 많으면 fusion 또는 CUDA Graphs.
register spill
너무 많이 쓰면 “local memory” = HBM 으로 보내짐. nvcc -Xptxas=-v 로 dump.
int 의 dtype
indexing 은 int 로. long 은 register 두 배 → occupancy 떨어진다.
fusion 의 동기
elementwise op chain 은 거의 항상 BW-bound. fuse 하면 HBM 왕복이 줄어 시간 절감.
Code cuda-mode-session-4.ipynb — Jeremy 의 L003 notebook 을 base 로 GELU + benchmarking 추가
참고서 PMPP Ch.4 (Memory architecture) · Ch.5 (Performance considerations) · NVIDIA Programming Guide · roofline model 원논문 (Williams 2009)

손에 새기기 — 실습 시퀀스

  1. 자기 GPU 의 roofline 그리기 — peak FLOPs (FP32, FP16, Tensor Core), peak HBM bandwidth 를 spec 에서 가져와 ridge point 계산. matplotlib 으로 직접.
  2. 빈 커널 시간 측정__global__ void empty(){} 을 launch 하고 cudaEvent 로 시간 측정. 자기 환경의 fixed cost 가 5 μs 인지 20 μs 인지 확정.
  3. elementwise vs matmul 의 roofline 위치x + ymatmul(M=1024, N=1024, K=1024) 의 시간을 측정해 arithmetic intensity 와 함께 roofline 위에 점 찍기.
  4. register sweep — 같은 커널을 일부러 변수 많이 만들어서 (nvcc -Xptxas=-v 로 register count 확인) occupancy 가 떨어지는 지점을 찾는다.
  5. int32 vs int64 indexing 비교 — Thomas 의 batchnorm 일화 재현. 같은 reduce 또는 stencil 커널을 두 dtype 으로 짜고 시간 비교.
  6. fusion 효과 측정relu(x) * 2 를 PyTorch eager 와 torch.compile 로 각각 돌려 시간 비교. TORCH_LOGS=output_code 로 fused 커널을 직접 본다.
  7. NCU 로 occupancy 보기ncu --set full python script.py 의 output 에서 “Achieved Occupancy” 와 “Theoretical Occupancy” 의 차이를 한 번 본다.
  8. 한 페이지 plan — 자기가 다루는 모델의 한 layer 를 골라 “이 layer 가 roofline 위에서 어디쯤 있는지, 어떤 천장을 깎으면 가장 효과적인지” 를 한 페이지로 정리.
§ 11다른 강의로 이어지는 길· connections

이 정량 모델이 다른 강의에서 어떻게 사용되는지

L004 의 단어들 (occupancy, scoreboard stall, arithmetic intensity, fusion) 이 거의 모든 후속 강의의 기본 어휘다.

§ 12열린 질문· open questions

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

강의의 framing 안에서 의도적으로 비워둔 자리들과, 자기 GPU 위에서 측정해야 손에 박히는 사실들.

검증 메모

이 노트의 모든 latency/bandwidth 숫자(register 1 cycle, shared 30 cycle, HBM 400 cycle 등) 는 Ampere 기준의 대략적 값이다. 정확한 측정은 micro-benchmark 가 필요. Demystifying Latency and Bandwidth (Mei et al., 2017) 같은 논문이 PTX 단위 측정을 한다.

← Lecture 003 Jeremy Howard — Python 위에서 CUDA 까지의 학습 사다리 Lecture 005 → Jeremy Howard — shared memory tiling 으로 matmul 가속