GPU 시스템 07 - Occupancy와 Latency Hiding
occupancy를 숫자로만 외우지 않고 latency hiding과 연결해서 이해하기
occupancy를 처음 배우면 자주 생기는 오해
GPU 최적화를 공부하다 보면 occupancy라는 말을 거의 바로 만나게 된다. 보통은 "SM에 얼마나 많은 warp가 동시에 올라가 있는가" 정도로 설명된다. 이 정의 자체는 맞지만, 그 정의만 외우면 실제 최적화에서 자주 잘못된 판단을 하게 된다.
가장 흔한 오해는 이렇다.
- occupancy가 높을수록 무조건 빠르다
- occupancy가 낮으면 반드시 잘못된 커널이다
- block size만 키우면 occupancy가 좋아지고 성능도 좋아진다
현실은 훨씬 더 미묘하다. occupancy는 중요한 지표이지만, 그것만으로 성능을 설명하지는 못한다. 더 정확히 말하면 occupancy는 latency hiding 능력과 연결해서 봐야 한다.
GPU는 왜 많은 warp를 필요로 할까
GPU에서는 global memory access나 dependency 때문에 어떤 warp가 잠깐 멈추는 일이 자주 생긴다. 이때 하드웨어는 멈춘 warp를 기다리는 대신, 다른 준비된 warp로 빠르게 전환해 계산을 이어간다. 이 구조 덕분에 GPU는 높은 처리량을 낼 수 있다.
즉, 많은 warp가 동시에 존재한다는 것은 단순한 숫자 경쟁이 아니라 "누가 멈춰도 다른 warp로 갈아탈 수 있는가"와 연결된다. 이것이 latency hiding의 핵심이다.
occupancy는 정확히 무엇을 뜻할까
occupancy는 보통 한 SM에 동시에 resident할 수 있는 최대 warp 수 대비 현재 resident warp 수의 비율로 정의한다. 예를 들어 어떤 SM이 최대 64 warp를 담을 수 있는데 현재 32 warp가 올라와 있으면 occupancy는 50%다.
이 값에 영향을 주는 요인은 생각보다 많다.
- block 당 thread 수
- block 당 shared memory 사용량
- thread 당 register 사용량
- 하드웨어의 SM 자원 제한
즉, occupancy는 launch parameter 하나로만 결정되는 값이 아니라 커널 전체 자원 사용 구조의 결과다.
occupancy가 너무 낮으면 왜 문제가 될까
occupancy가 낮다는 것은 warp 수가 충분하지 않아 memory latency나 pipeline stall을 다른 warp로 덮기 어렵다는 뜻일 수 있다. 예를 들어 register를 너무 많이 써서 한 SM에 block이 거의 못 올라가면, warp가 잠깐 멈출 때 다른 warp로 전환할 선택지가 줄어든다.
이런 상황에서는 global memory access가 조금만 길어져도 SM이 놀게 된다. 그래서 낮은 occupancy는 종종 throughput 저하로 이어진다.
특히 memory-bound kernel에서는 이 문제가 더 크게 보인다. 계산 자체보다 대기 시간이 더 크기 때문에, 그 대기를 다른 warp로 얼마나 잘 숨기느냐가 성능을 좌우하기 때문이다.
그렇다면 occupancy가 높으면 항상 좋을까
그렇지는 않다. 높은 occupancy를 만들려고 block size를 억지로 키우거나 register 사용을 줄이기 위해 코드를 부자연스럽게 바꾸면 오히려 더 느려질 수 있다.
예를 들어:
- register를 너무 아끼다가 local memory spill이 생길 수 있다
- block 구조가 메모리 접근 패턴과 맞지 않을 수 있다
- shared memory 재사용 구조가 약해질 수 있다
즉, occupancy는 목적이 아니라 수단이다. 중요한 것은 충분한 resident warp를 확보해 latency를 숨기면서도, 메모리 접근과 연산 구조를 망치지 않는 것이다.
practical한 감각은 어떻게 잡아야 할까
실무적으로는 occupancy를 아래처럼 보는 편이 좋다.
- 너무 낮으면 자원 사용 구조를 의심한다
- 적당히 확보되면 그 다음부터는 memory throughput과 stall reason을 본다
- 높은데도 느리면 다른 병목이 있다고 본다
즉, occupancy는 "첫 번째 진단 지표"로는 좋지만 "최종 결론"으로 쓰면 위험하다.
예를 들어 100% occupancy인데도 memory bandwidth가 이미 포화라면 더 올릴 곳이 없다. 반대로 50~60% 정도여도 warp scheduling이 충분하고 coalescing이 잘 되어 있으면 아주 좋은 성능이 나올 수도 있다.
register pressure와 occupancy의 관계
occupancy를 볼 때 가장 자주 다시 보게 되는 것이 register pressure다. 커널이 intermediate를 많이 들고 있으면 register 사용량이 커지고, 그 결과 한 SM에 동시에 올라갈 수 있는 warp 수가 줄어든다.
이 지점이 흥미로운 이유는, 어떤 local optimization이 global throughput을 해칠 수 있기 때문이다. 예를 들어 계산 재사용을 늘리려다가 register를 너무 많이 쓰면, 개별 thread는 효율적이어도 전체 SM throughput은 떨어질 수 있다.
그래서 최적화는 항상 다음 질문으로 이어진다.
- register를 더 써서 arithmetic를 줄일 것인가
- register를 덜 써서 occupancy를 확보할 것인가
이 판단은 커널마다 다르다.
occupancy calculator를 어떻게 봐야 하나
CUDA 개발에서는 occupancy calculator나 Nsight 지표를 통해 예상 occupancy를 자주 본다. 이 도구는 유용하지만, 결과 숫자를 절대적인 평가처럼 보면 안 된다.
도구가 알려주는 것은 "이 자원 사용 구조에서 이 정도 resident warp가 가능하다"는 사실이지, "이 커널이 최적이다"라는 뜻이 아니다.
그래서 calculator는 아래 용도로 쓰는 편이 좋다.
- block size 후보를 비교해보기
- register/shared memory 사용이 resident block 수를 어떻게 바꾸는지 보기
- 왜 특정 kernel이 예상보다 warp를 적게 유지하는지 확인하기
정리
occupancy를 제대로 이해하려면 다음 흐름으로 봐야 한다.
- 많은 warp가 필요한 이유는 latency hiding 때문이다
- occupancy는 그 가능성을 보여주는 지표다
- 하지만 높은 occupancy 자체가 목적은 아니다
- 실제 성능은 register pressure, memory access, stall reason까지 함께 봐야 한다
이 감각이 생기면 occupancy를 숫자 게임으로 보지 않고, GPU가 왜 바쁘게 혹은 한가하게 도는지를 설명하는 구조적 지표로 볼 수 있게 된다.
다음 글에서는 프로파일링과 roofline 관점으로 넘어가서, 커널이 실제로 memory-bound인지 compute-bound인지 어떻게 판단하는지 본다.