1) 커널을 나눌 때(멀티 커널) 생기는 것들
장점
- 라이브러리 최적화 극대화: cuBLAS/cuBLASLt, cuDNN이 텐서코어, 스케줄링, 타일링을 최적화. 거대한 GEMM은 이게 최고.
- 구현 단순/유연: 그래프가 바뀌어도 재컴파일·특수화 부담 적음.
- 병렬성/오버랩: 독립 서브그래프는 여러 스트림에 분산해 오버랩 가능.
단점
- 런치 오버헤드 누적: 작은 커널이 줄줄이면 수 µs 단위 오버헤드가 많이 쌓임(특히 Windows/WDDM).
- 글로벌 메모리 왕복: 중간 텐서를 매번 global mem -> kernel -> global mem로 쓰고 읽음. 메모리 대역폭이 병목이면 치명적.
- 캐시/L2 재사용 기회 상실: 바로 다음 연산이 같은 데이터를 써도, 커널이 달라지면 레지스터/SMEM에 남아 있던 게 사라짐.
2) 단일(혹은 과한) 퓨전 커널에서 생기는 것들
장점
- 중간값 미물질화(no materialization): 레지스터/SMEM에서 이어서 쓰니 글로벌 메모리 왕복 감소 → 메모리 바운드 op에 큰 이득.
- 런치 오버헤드 감축: 커널 수를 줄이면 호출 비용 감소.
- 데이터 지역성 개선: L2/SMEM/레지스터에서 붙여 연산.
단점/위험
- 레지스터 압박: 체인을 많이 합칠수록 스칼라/타일 누적이 커져 레지스터 수↑ → 활성 워프↓(점유율 하락) → 스필 발생 시 오히려 global mem 트래픽↑.
- SMEM 사용량 증가: 큰 타일/더블버퍼링 등으로 블록당 SMEM↑ → 동시 resident 블록 수↓ → 점유율 하락.
- 라이브러리 품질 포기 위험: 직접 mega-kernel로 GEMM까지 흡수하면 cuBLASLt의 텐서코어 타일링/파이프라이닝 품질을 못 따라갈 수 있음.
- 확장성/유지보수 비용: activation, dtype, 스트라이드/shape 변화마다 변종 커널 필요 → 코드 폭증/빌드 시간 증가.
3) 속도 관점의 직감적 룰(현실 팁)
- GEMM/Conv는 라이브러리 호출 + Epilogue Fusion
- C = activation(alpha * A @ B + beta * C + bias)를 cuBLASLt나 CUTLASS epilogue로 한 번에.
- ReLU/SiLU/GELU(근사)/bias-add/scale/residual 같은 건 epilogue에 태우기 쉬움.
- 이때 accumulator가 레지스터에 있을 때 바로 활성화/바이어스가 적용되어 중간 C를 global에 쓰고 다시 읽는 비용을 없앰.
- Elementwise 체인은 적극 fuse
- 예: y = relu(a*x + b), z = layernorm(y * gate + bias) 중에서 형상이 동일하고 메모리 액세스 패턴이 단순한 구간은 한 번에.
- elementwise는 메모리 바운드라 왕복 줄이면 바로 이득.
- 작은 연산이 N개 연달아 있을 때는
- CUDA Graph capture로 멀티 커널 구조는 유지하면서 런치 오버헤드만 제거해도 큰 체감이 남.
- Graph는 의존성 순서 보장 + 한 번의 submit로 전체 서브그래프 실행.
- 너무 많은 퓨전은 경계
- 컴파일 후 ptxas 레포트에서 registers per thread가 96~128을 크게 넘기 시작하고, **local memory(spill)**가 보이면 역효과일 가능성 큼.
- 점유율 100%가 항상 정답은 아니지만, **<~25%**로 떨어지면 대개 안 좋은 신호.
- 메모리 트래픽 계산으로 판단
- 예: GEMM(MxK)*(KxN) 뒤에 bias add + ReLU
- 분리 시: C를 global에 1회 write(GEMM), 다음 커널이 1회 read, activation 또 1회 read/write → 대략 2~3×|C| 추가 왕복.
- epilogue fuse 시: GEMM accumulator → bias → activation까지 레지스터에서 끝 → 추가 왕복 ≈ 0.
- 텐서 크기가 L2를 넘고, op가 메모리 바운드면 체감 차이가 큼.
- 예: GEMM(MxK)*(KxN) 뒤에 bias add + ReLU
4) 메모리 사용량 트레이드오프
- 퓨전의 이득: 중간 텐서 비물질화로 피크 메모리와 대역폭 수요 동시 절감.
- 퓨전의 비용: 레지스터/SMEM 사용량 증가 → 점유율 하락. 심하면 레지스터 스필 → 로컬 메모리(=글로벌) → 이건 메모리 폭탄.
- 분리 커널: 중간 텐서를 항상 materialize → 피크 메모리↑, 그러나 레지스터/SMEM 압박은 낮아 안정적 점유율 확보가 쉬움.
5) Attention/트랜스포머 특화 팁
- FlashAttention류: QK^T → softmax → V의 대형 중간행렬(O(N²))을 저장하지 않고 타일링/재계산으로 처리하는 “알고리즘적 퓨전”.
- 이건 일반적 epilogue 넘어선 고난이도지만, 메모리 왕복과 메모리 풋프린트를 근본 감소시켜 큰 차이를 냅니다.
- MLP 블록: GEMM → bias+activation → GEMM → bias+activation
- 각 GEMM은 cuBLASLt epilogue로 처리, 그 사이의 **elementwise 체인(예: dropout·residual·norm 일부)**은 하나의 elementwise fused 커널로.
6) 당신의 현재 방식(연산자 단위 순차 실행)을 개선하는 실용 설계
- Op Fusion Pass 추가(그래프 레벨)
- 규칙: 동일 shape/stride, elementwise-only, 정적 브로드캐스트 가능인 연속 노드 묶기.
- **GEMM/Conv 노드는 “경계”**로 두고, epilogue로만 합치기(bias/act/residual/scale).
- 시그니처( dtype, layout, shape tile, 활성화 종류 )로 커널 캐시.
- 라이브러리 이용
- cuBLASLt: matmul + bias + activation(+residual/scale) epilogue 옵션 적극 사용.
- CUTLASS: 필요한 변종(epilogue functor) 있을 때 템플릿으로 커스터마이즈.
- cuDNN: Conv/BN/Act 가능한 범위 내 fusion API 활용.
- CUDA Graph Capture
- “분리 커널 유지 + 런치오버헤드 제거” 플랜.
- 고정 shape/정적 그래프 구간은 warm-up 후 capture 실행으로 전환.
- 리소스 힌트/검증 루프
- ptxas(--ptxas-options=-v)로 레지스터/SMEM 확인, 스필/점유율 체크.
- Nsight Compute로 메모리 대역폭/SM 효율 확인 → roofline 감각으로 메모리/연산 바운드 구분.
- 히ュー리스틱
- 메모리 바운드(elementwise 체인): 최대한 fuse.
- 연산 바운드(큰 GEMM/Conv): 라이브러리 그대로 + epilogue만.
- 작은 커널이 수십~수백 개면 Graph 또는 소규모 fuse로 런치 오버헤드 제거.
- 레지스터/thread 96~128 부근에서 성능이 꺾이는지 체감 테스트.
- cp.async, 더블버퍼링, warp-specialization은 GEMM류 타일 내에서 이득. 직접 mega-kernel로 시도할 땐 레지스터/SMEM 예산부터 계산.
'dev_AI_framework' 카테고리의 다른 글
| 폴더 및 파일 구조 변경 - Python 내 통제 방법으로 변경 예정 (2) | 2025.09.01 |
|---|---|
| AI 컴파일러의 정의와 Frame_work 내 사용 방법 고민 (1) | 2025.09.01 |
| on-the-fly 전치 + 타일렁 누적 VS cuBLAS 의 비교 (0) | 2025.08.31 |
| 현재 GEMM 구현 내용 (0) | 2025.08.31 |
| 행렬 곱 용어, 차원 표기 정의 - GEMM, (B, T, C, H, Dh) ... (0) | 2025.08.31 |