分佈式TensorFlow


      在大型的數據集上進行神經網絡訓練,往往需要更大的運算資源, 而且需要耗費的時間也是很久的。因此TensorFlow提供了一個可以分佈式部署的模式,將一個訓練任務拆成若干個小任務,分配到不同的計算機來完成協同運算,這樣可以節省大量的時間。

我們先看一下簡單情況下的訓練模式:
1)單CPU單GPU

       這種情況就是最簡單的,對於這種情況,可以把參數和計算都定義再gpu上,不過如果參數模型比較大,顯存不足等情況,就得放在CPU。

import  tensorflow as tf 
with tf.device('/cpu:0'):   #   也可以放在gpu上
	w=tf.get_variable('w',[1],tf.float32,initializer=tf.constant_initializer(2))
	b=tf.get_variable('b',[1],tf.float32,initializer=tf.constant_initializer(5))
with tf.device('/gpu:0'):
	add=w+b
	mut=w*b
init = tf.initialize_all_variables()
with tf.Session() as sess:
	sess.run(init)
	tensor1,tensor2=sess.run([add,mut])
	print tensor1
	print tensor2

2) 單CPU多GPU

      這種情況我們就可以指定不同的GPU進行訓練了。一般共享操作定義在cpu上,然後並行操作定義在各自的gpu上,比如對於深度學習來說,我們一般參數定義、參數梯度更新統一放在cpu上,各個gpu通過各自計算各自batch 數據的梯度值,然後統一傳到cpu上,由cpu計算求取平均值,更新參數。

具體的深度學習多GPU訓練代碼,請參考:

https://github.com/tensorflow/models/blob/master/inception/inception/inception_train.py

import  tensorflow as tf
with tf.device('/cpu:0'):
	w=tf.get_variable('w',[1],tf.float32,initializer=tf.constant_initializer(2))
	b=tf.get_variable('b',[2],tf.float32,initializer=tf.constant_initializer(5))
with tf.device('/gpu:0'):
	add=w+b
with tf.device('/gpu:1'):
	mut=w*b
init = tf.initialize_all_variables()
with tf.Session() as sess:
	sess.run(init)
	print sess.run([add,mut])

3)多CPU多GPU

這個時候就會定義各自的角色,便於不同角色之間相互配合,分工明確。

Cluster、Job、task概念:

task可以看成每臺機器上的一個進程,多個task組成job

job又可分爲:ps(Parameter Server)、worker兩種,分別用於參數服務、計算服務,組成cluster。

tensorflow的分佈式有in-graph和between-gragh兩種架構模式。 
in-graph 模式
       in-graph模式, 把計算已經從單機多GPU,已經擴展到了多機多GPU了, 不過數據分發還是在一個節點。 這樣的好處是配置簡單, 其他多機多GPU的計算節點,只要起個join操作, 暴露一個網絡接口,等在那裏接受任務就好了。 這些計算節點暴露出來的網絡接口,使用起來就跟本機的一個GPU的使用一樣, 只要在操作的時候指定tf.device("/job:worker/task:n"), 就可以向指定GPU一樣,把操作指定到一個計算節點上計算,使用起來和多GPU的類似。 但是這樣的壞處是訓練數據的分發依然在一個節點上, 要把訓練數據分發到不同的機器上, 嚴重影響併發訓練速度。在大數據訓練的情況下,不推薦使用這種模式。
between-graph模式

       between-graph模式下,訓練的參數保存在參數服務器, 數據不用分發, 數據分片的保存在各個計算節點, 各個計算節點自己算自己的, 算完了之後, 把要更新的參數告訴參數服務器,參數服務器更新參數。這種模式的優點是不用訓練數據的分發了, 尤其是在數據量在TB級的時候, 節省了大量的時間,所以大數據深度學習還是推薦使用between-graph模式。

同步更新和異步更新

TensorFlow的兩種模式都支持同步更新和異步更新。

同步更新:將數據拆分成多份,每份基於參數計算出各自部分的梯度;當每一份的部分梯度計算完成後,收集到一                 起算出總梯度,再用總梯度去更新參數。
異步更新:同步更新模式下,每次都要等各個部分的梯度計算完後才能進行參數更新操作,處理速度取決於計算梯                 度最慢的那個部分,其他部分存在大量的等待時間浪費;異步更新模式下,所有的部分只需要算自己的                 梯度,根據自己的梯度更新參數,不同部分之間不存在通信和等待。

下面通過代碼解釋各種函數及某些用法的含義:

import numpy as np
import tensorflow as tf

flags = tf.app.flags
# 定義角色名稱
flags.DEFINE_string('job_name', None, 'job name: worker or ps')
# 指定任務的編號
flags.DEFINE_integer('task_index', None, 'Index of task within the job')
# 定義ip和端口
flags.DEFINE_string('ps_hosts', 'localhost:1681', 'Comma-separated list of hostname:port pairs')
flags.DEFINE_string('worker_hosts', 'localhost:1682,localhost:1683', 'Comma-separated list of hostname:port pairs')
# 定義保存文件的目錄
flags.DEFINE_string('log_dir', 'log/super/', 'directory path')
# 訓練參數設置
flags.DEFINE_integer('training_epochs', 20, 'training epochs')
FLAGS = flags.FLAGS

上面的代碼就很好理解了,只是定義了一些參數。

1) 在運行時通過  job_name 和 task_index 傳遞參數,定義不同的角色(主要是 ps 和 worker)和任務編號

2) 通過 ps_hosts 和 worker_hosts 定義參與訓練的主機 ip 和 端口,用 ' , ' 隔開。

# 生成模擬數據
train_X = np.linspace(-1, 1, 100)
train_Y = 2 * train_X + np.random.randn(*train_X.shape) * 0.3  # y=2x,但是加入了噪聲

tf.reset_default_graph()

ps_hosts = FLAGS.ps_hosts.split(',')
worker_hosts = FLAGS.worker_hosts.split(',')
cluster_spec = tf.train.ClusterSpec({'ps': ps_hosts, 'worker': worker_hosts})

上面這段代碼主要就是:

1)生成模擬數據

2)分割 ps_hosts 和 worker_hosts,然後通過 tf.train.ClusterSpec() 把你要跑這個任務的所有 ps 和 worker          節點的ip和端口的信息都包含進去,所有的節點都要執行這段代碼,大家就互相知道這個集羣裏面都有哪些成        員,不同的成員的角色是什麼,是 ps 還是 worker。

# 創建server
server = tf.train.Server({'ps': ps_hosts, 'worker': worker_hosts},
                         job_name=FLAGS.job_name,
                         task_index=FLAGS.task_index)
# ps角色使用join進行等待
if FLAGS.job_name == 'ps':
    print("waiting...")
    server.join()
       tf.train.Server() 將根據參數對主機進行分工。根據參數的不同,決定了這個任務是哪個任務。如果任務名字是 ps 的話,程序就join到這裏,等待其他主機的連接,作爲參數更新的服務,等待其他worker節點給他提交參數和更新的數據。如果是worker任務,就執行後面的計算任務。
with tf.device(tf.train.replica_device_setter(
        worker_device="/job:worker/task:%d" % FLAGS.task_index,
        cluster=cluster_spec)):
    X = tf.placeholder("float")
    Y = tf.placeholder("float")
    # 模型參數
    W = tf.Variable(tf.random_normal([1]), name="weight")
    b = tf.Variable(tf.zeros([1]), name="bias")

    global_step = tf.contrib.framework.get_or_create_global_step()  # 獲得迭代次數

    # 前向結構
    z = tf.multiply(X, W) + b
    # 反向優化
    cost = tf.reduce_mean(tf.square(Y - z))
    learning_rate = 0.01
    # Gradient descent
    optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost, global_step=global_step)

    init = tf.global_variables_initializer()

tf.device() 函數中的任務是通過 tf.train.replica_device_setter() 來指定的。

tf.train.replica_device_setter()  可以看看文章後面的具體參數。

worker_device 定義具體的任務名稱,
cluster 指定角色及對應的IP地址,從而實現管理整個任務下的圖節點。
init = tf.global_variables_initializer() 是將前面的參數全部初始化,如果後面在再有變量,將不會被初始化。

在這個with語句之下定義的參數, 會自動分配到參數服務器上去定義,如果有多個參數服務器, 就輪流循環分配。

sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0),
                         init_op=init,
                         global_step=global_step)

       tf.train.Supervisor() 類似一個監督者,在分佈式訓練中,很多機器都在運行,像什麼參數初始化,保存模型,寫summary.......,這個supervisoer幫你一起弄起來了, 就不用自己手動去做這些事情了,而且在分佈式的環境下涉及到各種參數的共享,比較麻煩,所以就有了 tf.train.Supervisor()

      is_chief 表明是否爲 chief supervisors 角色,這裏將 task_index=0 的worker設置成了 chief supervisors 。負責初始化參數, 模型的保存,summary的保存。 
      init_op 表示使用初始化變量的函數。
      global_step是可以所有計算節點共享的,在執行optimizer的minimize的時候,會自動加1

      在這個函數中,已經通過 init_op 初始化參數了,所以就不需要在運行 sess.run(init) 來初始化參數了,如果用其再次初始化,會導致載入模型的變量被清空。

其他的一些參數:

logdir 就是檢查點文件和summary文件的保存路徑。 訓練啓動就會去logdir的目錄去看有沒有checkpoint的文件,有的話就自動裝載,沒有就用init_op指定的初始化參數。
saver 需要保存檢查點的saver對象傳入,supervisor就會自動保存檢查點文件。如不想自動保存設置爲None
summary_op 也是自動保存summary文件。設置爲None,表示不自動保存。
save_model_secs 爲保存檢查點文件的時間間隔。
# 連接目標角色創建session
with sv.managed_session(server.target) as sess:
    print(global_step.eval(session=sess))

    for epoch in range(global_step.eval(session=sess), FLAGS.training_epochs*len(train_X)):

        for (x, y) in zip(train_X, train_Y):
            _, epoch = sess.run([optimizer, global_step], feed_dict={X: x, Y: y})

            loss = sess.run(cost, feed_dict={X: train_X, Y: train_Y})
            print("Epoch:", epoch + 1, "cost=", loss, "W=", sess.run(W), "b=", sess.run(b))

    print(" Finished!")
sv.stop()

      上面的代碼是通過 tf.train.Supervisor() 中的managed_session來管理打開一個session。session中只負責運算,而通信協調的事情就會交給supervisor來管理。

   在上面的程序中如果要保存 summary 文件,將使用sv.summary_computed(), 想要手動保存使用 sv.saver.save(),在設置自動保存檢查點文件之後,手動保存仍然有效。在程序運行中止時,在運行 supervisor 時會自動載入模型的參數,不需要手動調用saver.restore()。

   但是在分佈式部署時,保存 summary 還需要注意幾點:

1)不是 chief supervisor 不能使用 sv.summary_computed() ,即使使用也無法執行,還會報錯

2)手寫控制 summary 與檢查點文件保存時,需要將chief supervisor 以外的worker全部去掉纔可以。可以使用 supervisor 按時間間隔保存的形式來管理,這樣用一套代碼就足夠了。

下面是完整的代碼:

運行時打開三個終端,分別輸入:

1)python  distribute.py  --job_name=ps  --task_index=0
2)python  distribute.py  --job_name=worker  --task_index=0
3)python  distribute.py  --job_name=worker  --task_index=1
import numpy as np
import tensorflow as tf

flags = tf.app.flags

# 定義角色名稱
flags.DEFINE_string('job_name', None, 'job name: worker or ps')
# 指定任務的編號
flags.DEFINE_integer('task_index', None, 'Index of task within the job')

# 定義ip和端口
flags.DEFINE_string('ps_hosts', 'localhost:1681', 'Comma-separated list of hostname:port pairs')
flags.DEFINE_string('worker_hosts', 'localhost:1682,localhost:1683', 'Comma-separated list of hostname:port pairs')
# 定義保存文件的目錄
flags.DEFINE_string('log_dir', 'log/super/', 'directory path')

# 參數設置
flags.DEFINE_integer('training_epochs', 20, 'training epochs')

FLAGS = flags.FLAGS

# 生成模擬數據
train_X = np.linspace(-1, 1, 100)
train_Y = 2 * train_X + np.random.randn(*train_X.shape) * 0.3  # y=2x,但是加入了噪聲

tf.reset_default_graph()

ps_hosts = FLAGS.ps_hosts.split(',')
worker_hosts = FLAGS.worker_hosts.split(',')
cluster_spec = tf.train.ClusterSpec({'ps': ps_hosts, 'worker': worker_hosts})
# 創建server
server = tf.train.Server({'ps': ps_hosts, 'worker': worker_hosts},
                         job_name=FLAGS.job_name,
                         task_index=FLAGS.task_index)

# ps角色使用join進行等待
if FLAGS.job_name == 'ps':
    print("waiting...")
    server.join()

with tf.device(tf.train.replica_device_setter(
        worker_device="/job:worker/task:%d" % FLAGS.task_index,
        cluster=cluster_spec)):
    X = tf.placeholder("float")
    Y = tf.placeholder("float")
    # 模型參數
    W = tf.Variable(tf.random_normal([1]), name="weight")
    b = tf.Variable(tf.zeros([1]), name="bias")

    global_step = tf.contrib.framework.get_or_create_global_step()  # 獲得迭代次數

    # 前向結構
    z = tf.multiply(X, W) + b
    # 反向優化
    cost = tf.reduce_mean(tf.square(Y - z))
    learning_rate = 0.01
    # Gradient descent
    optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost, global_step=global_step)

    init = tf.global_variables_initializer()


sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0),
                         init_op=init,
                         global_step=global_step)

# 連接目標角色創建session
with sv.managed_session(server.target) as sess:
    print(global_step.eval(session=sess))

    for epoch in range(global_step.eval(session=sess), FLAGS.training_epochs*len(train_X)):

        for (x, y) in zip(train_X, train_Y):
            _, epoch = sess.run([optimizer, global_step], feed_dict={X: x, Y: y})

            loss = sess.run(cost, feed_dict={X: train_X, Y: train_Y})
            print("Epoch:", epoch + 1, "cost=", loss, "W=", sess.run(W), "b=", sess.run(b))

    print(" Finished!")
sv.stop()
replica_device_setter(ps_tasks=0, ps_device="/job:ps",
                          worker_device="/job:worker", merge_devices=True,
                          cluster=None, ps_ops=None, ps_strategy=None):
  """Return a `device function` to use when building a Graph for replicas.

  Device Functions are used in `with tf.device(device_function):` statement to
  automatically assign devices to `Operation` objects as they are constructed,
  Device constraints are added from the inner-most context first, working
  outwards. The merging behavior adds constraints to fields that are yet unset
  by a more inner context. Currently the fields are (job, task, cpu/gpu).

  If `cluster` is `None`, and `ps_tasks` is 0, the returned function is a no-op.
  Otherwise, the value of `ps_tasks` is derived from `cluster`.

  By default, only Variable ops are placed on ps tasks, and the placement
  strategy is round-robin over all ps tasks. A custom `ps_strategy` may be used
  to do more intelligent placement, such as
  `tf.contrib.training.GreedyLoadBalancingStrategy`.

  For example,

  ```python
  # To build a cluster with two ps jobs on hosts ps0 and ps1, and 3 worker
  # jobs on hosts worker0, worker1 and worker2.
  cluster_spec = {
      "ps": ["ps0:2222", "ps1:2222"],
      "worker": ["worker0:2222", "worker1:2222", "worker2:2222"]}
  with tf.device(tf.train.replica_device_setter(cluster=cluster_spec)):
    # Build your graph
    v1 = tf.Variable(...)  # assigned to /job:ps/task:0
    v2 = tf.Variable(...)  # assigned to /job:ps/task:1
    v3 = tf.Variable(...)  # assigned to /job:ps/task:0
  # Run compute
  ```

  Args:
    ps_tasks: Number of tasks in the `ps` job.  Ignored if `cluster` is
      provided.
    ps_device: String.  Device of the `ps` job.  If empty no `ps` job is used.
      Defaults to `ps`.
    worker_device: String.  Device of the `worker` job.  If empty no `worker`
      job is used.
    merge_devices: `Boolean`. If `True`, merges or only sets a device if the
      device constraint is completely unset. merges device specification rather
      than overriding them.
    cluster: `ClusterDef` proto or `ClusterSpec`.
    ps_ops: List of strings representing `Operation` types that need to be
      placed on `ps` devices.  If `None`, defaults to `STANDARD_PS_OPS`.
    ps_strategy: A callable invoked for every ps `Operation` (i.e. matched by
      `ps_ops`), that takes the `Operation` and returns the ps task index to
      use.  If `None`, defaults to a round-robin strategy across all `ps`
      devices.

  Returns:
    A function to pass to `tf.device()`.

  Raises:
    TypeError if `cluster` is not a dictionary or `ClusterDef` protocol buffer,
    or if `ps_strategy` is provided but not a callable.
  """
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章