2017년 12월 27일 수요일

Deep Learning Inference & Serving Architecture 를 위한 실험 및 고찰 1 - GPU vs CPU

최근 Production을 위한 Deep Learning Serving 레이어의 아키텍처를 구성하기 위해 몇가지 실험을 했던 내용을 정리해 보았다. 몇번에 걸쳐 연재 될 수 있을 듯 한데...

우선 오늘의 주제는 Inference 에 있어서도 정말 GPU 가 CPU보다 유리한가? (결론이 유추 되는가???)


아키텍처를 구성함에 있어, 다양한 Use Case 가 있을 수는 있으나, 정답이 존재하지는 않는다고 생각한다. 다양한 비지니스로직, 데이타의 양이나 성격, 사용하는 기술들, 개발 언어, 목적하는 바, 동시접속자 수, On-premise , Cloud 여부 등에 따라 다양한 조합이 있을 수 있을 것이고, 각각이 장단점이 존재할 것이기 때문이다.

여러 참고 자료로 사전 조사를 해본것은 사실이지만, 우리의 Deep Learning 모델과 우리의 데이타, 우리의 로직으로 후보 아키텍처에서 직접 실험을 하고 아키텍처를 확정하고자 아래와 같은 실험을 진행하였다.

실험에 앞서, 주안점은 다음과 같다.

  1. 우리는 Keras , Tensorflow (일부 CNTK) 등의 Deep Learning Framework 로 만들어진 Deep Neural Network 모델을 이용, 대국민 서비스를 준비하고 있다.
  2. 다수의 동접이 있을 수 있고, 이벤트나 행사 여부, 광고 및 홍보 여부에 따라, 트래픽이 매우 가변적일 수 있고, 다수의 동접 및 다수의 inference 가 일어날 수 있다.
  3. 많은 동접의 경우에도 응답은 1초 이내를 목표로 한다.
  4. Deep Learning 모델은 RNN, LSTM 류와 간단한 류의 CNN 이 주를 이룬다.(우리의 모델은 vgg, inception 류의 heavy CNN  은 아니다.)
  5. 우리의 모델이 특이한 점은 주기적인 Fine Tuning 과 시시 각각의 RealTime on-line Training 이 운영중인 Model 에 시시각각 이루어진다는 점이다. 이는 Serving Layer 설계에 있어 중요 고려 사항 중 하나인데, Serving 되는 모델의 Size 를 줄이고 응답 속도를 빠르게 하기 위한 알려진 기법들을 적용하는데 방해가 되는 요소이기 때문이다. 
  6. Tensorflow 의 Serving 은 써본 사람들은 알겠지만, Web Service 로 만들어 배포하기에는 몇가지 제약이 느껴진다. 이 실험은 Tensorflow Serving 전용 엔진을 통한 실험은 아니며, 보다 High Level 로 접근하여 Inference Layer 를 직접 구현했을 때의 실험이다.
일반적으로 Inference 전용 Model Optimization 을 할 때는 주로 아래의 방법들이 사용된다.

  1. check point 등 training 에서만 사용되는 operation 을 없앤다.
  2. batch normalization ops 도 제거한다.
  3. 도달하지 않는 graph 영역 제거
  4. Check Numeric 제거
  5. 각종 variable 값 들은 constant 로 바꾸어, 크기를 줄이고 속도를 빠르게 하며, Thread Safe 하게 변경한다.
위 기법들은 Model 이 Freezing 된 경우에 한하여서이다. realtime training 과 inference 가 동시에 일어나는 경우는 
  1. training  model 과 inference  model 을 분리하고, 지연 동기화 시키거나
  2. 둘을 하나로 가져가되 constant 화 하고 freezing 하는 것을 포기해야 한다.
위 둘의 중간도 가능할 수는 있다. Node 가 복수개인 경우 Training  중인 Node 가 잠시 Serving Node 에서 빠져 있는 경우가 그 경우에 해당 할 것이다. 후에 우리는 Serving Layer 에 있어, Auto Scale Out 가능한 Docker PaaS나 Microservice Serverless PaaS 를 중요 고려 요소로 낙점하고 추가적인 실험을 하였는데, (아마 이 연재가 좀더 계속되어진다면, 다시 상세하게 다루어 보도록 하겠다. ) 이 시나리오 에서는 Training Layer 와 Serving Layer 를 각각의 장점을 극대화 시키고, Model 파일의 경우만 지연 공유시키는 또다른 시나리오가 나올 수 있다.


우선 오늘 다룰 내용은 위에서 언급된 다양한 방법론의 첫단추로서, 우리 모델이 과연 CPU 에서 더 잘 inference 되는 지, GPU 에서 더 잘 inference 되는지 의 여부에 대하여 실험 해본 결과이다. (ps. 실험에서 사용된 모델은 실험용으로 실제 모델이 다소 단순화된 General Model 임)
다수 동접 Inference 테스트 전 training 퍼포먼스 또한 실험한 결과를 함께 정리 하였다.

[1] 실험에 사용된 딥러닝 모델 및 모델 크기

- 일반적인 Language Model 용 LSTM 모델
- categorical_cross_entropy 사용
- Top 1 분류 모델
- Word Embedding Layer 차원 수 : 300차원
- Total Parameter 수 : 80,656,435 개

- [특이사항] Language Model 특성상 Total Parameter 에서 앞부분이 많은 부분 차지.
- [특이사항] LSTM 이 번역등의 문제가 아닌 Text Classification 문제에 적용된 경우 이므로, 층이 복잡하지는 않음. 층을 복잡하게 하여도 성능 향상 없었음. 그러나, 일반적인 word2vec + cnn 보다는 3% 정도 성능 향상된 모델임.


[2] 실험에 사용된 hyper parameter

- epochs=4 , batch_size=64 , optimizer=adam
- learning Rate=0.001, beta_1=0.9 , beta_2=0.999, epsilon=1e-8

- [특이사항] CPU Training 은 GPU 와 달리 batch size 를 4096등 훨씬 큰 수치를 주어도 memory resource 고갈 에러가 발생하지 않음, drop out 이나 batch bormalize 를 적절히 쓰는 경우 batch size 를 크게 주는 경우, 정확도는 비슷한데, 더 빨리 Training 이 될 수 있음.
- 즉, CPU vs GPU 트레이닝 퍼포먼스는 실무에서는 batch_size 를 달리 주어, CPU가 아래보다 더 빨리 응답하는 것도 가능하나, 동일 hyper parameter 값에 대한 성능 비교를 위해 아래에서는 동일 값으로 수행한 결과 이다.

[3] 실험에 동원된 HW 장비 Spec

- cpu : 12 vcore , 112gb Memory ( On Azure Cloud VM )
- gpu : K80 GPU * 2개 , 12 vcore , 112GB Memory ( On Azure Cloud Data Science GPU VM ) (단, 모델 inference 시에는 1개 GPU 만 사용하여 실험하였음)

[4] Training Data 크기

- training data row 수 : 1,649,415 건

[5] Training 속도 및 성능


  1. CPU Training Performance
    1. epoch1 : 23,790 초
    2. epoch2 : 24,071 초
    3. epoch3 : 24,026 초
    4. epoch4 : 24,100 초
  2. GPU Training Performance

    1. epoch1 : 8,612초
    2. epoch2 : 8,370초
    3. epoch3 : 8,377초
    4. epoch4 : 8,360초
[6] Single Inference 성능

      

예상과 달리 CPU가 Wall Time 이 더 빠르다. Wall Time 은 벽걸이 시계 시간을 의미한다. 즉 실제, 걸린 시간이다. CPU 의 경우 user time 은 150에 육박하는 경우가 있는데, 그래도 wall time 은 일정하게 30에서 40 사이의 값을 보여준다. 여기에서 다음과 같은 가정을 해볼 수 있다. CPU 는 멀티코어를 써서 더 빠른가??

[7] Multiple Sequential Inference 성능

이번에는 1개 Inference 가 아닌 1000번의 inference 를 sequencial  하게 (Not 병렬) 수행해보았다. 그리고, 위 (6)의 가정이 맞는지 cpu 및 gpu 의 usage 상황을 확인해 보았다.

사용된 코드도 아래에 참고로 넣어 보았다.

1000번을 연속하여 serial 하게 수행해보자 위와 같은 속도가 측정되었으며, 평균을 내 보면, CPU는 20ms , GPU는 60 ms 정도가 걸렸다.

특이한점은 , 처음 예상과 비슷하게, CPU는 멀티코어를 쓰고 있고,(코어 전체를 쓰진 않았음, 위 스크린샷 시점에는 420% 정도가 동작하고 있음.), GPU 는 GPU 1번 코어만을 50~70% 정도 사용하고 있으며, CPU 는 1개 CPU만 100% 사용하고 있다는 점 이었다.

여기에서 한가지 의문이 생겼다. 그렇다면 혹시 CPU가 병목인가?
그리고, 그렇다면, 혹시 위 코드에서 사용된 유일한 Pre Processing 인 Tokenizer 가 영향을 주고 있는 것인가?

그래서 위 실험에 사용된 코드에서 Tokenizer 전처리 Pre Processing 부를 1000번의 loop 바깥으로 빼보고 성능 향상 정도를 측정해 보았다.

[8] CPU  에서 Tokenizer 가 주는 영향


위 처럼 CPU 실험에서는 Pre-Processing 영역인 Tokenizer 가 주는 영향은 5% 정도에 지나지 않았다.

[9] GPU에서 Tokenizer 가 주는 영향


GPU에서도 마찬가지로 Pre-Processing 부분이 주는 영향은 2% 정도에 지나지 않았다. %는 줄었고, 절대치는 비슷하다. 즉, CPU에서이든 GPU에서이든 Pre-Processing 은 CPU 를 이용하기 때문에 절대치는 비슷한것이 이치에 맞다.

위 상황에서 CPU와 GPU  의 USAGE 도 확인해 보았다.



여전히 CPU 는 1Core 만 100% Full로 일하고 있고, GPU 는 50%에서 70%를 왔다갔다 하였다. 그러므로, CPU 는 전처리 때문이 아닐까 하는 가정은 False 라고 할 수 있을 것이다. 해당 가정에 대한 의문은 해소 되었다.

[10] 이제 실제 Inference 테스트를 위해 모델을 flask microservice 형태로 배포하였다. 


위 구동은 CPU 전용 머신과 GPU 전용 머신에서 각각 해 주었다.
실제 Production 에서는 성능 극대화를 위해 하나의 머신에 port 를 달리하여 여러 Process 를 띄우고 앞에 웹프록시 등을 두는 것이 일반적이지만, 이 실험은 상대적인 비교를 위함이 목적이므로, 그런 작업을 해주지는 않았다.

[11] 이제 실제 운영 Production 환경과 유사한 환경에서 동시접속수행 Test 를 해보겠다.

이곳에 따로 기재하진 않았으나, 앞선 시점에서의 비교는 Jupyter Notebook 에서 이루어졌다. 모두가 아는 바와 같이 Jupyter Notebook 위에서의 구동은 단일 Python 독립 프로세스보다 훨씬 느리다. 즉, 동일한 실험이 flask 위에서 구동된 경우 Jupyter 위에서 구동된 경우보다 훨씬 빠르다. 그리고, flask 는 경량의 비동기 웹 프레임워크 이기 때문에, 병렬 수행이 위의 경우보다 훨씬 빠르게 일어난다.

위에서는 Serial 하게 한번에 하나씩만 Job 을 수행하였다. 이번 flask 에서의 수행은 다수 User 에서의 Parallel 동접 수행이다. 때문에 Serial 하게 수행했을 때보다 더 많은 병렬 Job 이 수행되었고, 그 결과 Jupyter 에서의 serial 수행보다 성능이 훨씬 더 좋게 나오고 있다.

(1) CPU 에서의 최종 결과

[Run User]      [TPS]   [Time Per Request : millisec]   [Transfer rate: Kbytes/sec]     [Complete Request]      [Failed Request]
1       200.35  4.991   30.13   201     0
11      273.97  3.650   41.20   274     0
21      245.21  4.078   36.88   246     0
31      265.94  3.760   39.99   266     0
41      265.97  3.760   40.00   266     0
51      265.95  3.760   40.00   266     0
61      258.76  3.865   38.92   259     0
71      260.95  3.832   39.24   262     0
81      244.99  4.082   36.84   245     0
91      251.80  3.971   37.87   253     0

[Run User]      [TPS]   [Time Per Request : millisec]   [Transfer rate: Kbytes/sec]     [Complete Request]      [Failed Request]
300     274.39  3.644   41.27   275     0
310     233.33  4.286   35.09   234     0
320     173.71  5.757   26.12   174     0
330     133.75  7.476   20.12   134     0
340     142.73  7.006   21.46   143     0
350     130.86  7.642   19.68   131     0
360     128.93  7.756   19.39   129     0
370     280.45  3.566   42.18   281     0
380     267.97  3.732   40.30   268     0
390     278.43  3.592   41.87   279     0
400     200.84  4.979   30.20   201     0

Apach 오픈소스 부하테스트 도구를 이용하여 동시접속 수행 성능을 측정하였다.
하나의 단일 User 가 비동기로 여러개의 병렬 수행을 요청하며, 그러한 User 또한 1에서 최고 400까지 늘려가며 측정 하였다.

CPU는 TPS 가 270 정도씩 나와 주고 있다. 그 시점 flask  의 로그를 보면, 실제 초단위로 동일한 log 가 270여개 실제로 모두 200 응답으로 존재함을 확인 할 수 있다.




(2) GPU 에서의 최종 결과

[Run User]      [TPS]   [Time Per Request : millisec]   [Transfer rate: Kbytes/sec]     [Complete Request]      [Failed Request]
1       16.47   60.735  2.48    17      0
11      16.24   61.565  2.44    17      0
21      15.69   63.740  2.36    16      0
31      11.60   86.215  1.74    12      0
41      4.98    200.914         0.75    5       0
51                              0       0
61      17.80   56.186  2.68    18      0
71                              0       0
81      17.47   57.242  2.63    18      0
91                              0       0

[Run User]      [TPS]   [Time Per Request : millisec]   [Transfer rate: Kbytes/sec]     [Complete Request]      [Failed Request]
1       15.87   63.014  2.39    16      0
11      17.28   57.886  2.60    18      0
21      15.69   63.744  2.36    16      0
31      12.46   80.263  1.87    13      0
41      5.76    173.748         0.87    6       0
51                              0       0
61      17.33   57.706  2.61    18      0
71                              0       0
81      12.42   80.529  1.87    13      0
91                              0       0



GPU 는 TPS 가 16 정도밖에 되지 않는 저조한 결과를 보여 주었다.

[결론]

(ps. 동시수행 과정에서의 cpu , gpu 사용량은 serial 1000건 수행의 경우와 매우 유사했다.)

CPU 의 경우 특징은 느려질 지언정 400동접까지도 Fail Request 가 하나도 없었다는 점이다.
그리고 Serial 하게 1000개를 수행했을때, 개당 20 ms 이던것이 병렬로 수행했을때에는 약 5배정도 빠른 성능을 보여 주었다. 

하지만 GPU 의 경우는 동접자가 많아지면, Fail Request 가 매우 많아 졌고, TPS 자체도 CPU 에 비하여 매우 느렸다. Serial 수행에서 3배 느렸으나, 동접 수행은 거의 10배 이상 느렸다. CPU 코어가 여러개인것도 그렇지만, 메모리 등도 한몫을 했을 것으로 보여진다. GPU 의 경우는 serial 하게 측정했을때나 parallel 하게 측정했을때 모두 개당 응답 속도가 60ms  정도로 일정하다는 특징도 보여주었다. 즉, 동시 처리가 좀 매우 취약해 보인다. 동시 수행하는 속도 개선 효과가 거의 없었다.(CPU 와는 전혀 다른 양상이다.)

또한 GPU inference 의 경우 단일 CPU 가 GPU 한개와 함께 힘들게 일하고 있었던 양상 또한, 병렬 동시접속 능력을 떨어뜨리는 계기가 된듯 하다.
(이는 Tensorflow 에서의 실험 내용이다. CNTK  로도 동일한 모델을 수행할 수 있는데, 그 비교 결과는 다음 기회에 Posting 해보도록 하겠다.)

일단 CPU Inference 의 경우는, 메모리 사용량, CPU 사용량, 모두 저정도의 traffic 에 대하여는 안정적인 결과를 보여주는 것을 확인 할 수 있었다. GPU 의 경우는 Production 으로 대국민 서비스를 하기에 매우 위험한 수준이었다.

Developer 입장에서, 우리에게 CPU는 사실 GPU에 비하여 상대적으로 너무나 친숙하고 풍족한 리소스 이다. 게다가, 우리는 Docker , Microservice , Spark on Hadoop Yarn, Mesos  등 다양한 CPU + in-Memory 병렬 cluster 솔루션이 있다. 그리고 Public Cloud 에도 Auto Scale Out  이 가능한 Docker PaaS 나 Serverless MicroService PaaS 가 GPU 보다는 훨씬 저렴한 가격으로 구비되어 있다.

AI Serving Layer 는 이렇게 일단 우리의 시나리오와 우리의 모델에 있어서는 GPU 를 제끼고 고려할만 한 실험 결과가 나왔다. ( 물론 VGG, Inception 등 층이 깊은 Deep Learning 모델은 이것과 다른 양상이 나올 지도 모른다. TensorRT 등 Nvidia 는 Inference 전용 솔루션도 내놓은 것으로 알고 있다.) 

지금 담담하게 결론을 적고 있지만, 사실 실험결과가 이렇게 나왔을때 나는 흥분을 감추지 못했다. 하마터면 수억원짜리 GPU가 6~8개씩 꼳 힌 장비를 수대 구매하는 프로세스를 태울 뻔 했기도 했지만.... 무엇보다, 대용량 서비스와 다수 동접자를 위한 아키텍처에, 위 결론에서는 많은 솔루션과 오픈소스 그리고 public cloud 를 쓸 수 있는 풍부한 가능성이 생겼다는 점에서 안도의 한숨을 쉴 수 있었다.

PS. 위 실험 결과와 결론 유추 과정에서 제가 범한 논리적 오류가 있었다면 지적해주시기 바랍니다. 공유 이후 컴멘트는 곧 저에게는 배움이기도 하다는 자세로 공유를 실천 하고 있습니다.