TensorFlow在大规模分布式系统上的并行效率相当高,如下图所示:
在GPU数量小于16时,基本没有性能损耗,在50块的时候,可以获得80%的效率,也就是40倍 的单GPU提速。100块的时候,获得56块的提速。
为了达到这种高效并发性能,tensorflow做了很多优化,包括单不限于以下几点:
子图消重: 在Tensorflow中有很多高层的运算操作,这些运算操作可能是有很多复杂的底层计算组合而成的,当有很多个高层运算存在时,它们的前几层的运算可能是重复计算的(输入何运算内容都一样)。tf会自动识别出这些重复的计算,然后通过改写计算图,共享计算结果,消除重复计算量。
计算顺序优化: 通过调整节点的执行顺序,改善数据传输何内存占用问题。例如错开某些节点的计算时机,避免某些数据同时存在于内存,这对于先显存有限的GPU设备来说至关重要。
复用高效的第三方计算库: 包括线性代数计算库Eigen, 矩阵计算库BLAS,cuBLAS,深度学习计算库cuda-convnet,cuDNN
节点分配设备策略的持续优化: 持续优化节点执行设备的分配策略,未来计划用一个强化学习的网络辅助分配策略。
XLA编译优化: 通过编译优化加速Graph的计算。
多重并行计算模式: TensorFlow提供三种不同的加速神经网络训练的并行计算模式,分别是数据并行,模型并行,流水线并行:
数据并行模式中,模型在不同的设备上存在相同多份拷贝,共享相同的参数,采用不同的训练数据并行训练。
根据共享参数的更新方式,又分为同步更新模式与异步更新模式;同步更新模式中(图6上),参数的更新值需要进行汇总,然后更新共享参数,这就意味着,需要等待所有的设备当前训练批次训练完成后才能更新共享参数;而异步更新模式中(图6下),不用进行更新值的汇总,每台设备当前批次训练完成后分别更新共享参数,避免了等待的问题。
模型并行模式中,是将模型的不同部分别放在不同的机器上进行训练。模型并行需要模型本身有大量的可以并行的,互相不依赖的或则依赖程度不高的子图。
流水线并行模式与数据并行模式类似,区别在于流水线并行模式是在单机上训练的。
每个目录的功能:
目录 功能
tensorflow/c C API代码
tensorflow/cc C++ API代码
tensorflow/compiler XLA,JIT等编译优化相关
tensorflow/contrib contributor贡献的代码,这个目录并不是官方支持的, 很有可能在高级 API 完善后被官方迁移到核心的 TensorFlow 目录中或去掉
tensorflow/core tf核心代码
tensorflow/docs_src 文档相关文件
tensorflow/examples 例子相关代码
tensorflow/g3doc TF文档
tensorflow/go go API相关代码
tensorflow/java java API相关代码
tensorflow/python Python API相关代码
tensorflow/stream_executor 并行计算框架代码
tensorflow/tools 各种辅助工具工程代码,例如第二章中生成Python安装包的代码就在这里
tensorflow/user_ops tf插件代码
third_party/ 依赖的第三方代码
tools 工程编译配置相关
util 工程编译相关
表1:TF根目录
其中tensorflow/core是tf的核心模块
————————————————
TF Core目录
目录功能如下:
目录 功能
tensorflow/core/common_runtime 公共运行库
tensorflow/core/debug 调试相关
tensorflow/core/distributed_runtime 分布式执行模块
tensorflow/core/example 例子代码
tensorflow/core/framework 基础功能模块
tensorflow/core/graph 计算图相关
tensorflow/core/grappler 模型优化模块
tensorflow/core/kernels 操作核心的实现代码,包括CPU和GPU上的实现
tensorflow/core/lib 公共基础库
tensorflow/core/ops 操作代码
tensorflow/core/platform 平台实现相关代码
tensorflow/core/protobuf .proto定义文件
tensorflow/core/public API头文件
tensorflow/core/user_ops
tensorflow/core/util
表2:TF Core目录
————————————————
4
本章中,我们通过工具bazel query,找到了混合编程中链接两个世界的模块pywrap_tensorflow_internal,实际上它就是Python的一个扩展,python的代码通过这个扩展就可以调用底层的C/C++代码了。然后分析此工程的过程中,引入了SWIG工具,它使得C/C++代码很方便的导出到各种其他的脚本语言。
————————————————
5
图1是C API中对Tensor的封装,Tensor的纬度、数据类型、数据内容都有对应的成员表示。数据内容存放在TensorBuffer中,这个类支持引用计数,在引用数为0的时候则自动释放内存。
以上是接口层对Tensor的封装,比较简单直接,适合接口中传递参数使用,但是在tf的内核中,Tensor的封装是tensorflow.Tensor,它的设计目标之一是为了能方便的使用线性代数运算库Eigen,另外TensorBuffer的具体实现类也不一样:
在 UML 模型中,模板参数是一些形参,一旦将它们与实际值(称为模板自变量)进行绑定,就会使模板成为可用的模型元素。
可以使用模板参数来创建特殊类型的模板的常规定义。例如,当对类添加模板参数时,该类就会变成模板类,有时称为参数化类。通过将模板类用作常规模式,可以创建一组使用模板参数来定义更具体行为的类。 每个模板参数都必须具有一个名称和类型。参数的名称在模板参数列表中必须是唯一的。类型是对模型元素(例如,类、接口或属性)或者基本数据类型(例如,Integer 或 String)的引用。如果您在将参数绑定至模板时不指定模板自变量,那么模板参数会采用缺省值。 当您将模型元素绑定至模板时,就对模板参数指定值(称为模板自变量)。在绑定至模板的模型元素中,模板自变量将替换模板参数。此操作将创建一个新的模型元素,该模型元素具有模板的结构并且使用它的模板自变量的值。 模板参数的语法为 name : type。 在图编辑器中,模板参数通过位于类元形状右上角的带虚线边框的一个框来表示。项目资源管理器视图将模板参数列示在定义了这些模板参数的类元下。下表说明了这两种表示法。 |
|||||
TensorFlow中Op代表一个基本运算,比如矩阵或则标量的四则运算。
运算类型 运算名称
标量运算 Add,Sub,Mul,Div,Exp,Log,Greater,Less,Equal
向量运算 Concat,Slice,Split,Constant,Rank,Shape,Shuffle
矩阵运算 MatMul,MatrixInverse,MatrixDeterminant
带状态的运算 Variable,Assign,AssignAdd
神经网络组件 SoftMax,Sigmoid,ReLU,Convolution2D,MaxPooling
存储、恢复 Save,Restore
队列和同步 Enqueue,Dequeue,MutexAcquire,MutexRelease
控制流 Merge,Switch,Enter,Leave,NextIteration
————————————————
版权声明:本文为CSDN博主「jony0917」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gaofeipaopaotang/article/details/80598840
在tf的设计中,运算和运算实现是两个分开的概念,通过引入的运算核(OpKernel)的概念来表示运算的具体实现。这么设计的原因是,运算的语义是平台不相关的,是不变的,而运算的实现运算核是跟具体的平台(CPU、GPU、TPU)相关的。这样,就可以很方便的对语义不变的运算提供不同平台的实现了。tf中的运算核也有注册机制,为一个运算提供多平台的实现:
/* tensorflow/core/kernels/conscat_op.cc */
...
REGISTER_KERNEL_BUILDER(Name("Concat") .Device(DEVICE_CPU) .TypeConstraint<type>("T") .HostMemory("concat_dim"), ConcatOp<CPUDevice, type>)
...
REGISTER_KERNEL_BUILDER(Name("Concat")
.Device(DEVICE_GPU)
.TypeConstraint<int32>("T")
.HostMemory("concat_dim")
.HostMemory("values")
.HostMemory("output"),
ConcatOp<CPUDevice, int32>);
...
以上的这段代码,就为Concat运算注册了两个运算核,分别对应DEVICE_CPU和DEVICE_GPU,运算核的实现代码就在模板类ConcatOp中。
Node的定义中,包括名称,输入来源,运算名,设备以及属性。另外,在执行Node的运算前,需要通过设备类型和运算名找到相应的运算核(OpKenel)。
背后实现:
图3:计算图的构建
第一步、 TF_NewGraph会创建一个tensorflow.Graph对象,这就是计算图在TF内核中的表示;TF_NewGraph返回的结果是TF_Graph的指针,这个结构体是C API层对tensorflow.Graph的封装对象。
第二步、 TF_NewOperation创建Graph中的Node,这一步中涉及的类比较多,tensorflow.NodeBuilder,tensorflow.NodeDefBuilder是为了构建tensorflow.NodeDef的工具类;为了最终构建Node对象,还需要通过tensorflow.OpRegistryInterface来找到Node绑定的OpDef。就像前面说的,Op是通过注册来提供给tf使用的。
细心的用户发现,其实这步并没有创建Node对象,为什么呢?我们先往后看。
第三步、设置Node的输入,设备以及属性,如图1中调用10到22。
**最后,**TF_FinishOperation创建Node对象,并添加到Graph中。我们看到,实际的Node对象的创建是到这一步才发生的(调用26),并且根据节点的输入和控制输入,添加所需的数据边和流控制边。这也是为什么Node对象的创建放在最后一步的原因。
————————————————
版权声明:本文为CSDN博主「jony0917」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gaofeipaopaotang/article/details/80598840
Session 本地执行:
本地执行的步骤如下:
第一步、图4中的1-6,创建session对象;根据Option的设置,返回具体的session实现类,设置本地执行后,返回的session对象的实现类是tensorflow.DirectSession.
第二步、执行计算图;这个过程比较关键,tf很多的优化技术都在这里。TF_SessionRun直接调用tensorflow.DirectSession.Run,此函数大致可以分为两个阶段:准备执行阶段和执行阶段。
1,准备执行阶段逻辑主要在函数tensorflow.DirectSession.GetOrCreateExecutor内,函数首先会调用函数tensorflow.DirectSession.CreateGraphs,然后为新生成的多张计算图分别创建各自的Executor(图4中的Loop for every graph)。
那么问题来了,创建session的时候,已经关联了一个graph,为什么要重新创建?甚至,重新创建的了多张图,这是为什么?简而言之,目的是为了分配设备和优化执行效率。这里的逻辑在tensorflow.DirectSession.CreateGraphs中。创建session时候关联的graph不适合直接进行计算,需要做的准备还很多,包括设备分配,裁剪,各种优化。
设备分配相关的类是tensorflow.CostModel和tensorflow.SimplePlacer,具体调用tensorflow.SimplePlacer.Run进行设备分配(图4中的16)。这里会根据一些启发式的经验规则加上一些通过实际运算收集的数据进行设备分配。(>>> 需要具体看)
tf中的各种效率优化是分阶段多次执行的,在设备分配前、设备分配之后、计算图执行之前、计算图分区之前等,都有优化逻辑存在,涉及tensorflow.grappler.MetaOptimizer,tensorflow.OptimizationPassRegitry,tensorflow.GraphOptimizer等类,相关的类如下:(>>> 需要具体看)
优化是个比较大主题,篇幅限制,本章中暂不展开介绍了,后面章节再讨论。
回归我们的讨论,在这些处理之后,调用Parition的进行计算图的分区操作,将重建的已经分配过设备和优化过的计算图进行分区。所谓分区的主要依据就是执行设备,同一个设备上的节点在一个分区。
在准备阶段的后半部分,需要为每一个分区的计算图创建独立的Executor(图4 Loop for every graph),目的是为了提高并发效率; 这部分逻辑还负责为分区计算图创建设备对象;另外,细心的用户还会发现,分区计算图中的每个节点的运算核也是在这时候创建的(图4 loop for evey node in graph)。
到此,每个分区计算图已经准备完毕,可以执行了。
2,执行阶段,并发调用每个Executor的异步执行方法tensorflow.Executor.RunAsync方法。RunAsync将当前计算图中输入依赖为0的节点放入ready_node_queue中,每次从ready_node_queue中取下一个待执行的节点执行,并在执行完成后,将它的下游节点的输入依赖减一,如此循环,直到ready_node_queue空为止(图4 loop ready_node_queue大于0)。
这里还需要提醒一点的是,每张分区计算图的执行并非完全独立的,也会发生等待的事件,因为分区间也存在输入依赖的问题。tf中通过在分区图间引入send/recv节点的方式解决这个问题。第一章中我们已经介绍过这个设计。
最后调用WaitForNotification等待计算图执行完成,提出执行结果。
相比本地执行,服务端执行流程看起来比较简单,这是因为我们隐去了服务端的逻辑,只画了客户端的逻辑。我会在后面单独的章节中介绍tf的分布式执行架构,这里暂不展开讨论服务端的情况。
在配置了服务端执行后,创建的session对象的具体实现类是GrpcSession,它通过一个gprc的通信类与服务端通信。
本章中介绍了tf核心概念在内核中的实现,包括Tensor,Op,Node,Graph。然后介绍了session驱动计算的内核实现。
6.
本章中分析TensorFlow的Grappler模块的实现。代码位于tensorflow/core/grappler。
上一章中分析session类的时候,已经介绍过了grappler模块的调用时机。
Grappler是TensorFlow的优化模块。模块中的主要包括这些类:
tensorflow.grappler.GrapplerItem表示待优化的TensforFlow模型,主要包括计算图、fetch节点、feed节点。
tensorflow.grappler.Cluster表示可以运行TensorFlow模型的硬件资源集合。一个进程同一时间只能创建一个Cluster.
tensorflow.grappler.GraphOptimizer是grappler中所有优化类的父类。
grappler模块的调用方式如下:
tensorflow.grappler.MetaOptimizer.Optimize()作为所有优化实现类是入口,根据优化的配置决定是否调用之后的每个优化类。
tensorflow.gappler.ModelPruner类的主要优化逻辑是裁剪计算图,剔除不需要的节点。
目前版本的实现中, 主要剔除符合一定条件的"StopGradient"和"Identity"节点:
以上的定义中可以看出两个操作都是直接输出节点的输入。这两类节点也不是完全没有用处的,所以ModelPruner剔除前还要检查一些规则条件,比如明确绑定设备的这类节点不能剔除,有参与计算图流控制的节点不能提出,等等。
来看一个ModelPruner优化的例子:
tensorflow.grappler.ConstantFolding类的主要优化逻辑是做常量的折叠,所谓的常量折叠是将计算图中可以预先可以确定输出值的节点替换成常量,并对计算图进行一些结构简化的操作。
tensorflow.grappler.ConstantFolding.Optimize函数主要调用了三个方法:MaterializeShapes,FoldGraph 和 SimplifyGraph。
目前版本中,MaterializeShapes函数处理"Shape", “Size”, "Rank"三类运算节点:
三类节点的输出都取决与输入Tensor的形状,而与具体的输入取值没关系,所以输出可能是可以提前计算出来的。
MaterializeShapes函数将可以提前计算出输出值的这三类节点全部替换成 Const 节点。
FoldGraph函数中做折叠计算图的操作。如果一个节点的输入都是常量,那么它的输出也是可以提前计算的,基于这个原理不断地用常量节点替换计算图中的此类节点,直到没有任何可以替换的节点为止。
目前版本中,SimplifyGraph函数主要处理Sum,Prod,Min,Max,Mean,Any,All这几类运算节点。这几类节点的共同点是都沿着输入Tensor的一定维度做一定的运算,或是求和或是求平均,等等。SimplifyGraph将符合一定条件的这几类节点替换为Identity节点。
来看一个ConstantFolding的例子:
tensorflow.grappler.LayoutOptimizer类的主要优化逻辑是改变一些运算节点的输入数据格式来提高运算效率,这些运算节点包括:
“AvgPool”,“AvgPoolGrad”,“Conv2D”,“Conv2DBackpropFilter”,“Conv2DBackpropInput”,
“BiasAdd”,“BiasAddGrad”,“FusedBatchNorm”,“FusedBatchNormGrad”,
“MaxPool”,“MaxPoolGrad”。
这几类节点的输入数据支持NHWC和NCHW两种格式,但是在GPU的上的运算核实现上,采用NNCHW格式运算效率要高,LayoutOptimizer的优化就是将GPU设备上的这几类节点的输入格式从NHWC转换为NCHW。
说明:
1, 这类操作的输入一般是一批图片数据,NHWC表示输入格式为
[batch,height,width,channel],NCHW表示输入格式为
[batch,height,width,channel]。
2,之所以有两类格式的存在,是因为在CPU和GPU上两类OpKernel,
要求的最优格式不一样;也因为这个,tf中默认采用的格式是NHWC.
LayoutOptimizer 采用的优化方法是在适当的位置插入Transpose运算节点:
最后看一个LayoutOptimizer优化的例子:
tensorflow.grappler.MemoryOptimizer的优化逻辑是降低设备的内存的占用。
在模型的调用计算过程中,计算产生的中间结果是不需要保存的,我们的目标是得出fetch的结果。
但是在模型训练过程中,节点运算产生的中间结果可能是需要保留的,因为在计算梯度的时候需要用,这就造成了设备内存的占用问题,而且模型越大,占用的内存就越多,而如GPU之类是设备内存是很稀缺和宝贵的资源。MemoryOptimizer就是为了解决这个问题的。
MemoryOptimizer采用的方法就是把一些中间结果交换到host主机的CPU内存中,等到需要用的时候,再交换进设备内存。
具体的实现是在计算图中适当的位置插入一对相连的Identity节点,一个分配到HOST CPU设备,另一个分配到如GPU的设备中,并设置好合适的上下游依赖。这样上游的中间计算结果就会被传出到HOST CPU中,然后在下游节点需要的时候通过这对节点交换回设备内存中。这个逻辑比较简单,就不做过多解释了,来看一个优化的例子:
tensorflow.grappler.AutoParallel的优化逻辑是通过重构原来的计算图,使得模型的训练过程实现数据并行,准确的说是多个batch的数据能并行训练,而不用等前一个batch训练完成。
实际上,tensorflow的分布式模式,已经支持多个batch同时训练,目的一样,与AutoParallel的实现方式不一样。
分布式的数据并发模式中,存在多份一样的模型,共享一份待训练的参数,而AutoParallel的优化中,只存在一个模型,AutoParallel通过过修改模型的结构来实现的并发。
下面通过一个例子来学习AutoParaller的优化逻辑:
图3是AutoParaller优化前的模型,分号前的是节点的名称,分号后面的部分是节点的运算名称。
dequeue节点会每次从fifo节点中取出一定数量的数据,与constant_a相加后最后作为apply_gradient节点的输入之一。apply_gradient节点的运算是:
var−=add∗learning_rate.
这个模型简单模拟的一下模型训练的过程,add节点代表梯度的计算,不过真实训练中,计算梯度的子网络比这里的要复杂。
先面我们把这个模型输入到AutoParaller中,并设置并发度为2:
图4为经过AutoParaller优化后的模型。图中的虚线代表控制输入依赖,实现代表数据输入依赖。
可以看出,原始模型中的一些节点保留了下来,例如fifo,constant_b等等,一些节点被复制了一份,例如add, learning_rate,最后新添加了一些节点,例如AutoParaller-Div-apply_gradient, AutoParallerl-Contol-Fetch.
可以看出,两份ApplyGradientDescent节点可以并发运行。分别执行运算:
var−=2add(autoparallel−replica−0)?∗learning_rate(autoparallel−replica−0)??
var−=2add(autoparallel−replica−1)?∗learning_rate(autoparallel−replica−1)??
原文:https://www.cnblogs.com/cx2016/p/11385479.html