register는 빠르지만 공짜가 아니다

GPU 최적화에서 register는 가장 가까운 저장 공간이기 때문에 매우 중요하다. intermediate를 register에 잘 유지하면 memory access를 줄이고 연산 재사용을 늘릴 수 있다. 문제는 register가 무한하지 않다는 점이다.

thread당 register 사용량이 늘어나면 resident warp 수가 줄고, 심하면 spill까지 생긴다.

spill은 무엇인가

필요한 register 수가 너무 많아 하드웨어 register 안에 다 못 담기면 일부 값이 local memory 쪽으로 밀려난다. 이름은 local이지만 실제 성능 관점에서는 훨씬 비싼 접근이 된다.

즉, register 최적화를 잘못하면:

  • occupancy가 떨어지고
  • spill로 memory traffic가 늘고
  • 결국 더 느려질 수 있다

이 지점이 특히 헷갈리는 이유는, 코드만 보면 intermediate를 많이 들고 있는 쪽이 더 똑똑해 보이기 때문이다. 하지만 GPU에서는 thread 하나의 똑똑함보다, SM 전체가 얼마나 많은 warp를 안정적으로 돌릴 수 있는지가 더 중요할 때가 많다.

register pressure는 어디서 커질까

register pressure는 보통 아래 같은 상황에서 커진다.

  • loop unrolling을 많이 했을 때
  • thread 하나가 여러 output을 들고 있을 때
  • 큰 fused kernel에서 intermediate가 많을 때
  • vectorized path에서 unpacked 값을 오래 유지할 때

즉, 고급 최적화처럼 보이는 기법들이 register pressure를 같이 밀어 올리는 경우가 매우 많다.

spill이 왜 특히 위험한가

spill은 단순히 조금 느려지는 정도로 끝나지 않을 수 있다. spill이 생기면 local memory access가 늘어나고, 이는 다시 memory traffic와 latency 문제로 연결된다. 결국 compute를 줄이려던 최적화가 다시 memory-bound 성격을 키우는 역효과를 낼 수 있다.

이 때문에 프로파일러에서 spill 관련 신호가 보이면 꽤 강한 경고로 받아들이는 편이 좋다.

왜 GPU 최적화가 trade-off가 되는가

예를 들어 loop unrolling, per-thread work 증가, fusion 같은 기법은 종종 register 사용량을 키운다. 개별 thread 입장에서는 더 효율적일 수 있지만, SM 전체 관점에서는 동시에 돌 수 있는 warp 수가 줄어든다.

그래서 최적화는 항상 다음 질문과 함께 간다.

  • register를 더 써서 재사용을 늘릴 것인가
  • register를 덜 써서 occupancy를 확보할 것인가

이 균형을 잘못 잡으면 local optimum에 빠지기 쉽다.

실무에서는 이 문제를 대개 다음처럼 본다.

  • 먼저 baseline kernel의 register usage를 본다
  • block size와 occupancy를 함께 본다
  • unrolling/fusion 이후 register 증가가 얼마나 큰지 본다
  • 실제 step time과 stall reason이 개선되는지 확인한다

즉, register pressure는 이론보다 profiling과 함께 보는 편이 훨씬 현실적이다.

launch bounds나 compiler hint는 어떻게 보나

CUDA 코드에서는 때때로 launch bounds나 compiler option을 통해 register 사용량을 어느 정도 유도하려는 시도를 하기도 한다. 이런 방법은 분명 유용하지만, 마법처럼 생각하면 안 된다.

register를 억지로 줄이면 occupancy는 좋아질 수 있어도 instruction 증가나 spill로 다시 다른 비용이 생길 수 있기 때문이다. 결국 중요한 것은 숫자 하나가 아니라 전체 균형이다.

정리

register pressure는 GPU 최적화에서 아주 자주 다시 보게 되는 제약이다. 빠른 저장 공간을 더 많이 쓰는 것이 항상 좋은 게 아니라, 전체 SM throughput과 함께 봐야 한다는 사실을 계속 상기시켜준다.

좋은 kernel은 register를 아끼는 kernel도 아니고, register를 많이 쓰는 kernel도 아니다. 필요한 곳에는 충분히 쓰되, 그 결과로 resident warp와 throughput이 무너지지 않는 kernel에 가깝다.

다음 글에서는 tensor core와 mixed precision을 보면서, compute-bound 영역에서 성능을 크게 끌어올리는 수단을 본다.