레이블이 mpirun인 게시물을 표시합니다. 모든 게시물 표시
레이블이 mpirun인 게시물을 표시합니다. 모든 게시물 표시

2020년 2월 13일 목요일

IBM AC922 서버에서 CUDA-enabled HPL 수행하기


HPL (High Performance Linpack) 테스트는 수퍼컴 클러스터의 성능 측정에 널리 쓰이는 오픈소스 프로그램입니다.   GPU를 사용하여 HPL을 수행하기 위해서는 CUDA-enabled HPL이 필요한데, 그건 NVIDIA가 지적 재산권을 가진 프로그램이며 그건 오픈소스가 아닙니다.  WWW 상을 뒤져보면 CUDA-enabled HPL의 source code를 NVIDIA가 공개하기는 하는데, 그건 매우 오래된 GPU architecture인 Fermi 아키텍처의 GPU에 대한 것이라서 최신 GPU의 성능 측정에는 적절하지 않습니다.

아래에서는 NVIDIA의 협조를 받아 CUDA-enabled HPL의 executible binary file을 가지고 있다는 전제 하에 IBM AC922 서버 (POWER9 * 2, V100 SXM2 32GB GPU * 4) 1대로 CUDA-enabled HPL을 수행하는 과정만 제시합니다.

그 결과는 역시 confidential 정보라 공개하지 못하는 점 양해 부탁드립니다.

이 테스트 수행을 위해서는 서버에 먼저 CUDA 10.1이 설치되어 있어야 합니다.  또한 IBM의 XL Fortran, Spectrum MPI, ESSL 등의 library 등이 필요합니다.

[cecuser@p1235-met1 HPC]$ ls
ESSL_FOR_LINUX_ON_POWER_V6.2.0.tar.gz
hpl_cuda10.1.4gpus.tgz
IBM_SMPI_10.2_IP_GR_LINUX_PPC64LE.tgz
ibm_smpi_lic_s-10.02-p9-ppc64le.rpm
lsf10.1_lnx310-lib217-ppc64le.tar.Z
XL_FORTRAN_FOR_LINUX_V16.1.1_PRO.gz

먼저 XL Fortran을 설치합니다.

[cecuser@p1235-met1 HPC]$ mkdir xlf

[cecuser@p1235-met1 HPC]$ cd xlf

[cecuser@p1235-met1 xlf]$ tar -zxvf ../XL_FORTRAN_FOR_LINUX_V16.1.1_PRO.gz

[cecuser@p1235-met1 xlf]$ ./install
...
Press Enter to continue viewing the license agreement, or, Enter "1" to accept the agreement,
"2" to decline it or "99" to go back to the previous screen, "3" Print.
1
INFORMATIONAL: Unexpected CUDA Toolkit version detected '10.1' (9.2, 10.0 are supported), defaulting to __CUDA_API_VERSION=10000.  Re-configure with '-cudaVersion 9.2' to override.
Installation and configuration successful


이어서 Spectrum MPI를 설치합니다.  10.1이 아니라 10.2가 필요합니다.

[cecuser@p1235-met1 HPC]$ tar -zxf IBM_SMPI_10.2_IP_GR_LINUX_PPC64LE.tgz

[cecuser@p1235-met1 HPC]$ cd ibm_smpi-10.02.00.03-p9-ppc64le

[cecuser@p1235-met1 ibm_smpi-10.02.00.03-p9-ppc64le]$ sudo rpm -Uvh *.rpm ../ibm_smpi_lic_s-10.02-p9-ppc64le.rpm

[cecuser@p1235-met1 ibm_smpi-10.02.00.03-p9-ppc64le]$ su -

[root@p1235-met1 ~]# IBM_SPECTRUM_MPI_LICENSE_ACCEPT=yes /opt/ibm/spectrum_mpi/lap_se/bin/accept_spectrum_mpi_license.sh

[root@p1235-met1 ~]# exit

이어서 ESSL을 설치합니다.  이건 engineering용 library인데, GPU를 이용하도록 되어 있습니다.

[cecuser@p1235-met1 HPC]$ tar -zxvf ESSL_FOR_LINUX_ON_POWER_V6.2.0.tar.gz

[cecuser@p1235-met1 HPC]$ cd RHEL/RHEL7/

[cecuser@p1235-met1 RHEL7]$ su
Password:

[root@p1235-met1 RHEL7]# rpm -Uvh essl.license-6.2.0-0.ppc64le.rpm

[root@p1235-met1 RHEL7]# export IBM_ESSL_LICENSE_ACCEPT=yes

[root@p1235-met1 RHEL7]# /opt/ibmmath/essl/6.2/lap/accept_essl_license.sh

[root@p1235-met1 RHEL7]# rpm -Uvh essl.3264.rte-6.2.0-0.ppc64le.rpm essl.6464.rte-6.2.0-0.ppc64le.rpm essl.rte.common-6.2.0-0.ppc64le.rpm essl.man-6.2.0-0.ppc64le.rpm essl.3264.rtecuda-6.2.0-0.ppc64le.rpm essl.common-6.2.0-0.ppc64le.rpm essl.msg-6.2.0-0.ppc64le.rpm essl.rte-6.2.0-0.ppc64le.rpm


이제 CUDA-enabled HPL의 binary 및 script를 풀어냅니다.

[cecuser@p1235-met1 HPC]$ tar -zxvf hpl_cuda10.1.4gpus.tgz

[cecuser@p1235-met1 HPC]$ cd hpl

이 속에 들어있는 것은 간단합니다.  Binary 실행 파일인 xhpl과 함께, 그 수행에 필요한 HPL.dat, 기타 mpirun을 위한 script 입니다.

먼저 HPL.dat의 내용입니다.  Edit해야 하는 주요 내용은 아래 붉은 색으로 표시한 Ns (계산해야 하는 문제의 크기), NBs (한번에 어느 정도 크기로 문제를 풀 것인지 결정하는 block size), 그리고 mesh 구조를 결정하는 Ps와 Qs입니다. 

간단히 말하면 Ns는 가급적 GPU들의 메모리를 꽤 가득 채울 정도로 크게 하고, NBs는 적절한 크기를 trial & error 방식으로 찾아야 합니다.  가령 제가 해보니 아래와 같은 크기의 Ns면 32GB memory의 GPU 4장을 가득 채웁니다.  또한 256이나 768에 비해 512로 NBs를 두는 것이 가장 성능이 잘 나오는 것 같습니다.  Ps와 Qs는 서로 곱해서 GPU 갯수가 나오면 되는데, 가급적 서로 비슷하게, 그리고 가급적 Ps가 Qs보다 작게 설정하면 됩니다.

[cecuser@p1235-met1 hpl]$ cat HPL.dat
HPLinpack benchmark input file
Innovative Computing Laboratory, University of Tennessee
HPL.out      output file name (if any)
6            device out (6=stdout,7=stderr,file)
1            # of problems sizes (N)
128000       Ns
1          # of NBs
512        NBs
0            PMAP process mapping (0=Row-,1=Column-major)
1            # of process grids (P x Q)
2            Ps
2            Qs
16.0         threshold
1            # of panel fact
2            PFACTs (0=left, 1=Crout, 2=Right)
1            # of recursive stopping criterium
4            NBMINs (>= 1)
1            # of panels in recursion
2            NDIVs
1            # of recursive panel fact.
0            RFACTs (0=left, 1=Crout, 2=Right)
1            # of broadcast
3            BCASTs (0=1rg,1=1rM,2=2rg,3=2rM,4=Lng,5=LnM)
1            # of lookahead depth
0            DEPTHs (>=0)
1            SWAP (0=bin-exch,1=long,2=mix)
192          swapping threshold
1            L1 in (0=transposed,1=no-transposed) form
0            U  in (0=transposed,1=no-transposed) form
0            Equilibration (0=no,1=yes)
8            memory alignment in double (> 0)

여기서는 1대로 수행하니까 hosts 파일은 사실 필요가 없습니다만 아래와 같은 format으로 설정하면 됩니다.

[cecuser@p1235-met1 hpl]$ cat hosts
localhost  slots=4

아래는 mpirun을 수행하는 script입니다.  제가 쓴 환경처럼 infiniband가 없는 경우 "-pami_noib" 옵션을 써야 합니다.

[cecuser@p1235-met1 hpl]$ cat run_me_4_gpu_xlc_spectrum.sh
#!/bin/bash
export MPI_ROOT=/opt/ibm/spectrum_mpi
export MANPATH=$MPI_ROOT/share/man:$MANPATH
export PATH=/usr/local/cuda-10.1/bin:/opt/ibm/spectrum_mpi/bin:$PATH
export LD_LIBRARY_PATH=/opt/ibmmath/essl/6.2/lib64/:/opt/ibm/spectrum_mpi/lib:$LD_LIBRARY_PATH
sudo nvidia-smi -ac 877,1395
#echo always > /sys/kernel/mm/transparent_hugepage/enabled
TUNE="-x PAMI_IBV_DEVICE_NAME=mlx5_0:1 -x PAMI_IBV_DEVICE_NAME_1=mlx5_3:1 -x PAMI_ENABLE_STRIPING=0 -x PAMI_IBV_CQEDEPTH=4096 -x PAMI_IBV_ADAPTER_AFFINITY=1 -x PAMI_IBV_OPT_LATENCY=1 -x MLX5_SINGLE_THREADED=1 -x MLX5_CQE_SIZE=128 -x PAMI_IBV_ENABLE_DCT=1 -x PAMI_IBV_ENABLE_OOO_AR=1 -x PAMI_IBV_QP_SERVICE_LEVEL=8"
sudo ppc64_cpu --dscr=7
#mpirun -N 4 -npernode 4 --allow-run-as-root -x OMPI_MCA_common_pami_use_odp=0 -x PAMI_IBV_DEBUG_PRINT_DEVICES=1 -tag-output $TUNE --hostfile nodes -bind-to none ./run_linpack_6_gpu_xlc_spectrum_0726
mpirun -N 4 -npernode 4 --hostfile hosts -pami_noib -bind-to none ./run_linpack_4_gpu_xlc_spectrum.sh


그리고 아래가 실제 xhpl을 수행하는 script입니다.  위의 mpirun script를 수행하면 결국 아래의 script가 수행됩니다.  IBM의 Spectrum MPI에서는 내부적으로 OMPI_COMM_WORLD_LOCAL_RANK, PMIX 등의 환경 변수를 자동 생성하여 GPU를 할당하는데 사용합니다.  아래 script를 보면 case 문을 이용하여 CUDA_VISIBLE_DEVICES 환경 변수를 이용하여 GPU 1개씩마다 xhpl을 하나씩 수행합니다.


[cecuser@p1235-met1 hpl]$ cat run_linpack_4_gpu_xlc_spectrum.sh
#!/bin/bash
#location of HPL
HPL_DIR=`pwd`
# Number of CPU cores
# Total CPU cores / Total GPUs (not counting hyperthreading)
#CPU_CORES_PER_RANK=16
CPU_CORES_PER_RANK=8
export MPI_ROOT=/opt/ibm/spectrum_mpi
export OMP_NUM_THREADS=$CPU_CORES_PER_RANK
export MAX_H2D_MS=10
export MAX_D2H_MS=10
export RANKS_PER_SOCKET=2
export RANKS_PER_NODE=4
export NUM_WORK_BUF=4
export SCHUNK_SIZE=128
export GRID_STRIPE=4
export FACT_GEMM=1
export FACT_GEMM_MIN=128
export SORT_RANKS=0
export PRINT_SCALE=1.0
export TEST_SYSTEM_PARAMS=1
sudo rm -rf /dev/shm/sh_*
export LIBC_FATAL_STDERR_=1
#export PAMI_ENABLE_STRIPING=0
export CUDA_CACHE_PATH=/tmp
export OMP_NUM_THREADS=$CPU_CORES_PER_RANK
export CUDA_DEVICE_MAX_CONNECTIONS=8
export CUDA_COPY_SPLIT_THRESHOLD_MB=1
export GPU_DGEMM_SPLIT=1.0
export TRSM_CUTOFF=1000000
#export TRSM_CUTOFF=99000
export TEST_SYSTEM_PARAMS=1
export MONITOR_GPU=1
export GPU_TEMP_WARNING=70
export GPU_CLOCK_WARNING=1310
export GPU_POWER_WARNING=350
export GPU_PCIE_GEN_WARNING=3
export GPU_PCIE_WIDTH_WARNING=2
#export ICHUNK_SIZE=1536
export ICHUNK_SIZE=384
export CHUNK_SIZE=5120
APP=$HPL_DIR/xhpl
#lrank=$OMPI_COMM_WORLD_LOCAL_RANK
lrank=$(($PMIX_RANK%4))
nrank=$(($PMIX_RANK/4))
#crank=$(($nrank/89))
#neven=$(($crank%2))
neven=$(($nrank%2))
#neven=0
export CUDA_VISIBLE_DEVICES=$lrank
echo "RANK $PMIX_RANK on host $HOSTNAME PID $$ even: $neven"
if [ $neven -eq 0 ]
then
case ${lrank} in
[0])
#ldd $APP
sudo nvidia-smi -ac 877,1395 > /dev/null;
#export PAMI_IBV_DEVICE_NAME=mlx5_0:1;
#export OMPI_MCA_btl_openib_if_include=mlx5_0:1;
export CUDA_VISIBLE_DEVICES=0; numactl --physcpubind=0,4,8,12,16,20,24,28,32,36 --membind=0 $APP
  ;;
[1])
#export PAMI_IBV_DEVICE_NAME=mlx5_1:1;
#export OMPI_MCA_btl_openib_if_include=mlx5_1:1;
export CUDA_VISIBLE_DEVICES=1; numactl --physcpubind=40,44,48,52,56,60,64,68,72,76 --membind=0 $APP
  ;;
[2])
#export PAMI_IBV_DEVICE_NAME=mlx5_0:1;
#export OMPI_MCA_btl_openib_if_include=mlx5_0:1;
export CUDA_VISIBLE_DEVICES=2; numactl --physcpubind=80,84,88,92,96,100,104,108,112,116 --membind=8 $APP
  ;;
[3])
#export PAMI_IBV_DEVICE_NAME=mlx5_3:1;
#export OMPI_MCA_btl_openib_if_include=mlx5_3:1;
export CUDA_VISIBLE_DEVICES=3; numactl --physcpubind=120,124,128,132,136,140,144,148,152,156 --membind=8 $APP
  ;;
esac
exit
fi


이제 다음과 같이 run_me_4_gpu_xlc_spectrum.sh를 수행하시면 됩니다.  대략 10분 이내의 시간이 걸릴 것입니다.

[cecuser@p1235-met1 hpl]$ ./run_me_4_gpu_xlc_spectrum.sh


중간값을 빼면 결과적으로는 아래와 같은 결과물이 display 됩니다.   결과는 공개하지 못하는 점 다시 한번 양해 부탁드립니다.

...

================================================================================
T/V                N    NB     P     Q               Time                 Gflops
--------------------------------------------------------------------------------
WR03L2R4      128000   512     2     2             XXX              X.XXXe+04
--------------------------------------------------------------------------------
||Ax-b||_oo/(eps*(||A||_oo*||x||_oo+||b||_oo)*N)=        0.0005540 ...... PASSED
================================================================================




2017년 9월 15일 금요일

PowerAI 4.0의 DDL을 이용한 caffe와 tensorflow의 병렬처리

PowerAI 4.0에 포함된 DDL(Distributed Deep Learning)의 구체적인 사용법에 대해서 보시겠습니다.

일단 caffe는 IBM 버전 caffe (caffe-ibm)에 DDL 옵션이 통합되어 있으므로 별도 debian 패키지를 설치할 필요가 없습니다.  이 caffe-ibm도 내부적으로는 OpenMPI를 이용하는 것이므로 관련 library들이 설치되기는 해야 합니다만, 이는 caffe-ibm을 설치할 때 함께 자동으로 설치되므로 따로 신경쓰지 않으셔도 됩니다.

nimbix@JARVICENAE-0A0A1835:/data/mnist$ dpkg -l | grep openmpi
ii  libopenmpi2-cuda:ppc64el               2.0.1-4ibm1                                ppc64el      high performance message passing library -- shared library
ii  openmpi-bin-cuda                       2.0.1-4ibm1                                ppc64el      high performance message passing library -- binaries
ii  openmpi-common-cuda                    2.0.1-4ibm1                                all          high performance message passing library -- common files
ii  openmpi-doc-cuda                       2.0.1-4ibm1                                all          high performance message passing library -- man pages

가령 위에서 보는 것과 같이 CUDA-aware OpenMPI를 설치하고나면, mpirun이라는 MPI utility가 설치됩니다.  이 mpirun이라는 것은 여러단계의 soft link가 걸린 orterun이라는 명령어이고, 결국 아래와 같이 openmpi-bin-cuda에서 제공됩니다.

nimbix@JARVICENAE-0A0A1835:/data/mnist$ dpkg -S /usr/bin/orterun
openmpi-bin-cuda: /usr/bin/orterun

IBM 버전 caffe에서의 DDL 사용법은 알고 보면 단순합니다.  다음 4가지만 아시면 됩니다.

1) caffe 명령을 수행할 때 -ddl 옵션을 준다
2) Train/Validation용 dataset은 모든 서버에서 동일한 위치(directory)에 존재해야 한다  (병렬파일시스템 또는 NFS가 편리)
3) 모든 서버는 암호 없이 ssh가 되도록 ssh-keygen과 ssh-copy-id가 되어 있어야 한다
4) 환경변수 등을 다른 서버 노드에도 전달하기 위해서는 mpirun 명령을 사용하는 것이 편하다

다른 것은 다 쉽습니다만 1)번 항목이 조금 어렵게 느껴질 수도 있습니다.  복잡한 부분은 다 빼고, 그냥 쉽게 보면 이렇습니다.

DDL 옵션을 쓴다고 해서 caffe가 여러분이 가진 GPU서버 및 network 환경을 스스로 이해하고 그에 맞게 자동으로 최적화할 수는 없습니다.  따라서 그런 환경, 즉 topology를 caffe에게 여러분이 직접 알려주셔야 합니다.  그게 -ddl 옵션의 mode입니다.  쉽게 예를 들어 설명하면 다음과 같습니다.

$ mpirun -x PATH -x LD_LIBRARY_PATH -n 12 -rf 4x3.rf caffe train -solver /data/mnist/lenet_solver.prototxt -gpu 0 -bvlc -ddl "-mode n:4x3x1 -dev_sync 1"

- mpirun은 여러대의 서버 노드에 동일한 명령을 동일한 환경변수 (-x 옵션)을 써서 수행해주는 병렬환경 명령어입니다.
- 4x3.rf라는 이름의 파일은 rank file입니다.  이 속에 병렬 서버 환경의 toplogy가 들어있습니다.  이걸 어떻게 만드는지는 아래에서 다루겠습니다.
- -n 12라는 것은 MPI client의 총 숫자이며, 쉽게 말해 training에 이용하려는 GPU의 갯수입니다.
- -gpu 0에서, 왜 12개가 아니라 gpu 0이라고 1개로 지정했는지 의아하실 수 있는데, MPI 환경에서는 각각의 GPU가 하나의 learner가 됩니다.  따라서 실제 물리적 서버 1대에 GPU가 몇 장 장착되어있든 상관없이 모두 -gpu 0, 즉 GPU는 1개로 지정한 것입니다.
- "-mode n:4x3x1"에서 n이라는 것은 NCCL (NVIDIA Collective Communications Library, 니클이라고 읽습니다)을 이용하라는 뜻입니다.  4x3x1은 4장의 GPU를 가진 서버 3대가 하나의 rack에 들어있다는 뜻입니다.  사실 어느 rack에 들어있느냐가 중요한 것은 아닌데, 보통 병렬수퍼컴 환경에서는 한대의 rack 안에 장착된 서버끼리는 좀더 고속의 low latency network으로 연결되어있기 때문에 이렇게 rack 표시까지 해주는 것입니다.  만약 4장의 GPU를 가진 서버가 6대씩 장착된 rack이 5대있다면 4x6x5로 표시됩니다.
- dev_sync에서 0은 GPU간 sync를 하지 말라는 것이고, 1은 통신 시작할 때 sync하라는 뜻, 2는 시작할 때와 끝낼 때 각각 sync하라는 뜻입니다.

잠깐, 데이터는 어디에 있는지 어떻게 지정하냐고요 ?  저 위에 지정된 solver 파일, 즉 lenet_solver.prototxt에 neural network이 지정되어 있고, 다시 그 neural network의 prototxt 파일 속에 데이터 위치가 지정되어 있습니다.   아래처럼요.

$ vi lenet_solver.prototxt
#net: "examples/mnist/lenet_train_test.prototxt"
net: "/data/mnist/lenet_train_test.prototxt"

$ vi lenet_train_test.prototxt
...
#    source: "examples/mnist/mnist_train_lmdb"
    source: "/data/mnist/mnist_train_lmdb"
...
#    source: "examples/mnist/mnist_test_lmdb"
    source: "/data/mnist/mnist_test_lmdb"

여러 서버 노드들의 GPU마다 수행될 learner들이 어떻게 data를 나누어 가져가느냐고요 ?  가급적이면 서버 노드마다 미리 파티셔닝되어 적절히 분배된 data들을 넣어두는 것이 좋습니다.  Data를 N개의 learner들이 읽어갈 때, 각자 순차적으로 파일 이름들이 뒤섞여 들어간 목록으로부터 data를 읽어가는데, 만약 이 data가 물리적으로 미리 파티셔닝하여 노드 별로 분배해놓은 것이 아니라면 그냥 1번 training을 끝낼 때마다 전체 data를 N번 (N epochs) training한 것과 같은 효과를 냅니다.   저 data들의 저장소는 여러 노드에서 동시에 access할 수 있도록 IBM Spectrum Scale (구명칭 GPFS) 같은 병렬 파일시스템으로 하든가, 그게 없다면 성능이 떨어지더라도 NFS 같은 것으로 구성하는 것이 좋습니다.

이제 저 rf 파일, 즉 랭크 파일을 어떻게 만드는지 보시겠습니다.  그냥 손으로, 즉 vi 에디터 같은 것을 이용해서 만드셔도 됩니다만, PowerAI에서 기본 제공되는 rank_gen.py를 이용해서 다음과 같이 만드시는 것이 편합니다.

$ python /opt/DL/ddl/bin/rank_gen.py 4x2x3 sys-89074,sys-89075,sys-89076,sys-89077,sys-89078,sys-89079 > 4x2x3.rf

위에서 콤마(,)로 분리된 이름들이 서버 이름들입니다.  4x2x3이니 4장의 GPU를 가진 서버가 총 6대 있는 것이니, 서버 이름은 반드시 6대를 적으셔야 합니다.   이렇게 만들어진 4x2x3.rf 파일의 내용은 아래와 같습니다.  rank_gen.py는 기본적으로 10-core POWER8 chip 2장을 장착한 Minsky 서버를 기준으로 만들기 때문에 아래와 같이 10개의 core를 가진 slot 2개가 있는 것으로 나옵니다.  그래서 rank, 즉 GPU 1개마다 slot이 5개 (0-4) 있는 것으로 나오는데, 만약 그게 아니라 8-core POWER8 chip이 장착된 서버라면 수작업으로 0-4가 아닌 0-3으로 수정해주셔야 합니다.

u0017649@sys-89075:~$ cat 4x2x3.rf
#2017-09-14 04:45:51 by rank_gen
#dims = 4x2x3
#host = sys-89074,sys-89075,sys-89076,sys-89077,sys-89078,sys-89079
#dimX = 4
#dimY = 2
#dimZ = 3
#sockets = 2
#cores = 10

rank 0=sys-89074           slot=0:0-4
rank 6=sys-89074           slot=0:5-9
rank 12=sys-89074          slot=1:0-4
rank 18=sys-89074          slot=1:5-9

rank 3=sys-89075           slot=0:0-4
rank 9=sys-89075           slot=0:5-9
rank 15=sys-89075          slot=1:0-4
rank 21=sys-89075          slot=1:5-9


rank 1=sys-89076           slot=0:0-4
rank 7=sys-89076           slot=0:5-9
rank 13=sys-89076          slot=1:0-4
rank 19=sys-89076          slot=1:5-9

rank 4=sys-89077           slot=0:0-4
rank 10=sys-89077          slot=0:5-9
rank 16=sys-89077          slot=1:0-4
rank 22=sys-89077          slot=1:5-9


rank 2=sys-89078           slot=0:0-4
rank 8=sys-89078           slot=0:5-9
rank 14=sys-89078          slot=1:0-4
rank 20=sys-89078          slot=1:5-9

rank 5=sys-89079           slot=0:0-4
rank 11=sys-89079          slot=0:5-9
rank 17=sys-89079          slot=1:0-4
rank 23=sys-89079          slot=1:5-9



Caffe는 그렇게 쉽게 됩니다만, tensorflow는 그보다 좀 어렵습니다.  일단 별도의 ddl-tensorflow라는 debian package가 PowerAI 4.0에 포함되어 있는데, 이는 사실 tensorflow DDL에 꼭 필요한 것이 아니라, tensorflow DDL을 좀더 쉽게 사용하실 수 있도록 해주는 Google Slim에 기반한 script들과 example 파일들을 제공해주는 것입니다.  정작 tensorflow는 별도로 설치하셔야 하는데, 물론 그건 PowerAI에서 제공되는 tensorflow를 apt-get install 명령으로 설치하시면 됩니다.

$ sudo apt-get install ddl-tensorflow tensorflow

$ dpkg -L ddl-tensorflow
/.
/opt
/opt/DL
/opt/DL/ddl-tensorflow
/opt/DL/ddl-tensorflow/examples
/opt/DL/ddl-tensorflow/examples/mnist
/opt/DL/ddl-tensorflow/examples/mnist/ddl_mnist.py
/opt/DL/ddl-tensorflow/examples/mnist/README.md
/opt/DL/ddl-tensorflow/examples/slim
/opt/DL/ddl-tensorflow/examples/slim/BUILD
/opt/DL/ddl-tensorflow/examples/slim/WORKSPACE
/opt/DL/ddl-tensorflow/examples/slim/scripts
/opt/DL/ddl-tensorflow/examples/slim/scripts/finetune_inception_resnet_v2_on_flowers.sh
/opt/DL/ddl-tensorflow/examples/slim/scripts/train_lenet_on_mnist.sh
/opt/DL/ddl-tensorflow/examples/slim/scripts/finetune_resnet_v1_50_on_flowers.sh
/opt/DL/ddl-tensorflow/examples/slim/scripts/finetune_inception_v3_on_flowers.sh
/opt/DL/ddl-tensorflow/examples/slim/scripts/train_cifarnet_on_cifar10.sh
/opt/DL/ddl-tensorflow/examples/slim/scripts/finetune_inception_v1_on_flowers.sh
/opt/DL/ddl-tensorflow/examples/slim/train-alexnet.sh
/opt/DL/ddl-tensorflow/examples/slim/deployment
/opt/DL/ddl-tensorflow/examples/slim/deployment/__init__.py
...

이 ddl-tensorflow를 사용하시기 위해서는 PYTHONPATH 등의 환경변수 설정을 위해 source 명령으로 아래와 같이 ddl-tensorflow-activate를 수행해주셔야 합니다.

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

이제 ddl-tensorflow-install-samples 명령을 사용하시어 지정하는 directory에 sample들을 설치하실 수 있습니다.

nimbix@JARVICENAE-0A0A1835:~$ ddl-tensorflow-install-samples /data
Write into existing directory /data? (yN)
y
Copying examples/ into /data...
Success

가장 간단한 것으로, 손글씨 숫자를 판독하는 MNIST가 들어 있습니다.

nimbix@JARVICENAE-0A0A1835:~$ cd /data/examples/mnist

nimbix@JARVICENAE-0A0A1835:/data/examples/mnist$ ls
ddl_mnist.py  README.md

여기에 나온 것처럼, tensorflow는 명령어라기보다는 python에서 불러 사용하는 library로 되어 있기 때문에, 결국 multi-node 병렬처리를 하기 위해서는 python script를 위의 ddl_mnist.py에서처럼 작성해야 합니다.

일단 4-GPU 서버 2대(sys-89074와 sys-89075)로 수행하는 환경이라고 가정하고 아래와 같이 rank file을 먼저 만듭니다.

nimbix@JARVICENAE-0A0A1835:/data/examples/mnist$ python /opt/DL/ddl/bin/rank_gen.py 4x2x1 sys-89074,sys-89075 > 4x2.rf

nimbix@JARVICENAE-0A0A1835:/data/examples/mnist$ cat 4x2.rf
#2017-09-15 03:19:14 by rank_gen
#dims = 4x2x1
#host = sys-89074,sys-89075
#dimX = 4
#dimY = 2
#dimZ = 1
#sockets = 2
#cores = 10

rank 0=sys-89074           slot=0:0-4
rank 2=sys-89074           slot=0:5-9
rank 4=sys-89074           slot=1:0-4
rank 6=sys-89074           slot=1:5-9

rank 1=sys-89075           slot=0:0-4
rank 3=sys-89075           slot=0:5-9
rank 5=sys-89075           slot=1:0-4
rank 7=sys-89075           slot=1:5-9


이제 다음과 같이 수행하면 됩니다.

nimbix@JARVICENAE-0A0A1835:/data/examples/mnist$ mpirun -x PATH -x LD_LIBRARY_PATH -x PYTHONPATH -n 8 -rf 4x2.rf python ddl_mnist.py

(사실 mnist는 워낙 작은 dataset만 사용하므로, 병렬화의 의미가 없습니다.  그래서인지 ddl_mnist.py는 위에서 제가 예로 든 것처럼 4x2 구조는 애초에 불가능하고, 저 아래에 보시듯이 -mode r:2로 되어 있어 그냥 GPU 2장으로 병렬화하는 것만 가능합니다.)

결국 문제는 tensorflow를 병렬로 수행하기 위해서 python script를 어떻게 작성해야 하느냐인데, 이 부분에 대해서는 저도 개발자가 아닌 관계로 별 도움을 못 드리겠습니다.  (사실 제겐 흰건 글씨요 검은건 공백이며, 깜빡이는 것은 커서 정도로만 보입니다.)

대신, 다소 깁니다만, 아래에 PowerAI에 포함된 ddl_mnist.py의 내용을 그대로 올려두겠습니다.

nimbix@JARVICENAE-0A0A1835:/data/examples/mnist$ vi ddl_mnist.py

import tensorflow as tf
import numpy as np

############################################################################
#   IBM PowerAI Distributed Deep Learning (DDL) setup
############################################################################

# Disable GPU memory preallocation
config = tf.ConfigProto()
config.gpu_options.allow_growth = True

############################################################################
#   DDL Initialize BEGIN
############################################################################
# Load DDL operator
ddl = tf.load_op_library('/opt/DL/ddl-tensorflow/lib/ddl_MDR.so')


# DDL initializes MPI on CPU
# ddl.init takes two inputs
# 1) the number of GPUs to utilize on each host in training.
#    this number is not the number of GPUs to use for each leaner. It simply tells DDL that there are X GPUs in each host to be used for training
# 2) DDL options (refer to README for details)
with tf.Session(config=config) as sess:
    with tf.device('/cpu:0'):
        rank, size, gpuid = sess.run(ddl.init(2, mode = '-mode r:2 -dump_iter 100'))

# MPI info and assigned GPU
print [rank, size, gpuid]
############################################################################
#   DDL Initialize END
############################################################################


# Perform all TensorFlow computation within gpuid
with tf.device('/gpu:%d' %gpuid):
    ##############################################################################
    # Import MNIST data

    from tensorflow.examples.tutorials.mnist import input_data
    mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

    # Parameters
    learning_rate = 0.001
    training_iters = 200000
    batch_size = 100
    display_step = 1

    # Network Parameters
    n_input = 784 # MNIST data input (img shape: 28*28)
    n_classes = 10 # MNIST total classes (0-9 digits)
    dropout = 0.75 # Dropout, probability to keep units

    # tf Graph input
    x = tf.placeholder(tf.float32, [None, n_input])
    y = tf.placeholder(tf.float32, [None, n_classes])
    keep_prob = tf.placeholder(tf.float32) #dropout (keep probability)


    # Create some wrappers for simplicity
    def conv2d(x, W, b, strides=1):
        # Conv2D wrapper, with bias and relu activation
        x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
        x = tf.nn.bias_add(x, b)
        return tf.nn.relu(x)


    def maxpool2d(x, k=2):
        # MaxPool2D wrapper
        return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
                              padding='SAME')


    # Create model
    def conv_net(x, weights, biases, dropout):
        # Reshape input picture
        x = tf.reshape(x, shape=[-1, 28, 28, 1])

        # Convolution Layer
        conv1 = conv2d(x, weights['wc1'], biases['bc1'])
        # Max Pooling (down-sampling)
        conv1 = maxpool2d(conv1, k=2)

        # Convolution Layer
        conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])
        # Max Pooling (down-sampling)
        conv2 = maxpool2d(conv2, k=2)

        # Fully connected layer
        # Reshape conv2 output to fit fully connected layer input
        fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
        fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
        fc1 = tf.nn.relu(fc1)
        # Apply Dropout
        fc1 = tf.nn.dropout(fc1, dropout)

        # Output, class prediction
        out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
        return out


    # Store layers weight & bias
    weights = {
        ############################################################################
        #   DDL BROADCAST BEGIN
        ############################################################################
        # This step ensures that all learners start with the same initial parameters

        # 5x5 conv, 1 input, 32 outputs
        'wc1': tf.Variable(ddl.bcast(tf.random_normal([5, 5, 1, 32]))),
        # 5x5 conv, 32 inputs, 64 outputs
        'wc2': tf.Variable(ddl.bcast(tf.random_normal([5, 5, 32, 64]))),
        # fully connected, 7*7*64 inputs, 1024 outputs
        'wd1': tf.Variable(ddl.bcast(tf.random_normal([7*7*64, 1024]))),
        # 1024 inputs, 10 outputs (class prediction)
        'out': tf.Variable(ddl.bcast(tf.random_normal([1024, n_classes])))
        ############################################################################
        #   DDL BROADCAST END
        ############################################################################
    }

    biases = {
        'bc1': tf.Variable(ddl.bcast(tf.random_normal([32]))),
        'bc2': tf.Variable(ddl.bcast(tf.random_normal([64]))),
        'bd1': tf.Variable(ddl.bcast(tf.random_normal([1024]))),
        'out': tf.Variable(ddl.bcast(tf.random_normal([n_classes])))
    }

    # Construct model
    pred = conv_net(x, weights, biases, keep_prob)

    # Define loss and optimizer
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)


    ############################################################################
    #   DDL ALLREDUCE BEGIN
    ############################################################################

    # Collect the gradients and the corresponding parameters w.r.t the given cost
    grads_and_vars = optimizer.compute_gradients(cost)

    # Separate out the tuple
    grads, vars = zip(*grads_and_vars)

    # This step takes the average of the gradients on all the learners
    grads_and_vars_ddl = zip(ddl.all_reduce_n(grads, op='avg'), vars)

    # Update the parameters with the averaged gradient
    objective = optimizer.apply_gradients(grads_and_vars_ddl)

    ############################################################################
    #   DDL ALLREDUCE END
    ############################################################################

    # Evaluate model
    correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
    ##############################################################################

def split(a, n):
    k, m = divmod(len(a), n)
    return (a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in xrange(n))

# Launch the graph
with tf.Session(config=config) as sess:
    sess.run(tf.global_variables_initializer())
    step = 1
    # Keep training until reach max iterations
    while step * batch_size < training_iters:

        # Each learner will read batch_size*size samples and
        # use only the portion correspoding to the current learner (or rank)

        batch_x, batch_y = mnist.train.next_batch(batch_size*size)

        batch_x = np.split(batch_x,size)[rank]
        batch_y = np.split(batch_y,size)[rank]

        # Run optimization op (backprop)
        sess.run(objective, feed_dict={x: batch_x, y: batch_y,
                                       keep_prob: dropout})
        if step % display_step == 0:
            # Calculate batch loss and accuracy
            loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x,
                                                              y: batch_y,
                                                              keep_prob: 1.})
            print("MPI "+str(rank)+"] Iter " + str(step*batch_size) + ", Minibatch Loss= " + \
                  "{:.6f}".format(loss) + ", Training Accuracy= " + \
                  "{:.5f}".format(acc))
        step += 1

    print("MPI "+str(rank)+"] Optimization Finished!")

    # Calculate accuracy for 256 mnist test images
    print("MPI "+str(rank)+"] Testing Accuracy:", \
        sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
                                      y: mnist.test.labels[:256],
                                      keep_prob: 1.}))