Tensorflowソース解析3--TensorFlowコアオブジェクト-Graph


1 Graphの概要
計算図GraphはTensorFlowのコアオブジェクトであり、TensorFlowの実行プロセスは基本的にそれをめぐって行われている.図の構築、転送、枝切り、workerによる分割、デバイスによる二次分割、実行、ログアウトなどが含まれます.したがって,計算図Graphを理解することはTensorFlowの動作を把握する上で特に重要である.
2デフォルトGraph
デフォルト図の置換
前にセッションを説明したときに言ったように、1つのセッションはrunに1つのGraphしかありませんが、1つのGraphは複数のセッションで実行できます.一般的に、sessionはグローバルで唯一の暗黙的なデフォルトのGraphを実行し、operationもこのGraphに登録します.
Graphの作成を表示し、as_を呼び出すこともできます.default()はデフォルトのGraphを置き換えます.このコンテキストマネージャで作成したopは、このgraphに登録されます.コンテキストマネージャを終了すると、元のデフォルトgraphが復元されます.一般的には、Graphを明示的に作成する必要はありません.システムで作成されたデフォルトのGraphを使用します.
print tf.get_default_graph()

with tf.Graph().as_default() as g:
    print tf.get_default_graph() is g
    print tf.get_default_graph()

print tf.get_default_graph()

出力は次のとおりです.

True


このように、コンテキストマネージャでは、現在のスレッドのデフォルト図が置き換えられ、コンテキスト管理を終了すると、元のデフォルト図に戻ります.
デフォルトの管理
デフォルトgraphはデフォルトsessionと同様にスレッドの役割ドメインです.現在のスレッドには、graphがデフォルトマップとして1つしかありません.TensorFlowも同様にスタックによってスレッドのデフォルトgraphを管理します.
@tf_export("Graph")
class Graph(object):
    #        
    def as_default(self):
        return _default_graph_stack.get_controller(self)
    
    #     ,push pop
    @tf_contextlib.contextmanager
    def get_controller(self, default):
        try:
          context.context_stack.push(default.building_function, default.as_default)
        finally:
          context.context_stack.pop()

代替デフォルト図はスタックの管理方式を採用し,push pop操作により管理する.デフォルトマップを取得するには、デフォルトgraphスタックを介して次のようにします.default_graph_stackが取得します.
@tf_export("get_default_graph")
def get_default_graph():
  return _default_graph_stack.get_default()

次に見てみましょうdefault_graph_stackの作成
_default_graph_stack = _DefaultGraphStack()
class _DefaultGraphStack(_DefaultStack):  
  def __init__(self):
    #        
    super(_DefaultGraphStack, self).__init__()
    self._global_default_graph = None
    
class _DefaultStack(threading.local):
  def __init__(self):
    super(_DefaultStack, self).__init__()
    self._enforce_nesting = True
    #    session   ,       list
    self.stack = []

_default_graph_stackの作成は上述したように,最終的にはデフォルトsessionスタックと同様に本質的にlistである.
3フロントエンドGraphデータ構造
Graphデータ構造
オブジェクトを理解するには、まずデータ構造から始めます.まずPythonフロントエンドにおけるGraphのデータ構造を見てみましょう.Graphの主なメンバー変数はOperationとTensorです.Operationは、演算演算子を表すGraphのノードです.TensorはGraphのエッジで、演算データを表しています.
@tf_export("Graph")
class Graph(object):
    def __init__(self):
           #     ,    op ,         op graph ,      graph      
        self._lock = threading.Lock()
        
        # op    。
        #  graph   op    id,  id         op。    _nodes_by_id  
        self._nodes_by_id = dict()  # GUARDED_BY(self._lock)
        self._next_id_counter = 0  # GUARDED_BY(self._lock)
        #        name     op,    _nodes_by_name  
        self._nodes_by_name = dict()  # GUARDED_BY(self._lock)
        self._version = 0  # GUARDED_BY(self._lock)
        
        # tensor    。
        #   tensor placeholder
        self._handle_feeders = {}
        #   tensor read  
        self._handle_readers = {}
        #   tensor move  
        self._handle_movers = {}
        #   tensor delete  
        self._handle_deleters = {}

次にgraphがopを追加する方法と、スレッドの安全を保証する方法を見ます.
  def _add_op(self, op):
    # graph    final ,      ,    op 。
    self._check_not_finalized()
    
    #     graph     
    with self._lock:
      #  op id name      ,   _nodes_by_id _nodes_by_name   ,        
      self._nodes_by_id[op._id] = op
      self._nodes_by_name[op.name] = op
      self._version = max(self._version, op._id)

グラフグループ
各Operationノードには特定のラベルがあり、ノードの分類を実現します.同じラベルのノードはクラスに分類され、同じCollectionに配置されます.ラベルは唯一のGraphKeyであり、GraphKeyはクラスGraphKeysに定義され、以下のように定義されています.
@tf_export("GraphKeys")
class GraphKeys(object):
    GLOBAL_VARIABLES = "variables"
    QUEUE_RUNNERS = "queue_runners"
    SAVERS = "savers"
    WEIGHTS = "weights"
    BIASES = "biases"
    ACTIVATIONS = "activations"
    UPDATE_OPS = "update_ops"
    LOSSES = "losses"
    TRAIN_OP = "train_op"
    #     

name_scopeノードネーミングスペース
name_の使用scopeはgraphのノードを階層化し、上下層間をスラッシュで区切る.
# graph      
g = tf.get_default_graph()
with g.name_scope("scope1"):
    c = tf.constant("hello, world", name="c")
    print c.op.name

    with g.name_scope("scope2"):
        c = tf.constant("hello, world", name="c")
        print c.op.name

出力は次のとおりです.
scope1/c
scope1/scope2/c  #    scope      ,    ,       

4バックエンドGraphデータ構造
Graph
まずgraphを見てみましょうhファイルのGraphクラスの定義は、キーコードのみを参照してください.
 class Graph {
     private:
      //      op        
      FunctionLibraryDefinition ops_;

      // GraphDef   
      const std::unique_ptr versions_;

      //   node  ,  id   
      std::vector nodes_;

      // node  
      int64 num_nodes_ = 0;

      //  edge  ,  id   
      std::vector edges_;

      // graph   edge   
      int num_edges_ = 0;

      //       ,      node edge
      std::vector free_nodes_;
      std::vector free_edges_;
 }

バックエンドのGraphの主要メンバーもノードnodeとエッジedgeです.ノードnodeは計算演算子Operation,エッジは演算子に必要なデータ,あるいはノード間の依存関係を表す.これはPythonの定義と似ています.エッジEdgeのソースノードとターゲットノードのポインタを持ち、2つのノードを接続します.次に、Edgeクラスの定義を見てみましょう.
Edge
class Edge {
     private:
      Edge() {}

      friend class EdgeSetTest;
      friend class Graph;
      //    ,               。         
      Node* src_;

      //     ,               。          
      Node* dst_;

      //  id,        
      int id_;

      //            src_output_  。            
      int src_output_;

      //             dst_input_  。             。
      int dst_input_;
};

Edgeはtensorデータを担持し,ノードOperationに提供して演算することもできるし,ノード間の依存関係を表すこともできる.ノード依存を表すエッジのsrc_output_, dst_input_いずれも-1で、エッジはデータをロードしません.
ノードクラスの定義を見てみましょう.
Node
class Node {
 public:
    // NodeDef,    Operation   ,  op         ,op    ,        。
      const NodeDef& def() const;
    
    // OpDef,     Operation    ,    。  Operation     ,     
      const OpDef& op_def() const;
 private:
      //    ,       。     
      EdgeSet in_edges_;

      //    ,          。     
      EdgeSet out_edges_;
}

ノードノードノードに含まれるプライマリデータには、入力エッジと出力エッジのセットがあり、ノードによって関連するすべてのエッジを見つけることができます.NodeにはNodeDefとOpDefの2つのメンバーも含まれています.NodeDefはノード演算子の情報を表し、実行時に変化する可能性があり、Nodeを作成するとnewのNodeDefオブジェクトが表示されます.OpDefはノード演算子のメタ情報を表し,実行時は変化せず,Node作成時はnew OpDefを必要とせず,OpDef倉庫から取り出すだけでよい.メタ情報は確定しているので,たとえばOperationの入参個数などである.
NodeとEdge、すなわち図Graphを構成することができ、任意のノードと任意のエッジを通じて、完全な図を遍歴することができます.Graphが計算を実行する場合,トポロジー構造に従ってノードごとのop計算を順次実行し,最終的に出力結果を得ることができる.入度0のノード、すなわちデータに依存して準備されたノードは、同時に実行でき、実行効率が向上します.
システムにはデフォルトのGraphが存在し、Graphを初期化するとSourceノードとSinkノードが追加されます.SourceはGraphの開始ノードを表し,Sinkは終了ノードである.Sourceのidは0,Sinkのidは1であり,他のノードのidはいずれも1より大きい.
5 Graphランタイムライフサイクル
GraphはTensorFlowのコアオブジェクトであり、TensorFlowの実行はいずれもGraphを中心に行われます.実行時Graphはほぼ以下の段階を経ています
  • 図構築:clientエンドユーザーは作成したノードをGraphに登録します.一般的にGraphの作成を表示する必要はありません.システムで作成したデフォルトを使用すればいいです.
  • 図送信:clientはセッションを通過する.run()実行時に構築した整図をGraphDefにシーケンス化してmaster
  • に渡す.
  • 図剪断:masterはまず逆シーケンス化してGraphを手に入れ、sessionに従った.run()伝達fetchesとfeedsリストは,全図full graphを逆方向に遍歴し,剪断を実施し,最小依存子図を得た.
  • 図分割:masterは最小サブ図を複数のGraph Partitionに分割し、複数のworkerに登録します.1つのワークはGraph Partitionに対応します.
  • 図二次分裂:workerは、CPU GPUのような現在利用可能なハードウェアリソースに基づいて、Graph Partitionをop演算子デバイス制約仕様(例えばtf.device(’/cpu:0’)に従って、異なるデバイスに二次分裂させる.各コンピューティングデバイスはGraph Partitionに対応します.
  • 図実行:各コンピューティングデバイスに対して、workerはopのkernelでの実装に従ってopの演算を完了する.デバイス間データ通信はsend/recvノードを使用することができ、worker間通信はGRPCまたはRDMAプロトコルを使用する.

  • これらのフェーズはTensorFlowの実行時によって異なる処理が行われます.ランタイムには、ローカルランタイムと分散ランタイムの2種類があります.したがって、Graphライフサイクルは、ローカルランタイムと分散ランタイムを後で分析するときに詳細に説明します.
    著者:揚易
    原文を読む
    本文は雲栖コミュニティのオリジナル内容で、許可を得ずに転載してはならない.