GPU 시스템 11 - Shared Memory Bank Conflict
shared memory가 빠르다고 끝이 아닌 이유와 bank conflict를 피하는 기본 원리
shared memory는 빠르지만 무조건 빠르진 않다
shared memory를 처음 배우면 global memory보다 훨씬 빠르다는 사실에 집중하게 된다. 맞는 말이다. 하지만 여기서 한 걸음 더 들어가면, shared memory도 접근 패턴에 따라 성능이 갈릴 수 있다는 사실을 보게 된다. 그 대표적인 문제가 bank conflict다.
bank라는 구조를 왜 생각해야 하나
shared memory는 내부적으로 여러 bank로 나뉘어 있다. 이상적인 경우에는 warp의 thread들이 서로 다른 bank에 접근해 동시에 데이터를 읽거나 쓸 수 있다. 그런데 여러 thread가 충돌하는 방식으로 접근하면 요청이 직렬화되면서 속도가 떨어진다.
즉, shared memory가 빠르다는 말은 "잘 접근했을 때 빠르다"는 뜻에 더 가깝다.
bank conflict는 언제 생기나
아주 단순하게 말하면, 같은 시점에 여러 thread가 bank 구조상 겹치는 위치를 접근할 때 생긴다. 이 현상은 데이터 레이아웃과 접근 stride에 따라 자주 발생한다.
특히 transpose나 특정 tiled access 패턴에서는 row-major 기준으로는 괜찮아 보이는데 실제 bank 매핑 관점에서는 충돌이 심할 수 있다.
왜 tiled kernel에서 자주 보일까
tiled matmul이나 transpose 같은 커널은 shared memory를 적극적으로 사용한다. 이때 입력을 shared memory에 저장하는 방식이나 읽는 순서에 따라 bank conflict가 생길 수 있다.
즉, shared memory를 도입해 global memory traffic는 줄였는데, 그 대신 shared memory 내부 접근이 비효율적이 되는 상황이 나올 수 있다. 그래서 shared memory optimization은 한 단계 더 깊은 설계가 필요하다.
padding이 자주 등장하는 이유
bank conflict를 줄이는 대표적인 방법 중 하나는 padding이다. 예를 들어 shared memory tile의 열 수를 약간 늘려 alignment를 깨면, thread 접근이 같은 bank에 몰리는 문제를 완화할 수 있다.
겉으로 보면 메모리를 조금 낭비하는 것처럼 보이지만, 실제로는 직렬화 비용을 줄여 더 나은 성능을 만드는 경우가 많다.
shared memory 최적화에서 중요한 관점
shared memory를 쓸 때는 아래 두 단계를 같이 봐야 한다.
- global memory 접근을 얼마나 줄이는가
- shared memory 내부 접근이 얼마나 깔끔한가
첫 번째만 보고 두 번째를 놓치면 "shared memory를 썼는데 생각보다 안 빠르다"는 상황이 생긴다.
practical한 체크포인트
- warp 내부 thread가 shared memory를 어떤 stride로 읽는가
- row/column 접근이 bank 구조에 맞는가
- transpose나 tile layout 변경이 conflict를 만들고 있지 않은가
- padding 하나로 완화할 수 있는가
이 질문이 익숙해지면 shared memory를 더 정교하게 다룰 수 있다.
정리
bank conflict는 shared memory의 속도를 망치는 대표적인 내부 병목이다. 그래서 shared memory 최적화는 단순히 "global memory보다 빠르니까 쓴다"에서 멈추지 않고, bank 구조까지 고려해야 진짜 최적화가 된다.
다음 글에서는 warp shuffle과 warp-level primitive를 통해 shared memory 없이도 일부 협업을 더 효율적으로 할 수 있는 방법을 본다.