GPU 시스템 08 - 프로파일링과 Roofline 관점
Nsight와 roofline 관점으로 커널 병목을 읽는 기본 프레임
감으로 최적화하면 금방 막힌다
GPU 최적화를 공부하다 보면 어느 순간부터 직감만으로는 답이 안 나온다. coalescing이 중요하고 shared memory가 중요하다는 사실을 알아도, 실제 커널이 왜 느린지는 측정하지 않으면 쉽게 착각하게 된다.
예를 들어:
- arithmetic instruction이 많아서 느릴 것 같았는데 실제로는 memory-bound일 수 있다
- occupancy가 낮아서 문제인 줄 알았는데 stall reason은 다른 곳일 수 있다
- kernel fusion이 좋아 보였는데 register pressure 때문에 더 느려질 수 있다
이 지점에서 프로파일링과 roofline 관점이 중요해진다.
프로파일링은 무엇을 보려고 하는가
프로파일링의 목적은 단순히 시간을 재는 것이 아니다. 더 중요한 것은 "이 커널이 어떤 자원 한계에 먼저 부딪히는가"를 알아내는 것이다.
GPU 프로파일링에서 자주 보게 되는 질문은 다음과 같다.
- memory throughput이 어느 정도 나오는가
- SM utilization이 어떤가
- warp stall의 주된 이유는 무엇인가
- instruction mix는 어떤가
- occupancy는 충분한가
이 질문에 답할 수 있어야 최적화 방향을 고를 수 있다.
Nsight를 볼 때 너무 많은 지표에 압도되지 않는 법
Nsight Compute 같은 도구를 처음 열면 지표가 너무 많아서 오히려 길을 잃기 쉽다. 이럴 때는 모든 항목을 다 보려 하지 말고, 아래 순서로 정리하는 편이 좋다.
- 실행 시간과 launch count
- memory throughput과 cache behavior
- achieved occupancy와 register/shared memory usage
- warp stall reason
- instruction mix와 tensor core 사용 여부
이 순서로 보면 "무엇이 가장 먼저 병목처럼 보이는가"를 비교적 빠르게 잡을 수 있다.
roofline은 왜 유용한가
roofline 관점은 커널이 arithmetic intensity 대비 어떤 상한에 가까운지를 보는 방식이다. 아주 단순화해서 말하면:
- 데이터 이동 대비 계산량이 낮으면 memory-bound일 가능성이 크다
- 데이터 이동 대비 계산량이 높으면 compute-bound일 가능성이 커진다
이 관점이 좋은 이유는 최적화 방향을 정하는 데 도움이 되기 때문이다.
예를 들어 memory-bound kernel이라면:
- coalescing 개선
- shared memory 재사용
- kernel fusion
- intermediate write/read 줄이기
같은 접근이 더 의미 있다.
반대로 compute-bound kernel이라면:
- instruction efficiency
- tensor core 활용
- unrolling
- arithmetic pipeline 활용
쪽이 더 중요해진다.
arithmetic intensity를 어떻게 생각하면 좋을까
arithmetic intensity는 옮긴 byte 대비 수행한 연산량 정도로 이해할 수 있다. 높은 arithmetic intensity는 한 번 읽은 데이터를 오래 활용한다는 뜻이고, 낮은 intensity는 데이터를 읽고 쓰는 데 비해 계산이 적다는 뜻이다.
matrix multiply가 대표적으로 arithmetic intensity가 높은 편이고, 단순 elementwise 연산은 낮은 편이다. softmax나 layernorm은 구조에 따라 memory traffic가 커져서 기대보다 bandwidth-bound가 되기 쉽다.
즉, 연산 이름만 보고 감으로 판단하지 말고, 실제 데이터 흐름을 같이 봐야 한다.
stall reason은 왜 중요할까
프로파일러에서 warp stall reason을 보면, 단순히 느리다는 사실보다 왜 느린지가 보이기 시작한다.
예를 들어:
- memory dependency stall이 큰가
- execution dependency stall이 큰가
- barrier나 synchronization 대기가 큰가
- not selected 상태가 많은가
이 값들은 optimization 방향을 바꿔준다. memory dependency가 크면 global memory 패턴과 reuse를 다시 봐야 하고, barrier stall이 크면 block 내부 synchronization 구조를 다시 봐야 한다.
좋은 profiling 습관
GPU 커널을 볼 때는 아래 습관이 중요하다.
- 최적화 전 baseline을 먼저 저장한다
- 한 번에 한 요소씩 바꾼다
- 성능 향상이 왜 일어났는지 설명할 수 있어야 한다
- timing뿐 아니라 throughput과 stall reason을 같이 본다
이 원칙이 없으면 "빨라졌다"는 사실은 남아도, 왜 빨라졌는지는 남지 않는다.
practical한 예시
예를 들어 softmax kernel을 프로파일링한다고 하자. 시간이 길다 해서 바로 exp 연산 자체를 의심하기 쉽다. 하지만 실제로는:
- row를 여러 번 읽고 있고
- reduction 과정에서 global memory 왕복이 많고
- coalescing이 좋지 않아 bandwidth를 못 쓰고 있을 수 있다
이 경우 exp approximation보다 memory flow를 먼저 고치는 것이 더 큰 효과를 낸다.
정리
GPU 프로파일링에서 중요한 것은 지표를 많이 아는 것이 아니라, 아래 연결을 보는 것이다.
- 이 kernel은 compute-bound인가, memory-bound인가
- throughput 상한에 어느 정도 가까운가
- stall reason이 무엇을 말해주는가
- 다음 최적화는 어느 방향이어야 하는가
roofline 관점과 profiling 습관이 생기면 최적화가 덜 추측 같아지고, 더 시스템적인 작업처럼 보이기 시작한다.
다음 글에서는 naive matrix multiply와 tiled matrix multiply를 비교하면서, 이 profiling 관점이 실제로 어떻게 쓰이는지 본다.