【TVM帮助文档学习】Relay的类型系统

本文翻译自Relay’s Type System — tvm 0.9.dev0 documentation

在介绍Relay表达式细节时,我们简单的涉及了Relay的类型,但是还没有详细描述整个类型系统。Relay是一种静态类型和类型推断语言,它在允许程序完全类型化的同时,只需要少量的显式的类型说明。
静态类型在执行编译器优化时非常有用,因为它们可以传递程序所处理的数据的属性,例如运行时形状、数据布局和存储,而无需运行程序。Relay的代数数据类型允许轻松、灵活地组合类型,以便构建可以归纳推理和用于编写递归函数的数据结构。
Relay的类型系统提供了一种形状依赖类型的形式。也就是说,它的类型系统在Relay程序中跟踪张量的形状。将张量形状视为类型允许Relay在编译时执行更强大的推理;特别是,Relay可以在程序操作很复杂的情况下,根据输入形状变化静态推理输出形状。将形状推断作为类型推断问题允许Relay在编译时推断所有张量的形状,包括在程序中使用分支和函数调用时。
以这种方式对形状进行静态推理允许对Relay做AOT编译(预编译?),并提供更多关于张量的信息,以便在编译过程中进一步优化。这样的优化可以实现为passes,这是Relay到Relay的AST转换,并且可以使用推断类型(例如,形状信息)来做出关于程序转换的决策。例如,src/relay/transforms/fuse_ops. cc给出了一个pass的实现,它通过张量形状推断将Relay程序中的算子调用替换为融合后算子的实现。
Relay使用type relation来编码实现张量类型的推理,这意味着Relay中的大部分类型检查是约束求解(确保在调用点满足所有类型关系)。类型关系提供了一种灵活且相对简单的方法,使依赖类型功能在Relay中可用,却不会大大增加类型系统的复杂度。
下面我们将详细介绍Relay中的类型语言,以及如何将它们应用到Relay表达式。

类型


Type类所有Relay类型的基类。所有的Relay类型都是它的子类。

相关定义和文档请查阅Type定义 

张量类型

表示Relay中的一个具体张量类型。
张量类型由元素类型和形状决定。当前它们使用TVM的数据类型和形状,但是将来Relay可能会为shape引入独立的AST。具体来说,数据类型包括bool、float32、int8和其他各种位宽和通道数。形状以维度元组(TVM IndexExpr)的形式给出,例如(5,5);标量也被赋予元组类型并具有()的形状。
不过请注意,TVM形状也可以包含变量和包含变量的算术表达式,因此Relay的约束求解阶段将尝试找到所有形状变量的赋值,以确保在运行程序之前所有形状都是确定的。
例如,下面是一个简单的具体张量类型,对应于一个10x10的32位浮点张量:

Tensor[(10, 10), float32]

元组类型

Relay的元组类型
元组是一个静态已知长度的值序列,元组的类型由与元组的每个成员相对应的类型序列组成。
因为元组类型的大小是静态已知的,所以元组类型的投影是元组类型的对应索引。
例如下面的代码中, %t的类型是(Tensor[(), bool], Tensor[(10, 10), float32]), %c的类型是

Tensor[(10, 10), float32]
let %t = (False, Constant(1, (10, 10), float32));
let %c = %t.1;
%c

类型参数

类型参数表示函数中用于多态性的占位符类型。类型参数是根据种类指定的,对应于允许这些参数替换的类型:

  • Type,对应Relay中最重要的类型,如张量类型、元组类型和函数类型   
  • BaseType,对应于张量的基类型(例如,float32, bool)   
  • Shape,对应于一个张量形状   
  • ShapeVar,对应于张量形状内的变量  

Relay的类型系统强制类型参数只允许出现在类型种类允许的地方,所以如果类型变量t是Type种类,张量[t, float32]不是有效的类型。
与普通参数一样,在调用点类型参数必须给出具体的实参。  
 
例如,下面的s是Shape种类的类型参数,它将在下面调用位置被(10,10)取代:  

def @plus<s : Shape>(%t1 : Tensor[s, float32], %t2 : Tensor[s, float32]) {
     add(%t1, %t2)
}
plus<(10, 10)>(%a, %b)

有关类型参数的定义和文档,请参阅TypeVar。  

类型约束

TypeConconstraint类是表示类型约束的抽象类,将在以后的版本中详细说明。 目前类型约束仅支持类型关系, 后面将会讨论到它。   
关于类型约束的定义和文档,请参阅TypeConconstraint。 

函数类型

可以从tvm/relay/type.h中查看函数类型的细节
这是Relay赋予函数的类型。一个函数类型由形参类型列表、类型约束集合、一系列实参类型和返回类型组成。
我们可以非正式地将函数类型写成:fn<type_params>(arg_types) -> ret_type where type_constraints
函数类型中的类型形参可以出现在实参类型或返回类型中。 此外,所有类型约束必须在函数的每个调用点执行。 类型约束通常接受函数的实参类型和函数的返回类型作为实参,但也可以只接受其中的部分。  
函数类型的定义和文档可以查看FuncType

类型关系

类型关系是Relay类型系统中最复杂的特性。 它允许用户使用新规则扩展类型推断。 我们使用类型关系为那些以复杂的方式处理张量shape的算子(例如广播算子或平坦化算子)定义类型,允许Relay为算子shape做静态推理。  
类型关系R描述了Relay函数的输入和输出类型之间的关系。 也就是说,R是一个类型函数,在关系成立输出true,关系不成立输出false。 提供给关系的类型可能是不完整的或包含形状变量,因此类型推断必须为不完整类型和形状变量分配适当的值,以保证关系成立(如果存在这些值)。  
 
例如,我们可以将恒等关系定义为:  

Identity(I, I) :- true

通过定义一个特定于算子的关系,对参数类型和返回类型的所有必要约束进行编码,可以方便地在Relay中添加算子类型关系。例如,我们可以定义flatten的关系:

Flatten(Tensor(sh, bt), O) :-
  O = Tensor(sh[0], prod(sh[1:]))

如果我们有一个像Broadcast这样的关系,就可以输入像add这样的算子:

add : fn<t1 : Type, t2 : Type, t3 : Type>(t1, t2) -> t3
            where Broadcast

 上面的Broadcast表明参数类型和返回类型必须是张量,其中t3的shape是t1和t2的shape的广播。类型系统可以接受任何满足广播规则的参数类型和返回类型。            
注意,上面的示例关系是用类似prolog的语法编写的,但目前在Relay中用户必须用c++或Python实现。更具体地说,Relay的类型系统为类型关系使用了一个专门的求解器,而类型关系使用c++或Python函数实现的,在这些函数中检查类型关系是否满足,并强制更新每个形状变量或不完整类型。类型关系函数在关系不成立返回False;在关系成立,或者没有足够的信息来确定关系是否成立时,返回True。
所有关系的函数都根据需要运行(如果输入被更新),直到下列条件之一满足:
1.所有关系成立,不存在不完整的类型(类型检查成功)。
2.关系不满足(类型错误)。
3.到达一个定点, 虽然shape变量或不完整的类型仍然存在(可能需要一个类型错误或更多的类型说明)。
当前Relay中使用的所有关系都是用c++实现的。在src/relay/op下可以看到c++实现关系的例子。
类型关系的定义和文档可以参考TypeRelation 

不完整类型

不完全类型是指未知的类型或类型的一部分。这只在类型推断期间使用。任何被省略的类型说明都将被不完整的类型所替换,并稍后将被另一种类型所替换。
在编程语言文献中,不完全类型被称为“类型变量”或“类型空洞”。我们使用“不完整类型”这个名字是为了更清楚将他们与类型参数区分:类型参数必须绑定到一个函数,并在调用点被具体类型参数(实例化)替换,而不完全类型可能出现在程序的任何地方,并在类型推断时被填充。
定义和文档可以查看IncompleteType

代数数据类型

注意:当前文本格式不支持ADT
在概述中详细描述了代数数据类型(ADT);本节描述ADT在类型系统中的实现。
ADT由一组命名构造函数定义,每个构造函数都接受特定类型的参数。ADT的实例是一个容器,存储构造函数参数的值,以及构造函数的名称;这些值可以在析构时通过构造函数匹配来检索。因此,ADT有时被称为“带标签的联合”:像c风格的联合一样,给定ADT实例的内容在某些情况下可能有不同的类型,而构造函数作为一个标记来指示如何解释内容。
从类型系统的角度来看,最恰当的是ADT可以接受类型参数(构造函数参数可以是类型参数,尽管具有不同类型参数的ADT实例必须被视为不同类型),并且可以递归(ADT的构造函数可以接受该ADT的实例,因此,像树或列表这样的ADT可以归纳建立)。类型系统中adt的表示必须能够满足这些场景,下面将详细介绍。

全局类型变量

为了紧凑而简单的表示ADT,允许ADT递归定义,我们以全局类型变量的形式给ADT定义一个句柄,该句柄可以唯一地标识ADT定义。每个ADT定义都有一个新的全局类型变量作为句柄,因此可以使用指针相等来区分不同的ADT名称。
对于Relay类型系统,ADT按名称进行区分;这意味着如果两个adt有不同的句柄,它们将被认为是不同的类型,即使它们的所有构造函数在结构上是相同的。
因此,ADT定义中的递归就像全局函数的递归一样:构造函数可以简单地在其定义中引用ADT句柄(全局类型变量)。
定义和文档可以查看GlobalTypeVar。

定义(类型数据)

除了名称之外,ADT还需要存储用于定义它的构造函数以及函数中使用的所有类型参数。它们存储在模块中,类似于全局函数定义。
在使用ADT进行类型检查时,类型系统有时必须使用ADT名称在模块中建立索引,以查找有关构造函数的信息。例如,如果一个构造函数在匹配表达式子句中被模式匹配,那么类型检查器必须检查构造函数的签名,以确保任何绑定变量被赋值为正确的类型。
有关其定义和文档,请参阅TypeData

类型调用

因为ADT定义可以接受类型参数,所以Relay的类型系统将ADT定义视为类型级函数(该定义接受类型参数,并返回带有这些类型参数的ADT实例的类型)。因此,ADT的任何实例都是使用类型调用来类型化的,该调用显式地列出了传给ADT定义的类型参数。
列出一个ADT实例的类型参数是很重要的,因为使用相同类型参数的两个不同构造函数构建的两个ADT实例,属于同一类型,而两个具有不同类型参数的ADT实例是不应该被认为是相同的类型(例如,整数列表的类型不应该与浮点张量对列表相同)。
类型调用中的“函数”是ADT句柄,ADT定义中的每个类型形参必须有一个实参。(一个没有参数的ADT定义意味着任何实例都没有类型参数传递给类型调用)。
有关其定义和文档,请参阅TypeCall。

示例:ADT表

本小节使用简单列表ADT(在Relay中作为默认ADT)来演示前面部分中描述的结构。其定义如下:

data List<a> {
  Nil : () -> List
  Cons : (a, List[a]) -> List
}


因此,全局类型变量List是ADT的句柄。模块中ADT表的类型数据提示List接受一个类型参数并有两个构造函数,Nil(签名为 fn() -> List[a])和 Cons (签名为fn(a, List[a]) -> List[a])。在Cons构造函数中对List的递归引用是通过在构造函数定义中使用全局类型变量List来完成的。
下面两个实例使用类型调用给出了类型列表:

Cons(1, Cons(2, Nil())) # List[Tensor[(), int32]]
Cons((1, 1), Cons((2, 2), Nil())) # List[(Tensor[(), int32], Tensor[(), int32])]

请注意,Nil()可以是任何列表的实例,因为它不接受任何使用类型参数的实参。(不过,对于任何特定的Nil()实例,都必须指定类型参数。)
下面是两个因为类型参数不匹配而被类型系统拒绝的列表:

# attempting to put an integer on a list of int * int tuples
Cons(1, Cons((1, 1), Nil()))
# attempting to put a list of ints on a list of lists of int * int tuples
Cons(Cons(1, Cons(2, Nil())), Cons(Cons((1, 1), Cons((2, 2), Nil())), Nil()))


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值