레이블이 caffe-ibm인 게시물을 표시합니다. 모든 게시물 표시
레이블이 caffe-ibm인 게시물을 표시합니다. 모든 게시물 표시

2018년 1월 24일 수요일

infiniband를 이용한 caffe DDL에서의 색다른 error와 그 해결책


caffe-ibm이 자랑하는 기능 중 하나인 DDL (Distributed Deep Learning)은 여러대의 GPU 서버에 들어있는 GPU들을 OpenMPI로 연결하여 하나의 큰 모델을 training할 수 있도록 해주는 기능입니다.   당연히 여러대의 GPU 서버를 연결하는 network의 latency와 bandwidth에 큰 영향을 받습니다.  여기서는 minsky1과 minsky2라는 hostname의 서버에, IP over Infiniband를 구성하고, 그 interface에 각각 ib1과 ib2라는 IP name을 /etc/hosts에 등록하여 별도의 고속 private network을 구성하여 DDL을 해봤습니다.

이 경우 4-GPU 서버가 2대이고 하나의 network으로 물려있으므로 그 topology를 알려주는 rank file은 아래와 같이 하면 됩니다.

$ cat 4x2x1.rf 

rank 0=ib1         slot=0:0-3
rank 2=ib1         slot=0:4-7
rank 4=ib1         slot=1:0-3
rank 6=ib1         slot=1:4-7



rank 1=ib2         slot=0:0-3
rank 3=ib2         slot=0:4-7
rank 5=ib2         slot=1:0-3
rank 7=ib2         slot=1:4-7


그런데, 막상 돌려보니 아래와 같이 error 메시지가 나옵니다. 

$ mpirun -x PATH -x LD_LIBRARY_PATH -n 8 -rf 4x2x1.rf caffe train --solver=solver.prototxt -gpu 0 -ddl "-mode b:4x1x1 -dev_sync 1"
--------------------------------------------------------------------------
Failed to create a completion queue (CQ):
Hostname: minsky1
Requested CQE: 16384
Error:    Cannot allocate memory
Check the CQE attribute.
--------------------------------------------------------------------------
--------------------------------------------------------------------------
Open MPI has detected that there are UD-capable Verbs devices on your
system, but none of them were able to be setup properly.  This may
indicate a problem on this system.
You job will continue, but Open MPI will ignore the "ud" oob component
in this run.
Hostname: minsky1
--------------------------------------------------------------------------
--------------------------------------------------------------------------
Failed to create a completion queue (CQ):
Hostname: minsky2
Requested CQE: 16384
Error:    Cannot allocate memory
Check the CQE attribute.
--------------------------------------------------------------------------
--------------------------------------------------------------------------
Open MPI has detected that there are UD-capable Verbs devices on your
system, but none of them were able to be setup properly.  This may
indicate a problem on this system.
You job will continue, but Open MPI will ignore the "ud" oob component
in this run.
Hostname: minsky2
--------------------------------------------------------------------------
--------------------------------------------------------------------------
The rankfile that was used claimed that a host was either not
allocated or oversubscribed its slots.  Please review your rank-slot
assignments and your host allocation to ensure a proper match.  Also,
some systems may require using full hostnames, such as
"host1.example.com" (instead of just plain "host1").
  Host: minsky1
--------------------------------------------------------------------------

이건 2가지 error입니다.  하나는 "Cannot allocate memory"이고, 다른 하나는 "either not
allocated or oversubscribed its slots" 인데, 각각 다른 원인에 의한 것입니다.


1)  "Cannot allocate memory"  error

이건 limits 값 때문입니다.  다음과 같이 ulimit 값을 보면 max locked memory가 기본으로는 64로 되어 있습니다.  

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15880
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15880
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

이걸 풀어주기 위해서는 아래와 같이 limits.conf의 맨 끝에 해당 user에 대한 limit를 풀어준 뒤, 반드시 re-login을 하셔야 합니다.   이걸로 해결됩니다.

$ sudo vi /etc/security/limits.conf
...
user1   soft    memlock   -1
user1   hard    memlock   -1


2) "either not allocated or oversubscribed its slots"  error

이건 정말 제가 예상 못 했던 것인데, 구글링을 해보니 뜻 밖에도 mpirun 등의 MPI command는 IP name이 아니라 hostname에 민감한 것 같습니다.  즉, 저 GPU 서버들의 hostname이자 ethernet interface의 IP name이 minsky1 (10.1.1.1), minsky2 (10.1.1.2)이고, infiniband interface의 IP name이 ib1 (9.1.1.1), ib2 (9.1.1.2)인데, 이렇게 hostname과 rank file 안에 들어가는 IP name이 각각 다르면 안되나 봅니다.

이 경우 다음과 같이 두 서버의 hostname을 IB interface의 이름인 ib1, ib2로 각각 바꿔주면 해결이 됩니다.

$ hostnamectl set-hostname ib1
$ hostnamectl set-hostname ib2


2017년 10월 27일 금요일

caffe-ibm의 LMS 기능에 대한 설명

전에 올린 DDL 관련 포스팅(https://hwengineer.blogspot.kr/2017/10/caffe-ddl-alexnet-training.html)에서, LMS(large model support) 기능을 믿고 batch_size를 화끈하게 2048이나 4096으로 올리면 어떤가라는 질문이 있을 수 있습니다.  결론부터 말씀드리면 LMS를 쓴다고 해도 batch_size를 무한정 키울 수는 없습니다.

먼저 저 포스팅에 나와 있듯이, LMS 기능은 '기존 GPU memory 한계 때문에 돌릴 수 없었던 큰 모델도 돌릴 수 있게 해주는 기능'이지, 이 때문에 반드시 더 빠른 성능을 낼 수 있는 것은 아닙니다.  아무리 NVLink를 통해 가져온다고 해도, host server memory가 GPU memory보다는 느리니까요.

그와는 별도로, LMS도 무한정 host memory를 쓸 수는 없습니다.  Lab에서 들은 이야기입니다만, LMS를 쓴다고 해도 아래 정보들은 반드시 GPU memory 상에 올라가야 한다고 합니다.

- input tensor
- output tensor
- weight
- gradient (training인 경우)

그리고 이 정보들이 차지하는 memory의 양은 batch_size가 늘어날 수록 함께 늘어나는데, 그로 인해 결국 한계가 있습니다.  LMS의 핵심은, deep learning에서 layer별로 training을 할 때, 당장 처리하고 있는 layer는 GPU memory 위에 두더라도, 이미 처리했거나 나중에 처리할 layer들은 host memory에 저장할 수 있다는 것입니다.  따라서, LMS로 처리가능한 최대 neural network 크기는 그 neural network에서 가장 큰 layer의 크기에 달려 있다고 할 수 있습니다.



가령 전에 테스트했던, Alexnet의 deploy.prototxt 이용해서 'caffe time'을 수행할 때 보면 아래와 같이 data, conv1, relu1 등 총 24개의 layer가 만들어집니다.

$ grep "Creating Layer" caffe_time.log

I1025 16:37:01.848961 29514 net.cpp:90] Creating Layer data
I1025 16:37:01.867213 29514 net.cpp:90] Creating Layer conv1

I1025 16:37:03.477823 29514 net.cpp:90] Creating Layer fc8
I1025 16:37:03.481210 29514 net.cpp:90] Creating Layer prob

그리고 각 layer마다 다음과 같이 Top shape가 정해지면서 "Memory required for data"가 연산됩니다.  그리고 그 값은 처음 layer에서는 작아도 나중에는 매우 커지지요.


I1025 16:37:01.867137 29514 net.cpp:135] Top shape: 10 3 1600 1200 (57600000)
I1025 16:37:01.867183 29514 net.cpp:143] Memory required for data: 230400000

I1025 16:37:02.231456 29514 net.cpp:135] Top shape: 10 96 398 298 (113859840)
I1025 16:37:02.231468 29514 net.cpp:143] Memory required for data: 685839360

I1025 16:37:02.246103 29514 net.cpp:135] Top shape: 10 256 49 37 (4641280)
I1025 16:37:02.246112 29514 net.cpp:143] Memory required for data: 3315185920


이런 특성들로 인해, wide한 (layer별 메모리 필요량이 많은) neural network보다는 deep한 (layer 개수가 많은) neural network이 LMS의 잇점을 훨씬 더 잘 살릴 수 있습니다.



LMS의 진정한 장점을 정리하면 아래와 같습니다.

1) width로는 10-30배 정도 더 큰 모델 사용 가능
2) depth로는 무한대의 큰 모델을 사용 가능 

특성상 layer들이 많은 RNN에서 LMS가 특히 유용하게 쓰일 수 있다고 합니다.   그리고 요즘 신경망 발전 방향이 wide해지는 방향이 아니라 점점 deep해지는 방향이라고 합니다.  가령 몇년 전에 나온 Alexnet 같은 경우 이론상 7개 layer라고 하는데, 그로부터 4년 뒤에 나온 ResNet 같은 경우 1000개 layer라고 하지요.  LMS의 적용 범위는 점점 넓어지고 있다고 할 수 있습니다. 

caffe DDL을 이용한 Alexnet training

지난 9월 포스팅(http://hwengineer.blogspot.kr/2017/09/ibm-powerai-40-caffe-distributed-deep.html)에서 PowerAI에 포함된 DDL(Distributed Deep Learning), 즉 MPI를 이용한 분산처리 기능에 대해 간단히 설명드린 바 있습니다.  이번에는 그것으로 ILSVRC2012의 128만장 image dataset을 caffe alexnet으로 training 해보겠습니다.

제가 잠깐 빌릴 수 있는 Minsky 서버가 딱 1대 뿐이라, 원래 여러대의 Minsky 서버를 묶어서 하나의 model을 train시킬 수 있지만 여기서는 1대의 서버에서 caffe DDL을 수행해보겠습니다.  잠깐, 1대라고요 ?  1대에서 MPI 분산처리가 의미가 있나요 ?

예, 없지는 않습니다.  Multi-GPU를 이용한 training을 할 때 일반 caffe와 caffe DDL의 차이는 multi-thread냐, multi-process냐의 차이입니다.  좀더 쉽게 말해, 일반 caffe에서는 하나의 caffe process가 P2P를 통해 여러개의 GPU를 사용합니다.  그에 비해, caffe DDL에서는 GPU당 1개씩 별도의 caffe process가 떠서, 서로간에 MPI를 이용한 통신을 하며 여러개의 GPU를 사용합니다.

이를 그림으로 표현하면 아래와 같습니다.



실제로, 일반 caffe를 사용할 경우 nvidia-smi로 관찰해보면 다음과 같이 caffe의 PID가 모두 같지만, caffe DDL에서는 각 GPU를 사용하는 caffe PID가 서로 다릅니다.

일반 caffe :

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
|    0     30681    C   caffe                                        15497MiB |
|    1     30681    C   caffe                                        14589MiB |
|    2     30681    C   caffe                                        14589MiB |
|    3     30681    C   caffe                                        14589MiB |
+-----------------------------------------------------------------------------+

caffe DDL :

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
|    0     31227    C   caffe                                        15741MiB |
|    1     31228    C   caffe                                        14837MiB |
|    2     31229    C   caffe                                        14837MiB |
|    3     31230    C   caffe                                        14837MiB |
+-----------------------------------------------------------------------------+

자, 대략 차이를 이해하셨으면, alexnet training을 한번은 일반 caffe로, 또 한번은 caffe DDL로 training해보시지요.  물론 모두 같은 Minsky 서버, 즉 4-GPU 시스템 1대를 써서 테스트한 것입니다.  각각의 성능 측정은 128만장을 2-epochs, 즉 2회 반복 training할 때 걸린 시간으로 측정하겠습니다.

일반 caffe :

test@ubuntu:/nvme$ caffe train --solver=models/bvlc_alexnet/solver.prototxt -gpu all

caffe DDL :

test@ubuntu:/nvme$ mpirun -x PATH -x LD_LIBRARY_PATH -n 4 -rf 4x1x1.rf caffe train --solver=models/bvlc_alexnet/solver.prototxt -gpu 0 -ddl "-mode n:4x1x1 -dev_sync 1"

지난번에 잠깐 설명드린 것을 반복하자면 이렇습니다.

- mpirun은 여러대의 서버 노드에 동일한 명령을 동일한 환경변수 (-x 옵션)을 써서 수행해주는 병렬환경 명령어입니다.
- 4x1x1.rf라는 이름의 파일은 rank file입니다.  이 속에 병렬 서버 환경의 toplogy가 들어있습니다.
- -n 4라는 것은 MPI client의 총 숫자이며, 쉽게 말해 training에 이용하려는 GPU의 갯수입니다.
- -gpu 0에서, 왜 4개가 아니라 gpu 0이라고 1개로 지정했는지 의아하실 수 있는데, MPI 환경에서는 각각의 GPU가 하나의 learner가 됩니다.  따라서 실제 물리적 서버 1대에 GPU가 몇 장 장착되어있든 상관없이 모두 -gpu 0, 즉 GPU는 1개로 지정한 것입니다.
- "-mode b:4x1x1"에서 b라는 것은 가능하면 enhanced NCCL을 이용하라는 뜻입니다.  4x1x1은 4장의 GPU를 가진 서버 1대가 하나의 rack에 들어있다는 뜻입니다.
- dev_sync에서 0은 GPU간 sync를 하지 말라는 것이고, 1은 통신 시작할 때 sync하라는 뜻, 2는 시작할 때와 끝낼 때 각각 sync하라는 뜻입니다.

여기서 사용된 rank file 4x1x1.rf 속의 내용은 아래와 같습니다.

rank 0=minksy           slot=0:0-3
rank 1=minksy           slot=0:4-7
rank 2=minksy           slot=1:0-3
rank 3=minksy           slot=1:4-7

128만장 x 2-epochs를 처리하기 위해서는, solver.prototxt와 train_val.prototxt 속에 표시된 batch_size와 max_iter의 곱이 128만장 x 2 = 256만장이면 됩니다.  batch_size를 조절함에 따라 training 속도가 꽤 달라지는데, 여기서는 256부터 512, 768 순으로 늘려가며 테스트해보겠습니다.



위 표에서 보시다시피, batch_size가 작을 때는 일반 caffe의 성능이 더 빨랐는데, batch_size가 점점 커지면서 caffe DDL의 성능이 점점 더 빨라져서 결국 역전하게 됩니다.  batch_size와 MPI를 이용한 DDL의 성능과의 상관 관계가 있을 것 같기는 한데, 아직 그 이유는 파악을 못 했습니다.  Lab에 문의해봤는데, batch_size와는 무관할 것이라는 답변을 받긴 했습니다.


여기서 batch_size를 1024보다 더 키우면 어떻게 될까요 ?

...
F1025 17:48:43.059572 30265 syncedmem.cpp:651] Check failed: error == cudaSuccess (2 vs. 0)  out of memoryF1025 17:48:43.071281 30285 syncedmem.cpp:651] Check failed: error == cudaSuccess (2 vs. 0)  out of memory
*** Check failure stack trace: ***
    @     0x3fffb645ce0c  google::LogMessage::Fail()
    @     0x3fffb69649cc  caffe::Solver<>::Step()
    @     0x3fffb645f284  google::LogMessage::SendToLog()
...

일반 caffe든 caffe DDL이든 batch_size가 1100만 되어도 이렇게 out-of-memory (OOM) error를 내며 죽어버립니다.  그러니 아쉽게도 더 큰 batch_size에서는 테스트가 안되는 것이지요.

batch_size가 너무 커서 OOM error가 난다면 그걸 또 피해가는 방법이 있습니다.  역시 caffe-ibm에 포함된 LMS(large model support)입니다.   아래와 같이 -lms 옵션을 주면 caffe나 caffe DDL이나 모두 batch_size=1200 정도까지는 무난히 돌릴 수 있습니다.  -lms 800000이라는 것은 800000KB 이상의 memory chunk는 GPU 말고 CPU에 남겨두라는 뜻입니다.   (http://hwengineer.blogspot.kr/2017/09/inference-gpu-sizing-ibm-caffe-large.html 참조)


일반 caffe with LMS :

test@ubuntu:/nvme$ caffe train -lms 800000 --solver=models/bvlc_alexnet/solver.prototxt -gpu all

caffe DDL with LMS :

test@ubuntu:/nvme$ mpirun -x PATH -x LD_LIBRARY_PATH -n 4 -rf 4x1x1.rf caffe train -lms 800000 --solver=models/bvlc_alexnet/solver.prototxt -gpu 0 -ddl "-mode n:4x1x1 -dev_sync 1"


그 결과는 아래와 같습니다.   확실히 batch_size가 커질 수록 일반 caffe보다 caffe DDL의 성능이 더 잘 나옵니다.




궁금해하실 분들을 위해서, caffe DDL을 수행할 경우 나오는 메시지의 앞부분과 뒷부분 일부를 아래에 붙여놓습니다.

--------------------------------------------------------------------------
[[31653,1],2]: A high-performance Open MPI point-to-point messaging module
was unable to find any relevant network interfaces:

Module: OpenFabrics (openib)
  Host: minsky

Another transport will be used instead, although this may result in
lower performance.
--------------------------------------------------------------------------
ubuntu: n0(0) n1(0) n2(0) n3(0) 
I1025 18:59:45.681555 31227 caffe.cpp:151] [MPI:0   ] spreading GPUs per MPI rank
I1025 18:59:45.681725 31227 caffe.cpp:153] [MPI:0   ]    use gpu[0]
I1025 18:59:45.681541 31228 caffe.cpp:151] [MPI:1   ] spreading GPUs per MPI rank
I1025 18:59:45.681725 31228 caffe.cpp:153] [MPI:1   ]    use gpu[1]
I1025 18:59:45.681541 31229 caffe.cpp:151] [MPI:2   ] spreading GPUs per MPI rank
I1025 18:59:45.681726 31229 caffe.cpp:153] [MPI:2   ]    use gpu[2]
I1025 18:59:45.681735 31229 caffe.cpp:283] Using GPUs 2
I1025 18:59:45.681541 31230 caffe.cpp:151] [MPI:3   ] spreading GPUs per MPI rank
I1025 18:59:45.681726 31230 caffe.cpp:153] [MPI:3   ]    use gpu[3]
I1025 18:59:45.681735 31230 caffe.cpp:283] Using GPUs 3
I1025 18:59:45.681735 31227 caffe.cpp:283] Using GPUs 0
I1025 18:59:45.681733 31228 caffe.cpp:283] Using GPUs 1
I1025 18:59:45.683846 31228 caffe.cpp:288] GPU 1: Tesla P100-SXM2-16GB
I1025 18:59:45.683897 31227 caffe.cpp:288] GPU 0: Tesla P100-SXM2-16GB
I1025 18:59:45.683955 31230 caffe.cpp:288] GPU 3: Tesla P100-SXM2-16GB
I1025 18:59:45.684010 31229 caffe.cpp:288] GPU 2: Tesla P100-SXM2-16GB
I1025 18:59:46.056959 31227 caffe.cpp:302] [MPI:0   ]  name = minsky root = 1
I1025 18:59:46.067212 31228 caffe.cpp:302] [MPI:1   ]  name = minsky root = 1
I1025 18:59:46.070734 31230 caffe.cpp:302] [MPI:3   ]  name = minsky root = 1
I1025 18:59:46.071211 31229 caffe.cpp:302] [MPI:2   ]  name = minsky root = 1
I1025 18:59:46.073958 31227 solver.cpp:44] Initializing solver from parameters: 
test_iter: 1000
test_interval: 1000
base_lr: 0.01
display: 500
max_iter: 2500
lr_policy: "step"
gamma: 0.1

...중략...

I1025 19:24:53.536928 31227 solver.cpp:414]     Test net output #0: accuracy = 0.20032
I1025 19:24:53.536965 31227 solver.cpp:414]     Test net output #1: loss = 4.09802 (* 1 = 4.09802 loss)
I1025 19:24:54.180562 31227 solver.cpp:223] Iteration 2000 (1.27922 iter/s, 390.864s/500 iters), loss = 4.18248
I1025 19:24:54.180598 31227 solver.cpp:242]     Train net output #0: loss = 4.18248 (* 1 = 4.18248 loss)
I1025 19:24:54.180613 31227 sgd_solver.cpp:121] Iteration 2000, lr = 0.01
I1025 19:26:57.349701 31256 data_layer.cpp:86] Restarting data prefetching from start.
I1025 19:30:28.547333 31256 data_layer.cpp:86] Restarting data prefetching from start.
I1025 19:30:29.081480 31227 solver.cpp:466] Snapshotting to binary proto file models/bvlc_alexnet/caffe_alexnet_train_iter_2500.caffemodel
I1025 19:30:29.283386 31228 solver.cpp:315] Iteration 2500, loss = 3.91634
I1025 19:30:29.283444 31228 solver.cpp:320] Optimization Done.
I1025 19:30:29.283535 31230 solver.cpp:315] Iteration 2500, loss = 3.9612
I1025 19:30:29.283582 31230 solver.cpp:320] Optimization Done.
I1025 19:30:29.285512 31228 caffe.cpp:357] Optimization Done.
I1025 19:30:29.285521 31228 caffe.cpp:359] [MPI:1   ] MPI_Finalize
I1025 19:30:29.285697 31230 caffe.cpp:357] Optimization Done.
I1025 19:30:29.285706 31230 caffe.cpp:359] [MPI:3   ] MPI_Finalize
I1025 19:30:29.286912 31229 solver.cpp:315] Iteration 2500, loss = 3.90313
I1025 19:30:29.286952 31229 solver.cpp:320] Optimization Done.
I1025 19:30:29.290489 31229 caffe.cpp:357] Optimization Done.
I1025 19:30:29.290498 31229 caffe.cpp:359] [MPI:2   ] MPI_Finalize
I1025 19:30:29.973234 31227 sgd_solver.cpp:356] Snapshotting solver state to binary proto file models/bvlc_alexnet/caffe_alexnet_train_iter_2500.solverstate
I1025 19:30:30.727695 31227 solver.cpp:315] Iteration 2500, loss = 3.89638
I1025 19:30:30.727744 31227 solver.cpp:320] Optimization Done.
I1025 19:30:30.729465 31227 caffe.cpp:357] Optimization Done.
I1025 19:30:30.729475 31227 caffe.cpp:359] [MPI:0   ] MPI_Finalize



2017년 9월 13일 수요일

IBM PowerAI 4.0에 포함된 Caffe Distributed Deep Learning (DDL)


Tensorflow는 distributed tensorflow를 예전부터 지원하여, 여러대의 서버에 장착된 여러장의 GPU를 이용한 분산 training이 가능했습니다.  IBM PowerAI toolkit에도 ddl-tensorflow가 포함되어 있습니다.

Caffe는 tensorflow와는 달리 분산 모델이 정식으로는 지원되지 않아 한대의 서버에서만 training이 가능했습니다.  물론 각 기업이나 연구소별로 open source로 공개되지 않은 자체적인 버전의 distributed caffe를 자체 개발하여 사용하고 있긴 했습니다.

최근 새로 나온 IBM PowerAI 4.0에 포함된 IBM 버전의 caffe에서는 Distributed Deep Learning (DDL) 옵션을 지원합니다.  이는 OpenMPI 기술에 기반하여 caffe가 하나의 큰 모델을 여러대의 서버에서 분산 처리할 수 있도록 만든 것입니다.  

구체적으로는 caffe 명령어에 -ddl 옵션이 추가된 형태로 제공됩니다.   구체적인 내용은 아래 link에 설명되어 있습니다.

https://public.dhe.ibm.com/software/server/POWER/Linux/mldl/ubuntu/README.html

문제는 여기에 설명되는 option parameter들에 대해, 충분한 설명이 없다는 것입니다.  가령 -ddl "-mode b:4x3"이라고 쓸 때, b는 뭐고 4는 무엇이며 3은 무엇인지 위 link만 보고는 알기가 어렵습니다.

-ddl "-mode b:4x3"를 설명하자면 b는 enhanced NCCL library를 쓰되, 4장의 GPU를 장착한 서버 3대를 쓰라는 것입니다.

또 가령 -ddl "-mode r:2x8"이라는 것은 RING 구성만 써서 2장의 GPU를 장착한 서버 8대를 쓰라는 것이고요.

이에 대해서 설명이 없는 이유를 IBM 본사에 물어보니, "인터넷에는 없지만 PowerAI 4.0을 설치하면 민스키 서버 안에 생성되는 /opt/DL/ddl/doc/README.md 파일 속에 설명이 다 들어있다" 라고 합니다.

해서, 많은 분들이 쉽게 보실 수 있도록 제가 여기에 그 파일 내용을 올려둡니다.


# Overview

IBM PowerAI Distributed Deep Learning (or DDL) is a MPI-based
communication library, which is specifically optimized for Deep Learning
training.  An application integrated with DDL becomes a MPI-application,
which will allow the use of the `mpirun` command to invoke the job in
parallel across a cluster of systems. DDL understands multi-tier network
environment and uses different libraries (e.g. NCCL) and algorithms to
get the best performance in multi-node, multi-GPU environments.

IBM PowerAI Distributed Deep Learning considers each GPU in a cluster as
an individual "learner".  The overall set of learners is described to
IBM PowerAI Distributed Deep Learning in terms of 3 dimensions (X-Y-Z)
that correspond to a multi-tier network hierarchy.  The recommended
mapping is:

   - X for within-host (e.g. number of GPUs per host for multi-GPU hosts)
   - Y for between nearby-hosts (e.g. number of hosts in a single rack)
   - Z for between distant-hosts (e.g. number of racks)

For example, 256 learners can be configured as 4x8x8 or 4x16x4 and so on.

**Example: 2 racks of 8 S822LC for HPC systems with 4 GPUs each**

In this setup, there are 64 learners (4 GPUs in each of 8 hosts in each
of 2 racks) and a simple description would be 4x8x2.

If this configuration includes a truly hierarchical network setup--for
example a high-speed, low-latency network within each rack, and a
slower, higher-latency network between the racks--then 4x8x2 might be
the optimal description.

But if the network configuration is not actually hierarchical--if all
the hosts are connected to a "flat" network regardless of the physical
racking--then a 4x4x4 description may perform better than 4x8x2. Some
experimentation may be needed to find the optimal description.


# Required Libraries

Pre-requisite packages required for IBM PowerAI Distributed Deep
Learning are provided with PowerAI:

   1. OpenMPI with CUDA Support
   2. NVIDIA NCCL


# Integration with Caffe and TensorFlow

IBM PowerAI Distributed Deep Learning has been integrated with the
PowerAI IBM Caffe and TensorFlow packages. `mpirun` must be used to
launch training using the IBM PowerAI Distributed Deep Learning
integration.  General information about `mpirun` is available on the
OpenMPI website
[https://www.open-mpi.org/doc/v2.0/man1/mpirun.1.php](https://www.open-mpi.org/doc/v2.0/man1/mpirun.1.php).

   1. Caffe

      IBM PowerAI Distributed Deep Learning is directly integrated into
      Caffe, and can be exercised by adding the following to the command line.

           -ddl ?쏡DL_OPTIONS HERE??
   2. TensorFlow

      DDL is indirectly integrated into TensorFlow in the form of custom
      operator. The custom operator is provided as a shared library, which is
      loaded and invoked in the python training script.

      The PowerAI ddl-tensorflow package provides an example training
      setup based on the TensorFlow-Slim model library from the TensorFlow
      models repository. Those can be found on your system in:

           /opt/DL/ddl-tensorflow/examples/

      More details on IBM PowerAI Distributed Deep Learning integration
      into TensorFlow, can be found in

           /opt/DL/ddl-tensorflow/doc/README.md


# Using IBM PowerAI Distributed Deep Learning

IBM PowerAI Distributed Deep Learning takes advantage of the network
topology to perform communication quickly. Network topology is described
to IBM PowerAI Distributed Deep Learning in two ways, through an MPI
rank file and via DDL options.

## MPI rank file

A rank file is a standard file that maps MPI clients (IBM PowerAI
Distributed Deep Learning learners) to specific hosts, sockets, and
cores. To get the best performance from IBM PowerAI Distributed Deep
Learning , it is crucial to generate an optimally mapped rank file. To
help with this effort, a script (`rank_gen.py`) is provided to
automatically generate rank files that are appropriate for most S822LC
systems. The script takes two inputs: the network decomposition and a
list of comma-separated hostnames.

**How to use rank file generator script**

        $ python rank_gen.py XxYxZ host_list > rank_file

Here, `XxYxZ` specifies the topology of the GPU and multi-tier network
hierarchy.

For example, for 64 learners (e.g. 16 hosts each with 4 GPUs), any of
4x16x1, 4x8x2, or 4x4x4 might be reasonable choices, depending on the
network topology. All 3 dimensions must be specificed (use 1 to fill any
spaces).

`host_list` is a comma separated list of host names (e.g.
host1,host2,host3,...).  It must contain `Y` times `Z` hostnames,
ordered "by Z". For example, a 4x2x2 configuration with 2 racks of 2
hosts each might have a host list of: `r1h1,r1h2,r2h1,r2h2`.The
hostnames provided in the rankfile should match the system hostnames.

It is possible in a distributed environment to have more than one
interface for each host. In such a scenario, OpenMPI by default, uses
any and all interfaces that are "up" to communicate with a host. To
avoid problems in such cases you can tell OpenMPI to use given
interface. E.g.:

        $ mpirun --mca btl_tcp_if_include ib0 ...

        $ mpirun --mca btl_tcp_if_exclude lo,enp1s0f2 ...

More details available on OpenMPI FAQ page:
[https://www.open-mpi.org/faq/?category=tcp#tcp-selection]([https://www.open-mpi.org/faq/?category=tcp#tcp-selection)

**Parameters for optimal rankfile**

An optimal rank file will depend on the number of sockets or nodes in
the system and the number of cores per socket/node. The `numactl` and
`ppc64_cpu` commands can help determine this information.

   1. Number of sockets and thread slots for each socket.

      `numactl -H` shows the number of sockets ("nodes") in a system,
      and also lists the CPUs (thread slots) for each. For example:

           $ numactl -H
           available: 2 nodes (0-1)
           node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
           node 0 size: 261788 MB
           node 0 free: 6042 MB
           node 1 cpus: 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
           node 1 size: 261334 MB
           node 1 free: 158805 MB
           node distances:
           node   0   1
             0:  10  40
             1:  40  10

      Here the system has two sockets with 80 thread slots each.

   2. Mapping between physical cores and CPUs/thread slots.

           $ ppc64_cpu --info
           Core   0:    0*    1*    2*    3*    4*    5*    6*    7*
           Core   1:    8*    9*   10*   11*   12*   13*   14*   15*
           Core   2:   16*   17*   18*   19*   20*   21*   22*   23*
           Core   3:   24*   25*   26*   27*   28*   29*   30*   31*
           Core   4:   32*   33*   34*   35*   36*   37*   38*   39*
           Core   5:   40*   41*   42*   43*   44*   45*   46*   47*
           Core   6:   48*   49*   50*   51*   52*   53*   54*   55*
           Core   7:   56*   57*   58*   59*   60*   61*   62*   63*
           Core   8:   64*   65*   66*   67*   68*   69*   70*   71*
           Core   9:   72*   73*   74*   75*   76*   77*   78*   79*
           Core  10:   80*   81*   82*   83*   84*   85*   86*   87*
           Core  11:   88*   89*   90*   91*   92*   93*   94*   95*
           Core  12:   96*   97*   98*   99*  100*  101*  102*  103*
           Core  13:  104*  105*  106*  107*  108*  109*  110*  111*
           Core  14:  112*  113*  114*  115*  116*  117*  118*  119*
           Core  15:  120*  121*  122*  123*  124*  125*  126*  127*
           Core  16:  128*  129*  130*  131*  132*  133*  134*  135*
           Core  17:  136*  137*  138*  139*  140*  141*  142*  143*
           Core  18:  144*  145*  146*  147*  148*  149*  150*  151*
           Core  19:  152*  153*  154*  155*  156*  157*  158*  159*

      Here the system has 20 physical cores with 8 thread slots/CPUs each. The
      thread slot numbers match with the numbers in the `numactl` output. The
      asterisks indicate which thread slots are enabled.

      The rankfile only cares about cores (not CPUs/thread slots), and the
      core numbering is relative to the to the node/socket (which is named
      "slot" in the rankfile). So in rank file terms, this system has socket 0
      cores 0-9 and socket 1 cores 0-9.

**Note:** If the number of cores specified in the rankfile exceeds the
actual number of cores, `mpirun` will fail with a non-obvious message.
For example, on a machine with 8-cores per socket:

        $ cat 2x10core.rf
        rank 0=host1     slot=0:0-9
        rank 1=host1     slot=1:0-9

        $ mpirun -n 2 -rf 2x10core.rf /bin/true
        [host1:46256] [[20503,0],0] ORTE_ERROR_LOG: Not found in file rmaps_rank_file.c at line 320
        [host1:46256] [[20503,0],0] ORTE_ERROR_LOG: Not found in file base/rmaps_base_map_job.c at line 351
        $

Versus the working:

        $ cat 2x8core.rf
        rank 0=host1     slot=0:0-7
        rank 1=host1     slot=1:0-7

        $ mpirun -n 2 -rf 2x8core.rf /bin/true
        $

The `-report-bindings` flag may be useful for diagnosing problems:

        $ mpirun -report-bindings ......

## DDL options

There are a number of runtime options for the DDL engine. The options are:

`-mode`: This optionally indicates the algorithm and topology. The topology
should match to the rank assignment (e.g. via rankfile) to get the best
performance. If a mode is not provided, it will work as a single ring
configuration (e.g., r:N). Therefore, the total number of MPI clients
(specified as -n N to mpirun) must match with the number of learners in the
topology (specified as -mode in DDL): otherwise, it will show an error like
`invalid dim size=A usr_size=B dim[0]=...`

        b:4x2  => use enhanced NCCL whenever possible (otherwise use ring) for 4x2 configuration

        n:4x2  => use NCCL whenever possible (otherwise use ring) for 4x2 configuration

        r:4x4  => use only RING for 4x4 configuration

        m:4x6  => use only MPI reduce_scatter and all_gatherV for 4x6 configuration (currently disabled)

        c:4x8  => use only RCS for 4x8 configuration

        p:4x16x4 => first activate ex"p"location mode to get the best algorithms for each dimension of 4x16x4

`-dump_iter <N>`: This optionally makes DDL dump network performance on
every N iterations

`-dev_sync <0, 1, or 2>` : This optionally calls cudaDeviceSynchronize
to minimize jitter, default is 0 (which means off). With 1, it
invokes sync once in the beginning of communication. With 2, it invokes
sync twice in the beginning AND end of communication

 `-rebind_iter <N>`: This optionally monitors variation every N
iterations, and performs rebind if a leaner has been slow for the last 3
checks. Too small number will incur variation-check overhead, but too
big number will make training suffer from variation for long time

 `-dbg_level <0,1,2>`: 0 for no, 1 for mild, and 2 for detailed debug
messages

When `dump_iter` is given, you can see the following periodically where
you can find which learner has the maximum jitter and end to end DDL
elapsed time. Also, per dimension, it shows runtime breakdown along with
the selected algorithm for that particular dimension.

![Alt text](ddl_dump.png?raw=true "DDL dump")


**Example of 2 racks of 8 S822LC HPC systems with 4 GPUs on each host**

Generate an appropriate rank file:

        $ python rank_gen.py 4x8x2 host0,host1,host2,??,host15 > 4x8x2.rf

To start IBM Caffe with `mpirun`, specifying rank file and DDL options:

        $ source /opt/DL/caffe/bin/caffe-activate

        $ mpirun -x PATH -x LD_LIBRARY_PATH -n 16 -rf 4x8x2.rf caffe train -solver solver.prototxt -gpu 0 -bvlc -ddl "-mode b:4x8x2 -dump_iter 100"

To start TensorFlow with `mpirun` using custom operator for DDL:

   - Update `train_image_classifier.py` to specify DDL options during
     initialization:

        ddl.Init(4, mode =??mode b:4x8x2 -dump_iter 100??

   - Execute with `mpirun`:

        $ source /opt/DL/ddl-tensorflow/bin/ddl-tensorflow-activate

        $ mpirun -x PATH -x LD_LIBRARY_PATH -x PYTHONPATH -n 16 -rf 4x8.2.rf python train_image_classifier.py ...

Inference 시스템을 위한 GPU 용량 sizing, 그리고 IBM caffe의 Large Model Support (LMS) 옵션

오늘은 inference, 그 중에서도 inference를 위한 GPU 시스템의 sizing을 어떻게 해야 하는지에 대해서 보겠습니다.  여기서는 특정적으로, caffe를 이용하여 image data를 inference할 때 어떻게 하는지를 보겠습니다.  그리고 덧붙여, IBM Minsky 서버에서만 가능한 옵션, -lms (Large Model Support)가 어떤 혜택을 주는지도 보시겠습니다.

이에 대해서는 아래 site에 기본적인 방법이 소개됩니다.  IBM China의 Deep Learning 개발팀의 박사님들에게 물어보니, 이 방법이 맞다고 합니다.

https://stackoverflow.com/questions/36867591/how-to-estimate-inference-time-from-average-forward-pass-time-in-caffe

여기서 핵심적인 부분은 바로 아래 부분입니다.

For instance, if I run the default command that comes with Caffe:

build/tools/caffe time --model=models/bvlc_alexnet/deploy.prototxt --gpu=0
I get the following output

...
I0426 13:07:32.701490 30417 layer_factory.hpp:77] Creating layer data
I0426 13:07:32.701513 30417 net.cpp:91] Creating Layer data
I0426 13:07:32.701529 30417 net.cpp:399] data -> data
I0426 13:07:32.709048 30417 net.cpp:141] Setting up data
I0426 13:07:32.709079 30417 net.cpp:148] Top shape: 10 3 227 227 (1545870)
I0426 13:07:32.709084 30417 net.cpp:156] Memory required for data: 6183480
...
I0426 13:07:34.390281 30417 caffe.cpp:377] Average Forward pass: 16.7818 ms.
I0426 13:07:34.390290 30417 caffe.cpp:379] Average Backward pass: 12.923 ms.
I0426 13:07:34.390296 30417 caffe.cpp:381] Average Forward-Backward: 29.7969 ms.
The following line:

I0426 13:07:32.709079 30417 net.cpp:148] Top shape: 10 3 227 227 (1545870)
is super important. It says that your input layer is 10x3x227x227-dimensional. In this case, the batch size is 10 images, each of size 3x227x227 (the 3 refers to each of the rgb channels in an image).

So effectively, it took 1.67818 ms/image to do a forward pass or inference time per image.


즉, caffe 명령어의 sub-comand 중 time 명령, 즉 caffe를 이용한 성능 benchmark 결과에서 평균 forward pass에 걸린 시간이 해당 model과 해당 이미지에 대해서 걸릴 inference time이라는 것입니다.  당연한 이야기지만 해당 model에 지정하는 data layer의 Top shape 10 3 227 227, 즉 batch size 10 x channel (RGB) 3 x height 227 x width 227이 클 수록 더 많은 시간이 걸립니다.

HPC cloud 서비스 업체인 Nimbix (nimbix.net/powerai)에서 제공하는 Minsky 서버의 P100 1장짜리 가상머신을 사용할 기회가 있어, 거기에서 이 test를 해봤습니다.  참고로 Nimbix는 docker 기반의 NVLink P100 GPU 가상 머신을 제공하는데, 이에 대해서도 나중에 다룰 기회가 있을 것입니다.

먼저, 1200x1200 크기의 이미지 1장에 대해서 GoogleNet으로 inference하는데 NVLink P100으로는 시간이 얼마나 걸리는지 보시겠습니다.  이를 위해서 먼저 GoogleNet에 포함된 deploy.prototxt를 아래와 같이 편집합니다.  원본 line은 아래에 #으로 comment-out 처리했습니다.

nimbix@JARVICENAE-0A0A1844:/data$ vi bvlc_googlenet/deploy.prototxt
name: "GoogleNet"
layer {
  name: "data"
  type: "Input"
  top: "data"
  input_param { shape: { dim: 1 dim: 3 dim: 1200 dim: 1200 } }
#  input_param { shape: { dim: 10 dim: 3 dim: 224 dim: 224 } }
}

이제 이렇게 수정된 model로 caffe time을 수행합니다.

nimbix@JARVICENAE-0A0A1844:/data$ caffe time -gpu 0 -model=/data/bvlc_googlenet/deploy.prototxt --iterations=1

그 과정을 다 보실 필요는 없고, 사실 맨 끝의 benchmark 결과에서 Average Forward pass 시간만 보시면 됩니다.

I0908 05:39:36.830621   567 caffe.cpp:513]       prob   forward: 0.020864 ms.
I0908 05:39:36.830627   567 caffe.cpp:516]       prob   backward: 0.00368 ms.
I0908 05:39:36.830641   567 caffe.cpp:521] Average Forward pass: 45.3671 ms.
I0908 05:39:36.830649   567 caffe.cpp:523] Average Backward pass: 102.551 ms.
I0908 05:39:36.830657   567 caffe.cpp:525] Average Forward-Backward: 150.178 ms.
I0908 05:39:36.830673   567 caffe.cpp:527] Total Time: 150.178 ms.
I0908 05:39:36.830689   567 caffe.cpp:528] *** Benchmark ends ***

여기서 만약 우리가 batch size(맨 앞의 dim)를 10으로 했다면 저 Average Forward pass 시간을 10으로 나눠야 합니다.  그러나 우리는 dim을 1로 주었으므로 그럴 필요없이 저것을 그대로 쓰면 됩니다.  즉, RGB 3 채널의 1200x1200 이미지 1장을 P100 GPU를 이용하여 GoogleNet으로 inference하는데 0.045초가 걸린다고 보시면 됩니다.

위의 테스트에서 display되는 benchmark 과정을 보면 Deep Learning의 얼개를 대충 보실 수 있습니다.  아래처럼 먼저 Top shape를 1 x 3 x 1200 x 1200으로 시작했다가, 다음 단계에서는 1 x 64 x 600 x 600으로, 그 다음에는 다시 300 x 300으로 계속 절반으로 줄여나가다가 결국 31 x 31에서 마무리 됩니다.   마지막 단계에서의 channel 수는 무려 1024로 늘어나게 되는데, 그 의미를 (저 같은 무식한 HW 엔지니어는) 잘 모르겠군요.   사실 HW 엔지니어에게 중요한 것은 거기에 필요로 하는 메모리 사이즈입니다.  각 단계별 top shape마다 필요로 하는 메모리 사이즈가 'Memory required for data'라는 항목으로 display되는데, 처음 단계에서는 17MB 정도로 시작했다가 맨 마지막 단계에서는 거의 1.6GB 가까이 갑니다.

...
I0908 05:39:25.035709   567 net.cpp:135] Top shape: 1 3 1200 1200 (4320000)
I0908 05:39:25.035733   567 net.cpp:143] Memory required for data: 17280000
I0908 05:39:25.035754   567 layer_factory.hpp:77] Creating layer conv1/7x7_s2
I0908 05:39:25.035786   567 net.cpp:90] Creating Layer conv1/7x7_s2
I0908 05:39:25.035799   567 net.cpp:635] conv1/7x7_s2 <- data
I0908 05:39:25.035816   567 net.cpp:609] conv1/7x7_s2 -> conv1/7x7_s2
I0908 05:39:29.695616   567 net.cpp:128] Setting up conv1/7x7_s2
I0908 05:39:29.695672   567 net.cpp:135] Top shape: 1 64 600 600 (23040000)
I0908 05:39:29.695695   567 net.cpp:143] Memory required for data: 109440000
...
I0908 05:39:29.862272   567 net.cpp:128] Setting up pool5/drop_7x7_s1
I0908 05:39:29.862279   567 net.cpp:135] Top shape: 1 1024 31 31 (984064)
I0908 05:39:29.862287   567 net.cpp:143] Memory required for data: 1587930496
I0908 05:39:29.862294   567 layer_factory.hpp:77] Creating layer loss3/classifier
I0908 05:39:29.862305   567 net.cpp:90] Creating Layer loss3/classifier
I0908 05:39:29.862311   567 net.cpp:635] loss3/classifier <- pool5/7x7_s1
I0908 05:39:29.862320   567 net.cpp:609] loss3/classifier -> loss3/classifier
I0908 05:39:36.385628   567 net.cpp:128] Setting up loss3/classifier
I0908 05:39:36.385684   567 net.cpp:135] Top shape: 1 1000 (1000)
I0908 05:39:36.385696   567 net.cpp:143] Memory required for data: 1587934496
I0908 05:39:36.385712   567 layer_factory.hpp:77] Creating layer prob
I0908 05:39:36.385728   567 net.cpp:90] Creating Layer prob
I0908 05:39:36.385737   567 net.cpp:635] prob <- loss3/classifier
I0908 05:39:36.385749   567 net.cpp:609] prob -> prob
I0908 05:39:36.386745   567 net.cpp:128] Setting up prob
I0908 05:39:36.386756   567 net.cpp:135] Top shape: 1 1000 (1000)
I0908 05:39:36.386765   567 net.cpp:143] Memory required for data: 1587938496
I0908 05:39:36.386771   567 net.cpp:206] prob does not need backward computation.
...


잠깐만요, 1.6GB라고요 ?  P100의 GPU 메모리 크기가 16GB 밖에 안되는데, 저런 image를 10장을 한꺼번에 inference하면 어떻게 된다는 것일까요 ?  설마 error가 날까요 ?  한번 해보겠습니다.  위와 동일한 모델을 사용하되, 단지 맨 앞의 dim, 즉 batch size를 1에서 10으로 바꾸겠습니다.

nimbix@JARVICENAE-0A0A1844:/data$ vi bvlc_googlenet/deploy.prototxt
name: "GoogleNet"
layer {
  name: "data"
  type: "Input"
  top: "data"
  input_param { shape: { dim: 10 dim: 3 dim: 1200 dim: 1200 } }
#  input_param { shape: { dim: 1 dim: 3 dim: 1200 dim: 1200 } }
#  input_param { shape: { dim: 10 dim: 3 dim: 224 dim: 224 } }
}

이제 동일하게 caffe time을 수행합니다.

nimbix@JARVICENAE-0A0A1844:/data$ caffe time -gpu 0 -model=/data/bvlc_googlenet/deploy.prototxt --iterations=1

I0908 05:43:44.249899   646 net.cpp:135] Top shape: 10 3 1200 1200 (43200000)
I0908 05:43:44.249914   646 net.cpp:143] Memory required for data: 172800000
I0908 05:43:44.249928   646 layer_factory.hpp:77] Creating layer conv1/7x7_s2
I0908 05:43:44.249949   646 net.cpp:90] Creating Layer conv1/7x7_s2
I0908 05:43:44.249956   646 net.cpp:635] conv1/7x7_s2 <- data
I0908 05:43:44.249967   646 net.cpp:609] conv1/7x7_s2 -> conv1/7x7_s2
I0908 05:43:44.614331   646 net.cpp:128] Setting up conv1/7x7_s2
I0908 05:43:44.614367   646 net.cpp:135] Top shape: 10 64 600 600 (230400000)
I0908 05:43:44.614382   646 net.cpp:143] Memory required for data: 1094400000
...
I0908 05:43:44.763245   646 net.cpp:135] Top shape: 10 1024 31 31 (9840640)
I0908 05:43:44.763254   646 net.cpp:143] Memory required for data: 15839942400
I0908 05:43:44.763260   646 layer_factory.hpp:77] Creating layer pool5/drop_7x7_s1
I0908 05:43:44.763272   646 net.cpp:90] Creating Layer pool5/drop_7x7_s1
I0908 05:43:44.763278   646 net.cpp:635] pool5/drop_7x7_s1 <- pool5/7x7_s1
I0908 05:43:44.763285   646 net.cpp:596] pool5/drop_7x7_s1 -> pool5/7x7_s1 (in-place)
I0908 05:43:44.763319   646 net.cpp:128] Setting up pool5/drop_7x7_s1
I0908 05:43:44.763325   646 net.cpp:135] Top shape: 10 1024 31 31 (9840640)
I0908 05:43:44.763334   646 net.cpp:143] Memory required for data: 15879304960
I0908 05:43:44.763340   646 layer_factory.hpp:77] Creating layer loss3/classifier
I0908 05:43:44.763352   646 net.cpp:90] Creating Layer loss3/classifier
I0908 05:43:44.763358   646 net.cpp:635] loss3/classifier <- pool5/7x7_s1
I0908 05:43:44.763367   646 net.cpp:609] loss3/classifier -> loss3/classifier
I0908 05:43:51.338423   646 net.cpp:128] Setting up loss3/classifier
I0908 05:43:51.345638   646 net.cpp:135] Top shape: 10 1000 (10000)
I0908 05:43:51.345651   646 net.cpp:143] Memory required for data: 15879344960
I0908 05:43:51.345667   646 layer_factory.hpp:77] Creating layer prob
I0908 05:43:51.345683   646 net.cpp:90] Creating Layer prob
I0908 05:43:51.345693   646 net.cpp:635] prob <- loss3/classifier
I0908 05:43:51.345705   646 net.cpp:609] prob -> prob
I0908 05:43:51.346666   646 net.cpp:128] Setting up prob
I0908 05:43:51.346678   646 net.cpp:135] Top shape: 10 1000 (10000)
I0908 05:43:51.346685   646 net.cpp:143] Memory required for data: 15879384960
...
I0908 05:43:51.724148   646 caffe.cpp:465] Initial loss: 0
I0908 05:43:51.724202   646 caffe.cpp:466] Performing Backward
I0908 05:43:51.724215   646 caffe.cpp:474] *** Benchmark begins ***
I0908 05:43:51.724222   646 caffe.cpp:475] Testing for 1 iterations.
F0908 05:43:51.915272   646 syncedmem.cpp:651] Check failed: error == cudaSuccess (2 vs. 0)  out of memory
*** Check failure stack trace: ***
    @     0x100000f5ce0c  google::LogMessage::Fail()
    @     0x100000f5f284  google::LogMessage::SendToLog()
    @     0x100000f5c768  google::LogMessage::Flush()
    @     0x100000f611c4  google::LogMessageFatal::~LogMessageFatal()
    @     0x10000026e3a0  caffe::SyncedMemory::mutable_gpu_data()
    @     0x1000002736c4  caffe::Blob<>::mutable_gpu_diff()
    @     0x1000004e774c  caffe::InnerProductLayer<>::Backward_gpu()
    @         0x10018ca8  (unknown)
    @         0x10012974  (unknown)
    @     0x100001c2309c  (unknown)
    @     0x100001c23298  __libc_start_main
    @              (nil)  (unknown)


아 !  정말 error가 납니다.   정말 data에만 무려 15.8GB의 메모리가 필요하다고 나오더니, 실제 벤치마크에 들어가자마자 out of memory 에러가 나면서 중단됩니다.  정말 GPU의 발목을 잡는 것은 GPU 메모리 크기의 한계라는 것을 절실히 깨닫는 순간입니다.

하지만 IBM과 NVIDIA는 여기서 포기하지 않습니다.  원래 NVIDIA의 CUDA에서는 Unified Memory라고 해서, GPU가 CPU 메모리를 마치 GPU 메모리처럼 쓸 수 있는 기능을 내놓았지요.  그러나 실제로는 그렇게 GPU가 CPU memory에 접근하는 통로가 느려터진 PCIe이다보니, Unified Memory를 쓰면 편리하기는 해도 성능은 거의 1/10 수준으로 떨어져 버리는 것이 상식이었습니다.  이는 NVLink P100을 장착한 DGX-1 서버에서도 마찬가지였습니다.  DGX-1도 GPU끼리만 NVLink로 연결될 뿐, 정작 CPU와 GPU를 연결하는 것은 PCIe거든요.  그래서 결국 아무도 caffe에서 unified memory를 쓸 생각을 하지 않았습니다.

그러나 IBM Minsky는 다릅니다.  POWER8 processor에는 NVLink port가 박혀있으므로, CPU와 GPU가 NVLink로 직접 연결되며, 그것도 NVLink 2개를 뭉쳐서 무려 80GB/sec로 연결됩니다.  PCIe의 2.5배나 되는 대역폭입니다.  이를 활용하여 caffe에서 CPU-GPU 간에 data를 직접 주고받을 수 있습니다 !  실제로 IBM은 최근 발표한 PowerAI 4.0에 포함된 IBM caffe (caffe-ibm)에 이를 적용했습니다.  그 결과, IBM caffe에서는 일반 bvlc caffe나 NV caffe에는 없는 새로운 옵션, -lms (LMS, Large Model Support)를 사용할 수 있습니다.

이에 대해서는 아래 문서를 참조하시면 됩니다.

https://public.dhe.ibm.com/software/server/POWER/Linux/mldl/ubuntu/README.html

역시 귀찮으신 분들을 위해 간략히 요약해드리면 이렇습니다.

-lms 8000000 : 이는 8000000 (kbyte 단위, 즉 8GB) 이상의 메모리 덩어리는 그냥 CPU 메모리 상에 두라는 뜻입니다.

즉, -lms 뒤에 큰 수를 적을 수록 가급적 GPU 메모리를 많이 쓰고 CPU 메모리는 정말 필요한 경우에만 쓰라는 이야기입니다.  당연히 최대치는 16000000 정도가 될 것이고, 이보다 더 큰 수를 적는 것은 사실상 LMS 옵션을 disable하는 효과를 냅니다.  반면에 -lms를 매우 작게, 가령 100으로 주는 것은 사실상 GPU 메모리를 쓰지 말고 다 CPU 메모리를 쓰라는 이야기가 됩니다.

또 -lms_frac <0~1.0> 이라는 옵션을 줄 수도 있습니다.  가령 -lms_frac 0.4로 주면, GPU 메모리 사용률이 40%가 되기 전에는 LMS 기능을 쓰지 말라는 것이 됩니다.  작은 크기의 model을 수행할 때는 굳이 느린 CPU 메모리를 쓸 필요가 없으므로, -lms_frac 0.9 정도로 주는 것이 좋습니다.

이제 위에서 out of memory를 낸 model에 대해 실제로 -lms 옵션을 적용해 보시지요.  먼저 -lms 8192, 즉 8MB 이상의 메모리 덩어리는 모두 CPU 메모리에 두라고 지시했습니다.

nimbix@JARVICENAE-0A0A1844:/data$ caffe time -gpu 0 -lms 8192 -model=/data/bvlc_googlenet/deploy.prototxt --iterations=1

I0908 05:47:44.949090   676 net.cpp:135] Top shape: 10 3 1200 1200 (43200000)
I0908 05:47:44.949105   676 net.cpp:143] Memory required for data: 172800000
I0908 05:47:44.949124   676 layer_factory.hpp:77] Creating layer conv1/7x7_s2
I0908 05:47:44.949146   676 net.cpp:90] Creating Layer conv1/7x7_s2
I0908 05:47:44.949153   676 net.cpp:635] conv1/7x7_s2 <- data
I0908 05:47:44.949167   676 net.cpp:609] conv1/7x7_s2 -> conv1/7x7_s2
I0908 05:47:45.580006   676 net.cpp:128] Setting up conv1/7x7_s2
I0908 05:47:45.580046   676 net.cpp:135] Top shape: 10 64 600 600 (230400000)
I0908 05:47:45.580060   676 net.cpp:143] Memory required for data: 1094400000
...
I0908 05:47:57.704324   676 caffe.cpp:465] Initial loss: 0
I0908 05:47:57.704356   676 caffe.cpp:466] Performing Backward
I0908 05:47:57.704371   676 caffe.cpp:474] *** Benchmark begins ***
I0908 05:47:57.704377   676 caffe.cpp:475] Testing for 1 iterations.
I0908 05:47:57.711424   676 syncedmem.cpp:355] [LMS] memory[0x110024232400]  device_=0 size_ = 921600000 allocation=7349057792 fragmented size = 655558000 gpu_ptr_=1155371368464
I0908 05:47:57.769644   676 syncedmem.cpp:355] [LMS] memory[0x110024258aa0]  device_=0 size_ = 230400000 allocation=7579458048 fragmented size = 425158224 gpu_ptr_=1122381070352
I0908 05:47:57.778683   676 syncedmem.cpp:355] [LMS] memory[0x110024286d30]  device_=0 size_ = 230400000 allocation=7809858304 fragmented size = 425158464 gpu_ptr_=1122842444032
I0908 05:47:57.790587   676 syncedmem.cpp:355] [LMS] memory[0x1100242c0be0]  device_=0 size_ = 691200000 allocation=8731458560 fragmented size = 655558704 gpu_ptr_=1156294115344
I0908 05:47:57.838747   676 syncedmem.cpp:355] [LMS] memory[0x1100242df300]  device_=0 size_ = 691200000 allocation=9653058816 fragmented size = 885958944 gpu_ptr_=1157447262464
...
I0908 05:47:58.203995   676 caffe.cpp:513] pool5/7x7_s1 forward: 4.48429 ms.
I0908 05:47:58.204002   676 caffe.cpp:516] pool5/7x7_s1 backward: 0.002144 ms.
I0908 05:47:58.204010   676 caffe.cpp:513] pool5/drop_7x7_s1    forward: 0.367552 ms.
I0908 05:47:58.204015   676 caffe.cpp:516] pool5/drop_7x7_s1    backward: 0.002112 ms.
I0908 05:47:58.204022   676 caffe.cpp:513] loss3/classifier     forward: 18.1078 ms.
I0908 05:47:58.204033   676 caffe.cpp:516] loss3/classifier     backward: 0.002112 ms.
I0908 05:47:58.204041   676 caffe.cpp:513]       prob   forward: 0.022848 ms.
I0908 05:47:58.204047   676 caffe.cpp:516]       prob   backward: 0.011328 ms.
I0908 05:47:58.204061   676 caffe.cpp:521] Average Forward pass: 495.206 ms.
I0908 05:47:58.204067   676 caffe.cpp:523] Average Backward pass: 2.21437 ms.
I0908 05:47:58.204074   676 caffe.cpp:525] Average Forward-Backward: 499.65 ms.
I0908 05:47:58.204092   676 caffe.cpp:527] Total Time: 499.65 ms.
I0908 05:47:58.204107   676 caffe.cpp:528] *** Benchmark ends ***


예 !  도중에 LMS가 사용된다는 메시지가 display되면서 성공적으로 완료되었습니다 !  아무래도 느린 CPU 메모리를 사용하니까 당연히 성능은 떨어졌을 것입니다.  얼마나 떨어졌을까요 ?  여기서의 결과는 Average Forward pass: 495.206 ms 인데, batch size가 10이므로 이미지 1장당 0.0495초 걸린 것입니다.  위에서 1장씩 테스트했을 때의 결과 0.045초보다 10% 정도 느려졌습니다.   10장씩 batch로 돌리면 사실 1장씩 돌린 것보다는 빨리 나와야 하는데 오히려 10% 느려진 것은 많이 느려진 것이지요.

결국 LMS를 사용하면 심각한 성능 저하는 어쩔 수 없이 발생하는 것일까요 ?  꼭 그렇지는 않습니다.  방금 제가 수행한 것은 극단적으로 거의 모든 메모리 덩어리를 CPU 메모리에 두라고 지시한 것입니다.  GPU 메모리를 적극적으로 활용하되, GPU 메모리 크기보다 큰 것들만 어쩔 수 없이 CPU 메모리를 사용하라고 지시하면 성능이 훨씬 더 좋을 것입니다.

이번에는 그렇게 -lms 160000000 옵션으로 돌려 보겠습니다.

nimbix@JARVICENAE-0A0A1844:/data$ caffe time -gpu 0 -lms 160000000 -model=/data/bvlc_googlenet/deploy.prototxt --iterations=1

I0908 06:32:20.006875  1126 net.cpp:135] Top shape: 10 3 1200 1200 (43200000)
I0908 06:32:20.006891  1126 net.cpp:143] Memory required for data: 172800000
I0908 06:32:20.006904  1126 layer_factory.hpp:77] Creating layer conv1/7x7_s2
I0908 06:32:20.006927  1126 net.cpp:90] Creating Layer conv1/7x7_s2
I0908 06:32:20.006933  1126 net.cpp:635] conv1/7x7_s2 <- data
I0908 06:32:20.006944  1126 net.cpp:609] conv1/7x7_s2 -> conv1/7x7_s2
I0908 06:32:20.591289  1126 net.cpp:128] Setting up conv1/7x7_s2
I0908 06:32:20.591329  1126 net.cpp:135] Top shape: 10 64 600 600 (230400000)
I0908 06:32:20.591343  1126 net.cpp:143] Memory required for data: 1094400000
...
I0908 06:32:28.272960  1126 net.cpp:296] [LMS] BuildLargeModelSupport
W0908 06:32:28.273018  1126 net.cpp:348] [LMS] ######################################################
W0908 06:32:28.273172  1126 net.cpp:349] [LMS] uncovered layer type: Softmax
W0908 06:32:28.273182  1126 net.cpp:350] [LMS] ######################################################
W0908 06:32:28.273310  1126 net.cpp:348] [LMS] ######################################################
W0908 06:32:28.273320  1126 net.cpp:349] [LMS] uncovered layer type: Input
W0908 06:32:28.273329  1126 net.cpp:350] [LMS] ######################################################
I0908 06:32:28.273347  1126 net.cpp:425] [LMS] data_forward [0] data:  -> data: 0x110009bfa4f0(172800000)  ### flag=0   data:
I0908 06:32:28.273361  1126 net.cpp:425] [LMS] conv1/7x7_s2_forward [1] data: 0x110009bfa4f0(172800000)  -> data: 0x1100233f7520(921600000)  ### flag=0   data: 0x110009bfa4f0(1,1)
...
I0908 06:32:29.055697  1126 caffe.cpp:513]       prob   forward: 0.022016 ms.
I0908 06:32:29.055704  1126 caffe.cpp:516]       prob   backward: 0.006848 ms.
I0908 06:32:29.055716  1126 caffe.cpp:521] Average Forward pass: 263.516 ms.
I0908 06:32:29.055724  1126 caffe.cpp:523] Average Backward pass: 2.21066 ms.
I0908 06:32:29.055730  1126 caffe.cpp:525] Average Forward-Backward: 267.967 ms.
I0908 06:32:29.055748  1126 caffe.cpp:527] Total Time: 267.967 ms.
I0908 06:32:29.055764  1126 caffe.cpp:528] *** Benchmark ends ***


이번에는 10장에 대해 263.516 ms, 즉 1장에 대해서는 0.0263초가 걸렸습니다.  이는 1장씩 테스트했을 때의 결과 0.045초보다 무려 71% 빠른 결과입니다 !  LMS 덕분에 10장씩 batch로 돌리니까 더 빨라진 것이지요.  결국 LMS를 사용하면 오히려 더 빠른 성능을 낼 수도 있는 것입니다.