gpumode · 강의 아카이브
《GPU Mode》 L010 2024 · MAR · 09 High priority transcript · available

Build a Prod Ready CUDA library

9년 동안 University of Barcelona 에서 OpenCL/CUDA 를 가르치다가 산업으로 넘어간 Oscar Amoros Huguet 가 — “CUDA 닌자가 아닌 동료 엔지니어가 GPU 성능을 누리게 하려면 코드를 어떻게 짜야 하는가” 를 두 가지 실제 사례로 깐다. 이 강의는 단일 커널 최적화가 아니라 호스트 측 아키텍처, vertical/horizontal fusion, graph 기반 파이프라이닝, delay buffer 같은 시스템 패턴이 중심이다.

CUDA library design vertical fusion horizontal fusion CUDA streams CUDA graphs delay buffer producer-consumer latency hiding FastKAT (FK)
O
Speaker
Oscar Amoros Huguet
Mediapro · CUDA 9년 강의 + 산업 production 코드
강의 번호
L010
스피커
Oscar Amoros Huguet
학습 우선순위
High · 정독
원본 슬라이드
Drive · 외부 링크
§ 01강의가 풀려는 문제· why this lecture exists

“닌자 한 명이 모두를 가르치는 길”은 산업에서 안 통한다

Oscar 의 출발점은 단순하다 — 회사에서 그는 CUDA 를 아는 유일한 엔지니어였고, 동료 비전·미디어 엔지니어들에게 CUDA 를 가르치려고 9년 가까운 노력을 쏟았는데 성과는 미미했다. 그래서 방향을 뒤집었다 — “이 사람들이 CUDA 를 모르더라도, 내가 만든 라이브러리를 부르기만 하면 좋은 성능을 얻게 한다.”

강의의 두 사례는 둘 다 같은 메시지를 다른 도메인에서 보여준다.

강의의 인지적 frame

대부분의 GPU Mode 강의가 “커널을 어떻게 빠르게 짜는가” 를 다룬다면 — 이 강의는 그 한 단계 위, “여러 커널을 어떻게 묶어 production 라이브러리로 출하하는가” 를 다룬다. 단일 SM 위 warp scheduling 보다 호스트 측 그래프, iteration boundary, API 표면적 이 더 자주 등장한다.

“커널 하나 잘 짜는 건 시간 문제다. 다른 사람이 그 커널을 안전하게 부르도록 만드는 게 진짜 일이다.”Oscar Amoros · 강의 paraphrase

그래서 이 강의의 끝에 손에 잡혀야 하는 건 단순한 “fast kernel” 가 아니라 — 4가지 라이브러리 패턴이다: vertical fusion, horizontal fusion, producer-consumer streams, delay-buffered graph iteration.

§ 02호스트 측이 첫 병목이다· application before kernel

“커널만 보지 마라 — 응용프로그램 전체에서 GPU 가 무엇을 기다리고 있는가”

Oscar 가 강의 초반에 못 박는 진단 — 회사에서 새 GPU 코드 컨설팅 문의가 들어왔을 때 가장 먼저 들여다보는 건 커널이 아니라 호스트 코드다. 왜냐하면 대부분의 application 에서 GPU 는 잘 도는데 — 호스트가 GPU 를 제대로 먹이지 못하기 때문에 비어 있다.

호스트 측에서 흔히 보이는 패턴들 — Oscar 의 진단 체크리스트.

  1. cudaMemcpy 동기화 (default stream) — 별생각 없이 부른 memcpy 가 default stream 위에서 동기화되어 GPU 가 매 frame 마다 비는 구간을 만든다.
  2. OpenCV / 외부 라이브러리의 host wrapping — 도메인 라이브러리(영상처리)가 내부에서 cuda kernel 을 부르면서도 외부에서는 host 함수처럼 보여서, 호출자가 stream 을 의식하지 않게 만든다.
  3. op 마다 새 tensor 할당 — cudaMalloc 은 비싸다. iteration 안에서 반복되면 파이프라인이 그대로 멈춘다.
  4. iteration 경계의 sync — 한 frame 끝마다 host 가 명시적으로 sync 하는 코드. 그 sync 가 사실은 필요 없는 경우가 대부분.
접근 순서 — Oscar 의 추천

새 application 을 받으면 먼저 nsys 로 system-level timeline 을 본다. 커널이 비는 갭이 있는지, memcpy 가 길게 박혀 있는지, host API 호출이 GPU 작업과 어떤 패턴으로 겹치는지. 그 갭을 어떻게 메울지 결정한 뒤에야 NCU 로 단일 커널을 들여다본다.

실전 신호

“GPU utilization” 만 봐서는 안 된다. nvidia-smi 의 SM utilization 이 80% 라도, 그 80% 가 20개의 작은 커널들이 매번 launch overhead 로 채워서 만들어진 숫자라면 진짜 일은 5%만 한 것일 수 있다. system trace 로만 잡힌다.

Oscar 는 이 진단을 바탕으로 — 라이브러리를 설계할 때 “호스트가 단순한 호출 한 줄로 GPU 위에 큰 일감을 던지게 하는 것” 이 가장 중요한 디자인 결정이라고 본다. 그래서 다음 섹션의 추상화 층 분리가 의미를 갖는다.

§ 03추상화의 3개 층· user · author · ninja

같은 코드를 세 종류의 사람이 본다 — 각자에게 무엇을 보여줄 것인가

Oscar 의 핵심 디자인 원칙 — 라이브러리는 같은 코드를 세 가지 시각으로 동시에 만족시켜야 한다. 각 층은 서로 다른 추상화 수준으로 같은 기능을 노출한다.

L0 · USER LEVEL
CUDA 를 모른다. fk::resize_normalize_crop(in, out, params) 같은 한 줄짜리 호출만 본다. stream 도 자기가 관리할 필요 없도록 default 가 합리적으로 잡혀 있어야 한다. 이 층이 가장 큰 사용자층이다.
L1 · AUTHOR LEVEL
파이프라인을 조립하는 엔지니어. fusion graph 를 정의하고 각 단계의 op 를 연결한다. 템플릿 매개변수로 stage 를 합성하지만 커널 안의 thread/warp 코드는 안 짠다. CUDA 의 grid/block 모델을 이해하긴 한다.
L2 · NINJA LEVEL
실제 GPU 커널을 짜는 사람. shared memory layout, register usage, MMA instruction 같은 디테일까지 들어간다. 라이브러리 안에서 가장 안쪽 — 가장 적은 수의 사람이 들어가는 영역.
디자인 implication

이 층 분리가 실제 코드에 어떻게 반영되는지 — Oscar 의 FK 라이브러리는 op 를 lambda 로 표현하고 fusion graph 를 템플릿 합성으로 만든다. L1 author 는 fk::compose(resize, normalize, crop) 같은 식으로 새 파이프라인을 만들 수 있고, L2 ninja 가 lambda 본문을 더 잘 만들면 L0 user 의 코드는 그대로 두고도 빨라진다.

그리고 이 디자인의 실용적인 장점 — 새로운 동료가 회사에 들어왔을 때, 그 사람이 어떤 layer 에서 일을 시작할지를 명시적으로 정할 수 있다. 모두를 닌자로 만들 필요가 없다.

§ 04vertical fusion· kernel 간 HBM 왕복 제거

같은 픽셀을 두 번 읽지 마라 — chain 을 한 커널로 합친다

Memory-bound op 들의 가장 흔한 낭비 — resize 가 결과를 HBM 에 쓰고, 다음 op 인 normalize 가 그걸 다시 읽고, 또 쓰고, 다음 crop 이 또 읽는다. 같은 픽셀이 HBM 사이를 여러 번 왕복한다. vertical fusion 은 이 chain 을 한 커널로 합쳐서 — 픽셀이 한 번 SRAM 에 올라오면 그 위에서 모든 op 가 도는 구조다.

FIG · 같은 픽셀이 HBM 을 왕복하는 패턴 vs vertical fusionresize → normalize → crop
read · resize 입력
kernel · resize
write · resize 결과
read · normalize 입력
kernel · normalize
write · normalize 결과
read · crop 입력
kernel · crop
write · 최종 결과
read · 입력 한 번
fused kernel
(resize ∘ normalize ∘ crop)
write · 최종 결과
왼쪽은 HBM read 4번 + write 3번. 오른쪽은 read 1번 + write 1번. memory-bound op 라면 후자가 약 4× 빠르다 — compute 를 깎지 않아도. 픽셀 하나가 register 에 올라와 있는 동안 그 위에서 모든 변환을 끝낸다.

vertical fusion 을 라이브러리에서 자동화하려면 — op 를 read fragment, compute fragment, write fragment 로 분해하고, 합성할 때 read/write 는 양 끝에서 한 번만, compute 는 chain 으로 이어붙이는 구조가 필요하다. Oscar 의 FK 가 정확히 이 모델.

이게 compiler-driven fusion(torch.compile 의 Inductor 가 하는 것) 과 다른 점은 — compiler 는 사용자 코드의 의미를 파싱해야 하지만, 라이브러리는 op 의 모양을 미리 알기 때문에 항상 안전하게 합칠 수 있다는 것. 두 접근이 상호보완.

// FK 스타일 fusion (의사코드)
auto pipeline = fk::compose(
    fk::read(input),
    fk::resize(out_w, out_h),
    fk::normalize(mean, std),
    fk::crop(box),
    fk::write(output)
);

// 한 번의 launch 로 SRAM 위에서 모두 합성
pipeline.launch(stream);
“fusion 의 효과는 커널 수를 세서 검증한다 — N 개에서 1 개로 줄었으면 그게 fusion 이고, 그렇지 않으면 진짜 fusion 이 아니다.”강의 핵심 paraphrase
§ 05horizontal fusion· 같은 데이터에 여러 op

같은 입력을 여러 출력 branch 로 — 한 번 읽고 여러 번 쓴다

Vertical fusion 이 “한 픽셀이 여러 stage 를 통과한다” 면, horizontal fusion 은 “한 입력에서 여러 출력이 동시에 나간다”. 비전 파이프라인에서 흔하다 — 같은 frame 에서 작은 thumbnail, 큰 inference 입력, 시각화용 overlay 가 동시에 필요한 경우.

FIG · horizontal fusion — 같은 입력 → 여러 출력shared SRAM read
input frame SRAM tile (shared) thumbnail out inference input overlay frame
SRAM 으로 한 번 올라온 입력 tile 위에서 3개 branch 가 각자 다른 변환을 적용해 자기 출력에 쓴다. HBM read 1번, write 3번. 별도의 3개 커널이면 read 가 3번씩 일어나 6× 손해.
vertical 과의 조합

실제 라이브러리에서는 둘이 섞인다. resize → normalize 의 vertical chain 끝에서 동시에 thumbnail 과 inference 입력으로 분기하는 식. Oscar 의 FK 는 op 합성 시 tree 를 받을 수 있게 설계되어 있어 둘을 같이 표현한다.

이 패턴이 가장 많이 만나는 자리는 — multi-resolution / multi-output 모델 추론 파이프라인. 같은 이미지에서 detector 와 classifier 가 다른 해상도를 요구하는데, 각 모델이 자기 전처리를 따로 부르면 H2D copy 가 두 배가 된다.

§ 06producer-consumer 와 streams· latency hiding

두 일이 동시에 돌게 — stream 두 개로 깐 가장 단순한 파이프라인

Vertical/horizontal fusion 으로 단일 iteration 안의 HBM 왕복을 줄였다면, 다음 도구는 iteration 사이의 latency를 줄이는 것. CUDA streams 와 producer-consumer 패턴이 표준 도구다.

FIG · 단일 stream vs 두 stream — 같은 일감, 다른 timingiteration overlap
단일 stream H2D 1 kernel 1 D2H 1 H2D 2 kernel 2 D2H 2 두 stream (overlap) copy stream H2D 1 H2D 2 H2D 3 compute stream kernel 1 kernel 2 kernel 3
단일 stream 에서 H2D · kernel · D2H 가 직렬로 도는 동안 GPU 는 카피 하는 동안 비어 있다. 두 stream 으로 나누면 다음 입력의 H2D 가 현재 kernel 과 겹쳐 돈다. copy enginecompute engine 이 별도 하드웨어이므로 실제로 동시에 돈다.

이 패턴을 라이브러리에 넣을 때 주의할 함정 두 가지.

  1. Pinned memory 가 아니면 비동기 copy 가 안 된다cudaMemcpyAsync 는 host buffer 가 pinned (cudaMallocHost) 일 때만 진짜로 비동기다. 일반 malloc 으로 잡힌 host 메모리에서는 동기로 떨어진다.
  2. default stream 의 sync 의미 — 어디선가 cudaMemcpy(non-async) 한 번 부르면 모든 stream 이 그 시점에서 sync 된다. legacy default stream 모델. --default-stream per-thread 로 빌드하면 완화.
// 두 stream — pinned host buffer + async copy
cudaStream_t copy_s, compute_s;
cudaStreamCreate(&copy_s);
cudaStreamCreate(&compute_s);

// host pinned buffer 풀
float* h[N];
for(int i=0; i<N; ++i)
    cudaMallocHost(&h[i], bytes);

for(int i=0; i<num_iters; ++i){
    // 다음 입력을 copy stream 으로 미리
    cudaMemcpyAsync(d_in[i+1], h[i+1], bytes,
                    cudaMemcpyHostToDevice, copy_s);
    // 현재 입력으로 compute stream 위 kernel
    kernel<<<..., compute_s>>>(d_in[i], d_out[i]);
}
§ 07graph 와 delay buffer· iteration synchronization

iteration 끝마다 sync 하지 마라 — 6 step 늦어도 되는 데이터를 쓴다

Oscar 가 강의 후반부에 가장 길게 깐 패턴 — delay buffer. iteration 사이에 작업이 의존성을 가질 때, 매번 sync 하면 파이프라인이 멈춘다. 그런데 어떤 종류의 데이터는 몇 step 늦어도 결과가 같다 — 그 자리에 delay buffer 를 박아 sync 자체를 우회한다.

FIG · iteration graph 의 일반 형태kernel · copy · 또 다른 kernel
kernel A copy 1 copy 2 kernel B (memmgr) iteration n — 모든 step 끝에서 sync (X) iteration n+1 시작 시 swap pointer — 이전 6 iter 전 결과를 사용
graph 의 모든 노드가 끝날 때까지 host 가 기다리지 않는다. iteration n+1 의 kernel B 가 iteration n−6 의 결과를 입력으로 받는다 — 그 시점에는 이미 GPU 위에서 끝나 있다. host sync 가 사라진다.

delay buffer 패턴이 적용 가능한 조건.

  • 해당 데이터가 “최근값” 이면 충분하지, exact iteration 결과일 필요가 없을 때 — 예: telemetry, monitoring, low-rate visualization, smoothing filter 의 long-tail term.
  • iteration 빈도가 높아서 delay (예: 6 iter, 50ms 정도) 가 application 의 latency budget 안에 들어올 때.

이 조건이 맞으면 — 같은 그래프가 매 iteration 시작 시 pointer 만 swap 하고 sync 없이 그냥 도는 형태가 된다. Oscar 가 강의에서 비디오 파이프라인의 monitoring/feedback loop 에 이걸 적용한 예를 든다.

CUDA Graphs 와의 관계

이 패턴은 CUDA Graphs API(cudaGraph) 와 자연스럽게 어울린다. 한 iteration 의 모든 작업을 graph 로 capture 한 뒤 cudaGraphInstantiate 로 instance 를 만들고, cudaGraphLaunch 로 호출한다. host 측 launch overhead 가 N→1 로 떨어지면서 delay buffer 의 효과가 더 커진다.

검증 신호

delay buffer 가 잘 박혔는지는 nsys timeline 에서 “iteration 사이에 GPU 가 비는 갭이 있는가” 를 보면 된다. 갭이 사라졌다면 host sync 가 진짜로 빠졌다는 뜻.

“GPU 가 비는 시간을 0 으로 만드는 게 진짜 라이브러리 디자인의 목표다 — 단일 커널을 0.1ms 깎는 게 아니라.”Oscar Amoros · 강의 paraphrase
§ 08case A — 컴퓨터 비전 파이프라인· FastKAT (FK) 의 적용

OpenCV 의 GPU 모듈 위에 한 layer 얹기 — 같은 API, 다른 fusion 정책

강의의 첫 사례. Oscar 의 회사에서는 OpenCV 의 GPU 모듈을 쓴다 — 그런데 OpenCV 는 op 단위로 host API 를 노출하기 때문에 여러 op 를 묶어 부르면 자연스럽게 vertical fusion 기회를 놓친다. FK (FastKAT) 가 그 위에 얹히는 layer.

L0 · 호출 user coderesize → normalize → crop 을 한 줄로 호출 CUDA 모름
L1 · FK fusion graph composeop 들을 lambda 로 받아 하나의 kernel 로 합성 템플릿 합성
L2 · OpenCV GPU 기존 op 구현FK 의 fragment 로 잘게 쪼갠 형태 CUDA 직접
L3 · driver stream · launchcopy stream + compute stream 분리 producer-consumer
실제 측정의 형태 — 강의 paraphrase

같은 비전 파이프라인 (resize → normalize → crop → ToTensor) 을 — (1) OpenCV GPU op 4개로 직렬 호출, (2) FK 합성으로 한 커널로 fuse — 두 방식으로 측정. memory-bound 한 op 들이라서 fused 버전이 약 3-4× 빠르다. 같은 GPU, 같은 입력. 차이는 HBM 왕복 횟수에서 온다. (정확한 수치는 강의 슬라이드 직접 확인 필요 — 본 노트는 패턴만 재구성.)

이 사례에서 반복되는 디자인 결정 — “user 가 손을 한 번도 안 대게 한다”. FK 가 도입되기 전과 후의 user 코드 한 줄도 안 바뀐다. 라이브러리가 위에서 자동으로 fusion graph 를 만든다. 새 user 가 회사에 들어와도 OpenCV 만 알면 된다.

§ 09case B — Linux 외 환경의 함정· Windows · WDDM driver 모델

같은 코드가 Windows 에서 더 느리다 — driver 모델이 본격적인 변수다

강의에서 짧지만 인상적으로 다룬 부분 — Oscar 가 회사의 일부 product 를 Windows 환경에 배포하면서 만난 시스템 차이. 같은 GPU, 같은 코드인데 Windows 에서 더 느리고 더 비결정적이다.

주된 원인은 Windows 의 WDDM (Windows Display Driver Model). WDDM 은 GPU 자원을 OS 가 시간분할로 관리하기 때문에, application 이 stream 을 자기가 가진 것처럼 직접 제어하지 못한다. 결과:

  • kernel launch overhead 가 Linux 대비 더 크고 더 variance 가 크다.
  • multi-stream 로 깐 producer-consumer 가 OS 의 scheduling 에 의해 직렬화될 수 있다.
  • 큰 batched copy 가 자동으로 잘게 쪼개지면서 throughput 이 떨어진다.

NVIDIA 가 제공하는 TCC (Tesla Compute Cluster) driver 모드가 있긴 하지만 — display 출력이 없는 GPU 에서만 가능하고 GeForce 카드에선 못 켠다. 즉 production application 의 deployment target 이 Windows 인지 Linux 인지가 — 라이브러리 디자인 단계부터 영향을 준다.

실용적 결론

Oscar 의 권고 — “performance critical application 은 Linux 에서 돌려라”. Windows 에서 도는 게 deployment 요건이라면 — (1) launch 횟수를 더 공격적으로 줄여라 (vertical fusion 더 강하게), (2) CUDA Graphs 로 graph capture 후 한 번에 launch, (3) 측정을 Windows 에서 직접 한다.

학습 메모

이 자리는 강의에서 가장 짧게 다뤄지지만 — 실제 production deployment 에서는 가장 자주 만나는 함정 중 하나. NVIDIA 의 driver mode 문서와 CUDA 의 cudaDeviceGetAttribute(cudaDevAttrComputeMode) 가 첫 진입점.

§ 10기억할 메모와 코드· key takeaways · repo

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

이 강의의 가치는 단일 커널 최적화가 아니라 — 여러 커널을 묶어 production 라이브러리로 만드는 패턴 카탈로그다. 다음 라이브러리 디자인을 시작할 때 빠르게 복원해야 하는 핵심.

3 layer abstraction
user (CUDA 모름) · author (graph 합성) · ninja (커널 직접). 같은 라이브러리가 세 시각을 동시에 만족.
vertical fusion
memory-bound chain 을 한 커널로 합쳐 HBM 왕복을 N→1 로 줄인다. 픽셀 한 번 SRAM 에 올라오면 그 위에서 모든 변환 처리.
horizontal fusion
같은 입력에서 여러 출력 branch — read 한 번, write N 번. multi-resolution 추론 파이프라인의 표준 패턴.
two-stream pattern
copy stream + compute stream 분리, pinned host buffer 사용, default stream 의 implicit sync 회피.
delay buffer
iteration 사이의 sync 자체를 우회. N step 늦어도 되는 데이터에 한해 적용 — monitoring, smoothing, feedback loop.
CUDA Graphs
한 iteration 의 모든 launch 를 graph 로 capture → 한 번에 launch. host launch overhead N→1.
호스트 진단 우선
새 application 컨설팅은 nsys system-level timeline 부터. NCU 는 그 다음. 호스트 갭이 가장 큰 손해.
WDDM 함정
Windows GeForce 의 driver 모델이 multi-stream 의 효과를 깎는다. performance critical 은 Linux.
Code github.com/morousg · Oscar 의 FK 관련 repo
Repo gpu-mode/lectures 에 별도 lecture_010 폴더 없음 — 슬라이드는 외부 Drive

손에 새기기 — 실습 시퀀스

  1. vertical fusion baseline — torchvision 의 Resize → Normalize → CenterCrop 전처리 chain 을 (1) 단계별 따로 호출, (2) torch.compile 로 compose, (3) 직접 Triton 으로 fuse. nsys 로 커널 수와 H2D 트래픽 비교.
  2. two-stream copy/compute — pinned host buffer pool 을 만들고 cudaMemcpyAsync + 두 stream 으로 image preprocessing 파이프라인을 깐다. 단일 stream 대비 throughput 측정.
  3. CUDA Graphs capture — 같은 iteration 을 한 번 capture 해서 graph instance 로 재사용. launch overhead 가 N→1 로 줄어드는 시점을 nsys 로 확인.
  4. delay buffer toy — frame n 의 monitoring 통계가 frame n+5 의 control loop 에 들어가는 toy 그래프. host sync 없이 pointer swap 만으로 도는 형태로 구현.
  5. WDDM vs Linux 비교 — 같은 코드를 듀얼부트나 두 환경에서 돌려 launch overhead variance 측정. variance 가 큰 자리가 어디에 박히는지 확인.
  6. FK 스타일 fusion library — C++ template + lambda 로 작은 영상 변환 op 3개를 fuse 하는 toy 라이브러리 작성. user-level API 한 줄, internal 에서 한 커널로 launch.
§ 11다른 강의로 이어지는 길· connections

이 강의의 도구가 다음에 어디에 다시 등장하는지

L010 의 “라이브러리 디자인” 시각이 다른 강의의 단일 도구들과 어떻게 맞물리는지.

§ 12열린 질문· open questions

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

이 강의는 산업 사례 위주라서 — 본문 안의 구체적 수치와 코드 디테일이 슬라이드/repo 에 흩어져 있다. 다음 회독 때 채울 자리들.

검증 메모

이 노트의 거의 모든 수치/예시는 강의 패턴을 재구성한 형태이며, 정확한 측정값과 API 시그니처는 Oscar 의 슬라이드와 morousg 의 repo 를 직접 봐야 한다. 다음 회독에서 채워 넣기.

← Lecture 009 Reductions — 단일 커널 깊이파기에서 라이브러리 시각으로 Lecture 011 → Sparsity — Jesse Cai 가 깐 2:4 sparse pattern 과 dense 대비 1.6× 의 자리