GPU 커널을 최적화해 본 개발자라면 '공유 메모리 뱅크 충돌(shared memory bank conflict)'이라는 벽에 한 번쯤 부딪혀 봤을 것이다. NVIDIA GPU의 공유 메모리는 32개의 뱅크로 나뉘어 있고, 한 워프(warp)의 32개 스레드가 같은 뱅크의 서로 다른 주소에 동시에 접근하면 하드웨어가 이를 직렬화한다. 여러 스레드가 같은 뱅크로 몰릴수록 접근이 순차적으로 처리되면서 지연 시간이 늘어난다. Lei Mao의 기술 블로그는 이 문제를 padding이 아닌 '스위즐링(swizzling)'으로 해결하는 방법을 정리했다.
왜 패딩만으로는 부족한가
뱅크 충돌을 피하는 가장 손쉬운 방법은 패딩이다. 예를 들어 32×32 배열 대신 32×33처럼 한 칸을 더 붙여 각 행의 시작 뱅크를 어긋나게 만들면 열 방향 접근에서 충돌이 사라진다. 구현과 이해가 쉽다는 것이 패딩의 가장 큰 장점이다. 하지만 대가가 따른다. 우선 추가된 칸만큼 귀중한 공유 메모리를 낭비한다. 더 까다로운 문제는 정렬(alignment)이다. reinterpret_cast로 데이터를 큰 청크 단위로 묶어 읽는 벡터화 접근을 할 때, 패딩 크기를 신중히 고르지 않으면 데이터 주소 정렬이 깨져 미정의 동작(undefined behavior)으로 이어질 수 있다. 특히 2D 패딩 배열에 벡터화 접근을 적용할 때 이런 정렬 붕괴가 무심코 발생하기 쉽다.
스위즐링은 공유 메모리 공간을 전혀 낭비하지 않으면서 같은 목적을 달성한다. 핵심 아이디어는 데이터를 저장할 실제 인덱스를 XOR 연산으로 재배치하는 것이다. 논리적으로는 array[y][x]에 접근하지만, 물리적으로는 y값에 따라 x를 뒤섞은 위치에 저장한다. 이렇게 하면 원래는 같은 뱅크로 몰렸을 접근들이 서로 다른 뱅크로 흩어진다.
스위즐링이 작동하는 원리
블로그가 제시하는 공식은 다음 순서로 동작한다. 먼저 array[][NX]에서 NX × sizeof(T)를 SWIZZLE_SIZE로 정의한다. SWIZZLE_SIZE는 32 이상의 2의 거듭제곱(32, 64, 128, 256 등)이어야 한다. 그다음 [y][x] 인덱스를 TC바이트 청크 단위로 나눠 세그먼트 내 위치(x_chunk)와 세그먼트 번호(y_chunk)를 구하고, 두 값을 XOR한 x_chunk_swz로부터 최종 스위즐 인덱스 x_swz를 계산한다. XOR을 쓰는 이유는 이 연산이 한쪽 값을 고정하면 항상 일대일 대응을 보장하기 때문이다. 블로그는 y_chunk가 결국 y와 같고 x_chunk가 x에 비례하는 값으로 정리된다는 것을 수식 전개로 보인다. 그 결과 스위즐 전후 인덱스가 데이터 손실 없이 일대일로 매핑되며, 특정 열을 이루는 NX개의 접근이 모두 서로 다른 뱅크에 놓인다는 성질이 성립한다.
행렬 전치(transpose)는 이 기법이 빛을 발하는 대표 사례다. 각 워프는 전역 메모리에서 32개 값으로 이뤄진 한 행을 읽어 스위즐링을 적용해 공유 메모리에 쓴다. 이때 쓰기에서는 뱅크 충돌이 발생하지 않는다. 전치를 위해 워프가 스위즐된 '열' 두 개를 읽어 다시 전역 메모리에 쓰는데, 스위즐링 덕분에 읽기 과정의 뱅크 충돌은 단 한 번으로 줄어든다. 스위즐링이 없다면 같은 상황에서 16번의 2-way 뱅크 충돌이 생긴다. 참고로 32×32처럼 정사각 형태라면 애초에 읽기·쓰기 모두 충돌이 없다.
실측 성능과 실무적 판단
Intel i9-9900K와 NVIDIA RTX 3090 환경에서 세 가지 방식의 전치 커널을 비교한 결과, 뱅크 충돌이 있는 커널이 가장 느렸고 패딩과 스위즐링을 적용한 두 커널은 동일한 지연 시간으로 약 20% 더 빠르게 동작했다. 다만 이 구현은 RTX 3090 최대 메모리 대역폭의 약 65% 수준에 그친다. 블로그는 행렬이 항상 패딩되어 있고(보통 cudaMallocPitch로 할당) 각 행이 코얼레싱 요건을 계속 만족한다고 가정하면, 벡터화 메모리 접근으로 성능을 훨씬 더 끌어올릴 수 있다고 설명한다.
정리하면 스위즐링과 패딩은 같은 문제를 푸는 두 갈래 길이다. 스위즐링은 메모리를 낭비하지 않지만 인덱스 매핑이 선형이 아니어서 구현과 디버깅이 어렵다. 패딩은 직관적이지만 공간을 소모하고 정렬 문제를 안고 있다. 실무자 입장에서 선택 기준은 명확하다. 공유 메모리 용량이 점유율(occupancy)의 병목이거나 벡터화 접근에서 정렬을 엄격히 지켜야 하는 커널이라면 스위즐링이 유리하다. 반대로 메모리 여유가 넉넉하고 빠른 프로토타이핑이 우선이라면 패딩이 합리적이다. 다만 이번 사례처럼 두 기법의 성능이 동일하게 나올 수 있다는 점은, 스위즐링의 복잡성을 감수할지 여부가 순수 속도보다는 메모리 예산과 정렬 제약에 달려 있음을 시사한다.
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공