커널 최적화에서 자주 다시 보는 주제들

CUDA 커널을 조금만 깊게 들어가도 반복해서 등장하는 주제가 있다. memory coalescing, shared memory, reduction이다. 이 세 가지는 따로 떨어진 기법이 아니라, 메모리 병목을 줄이고 병렬 계산 구조를 더 효율적으로 만드는 핵심 축이다.

Memory Coalescing은 왜 중요한가

coalescing은 warp 내부 thread들의 메모리 접근이 가능한 한 연속적이고 묶여서 일어나도록 만드는 것이다. 쉽게 말해 32개 thread가 제각각 흩어진 주소를 읽는 것보다, 정돈된 구간을 읽는 편이 훨씬 유리하다.

예를 들어 vector add처럼 i, i+1, i+2 식으로 연속 접근하면 coalescing이 잘 일어나기 쉽다. 반대로 stride가 큰 접근이나 랜덤한 gather 패턴은 성능을 빠르게 깎는다.

이 때문에 커널 최적화에서는 연산식보다 먼저 "warp가 어떤 주소를 어떤 순서로 읽는가"를 보는 경우가 많다.

즉, 같은 수식이라도 memory access pattern이 다르면 성능이 완전히 달라질 수 있다. 이 점 때문에 GPU kernel 최적화는 알고리즘을 바꾸는 작업이라기보다, 같은 계산을 더 좋은 memory flow로 재배치하는 작업처럼 느껴지는 경우가 많다.

Shared Memory는 재사용 가치를 만들 때 쓴다

shared memory는 단순히 빠른 메모리이기 때문에 쓰는 것이 아니다. 핵심은 같은 데이터를 여러 thread가 다시 쓸 때 global memory 재접근을 줄일 수 있다는 점이다.

matrix multiply가 대표적이다. 한 block이 필요한 입력 tile을 shared memory로 가져오고 block 내부 thread들이 함께 쓰면, global memory에서 같은 값을 반복해서 읽는 낭비를 줄일 수 있다.

하지만 shared memory도 무턱대고 쓰면 안 된다.

  • 재사용이 거의 없으면 옮기는 비용만 생긴다
  • bank conflict가 생기면 기대만큼 빠르지 않다
  • 많이 쓰면 occupancy가 떨어질 수 있다

즉, shared memory는 "빠른 캐시"가 아니라 "의도적으로 관리하는 협업 공간"에 가깝다.

이 표현이 중요하다. shared memory는 자동으로 똑똑하게 동작하는 캐시라기보다, 개발자가 재사용 구조를 직접 설계하는 영역이다. 그래서 잘 쓰면 강력하지만, 재사용과 synchronization 구조를 잘못 잡으면 오히려 복잡도만 늘 수 있다.

Reduction은 GPU 사고방식에 익숙해지는 좋은 예제다

reduction은 합계, 최대값, softmax의 분모 계산처럼 많은 값을 하나 또는 몇 개로 줄이는 패턴이다. 겉으로는 단순하지만 GPU에서는 꽤 많은 개념이 들어간다.

  • thread별 partial result 계산
  • shared memory를 통한 block 내부 reduction
  • warp-level primitive 활용
  • 여러 block 결과를 다시 합치는 구조

이 패턴을 제대로 이해하면 softmax, layernorm, attention 일부 연산처럼 실제 딥러닝 커널에서 자주 만나는 구조를 읽기 쉬워진다.

또한 reduction은 "병렬 장치에서 많은 값을 하나로 줄이는 법"을 다루기 때문에, GPU가 잘하는 일과 어려워하는 일이 동시에 드러난다. 그래서 reduction을 이해하면 block-level 협업과 warp-level primitive를 보는 눈도 함께 좋아진다.

Softmax가 좋은 연습 문제인 이유

softmax는 생각보다 많은 것을 보여준다.

  • 최대값 reduction이 필요하다
  • exp 후 합계 reduction이 또 필요하다
  • 메모리를 여러 번 읽고 쓰기 쉽다
  • numerical stability까지 신경 써야 한다

그래서 softmax를 최적화해보면 단순 산술보다 메모리 이동과 reduction 구조가 얼마나 중요한지 몸으로 느끼기 좋다.

실무적인 체크포인트

이 단계에서는 아래 질문을 습관처럼 보는 편이 좋다.

  • warp 내부 thread 접근이 연속적인가?
  • shared memory로 옮겼을 때 정말 재사용 이득이 있는가?
  • reduction을 block 단위와 warp 단위로 어떻게 나눌 것인가?
  • register, shared memory, occupancy 사이 균형이 깨지지 않았는가?

이 체크포인트가 없으면 최적화가 아니라 그냥 코드 변형만 하게 되기 쉽다.

결국 이 글의 핵심은 세 가지를 별개 기술로 외우는 것이 아니라, memory access와 협업 구조를 설계하는 하나의 묶음으로 보는 것이다. 그래야 이후 softmax, layernorm, fused op가 더 자연스럽게 읽힌다.

중요한 감각 하나

GPU 커널 최적화는 종종 새로운 알고리즘을 발명하는 작업이 아니다. 이미 있는 계산을 더 좋은 메모리 흐름과 더 나은 병렬 구조로 재배치하는 작업인 경우가 많다. coalescing, shared memory, reduction은 그 재배치의 대표적인 도구다.

다음 글에서는 CUDA만이 아니라 Triton으로 같은 문제를 어떻게 더 빠르게 실험하고 최적화할 수 있는지 본다.