2018년 12월 19일 수요일

Tensorflow보다 46배 빠른 deep learning framework ?? - Snap ML 소개

IBM에서는 GPU 서버 등 HW만 만드는 것이 아니라 tensorflow나 caffe와 같은 deep learning용 framework SW도 개발합니다.  Snap ML이라고 하는 것이 바로 그것입니다.   이름이 왜 Snap ML이냐하면 손가락을 딱 튕기는(snap) 순식간에 training이 이루어질 정도로 빠르다고 해서 그렇습니다.



(finger snap 하면 타노스인데 말입니다.)


이 Snap ML의 성능과 효용성에 대해서는 아래 IBM 공식 blog에 잘 나와 있습니다.

https://www.ibm.com/blogs/research/2018/03/machine-learning-benchmark/

위 블로그 내용을 요약하면 구글이 Google Cloud Platform (worker machine 60대 + parameter machine 34대)에서 tensorflow를 60분 동안 걸려 training한 결과 얻은 log loss를 IBM Snap ML은 IBM POWER9과 NVIDIA GPU를 장착한 AC922 4대에서 Snap ML로 불과 91.5초의 training 만으로 얻었다는 것입니다.  여기에 사용된 dataset은 Criteo Labs에서 제공하는 실제 internet 광고 click 건수 data로서, 총 40억 row에 달하는 광대한 data입니다. 

이런 고성능이 가능했던 것은 물론 CPU와 GPU 간을 PCI 버스가 아닌 NVLink로 연결하는 AC922 서버의 우수한 아키텍처 영향도 있습니다만, 이 Snap ML이라는 framework SW 자체의 우수성도 있습니다. 

이 posting에서는 PowerAI 5.4 버전에 포함된 Snap ML을 설치하고 거기에 포함된 example 몇 개를 돌려보겠습니다. 

먼저, Anaconda2 5.2를 설치하여 conda/python 환경을 구성합니다.

[root@localhost home]# wget https://repo.continuum.io/archive/Anaconda2-5.2.0-Linux-ppc64le.sh

[root@localhost home]# chmod a+x Anaconda2-5.2.0-Linux-ppc64le.sh

[root@localhost home]# ./Anaconda2-5.2.0-Linux-ppc64le.sh

[root@localhost home]# . /root/.bashrc

[root@localhost home]# which python
/opt/anaconda2/bin/python


그 다음에 PowerAI의 장점을 살려 아주 간단하게 Snap ML을 설치합니다. 

[root@localhost home]# yum install snap-ml-local.ppc64le
==============================================================================================
 Package              Arch         Version                 Repository                    Size
==============================================================================================
Installing:
 snap-ml-local        ppc64le      1.1.0-241.1887dec       mldl-repo-local               29 M
Installing for dependencies:
 powerai-license      ppc64le      5.4.0-124.150cee0       mldl-repo-local              4.3 M
 python-devel         ppc64le      2.7.5-76.el7            rhel-7-for-power-9-rpms      398 k


이렇게 설치된 snap-ml-local 속에는 아래와 같은 fileset들이 들어 있습니다.   보시다시피 저 위의 IBM 공식 블로그에 소개했던 Criteo Teraclick training의 미니 버전이라고 할 수 있는 criteo Kaggle example도 들어있습니다.

[root@localhost home]# rpm -ql snap-ml-local-1.1.0-241.1887dec.ppc64le
/opt/DL
/opt/DL/snap-ml-local
/opt/DL/snap-ml-local/bin
/opt/DL/snap-ml-local/bin/check_dependencies
/opt/DL/snap-ml-local/bin/install_dependencies
/opt/DL/snap-ml-local/bin/snap-ml-local-activate
/opt/DL/snap-ml-local/conda-pkgs
/opt/DL/snap-ml-local/conda-pkgs/channeldata.json
/opt/DL/snap-ml-local/conda-pkgs/icons
...
/opt/DL/snap-ml-local/examples/creditcard
/opt/DL/snap-ml-local/examples/creditcard/README.md
/opt/DL/snap-ml-local/examples/creditcard/example-creditcard.py
/opt/DL/snap-ml-local/examples/creditcard/preprocess-creditcard.py
/opt/DL/snap-ml-local/examples/criteo
/opt/DL/snap-ml-local/examples/criteo/README.md
/opt/DL/snap-ml-local/examples/criteo/example-criteo-kaggle-privacy.py
/opt/DL/snap-ml-local/examples/criteo/example-criteo-kaggle.py
/opt/DL/snap-ml-local/examples/criteo/preprocess-criteo-kaggle-privacy.py
/opt/DL/snap-ml-local/examples/criteo/preprocess-criteo-kaggle.py
...

Snap ML의 사용을 위해서는 먼저 install_dependencies를 수행하여 관련 conda package들을 설치합니다.

[root@localhost home]# /opt/DL/snap-ml-local/bin/install_dependencies
...
    package                    |            build
    ---------------------------|-----------------
    powerai-snap-ml-local-prereqs-1.1.0_241.1887dec|           py27_0           4 KB  file:///opt/DL/snap-ml-local/conda-pkgs
    conda-4.5.11               |           py27_0         1.0 MB
    openssl-1.0.2p             |       h14c3975_0         3.3 MB
    openblas-devel-0.2.20      |                7         101 KB
    blas-1.1                   |         openblas           4 KB  file:///opt/DL/snap-ml-local/conda-pkgs
    certifi-2018.11.29         |           py27_0         146 KB
    pai4sk-0.20rc1_241.1887dec |           py27_0        14.0 MB  file:///opt/DL/snap-ml-local/conda-pkgs
    ------------------------------------------------------------
                                           Total:        18.5 MB
...

그리고 PowerAI license에 동의하는 script를 수행하고 이어서 snap-ml-local-activate를 수행하여 PATH 등의 환경 변수를 설정합니다.

[root@localhost home]# /opt/DL/license/bin/accept-powerai-license.sh

[root@localhost home]# source /opt/DL/snap-ml-local/bin/snap-ml-local-activate

이제 Amazone cloud에서 Criteo의 test dataset 미니 버전을 download 합니다.  4.5GB 정도 됩니다.

[root@localhost home]# mkdir data && cd data

[root@localhost data]# wget https://s3-us-west-2.amazonaws.com/criteo-public-svm-data/criteo.kaggle2014.svm.tar.gz
Length: 4849417655 (4.5G) [application/x-tar]
Saving to: ‘criteo.kaggle2014.svm.tar.gz’
100%[======================================>] 4,849,417,655 22.5MB/s   in 4m 29s
2018-12-14 12:28:34 (17.2 MB/s) - ‘criteo.kaggle2014.svm.tar.gz’ saved [4849417655/4849417655]

이 tar 압축을 풀고 data preprocessing을 위한 python code를 수행합니다.  약 1시간 정도 걸립니다.

[root@localhost data]# tar xzf criteo.kaggle2014.svm.tar.gz 

[root@localhost data]# time python /opt/DL/snap-ml-local/examples/criteo/preprocess-criteo-kaggle.py --data_path=/home
real    67m57.038s
user    67m20.277s
sys     0m27.750s


이제 다음과 같이 criteo kaggle2014 dataset의 포맷이 완료되었습니다. 

[root@localhost data]# ls -ltr crit*
-r--r--r-- 1 1007 1007  3523847664 Sep 27  2017 criteo.kaggle2014.test.svm
-r--r--r-- 1 1007 1007 26737621696 Sep 27  2017 criteo.kaggle2014.train.svm
-rw-r--r-- 1 root root  4849417655 Sep 27  2017 criteo.kaggle2014.svm.tar.gz
-rw-r--r-- 1 root root 16227489113 Dec 14 13:46 criteo.kaggle2014.X_train.npz
-rw-r--r-- 1 root root  5409163153 Dec 14 13:47 criteo.kaggle2014.X_test.npz
-rw-r--r-- 1 root root   275043776 Dec 14 13:47 criteo.kaggle2014.y_train.npy
-rw-r--r-- 1 root root    91681320 Dec 14 13:47 criteo.kaggle2014.y_test.npy


이걸 GPU 1번 1개를 이용하여 training하기 위해 다음과 같이 example-criteo-kaggle.py를 수행해 줍니다.

[root@localhost data]# time python /opt/DL/snap-ml-local/examples/criteo/example-criteo-kaggle.py --data_path=/home --use_gpu --device_ids 1
Data load time (s): 28.92
[Info] Tolerance 0.001 attained after 49 epochs.
/opt/anaconda2/lib/python2.7/site-packages/pai4sk/sml_solvers/logistic.py:406: UserWarning: PowerAI: Default solver is set to 'snapml'. Specify a solver to silence this warning.
  UserWarning)
[pai4sk] Training time (s):  25.89
[pai4sk] Logarithmic loss:   0.4558
[Info] Tolerance 0.001 attained after 49 epochs.
/opt/anaconda2/lib/python2.7/site-packages/pai4sk/linear_model/logistic.py:432: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)
[pai4sk.lmodel] Training time (s):  32.28
[pai4sk.lmodel] Logarithmic loss:   0.4558
[sklearn] Training time (s):  624.55
[sklearn] Logarithmic loss:   0.4558

real    12m14.974s
user    11m57.544s
sys     0m34.245s


위의 example 결과는 크게 3가지입니다. 

[pai4sk] Training time (s):  25.89 :  Snap ML을 이용한 training 결과
[pai4sk.lmodel] Training time (s):  32.28 :  Snap ML Linear Regression을 이용한 training 결과
[sklearn] Training time (s):  624.55 :  일반 sklearn을 이용한 training 결과

즉, Snap ML을 이용하면 일반 sklearn을 이용하는 것보다 거의 25배 더 빨리 training을 마칠 수 있으며, 그럼에도 불구하고 log loss는 비슷한 수준이라는 것을 아실 수 있습니다.

더 자세한 내용은 아래 URL들을 참조하시기 바랍니다.

https://ibmsoe.github.io/snap-ml-doc/index.html
https://ibmsoe.github.io/snap-ml-doc/tutorials.html
https://ibmsoe.github.io/snap-ml-doc/notebookcreditlocal.html#notebook-credit-local
https://ibmsoe.github.io/snap-ml-doc/pai4skapidocumentation.html#pai4sk-api-documentation

H2O Driverless AI를 이용한 Kaggle 도전 : Creditcard fraud detection

이번 posting에서는 H2O Driverless AI의 정확도가 과연 어느 정도인지 좀더 현실적으로 보여주는 테스트를 돌려보겠습니다.

Kaggle이라는 것은 일종의 data science 경쟁 대회라고 보시면 됩니다.  국가기관이나 기업 등에서 자신들이 해석하기 어려운 (주로 비인식처리된) 실제 data를 올려놓고 전세계 scientist들에게 그 분석 방법에 대해 경쟁을 벌이도록 하는 것입니다.  저는 data scientist와는 100만 광년 정도의 거리에 있는 일개 시스템 엔지니어입니다만, H2O Driverless AI의 힘을 빌어 아래의 신용카드 사기 예측 모델 생성에 도전해보겠습니다.

https://www.kaggle.com/mlg-ulb/creditcardfraud/home

여기서 제공되는 data는 https://www.kaggle.com/mlg-ulb/creditcardfraud/downloads/creditcardfraud.zip 에 올려져 있습니다.  Facebook이나 Google 등으로 로그인해야 download가 가능합니다.

압축을 풀면 다음과 같이 28만이 넘는 row를 가진 csv 파일을 얻을 수 있습니다.  이 중 마지막 3천 row를 뚝 잘라 creditcard_test1.csv로 만들고, 나머지 것을 creditcard_train.csv으로 쓰겠습니다.

[bsyu@redhat74 data]$ wc -l creditcard.csv
284808 creditcard.csv

[bsyu@redhat74 data]$ ls -l creditcard*.csv
-rw-rw-r-- 1 bsyu bsyu 150828752 Mar 23  2018 creditcard.csv

[bsyu@redhat74 data]$ head -n 1 creditcard.csv > creditcard_test.csv

[bsyu@redhat74 data]$ tail -n 3000 creditcard.csv >> creditcard_test1.csv

[bsyu@redhat74 data]$ head -n 283808 creditcard.csv > creditcard_train.csv

각 row는 아래와 같이 비식별화처리를 거친 모종의 값으로 되어 있습니다.  여기서 비식별화처리가 되지 않은 column은 맨마지막 2개, 즉 Amount와 Class입니다.  Amount는 카드 사용금액이고, Class가 0이면 정상거래, 1이면 사기거래입니다.

[bsyu@redhat74 data]$ head -n 2 creditcard_train.csv
"Time","V1","V2","V3","V4","V5","V6","V7","V8","V9","V10","V11","V12","V13","V14","V15","V16","V17","V18","V19","V20","V21","V22","V23","V24","V25","V26","V27","V28","Amount","Class"
0,-1.3598071336738,-0.0727811733098497,2.53634673796914,1.37815522427443,-0.338320769942518,0.462387777762292,0.239598554061257,0.0986979012610507,0.363786969611213,0.0907941719789316,-0.551599533260813,-0.617800855762348,-0.991389847235408,-0.311169353699879,1.46817697209427,-0.470400525259478,0.207971241929242,0.0257905801985591,0.403992960255733,0.251412098239705,-0.018306777944153,0.277837575558899,-0.110473910188767,0.0669280749146731,0.128539358273528,-0.189114843888824,0.133558376740387,-0.0210530534538215,149.62,"0"

이 dataset은 Class 측면에서 보면 굉장히 빈도가 낮은 것입니다.  즉, 전체 28만건이 넘는 거래들 중, 사기거래, 즉 Class가 1인 거래는 고작 492건에 불과합니다.  Percentage로 따지면 고작 0.173%에 불과합니다.   보통 machine learning에서 만든 모델의 정확도가 90%를 넘어가면 꽤 성공적이라고들 하는데, 이런 경우에는 어떤 거래를 판정하라는 요청을 받았을 때 그냥 무조건 정상거래라고 판정하면 99.8% 이상의 정확도를 나타냈다고(?) 주장할 수도 있습니다.  과연 이런 극악한 조건에서 H2O Driverless AI는 제대로 된 모델을 만들 수 있을까요 ?

[bsyu@redhat74 data]$ cut -d"," -f31 creditcard.csv | grep 1 | wc -l
492

우리의 목표는 이 data를 이용해서 어떤 거래가 사기인지 정상인지 판별하는 model을 만드는 것입니다.  그 성공 여부 판별을 위해 잘라낸 test dataset 3천 row 중에 실제 사기 건수는 얼마나 될까요 ?  고작 4건입니다.  588번째, 871번째, 874번째, 그리고 921번째 row입니다.

[bsyu@redhat74 data]$ cut -d"," -f31 creditcard_test1.csv | grep 1
"1"
"1"
"1"
"1"

[bsyu@redhat74 data]$ cut -d"," -f31 creditcard_test1.csv | head -n 589 | tail -n 5
"0"
"0"
"0"
"1"
"0"

[bsyu@redhat74 data]$ cut -d"," -f31 creditcard_test1.csv | head -n 875 | tail -n 7
"0"
"0"
"1"
"0"
"0"
"1"
"0"

[bsyu@redhat74 data]$ cut -d"," -f31 creditcard_test1.csv | head -n 923 | tail -n 5
"0"
"0"
"1"
"0"
"0"

다음과 같이 H2O Driverless AI에 creditcard_train.csv를 add 하고 target column은 'Class'로 지정한 뒤, control knob은 Accuracy 10, Time 8, Interpretability 2로 맞추고 training (H2O 용어로는 experiment)를 시작했습니다.   Accuracy 항목에서 Regression으로 수행하고 있기 때문에, 결과 예측값은 0이나 1의 숫자가 아닌, 그 중간의 소수로 나올 것입니다.  그 값이 1에 가까울 수록 사기일 확률이 높고, 0에 가까울 수록 정상거래일 것입니다. 



참고로 이번에는 GPU를 갖춘 시스템을 구하지 못해서, POWER8 가상 머신에서 CPU core 2개를 이용해서 수행했습니다.   SMT=8으로 되어 있기 때문에, OS에게는 이것이 2개의 core가 아닌 16개의 logical CPU로 인식됩니다.  덕분에 CPU 사용률이 매우 높고, 또 시간도 엄청나게 오래 걸리는 것을 보실 수 있습니다.




최종적으로 만들어진 model은 크기가 무려 7GB나 되고, 만드는데 거의 9시간이 걸렸습니다.



이제 이 model을 열어 test dataset으로 판별을 해보겠습니다.  맨 처음에 training을 시작할 때 RMSE (root mean square error)가 0.0194로 시작했었는데, 최종적으로는 0.0191로 약간 줄어있는 것을 보실 수 있습니다.



Score on Anonther Dataset을 클릭하면 dataset을 선택하게 되어 있는데, 미리 서버에 올려둔 creditcard_test1.csv을 선택하면 약 1분 정도 prediction이 수행된 뒤에 그 결과로 (input인  creditcard_test1.csv가 3천 row이므로)  Class column에 대한 3천 row의 예측값을 담은 csv 파일을 download 할 수 있게 해줍니다.   그 결과는 아래와 같습니다.






최소한 정상거래를 사기거래라고 판정한 것은 하나도 없습니다.   대부분의 row는 사기 확률이 0%로 나오고, 실제로는 사기였던 4개 row에 대해서는 위와 같이 나옵니다.  즉 2개는 98%, 99%로서 사기가 확실하고, 나머지 2개는 사기 확률이 36%와 49%로 나오는 것이지요.  이 정도면 상당히 높은 수준의 결과라고 판단됩니다.   정말 멋지지 않습니까 ?

---------------------------

추가로, training dataset을 좀더 작게, 대신 test dataset을 좀더 크게 하여 model을 생성하고 테스트해보겠습니다.   아래와 같이, 전체 28만4천건 중에 26만건을 training dataset으로, 그리고 나머지 2만4천건을 test dataset으로 잘라냈습니다.

[bsyu@redhat74 data]$ wc -l credi*
   284808 creditcard.csv

[bsyu@redhat74 data]$ head -n 260001 creditcard.csv > credit_train.csv

[bsyu@redhat74 data]$ head -n 1 creditcard.csv > credit_test.csv

[bsyu@redhat74 data]$ tail -n 24807 creditcard.csv >> credit_test.csv

[bsyu@redhat74 data]$ wc -l creditcard.csv credit_train.csv credit_test.csv
   284808 creditcard.csv
   260001 credit_train.csv
    24808 credit_test.csv

이것을 다음과 같이 Accuracy 10, Time 7, Interpretability 2로 맞추고 training 했습니다.



그 결과로 (역시 GPU 없이 POWER8 CPU core 2개만 이용해서) training 하는데 총 13시간 정도가 걸렸습니다.



이 자동생성된 model을 이용하여 위에서 준비한 2만4천건의 test dataset인 credit_test.csv에 대한 prediction을 수행했고, 그 결과로 gerewika_preds_799411be.csv 파일을 얻었습니다.


[bsyu@redhat74 data]$ head /tmp/gerewika_preds_799411be.csv
Class
4.00790426897506e-5
2.7836374905953808e-5
4.3067764191826185e-5
6.776587835599978e-5
0.0007914757505702477
0.00026333562696054583
0.0003490925939070681
0.00012611508177717525
3.6875439932445686e-5


위에서 준비했던 credit_test.csv 속에는 과연 사기 건수가 몇건이었을까요 ?  그 column 중 31번째 column인 Class의 값이 1인 것이 몇건인지를 세어보겠습니다.  

[bsyu@redhat74 data]$ cut -f31 -d',' ./credit_test.csv | grep 1 | wc -l
21

위와 같이 총 21건입니다.

과연 우리가 얻은 gerewika_preds_799411be.csv 파일 속에서 사기일 가능성이 높은 칼럼은 몇 개일까요 ?  그것을 세기 위해 다음과 같은 script를 만들었습니다.

[bsyu@redhat74 data]$ cat count.sh
if [[ $# -ne 2 ]]
then
echo "Usage ./count.sh digit filename"
fi
j=1
for i in `cat $2 `
do
m=$(printf "%f" "$i")
if (( $(echo "$m >= $1" | bc -l) ))
then
echo "Row_num is $j and the vlaue is $m"
fi
(( j=j+1 ))
done


여기서 몇 %일 때 이를 사기로 볼 것인지는 여러분이 직접 정하셔야 합니다.   일단 70%, 즉 0.70 이상이면 사기로 간주하는 것으로 해서 세어보겠습니다.

[bsyu@redhat74 data]$ ./count.sh 0.7 /tmp/gerewika_preds_799411be.csv
./count.sh: line 9: printf: Class: invalid number
Row_num is 1058 and the vlaue is 0.740187
Row_num is 1475 and the vlaue is 0.897596
Row_num is 1927 and the vlaue is 0.975030
Row_num is 2562 and the vlaue is 0.996552
Row_num is 2828 and the vlaue is 0.968835
Row_num is 3276 and the vlaue is 0.979910
Row_num is 3326 and the vlaue is 0.939915
Row_num is 3879 and the vlaue is 0.896258
Row_num is 16866 and the vlaue is 0.995111
Row_num is 19865 and the vlaue is 0.958619
Row_num is 20145 and the vlaue is 0.905748
Row_num is 20151 and the vlaue is 0.911095
Row_num is 21146 and the vlaue is 0.964987


70%를 기준으로 하니 13건이 사기로 판명되었습니다.   실제값인 21건을 정확하게 맞추지는 못했습니다.   Training dataset이 28만 row에서 23만 row로 줄어드니 확실히 정확도가 떨어지는 것을 보실 수 있습니다.   그런 상황에서도 적어도 정상 transaction을 사기 transaction으로 평가하는 일은 없군요.   참고로 test dataset 속의 실제 사기 transaction의 row #는 다음과 같습니다.

Row_num is 1058 and the vlaue is 1.000000
Row_num is 1475 and the vlaue is 1.000000
Row_num is 1927 and the vlaue is 1.000000
Row_num is 2562 and the vlaue is 1.000000
Row_num is 2828 and the vlaue is 1.000000
Row_num is 3082 and the vlaue is 1.000000
Row_num is 3276 and the vlaue is 1.000000
Row_num is 3326 and the vlaue is 1.000000
Row_num is 3879 and the vlaue is 1.000000
Row_num is 8377 and the vlaue is 1.000000
Row_num is 12523 and the vlaue is 1.000000
Row_num is 14384 and the vlaue is 1.000000
Row_num is 14477 and the vlaue is 1.000000
Row_num is 15994 and the vlaue is 1.000000
Row_num is 16073 and the vlaue is 1.000000
Row_num is 16866 and the vlaue is 1.000000
Row_num is 19865 and the vlaue is 1.000000
Row_num is 20145 and the vlaue is 1.000000
Row_num is 20151 and the vlaue is 1.000000
Row_num is 21146 and the vlaue is 1.000000
Row_num is 21676 and the vlaue is 1.000000


-----------------------------------------------


이하는 상기 model을 training한 뒤 얻은 MLI (machine learning interpretation) 항목의 결과입니다.

가장 알아보기 쉬운 KLIME의 설명을 보면 다음과 같습니다.  아래 그림에도 있습니다만, Class 항목이 1 증가할 때 V11은 0.0058 증가, V4는 0.0036 증가... 등의 상관관계를 Driverless AI가 분석해냈습니다.

Top Positive Global Attributions
V11   increase of  0.0058
V4    increase of  0.0036
V2    increase of  0.0024










------------------------------------

추가 테스트를 해봤습니다.  다음과 같이 약 250000번째 줄부터 4000 줄을 빼내어 credit_test.csv라는 test용 dataset을 만들고, 그 4000 줄을 뺀 나머지 부분으로 credit_train.csv이라는 training용 dataset을 만들었습니다.

ibmtest@digits:~/files/csv$ head -n 250001 creditcard.csv > credit_train.csv
ibmtest@digits:~/files/csv$ tail -n 30808 creditcard.csv >> credit_train.csv
ibmtest@digits:~/files/csv$ head -n 1 creditcard.csv > credit_test.csv
ibmtest@digits:~/files/csv$ head -n 254809 creditcard.csv | tail -n 4000 >> credit_test.csv
ibmtest@digits:~/files/csv$ wc -l credit_train.csv credit_test.csv
   280809 credit_train.csv
     4001 credit_test.csv
   284810 total

Training용 dataset에는 class가 1, 즉 사기 transaction이 총 484개 들어있고, test용 dataset에는 9개 들어있습니다.

ibmtest@digits:~/files/csv$ cut -f31 -d',' ./credit_train.csv | grep 1 | wc -l
484

ibmtest@digits:~/files/csv$ cut -f31 -d',' ./credit_test.csv | grep 1 | wc -l
9

이제 test용 dataset 중 몇번째 row가 사기 transaction인지 row #를 찾아보겠습니다.

ibmtest@digits:~/files/csv$ j=1

ibmtest@digits:~/files/csv$ for i in `cut -f31 -d',' ./credit_test.csv`
> do
> if [ "$i" == "\"1\"" ]
> then
> echo "Row_num is " $j
> fi
> ((j=j+1))
> done


Row_num is  671
Row_num is  1060
Row_num is  1075
Row_num is  1085
Row_num is  1098
Row_num is  1318
Row_num is  1968
Row_num is  3538
Row_num is  3589

이제 H2O DAI로 training을 한 뒤, 이 test dataset을 넣어서 예측을 해보았습니다.  그 결과는 다음과 같습니다.

30% (0.3)을 기준으로 하면 아래와 같이 9건 중 6건을 찾아냅니다.

ibmtest@digits:~/files/csv$ ./count.sh  0.3 miwatuvi_preds_c5262e68.csv
./count.sh: line 8: printf: Class: invalid number
Row_num is 671 and the vlaue is 1.000000
Row_num is 1060 and the vlaue is 0.950008
Row_num is 1098 and the vlaue is 0.904203
Row_num is 1318 and the vlaue is 1.000000
Row_num is 1968 and the vlaue is 1.000000
Row_num is 3538 and the vlaue is 0.303216

20% (0.2)을 기준으로 하면 아래와 같이 9건 중 7건을 찾아냅니다. 

ibmtest@digits:~/files/csv$ ./count.sh  0.2 miwatuvi_preds_c5262e68.csv
./count.sh: line 8: printf: Class: invalid number
Row_num is 671 and the vlaue is 1.000000
Row_num is 1060 and the vlaue is 0.950008
Row_num is 1098 and the vlaue is 0.904203
Row_num is 1318 and the vlaue is 1.000000
Row_num is 1968 and the vlaue is 1.000000
Row_num is 3538 and the vlaue is 0.303216
Row_num is 3589 and the vlaue is 0.296002




2018년 11월 22일 목요일

x86 hadoop cluster의 HDFS를 IBM POWER linux (ppc64le) 서버에 mount하기

x86 서버로 구성된 hadoop cluster의 HDFS에 AC922과 같은 IBM POWER linux (ppc64le) 서버가 access해야 할 경우가 있습니다.  이럴 때 가장 편리한 방법은 HortonWorks나 Cloudera 등에 포함된 NFS Gateway service를 이용하는 것입니다.   IBM POWER linux 쪽에서는 그냥 NFS mount만 하면 되며, NFS Gateway service는 x86 hadoop cluster에서 설정하면 됩니다.  IBM POWER linux 쪽에서는 그냥 일반 NFS처럼 read/write가 가능합니다.


아래에 정리된 HOW TO는 Cloudera 6.x을 기준으로, 다음 문서부터 시작되는 매뉴얼을 따라 진행했습니다.

https://www.cloudera.com/documentation/enterprise/6/6.0/topics/configure_cm_repo.html#cm_repo


아래에서, powervc131이라는 서버는 Redhat 7.5 x86_64 서버이고, redhat74라는 서버는 Redhat 7.5 ppc64le, 즉 IBM POWER linux 서버입니다.

먼저 Cloudera가 설치된 x86 서버에서 hadoop 버전을 확인합니다.  Cloudera 6.x이므로 hadoop 버전은 3.0.0입니다.

[ibm@powervc131 ~]$ hadoop version
Hadoop 3.0.0-cdh6.0.1
Source code repository http://github.com/cloudera/hadoop -r 47165556c2149deb60153448d73226104921b1ef
Compiled by jenkins on 2018-09-19T16:09Z
Compiled with protoc 2.5.0
From source with checksum a3d0e319db58ceb558757daf384381c
This command was run using /opt/cloudera/parcels/CDH-6.0.1-1.cdh6.0.1.p0.590678/jars/hadoop-common-3.0.0-cdh6.0.1.jar

[ibm@powervc131 ~]$ hadoop fs -df
Filesystem                     Size        Used    Available  Use%
hdfs://powervc131:8020  48294789120  1428176896  11857518592    3%

HDFS의 /user/root 밑에 input이라는 directory를 만들고 거기에 OS의 /etc/hosts 파일을 put 명령으로 복사해 넣습니다.

[ibm@powervc131 ~]$ sudo -u hdfs hadoop fs -mkdir /user/root

[ibm@powervc131 ~]$ sudo -u hdfs hadoop fs -chown root /user/root

[ibm@powervc131 ~]$ sudo hadoop fs -mkdir /user/root/input

[ibm@powervc131 ~]$ sudo hadoop fs -ls
Found 1 items
drwxr-xr-x   - root supergroup          0 2018-11-22 16:06 input

[ibm@powervc131 ~]$ sudo hadoop fs -put /etc/hosts input

그 내용은 아래와 같습니다.

[ibm@powervc131 ~]$ sudo hadoop fs -text input/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
10.10.14.71    powervc131
10.10.14.66    nim


혹시 아직 Cloudera manager에 NFS Gateway service가 추가되어 있지 않다면 다음과 같이 추가하시면 됩니다.  먼저, Cloudera manager에서 '인스턴스' tab을 클릭합니다.  그리고 '역할 인스턴스 추가' 상자를 클릭합니다.



그리고 오른쪽의 'NFS Gateway'의 아래에 있는 '호스트 선택' 부분을 클릭하여 NFS Gateway로 사용할 노드를 지정합니다.  여기서는 1대 뿐인 hadoop cluster이므로, Cloudera manager가 곧 name node요 data node이자 NFS Gateway 서버입니다.  그리고 이 서버에는 이미 NFS Gateway service가 추가되어 있습니다.




이제 '인스턴스' tab에서 보면 '역할 유형'에 NFS Gateway가 보입니다만, 현재는 '중지됨' 상태입니다.  이것을 클릭하여 구성으로 들어갑니다.



NFS Gateway service의 구성을 확인합니다.  기본적으로 다른 모든 서버가 read/write 권한을 가지고 access할 수 있도록, 허용된 호스트 및 권한 (dfs.nfs.exports.allowed.hosts) 부분은 " * rw "로 되어 있습니다.



이제 '인스턴스' tab에서 NFS Gateway를 선택하고 '선택된 작업' 상자를 클릭하여 start 합니다.  또는 아래와 같이 NFS Gateway를 클릭하여 들어간 뒤 '작업' 상자를 클릭하여 start 해도 됩니다.




이때 이 NFS Gateway 서버로 지정된 x86 서버 (여기서는 Cloudera manager 서버이자 data node이자 NFS Gateway 서버)에 혹시 OS에서 NFS 서비스가 활성화된 상태라면 'sudo systemctl stop nfs'로 비활성화를 미리 시켜 두어야 합니다.   그렇지 않을 경우 'NFS 서비스가 이미 start 되어 있다'라는 error가 발생할 것입니다.

[ibm@powervc131 ~]$ sudo systemctl status nfs
● nfs-server.service - NFS server and services
   Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
   Active: inactive (dead) since Thu 2018-11-22 16:35:08 KST; 9min ago
  Process: 30866 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS)
  Process: 30863 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=0/SUCCESS)
  Process: 30861 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS)
 Main PID: 2270 (code=exited, status=0/SUCCESS)

Nov 22 16:35:08 powervc131 systemd[1]: Stopping NFS server and services...
Nov 22 16:35:08 powervc131 systemd[1]: Stopped NFS server and services.



NFS Gateway service의 start가 완료된 뒤, x86 서버에서 showmount 명령을 통해 이 서버에서 export된 NFS directory를 확인합니다.  이때 10.10.14.71은 물론 x86 서버의 IP address입니다.   다음과 같이 / (OS의 /가 아니라 HDFS의 / 입니다)가 모든 권한을 다 주는 형태로 export 되어있는 것을 확인하실 수 있습니다.

[ibm@powervc131 ~]$ showmount -e 10.10.14.71
Export list for 10.10.14.71:
/ *


이제 IBM POWER linux 서버로 가서, 이 NFS를 mount 합니다.  일반적인 NFS mount와 동일합니다.

[bsyu@redhat74 ~]$ sudo mkdir /hdfs

[bsyu@redhat74 ~]$ sudo mount 10.10.14.71:/ /hdfs

NFS가 hdfs서버인 10.10.14.71로부터 mount되었는지 확인합니다.

[bsyu@redhat74 ~]$ df -h
Filesystem             Size  Used Avail Use% Mounted on
/dev/mapper/rhel-root   26G   16G   11G  58% /
devtmpfs               3.7G     0  3.7G   0% /dev
tmpfs                  3.8G     0  3.8G   0% /dev/shm
tmpfs                  3.8G   16M  3.8G   1% /run
tmpfs                  3.8G     0  3.8G   0% /sys/fs/cgroup
/dev/mapper/mpathb2   1014M  178M  837M  18% /boot
tmpfs                  765M     0  765M   0% /run/user/0
tmpfs                  765M     0  765M   0% /run/user/1001
10.10.14.71:/           45G   35G   11G  76% /hdfs

이제 그 mount point인 /hdfs에 들어가서 어떤 파일이 들어있는지 확인합니다.  HDFS의 /가 mount된 것을 확인하실 수 있습니다.

[bsyu@redhat74 ~]$ cd /hdfs

[bsyu@redhat74 hdfs]$ ls
tmp  user

[bsyu@redhat74 hdfs]$ cd user/

[bsyu@redhat74 user]$ ls
history  hive  hue  oozie  root  spark  yarn

[bsyu@redhat74 user]$ cd root

[bsyu@redhat74 root]$ ls
input

[bsyu@redhat74 root]$ cd input

[bsyu@redhat74 input]$ ls
hosts

아까 넣어둔 hosts 파일도 그대로 보입니다.  이것을 vi로 edit해서 한줄을 더 넣어보겠습니다.

[bsyu@redhat74 input]$ sudo vi hosts
...
10.10.14.66    nim
Cloudera HDFS was accessed by Redhat ppc64le on POWER server using NFS Gateway.

Save한 뒤 이제 다시 x86 서버의 HDFS로 가서 위의 한줄이 저장되어 있는지 확인합니다.

[ibm@powervc131 ~]$ sudo hadoop fs -text input/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
10.10.14.71    powervc131
10.10.14.66    nim
Cloudera HDFS was accessed by Redhat ppc64le on POWER server using NFS Gateway.

잘 된 것을 보실 수 있습니다.




--------------------------------------------------------

별첨 : 아래는 x86서버에 Cloudera를 설치하는 과정입니다. 

[ibm@powervc131 ~]$ sudo wget https://archive.cloudera.com/cm6/6.0.1/redhat7/yum/cloudera-manager.repo -P /etc/yum.repos.d/

[ibm@powervc131 ~]$ sudo rpm --import https://archive.cloudera.com/cm6/6.0.0/redhat7/yum/RPM-GPG-KEY-cloudera

[ibm@powervc131 ~]$ sudo yum install oracle-j2sdk1.8

[ibm@powervc131 ~]$ sudo yum install cloudera-manager-daemons cloudera-manager-agent cloudera-manager-server

[ibm@powervc131 ~]$ sudo yum install postgresql-server

[ibm@powervc131 ~]$ sudo yum install python-pip

[ibm@powervc131 ~]$ sudo pip install psycopg2==2.7.5 --ignore-installed

[ibm@powervc131 ~]$ cat /etc/locale.conf
LANG="en_US.UTF-8"
LC_ALL="en_US.UTF-8"

[ibm@powervc131 ~]$ su - postgres

-bash-4.2$ sudo postgresql-setup initdb

-bash-4.2$ sudo vi /var/lib/pgsql/data/pg_hba.conf
...
host    all             all             127.0.0.1/32            md5  #newly inserted
host    all             all             127.0.0.1/32            ident
...

[ibm@powervc131 ~]$ sudo systemctl enable postgresql

[ibm@powervc131 ~]$ sudo systemctl start postgresql

[ibm@powervc131 ~]$ sudo -u postgres psql

postgres=# CREATE ROLE scm LOGIN PASSWORD 'scm';
CREATE ROLE
postgres=# CREATE DATABASE scm OWNER scm ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE amon LOGIN PASSWORD 'amon';
CREATE ROLE
postgres=# CREATE DATABASE amon OWNER amon ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE rman LOGIN PASSWORD 'rman';
CREATE ROLE
postgres=# CREATE DATABASE rman OWNER rman ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE hue LOGIN PASSWORD 'hue';
CREATE ROLE
postgres=# CREATE DATABASE hue OWNER hue ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE hive LOGIN PASSWORD 'hive';
CREATE ROLE
postgres=# CREATE DATABASE metastore OWNER hive ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE sentry LOGIN PASSWORD 'sentry';
CREATE ROLE
postgres=# CREATE DATABASE sentry OWNER sentry ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE nav LOGIN PASSWORD 'nav';
CREATE ROLE
postgres=# CREATE DATABASE nav OWNER nav ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE navms LOGIN PASSWORD 'navms';
CREATE ROLE
postgres=# CREATE DATABASE navms OWNER navms ENCODING 'UTF8';
CREATE DATABASE
postgres=# CREATE ROLE oozie LOGIN PASSWORD 'oozie';
CREATE ROLE
postgres=# CREATE DATABASE oozie OWNER oozie ENCODING 'UTF8';
CREATE DATABASE
postgres=# ALTER DATABASE metastore SET standard_conforming_strings=off;
ALTER DATABASE
postgres=# ALTER DATABASE oozie SET standard_conforming_strings=off;
ALTER DATABASE


[ibm@powervc131 ~]$ wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.46.tar.gz

[ibm@powervc131 files]$ tar -zxvf ../mysql-connector-java-5.1.46.tar.gz

[ibm@powervc131 files]$ sudo mkdir -p /usr/share/java/

[ibm@powervc131 files]$ cd mysql-connector-java-5.1.46

[ibm@powervc131 mysql-connector-java-5.1.46]$ cp mysql-connector-java-5.1.46-bin.jar /usr/share/java/


[ibm@powervc131 ~]$ sudo /opt/cloudera/cm/schema/scm_prepare_database.sh postgresql scm scm scm
JAVA_HOME=/usr/java/jdk1.8.0_141-cloudera
Verifying that we can write to /etc/cloudera-scm-server
Creating SCM configuration file in /etc/cloudera-scm-server
Executing:  /usr/java/jdk1.8.0_141-cloudera/bin/java -cp /usr/share/java/mysql-connector-java.jar:/usr/share/java/oracle-connector-java.jar:/usr/share/java/postgresql-connector-java.jar:/opt/cloudera/cm/schema/../lib/* com.cloudera.enterprise.dbutil.DbCommandExecutor /etc/cloudera-scm-server/db.properties com.cloudera.cmf.db.
[                          main] DbCommandExecutor              INFO  Successfully connected to database.
All done, your SCM database is configured correctly!


[ibm@powervc131 ~]$ sudo systemctl start cloudera-scm-server

[ibm@powervc131 ~]$ sudo tail -f /var/log/cloudera-scm-server/cloudera-scm-server.log
...
2018-11-22 14:46:30,746 INFO WebServerImpl:org.eclipse.jetty.server.AbstractConnector: Started ServerConnector@143ff757{HTTP/1.1,[http/1.1]}{0.0.0.0:7180}
2018-11-22 14:46:30,750 INFO WebServerImpl:org.eclipse.jetty.server.Server: Started @92563ms
2018-11-22 14:46:30,750 INFO WebServerImpl:com.cloudera.server.cmf.WebServerImpl: Started Jetty server.


이제부터는 http://<서버주소>:7180로 들어갑니다.   기본 id/passwd는 admin/admin입니다.   혹시 web에서 접속이 되지 않을 경우 firewalld와 iptables를 중단시키십시요.

[ibm@powervc131 cm]$ sudo systemctl stop iptables
[ibm@powervc131 cm]$ sudo systemctl stop firewalld















2018년 11월 21일 수요일

Redhat ppc64le 환경에서의 boost와 pyarrow build 및 설치

Redhat 7.5의 기본 gcc 버전은 4.8.5입니다.   OS의 주요 library들도 모두 gcc 4.8.5로 build되어 있습니다.   그런데 Anaconda의 v5.2부터는 python이 gcc 7.2.0으로 build되어 있습니다.

[bsyu@redhat74 cudf]$ python
Python 3.7.0 (default, Jun 28 2018, 13:02:24)
[GCC 7.2.0] :: Anaconda, Inc. on linux

이로 인해 벌어지는 문제 중 하나가, OS의 기본 gcc/g++을 사용해 build한 python package의 경우 아래처럼 undefined symbol error를 내는 경우가 많다는 것입니다.

ImportError: /home/bsyu/anaconda3/lib/python3.7/site-packages/pyarrow/libparquet.so.12: undefined symbol: _ZN5boost13match_resultsIN9__gnu_cxx17__normal_iteratorIPKcNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEEESaINS_9sub_matchISB_EEEE12maybe_assignERKSF_

이는 gcc 5.1부터 CXXABI가 달라졌기 때문입니다.  그에 대한 자세한 내용은 아래에 나와 있습니다. 

https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html

이 문제를 해결하려면 gcc 버전을 7.2로 올리는 것이 좋습니다.  그에 대해서는 아래 link를 따라 하시면 됩니다.

http://hwengineer.blogspot.com/2018/05/ppc64le-gcc7-cmake3-source-build.html

이제 gcc 7.2.0을 갖게 되었습니다.

[bsyu@redhat74 files]$ which gcc
/usr/local/bin/gcc

[bsyu@redhat74 files]$ gcc --version
gcc (GCC) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

OS 기본 gcc가 아닌, 새로 만든 gcc 7.2.0을 쓰기 위해서는 PATH와 LD_LIBRARY_PATH를 환경변수에서 /usr/local을 먼저 참조하도록 설정하십시요.

[bsyu@redhat74 files]$ cat ~/.bashrc
...
export PATH=/usr/local/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/lib64:/usr/local/lib:/usr/lib64:/usr/lib:/lib64:/lib:$LD_LIBRARY_PATH
export MAKEFLAGS=-j16
export ARROW_BUILD_TYPE=release

이제 anaconda3를 설치하시고...

[bsyu@redhat74 files]$ ./Anaconda3-5.3.0-Linux-ppc64le.sh

[bsyu@redhat74 files]$ which python
~/anaconda3/bin/python

[bsyu@redhat74 files]$ python
Python 3.7.0 (default, Jun 28 2018, 13:02:24)
[GCC 7.2.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.

pyarrow를 설치하기 위해서는 먼저 boost library들도 gcc 7.2로 build 해야 합니다.  OS에서 기본 제공되는 것들은 맨 위의 예와 같이 undefined symbol error를 내기 때문입니다.

[bsyu@redhat74 files]$ wget https://dl.bintray.com/boostorg/release/1.68.0/source/boost_1_68_0.tar.gz

[bsyu@redhat74 files]$ tar -zxf boost_1_68_0.tar.gz

여기서 나중에 "pyconfig.h Not Found" error를 피하기 위해서 아래와 같이 pyconfig.h를 살짝 link를 걸어 줘야 합니다.

[bsyu@redhat74 include]$ ln -s python3.7m/pyconfig.h pyconfig.h

[bsyu@redhat74 files]$ cd boost_1_68_0

[bsyu@redhat74 boost_1_68_0]$ ./bootstrap.sh --with-python=/home/bsyu/anaconda3/bin/python --with-python-root=/home/bsyu/anaconda3

[bsyu@redhat74 boost_1_68_0]$ ./b2 -j16

[bsyu@redhat74 boost_1_68_0]$ sudo ./b2 install

이제 boost library들이 설치되었습니다.   이제 pyarrow를 build 합니다.

[bsyu@redhat74 files]$ conda install numpy six setuptools cython pandas pytest cmake flatbuffers rapidjson boost-cpp thrift snappy zlib gflags brotli lz4-c zstd -c conda-forge

(base) [bsyu@redhat74 files]$ which cmake
~/anaconda3/bin/cmake
(base) [bsyu@redhat74 files]$ cmake --version
cmake version 3.12.2

(base) [bsyu@redhat74 files]$ git clone https://github.com/apache/arrow.git

(base) [bsyu@redhat74 files]$ cd arrow

(base) [bsyu@redhat74 arrow]$ export CC=/usr/local/bin/gcc
(base) [bsyu@redhat74 arrow]$ export CXX=/usr/local/bin/g++

(base) [bsyu@redhat74 arrow]$ mkdir cpp/build && cd cpp/build

(base) [bsyu@redhat74 build]$ cmake -DCMAKE_BUILD_TYPE=release -DARROW_PYTHON=on -DARROW_PLASMA=on -DARROW_BUILD_TESTS=OFF  -DARROW_PARQUET=ON  ..

(base) [bsyu@redhat74 build]$ make -j16

(base) [bsyu@redhat74 build]$ sudo make install

(base) [bsyu@redhat74 build]$ cd ../../python

(base) [bsyu@redhat74 python]$ export ARROW_HOME=/usr/local

(base) [bsyu@redhat74 python]$ MAKEFLAGS=-j16 python setup.py build_ext --build-type=$ARROW_BUILD_TYPE --with-parquet --inplace

(base) [bsyu@redhat74 python]$ python setup.py build_ext --build-type=release --with-parquet --bundle-arrow-cpp bdist_wheel

위까지 마치면 dist directory 밑에 wheel file이 생성되어 있습니다.

(base) [bsyu@redhat74 python]$ ls -ltr dist
-rw-rw-r-- 1 bsyu bsyu 9645047 Nov 21 10:42 pyarrow-0.11.1.dev264+g7e6bf41.d20181121-cp37-cp37m-linux_ppc64le.whl

이제 이것을 pip로 설치합니다.

(base) [bsyu@redhat74 python]$ pip install ./dist/pyarrow-0.11.1.dev264+g7e6bf41.d20181121-cp37-cp37m-linux_ppc64le.whl

이제 import pyarrow를 해보면 맨 위의 예에서 보이던 undefined symbol error가 없어진 것을 확인하실 수 있습니다.

(base) [bsyu@redhat74 python]$ python
Python 3.7.0 (default, Jun 28 2018, 13:02:24)
[GCC 7.2.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyarrow
>>>

여기서 만든 pyarrow*.whl은 아래 google drive에 올려놓았습니다.   python 3.7용입니다.

https://drive.google.com/open?id=16cF8F9A2a8ZtpnpV0PdckJMyotkVYEns


참고로 ldd로 보면, 이제 lib.cpython-37m-powerpc64le-linux-gnu.so가 OS에서 기본 제공하는 library (/usr/lib64/libstdc++.so.6, /usr/lib/libboost_regex.so 등 )가 아닌 /usr/local/lib64/libstdc++.so.6, /usr/local/lib/libboost_regex.so 등을 참조하는 것을 보실 수 있습니다.

(base) [bsyu@redhat74 ~]$ ldd /home/bsyu/anaconda3/lib/python3.7/site-packages/pyarrow/lib.cpython-37m-powerpc64le-linux-gnu.so
        linux-vdso64.so.1 =>  (0x00003fff9f750000)
        libarrow.so.12 => /usr/local/lib64/libarrow.so.12 (0x00003fff9efc0000)
        libarrow_python.so.12 => /usr/local/lib64/libarrow_python.so.12 (0x00003fff9ee70000)
        libparquet.so.12 => /usr/local/lib64/libparquet.so.12 (0x00003fff9ebf0000)
        libstdc++.so.6 => /usr/local/lib64/libstdc++.so.6 (0x00003fff9e9c0000)
        libm.so.6 => /usr/lib64/libm.so.6 (0x00003fff9e8d0000)
        libgcc_s.so.1 => /usr/local/lib64/libgcc_s.so.1 (0x00003fff9e890000)
        libc.so.6 => /usr/lib64/libc.so.6 (0x00003fff9e6a0000)
        libdl.so.2 => /usr/lib64/libdl.so.2 (0x00003fff9e670000)
        libpthread.so.0 => /usr/lib64/libpthread.so.0 (0x00003fff9e630000)
        libz.so.1 => /usr/lib64/libz.so.1 (0x00003fff9e5f0000)
        libboost_system.so.1.68.0 => /usr/local/lib/libboost_system.so.1.68.0 (0x00003fff9e5c0000)
        libboost_filesystem.so.1.68.0 => /usr/local/lib/libboost_filesystem.so.1.68.0 (0x00003fff9e580000)
        libboost_regex.so.1.68.0 => /usr/local/lib/libboost_regex.so.1.68.0 (0x00003fff9e440000)
        librt.so.1 => /usr/lib64/librt.so.1 (0x00003fff9e410000)
        /lib64/ld64.so.2 (0x000000007f930000)
        libutil.so.1 => /usr/lib64/libutil.so.1 (0x00003fff9e3e0000)



2018년 10월 25일 목요일

GPUdb인 Kinetica와 Postgresql의 대결

Kinetica는 GPU를 이용한 in-memory DB로서, 주로 OLTP용보다는 OLAP용으로 사용됩니다.    아래에 간단한 TPC-H data와 query를 이용하여 일반 DBMS(여기서는 postgresql)와의 성능 비교 및 그때 CPU-GPU간의 NVLink가 어떤 효과를 내는지 nvprof로 분석해보았습니다.   아래 test는 모두 POWER9 + V100 GPU의 IBM AC922 서버로 진행했습니다.

Postgresql과 Kinetica 모두 TPC-H에서 제공되는 dbgen으로 생성한 data를 사용했습니다.  여기서 DB 크기는 10GB로 했습니다.  이렇게 하면 delimeter가 '|'로 되어 있는 SAM file들이 만들어지는데, 실제로 DB에 load를 하려면 맨 끝에 붙은 '|'은 제거해야 하더군요.

[root@localhost dbgen]# nohup ./dbgen -s 10 &

[root@localhost dbgen]# ls -ltr *.tbl
-rw-r--r--. 1 root root   14176368 Oct 23 09:35 supplier.tbl
-rw-r--r--. 1 root root        389 Oct 23 09:35 region.tbl
-rw-r--r--. 1 root root  243336157 Oct 23 09:35 part.tbl
-rw-r--r--. 1 root root 1204850769 Oct 23 09:35 partsupp.tbl
-rw-r--r--. 1 root root 1749195031 Oct 23 09:35 orders.tbl
-rw-r--r--. 1 root root       2224 Oct 23 09:35 nation.tbl
-rw-r--r--. 1 root root 7775727688 Oct 23 09:35 lineitem.tbl
-rw-r--r--. 1 root root  244847642 Oct 23 09:35 customer.tbl

Postgres에서는 다음과 같이 dbgen에 포함된 dss.ddl script를 이용하여 필요한 table들을 생성했습니다.

[root@localhost dbgen]# su - postgres
Last login: Mon Oct 22 17:27:48 KST 2018 on pts/1

-bash-4.2$ createdb dss
-bash-4.2$ psql dss
psql (9.2.23)
Type "help" for help.

dss=# \i /home/data/2.17.3/dbgen/dss.ddl

dss=# \d ORDERS
                Table "public.orders"
     Column      |         Type          | Modifiers
-----------------+-----------------------+-----------
 o_orderkey      | integer               | not null
 o_custkey       | integer               | not null
 o_orderstatus   | character(1)          | not null
 o_totalprice    | numeric(15,2)         | not null
 o_orderdate     | date                  | not null
 o_orderpriority | character(15)         | not null
 o_clerk         | character(15)         | not null
 o_shippriority  | integer               | not null
 o_comment       | character varying(79) | not null

그 다음에 다음과 같이 SAM file에서 postgresql table 안으로 loading을 합니다.  아래는 LINEITEM의 table 예만 들었습니다.

dss=# copy lineitem from '/home/data/2.17.3/dbgen/lineitem.csv' with (FORMAT csv, DELIMITER '|');
COPY 59986052


Kinetica에도 이 SAM file을 만들고 load 하기 위해서는 다음과 같은 menu를 이용하면 됩니다.   다만 Kinetica는 GPU를 이용한 분산 DB이기 때문에, shard key를 지정하는 등 추가적인 설정 과정을 거쳐야 합니다.  (여기서는 상세히 표시하지 않았습니다.  저도 제가 직접 못하고 저희 파트너사인 Unione INC의 엔지니어 도움을 받았습니다.)





먼저, TPC-H의 query들 중 1개씩의 query를 수행해보겠습니다.  이 query는 join 등이 그렇게까지 크지 않은 query라서 GPUdb가 일반 RDBMS에 비해 그다지 더 유리하지도 않은 query입니다.  그런데도, 또 별다른 tuning 없이도, Kinetica가 20배 정도 빠른 것을 보실 수 있습니다.  모두 2번씩 돌린 것이므로, postgresql에서도 data는 이미 memory에 cache된 상태라서 disk 병목은 전혀 없다고 보셔도 됩니다.

-bash-4.2$ time psql -d dss -f /home/data/RunQuery/q16.sql > /tmp/ooo

real    1m1.267s
user    0m0.374s
sys     0m0.000s

[root@localhost ~]# time /opt/gpudb/bin/kisql -host 127.0.0.1 -f /home/data/RunQuery/q16.sql > /tmp/iii

real    0m3.046s
user    0m3.729s
sys     0m0.770s

특히 Kinetica의 경우는 아래처럼 query 결과에 Query Execution Time과 Data Transfer Time을 각각 명기합니다.   GPUdb에서는 query execution보다 data를 transfer하는데 더 많은 시간이 걸린다는 것을 보실 수 있습니다.  이런 것 때문에 특히 CPU와 GPU간의 연결이 CPU냐 GPU냐가 매우 중요합니다.

| Brand#54   | ECONOMY PLATED BRASS        |       14 |              4 |
| Brand#55   | STANDARD BURNISHED BRASS    |       14 |              4 |
+------------+-----------------------------+----------+----------------+
Rows read = 27840
Query Execution Time: 0.495 s
Data Transfer Time: 2.283 s


이 시스템에 CPU core는 32개나 있지만 GPU는 딱 2장 뿐입니다.  이럴 경우 저런 query를 10개씩 병렬로 수행하면 CPU가 더 빠를 수도 있지 않을까요 ?  꼭 그렇지는 않습니다.

여기서는 TPC-H의 16번과 19번 query 2개를 5번씩, 총 10개를 한꺼번에 수행하는 방식으로 돌리겠습니다.

Postgres에서 돌릴 script는 다음과 같습니다.

-bash-4.2$ cat 1.sh
date
echo "Starting"
psql -d dss -f /home/data/RunQuery/q16.sql &
psql -d dss -f /home/data/RunQuery/q19.sql &
...(총 10줄)
wait
date
echo "Finished"

Kinetica에서 돌릴 script도 사실상 똑같은 내용입니다.

[root@localhost ~]# cat /tmp/1.sh
date
echo "Starting"
/opt/gpudb/bin/kisql -host 127.0.0.1 -f /home/data/RunQuery/q16.sql &
/opt/gpudb/bin/kisql -host 127.0.0.1 -f /home/data/RunQuery/q19.sql &
/opt/gpudb/bin/kisql -host 127.0.0.1 -f /home/data/RunQuery/q16.sql &
/opt/gpudb/bin/kisql -host 127.0.0.1 -f /home/data/RunQuery/q19.sql &
...  (총 10줄)
wait
date
echo "Finished"


각각의 수행 결과는 다음과 같습니다.   Kinetica도 GPU가 2장 뿐인 환경에서도 10개의 동시 query를 잘 수행해내며, 여전히 6배 정도 빠릅니다.

Postgresql는 다음과 같습니다.

-bash-4.2$ time ./1.sh > p.txt 

real    1m6.947s
user    0m1.891s
sys     0m0.055s

Kinetica는 다음과 같습니다.

[root@localhost ~]# time /tmp/1.sh > /tmp/k.txt   

real    0m8.545s
user    0m29.965s
sys     0m20.525s



Kinetica에서 이 query를 처리하기 전에, nvprof를 이용해 profile data를 받기 위해서 Kinetica의 구동 script 중 일부를 수정한 뒤 restart 해주어야 합니다.  수정은 다음과 같이 간단합니다.  아래 붉은 색 부분의 명령어를 삽입해주기만 하면 됩니다.

[root@localhost ~]# vi /opt/gpudb/core/bin/gpudb
...
#    nohup $HOST_MANAGER_CMD >> $HOST_MANAGER_LOG_FILENAME 2>&1 &
    nohup /usr/local/cuda-9.2/bin/nvprof --log-file /tmp/nvprof/%p.txt --export-profile /tmp/nvprof/%p.nvvp --print-gpu-trace --profile-child-processes $HOST_MANAGER_CMD >> $HOST_MANAGER_LOG_FILENAME 2>&1 &
...





이렇게 얻은 profile data를 분석해보면, GPUdb의 대표적인병목인 cuda memcpy HtoD (CPU to GPU) 및 DtoH (GPU to CPU)의 사용되는 대역폭을 보실 수 있습니다.   대략 35GB/sec에 달합니다.   참고로, POWER9과 V100 GPU 사이를 연결하는 NVLink는 3개의 port를 aggregate시킨 것이라서, 이론상 단방향 대역폭이 75GB/sec (양방향 150GB/sec)에 달합니다.  PCIe Gen3 버스의 이론상 단방향 대역폭이 16GB/sec(양방향 32GB/sec)에 불과하다는 것을 생각하면, Kinetica와 같은 GPUdb를 사용할 경우 CPU와 GPU의 연결은 반드시 NVLink가 되어야 한다는 것을 쉽게 아실 수 있습니다.

2018년 10월 15일 월요일

Python Image Library(PIL, Pillow)의 성능 테스트

Python Image Library (PIL, python3에서는 pillow)을 이용하여 python에서 image 생성/변경 등이 가능합니다.  그리고, 이 성능 평가를 위해서는 pillow-perf라는 code를 이용할 수 있습니다.   먼저 pillow-perf를 git으로부터 clone 합니다.

[root@python1 ~]# git clone https://github.com/python-pillow/pillow-perf.git

아래와 같이 pip로 필요 package들을 설치합니다.

[root@python1 ~]# cd pillow-perf/testsuite/

[root@python1 testsuite]# pip install -r ./requirements.txt

이제 다음과 같이 몇가지 테스트를 수행해봅니다.   아래의 결과는 Naver nCloud에서 제공되는 E5-2660 v4@2.00GHz에서 수행된 것입니다.  이 테스트는 single-thread로 되어 있고, IBM POWER8/POWER9에서도 동일한 방법으로 수행할 수 있습니다.

[root@python1 testsuite]#  ./run.py scale --progress

Scale 2560×1600 RGB image
    to 26x16 bil        0.01323 s   309.62 Mpx/s
    to 26x16 bic        0.02488 s   164.62 Mpx/s
    to 26x16 lzs        0.03783 s   108.28 Mpx/s
    to 320x200 bil      0.02107 s   194.43 Mpx/s
    to 320x200 bic      0.03579 s   114.45 Mpx/s
    to 320x200 lzs      0.05393 s    75.95 Mpx/s
    to 2048x1280 bil    0.05956 s    68.77 Mpx/s
    to 2048x1280 bic    0.08711 s    47.02 Mpx/s
    to 2048x1280 lzs    0.12037 s    34.03 Mpx/s
    to 5478x3424 bil    0.28072 s    14.59 Mpx/s
    to 5478x3424 bic    0.38422 s    10.66 Mpx/s
    to 5478x3424 lzs    0.49239 s     8.32 Mpx/s

[root@python1 testsuite]# ./run.py scale --mode RGBA

Scale 2560×1600 RGBA image
    to 26x16 bil        0.02508 s   163.33 Mpx/s
    to 26x16 bic        0.04012 s   102.09 Mpx/s
    to 26x16 lzs        0.05564 s    73.62 Mpx/s
    to 320x200 bil      0.03434 s   119.29 Mpx/s
    to 320x200 bic      0.04993 s    82.04 Mpx/s
    to 320x200 lzs      0.07300 s    56.11 Mpx/s
    to 2048x1280 bil    0.10238 s    40.01 Mpx/s
    to 2048x1280 bic    0.13699 s    29.90 Mpx/s
    to 2048x1280 lzs    0.18249 s    22.44 Mpx/s
    to 5478x3424 bil    0.51842 s     7.90 Mpx/s
    to 5478x3424 bic    0.63934 s     6.41 Mpx/s
    to 5478x3424 lzs    0.78471 s     5.22 Mpx/s

[root@python1 testsuite]# ./run.py scale --runs 50

Scale 2560×1600 RGB image
    to 26x16 bil        0.01308 s   313.08 Mpx/s
    to 26x16 bic        0.02493 s   164.28 Mpx/s
    to 26x16 lzs        0.03770 s   108.64 Mpx/s
    to 320x200 bil      0.02115 s   193.62 Mpx/s
    to 320x200 bic      0.03549 s   115.42 Mpx/s
    to 320x200 lzs      0.05399 s    75.87 Mpx/s
    to 2048x1280 bil    0.05944 s    68.91 Mpx/s
    to 2048x1280 bic    0.08735 s    46.89 Mpx/s
    to 2048x1280 lzs    0.12058 s    33.97 Mpx/s
    to 5478x3424 bil    0.28272 s    14.49 Mpx/s
    to 5478x3424 bic    0.38564 s    10.62 Mpx/s
    to 5478x3424 lzs    0.49298 s     8.31 Mpx/s

[root@python1 testsuite]# ./run.py blur

Blur 2560×1600 RGB image
    1px                 0.22742 s    18.01 Mpx/s
    10px                0.22500 s    18.20 Mpx/s
    30px                0.22543 s    18.17 Mpx/s

[root@python1 testsuite]# ./run.py convert

Convert 2560×1600 RGB image
    RGB to L            0.00539 s   760.07 Mpx/s
    RGBA to LA          0.00652 s   627.90 Mpx/s
    RGBa to RGBA        0.03590 s   114.08 Mpx/s
    RGBA to RGBa        0.00851 s   481.57 Mpx/s


그 외에, 실제 jpg 파일들을 이용하여 몇가지 PIL 처리를 하는 sample code를 수행해보는 것도 괜찮습니다.

[root@python1 ~]# cat piljpg.py
from PIL import Image, ImageFilter
list = [ "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg" ]
for x in list:
    im = Image.open(x)
    print(im.size)
    im.save('pasta.jpg')
    im = Image.open(x)
    size = (64, 64)
    im.thumbnail(size)
    im.save('python-thumb.jpg')
    im = Image.open(x)
    cropImage = im.crop((100, 100, 150, 150))
    cropImage.save('python-crop.png')
    im = Image.open(x)
    img2 = im.resize((600, 600))
    img2.save('python-600.jpg')
    img3 = im.rotate(90)
    img3.save('rotate.jpg')
    im = Image.open(x)
    blurImage = im.filter(ImageFilter.BLUR)
    blurImage.save('python-blur.jpg')


[root@python1 ~]# time python piljpg.py
(2281, 3072)
(972, 1422)
(972, 1422)
(972, 1422)
(972, 1422)
(972, 1422)
(5040, 4912)
(1600, 1280)
(2281, 3072)
(972, 1422)

real    0m15.047s
user    0m14.759s
sys     0m0.285s


piljpg.py의 코드와 거기에 쓰인 *.jpg 파일은 아래 구글 드라이브에 올려놓았습니다.

https://drive.google.com/open?id=1LdjcDXrQ3Dps0ertGwGMyMYKRhxo5FEB

2018년 10월 11일 목요일

Python을 위한 local private repository 만들기 (bandersnatch + nginx)

Python은 그 특성상 필요에 따라 자주 새로운 package를 설치해서 써야 하는데, 이를 위해서는 해당 서버가 443번 포트를 통해 인터넷에 연결되어 있는 환경이어야 합니다.  그러나 실제로는 대부분의 기업용 data center들에서는 인터넷과의 연결이 완전히 단절되어 있으므로 python을 쓰기에 매우 불편합니다.   이는 x86이든 ppc64le이든 arm이든 모든 CPU 아키텍처에서 공통적으로 골치아파하는 문제이며, 국내 대기업들은 물론 해외 유명 인터넷 업체에서도 다 공통적으로 겪는 문제입니다.  가령 Pillow라는 package를 import해서 쓰려면 먼저 그 package를 설치해야 하는데, pip 명령으로 설치하면 아래처럼 그때그때 인터넷에서 source를 download 받아서 즉석에서 build해서 설치합니다.


[bsyu@p57a22 ~]$ pip install Pillow
Collecting Pillow
  Downloading https://files.pythonhosted.org/packages/1b/e1/1118d60e9946e4e77872b69c58bc2f28448ec02c99a2ce456cd1a272c5fd/Pillow-5.3.0.tar.gz (15.6MB)
    100% |████████████████████████████████| 15.6MB 11.6MB/s
Building wheels for collected packages: Pillow
  Running setup.py bdist_wheel for Pillow ... done
  Stored in directory: /home/bsyu/.cache/pip/wheels/df/81/28/47e761b5e307472ba7c2c5ced6e52037bbefe33c9c4b2a627e
Successfully built Pillow
Installing collected packages: Pillow
Successfully installed Pillow-5.3.0


이런 문제를 해결하는 왕도는 따로 없으나, 가장 근본적인 해결책은 외부망과 분리된 data center 내에 python을 위한 private local repository를 구성하는 것입니다.   다만 여기에는 2가지 문제가 있습니다.

1) Public python repository는 pypi.org (실제로는 files.pythonhosted.org) 인데, 전체 repository 크기가 800GB가 넘고, 날마다 계속 커지고 있습니다.

2) 기존 python package들의 새버전과, 또 아예 새로 만들어지는 python package들이 날마다 쏟아져 나오므로 주기적으로 자주 update를 해줘야 합니다.

보통 1번 문제는 (돈 있는 기업들 입장에서는) 큰 문제가 아닙니다.  Local repository server에 넉넉한 크기의 disk를 쓰면 되기 때문입니다.   문제는 2번 문제인데, 이것도 뾰족한 방법은 없고 외부에서 (1달에 1번 정도) 주기적으로 pypi.org의 내용을 backup 받아서 USB 외장 disk에 담아 data center로 들여온 뒤 local repository server에 부어주는 수 밖에 없습니다.

이런 정책만 정해지면 local repository server를 구성하는 것은 그다지 어렵지 않습니다.    다음과 같이 bandersnatch라는 package를 이용하면 쉽습니다.

아래의 모든 테스트는 ppc64le 아키텍처인 IBM POWER8 processor와 Redhat 7.5를 이용해서 수행되었습니다.  혹자는 x86_64를 위한 python repository는 어디 있는지 알겠는데 ppc64le를 위한 것은 어디 있냐고 여쭈시는 분도 있습니다만,  x86이나 ppc64le나 aarch64나 모두 같은 repository를 사용합니다. 


먼저, 다음과 같이 Anaconda3가 설치된 환경에서 pip로 bandersnatch를 설치합니다. 

[bsyu@p57a22 ~]$ which pip
~/anaconda3/bin/pip

[bsyu@p57a22 ~]$ pip install -r https://bitbucket.org/pypa/bandersnatch/raw/stable/requirements.txt
...
Successfully installed apipkg-1.4 bandersnatch-2.0.0 coverage-4.3.1 execnet-1.4.1 mock-2.0.0 packaging-16.8 pbr-1.10.0 pep8-1.7.0 py-1.4.32 pyflakes-1.3.0 pyparsing-2.1.10 pytest-3.0.5 pytest-cache-1.0 pytest-catchlog-1.2.2 pytest-codecheckers-0.2 pytest-cov-2.4.0 pytest-timeout-1.2.0 python-dateutil-2.6.0 requests-2.12.4 setuptools-33.1.1 six-1.10.0 xmlrpc2-0.3.1

[bsyu@p57a22 ~]$ which bandersnatch
~/anaconda3/bin/bandersnatch

그리고 'bandersnatch mirror' 명령을 수행합니다.   그러면 기본 /etc/bandersnatch.conf을 만들어줍니다.  이 때문에 이 명령은 일단 sudo 권한이 필요합니다.

[bsyu@p57a22 ~]$ sudo /home/bsyu/anaconda3/bin/bandersnatch mirror
2018-10-10 03:31:32,069 WARNING: Config file '/etc/bandersnatch.conf' missing, creating default config.
2018-10-10 03:31:32,069 WARNING: Please review the config file, then run 'bandersnatch' again.

이 /etc/bandersnatch.conf를 필요에 따라 수정합니다.  여기서는 어느 directory에 python repository를 내려받을 것인지만 수정했습니다.

[bsyu@p57a22 ~]$ sudo vi /etc/bandersnatch.conf
...
; directory = /srv/pypi
directory = /home/bsyu/files/pypi
...

다시 'bandersnatch mirror' 명령을 수행하면 정규 public repository (files.pythonhosted.org)를 통째로 download 받습니다.   800GB가 넘는다는 점에 유의하시기 바랍니다.  저도 끝까지 download 받아본 적은 없고, disk가 부족하여 도중에 끊어야 했습니다.

[bsyu@p57a22 ~]$ bandersnatch mirror
2018-10-10 03:33:35,064 INFO: bandersnatch/2.0.0 (cpython 3.7.0-final0, Linux ppc64le)
2018-10-10 03:33:35,064 INFO: Setting up mirror directory: /home/bsyu/files/pypi/
2018-10-10 03:33:35,064 INFO: Setting up mirror directory: /home/bsyu/files/pypi/web/simple
2018-10-10 03:33:35,064 INFO: Setting up mirror directory: /home/bsyu/files/pypi/web/packages
2018-10-10 03:33:35,064 INFO: Setting up mirror directory: /home/bsyu/files/pypi/web/local-stats/days
2018-10-10 03:33:35,064 INFO: Generation file missing. Reinitialising status files.
2018-10-10 03:33:35,065 INFO: Status file missing. Starting over.
2018-10-10 03:33:35,065 INFO: Syncing with https://pypi.python.org.
2018-10-10 03:33:35,065 INFO: Current mirror serial: 0
2018-10-10 03:33:35,065 INFO: Syncing all packages.
2018-10-10 03:33:37,934 INFO: Trying to reach serial: 4358961
2018-10-10 03:33:37,934 INFO: 154638 packages to sync.
...
2018-10-10 03:53:50,360 INFO: Downloading: https://files.pythonhosted.org/packages/8a/5c/625ac1a93da3a672f52d947023770331b958a1100cd9889b727cde5f7ba5/CommonMark-0.7.5-py2.py3-none-any.whl
2018-10-10 03:53:50,360 DEBUG: Getting https://files.pythonhosted.org/packages/8a/5c/625ac1a93da3a672f52d947023770331b958a1100cd9889b727cde5f7ba5/CommonMark-0.7.5-py2.py3-none-any.whl (serial None)
2018-10-10 03:53:50,363 INFO: Syncing package: CompCamps-Cash-Api (serial 4042164)
2018-10-10 03:53:50,363 DEBUG: Getting /pypi/CompCamps-Cash-Api/json (serial 4042164)
2018-10-10 03:53:50,370 INFO: Downloading: https://files.pythonhosted.org/packages/77/16/44a297228a439484d049cdad818c7f6691c162b4cd741c619caeb208bb1e/CommonMark-0.7.5.tar.gz
2018-10-10 03:53:50,371 DEBUG: Getting https://files.pythonhosted.org/packages/77/16/44a297228a439484d049cdad818c7f6691c162b4cd741c619caeb208bb1e/CommonMark-0.7.5.tar.gz (serial None)
...

이제 해당 directory에 가보면 다음과 같이 web/packages 밑에 실제 python package 파일들이 download 되어진 것을 보실 수 있습니다.   web/simple은 그 파일들에 대한 index directory입니다.

[bsyu@p57a22 ~]$ cd files/pypi

[bsyu@p57a22 pypi]$ ls
generation  todo  web

[bsyu@p57a22 pypi]$ cd web

[bsyu@p57a22 web]$ ls
local-stats  packages  simple

[bsyu@p57a22 web]$ cd packages/

[bsyu@p57a22 packages]$ ls
00  0b  16  21  2c  37  42  4e  59  64  6f  7a  85  90  9b  a6  b1  bc  c7  d2  dd  e8  f3  fe
01  0c  17  22  2d  38  43  4f  5a  65  70  7b  86  91  9c  a7  b2  bd  c8  d3  de  e9  f4  ff
02  0d  18  23  2e  39  44  50  5b  66  71  7c  87  92  9d  a8  b3  be  c9  d4  df  ea  f5
03  0e  19  24  2f  3a  45  51  5c  67  72  7d  88  93  9e  a9  b4  bf  ca  d5  e0  eb  f6
04  0f  1a  25  30  3b  46  52  5d  68  73  7e  89  94  9f  aa  b5  c0  cb  d6  e1  ec  f7
05  10  1b  26  31  3c  47  53  5e  69  74  7f  8a  95  a0  ab  b6  c1  cc  d7  e2  ed  f8
06  11  1c  27  32  3d  49  54  5f  6a  75  80  8b  96  a1  ac  b7  c2  cd  d8  e3  ee  f9
07  12  1d  28  33  3e  4a  55  60  6b  76  81  8c  97  a2  ad  b8  c3  ce  d9  e4  ef  fa
08  13  1e  29  34  3f  4b  56  61  6c  77  82  8d  98  a3  ae  b9  c4  cf  da  e5  f0  fb
09  14  1f  2a  35  40  4c  57  62  6d  78  83  8e  99  a4  af  ba  c5  d0  db  e6  f1  fc
0a  15  20  2b  36  41  4d  58  63  6e  79  84  8f  9a  a5  b0  bb  c6  d1  dc  e7  f2  fd


이렇게 download된 generation todo web의 3개 directory를 포함하는 python repository directory 전체 (여기서는 /home/bsyu/files/pypi)를 USB 외장 disk 등에 복사하여 local repository로 사용할 서버에 옮깁니다.   여기서는 편의상 그냥 download 받은 서버에서 직접 local repository 서버를 구성하겠습니다.

https을 통해서 다른 서버들에게 python repository 서비스를 하기 위해서는 당연히 web 서버를 구성해야 합니다.  여기서는 간단하게 nginx를 사용하겠습니다.  먼저 nginx를 YUM 명령으로 설치한 뒤, /etc/nginx/nginx.conf를 수정합니다.  단, 여기서 http(80번 포트)가 아닌 https(443번 포트)를 구성해야 합니다.  그리고 root를 복사한 directory 중 'web' directory로 정해야 하고, SSL certificate과 그 key도 등록해야 합니다.   server_name은 FQDN(Fully Qualified Domain Name)으로 합니다.  여기서는 IP address로 했습니다.

[bsyu@p57a22 ~]$ sudo yum install nginx

[bsyu@p57a22 ~]$ sudo vi /etc/nginx/nginx.conf
...
    server {
        listen       443 ssl http2 default_server;
        listen       [::]:443 ssl http2 default_server;
#        root         /usr/share/nginx/html;
        root         /home/bsyu/files/pypi/web;
        autoindex on;
        charset utf-8;

#        server_name  _;
        server_name  129.40.xx.xx;
        ssl_certificate "/etc/pki/nginx/private/domain.crt";
        ssl_certificate_key "/etc/pki/nginx/private/domain.key";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
...

이제 SSL 구성을 위해 crt와 key를 생성합니다.  여기서는 1년간 유효한 Self-Signed Certificate (x509)를 만들었습니다.

[bsyu@p57a22 ~]$ cd /etc/pki/nginx/private

[root@p57a22 private]# openssl req -newkey rsa:2048 -nodes -keyout domain.key -x509 -days 365 -out domain.crt
Generating a 2048 bit RSA private key
.......................................................................................+++++
..............................+++++
writing new private key to 'domain.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:129.40.xx.xx
Email Address []:.

[root@p57a22 private]# ls -ltr
total 8
-rw-r--r-- 1 root root 1704 Oct 11 00:13 domain.key
-rw-r--r-- 1 root root 1107 Oct 11 00:13 domain.crt

생성된 certificate과 key의 검사는 아래와 같이 할 수 있습니다. 

[root@p57a22 private]# openssl x509 -text -noout -in domain.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            a7:03:d9:7b:33:2d:53:7c
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=129.40.xx.xx
        Validity
            Not Before: Oct 11 04:13:00 2018 GMT
            Not After : Oct 11 04:13:00 2019 GMT
        Subject: CN=129.40.116.82
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ca:73:3c:7f:e6:29:1e:5e:7b:ff:b5:98:30:ce:
                    fb:48:0e:bc:96:fd:5b:7f:1e:23:e5:62:8f:74:8e:
 ...

[root@p57a22 private]# openssl rsa -check -in domain.key
RSA key ok
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAynM8f+YpHl57/7WYMM77SA68lv1bfx4j5WKPdI6ftW2lRdrw
fUikVx0C+2ni3QB6y/xuT8yT0eiCPT5Ak4yGpDSsfcfzDOFgVSB02irWmX/KUNIS
/zS+E7SkAfariUEFa8iRjt2kmDpi65YGKH9NY7p136NcZOSZQx2wsAU0UM5Pjtci
....

이제 생성된 certificate과 key를 합해 PEM을 만듭니다. 

[root@p57a22 private]# cat domain.crt domain.key >> domain.pem

이제 nginx를 start 합니다.

[root@p57a22 private]# systemctl status nginx

그리고 pip에서 이 129.40.xx.xx를 trusted-host로 사용하도록 ~/.pip/pip.conf를 생성하여 다음과 같은 내용을 넣어줍니다.

[bsyu@p57a22 ~]$ vi .pip/pip.conf
[global]
trusted-host = 129.40.xx.xx
index = https://129.40.xx.xx:443/packages
index-url = https://129.40.xx.xx:443/simple
cert = /etc/pki/nginx/private/domain.pem

이제 CommonMark라는 package를 설치해봅니다.   기존처럼 files.pythonhosted.org가 아니라 129.40.xx.xx에서 package source를 가져오는 것을 보실 수 있습니다.

[bsyu@p57a22 ~]$ pip install CommonMark
Looking in indexes: https://129.40.xx.xx:443/simple
Collecting CommonMark
  Downloading https://129.40.xx.xx:443/packages/ab/ca/439c88039583a29564a0043186875258e9a4f041fb5c422cd387b8e10175/commonmark-0.8.1-py2.py3-none-any.whl (47kB)
    100% |████████████████████████████████| 51kB 37.8MB/s
Requirement already satisfied: future in ./anaconda3/lib/python3.7/site-packages (from CommonMark) (0.16.0)
Installing collected packages: CommonMark
Successfully installed CommonMark-0.8.1