필자는 iMedisync 분석서버 개발을 담당하고 있으며, 지금으로부터 약 4년 전에 GPU를 활용하여 분석 성능 개선을 시도했었던 경험을 포스팅하려고 합니다.

현재 iMedisync의 생체신호 분석 기능은 모두 클라우드를 통해 서비스되고 있지만, 과거에는 on-premise 형태로 서비스되었습니다.
당시 on-premise 서버는 대략 아래와 같이 상당히 높은 스펙을 갖추었었지만, 무거운 생체신호 데이터와 복잡한 알고리즘으로 분석 시간을 꽤 잡아먹고 있었으며,
대부분 multi-processing을 통해 CPU bound task 들을 처리하다보니, 고 가용성은 꿈도 못꾸는 상황이었습니다.

약 4년전 iMedisync의 On-premise 서버

  • 2.4 GHz * 20 physical cores (+ 20 logical cores)
  • 224 Gbytes memory

당시엔 서버리스에 대한 개념은 커녕 클라우드 조차도 생소했었기에,
당장 지푸라기라도 잡는 심정으로 아래와 같이 서버에서 놀고있던 NVIDA GPU를 활용해 보기로 마음먹었습니다.

  • GTX 1080 Ti * 2

GPGPU

General-Purpose computing on Graphics Processing Units,
말 그대로 본래 그래픽 처리 목적으로 사용되는 GPU를 범용적으로 사용하는 것을 의미합니다.

GPU는 수많은 픽셀에 채워질 값들을 연산해야하는 목적에 걸맞게, 병렬처리에 최적화된 처리장치입니다.
CPU가 복잡하고 다양한 연산처리가 가능하고 높은 클럭을 가지는 소수의 core로 이루어진 반면, GPU는 매우 단순한 구조와 상대적으로 낮은 클럭의 core 다수로 구성되어 있습니다.
(최근에 출시되는 그래픽 카드 제품들의 사양이 점점 좋아지면서, GPU 클럭도 많이 올라간 것 같습니다.)

즉, 벡터/행렬 연산에 최적화되어 있다는 의미이고, 이는 그래픽 처리 외에 다른 고연산 작업에서도 CPU보다 월등한 성능을 가져갈 수 있음을 의미합니다.

수천만개의 사칙연산 문제를 최대한 빠른 시간안에 풀어야 하는데, 수학 박사 16명보다는 (사칙연산을 마스터했다는 가정하에) 2000~3000명의 일반인이 나누어 푸는게 더 효율적이고 빠를 것입니다.

CUDA

Compute Unified Device Architecture

NVIDIA에서 만든 병렬 컴퓨팅 플랫폼이자, GPGPU를 실현할 수 있게 해주는 기술입니다.
NVIDIA에서 일찌감치 GPU가 연산 전용 장치로써 각광받을 것이라고 예상하고 출시했습니다.
그래픽 프로그래밍을 모르는 개발자들도 CUDA만 설치하면, C/C++, fortran, python 등의 다양한 high level language를 이용하여 GPU를 벡터/행렬 연산 목적으로 사용하는 것이 가능해진 것입니다.
어떠한 표준을 기준으로 만들어지지 않고, NVIDA GPU(CUDA core가 적용된 모델) 및 driver 환경에서만 동작하도록 설계되어 있습니다.

적용 및 결과

iMediSync의 iSyncBrain(생체신호 분석 서비스)은 당시 fortran으로 작성된 오픈소스 소프트웨어 사용을 검토중이었고,
해당 프로그램은 특정 개수의 신호 데이터(double type)를 가지고 벡터/행렬 연산을 반복하는 로직을 포함하고 있었습니다.
원본 프로그램 자체도 컴파일 언어라는 특성 외에, MKL과 같은 선형대수 라이브러리와 병렬처리(CPU) 등을 활용해 연산 속도를 이미 최적화해 놓은 상태였지만,
그만큼 resource를 많이 필요로 했기에, 사용량이 조금이라도 몰리면 delay가 발생하는 문제는 여전했습니다.

(본 포스팅에서는 CUDA, driver 설치 과정 및 CUDA fortran 사용 방법 등의 내용은 다루지 않겠습니다. 솔직히 오래되서 기억도 잘 안납니다.)

소스코드에 로그를 심어 가장 시간이 오래걸리는 3개 구간을 선별해냈고, 당시에도 참고할 만한 여러 좋은 포스팅들이 있어, 우여곡절 끝에 CUDA 프로그래밍을 완성할 수 있었습니다.
당시 정리해 놓았던 문서를 참고하여, 아래와 같이 GPGPU 적용 전/후를 정리해 봤습니다.

Input : 19x40000 (double)

Processor number of thread on CPU time consumed
CPU 16 (CPU burst) 20 secs
CPU 1 (CPU burst) 1 min 50 secs
GPU 1 (I/O burst) 1 min 15 secs

single thread 기준으로 비교해 보면 약 46.6% 의 속도 개선율을 보였습니다.
원본 프로그램이 이미 벡터/행렬 연산에 최적화된 라이브러리를 사용하고 있었다는 점과,
CPU burst 상태가 아닌 I/O burst 상태라는 점을 감안하더라도 기대했던 것 만큼의 높은 개선율은 아니었습니다.

왜 그랬을까?

필자가 당시 CUDA를 이용해서 GPU로 옮겼던 로직은 대략 아래와 같은 중첩 루프 구조였습니다.

loop

loop

loop

Logic to GPU 1

Logic to GPU 2

Logic to GPU 3

루프가 반복되면서 GPU로 연산되도록 프로그래밍된 위 세 가지 로직은 아마도 대략 아래와 같은 과정을 반복적으로 하게 될 것입니다.

  1. Main memory 에서 GPU memory(global)로의 input 데이터 전송
  2. GPU는 다수의 스레드를 생성하여 멀티 코어에서 병렬 연산 수행
  3. GPU memory(global)에서 Main memory로의 output 데이터 전송

이 중 1번과 3번의 데이터 전송에 상당히 load가 걸리는 것을 확인할 수 있었습니다.
단순히 메인보드와 그래픽 카드를 이어주는 PCI 버스가 가지는 대역폭 크가나 다른 디바이스와 대역폭을 나눠가지는 전송상의 특징 때문인지는 모르겠으나,
이 데이터 전송 지연 이슈는 이미 GPGPU computing 분야에서 알려져 있는 것으로 보이며, 그런 면에서 어쩌면 필자는 최악의 GPGPU 설계를 했었는지도 모르겠습니다.

예상컨대, 상위 루프에서 GPU 커널 함수를 호출하도록 설계했으면 데이터 전송 지연을 최소화할 수 있었을 것입니다.
단, 전체 루프 안에 포함되는 로직, 변수가 상당히 많고 복잡하기 때문에 위와 같이 코드를 최적화하는 작업은 분명 굉장한 시간과 노력이 들것으로 예상됩니다.

결론

GPGPU 프로그래밍을 할 때에는 GPU 연산 과정에 대한 높은 이해도가 중요하며, 이는 결과물의 성능을 크게 좌우합니다.
위에서 언급한 전송 지연 외에도 아래와 같은 부분도 성능 개선에 매우 중요한 포인트로 언급되는 것 같습니다.

  • global 메모리 접근 최소화
  • SIMT(Single Instruction multiple threads) 모델의 특성에 따른 분기문 최적화

다음 GPGPU 관련 포스팅에는 위와 같은 내용을 모두 고려한 설계로, 보다 성공적인 사례로써 소개할 수 있기를 희망해 봅니다.