matrix multiply는 왜 계속 등장할까

GPU 시스템을 공부할 때 matrix multiply는 거의 빠지지 않는다. 이유는 단순하다. 딥러닝 연산의 중심에 있고, GPU 최적화의 핵심 개념들이 이 예제 안에 매우 많이 들어 있기 때문이다.

특히 naive matrix multiply는 좋은 출발점이다. 왜냐하면 구현은 단순하지만, 왜 느린지는 아주 교육적이기 때문이다.

naive kernel은 어떻게 생길까

가장 단순한 형태에서는 thread 하나가 출력 행렬의 원소 하나를 계산한다. (row, col) 위치를 담당하는 thread는 해당 row와 col을 따라가며 누적합을 계산한다.

겉으로 보면 꽤 자연스럽다.

  • 병렬화도 되어 있다
  • 수식도 맞다
  • thread마다 해야 할 일이 명확하다

하지만 실제 성능은 대개 기대보다 한참 낮다.

왜 느릴까: 데이터 재사용이 거의 없다

naive matrix multiply의 가장 큰 문제는 같은 입력 데이터를 여러 thread가 반복해서 global memory에서 읽는다는 점이다.

예를 들어 같은 block 안의 여러 thread가 A의 비슷한 row 조각이나 B의 비슷한 column 조각을 필요로 해도, naive kernel에서는 각 thread가 independently 가져온다. 즉, 재사용 가능한 데이터가 재사용되지 않는다.

이 때문에 arithmetic 자체보다 memory traffic가 훨씬 큰 부담이 된다.

global memory 관점에서 보면 어떤가

matrix multiply는 계산량이 많은 연산이라 arithmetic intensity가 높은 편이다. 하지만 naive implementation은 그 잠재력을 잘 못 살린다. 같은 값을 여러 번 global memory에서 읽어오면서 bandwidth를 낭비하기 때문이다.

즉, 수학적으로는 좋은 연산인데 구현이 메모리 관점에서 어설퍼서 성능을 못 끌어낸다.

thread mapping도 중요하다

naive kernel에서 (row, col) 매핑을 어떻게 하느냐에 따라 memory access pattern이 달라진다. 어떤 방향은 더 coalescing에 유리하고, 어떤 방향은 그렇지 않다.

이 때문에 단순히 "thread 하나가 원소 하나"라는 사실보다 "warp가 어떤 순서로 메모리를 읽는가"가 더 중요하다. 같은 naive matmul이라도 indexing 설계에 따라 차이가 생긴다.

cache가 어느 정도 도와주지 않나

어느 정도는 도움이 될 수 있다. 하지만 naive kernel을 cache에 기대는 방식으로 보는 것은 한계가 크다. 이유는 다음과 같다.

  • 재사용 패턴을 명시적으로 통제하기 어렵다
  • block 협업 구조가 없다
  • 큰 문제에서는 cache만으로 충분하지 않다

즉, 성능을 더 안정적으로 올리려면 shared memory 같은 명시적 재사용 전략이 필요해진다.

profiling으로 보면 어떤 그림이 나올까

naive matmul을 profiling하면 보통 다음과 같은 생각을 하게 된다.

  • arithmetic는 많지만 memory traffic도 매우 크다
  • coalescing이 완벽하지 않을 수 있다
  • global memory latency를 줄일 구조가 약하다
  • shared memory 재사용이 없어서 같은 값을 여러 번 읽는다

이 분석이 바로 tiled matmul로 가는 출발점이다.

핵심은 계산량이 아니라 재배치다

많은 초심자가 여기서 "더 빠른 곱셈 공식을 써야 하나?" 같은 생각을 한다. 하지만 대부분의 GPU 최적화는 수학 공식을 바꾸는 것이 아니라, 같은 계산을 더 좋은 데이터 흐름으로 재배치하는 일이다.

matrix multiply도 마찬가지다. 다음 단계에서는 block이 tile을 맡고, thread들이 shared memory를 통해 입력 조각을 재사용하게 만든다.

정리

naive matrix multiply가 느린 이유는 아래로 요약할 수 있다.

  • global memory 재사용이 약하다
  • 같은 입력을 여러 번 불러온다
  • block 협업 구조가 없다
  • arithmetic 잠재력을 memory 구조가 살리지 못한다

이 구조를 이해하면 tiled matmul이 왜 GPU 최적화의 대표 예제가 되었는지 자연스럽게 보인다.

다음 글에서는 tiled matrix multiply를 통해 shared memory와 block 협업이 실제로 어떤 차이를 만드는지 본다.