目录
基础知识
系统结构
计算图
节点
边
张量
cluster-集群
XLA
symbolic tensor
MLIR
const folding
广播机制
广播的原则
数组维度不同,后缘维度的轴长相符
数组维度相同,其中有个轴为1
tfl
variable
Variable和Tensor的区别
数据类型
collective op
TensorFlow Eager模式
Stream & StreamExecutor
TensorFlow 中的一种计算规范。图中的节点表示操作。边缘具有方向,表示将某项操作的结果(一个张量)作为一个操作数传递给另一项操作。可以使用 TensorBoard 直观呈现图。
前向图中的节点,根据功能主要分为计算节点(Operation)、存储节点(Variable)和数据节点(Placeholder)3类。 Operation:对应无状态的计算或控制操作,主要负责算法逻辑表达或者流程控制。 Variable:对应有状态的变量操作,通常用来存储模型参数。 Placeholder:用于定义输入数据的类型和形状等属性,是对数据的统一抽象。
后向图中的节点,也可以分为3类,如下: 梯度:迭代过程中,模型参数的梯度。 参数更新操作:根据优化器的优化算法,结合梯度更新相应的模型参数。 更新后的参数:更新后的模型参数,用于模型的下一轮训练。
TensorFlow的OP代表一个基本运算,比如矩阵或则标量的四则运算。Node代表一个节点,是计算图的基本单位,可以为它绑定特定的运算,指定特定的设备等。
计算图中的边是有向边,定义了操作之间的关系,分为两类:一类用来传输数据,称为数据边;另一类用来定义依赖关系,称为控制边。 所有的节点都通过数据边或者控制边连接,其中入度为0的节点没有前置依赖,可以立即执行;入度大于0的节点,要等待其依赖的所有节点执行结束之后,才可以执行。
计算图结构
TensorFlow的系统结构以C API为界,将整个系统分为「前端」和「后端」两个子系统。前端系统扮演了Client的角色,完成计算图的构造,通过转发Protobuf格式的GraphDef
给后端系统的Master,并启动计算图的执行过程。
最终,Master将图进行分裂,通过RegisterGraph
接口,将GraphDef
的子图片段注册到Worker上。因此,GraphDef
是描述计算图的知识模型,整个TensorFlow的计算过程都是围绕GraphDef
所展开的。
NodeDef
通过op
从OpRegistry
中索引OpDef
。
计算图的构造过程,实际上就是GraphDef
定义过程。其中,OP的属性值承载于NodeDef
,计算图构造期间,NodeDef
的属性值得以确定。
OP表示某种抽象计算,它拥有0个或多个「输入/输出」,及其0个或多个「属性」。其中,输入/输出以Tensor的形式存在。
在系统实现中,OP的元数据使用Protobuf格式的OpDef
描述,实现前端与后端的数据交换,及其领域模型的统一。
OpDef定义包括OP的名字,输入输出列表,属性列表,优化选项等。其中,属性常常用于描述输入/输出的类型,大小,默认值,约束,及其OP的其他特性。
OP通过名字索引,因此必须保证OP的名字全局唯一。按照规范,OP的名字采用「驼峰」的命名风格,而Python前端则使用「小写下划线」的命名风格。后者也常常称为「OP构造器」,也是公开给用户的编程接口(API)。
SaverDef:记录持久化模型时需要用到的一些参数,比如保存到的文件名,保存操作和加载操作的名称以及保存频率,清理历史记录等。
张量:tensor
维度:dimension
The following domain-specific notation applies to memory format tags:
'n'
denotes the mini-batch dimension
'c'
denotes a channels dimension
When there are multiple channel dimensions (for example, in convolution weights tensor), 'i'
and 'o'
denote dimensions of input and output channels
'g'
denotes a groups dimension for convolution weights
'd'
, 'h'
, and 'w'
denote spatial depth, height, and width respectively
轴:同维度,axis
标量:scalar
阶:张量轴的个数,rank
数据类型:dtype
张量的属性:轴的个数、形状、数据类型
张量切片:tensor slicing
样本轴:sample axis,样本维度
批量轴:batch axis,批量维度
通道在后:channels-last
通道在前:channels-first
张量运算:tensor operation
逐元素:element-wise
广播:broadcast
张量积:点积运算,tensor product
张量变形:tensor reshaping
张量的存储
假设有一个k维张量,它的维数为(n1, n2, ..., nk),由于计算机的内存是连续的地址空间,所以在实际存储过程中存储的是1维向量。这个向量在内存中的大小为n1*n2*...*nk。
实际数值的排列方式可以从两个方向开始(从n1到nk或者从nk到n1),一般选择从nk这个维度开始,由小到大排列这个向量。即先填满nk的维度,再逐渐填满nk-1,直到n1维度。
假设有1个元素,它在张量中的具体下标是(i1, i2, ...ik),那么它在内存中是第i1*(n2*n3*...*nk)+i2*(n3*n4*...*nk)+...+ik-1*nk+ik个元素,我们称每个维度位置乘以的系数,即(n2*n3*...*nk), (n3*n4*...*nk), ..., 1为这个维度的步长(stride)或者系数(offset)。
Tensorflow 集群是一系列分布式执行计算图的tasks, 每一个 task 与一个 server 相对应,一个server 包含 master service 和 worker service。Master service 负责创建 session,worker 负责执行图中的计算操作。一个集群也可以被切分成多个 jobs,每个 job 包含一系列相同功能的 tasks。
见以下链接
TensorFlow之XLA_天边一坨浮云的博客-CSDN博客_tensorflow xla
A symbolic tensor differs from other tensors in that they do not specifically hold values.
Let's consider a simple example.
>>> a = tf.Variable(5, name="a")
>>> b = tf.Variable(7, name="b")
>>> c = (b**2 - a**3)**5
>>> print(c)
tf.Tensor(1759441920, shape=(), dtype=int32)
For the above, the values are specifically defined in tf.Variable format, and the output is in Tensor format. However, the tensor must contain a value in order to be considered as such.
Symbolic tensors are different in that no explicit values are required to define the tensor, and this has implications in terms of building neural networks with TensorFlow 2.0, which now uses Kerasas the default API.
现在机器学习模型已经深入到日常生活的方方面面,处理的任务也越来越复杂。那么随之而来的一个难题就是,怎样才能让机器学习模型的构建和训练过程变得更快?我们可以从这几个方面入手:
这个新的框架名为MLIR,全称是Multi-Level Intermediate Representation,是面向机器学习的编译架构,具有模块化、可扩展、可定制的特点。
对于用户而言,MLIR意味着可以调试模型更容易,还能获得更高的性能;而对于硬件供应商而言,MLIR意味着功能集成和优化更容易;对于研究人员而言,MLIR意味着基础架构的标准化。如今MLIR已经被许多大公司接受,全世界95%的数据中心的硬件也都支持MLIR,还有活跃的开源社区。
简单来说,MLIR是一个通用的图表示框架,一组通用的优化和转换过程,以及一个完整的代码生成流水线。
常量折叠(const folding
)的本质仅仅是一种编译器所提供的编译优化技术。
考虑以下代码
for (int i = 0; i < 200*800*700; ++i)
{
// 循环体
}
该循环的条件(i<200*700*800)是一个表达式(expression),如果放到判断时再求值那么200*700*800的计算将会进行112000000次。如果编译器在语法分析阶段进行常量合并,该循环将会变为这样:
for (int i = 0; i < 112000000; ++i)
{
// 循环体
}
当两个数组的形状并不相同的时候,我们可以通过扩展数组的方法来实现相加、相减、相乘等操作,这种机制叫做广播(broadcasting)。
比如,一个二维数组减去列平均值,来对数组的每一列进行距平化处理:
import numpy as np
arr = np.random.randn(4,3) #shape(4,3)
arr_mean = arr.mean(0) #shape(3,)
demeaned = arr -arr_mean
很明显上式arr和arr_mean维度并不形同,但是它们可以进行相减操作,这就是通过广播机制来实现的。
如果两个数组的后缘维度(trailing dimension,即从末尾开始算起的维度)的轴长度相符,或其中的一方的长度为1,则认为它们是广播兼容的。广播会在缺失和(或)长度为1的维度上进行。
这句话乃是理解广播的核心。广播主要发生在两种情况,一种是两个数组的维数不相等,但是它们的后缘维度的轴长相符,另外一种是有一方的长度为1。
我们来看一个例子:
import numpy as np
arr1 = np.array([[0, 0, 0],[1, 1, 1],[2, 2, 2], [3, 3, 3]]) #arr1.shape = (4,3)
arr2 = np.array([1, 2, 3]) #arr2.shape = (3,)
arr_sum = arr1 + arr2
print(arr_sum)
输入结果如下:
‘‘‘
[[1 2 3]
[2 3 4]
[3 4 5]
[4 5 6]]
‘‘‘
上例中arr1的shape为(4,3),arr2的shape为(3,)。可以说前者是二维的,而后者是一维的。但是它们的后缘维度相等,arr1的第二维长度为3,和arr2的维度相同。arr1和arr2的shape并不一样,但是它们可以执行相加操作,这就是通过广播完成的,在这个例子当中是将arr2沿着0轴进行扩展。
上面程序当中的广播如下图所示:
同样的例子还有:
从上面的图可以看到,(3,4,2)和(4,2)的维度是不相同的,前者为3维,后者为2维。但是它们后缘维度的轴长相同,都为(4,2),所以可以沿着0轴进行广播。
同样,还有一些例子:(4,2,3)和(2,3)是兼容的,(4,2,3)还和(3)是兼容的,后者需要在两个轴上面进行扩展。
我们来看下面的例子:
import numpy as np
arr1 = np.array([[0, 0, 0],[1, 1, 1],[2, 2, 2], [3, 3, 3]]) #arr1.shape = (4,3)
arr2 = np.array([[1],[2],[3],[4]]) #arr2.shape = (4, 1)
arr_sum = arr1 + arr2
print(arr_sum)
输出结果如下:
[[1 1 1]
[3 3 3]
[5 5 5]
[7 7 7]]
arr1的shape为(4,3),arr2的shape为(4,1),它们都是二维的,但是第二个数组在1轴上的长度为1,所以,可以在1轴上面进行广播,如下图所示:
在这种情况下,两个数组的维度要保证相等,其中有一个轴的长度为1,这样就会沿着长度为1的轴进行扩展。这样的例子还有:(4,6)和(1,6) 。(3,5,6)和(1,5,6)、(3,1,6)、(3,5,1),后面三个分别会沿着0轴,1轴,2轴进行广播。
The TensorFlow Lite dialect. This dialect maps to TensorFlow Lite operations.
Invariants: All values are of Tensor type (in particular, scalars are represented using zero-dimensional tensors);
A TensorFlow variable is the recommended way to represent shared, persistent state your program manipulates. ... Variable represents a tensor whose value can be changed by running ops on it. Specific ops allow you to read and modify the values of this tensor.
import tensorflow as tf
a = tf.Variable(1.0,name='a')
b = tf.Variable(2.0,name='b')
c = tf.add(a,b)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
print(sess.run(c))
sess.close()
其中,a,b是Variable,而c是Tensor。
注: 在TensorFlow中,变量的定义和初始化是分开的,所有关于图变量的赋值和计算都要通过tf.Session的run来进行。想要将所有图变量进行集体初始化时应该使用tf.global_variables_initializer。
Variable和Tensor之间的区别:
1. Variable是可更改的,而Tensor是不可更改的。
2. Variable用于存储网络中的权重矩阵等变量,而Tensor更多的是中间结果等。
3. Variable是会显示分配内存空间的,需要初始化操作(assign一个tensor),由Session管理,可以进行存储、读取、更改等操作。相反地,诸如Const, Zeros等操作创造的Tensor,是记录在Graph中,所以没有单独的内存空间;而其他未知的由其他Tensor操作得来的Tensor则是只会在程序运行中间出现。
4. Tensor可以使用的地方,几乎都可以使用Variable。
Tensor中的数据有数据类型 基本对应于常用的数据类型
下表列出了数据类型的名称和python的对应关系
数据类型 | Python 类型 | 描述 |
---|---|---|
DT_FLOAT | tf.float32 | 32 位浮点数. |
DT_DOUBLE | tf.float64 | 64 位浮点数. |
DT_INT64 | tf.int64 | 64 位有符号整型. |
DT_INT32 | tf.int32 | 32 位有符号整型. |
DT_INT16 | tf.int16 | 16 位有符号整型. |
DT_INT8 | tf.int8 | 位有符号整型. |
DT_UINT8 | tf.uint8 | 8 位无符号整型. |
DT_STRING | tf.string | 可变长度的字节数组.每一个张量元素都是一个字节数组. |
DT_BOOL | tf.bool | 布尔型. |
DT_COMPLEX64 | tf.complex64 | 由两个32位浮点数组成的复数:实数和虚数. |
DT_QINT32 | tf.qint32 | 用于量化Ops的32位有符号整型. |
DT_QINT8 | tf.qint8 | 用于量化Ops的8位有符号整型. |
DT_QUINT8 | tf.quint8 | 用于量化Ops的8位无符号整型. |
集合运算是 TensorFlow 计算图中的单个运算,它可以根据硬件、网络拓扑和张量大小在 TensorFlow 运行期间自动选择全归约算法。
使用过TensorFlow的大家都会知道, TF通过计算图将计算的定义和执行分隔开, 这是一种声明式(declaretive)的编程模型. 确实, 这种静态图的执行模式优点很多,但是在debug时确实非常不方便(类似于对编译好的C语言程序调用,此时是我们无法对其进行内部的调试), 因此有了Eager Execution, 这在TensorFlow v1.5首次引入. 引入的Eager Execution模式后, TensorFlow就拥有了类似于Pytorch一样动态图模型能力, 我们可以不必再等到see.run(*)才能看到执行结果, 可以方便在IDE随时调试代码,查看OPs执行结果。
Stream存在于计算机相关的各种技术中,比如在操作系统、流式计算、计算机网络传输或是CUDA编程中都有涉及。Stream从抽象角度来看其本质是定义了一个操作序列,处于同一个Stream的操作必须按顺序执行,不同Stream之间的并无顺序关系。在TensorFlow中存在一些高性能的并行编程设备,所以需要有一套抽象框架对这些设备的执行过程管理起来,这就是StreamExecutor的用武之地了。
其实StreamExecutor本身就是一个在Google内部为并行编程模型开发的单独的库,感兴趣的可以直接参考GitHub。在TensorFlow中的StreamExecutor是一个开源StreamExecutor的简版,并且并不是以第三方库的形式出现,而是在源码中单独放了一个stream_executor的文件夹,里面的代码非常的精简,目录结构部分截图如下图所示。
StreamExecutor为TensorFlow的执行层面提供了较为统一的抽象,而在底层各种Device的执行管理细节却完全不同。我们可以看到stream_executor下面有cuda和host两个子目录,他们分别是GPU执行引擎和CPU执行引擎所使用的子模块。