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인지 어떻게 판단하는지 본다.