《深入理解TensorFlow架构设计与实现原理》

第一章 TensorFlow系统概述

人工智能和深度学习的热潮将Tensoflow推向了很高的地位,本章主要是作为引子,来对TF进行一个概述

1.1 简介

1.1.1 产生背景

近年来深度学习在图像、视觉和语音领域的突破,促使了各种深度学习框架的诞生

1.1.2 独特价值

TF能在众多开源框架中杀出重围,除了Google的背书以外,还少不了以下独特价值:

  • 运算性能强劲:TF1.0利用线性代数编译器XLA全方位的提升了计算性能,XLA帮助TF在CPU、GPU、TPU、嵌入式设备等平台上更快速的运行机器学习模型的训练与推理任务。同时,还提供了大量针对不同软硬件环境的优化配置参数
  • 框架设计通用:TF既提供高层封装API(如Slim、Keras、TF Layers等),又提供底层原生API
  • 支持生产环境部署
  • 语言借口丰富:支持python、C、C++、java、Go等
  • 端云协同计算:支持同时在云侧和端侧运行(移动设备等终端)

1.1.3 版本变迁

迭代更新非常快,慢慢走向成熟

1.1.4 与其他主流深度学习框架对比

起初,TF的内存消耗和计算速度一直是短板。但是,随着XLA和RDMA等特性的发布,TF的性能在绝大多数情况下都不输于其他深度学习框架。

TF的灵活性导致了学习成本也相应较高,API过于丰富。(不过,可以使用Keras、TF Layers来解决此问题)

1.2 设计目标

TF的设计目标并非局限一套深度学习库,Google希望其成为一套面向多种应用场景和编程范式、支持异构计算平台、具备优异性能与可伸缩性的通用人工智能引擎。

1.2.1 灵活通用的深度学习库

TF的灵活性主要体现在以下几个方面:

  • 算子定义:粒度更细,数量更多
  • 编程范式:支持声明式编程,将模型的定义和执行解耦
  • runtime框架
  • 多语言支持

1.2.2 端云结合的人工智能引擎

TF对云计算场景的支持是其竞争力的基础,主要体现在以下方面:

  • 提供多种标准化的安装包、构建脚本和容器化封装,支持在不同Linux发行版以及Windows Server等服务器上部署
  • 支持对接多种常见的公有云和私有云服务
  • 兼容若干种常见的高性能计算与通信硬件
  • 灵活的runtime框架设计,既提供标准且易用的PS-worker分布式模式,也允许用户自由开发针对特定环境需求的分布式框架

TF在端侧方面也毫不逊色,主要体现在以下几个方面

  • 推理(预测)态代码能够运行于多种主流的终端平台
  • 通过XLA AOT(ahead of time)编译技术及其他软硬件解耦设计,简化对接
  • 提供量化参数和低精度代数等算法层机制
  • 提供模型与框架一体化的精简版runtime平台

1.2.3 高性能的基础平台软件

TF的高性能设计体现在它对高端和专用硬件的深入支持。

1.3 基本架构

1.3.1 工作形态

TF采用了库模式,其工作形态是有用户编写主程序代码,调用Python或其他语言函数库提供的借口以实现计算逻辑。

1.3.2 组件结构

结构示意图可查看书上p13

构成TF的主体使其运行时核心库。 对于普通的python应用层开发者而言,这个核心库就是值通过pip命令等方式安装TF之后,部署到site-packages或类似目录中的动态链接库文件。

生成这个库的C++源代码大致分为3个层次:分布式运行时、公共运行时和算子核函数。其中,公共运行时实现了数据流图计算的基本逻辑,分布式运行时在此基础上实现了数据流图的跨进程协同计算逻辑,算子核函数则包含图上具体操作节点的算法实现代码。

第二章 TensorFlow环境准备

2.1 安装

可查看官方文档

有一点需要注意:为了保证软件对操作系统和硬件平台的通用性,Google官方发布的TensorFlow whl包没有使用过多的编译优化选项,如 XLA、AVX、SSE等,如果想要打开这些编译优化选项来提升TF的计算性能,那么必须使用源代码编译安装的方式。

2.2 依赖项

2.2.1 Bazel软件构建工具

Bazel是Google开源的一套软件构建工具,功能定位与CMake、GNU Autotools和Apache Ant等类型,但具有一些独特的优势,如下所示:

  • 多语言支持:C++、Java、Python等
  • 高级构建语言
  • 多平台支持
  • 可重现性
  • 可伸缩性

Bazel使用工作空间、包和目标三层抽象组织待构建的对象

  • 工作空间(workspce):
  • 包(package):
  • 目标(target):

2.2.2 Protocal Buffers 数据结构序列化工具

2.2.3 Eigen线性代数计算库

2.2.4 CUDA统一计算设备架构

CUDA是NVIDIA公司退出的一种用于并行计算的软硬件架构,发布于2007年。该架构以通用计算图形处理器(GPGPU)作为主要的硬件平台,提供一组用于编写和执行通用计算任务的开发库与运行时环境。

CUDA作为软件依赖项提及时,往往指的是CUDA架构中的软件组件,即NVIDIA驱动程序和CUDA Toolkit。除了基本的CUDA开发库和编译器外,CUDA工具包还包括cuBLAS、cuFFT、cuSOLVER、cuDNN等高级算法库,以及IDE、调试器、可视化分析器等开发工具,其中部分组件需要独立安装。

在CUDA架构中,不同层次的软件组件均为开发者提供编程接口,以适应不同类型软件的开发需求:

  • NVIDIA驱动层的开发接口(即cu开头的函数,也称为CUDA Driver API)较为底层,暴露了GPU的若干内部实现抽象。这种接口能够对GPU的运行时行为进行细粒度控制,有助于提升程序的运行时效率,但缺点在于开发过程烦琐。一般的GPU应用程序不会直接使用这一层接口,然而TF内部的GPU计算引擎——StreamExecutor为了追求性能,选择使用这一层接口实现GPU任务调度和内存管理等功能
  • CUDA开发库的API(即以cuda开头的函数,也称为CUDA Runtime API)是CUDA架构中使用最为广泛的接口,功能涵盖GPU设备管理,内存管理,时间管理以及图形处理相关的逻辑。
  • cuBLAS、cuDNN等高级算法库:提供了面向通用计算(如线性代数)或领域专用计算(如神经网络)需求的高层次接口。在这个层次,GPU设备的很多技术细节已被屏蔽,开发者可以专注于算法逻辑的设计与实现。TF面向NVIDIA GPU的计算类操作大多基于cuBLAS和cuDNN接口实现。

对于TF而言,CUDA工具包是不受Bazel管理的外部依赖项,因此,用户如果想要使用NVIDIA GPU加速深度学习时,需要事先安装带有NVIDIA驱动程序的CUDA工具包即cuDNN库

2.3 源代码结构

2.3.1 根目录

TF源码的组织复合Bazel构建工具要求的规范。其根目录是一个Bazel项目的工作空间。

2.3.2 tensorflow目录

TF项目的源码主体位于tensorflow目录,该目录下的源文件几乎实现了TF的全部功能,同时体现了TF的整体模块布局。

2.3.3 tensorflow/core 目录

TF核心运行时库的源代码位于tensorflow/core目录

2.3.4 tensorflow/python 目录

TF Python API的源码位于tensorflow/python目录

2.3.5 安装目录

pip命令会将TF运行时所需的Python文件、动态链接库以及必要的依赖项复制到当前Python环境的site-packagesdist-packages目录中,其中TF软件本身的的运行时代码会被部署到tensorflow子目录,这一目录具有与源码tensorflow目录相似的组织结构。二者的不同点在于以下几点:

  • 安装目录中只包含每个模块的Python语言接口文件,不再包含C++源码。所有使用到的C++源码已被编译到了python子目录下的动态链接库文件中(在Linux下为_pywrap_tensorflow_internal.so)。如果某个模块未提供Python API,那么相应的子目录不会在安装目录中出现
  • 安装目录中的python/ops子目录比同名的源代码子目录增加了一系列名称有gen_开头的Python接口文件。这些文件是TensorFLow编译脚本自动创建的,旨在为C++核心库的一部分数据流图操作提供Python编程接口
  • 安装目录比源代码目录多出一个include子目录。这个目录包含了TensorFLow本身以及Protocol Buffers、Eigen等依赖库的C++头文件,允许用户通过编程方式使用核心库的功能。

第三章 TensorFlow基础概念

3.1 编程范式:数据流图

TF采用了更适合描述深度神经网络模型的声明式编程范式,并以数据流图作为核心抽象。

优势(相比更广泛的命令式编程范式):

  • 代码可读性强
  • 支持引用透明
  • 提供预编译优化能力

3.1.1 声明式编程与命令式编程

二者的最大区别在于:前者强调“做什么”, 后者强调“怎么做”。

声明式编程:结构化、抽象化,用户不必纠结每个步骤的具体实现,而是通过下定义的方式描述期望达到的状态。声明式编程比较接近人的思考模式;程序中的变量代表数学符号或抽象函数,而不是一块内存地址,程序的最终输出仅依赖于用户的输入数据,计算过程不受内部和外部状态影响。

命令式编程:过程化、具体化、用户告诉机器怎么做,机器按照用户的指示一步步执行命令,并转换到最终的停止状态。命令式编程起源于对汇编语言和机器指令的进一步抽象,本身带有明显的硬件结构特征。它通过修改存储器的值、产生副作用的方式实现计算。这里的副作用是值对外部环境产生的附加影响。

编程是一种输入到输出的转换机制,这两种范式提供了截然不同的解决方案:

  • 声明式编程:程序是一个数学模型,输入是自变量,输出是因变量,用户设计和组合一系列函数,通过表达式变换实现计算。
  • 命令式编程:程序是一个有穷自动机,输入是起始状态,输出是结束状态,用户设计一系列指令,通过指令的执行完成状态转换。

适用范围:

  • 声明式编程:DL、AI
  • 命令式编程:交互式UI、OS

3.1.2 声明式编程在DL应用上的优势

  1. 代码可读性强

  以目标为导向,更接近于数学公式或人类的思维方式

  1. 支持引用透明

  引用透明是指:如果一个函数的语义同他出现在程序中的上下文无关,则称它是引用透明的。关于引用透明的一个推论是:函数的调用语句可以被它的返回值取代,而不影响程序语义。因此,用户可以选择执行任意的模块组合(子图),以得到不同模型结构的输出结果。

  1. 提供预编译优化能力

  TF需要实现编译得到完整的数据流图,然后根据用户选择的子图、输入数据进行计算。因此,声明式编程能够实现多种预编译优化,包括无依赖逻辑并行化、无效逻辑移除、公共逻辑提取、细粒度操作融合等。

3.1.3 TensorFlow数据流图的基本概念

TF的数据流图是一个 有向无环图 。 图中的节点代表各类操作(opertion),具体包括数学运算、数据填充、结果输出和变量读写等操作,每个节点上的操作都需要分配到具体的物理设备(CPU、GPU)上执行。 图中的有向边描述了节点间的输入、输出关系(也就是各个操作的输入和输出),边上流动(flow)着代表高位数据的张量。

1.节点

前向图中的节点统一称为操作,它们根据功能可以分为以下3类:

  • 数学函数或表达式
  • 存储模型参数的变量(variable)
  • 占位符(placeholder)

后向图中的节点同样分为三类:

  • 梯度值
  • 更新模型参数的操作
  • 更新后的模型参数

2.有向边

数据流图中的有向边用于定义操作之间的关系,它们分为两类:一类用来传输数据,绝大部分流动着的张量的变都是此类,简称数据边;另一类用来定义控制依赖,通过设定节点的前置依赖决定相关节点的执行顺序,简称控制边。

3.执行原理

声明式编程的特点决定了在深度神经网络模型的数据流图上,各个节点的执行顺序并不完全依赖于代码中定义的顺序,而是与节点之间的逻辑关系以及运行时库的实现机制相关。

抛开运行时库内部的复杂实现,数据流图上节点的执行顺序参考了拓扑排序的设计思想,其过程可以简述为以下4个步骤:

  1. 以节点名称作为关键字、入度作为值,创建一张散列表,并将次数据流图上的所有节点放入散列表中。
  2. 为此数据流图创建一个可执行节点队列,将散列表中入度为0的节点加入到该队列,并从散列表中删除这些节点
  3. 依次执行该队列中的每一个节点,执行成功后将此节点输出指向的节点的入度值减1,更新散列表中对应节点的入度值
  4. 重复步骤2和步骤3,直到可执行节点队列变为空

3.2 数据载体:张量

TF提供Tensor和SparseTensor两种张量抽象,分别表示稠密数据和系数数据。后者旨在减少高维稀疏数据的内存占用。

3.2.1 张量:Tensor

与数学和物理学中的张量不同,在NumPy或TF中,通常使用多维数组的形式描述一个张量,数组的维数表示对应张量的阶数。张量的阶数决定了其描述的数据所在高维空间的维数,在此基础上,定义每一阶的长度可以唯一确定一个张量的形状。

TF中的张量形状用python中的列表表示,列表中的每个值依次表示张量各阶的长度(如图片的张量:[128,128,3])。

TF的张量在逻辑定义上是数据载体,但在物理实现时是一个句柄,它存储张量的元信息以及指向张量数据的内存缓冲区指针。这样设计是为了实现 内存复用。在某些前置操作(生产者)的输出值被输入到多个后置操作(消费者)的情况下,无须重复存储输出值。

1.创建

一般情况下,用户不需要使用Tensor类的构造方法直接创建张量,而是通过操作间接创建张量,如constant和add操作等:

1
2
3
4
5
6
7
import tensorflow as tf
a = tf.constant(1.0)
b = tf.constant(1.0)
c = tf.add(a,b)
print([a,b,c])

//output: [<tf.Tensor....>,<...>,<...>] 没有执行会话,所以不会输出值,而是输出abc的类型

2.求解

3.成员方法

Tensor具有eval,get_shape等成员方法

4.操作

TF为Tensor提供了abs,add,reduce_mean等大量操作

5.典型用例

见书p44

3.2.2 稀疏张量:SparseTensor

TF提供了专门用于处理高维稀疏数据的SparseTensor类。该类以键值对的形式表示高维稀疏数据,包含indices、values和dense_shape三个属性。

1.创建

在TF中创建稀疏张量时,一般可以直接用SparseTensor类的构造方法,如下:

1
2
3

import tensorflow as tf
sp = tf.SparseTensor(indices=[[0,2],[1,3]], values=[1,2],dense_shape=[3,4])

2.操作

TF为稀疏张量提供了一些专门的操作,方便用户处理。

3.典型用例

见书p46

模型载体:操作

TF中每个节点均对应一个具体的操作。因此,操作是模型功能的实际载体,数据流图主要有以下三种节点:

  • 计算节点:对应的是无状态的计算或控制操作,主要负责算法逻辑表达式或流程控制
  • 存储节点:对应的是有状态的变量操作,通常用来存储模型参数
  • 数据节点:对应的是特殊的占位符操作,用于描述待输入数据的属性

3.3.1 计算节点:Operation

Operation类定义在tensorflow/python/framework/ops.py文件中,提供了获取操作的名称、类型、输入张量、输出张量等基本属性的方法。

对于无状态节点,其输出有输入张量和节点操作共同决定

3.3.2 存储节点:Variable

存储节点作为数据流图中的有状态节点,其主要作用是在多次执行相同数据流图时存储特定的参数,如深度学习或机器学习的模型参数。

对于有状态的节点,其输出除了跟输入张量和节点操作有关之外,还会受到节点内部保存的状态值的影响。

1.变量

TF中的存储节点抽象是Variable类

2.变量操作

每个变量对应的变量操作对象在变量初始化时构造,变量支持两种初始化方式:

  • 初始值。根据用户输入或采用缺省值初始化
  • VariableDef。使用Protocol Buffers定义的变量完成初始化

3.read节点

通过解释read节点的实现原理,加深对于变量、变量操作和变量值的理解。

3.3.3 数据节点:Placeholder

数据流图本身是一个具有计算拓扑和内部结构的“壳”。在用户向数据流图填充数据前,图中并没有真正执行任何计算。当数据流图执行时,TF会向数据节点填充(feed)用户提供的、复合定义的数据。

TF的数据节点有占位符操作(placeholder Operation)实现,其对应的操作函数是tf.placeholder。针对稀疏数据,TensorFlow也提供了稀疏占位符操作(sparse placeholder operatin),其操作函数是tf.sparse_placeholde

3.4 运行环境:会话