04-28 14:02
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[Artificial Intelligence / h2o] R 딥러닝(II): 병렬 가속화를 이용하여 고성능 DNN 구현하기 본문

Artificial Intelligence/H2O

[Artificial Intelligence / h2o] R 딥러닝(II): 병렬 가속화를 이용하여 고성능 DNN 구현하기

cinema4dr12 2017. 2. 21. 23:37

이 글은 ParallelR의 R for Deep Learning (II): Achieve High-Performance DNN with Parallel Acceleration을 번역한 것입니다.


이전 포스트, R 딥러닝: 인공신경망 바닥부터 구현하기에서 신경망의 핵심 구성요소와 R에서 이것을 바닥부터 구현하는 방법에 대하여 알아보았다. 이제 R에서 구현안 것에 대한 연산 성능과 효율, 특히 멀티코어 CPU와 NVIDIA GPU 아키텍쳐에 대한 병렬 알고리즘에 관하여 집중적으로 알아보도록 하겠다.


성능 프로파일

이 글에서 성능 분석을 위해 작지만 큰 데이터세트인 MNIST를 활용할 것이다. MNIST는 머신러닝 분야에서 손으로 쓴 숫자의 분류에 대한 정확도를 측정하기 위해 자주 사용되며, Kaggle 대회에도 사용된다 (데이터 다운로드 페이지)Yann은 그의 웹페이지에서 다양한 머신러닝 알고리즘을 기반으로 분류 결과를 제공하고 있다.


Picture.1 MNIST 데이터세트의 손글씨 숫자


MNIST 데이터베이스에는 60,000개의 학습 이미지와 10,000개의 테스트 이미지가 저장되어 있다. 각 이미지는 28×28개의 포인트들로, 총 784 포인트들로 구성되어 있다. 이 글에서, 입력 피쳐로서 784개의 포인트를, 출력 클래스로 0-9의 숫자를 갖는 신경망을 학습시키고, 여러가지 개수의 Hidden Units (HU)의 2-레이어 네트워크에 대하여 작성한 R DNN 코드의 실행시간을 H2O Deep Learning으로 구현한 코드와 비교할 것이다.


R CODE:

# h2o
library(h2o)
# single thread
h2o.init()
 
 
train_file <- "https://h2o-public-test-data.s3.amazonaws.com/bigdata/laptop/mnist/train.csv.gz"
test_file <- "https://h2o-public-test-data.s3.amazonaws.com/bigdata/laptop/mnist/test.csv.gz"
 
train <- h2o.importFile(train_file)
test  <- h2o.importFile(test_file)
 
# To see a brief summary of the data, run the following command
summary(train)
summary(test)
 
y <- "C785"
x <- setdiff(names(train), y)
 
# We encode the response column as categorical for multinomial
#classification
train[,y] <- as.factor(train[,y])
test[,y]  <- as.factor(test[,y])
 
# Train a Deep Learning model and valid
system.time(
  model_cv <- h2o.deeplearning(x = x,
                               y = y,
                               training_frame = train,
                               distribution = "multinomial",
                               activation = "Rectifier",
                               hidden = c(32),
                               l1 = 1e-5,
                               epochs = 200)
)


아시다시피, H2O는 Java 백엔드로 구현된 R 플랫폼에서 가장 빠르고 가장 많이 사용되는 딥러닝 패키지이다. 그래서, H2O를 사용함에 있어 네이티브 R 코드 및 다른 성숙된 패키지와의 성능 차이를 아는 것은 중요하다. 아래 Bar Plot에서 보는 바와 같이, 32개, 64개, 128개의 Hidden Units를 200개의 스텝으로 테스트하였다.



분명히 R DNN은 H2O보다 꽤나 느리고 Hidden Units의 수가 증가함에 따라 실행시간도 급속히 증가한다. 보다 자세하게 실행시간을 파악하기 위해, R DNN 실행시간을 Rprof()summaryRprof()을 이용하여 각 함수호출로 나누어 최종 결과를 4개의 파트: total.time, total.pct, self.timeself.pct로 나누어 리포팅하였다. self.time self.pct 열(Column)은 각 함수에 대한 경과시간을 나타내는데, 내부에서의 함수 호출에 의한 시간은 제외된다. total.time  total.pct 열은 함수 호출에 사용되는 시간을 포함한 각 함수 실행에 대한 전체 경과시간이다[Aloysius Lim].

프로파일 결과로부터, 가장 시간이 많이 소요되는 함수는 행렬 곱셈을 나타내는 "%*%"이며 일반적으로 사람들은 이것을 GEMM (GEneral Matrix Multiplication)이라 부른다.

 

R CODE:

> Rprof()
> mnist.model <- train.dnn(x=1:784, y=785, traindata=train, hidden=64, maxit=200, display=50)
> Rprof(NULL)
> summaryRprof()
$by.self
                   self.time self.pct total.time total.pct
"%*%"                1250.08    90.19    1250.08     90.19
"t.default"            61.62     4.45      61.62      4.45
"pmax"                 24.40     1.76      28.42      2.05
"aperm.default"        11.60     0.84      11.60      0.84
"array"                10.36     0.75      10.36      0.75
"train.dnn"             9.74     0.70    1386.00    100.00
"<="                    5.72     0.41       5.72      0.41
"mostattributes<-"      4.02     0.29       4.02      0.29
"exp"                   3.60     0.26       3.60      0.26
"sweep"                 1.58     0.11     676.32     48.80
"is.data.frame"         1.28     0.09       1.28      0.09
"colSums"               0.86     0.06       0.86      0.06
"/"                     0.52     0.04       0.52      0.04
"rowSums"               0.36     0.03       0.36      0.03
"unname"                0.18     0.01       1.46      0.11
"-"                     0.04     0.00       0.04      0.00
"t"                     0.02     0.00      61.64      4.45
"sum"                   0.02     0.00       0.02      0.00
 
$by.total
                   total.time total.pct self.time self.pct
"train.dnn"           1386.00    100.00      9.74     0.70
"%*%"                 1250.08     90.19   1250.08    90.19
"sweep"                676.32     48.80      1.58     0.11
"t"                     61.64      4.45      0.02     0.00
"t.default"             61.62      4.45     61.62     4.45
"pmax"                  28.42      2.05     24.40     1.76
"aperm"                 21.96      1.58      0.00     0.00
"aperm.default"         11.60      0.84     11.60     0.84
"array"                 10.36      0.75     10.36     0.75
"<="                     5.72      0.41      5.72     0.41
"mostattributes<-"       4.02      0.29      4.02     0.29
"exp"                    3.60      0.26      3.60     0.26
"unname"                 1.46      0.11      0.18     0.01
"is.data.frame"          1.28      0.09      1.28     0.09
"data.matrix"            1.28      0.09      0.00     0.00
"colSums"                0.86      0.06      0.86     0.06
"/"                      0.52      0.04      0.52     0.04


병렬 가속화

상기 분석 결과로부터, 행렬 곱셉("%*%")은 신경망의 학습 단계에서 약 90%의 연산 시간을 차지하고 있음을 알 수 있다. 따라서, DNN 가속화의 핵심은 행렬 곱셈 속도를 높이는 것이다. 다행하게도, 이미 행렬 곱셈에 대한 다양한 병렬 라이브러리가 있으며 이들을 R에서 쉽게 활용이 가능하다. 이 글에서, 3개의 기본 선형대수 서브프로그램(BLAS) 라이브러리들인, openBLAS, Intel MKL, cuBLAS를 소개할 것이다. 앞의 두 라이브러리는 멀티-스레드 가속화 라이브러리이며 R과 함께 빌드되어야 한다 (이에 대한 설명은 여기여기를 참고 바란다.) 반면에, Linux 시스템에서는 시스템 네이티브한 Rblas.so를 R로 미리 불러올 수 있는 nvBLAS를 이용하여 NVIDIA cuBLAS를 R에 적용하는 것이 대체로 쉬운 편이다. 이러한 방법으로 별다른 프로그래밍 노력없이 NVIDIA의 GPU 성능을 R에서 활용할 수 있다.

아래의 그림은 R에서 활용되는 BLAS 라이브러리의 아키텍쳐를 보여주고 있다. 일반적으로 R은 모든 종류의 선형대수 함수(여기)를 다루는 Linux 시스템에서 R 고유의 BLAS, Rblas.so를 호출할 것이지만, 이것은 싱글-스레드 실행을 기반으로 한다. 선형대수 연산 속도를 높이는 트릭은 R 표준 BLAS를 멀티-스레드를 지원하는 라이브러리로 업데이트하는 것이다. R 개발자들에게 병렬 가속화를 위해 대부분의 경우 자신의 코드를 재작성할 필요없는 '공짜밥'이 존재한다.



아래의 코드에서 볼 수 있듯이, 32, 64, 128개의 뉴런을 갖는 2-레이어 신경망에 대하여 openBLAS,  Intel MKL, nvBLAS를 미리 R에서 빌드한 것을 테스트하였다. 아래의 Bar 차트로부터 R DNN의 실행시간은 급격하게 감소하였으며 (128개의 Hidden 뉴런 네트워크에 대하여 2816에서 365까지) H2O보다 거의 2 빠르고 오리지널 R 코드보다는 9배 빨라졌음을 알 수 있을 것이다.

# 현재 디텍터리에서 설정 파일 nvBLAS.conf을 생성해야 한다 (여기를 참고).

LD_PRELOAD=libnvblas.so /home/patricz/tools/R-3.2.0/bin/bin/R CMD BATCH MNIST_DNN.R

참고사항: 테스트 하드웨어 환경: Ivy Bridge E5-2690 v2 @ 3.00GHz, dual socket 10-core (total 20 cores), 128G RAM;   NVIDIA GPU K40m;  Software:  CUDA 7.5,  OpenBLAS 0.2.8,  Intel MKL 11.1 


최적화

지금까지는 멀티코어 CPU와 NVIDIA GPU 시스템에서 BLAS 라이브러리로부터 많은 성능 증가를 얻는 것처럼 보인다.

그러나 성능 최적화를 위해 우리가 더 할 수 있는 일이 없을까?

프로파일을 다시 들여다보고 성능을 제한하는 녀석들의 톱랭킹을 살펴보자. 아래의 표는 GEMM 성능이 10배 빠른 nvBLAS에 의한 가속화를 하는 R DNN 코드를 세부적으로 나눈 것이다 (원래의 1250초에서 114초까지). 그러나 가장 시간을 많이 잡아먹는 함수들인, "sweep()"와 "t()"는 실행시간의 27%(self.pct)를 차지한다. 따라서, 이들에 대해 좀 더 최적화를 할 필요가 있다.


질문:

'sweep'의 전체 시간은 87.28이지만 Self-time은 1.8 밖에 되지 않는데 진짜 연산 부분은 어디이며 이것이 최적화를 위한 합리적인 선택인가?


소스코드로부터 행렬 곱셈을 하기 전 행렬 변환을 위해 여러 함수가 t()를 호출하고 있음을 알 수 있다;

그러나, R은 이러한 연산을 위해 내부 함수인 'crossprod'와 'tcrossprod'를 이미 제공하고 있다.


R CODE:

# original: t() with matrix multiplication dW2 <- t(hidden.layer) %*% dscores dhidden <- dscores %*% t(W2)   # Opt1: use builtin function dW2 <- crossprod(hidden.layer, dscores) dhidden <- tcrossprod(dscores, W2)


두번째, Bias를 갖는 행렬 덧셈을 위해 'sweep()'가 실행된다. 그 대신, 아래의 코드에서 볼 수 있는 것처럼 Weight와 Bias를 결합하여 행렬 곱셈 연산을 할 수 있다. 그러나 메모리 압박을 증가시키는 행렬과 Bias의 조합을 위해 새로운 행렬을 생성해야 하는 단점이 있다.


R CODE:

 # Opt2: combine data and add 1 column for bias
 #  extra matrix for combinations
 X1   <- cbind(X, rep(1, nrow(X)))
 W1b1 <- rbind(W1, b1)
 W2b2 <- rbind(W2, b2)
 
 
 # Opt2: remove `sweep` 
 #hidden.layer <- sweep(X %*% W1 ,2, b1, '+')
 hidden.layer <- X1 %*% W1b1
 
 #score <- sweep(hidden.layer %*% W2, 2, b2, '+')
 hidden.layer1 <- cbind(hidden.layer, rep(1,nrow(hidden.layer)))
 score <- hidden.layer1 %*% W2b2


다시 결과들을 아래 표에 정리하였다. 't()'와 'sweep()'  함수를 제거 후 't.default'와 'aperm.default'의 연산 시간이 절약되었다.

전체적으로 성능은 다시 두 배가 되었다!


다른 질문:

최적화의 다음 단계에 대한 여러분의 의견은 무엇입니까? 좋은 아이디어가 있으면 제보 부탁 드립니다.


요약

본 포스팅에서, 멀티코어 CPU와 NVIDIA GPU 아키텍쳐에서 BLAS 라이브러리를 통해 R 코드를 가속화하는 병렬 화 기술을 소개하였다. 지금까지 비교적 작은 네트워크에 대하여 NVIDIA GPU 하에서 10배 이상 속도 증가와 20개의 스레드 H2O 패키지보다 2배 빠른 속도 증가를 얻었다; 그러나 여전히 개선할 수 있는 여지는 많으므로 여러가지 방법을 시도해 보길 권장한다.


마지막으로 Tesla K40m GPU에서 300개의 Hidden Units를 포함하고 있는 2-레이어 신경망 MNIST 데이터세트를 학습시켰으며, Yann은 보다 작은 Learning Rate ( lr=0.001) (일반적으로 Learning Rate가 작을수록 학습 시간을 증가시키만 보다 정확한 결과를 얻을 수 있다)로 4.7%의 오차율을 얻은 반면, 우리는 1시간에 94.7% 정확도(5.3% 오차율)를 얻을 수 있었다.

 

참고사항:
1. 본 포스팅에 실린 전체 소스코드는 여기에 있다
2. 본 포스팅의 PDF 버전


Comments