제3부 딥러닝 라이브러리
3.2 cuDNN I — 프리미티브
3.2.1 이 장에서 배우는 것 / 선수 지식
cuDNN(CUDA Deep Neural Network library)이 제공하는 딥러닝 프리미티브와, 컨볼루션을 계산하는 여러 알고리즘(implicit GEMM·Winograd·FFT) 을 익힌다. cuBLAS(3.1)가 "행렬곱 전체"를 책임진다면, cuDNN은 "컨볼루션·풀링·정규화·활성·RNN·어텐션 같은 신경망 연산 패턴"을 책임진다. 알고리즘 선택(heuristics/find)과 그래프 융합은 3.3으로 잇는다.
배우는 것:
- cuDNN 프리미티브 군을 분류하고, 각각이 cuBLAS가 아니라 cuDNN에 있는 이유.
- 한 컨볼루션에 대해 implicit GEMM·Winograd·FFT의 수학적 트레이드오프(연산량·메모리·정밀도·입력형상 의존)를 비교한다.
선수 지식: 1.11~1.12(텐서코어 — 컨볼루션이 결국 GEMM으로 환원), 1.13~1.14(정밀도), 2.7(타일링), 3.1(cuBLAS·에필로그 융합). 환경·cuDNN 설치는 0.3·A.1. cuDNN은 메이저 버전마다 API가 크게 바뀌므로 코드 골격은 개략이며 실제 시그니처는 설치 버전 헤더로 확인. 어텐션 본격 최적화는 3.6.
3.2.2 cuBLAS가 있는데 왜 cuDNN인가
cuBLAS(3.1)는 BLAS — 일반 행렬곱 같은 선형대수 프리미티브를 준다. 신경망의 무게중심도 행렬곱이지만, GEMM으로 바로 떨어지지 않거나 환원하려면 별도 데이터 재배열·메모리 트래픽이 필요한 패턴이 많다:
- 컨볼루션: 슬라이딩 윈도. naive하게 GEMM으로 바꾸려면
im2col(입력을 큰 행렬로 펼침)이 메모리를 폭발시킨다. - 풀링·정규화·활성·softmax: 원소별·축별 reduction. GEMM이 아니다.
- RNN(LSTM·GRU): 시간 스텝 GEMM 시퀀스 + 게이트. 스텝마다 작은 GEMM이면 launch 오버헤드가 지배.
- 어텐션: QKᵀ → softmax → ×V(3.6).
cuDNN은 이 패턴들을 신경망에 특화해 최적화한 프리미티브로 제공한다. 차별점 둘: ① 컨볼루션을 im2col 없이(암묵적으로) 처리하는 여러 알고리즘을 형상에 맞춰 고른다. ② 여러 연산을 한 커널로 융합한다(conv+bias+act 등) — 중간 텐서의 HBM 왕복(1.9)을 없앤다(3.3).
견해: 실무에서 cuDNN을 직접 호출하는 일은 드물다(PyTorch·TF·TensorRT가 내부에서). 그래도 익히는 이유는 "첫 스텝이 느리다", "특정 입력 크기에서만 느리다", "버전 올렸더니 느려졌다" 같은 증상이 전부 이 메커니즘(자동튜닝·형상 의존·알고리즘 회귀)에서 나오기 때문이다.
프리미티브 군:
| 군 | 대표 연산 | 비고 |
|---|---|---|
| 컨볼루션 | forward · data grad · filter grad | 핵심. 여러 알고리즘 |
| 풀링 | max · average | forward·backward |
| 정규화 | batch/layer/instance norm | 학습/추론 모드 구분 |
| 활성 | ReLU·sigmoid·tanh·GELU 등 | 보통 다른 연산에 융합 |
| 축약 | softmax·reduction | 축 지정 |
| 시퀀스 | RNN·LSTM·GRU | 다층·양방향·드롭아웃 통합 |
| 어텐션 | multi-head·SDPA 융합 | 그래프 API(3.3), 3.6 |
| 텐서 유틸 | transform·op tensor | NCHW↔NHWC 변환 등 |
컨볼루션이 cuDNN의 정체성이다. 나머지는 컨볼루션과 융합되거나 사이를 잇는 부속(어텐션은 예외 — Transformer 시대 새 중심축).
3.2.3 컨볼루션 알고리즘 — 같은 결과를 만드는 여러 길
컨볼루션 forward는 수학적으로 하나지만 GPU에서 계산하는 길은 여럿이다. 큰 세 부류: implicit GEMM 계열·Winograd·FFT(direct도 있으나 보조).
3.2.3.1 im2col + GEMM (explicit) — 출발점
컨볼루션은 "필터를 입력의 모든 위치에 미끄러뜨리며 내적"이다. 각 출력 위치마다 필터가 덮는 입력 패치를 한 줄로 펴서 행으로 쌓으면 하나의 큰 행렬곱이 된다.
문제: 펼친 행렬은 입력보다 $K_h\cdot K_w$ 배 커진다(겹치는 패치 중복 저장). 3×3이면 ~9배. 이 메모리 폭발이 explicit의 약점 — 워크스페이스를 크게 점유한다.
3.2.3.2 implicit GEMM — im2col을 메모리에 안 쓴다
cuDNN의 주력. im2col 행렬을 실제로 메모리에 만들지 않고, GEMM 커널이 입력을 읽을 때 인덱스 계산으로 마치 펼쳐진 것처럼 접근한다. 타일을 공유 메모리(2.7)로 적재할 때 "이 타일 원소가 원본 입력의 어디인지"를 인덱스로 계산해 가져온다.
이득: HBM 임시 행렬 왕복(1.9)을 없애고 텐서코어(1.11)를 그대로 쓴다. 대부분 형상에서 기본 선택. 변형 implicit precomp GEMM(인덱스 일부 미리 계산해 워크스페이스에 둠)은 인덱싱 오버헤드↔워크스페이스를 맞바꾼다.
3.2.3.3 Winograd — 곱셈 수를 줄인다
작은 필터(특히 3×3, stride 1)에서 곱셈 횟수 자체를 줄이는 변환. 입력·필터를 변환 행렬로 도메인 변환 → 그 도메인에서 원소별 곱(작은 GEMM) → 역변환. 직접 합 대비 곱셈이 줄어 연산량 감소(F(2×2,3×3)에서 ~2.25배 절감, 정확한 수치는 타일·구현 의존).
- 이득: 3×3·stride 1 같은 흔한 형상에서 빠르다.
- 비용: 변환·역변환에 덧셈이 늘고, 변환 계수 때문에 수치 정밀도가 나빠질 수 있다(큰 타일 F(4×4,3×3)). FP16에서 누적 오차가 커질 수 있어 cuDNN이 형상·정밀도에 따라 배제하기도.
- 제약: 작은 필터·특정 stride 한정. 큰 필터·dilated conv엔 부적합.
3.2.3.4 FFT — 큰 필터에서 컨볼루션을 곱으로
컨볼루션 정리: 공간 도메인 컨볼루션 = 주파수 도메인 원소별 곱. 입력·필터를 FFT → 곱 → 역FFT. 필터가 클수록 유리(직접 합 $O(K^2)$를 FFT 항으로 교체).
- 이득: 큰 필터(7×7 이상)·특정 형상에서 연산량 절감.
- 비용: FFT 결과를 담을 큰 워크스페이스(tiled FFT로 완화). 작은 필터(3×3)엔 변환 오버헤드가 이득을 상쇄해 오히려 느림.
3.2.3.5 알고리즘 선택 한눈 표
| 알고리즘 | 잘 맞는 형상 | 워크스페이스 | 정밀도 | 비고 |
|---|---|---|---|---|
| implicit GEMM | 범용, 대부분 | 작음~0 | 양호 | 기본 선택, 텐서코어 친화 |
| implicit precomp GEMM | 범용 | 중간 | 양호 | 인덱싱↔워크스페이스 교환 |
| Winograd | 3×3, stride 1 | 중간 | 주의(오차↑) | 흔한 형상에서 빠름 |
| FFT | 큰 필터 | 큼 | 양호 | 작은 필터엔 부적합 |
| FFT tiling | 큰 필터·큰 입력 | 중간 | 양호 | 워크스페이스 절충 |
| direct | 특수 케이스 | 작음 | 양호 | 비주력 |
정직성: "잘 맞는 형상"은 일반 경향이며, 실제 최적은 (입력 N·C·H·W, 필터 K·C·Kh·Kw, stride·padding·dilation, 데이터 타입, GPU 세대, cuDNN 버전)의 함수다. 그래서 "표 보고 고른다"가 아니라 라이브러리에 묻거나(자동튜닝) 실측한다 — 3.3.
3.2.4 중간 점검 (체크포인트)
- [ ] cuBLAS와 cuDNN의 역할 경계를 한 문장으로("행렬곱 계열 vs 신경망 연산 패턴·융합").
- [ ] 한 컨볼루션 형상을 주면 implicit GEMM·Winograd·FFT 중 어느 쪽이 유리할지 이유와 함께 추측한다(필터 크기·정밀도·워크스페이스).
- [ ] explicit im2col의 메모리 폭발과 implicit GEMM이 그것을 어떻게 피하는지 설명한다.
3.2.5 흔한 함정
- 알고리즘은 입력 형상의 함수다. "이 모델은 X 알고리즘을 쓴다"는 틀렸다 — 그 형상에서 X를 쓴다. 배치·해상도·시퀀스 길이가 바뀌면 다른 알고리즘이 선택된다. 벤치마크는 실제 운영 형상으로.
- Winograd/FFT 정밀도 가정. 변환 때문에 같은 입력에도 약간 다른 수치를 낸다. 알고리즘이 형상·버전 따라 바뀌면 결과 비트가 미세하게 흔들린다 — 회귀 테스트 허용 오차를 지나치게 엄격하게 잡지 말 것.
- 레이아웃(NCHW vs NHWC). 텐서코어·융합은 보통 NHWC(channels-last)에서 최적. NCHW면 내부 변환이 개입해 느려진다. PyTorch면
memory_format=torch.channels_last. - explicit im2col 워크스페이스 폭발. 큰 필터·고해상도에서 im2col 행렬이 HBM을 점유한다. implicit GEMM이 기본인 이유.
3.2.6 연습문제
연습 1 (알고리즘 추측). 3×3·stride 1과 7×7·stride 1 컨볼루션에서 cuDNN이 선호할 알고리즘 부류를 추측하고 이유를.
정답
3×3·stride 1: Winograd 또는 implicit GEMM(Winograd는 작은 필터·stride 1에서 곱셈을 줄여 빠름, 단 정밀도·워크스페이스 주의). 7×7: 필터가 크므로 FFT 계열이 후보(컨볼루션→주파수 곱). 단 실제 선택은 N·C·해상도·정밀도·GPU·버전 의존 — 실측이 최종 판단(3.3).
연습 2 (im2col 메모리). 입력 C=64, H=W=56, 필터 $3\times3$을 explicit im2col로 펼치면 펼친 행렬이 입력 대비 몇 배인가? 왜 implicit이 이를 피하나?
정답
펼친 행렬의 열 = $C\cdot K_h\cdot K_w = 64\cdot3\cdot3 = 576$, 입력 채널 대비 $K_h\cdot K_w = 9$배(겹치는 패치 중복). implicit GEMM은 이 행렬을 HBM에 안 쓰고 GEMM 커널이 인덱스 계산으로 입력을 직접 읽어 펼침 효과를 내므로 워크스페이스가 작거나 0.
연습 3 (레이아웃). 같은 conv를 NCHW와 NHWC로 실행했더니 NHWC가 빨랐다. 왜인가?
정답
텐서코어 친화 conv 알고리즘·융합은 보통 NHWC(channels-last)에서 최적이다. NCHW면 내부 레이아웃 변환(transform)이 개입해 추가 HBM 트래픽·융합 깨짐이 생긴다. PyTorch는 channels_last로 맞춘다.
3.2.7 요약 · 다음 장
- cuDNN은 신경망 연산 패턴(컨볼루션·풀링·정규화·활성·RNN·어텐션)을 책임진다. cuBLAS가 GEMM 계열이라면 cuDNN은 그 사이의 패턴과 융합.
- 컨볼루션 한 연산에도 여러 알고리즘(implicit GEMM·Winograd·FFT)이 있고, 최적은 (형상·정밀도·GPU·버전)의 함수다. explicit im2col은 메모리 폭발, implicit GEMM이 그것을 피해 기본 선택.
- Winograd는 작은 필터에서 곱셈을 줄이나 정밀도 주의, FFT는 큰 필터에서 유리하나 워크스페이스가 크다.
다음 장: 3.3 cuDNN II — 그래프 융합 API·휴리스틱. 알고리즘을 고르는 두 방식(heuristics/find), 워크스페이스 트레이드오프, 그리고 그래프 API로 conv-bias-act를 한 커널로 융합하는 법을 본다.