05-03 00:00
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[Data Science / Posts] R에서 GPU를 활용하여 병렬 컴퓨팅하기 본문

Data Science/Posts

[Data Science / Posts] R에서 GPU를 활용하여 병렬 컴퓨팅하기

cinema4dr12 2017. 2. 6. 18:25


들어가기에 앞서...

GPU(그래픽 처리 장치)는 최근 많은 계산이 요구되는 작업을 해야 하는 경우에 대해 더욱 인기를 얻고 있다. 이러한 장점에도, R에서의 GPU의 사용은 매우 제한되어 있었다. 불가능한 것이 아님에도 저수준 인터페이스 작업에 익숙하지 않은 프로그래머들에게 OpenCL이나 CUDA는 어렵다. 복잡한 GPGPU 코드를 추상화하는 R의 고수준 프로그래밍에 대한 바인딩을 생성하는 것은 R 유저들에게 GPU를 쉽게 활용할 수 있는 길을 열어준다. 이에 대한 핵심 아이디어를 제공하는 것이 gpuR 패키지이다. gpuR은 다음 세 가지 기발한 측면이 있다:

  1. '모든' GPU에 대해 적용이 가능하다.
  2. CUDA/OpenCL을 추상화하여 기존의 R 알고리즘에 쉽게 통합할 수 있다.
  3. 객체가 GPU에서 지속 될 수 있도록 copy/compute 함수를 분리한다.


폭넓은 응용분야

'gpuR' 패키지는 GPU 디바이스를 보유하고 있는 R 유저가 GPU 컴퓨팅 파워를 활용도록 하기 위해 개발되었다. GPU 능력을 제공하는 몇몇 패키지들(gputoolscudaBayesregHiPLARMHiPLARb, gmatrix 등)이 있지만 이들은 모두 NIVIDA GPU에 국한되어 있다. 이처럼 OpenCL에 기반한 백엔드(Backend)는 모든 유저들이 GPU 하드웨어의 혜택을 누릴 수 있도록 한다. 따라서, 'gpuR' 패키지는 GPU를 활용할 수 있는 자동튜닝 된 OpenCL 커널을 보유한 ViennaCL 선형 대수 라이브러리를 이용한다. 헤더는 RViennaCL 패키지에 간편하게 리패키지(Repackage)되어 있다. 성능을 좀 더 개선할 수 있는 NVIDIA GPU를 장착한 CUDA 백엔드도 지원한다 (gpuRcuda 패키지에는 포함되어 있으나 아직 정식 릴리즈 되지 않았다).


추상적 GPU 모드

gpuR 패키지는 모든 유저들이 R에서 간단하게 matrix 또는 vector를 캐스팅(Cast)하여 프로그래밍에 집중할 수 있도록 하는 명시적 클래스와 메써드를 포함하는 S4 객체 지향 시스템을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
library(gpuR)
ORDER = 1024
 
A = matrix(rnorm(ORDER^2), nrow=ORDER)
B = matrix(rnorm(ORDER^2), nrow=ORDER)
gpuA = gpuMatrix(A, type="double")
gpuB = gpuMatrix(B, type="double")
 
C = A %*% B
gpuC = gpuA %*% gpuB
 
all.equal(C, gpuC[])
[1] TRUE


matrix 객체는 원하는 함수가 호출될 때 GPU가 계산하는 RAM 내 행렬을 가리킨다. 이것은 객체의 메모리를 복사하는 R의 습관을 방지한다. 예를 들어:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
library(pryr)
 
# Initially points to same object
x = matrix(rnorm(16), 4)
y = x
 
address(x)
[1] "0x16177f28"
 
address(y)
[1] "0x16177f28"
 
# But once modify the second object it creates a copy
y[1,1] = 0
 
address(x)
[1] "0x16177f28"
 
address(y)
[1] "0x15fbb1d8"


반면, gpuMatrix의 동일한 문법은 복사하지 않고 오리지널 객체를 변경할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
library(pryr)
library(gpuR)
 
# Initially points to same object
x = gpuMatrix(rnorm(16), 4, 4)
y = x
 
x@address
[1] <pointer: 0x6baa040>
 
y@address
[1] <pointer: 0x6baa040>
 
# Modification affects both objects without copy
y[1,1] = 0
 
x@address
[1] <pointer: 0x6baa040>
 
y@address
[1] <pointer: 0x6baa040>

 

이 객체에 할당된 각각의 새로운 변수는 포인터만 복사하게 되는데 이는 프로그램의 메모리를 효율적으로 만든다. 그러나, gpuMatrix 클래스는 여전히 GPU 메모리 할당 및 각 함수 호출에 대한 디바이스로의 데이터 복사를 요구한다. 가장 많이 사용되는 메써드들은 %*%, +, -, *, /, crossprod, tcrossprod, trig 등과 같이 오버로딩(Overloading)되어 왔다. 이런 식으로, R 유저는 기존의 알고리즘을 파괴할 수도 있는 수많은 함수를 일일이 알 필요없이 객체들을 생성하고 GPU 리소스를 활용할 수 있다.


구별된 COPY/COMPUTE 기능:

gpuMatix와 gpuVector 클래스에 대하여 GPU RAM에 상주하는 객체를 가리키는 동반자 클래스인 vclMatrix와 vclVector 클래스가 있다. 이와 같이, 유저는 데이터가 호스트(Host)로 복귀해야 하는 시기를 명시할 수 있다. 호스와 디바이스 간에 불필요한 데이터 전송을 피함으로써 성능을 현저히 끌어올릴 수 있다. 예를 들어,

1
2
3
4
5
6
7
8
9
vclA = vclMatrix(rnorm(10000), nrow = 100)
vclB = vclMatrix(rnorm(10000), nrow = 100)
vclC = vclMatrix(rnorm(10000), nrow = 100)
 
# GEMM
vclD = vclA %*% vclB
 
# Element-wise addition
vclD = vclD + vclC


이 코드에서, 처음 세 개의 행렬은 이미 GPU 메모리에 존재하여 데이터 전송이 GEMM 호출에서 일어난다. 또한, 반환된 행렬은 GPU 메모리에 남아있다. 이 경우에서 vclD’ 오브젝트는 여전히 GPU RAM에 상주한다. 이와 같이, 요소 별 덧셈 호출 또한 데이터 전송없이 직접 GPU에서 일어난다. 유저가 일반적인 R 행렬과 똑같은 문법으로 행렬의 요소, 행, 열도 변경할 수 있다는 것을 유념하자.

1
2
3
vclD[1,1] = 42
vclD[,2] = rep(12, 100)
vclD[3,] = rep(23, 100)


이 연산들은 단순히 GPU로 새 요소들을 복사하고 GPU 메모리 내에서 객체를 변경한다. vclD 객체는 호스트로 절대 복사되지 않는다.


벤치마킹

모든 것을 염두에 둘 때 gpuR의 성능은 어떠할까? 다음은 유명한 GEMM 연산의 일반적인 벤칭마킹이다. 이 글의 시뮬레이션에 대하여 단일 NVIDIA GeForce GTX 970에만 접속하였다. 유저들은 여러가지 고성능 GPU(가령, AMD FirePro, NVIDIA Tesla, 등등)에 대한 차이를 보고 싶을런지도 모르겠다. CPU에 대한 상대적 속도 향상 또한 유저의 하드웨어 따라 다르다.


DEFAULT DGEMM VS BASE R

R은 두 개의 숫자 타입(정수와 더블)만을 지원한다. 그림 1.은 gpuMatrix와 vclMatrix 클래스를 사용하여 얻은 (X배) 속도 향상이다. R은 이미 가장 빠른 언어가 아닌 것으로 알려져 있기 때문에 OpenBLAS 백엔드로 실행한 것은 4 코어 Intel i5-2500 CPU @ 3.30GHz를 이용한 레퍼런스로 포함되었다. 보시다시피 OpenBLAS나 gpuMatrix 클래스를 이용하면 극적인 속도 향상이 있다. 흥미로운 것 중 하나는, 대다수의 GPU 실행에 있어 전형적인 요소인 호스트-디바이스-호스트 간의 데이터 전송 속도에 큰 임팩트가 있다. 행렬 크기를 계속 변화시키는 vclMatrix 클래스를 이용하여 전송에 대한 비용이 제거된다.


그림 1. gpuR에서 OpenBLAS (CPU), gpuMatrix/vclMatrix (GPU)를 이용하여 얻은 (X배) 속도 향상.


SGEMM VS BASE R 

다수의 GPU 벤치마킹에서 부동소수 연산 속도 측정이 빈번하다. 위에서 언급한 바와 같이, R은 기본으로 이를 지원하지 않는다. 이 문제를 해결하는 한 가지 방법은 RcppArmadillo or RcppEigen 패키지를 이용하여 R 객체를 명시적으로 부동소수 타입으로 캐스팅하는 것이다. armadillo 라이브러리도 기본적으로 BLAS 백엔드(가령, OpenBLAS)를 사용하도록 설정되어 있을 것이다. gpuR을 이용하여 행렬 호출(가령, vclMatrix(mat, type = "float"))에서 type = "float"를 설정하여 부동소수 타입을 실행할 수 있다. 그림 2는 부동소수 데이터 타입에 대한 영향을 보여준다. OpenBLAS는 지속적으로 주목할 만한 속도 향상을 보여주지만 gpuMatrix는 일단 행렬 차수가 1500을 초과해야 성능 효과가 나타난다. vclMatrix는 값을 GPU 메모리 내 객체를 유지하고 메모리 전송을 피하는 것을 보여준다.

 

그림 2. 부동소수 타입 GEMM 비교. (RcppArmadillo를 통한)openBLAS와 gpuR이 제공하는 gpuMatrix/vclMatrix (GPU)를  이용하여 얻은 (X배) 속도 향상.


gpuMatrix 및 vclMatrix 에 의해 얻은 성능이 추가적으로 제공하는 관점은 OpenBLAS 성능에 대한 직접 비교이다. vclMatrix는 OpenBLAS에 비해 100배 이상의 속도 향상을 보여주는 반면 gpuMatrix는 최대 2-3배의 속도 향상을 보여 준다. vclMatrix의 성능은 왜 그렇게 빠른지 궁금할 것이다 (단지 호스트-디바이스-호스트 전송 밖에 차이가 없는데도 말이다). BUS 전송 속도의 제한 하에 gpuMatrix의 좀 더 나은 성능 개선을 할 수 있는지 조사해 보아야 한다. NVIDIA의 NVLink와 같이 향상된 하드웨어 기능으로 성능을 확실히 개선할 수 있을 것이다.


NVIDIA NVLink



그림 3. OpenBLAS (RcppArmadillo) 부동소수 타입 GEMM vs gpuR이 제공하는 gpuMatrix/vclMatrix (GPU) 클래스에 의해 얻은 속도 비교.


결론

gpuR 패키지는 가능한 많은 R 유저들이 GPU 컴퓨팅을 할 수 있도록 개발되었다. gpuR을 사용하는 목적은 현재와 장래의 알고리즘에 GPU 가속에 대한 혜택을 보다 쉽게 부여하고자 함이다. gpuR 패키지는 현재 CRAN에서 다운로드 할 수 있다. Github를 방문하면 기존의 이슈와 Wiki 페이지(설치에 대한 기본적인 도움 포함)를 포함한 개발 버전을 얻을 수 있다. 향후 개발은 솔버(가령, QR, SVD, Cholesky 등), 다중 GPU를 이용한 스케일링, 'sparse' 클래스 객체 및 커스텀 OpenCL 커널을 포함한다. 위에 언급한 바와 같이, 이 패키지는 다양한 하드웨어와 OS 환경을 지원하고자 한다 (Windows, Mac, 다양한 Linux에서 테스트하였다). 다양한 하드웨어 사용에는 한계가 있다 (가격이 비싸기 때문에 모든 GPU를 테스트 할 수 없다). 그래서 gpuR 개발은 R 유저 커뮤니티에 의존하고 있다. 다양한 하드웨어를 보유한 지원자를 언제나 환영하며 발견된 버그들에 대한 이슈를 보고해 주기르 바란다. 사용자들이 다양한 하드웨어에 대한 성공적 사용법을 보고할 수 있도록 Gitter 계정을 만들었다. gpuR에 대한 제안과 일반적인 대화를 환영한다.

Comments