Thursday, September 4, 2008

CUDA shared memory bank conflict

CUDA 프로그래밍의 필수적인 과정은 coalesced data access pattern을 확보하는 것인데, 방법은 여러가지지만 두 가지 정도의 방법이 많이 쓰이는 것 같다.

첫 번째 방법은, 데이터를 restructuring해서 global memory에 항상 coalesced access를 할 수 있도록 알고리즘과 맞추는 것이다. 이 방법은 애초에 데이터 연관성이 없는 대량의 데이터를 처리하고자 할 때 사용할 수 있는 방법이고, 이를 위해 알고리즘 구성시 동시에 메모리 구성을 잘 결정해야 한다. 당연한 말이겠지만, 이 방법으로는 메모리 구성을 나중에 바꾸기가 매우 번거롭기 때문에 애초에 신중히 디자인 해야 한다.

두 번째 방법은, 대부분의 CUDA 예제에서 채택하는 방법으로 shared memory를 버퍼로 두어 global memory access를 모두 shared memory를 통하여 하는 방법이다. 이 때는 알고리즘에 데이터 연관성이 있다 해도 빠르게 access 가능하므로 일반적으로 사용할 수 있다.

그런데 shared memory를 통해서 coalesced access를 확보하고 비교적 빠른 속도를 얻었지만, 프로파일링을 해보면 warp serialize가 많다는 warning이 뜨는 경우가 있다. 도대체 이게 뭔가 싶어 검색을 해 봐도 그리 속시원한 대답은 없고, 잘 디자인된 예제의 경우에는 warp serialize가 모두 0이기 때문에 비교도 안되고 답답하다. 그러다 오늘 programming guide를 다시 읽어보다 눈에 띈 것이 shared memory도 32bit 단위로 bank가 만들어져 있다는 것. 지금 하고 있는 작업이 8bit 단위의 데이터를 쓰기 때문에 block내의 옆 thread가 동일 bank에 접근하게 되고 이 때문에 충돌이 일어난 것이다. 이를 알려준 것이 warp serialize 경고.

한 thread에서 4개의 연속된 8bit에대해 연산을 하면 warp serialize는 완벽히 피할 수 있다. thread수가 1/4로 줄어버리긴 했지만, struct로 32bit 연산을 하는 thread라면 이런 문제는 잘 발생하지 않는다. (그래서 예제에서는 거의 발생하지 않는다. 거의가 float 연산이므로.)

그런데 문제는 지금 첫 번째와 두 번째 coalesced access pattern을 모두 사용하고 있다는 점. bank conflict를 피하려고 4개씩 묶어냈더니만, global memory coalescing이 깨져버린다. 당연히 bank conflict가 일어나더라도 global memory coalescing을 확보하는 것이 빠르다. 그렇지만 global memory를 재구성하면 피할 수 있지 않을까 싶은데... 그 짓까지 해야하나 하는 생각이 들어 관둬버렸다.

전부 두번째 방법으로 하면 되지 않을까 싶겠지만 지금도 너무 shared memory사용량이 많아서 thread수를 늘리기가 어렵다. shared memory를 펑펑쓰면 그만큼 concurrent thread수가 줄어드니까 성능이 그리 향상되지 않는다. 이것도 어찌보면 trade-off 관계.

더 짜증나는 건 GPU time은 아주 작은데 CPU time이 엄청 크다고 프로파일러가 얘기하는 것이다. 간단한 커널인데도 오버헤드가 큰 이유를 잘 모르겠다.

이래저래 모르는 게 너무 많다.

No comments: