Distributed TensorFlow を Google Cloud Platform で動かしてみる


さて[前々回](http://qiita.com/ashitani/items/2e48729e78a9f77f9790)、[前回](http://qiita.com/ashitani/items/dbe76cb9194d60ead9de)とDistributed Tensorflowの仕組みを学んできましたが、いよいよGoogle Cloud Platform(GCP)上で動かしてみます。無料枠の\$300を突っ込んでvCPUインスタンスをたくさん作れば貧乏人にも高火力の鱗片が見れるのでは？という期待がモチベーションでした。

最初にネタばらししてしまうと、GCPの無料アカウントでは1regionにつきvCPU8個までに制限されていることがわかりましたので、それほどの高火力にはなりませんでした。今回はインターフェイスとマスタサーバに１個ずつvCPUを当てたので、vCPU6個での並列化となります。それでも、もちろんちゃんと速くなりました。

# Dockerイメージの作成

GCPのContainer Engine で動かしたいので、まずDockerイメージを作ります。

先日ビルドしたTensorflowサーバのみを入れたworker用のコンテナイメージtf\_serverと、インターフェース用のコンテナイメージtf\_ifを作ります。

まずtf\_serverは、素のubuntuイメージの/bin/にgrpc\_tensorflow\_serverのバイナリをコピーしただけです。[docker.io/ashipong/tf_server](https://hub.docker.com/r/ashipong/tf_server/)にあげておきました。

tf\_ifのほうはもう少しいろいろ入れておきましょう。bazelでビルドしたDistributed Tensorflow版のwhlをコピーして、

```bash
$ sudo apt-get install python python-pip python-dev
$ pip install tensorflow-0.7.1-py2-none-any.whl
```

でDistributed 対応のTensorFlow本体はインストールできました。jupyter notebookでアクセスできたら便利でしょうからjupyterもインストールしておきます。notebookサーバの設定もしておきます。[こちら](http://qiita.com/joemphilips/items/de5d12723b9b88b5b090)にしたがって~/.jupyter/jupyter\_notebook\_config.py を記載します。パスワードは"distributed_tensorflow"としました。IP制限をしておらず危険なのでこのイメージを公開する方はアクセス元のIPを制限する、パスワードを早々に書き換えるなど、万全の注意をお願いいたします。

こちらも同様にdockerhubに上げました。[docker.io/ashipong/tf\_if](https://hub.docker.com/r/ashipong/tf_if/)です。

# OSX上でDistributed TensorFlow on Dockerの練習

Dockerイメージでも同じように動かせるか確認してみます。まずgrpcサーバが１人の場合です。

```bash
$ docker run -d -p 2222:2222 ashipong/tf_server /bin/grpc_tensorflow_server  --cluster_spec='master|localhost:2222' --job_name=master --task_index=0 &
```

でサーバを立てて、クライアントからは、

```python
import tensorflow as tf
c = tf.constant("Hello, distributed TensorFlow on docker!")
sess = tf.Session("grpc://192.168.99.100:2222")
sess.run(c)
```

のようにアクセスします。192.168.99.100はdocker-machineのIPアドレスです。ちゃんと動きました。

つぎにサーバのコンテナが２人いるときはどうすればいいでしょう。下記のようにサーバコンテナを立てます。

```bash
$ docker run -d -p 2222:2222 --name tf_master ashipong/tf_server /bin/grpc_tensorflow_server --cluster_spec='master|192.168.99.100:2222,slave|192.168.99.100:2223' --job_name=master --task_index=0 &
$ docker run -d -p 2223:2222 --name tf_slave ashipong/tf_server /bin/grpc_tensorflow_server --cluster_spec='master|192.168.99.100:2222,slave|192.168.99.100:2223' --job_name=slave --task_index=0 &
```

コンテナは２人とも2222ポートにサーバを開きます。ホスト側では2222,2223に接続することにします。コンテナ同士はdockerの[仮想ネットワーク機能](http://qiita.com/Arturias/items/75828479c1f9eb8d43fa)を使って接続できるので、ホスト側からみたIPとポートをcluster_specに記載します。うーん、うまく書けませんがこういう感じでしょうか。

![docker.png](https://qiita-image-store.s3.amazonaws.com/0/30340/ede0debb-810d-00cf-497b-fec0813f75cd.png "docker.png")


この要領でdockerの使える環境でコンテナを次々に増やすことができますが、以上までの手順でもわかるように、cluster_specにはTensorFlowクラスタを構成するすべてのコンテナのIPとポートを、各コンテナからアクセスできる形で書く必要があります。各コンテナを別マシンで動かす場合は、別マシン上で動いているコンテナ同士が相互通信できるようなネットワーク設定が必要です。

# Distributed MNIST

さすがに[前回](http://qiita.com/ashitani/items/dbe76cb9194d60ead9de)まで使っていた$y=exp(x)$を関数近似するモデルでは、バッチの数を増やしてもたいして負荷が増えないので、もうすこしデータが大きいモデルのほうがよいでしょうかね。というわけで定番の[MNIST](https://www.tensorflow.org/versions/r0.7/tutorials/mnist/pros/index.html)を並列化してみました。[こちら](https://github.com/ashitani/DistributedTensorFlowSample/blob/master/gcp/mnist/mnist_single.py)がシングル版、[こちら](https://github.com/ashitani/DistributedTensorFlowSample/blob/master/gcp/mnist/mnist_distributed.py)がdistributed版のコードになります。

基本的には[前回](http://qiita.com/ashitani/items/dbe76cb9194d60ead9de)でやったことをMNISTに適用しただけです。コレクションを乱発していますが、もっと効率よい書き方ができるかもしれません。


# GCPへ（結果編）

さて、ではいよいよGCPで実行してみましょう。結果から先にいきます。冒頭で述べたようにvCPU1個のクラスタ8個を作り、if(interface)とmaster,worker6人をそれぞれ当てました。jobの名前とクラスタ（pod)名は同じにしました。

構成は絵で書くと下記のようになります。

![gcp.png](https://qiita-image-store.s3.amazonaws.com/0/30340/4f85d667-0e22-87b3-5fa3-659be1d0041e.png "gcp.png")


結果ですが、シングルだとこんなかんじ。

```
# python mnist_single.py
step 00000, training accuracy 0.123, loss 1762.99, time 1.253 [sec/step]
step 00010, training accuracy 0.218, loss 1308.50, time 1.293 [sec/step]
step 00020, training accuracy 0.382, loss 1191.41, time 1.223 [sec/step]
step 00030, training accuracy 0.568, loss 1037.45, time 1.235 [sec/step]
step 00040, training accuracy 0.672, loss 939.53, time 1.352 [sec/step]
step 00050, training accuracy 0.742, loss 853.92, time 1.303 [sec/step]
...
```

distributed版はこんなかんじ。引数にmasterサーバの走っているマシン名を渡します。今回はmasterと同名です。

```
# python mnist_distributed.py master
step 00000, training accuracy 0.130, loss 1499.24, time 0.433 [sec/step]
step 00010, training accuracy 0.597, loss 839.25, time 0.405 [sec/step]
step 00020, training accuracy 0.828, loss 437.39, time 0.478 [sec/step]
step 00030, training accuracy 0.893, loss 220.44, time 0.438 [sec/step]
step 00040, training accuracy 0.902, loss 219.77, time 0.383 [sec/step]
step 00050, training accuracy 0.942, loss 131.92, time 0.370 [sec/step]
...
```

3倍速ぐらいになりました。6人だから6倍ってわけにはいきませんが、3倍速ければ機体を赤く塗って角をつけたぐらい違いますので大したものです。もっと深いネットのほうが真価を発揮できるでしょう。収束具合も分散板のほうがいいかんじですが、今回は乱数のSeedをあわせなかったので、たまたまでしょう。

ちなみに手元のMacbook Proだとシングル版がだいたい0.8秒付近でした。少なくとも無料でもMacbook Proより速くはできる、ということですね。（まあGCPの速度はだいぶ混雑度で変わりますけど）

# GCPへ（手順編）

さて、上記に至るまでのGCPでの手順をメモっておきます。Dockerってなんだっけぐらいから始めたクラウド初心者なので、アホなことをやってるかもしれませんがご容赦ください。

まずGCPのWebからプロジェクトを作ります。それ以降はクライアントからの操作です。

まずIDを環境変数にいれておきましょう。手元のマシンとプロジェクトを関連付けます。

```bash
$ export PROJECT_ZONE=YOUR_ZONE
$ export PROJECT_ID=YOUR_PROJECT
$ gcloud config set project ${PROJECT_ID}
$ gcloud config set compute/zone ${PROJECT_ZONE}
```

つぎにContainer Registoryにさっき作っておいたコンテナイメージをpushします。

```bash
$ docker tag ashipong/tf_server asia.gcr.io/${PROJECT_ID}/tf_server
$ gcloud docker push asia.gcr.io/${PROJECT_ID}/tf_server
$ docker tag ashipong/tf_if asia.gcr.io/PROJECT_ID/tf_if
$ gcloud docker push asia.gcr.io/${PROJECT_ID}/tf_if
```
Container Engineでコンテナクラスタを作成します。名前はtfとします。

```bash
$ gcloud container clusters create tf --num-nodes 8 --machine-type n1-standard-1
$ gcloud container clusters get-credentials tf
```

クラスタにif1個、master1個、worker6個、というpodを作ります。podはたとえば下のようなmaster.ymlファイル

```master.yml
apiVersion: v1
kind: Pod
metadata:
  name: master
  labels:
    app: tfserver
spec:
  containers:
    - name: master
      command: ["/bin/grpc_tensorflow_server"]
      args: ["--cluster_spec=master|master:2222,worker0|worker0:2222,worker1|worker1:2222,worker2|worker2:2222,worker3|worker3:2222,worker4|worker4:2222,worker5|worker5:2222,","--job_name=master","--task_index=0"]
      image: asia.gcr.io/${PROJECT_ID}/tf_server
      ports:
        - containerPort: 2222
  nodeSelector:
    app: master
```

のようなものを作っておき、

```
$ kubectl create -f master.yml
```

のようにすれば作れるのですが、全員分用意するのが大変なので[生成スクリプト](https://github.com/ashitani/DistributedTensorFlowSample/blob/master/gcp/create_tf_servers.py)を作りました。nodeSelectorの設定は結果的には不要でしたが、デバッグ中にnodeとpodを関連付けたほうがよいのではないかと思い、node側にラベルをつけ、nodeとpodを対応づけました。このあたりも生成スクリプトでやっています。

今回はworker0,worker1,...という別名のpodを作りました。ほんとはworkerは一つのpodにしておいて、負荷を見ながらコンテナ数を増減したりできればカッコイイのですが、現状のgrpc_tensorflow_serverは起動時のcluster_spec引数に他の構成員をすべて記述する必要があるので、あとからコンテナ数を変更する場合はすべてのサーバを再起動する必要があると思います。この辺はもっと洗練された仕組みがきっと本家かどこかからリリースされるのではと思います。

まあともかく、下記生成スクリプトでnodeにラベルをつけてまわり、podを立てるところまでやります。

```bash
$ python ./create_tf_servers.py
```

Podが生成されたか確認します。

```bash
$ kubectl get pods
NAME      READY     STATUS    RESTARTS   AGE
if        1/1       Running   0          1m
master    1/1       Running   0          1m
worker0   1/1       Running   0          1m
worker1   1/1       Running   0          1m
worker2   1/1       Running   0          1m
worker3   1/1       Running   0          1m
worker4   1/1       Running   0          1m
worker5   1/1       Running   0          1m
```

最後にハマったのは各Pod間の名前解決でした。普通はDNSを立てたりするんでしょうかね？今回はあきらめてhostsに書いて回りました。最高にかっこわるいです。

下記でpodに割り当てられたIPがリストアップできます。

```bash
$ kubectl get pods -o=yaml |grep podIP
```

ここからなんと直接viを立ち上げて編集して回りました。最高にかっこわるいです。

```bash
$ kubectl exec -it master vi /etc/hosts
```

これでやっと準備ができたので、ifにログインしてスクリプトを実行します。

```bash
$ kubectl exec -it if /bin/bash
# apt-get install git
# cd home
# git clone https://github.com/ashitani/DistributedTensorFlowSample
# cd DistributedTensorFlowSample/gcp/mnist
# python mnist_distributed.py master
```

bashは5分ぐらいでタイムアウトするようです。exec /bin/bashとかするなってことですかね。ifではjupyter notebook が立ち上がっているので、サービスを公開してそっちから入ればいいと思いますが、今回はパスします。

GCP使いの方ならもっとスマートにやるのでしょうけど、どうにか動くところまで行ったのでこのへんでやめます。vCPU 8個で萎えたのでモチベの限界です（笑）


# 最後に

いちおうDistributed Tensoflow関係の投稿三部作はこれで終わりとします。

今回はGCP上でクラスタを作り、MNISTのデータ並列版を動かすところまで試してみました。残念ながら無料枠ではそれほど爆速体験はできませんでしたが、お金さえあれば（笑）いくらでもインスタンスを増やして楽しいことができるのではないでしょうか。

x10程度の高速化であればやはりGPU一発でよい気がしますが、x100とかもっと速い領域を狙うのであれば、GPUとクラスタの併用が必要でしょうね。GCPのGPUインスタンスが待ち遠しいですね。

それよりもっと高速化するにはやはりハードウェア、目先はFPGAでしょう。先日、Googleから[ストレージの信頼性を下げて良いので安くしてくれ](http://cloud.watch.impress.co.jp/docs/news/20160307_746930.html)というすごい発言がありましたが、極端な言い方をすれば、ストレージ工場の品質管理を現場で引き受けるということかと思います。FPGAが高価な理由の一つに素子歩留まりがあると思いますが、ディーブニューラルネットはもともとローカルの断線に強い構成をすることが多いので、歩留まりを下げた代わりに激しく安い、ディープラーニング向けFPGAというのがそのうち世に出回るのではないでしょうか。勝手に期待しています。