gpumode · 강의 아카이브
《GPU Mode》 L056 2024 High priority transcript · failed

Kernel Benchmarking Tales

Georgii Evtushenko (NVIDIA · CUB / Thrust 메인테이너) 의 GPU kernel 벤치마크 함정 모음. 워밍업 빠짐, cache 잔재, 컴파일러의 dead-code elimination, clock 의 jitter, 다른 process 의 메모리 압박 — 같은 코드가 5배씩 다르게 측정되는 자리들과, 그 자리를 잡는 방법론적 노트. 본 페이지는 transcript 가 실패해 NVIDIA cuda-samples, NVBench library, 그리고 Sebastian Aaltonen / Ryg / Andreas Köpf 의 공개된 benchmarking 가이드로 재구성됐다.

benchmark methodology warmup cache state noise / jitter CUDA Events NVBench dead-code elimination publication bar
G
Speaker
Georgii Evtushenko
NVIDIA · CUB / Thrust 메인테이너 · NVBench 저자
강의 번호
L056
스피커
Georgii Evtushenko
Transcript
failed · 본 노트는 재구성
학습 우선순위
High · 모든 측정의 토대
§ 01강의가 풀려는 문제· why this lecture exists

“같은 코드, 같은 GPU, 5배 다른 측정값” 이 일상적으로 일어나는 자리

benchmark 는 의외로 어렵다. 코드를 짜고 시간만 재면 끝날 것 같지만 — 실제로는 같은 코드가 셋팅에 따라 30%, 50%, 200% 다르게 나온다. 그래서 “이 새 커널이 빠르다” 의 주장이 거의 모두 — 측정의 함정에 빠진 채로 발표된다.

강의의 두 출발 질문.

  1. 왜 같은 코드가 다르게 측정되는가 — warmup, cache, clock, jitter, 컴파일러 — 어느 것이 결정적 변수인가?
  2. 어떻게 정직한 측정을 짤 것인가 — methodology 의 표준이 무엇이고, NVBench 같은 도구가 자동으로 잡아주는 것이 무엇인가?

본 노트는 transcript 실패로 — Georgii 의 공개 talk (CppCon, GTC), NVBench 의 README 와 benchmarking philosophy 문서, 그리고 보다 일반적인 GPU benchmarking 의 community 지식 (Sebastian Aaltonen 의 blog, Andreas Köpf 의 PMPP 강의) 으로 재구성.

강의의 frame

“정직한 측정” 이라는 표현이 강의의 톤을 잡는다. 새 커널의 “speedup 2.5×” 가 정말 2.5× 인지, 아니면 baseline 의 측정이 잘못된 건지 — 그걸 분리하는 것이 benchmark 의 진짜 일.

“잘못된 측정으로 빠른 커널을 발표하는 것은 — 사용자에게 거짓을 파는 것이다. 우리가 깐 도구가 정말 빠른지 우리가 검증해야 한다.” 학습 노트 · 재구성
§ 02흔한 잘못된 측정· tales of woe

실전에서 자주 보는 함정 모음

#1 time.time() / clock_gettime호스트 시간으로 GPU 코드 측정 CUDA 는 비동기. kernel<<<...>>> 호출 직후 time.time() 은 launch overhead 만 잡힘. 실제 커널은 그 후 GPU 에서 도는 중. 첫번째 강의에서도 같은 함정 — L001.
#2 first call 만 측정워밍업 안 함 첫 호출에는 — CUDA context init, JIT 컴파일, kernel cache 미스가 모두 섞임. 두번째부터의 호출과 시간이 10~100배 차이. 실제 production 시간은 보통 “warm steady state”.
#3 cache 잔재같은 입력 반복 같은 input tensor 를 100번 반복 측정하면 — 모든 input 이 L2 cache 에 살아있어서 HBM 안 읽음. 실전 학습/추론에서는 이런 luck 못 받음. 결과가 memory-bound 인데 compute-bound 처럼 측정.
#4 dead-code elimination출력을 안 쓰면 컴파일러가 통째로 제거 kernel(out, in) 의 결과를 어디에도 안 쓰면 — 컴파일러가 “이 호출은 효과 없음” 으로 판단해서 커널을 통째로 제거할 수 있음. 시간 0 으로 측정. 이게 일어나는지 검증 필요.
#5 clock의 jitterGPU clock boost 가 비결정적 GPU 의 clock 이 thermal / power 에 따라 자동으로 boost up/down. 같은 코드가 시간에 따라 10% 차이. nvidia-smi 로 lock 하지 않으면 측정 결과의 표준편차가 큼.
#6 다른 process 의 영향shared GPU cloud / shared cluster 에서 — 다른 사용자의 워크로드가 같은 GPU 의 메모리/PCIe/L2 를 공유. 측정값이 30% 까지 흔들림.
#7 launch overhead 와 kernel 시간 혼동작은 커널 kernel 자체가 1μs 인데 launch overhead 가 5μs. 측정값 6μs 가 “kernel 시간” 으로 해석되는 함정. 작은 커널의 비교는 항상 함정.
#8 async copy 의 시간 미포함memcpy 안 보임 stream 위 async memcpy 가 kernel 과 overlap 되는데 — kernel 만 측정하면 memcpy 시간이 빠짐. 실제 throughput 이 낮음.
함정의 패턴

위 8개 항목의 공통점 — “코드가 도는 환경의 어떤 변수를 안 통제했다”. cache 상태, clock 상태, compiler 의 결정, 다른 process — 모두 “외부 상태” 가 측정에 끼어드는 자리. 정직한 benchmark 는 이 외부 상태를 명시적으로 통제하거나 무력화한다.

§ 03워밍업의 중요성· cold vs warm

첫 호출은 항상 버린다 — 그리고 “충분히” 워밍업한다

cold call 은 — context init, kernel JIT, cache 첫 fill 이 섞여 매우 느림. warm call 만이 production 시간을 반영한다. 그래서 처음 N 회는 측정 전 “워밍업”으로 버린다.

FIG · cold → warm → hot 의 시간 변화schematic
call 1 (cold)
context init + JIT + first cache fill
~250 ms
call 2
first warm
~12 ms
call 3
warming
~11 ms
call 5+
hot steady
~10 ms
measurement
measured
~10 ms
cold call 이 250 ms — warm 의 25배. 이걸 평균에 포함시키면 측정값 완전히 깨짐. 경험칙 — 5~10 회 워밍업, 그 다음 100~1000 회 측정의 평균.

워밍업의 실전 패턴.

  1. 실제 커널을 N 번 실행 (보통 N=5~10).
  2. cudaDeviceSynchronize() 로 모두 끝났는지 확인.
  3. 측정 시작 — CUDA Event 기반.
  4. 측정도 N 회 (보통 100~1000) 반복하고 통계 처리.

그러나 — workload 가 cold start 가 중요한 경우 (e.g., LLM serving 의 첫 query latency) 는 warm 만 측정하면 안 됨. cold/warm 두 시나리오를 분리해서 측정.

# 표준 워밍업 패턴 (PyTorch)
import torch

# 1. warmup
for _ in range(10):
    out = kernel(in_data)
torch.cuda.synchronize()

# 2. measure
start = torch.cuda.Event(enable_timing=True)
end   = torch.cuda.Event(enable_timing=True)

start.record()
for _ in range(1000):
    out = kernel(in_data)
end.record()
torch.cuda.synchronize()

per_call_ms = start.elapsed_time(end) / 1000
cold start 측정의 별도 패턴

“처음 호출의 latency” 가 중요한 경우 — LLM 의 initial query, image inference 의 first request — cold call 만 따로 N 번 측정. 매번 fresh process 시작. Python script 의 시작 → 끝 시간 측정. 여러 번 반복해야 의미 있는 통계.

§ 04cache 상태가 결과를 뒤집는 자리· L1/L2/HBM 의 잔재

같은 함수 100번 호출 = 모두 cache hit

benchmark 의 가장 미묘한 함정. 같은 입력 데이터를 반복 측정하면 — 두번째 호출부터 데이터가 L2 cache 에 있음. HBM 을 안 읽음. memory-bound 워크로드가 compute-bound 처럼 측정. 실전에서는 매 호출이 fresh data 라 — measurement 와 production 의 throughput 이 다름.

두 가지 회피 패턴

(1) cache flush between calls — 매 call 사이에 cache 를 비움. NVBench 가 자동으로. 또는 충분히 큰 buffer 를 한 번 읽어 L2 evict. (2) different input each call — N 개의 다른 input 을 round-robin. cache 가 자연스럽게 evict.

cache 상태가 결과에 끼치는 영향을 정량화하면 —

  • compute-bound 워크로드 (e.g., 큰 matmul) — cache 영향 거의 없음. 측정값 robust.
  • memory-bound 워크로드 (e.g., elementwise, reduction, attention) — cache 영향 크고, 같은 입력 반복하면 30~100% 까지 빨라 보임.
  • L2 cache 사이즈 — A100 40MB, H100 50MB. 입력이 그 안에 들어가면 “모든 측정 후가 cache hit”. 큰 입력은 자동으로 evict.
# NVBench 의 cache flush 패턴 (의사코드)
state.exec(
    [&](nvbench::launch& launch) {
        // flush L2 by reading large dummy buffer
        flush_l2(launch.get_stream());
        kernel<<<..., launch.get_stream()>>>(...);
    });
// NVBench 가 자동으로 매 call 마다 flush

또 다른 미묘한 자리 — SM (Streaming Multiprocessor) 의 internal state. 같은 kernel 을 반복 호출하면 — instruction cache 도 hit. 이건 production 에서도 hit 하는 게 정상이라 보존. 그러나 다른 kernel 들이 섞여 도는 시나리오에서는 instruction cache miss 가 일어남. micro-benchmark 가 over-optimistic.

“cache 가 진실을 가린다 — 실전 시나리오의 cache 상태와 측정 시나리오의 cache 상태를 일치시키는 게 benchmark 의 본질.” 학습 노트 · 재구성
§ 05측정 도구별 차이· Event · nsys · NCU · NVBench

도구마다 “재는 것” 이 미세하게 다르다

도구측정하는 것오버헤드
CUDA Events 두 event 사이의 GPU 시간. start.record() / end.record() + synchronize. 가장 가벼움. ~μs
cudaDeviceSynchronize + clock host 의 wall clock 으로 측정. sync 가 비용 — block 까지 host 가 wait. ~ms
nsys (Nsight Systems) system-level timeline. CPU + GPU + memcpy + NCCL. 모든 stream 의 timeline. ~10%
ncu (Nsight Compute) single kernel 의 metric — occupancy, throughput, stall. 1 kernel 호출에 수십 ms 추가. ~100×
NVBench benchmark harness. 자동 워밍업, cache flush, 통계 처리, GPU clock lock 시도. 권장. ~ms
PyTorch profiler Chrome trace 출력. autograd 와 ATen op 단위. ~5%
torch.utils.benchmark Python-side wrapper. 자동 통계 처리. inline 측정 친화. ~ms
도구 선택 가이드

(1) 한 줄 측정 → torch.utils.benchmark 또는 cuda Event. (2) 한 kernel 의 deep-dive → ncu. (3) 전체 학습 step → nsys. (4) publication 수준 비교 → NVBench. 도구를 섞어 쓰는 게 표준 — 한 도구 결과만으로 결론짓지 않는다.

각 도구의 미묘한 점들.

  • CUDA Events — 가장 가볍지만 — kernel 외 시간 (host 처리, memcpy) 안 잡힘. 작은 kernel 의 “real wall-clock” 과 다를 수 있음.
  • nsys — 시각적 timeline 강력. 그러나 file 사이즈가 큼 (1초당 100MB). long run 에는 sampling.
  • ncu — kernel 별로 측정 비용 큼. 한 번에 하나의 metric 만 측정 가능 — 같은 kernel 을 N 번 다른 metric 으로 측정.
  • NVBench — Georgii 의 작품. C++ benchmark 작성 → 자동화. publication 수준 결과의 표준.
profiler 자체의 영향

profiler 가 켜져 있으면 — 측정 대상의 시간이 약간 변함. 일부 workload 에서는 ~5% 변화. 정확한 throughput 이 필요하면 — profiler off 상태에서 같은 코드 한번 더 측정해 sanity check.

§ 06noise 와 신뢰구간· 통계 처리

한 번 잰 숫자는 거짓말이다 — N 번의 분포로 본다

한 번 측정한 시간 — 99% 정확하지 않다. clock jitter, OS scheduler, thermal, neighbour process — 모두 작은 noise 를 더함. 그래서 N 회 측정의 분포 로 봐야 한다. 평균만 보지 말고 — min, p50 (median), p99, std.

표준 통계 처리

(1) N=100~1000 회 측정. (2) outlier 제거 (top/bottom 5%). (3) median 또는 trimmed mean 보고. (4) std/min/max 도 같이. (5) 두 baseline 비교 시 — 신뢰구간 겹치는지 확인. 안 겹치면 “유의미한 차이”.

measurement 1
10.2 ms
measurement 2
10.3 ms
measurement 3
12.1 ms · outlier?
measurement 4
10.1 ms
median (N=100)
10.2 ± 0.3

두 코드를 비교할 때 — “A 가 10.2ms, B 가 9.8ms 라서 B 가 4% 빠르다” 같은 결론은 위험. 만약 std 가 ±0.4ms 라면 두 분포가 겹친다 → 차이 유의미하지 않을 수 있음. 적어도 신뢰구간 겹침을 확인해야 함.

“숫자 하나로 결론 내리지 말 것 — 분포가 결론이고 평균이 그 분포의 한 점일 뿐이다.” 학습 노트 · 재구성
GPU clock lock

noise 의 가장 큰 단일 변수 — clock boost. nvidia-smi -lgc base_clock 로 GPU clock 을 base 에 lock 하면 측정값의 std 가 절반 이하로 떨어짐. publication 수준 측정의 표준. (단, 이 클럭에서의 측정값은 production 의 boost 클럭 측정값과 다르다는 점 명시 필요.)

§ 07micro vs macro benchmark· 두 다른 게임

한 kernel 의 측정과 전체 모델의 측정은 다른 패턴

micro-benchmark — 한 kernel 의 측정. cache, launch overhead, isolated env. 깊은 분석에 좋음. macro-benchmark — 전체 학습/추론의 wall-clock. 진짜 production 시간. 두 측정의 결과가 일치하지 않을 수 있음.

μ micro-benchmark한 kernel · 격리 환경 한 kernel 의 forward 를 1000 번 호출. cache 통제, clock lock, NVBench. 결과: “이 kernel 은 100μs 다”. 다른 kernel 과의 interaction 안 봄.
M macro-benchmark전체 모델 · 실 환경 학습 step 또는 추론 query 의 전체. 실제 cache 상태, 실제 launch sequence, 실제 memcpy. 결과: “이 모델 step 은 50ms 다”. 어느 kernel 이 dominant 인지 분리 어려움.
두 결과의 불일치흔한 사례 한 kernel 을 micro 에서 30% 빨라지게 했는데 macro 에서 model 시간이 안 빨라짐. 이유 — (1) 그 kernel 이 critical path 가 아니었음, (2) 다른 kernel 과 stream overlap 되어 있었음, (3) cache 상태가 micro 와 다름.
병행 사용

healthy 워크플로 — micro 에서 kernel 을 빠르게 짜고, macro 에서 그 kernel 이 모델 시간에 어떻게 반영되는지 확인. 두 측정이 모두 일치하면 진짜 빠름. 한쪽만 빨라지면 — critical path 또는 cache 의 영향 의심.

§ 08자동화 패턴· harness 디자인

NVBench 가 자동으로 잡아주는 것들

benchmark harness 의 일을 손으로 짜는 것은 — 매번 같은 함정을 만든다. NVBench 같은 framework 가 그 함정을 자동 처리. Georgii 의 큰 contribution.

자동 워밍업
N 회 워밍업 후 측정 시작. N 은 timing variance 가 stable 해질 때까지 자동 결정.
자동 측정 횟수
신뢰구간이 desired precision 에 도달할 때까지 추가 측정. 짧은 kernel 은 더 많이, 긴 kernel 은 적게.
cache flush
매 call 사이에 L2 cache flush. memory-bound workload 의 honest 측정.
GPU clock lock
자동으로 base clock 에 lock 시도. permission 있어야 함.
timer 정확도
CUDA Event 사용. ~0.5μs 정확도. 짧은 kernel 의 측정에 적합.
여러 input 자동
N 개의 다른 input 을 round-robin. cache 자연 evict.
통계 보고
median, mean, std, min, max. CSV / JSON 출력. publication 수준.
parametric sweep
여러 input size, BLOCK_SIZE 등을 자동 sweep. 결과 grid 로 출력.
PyTorch 사용자의 대안

NVBench 는 C++ benchmark 위주. PyTorch 환경에서는 — torch.utils.benchmark.Timer, triton.testing.do_bench. 비슷한 자동화. 두 도구 모두 — 자동 워밍업, 자동 N 결정, 통계 출력.

“정직한 measurement 의 가장 큰 trick — 기존의 검증된 harness 를 쓰는 것. 손으로 짜면 함정에 빠진다.” 학습 노트 · 재구성
§ 09publication 기준· paper / blog 의 bar

“이 kernel 이 빠르다” 를 발표하기 전 체크리스트

새 kernel 을 paper / blog / repo 에 발표할 때 — measurement 가 충분히 정직한가의 체크리스트. 강의의 가장 실용적 결론.

  1. warmup 5+ 회 — 첫 cold call 은 측정에 안 들어감.
  2. 측정 100+ 회 — 통계 처리. median 또는 trimmed mean.
  3. std / 신뢰구간 보고 — 평균만 발표하지 말 것.
  4. baseline 동일 환경 측정 — 자기 코드만 잘 측정하고 비교 baseline 은 default 셋팅이면 안 됨. 같은 GPU, 같은 driver, 같은 측정 도구.
  5. cache flush 또는 fresh input — memory-bound kernel 은 cache 잔재가 결정적.
  6. GPU clock 명시 — boost 인지 base 인지. base lock 권장.
  7. compile flags 명시 — nvcc 의 optimization level. 다르게 컴파일하면 다른 결과.
  8. dead-code 검증 — kernel 출력을 어디든 사용. 컴파일러가 통째로 제거 안 했는지.
  9. 여러 input shape — 한 shape 에서만 빠르고 다른 shape 에서는 안 그럴 수 있음. 표 또는 그래프로.
  10. 다른 GPU 에서 재측정 — A100 에서 빠른 게 H100 에서 느릴 수 있음.
  11. reproducibility — 코드 + 측정 스크립트 + 환경 (driver, CUDA 버전) 명시. 다른 사람이 재현 가능.
정직한 발표의 표준

“우리 kernel 이 baseline 보다 1.8× 빠르다 (median, N=1000, std=±0.05ms, A100 base clock, fresh input)” — 이 정도 모양이 publication 수준. 단순히 “1.8× faster” 만 발표하는 게 아니라 — 측정 셋업 명시.

“speedup 의 숫자는 항상 측정 셋업과 함께 기억된다 — 셋업이 빠진 숫자는 의미 없는 숫자다.” 학습 노트 · 재구성
§ 10기억할 메모와 자료· key takeaways

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

8가지 함정
host time, no warmup, cache 잔재, dead-code, clock jitter, neighbour process, launch overhead, async memcpy.
표준 워밍업
5~10 회 워밍업 + sync, 그 다음 100~1000 회 측정. cold start 가 중요한 워크로드는 별도 측정.
cache flush
memory-bound 워크로드는 매 call 사이 cache flush 또는 fresh input round-robin.
측정 도구
Event (가벼움), nsys (timeline), ncu (deep), NVBench (publication), torch.utils.benchmark (Python).
통계 처리
median + std. outlier 5% 제거. 신뢰구간 비교. clock lock 으로 std 절반 감소.
micro vs macro
micro 가 빨라져도 macro 가 안 빨라지는 경우 — critical path 아님 또는 stream overlap 문제.
dead-code elim 회피
kernel 결과를 어디든 사용. volatile 변수에 쓰기, side effect 만들기.
publication 체크리스트
warmup, N, std, baseline 동일 환경, cache, clock, compile flags, dead-code, input shapes, GPU, repro.
§ 11다른 강의로 이어지는 길· connections

같은 자리를 다른 각도에서 다루는 강의들

§ 12열린 질문· open questions

원본 자막 실패로 비워둔 자리들

검증 메모

본 노트의 모든 함정 목록과 측정 도구 비교는 — Georgii 의 NVBench 와 NVIDIA cuda-samples 의 best practice 문서, Andreas Köpf / Sebastian Aaltonen 의 공개 가이드를 종합한 재구성. 강의 자체에서 구체적 사례 (특정 paper, 특정 kernel 의 깨진 측정) 를 들었다면 — 그 디테일은 본 노트에 빠져 있음. 영상을 직접 본 사람이 노트 보강 권장.

← Lecture 055 Modular's unified accelerator language Lecture 057 → CuTe — Cris Cecka