📃

fvbar-kshan 알고리즘 CUDA 최적화

개요

1시간 이내에 12개 Slot 이미지에 대하여 L2 알고리즘 26종이 모두 처리되어야 하는데 현재 CUDA를 적용했음에도 가장 늦게 들어오는 12번 Slot 이미지의 fvbar-kshan 알고리즘 실행이 1시간 이상 소요되는 상황이어서 CUDA를 최적화하여 처리 속도를 개선하는 작업을 진행하였다.

결과

NVIDIA Tesla K40C GPU CUDA 기반 12번 Slot 이미지 FVBAR 알고리즘 처리에 기존 1시간 이상 소요되던 것이 약 0.12시간 소요되는 정도로 성능이 크게 개선되었다.

과정

중국을 촬영해서 육지 부분이 가장 넓기 때문에 처리 시간이 가장 오래 걸리는 Slot 12번 자료를 가지고 시간을 측정하며 CUDA 적용 작업을 진행하였다.

분기 제거

기존에는 특히 cal_k012 함수에서 least_square 함수를 호출할 때 너무 많은 수의 분기문이 있었다.
CUDA 커널 코드 내 분기문은 branch divergence를 발생시켜 속도를 심각하게 저하시킨다고 한다.
위 링크를 참고하여 불필요하고 대체 가능한 분기문을 제거하였다.

실행 시간

적용 전: 1시간 12분 2초
적용 후: 1시간 12분 11초
차이: -9초
속도: 99.8%
실행 시간이 오히려 늘었다.
외부 요인에 의해 따라잡힐 정도로 최적화 정도가 미미한 것으로 보인다.

산출물

원본 산출물과 완전히 동일했다.

Single Floating Point 연산

위 그림은 Kepler CUDA Multiprocessor 구조도이다.
위와 같이 CUDA Multiprocessor에는 Single Floating Point 연산을 위한 Core가 Double Floating Point 연산을 위한 DP Unit보다 3배 많다.
따라서 Double 형 자료의 연산보다 Float 형 자료의 연산 속도가 빠르다.
이전에 현업화 작업을 하며 산출물의 연산 정확도를 고려하여 자료형을 Double로 변경한 적이 있는데 이번 최적화 작업에서 다시 Float 형으로 변경하였다.

실행 시간

적용 전: 1시간 12분 11초
적용 후: 1시간 11분 59초
차이: 12초
속도: 100.3%
실행 시간은 거의 줄어들지 않았다.
다만 다음에 수행한 글로벌 메모리 사용 작업에서 글로벌 메모리 사용량을 절반으로 감소시키는 효과를 내었다.

산출물

32비트 부동 소수점 자료형의 정확도를 고려하였을 때 무시할 수 있는 수준의 오차가 발생하였다.

Block, Thread 개수 조정 및 글로벌 메모리 사용

Block 및 Thread 개수가 원인으로 추정되는 메모리 오류가 발생
커널 외부에서 글로벌 메모리를 할당하여 사용하도록 하려고 하니 Thread 개수가 너무 많았음. (Thread 개수 = 픽셀 개수였으므로 2720*2718*1299*4 Bytes = 약 36 GB의 메모리를 사전에 할당해 놓아야 했음.)

Block, Thread 개수 조정

처음에는 CUDA의 Grid, Block, Thread에 대한 개념을 정확히 이해하지 못하여
1개의 Thread에서 1개의 Pixel을 처리하고, 1개의 Block에서 2개의 Thread를 처리하고,
전체 프로세스에서 Pixel 개수(12번 슬롯 기준 약 6백만 개) / 2개, 즉 약 3백만 개의 블록을 처리하도록 하였다.
위와 같은 메모리 구조를 갖는 CUDA에서는 3백만 개의 블록을 처리하기 위해 3백만 번의 Shared Memory를 구성해야 하기 때문에 해당 작업을 위한 상당한 시간 소모가 있었을 것이다.
최적화 작업을 하며 그리드 크기(블록 개수)를 적절한 2의 제곱수를 선택하여 128개로,
블록 크기(쓰레드 개수)를 Tesla K40C의 Multiprocessor 당 CUDA 코어 개수인 192개로 조정하고
1개의 쓰레드 안에서 (처리해야 할 픽셀 개수 / (128 * 192))를 올림한 개수의 픽셀을 처리하도록 수정하였다.

글로벌 메모리 사용

기존에는 커널 내부에서 new 구문을 이용하여 메모리를 할당하고 사용하는 형태였으며 이렇게 할당되는 총 메모리 크기를 계산해 보니 약 122 MB 였다.
큰 크기가 아니라고 생각할 수 있지만 위에 올린 CUDA 메모리 구조를 생각해 보면 쓰레드에서 Register를 이용하여 저장할 만한 크기는 아님을 알 수 있다.
Tesla K40C의 L1 Cache 크기가 16KB로 공유 메모리에도 저장할 수 없는 용량이었다.
쓰레드에서 Register에 저장할 수 없을 정도로 많은 메모리를 요구할 경우 CUDA는 GPU의 메모리가 아니라 일반적인 DRAM을 사용하도록 한다.
이렇게 DRAM을 사용하게 될 경우 I/O에 소모되는 시간이 GPU 사이클 400~600회 정도라고 하니 엄청난 시간 낭비를 하고 있었다고 볼 수 있다.
최적화 작업을 수행하며 커널 내부에서 직접 할당하여 사용되던 메모리를 전부 커널 외부에서 글로벌 메모리로 할당해두고 내부에서 사용하도록 수정하였다.
추가로 코드 중 메모리 할당/해제를 반복하는 구문이 있었는데 일반적인 생각으로 자원을 아낀다고 그렇게 구현한 것이 CUDA 커널 내부에서는 메모리 해제를 해도 재사용이 불가능해서 오히려 전체 메모리 사용량을 늘리기만 하였다.
글로벌 메모리로 한 번만 할당해서 계속 사용하도록 했더니 눈에 띄게 전체 메모리 사용량이 줄었다.
최적화 이전 최대 메모리 사용량: 약 12.4 GB
최적화 이후 최대 메모리 사용량: 약 4.3 GB

실행 시간

적용 전: 1시간 11분 59초
적용 후: 22분 22초
차이: 49분 37초
속도: 321.8%
예상대로 실행 시간이 크게 줄었다.

산출물

마찬가지로 무시할 수 있는 수준의 오차가 발생하였다.

병렬 연산이 필요한 범위 확장

기존 MPI 사용 시에는 병렬 연산의 범위를 FVBAR 알고리즘 핵심 코드에 한정할 수밖에 없었다.
즉, 알고리즘에 넣을 데이터의 범위를 구하고 해당 범위의 데이터를 잘라서 알고리즘에 투입하는 작업은 여전히 Python에서 싱글 쓰레드로 수행하고 있었다.
위와 같은 입출력 작업을 CUDA 커널 내부에서 수행할 수 있도록 코드 및 데이터 크기와 형식 등을 수정하였다.

실행 시간

적용 전: 22분 22초
적용 후: 10분 36초
차이: 11분 46초
속도: 211%

산출물

원본 값 범위(0.125 ~ 0.359)의 최대 약 22%에 해당하는 상당히 큰 오차가 발생한다. 디버깅이 필요해 보인다.
일단 입력 자료가 동일하게 읽히는 것은 확인하였고 커널 내부에서 오차가 발생하는 것으로 추정된다.
// 2019/11/25 추가
jday와 tu를 가져오는 부분에서 numpy float32 배열과 파이썬 리스트를 인자로 하는 numpy.append 함수의 결과가 numpy float64 배열인 것을 모르고
커널 내부에서 float형으로 이용하여 태양각을 제대로 구하지 못하는 문제가 있었다.
이를 해결하니 다음과 같이 무시할 수 있는 오차만 발생하였다.

요약

실행 시간

산출물

결과 이미지