《智能计算系统》第五章

第五章 编程框架机理

5.1 TensorFlow设计原则

​ TensorFlow的设计原则主要集中在三方面:高性能、易开发、可移植。

5.1.1 高性能

​ 首先,TensorFlow中集成的算子在设计过程中已经针对底层硬件架构进行了充分的优化;同时,针对生成的计算图,TensorFlow提供了一系列的优化操作,提升了计算图的运行效率;并且TensorFlow调度器可以根据网络结构的特点,并行运行没有数据依赖的节点,异步发射满足依赖关系的多个节点而不同步等待每个节点的中间结果。

5.1.2 易开发

​ TensorFlow针对现有的深度学习算法,提取了大量的共性运算,并且将这些运算封装成TensorFlow中的各种算子,以便用户进行调用。

5.1.3 可移植

​ TensorFlow通过定义不同设备的通用抽象来实现应用程序的跨平台可移植目标。

5.2 TensorFlow计算图机制

5.2.1 计算图

​ 计算图是TensorFlow运行的核心,涵盖了TensorFlow的各类功能,包括数据计算、数据存取、逻辑控制和设备通信等。

5.2.1.1 自动求导

​ 深度学习通常采用梯度下降法来更新模型参数,根据前向计算得到的结果和目标结果产生损失函数值,通过计算梯度来得到每个参数应该调整的偏移量,进而更新参数。常见的求导方法有以下几种:

​ (1)手动求导

​ 用链式法则求出梯度公式,然后根据公式编码,代入数值计算得到梯度结果。其缺点就是无法通用或者复用,每次修改算法模型都需要重新求解梯度公式、重新编写代码。

​ (2)数值求导

​ 利用导数的定义进行求解,如公式:
$$
f’(x)=\lim_{h\rightarrow0}\frac{f(x+h)-f(x)}{h}
$$
但是计算量大,运行速度慢,并且会引入舍入误差和截断误差。

​ (3)符号求导

​ 利用求导规则对表达式自动操作,但可能会遇到“表达式膨胀”的问题,导致求解速度变慢。

​ (4)自动求导

​ 这是一种介于符号求导和数值求导之间的方法,自动求导首先实现了一批常用基本算子的求导表达式,然后带入数值计算,保留中间结果,最后求出整个函数的导数。TensorFlow计算图将多输入的复杂计算表示成了由多个基本二元计算组成的有向图,并保留了所有中间变量,有助于程序自动利用链式法则进行求导。

5.2.1.2 检查点

​ TensorFlow利用检查点机制进行模型的保存与恢复。通过向计算图中插入Save节点及其关联节点来完成保存模型的功能。其关联节点包括记录文件名的Const和tensor_names节点,分别用来指定检查点文件名以及所有需保存的Tensor列表;同样,恢复模型通过在计算图中插入Restore节点及其关联节点来完成,Restore通过赋值(Assign)节点来给待恢复的变量进行赋值。

5.2.1.3 控制流

​ TensorFlow提供了5个基本的控制流算子,分别为Switch、Merge、Enter、Exit和NextIteration,通过这五个算子的组合可以实现条件分支以及while循环的功能。

5.2.1.4 执行模式

​ TensorFlow的计算图执行包括客户端、主控进程以及一个或多个工作进程。每个工作进程可以访问一个或多个计算设备,并在上面执行计算图。TensorFlow提供了本地执行和分布式执行两种执行方式。其中本地执行主要是指客户端、主控进程以及工作进程都只在一个操作系统的单一物理机上运行,而分布式执行则可以支持三者在不同的机器上执行。

​ 计算图中的节点将按照图中的依赖关系顺序执行,TensorFlow调度器会持续监视未被执行的节点,一旦某个节点所依赖的前驱节点的数量为0,该节点就会处于就绪状态,会被立即放入预执行队列中。之后执行器从队列中取出节点,根据节点信息,选择合适的设备创建相应的算子交给设备端来执行。

5.2.2 计算图的本地执行

​ 计算图从创建到真正执行的过程包括以下四个步骤:

​ (1)计算图剪枝:根据输入输出列表在完整计算图上进行剪枝操作,从而得到最小依赖计算图。

​ (2)计算图分配:在最小依赖计算图上,根据特定的设备分配规则以及操作的设备约束对计算图中的节点进行设备分配。

​ (3)计算图优化:对计算图进行优化来提高运行效率。

​ (4)计算图切分:根据划分结果,对计算图进行切分,从而为每个设备创建自身的计算子图。

5.2.2.1 计算图剪枝

​ 计算图剪枝的目的是得到本地运行所需的最小子图,主要包括:去除计算图中与最终输出节点无关的节点和边,就是从输出节点进行宽度搜索遍历,对在遍历过程中没有接触到的节点和边进行删除;给输入输出节点建立和外界的交互,遍历完成之后可能会生成多个连通图,此时需要将每个连通图的入度为0的节点通过依赖控制边与Source节点相连,出度为0的节点通过控制依赖边和Sink节点相连,从而形成完整的计算图。

5.2.2.2 计算图分配

​ 计算图分配用以解决在多设备运行的环境中,计算图在哪个设备上执行的问题,用户可以自行指定某个操作的计算设备,也可以指定哪些计算节点需要绑定在同一个设备上。为了获得最合适的设备分配方案,TensorFlow设计了相应的代价模型,对于未指定的计算节点,TensorFlow采用贪心策略来对每个节点进行分配,从Source节点开始,算法模拟每个节点在支持该节点的所有不同设备上的执行情况,得到不同设备是该节点的执行开销,进而选择合适的设备进行分配。

5.2.2.3 计算图优化

​ TensorFlow中的图优化由Grappler模块来实现,Grappler已经实现了多种优化方法。典型的优化方法包括ConstFold(包括常量折叠等优化)、Arithmetic(包括算术简化等)、Layout(包括布局优化等)和Remapper(包括算子融合等)。通过计算图优化,可以根据不同的硬件结构调整计算调度策略,从而获得更快的计算速度和更高的硬件利用率。也能减少预测过程中所需的峰值内存,从而允许运行更大的模型。

5.2.2.4 计算图切分和设备通信

​ 计算图切分,是在计算图的一系列优化完成之后,将计算图放到多个设备上计算,每个设备对应一个切分子图,这时就需要解决各个设备间的通信问题。在新的设备中,所有跨设备的边都被替换为一对由Send和Recv组成的节点,在运行时,Send/Recv节点合作完成跨设备的通信。其中Send/Recv屏蔽了和设备相关的通信细节,简化了运行时的复杂性。在Recv没有得到有效数据之前,图的运行会被阻塞。而主控机只需要向不同的工作机传递运行请求,不需要负责不同工作机的同步问题,使得系统更可扩展和更高效。

5.2.3 计算图的分布式执行

​ 随着神经网络规模及数据规模指数型增加,为了有效提高神经网络训练效率,降低训练时间,在模型训练中普遍采用分布式技术。计算图的分布式执行与多设备计算图的本地执行类似,在图分配后为每个设备都创建一个子图,工作进程间的通信由Send/Recv节点合作通过远程通信机制进行数据传输。

5.3 TensorFlow系统实现

5.3.1 整体架构

​ 如图所示,架构主要分为三个部分:第一部分是面向各种语言的语言包;第二部分是C/C++API,基于TensorFlow的核心代码,使用C和C++语言封装出了两套APT,主要面向有较高性能需求的用户;第三部分是TensorFlow的后端代码,由C++代码实现,保证了可移植性和性能。

5.3.2 计算图执行模块
5.3.2.1 Session执行

​ Session是用户和TensorFlow运行时的接口。在Session接收到输入数据时,便可开始运行。一般情况下,每个设备会有一个执行器(Executor),负责本设备上子计算图的执行。Run函数是Session执行的核心逻辑,在其中完成计算图的执行,包括传参、运行和返回。

​ TensorFlow1.x通过先构建静态图,然后通过tf.Session来执行。TensorFlow2.0则将使用正常的python编程方式,不再使用tf.Session,而使用tf.function。tf.function做为装饰器来使用,可以将函数转换成图,帮助优化和序列化。

5.3.2.2 执行器逻辑

​ 引入执行流,执行流是一个可以存储计算任务的队列,队列中的计算任务按照加入队列的顺序执行。通常设备在执行任务时会存在不同的流,流间任务可以并行执行,流内任务串行执行。执行器在对计算图节点进行流分配的原则是将有数据依赖的节点分配到同一流中,没有数据依赖的节点分配到不同的流中,这样可以最大限度地实现流与流之间的并行。

​ 执行器逻辑的具体流程就是在对计算图分配完流之后,执行器启动ScheduleReady函数开始异步执行计算图中的节点,执行器调用完RunAsync函数后,返回主逻辑,等待执行结束。

​ ScheduleReady逻辑主要处理两个队列:ready队列(预执行队列,inline_ready队列。如果inline_ready队列为空,使用新线程分别处理ready队列中的每个节点。如果inline_ready队列不为空,如果节点都是低开销的,则逐一放到inline_ready队列中,如果节点是高开销的,如果inline_ready队列为空则将首个高开销节点放入inline_ready队列中,否则启用新线程去执行。ready队列中节点真正的计算都在possess函数中,possess函数为OpKernel设置运行参数、为OpKernel准备输入和参数、 调用设备计算、处理计算输出、传播输出、更新节点间依赖关系。

5.3.3 设备抽象和管理

​ TensorFlow将设备分成本地设备和远程设备两类,TensorFlow使用注册机制来管理设备。每个设备负责一个子图的运算,可以通过注册接口支持自定义设备。设备继承自DeviceBase类,其定义了基本的数据结构与接口,基于DeviceBase类又设计了LocalDevice类,本地设备可以基于LocalDevice类创建自己的设备类。

5.3.4 网络和通信

​ TensorFlow中不同设备间的通信都由Send和Recv节点进行,Send和Recv使用Rendezvous机制完成数据交互,Rendezvous是一个基于生产者-消费者模型的抽象类。每个Rendezvous实例拥有一个通道表,用来记录每对Send/Recv的关系和状态。不同的通道拥有唯一的键值,生产者使用Send方法将数据传到特定通道(可以传输多组数据),消费者可以在任何时候使用Recv方法从特定通道获取数据(可按照发送顺序获取),也可以使用回调或者阻塞的方法来获取数据。不论哪种方法,消费者都能在数据有效时尽快得到数据,生产者在任何时候都不会被阻塞。

​ 对于本地传输来说,TensorFlow提供了LocalRendezvous 实现类,对于远程通信来说,TensorFlow提供 了RemoteRendezvous实现类。

5.3.5 算子实现

​ 算子是TensorFlow的基本单元。OpKernel是算子的特定执行,依赖于底层硬件。基于不同设备、不同的数据类型,算子可以由不同的OpKernel实现:既可以使用底层硬件提供的高性能库,也可以采用特定的编程语言。TensorFlow通过注册机制来支持不同的算子和相应的OpKernel函数。

​ OpKernel的计算可以是同步的也可以是异步的,由于同一个图可能同时被执行多份,所以所有的OpKernel的Compute方法必须保证线程安全。大部分OpKernel的计算是同步的,”Compute()”返回即认为数据已经被正确处理。

作者

Cindy

发布于

2021-12-03

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×