除了mlir::Op
C++模板特化的方式,MLIR也支持表驱动的方式去定义操作和数据类型。它的实现是通过TableGen(LLVM 中有介绍),TableGen即是一种通用语言,也是维护特定领域信息记录的工具。与操作相关的事实被精确地指定到TableGen记录中,TableGen记录将在编译器构建时展开为等价的mlir::Op c++模板特化。
本手册详细解释了以这种表驱动方式定义操作的所有可用机制。它的目标是成为一个规范而不是教程。后者请参考Quickstart tutorial to adding MLIR graph rewrite
除了介绍每个机制,本手册会尽力教你最优的实践方法。它们以引用的方式展现。
动机?(Motivation)
MLIR框架允许方言和方言的一系列Op插入。这种开放和可扩展的生态系统导致了“字符串”类型的IR问题,例如,在优化和分析过程中重复的字符串比较,不直观的访问方法(例如,泛型/容易出错的getOperand(3) vs 自我文档化的getStride()),返回类型更泛型,冗长和泛型构造函数没有默认参数,详细的文本IR转储,等等。此外,操作验证如下:
- 最佳方案:一个从字符串到验证函数的中心映射
- 一般方案:跨代码库的重复验证
- 最差方案:没有验证函数
对于上述问题的修正,可以通过支持以表驱动的方式定义Op。这么做之后,每一种方言都拥有一个中心地带可包含你想知道的任意Op的所有内容,包括Op的约束,自定义装配形式,等等。这些描述(指这些内容)还用于生成辅助函数和类,以允许构建、验证、解析、打印、分析等(这些都是MLIR提供的机制)。
好处(Benefits)
相比于C++模板,这种表驱动的方法包括但不限于以下这些好处:
- 单一数据源:我们努力将有关操作的所有事实编码到记录中,这样读者就不需要在代码段之间跳转来完全理解操作。、
- 删除模板(减少模板):我们可以从记录中自动生成参数(operand)/属性(attribute)/返回值(result)的获取方法,Op构建方法,Op检验方法,很其他更多的公用工具。这极大的减少了定义一个新Op所需要的模板。
- 促进自动生成:这些Op信息记录的用途不仅限于Op定义本身。我们可以利用这些记录去驱动其他组件的自动生成,例如计算图形序列化。
TableGen语法(TableGen Syntax)
我们把TableGen作为说明Op信息的语言。TableGen本身仅提供书写记录的语法。一个TableGen文件(通常文件名后缀为.td
)中允许的语法和构造可以在这里找到(此处有个链接)。
- TableGen中的
class
和C++的class
很相似,它可以作为模板或者作为基类去派生子类。 - TableGen中的
def
和C++中的对象相似,可以用一个TableGenclass
的特化来声明,例如,def MyDef: MyClass<...>;
,也可以单独使用def MyDef;
。它不能用作模板,也不能作为基类去派生子类。 - TableGen中的
dag
是一种专门用于有向无环图元素的类型。一个dag
类型带有一个操作符和零个或者多个参数。语法形如(operator arg0, arg1, argN.)
,其中operator
可以是任意的TableGendef
。参数可以是任何东西,包括dag
本身。我们可以将名称附加到操作符和参数上,如(MyOp:$op_name MyArg:$arg_name)
。(有点像是形参名的样子)。
可以通过(这里有个链接)去学习更多关于TableGen支持的类型和表达式。
Op定义(Operation Definition)
MLIR定义了几个公共的结构用于帮助定义Op,并通过TableGen backend : OpDefinitionsGen
提供Op的语义。这些公共结构在文件OpBase.td
中定义。主要包括:
Op
类:这是定义Op时使用的主要结构。在特化该类时,通过下述结构的帮助,指定与Op有关的所有事实。Dialect
类:归属于同一个逻辑组的Op会被放置在同一个方言下。Dialect
包含了方言等级(dialect-level?)信息。OpTrait
类及其子类:它们用于指定Op的特殊属性和约束,包括Op是否具有副作用、Op的输出是否与输入具有相同的形状(shape?)。ins/outs
标记:这是OpDefinitionsGen
后端内置的两个特殊标记,分别引导操作数(operands)/属性(attributes)、结果(results)的定义。TypeConstraint
类及其子类:它们用于指定对操作数(operands)或结果(results)的约束。一个值得注意的子类是Type
,它代表通用C++类型的约束。AttrConstraint
类及其子类:它们用于指定对属性(attributes)的约束。一个值得注意的子类是Attr
,它代表值为通用类型的属性的约束。
一个Op是通过特化Op
类定义的,特化后的Op
类包含它需要的所有字段的具体内容。举个例子,tf.AvgPoll
定义如下:
def TF_AvgPoolOp : TF_Op<"AvgPool", [NoSideEffect]> {
let summary = "Performs average pooling on the input.";
let description = [{
Each entry in `output` is the mean of the corresponding size `ksize`
window in `value`.
}];
let arguments = (ins
TF_FpTensor:$value,
Confined<I64ArrayAttr, [ArrayMinCount<4>]>:$ksize,
Confined<I64ArrayAttr, [ArrayMinCount<4>]>:$strides,
TF_AnyStrAttrOf<["SAME", "VALID"]>:$padding,
DefaultValuedAttr<TF_ConvertDataFormatAttr, "NHWC">:$data_format
);
let results = (outs
TF_FpTensor:$output
);
TF_DerivedOperandTypeAttr T = TF_DerivedOperandTypeAttr<0>;
}
下面我们将描述所需的所有字段。有关支持的字段的完整列表,请参阅Op
类的定义(就是OpBase.td
)。
Op的名字(Operation name)
Op名字是Op在MLIR中唯一标识符。举个例子,tf.Add
指的是TensorFlow
方言中的相加Op。这相当于汇编语言中的助记符。这用于IR文本格式的解析与打印。它也用于图重写中的匹配模式。
完整的Op名字是由方言名和Op名组成的,前者通过方言提供,后者作为Op
类的第二个模板参数提供。
Op的批注(Operation documentation)
它同时包含了一个单行的summary
(总结),和一个较长的人类可读的description
(批注)。它们将用于驱动方言批注的自动生成。它们需要在Op的定义体中被提供:
let summary = "...";
let description = [{
...
}];
description
需要用Markdown语法编写。
建议将文档放在开头,因为这有助于理解Op。
- 将批注放置在Op定义的开头。
- 总结应该简短而简明。它应该是没有结尾标点的一行代码。把扩展的解释放在描述中。
Op的参数(Operation arguments)
有两种参数:operands
(操作数)和attributes
(属性)。操作数是其他Op在运行时产生的值。而属性是静态编译时就能知道的常量值,属性有两类:
- Natural attributes(自然属性):这种属性会影响Op的行为(例如,填充的卷积)。
- Derived attributes(派生属性):这些属性在定义Op时不被需要,它们会从Op的信息中派生而来。例如,字体的输出形状。这主要用于方便的界面生成,或与其他框架/转换的交互。
所有派生属性都应该具备被具体化为一个属性的能力。也就是说,即使它们没有被具体化,也应该可以将其存储为属性。
操作数和属性都在dag
类型的arguments
中被指定,以ins
引导:
let arguments = (ins
<type-constraint>:$<operand-name>,
...
<attr-constraint>:$<attr-name>,
...
);
这里的<type-constraint>
是一个来自TypeConstraint
类层次的TableGen def
。与此类似的,<attr-constraint>
是一个来自AttrConstraint
类层次的TableGen def
。在Constraints章节有更多详细内容。
对操作数和属性的相对顺序没有要求,它们可以自由搭配。操作数与操作数之间的相对顺序是重要的。每一个已命名的参数都会自动生成一个通过名字的getter
(获取)函数,该函数会用适当的返回类型(对于属性来讲,返回类型将由存储类型构建,而对于操作数返回类型则为Value
)来返回这个参数。每个属性的初始值(例如,存储的)可以通过自动生成的<name>Attr getter
来访问,可用于在转换Pass中不太适合使用上述中用户友好的返回值类型的场景。
所有的参数应该被命名为
- 提供文档
- 驱动自动生成
getter
方法 - 提供一个句柄用作其他地方的引用。例如,约束。
可变操作数(Variadic Operation)
定义一个可变操作数,需要用Variadic<...>
把TypeConstraint
包起来。
通常,Op是没有可变操作数或者只有一个可变操作数。对于后一种情况,可以通过静态可变操作数的定义很容易的推导出动态可变操作数。但是,如果一个Op有多个可变长度操作数(可选的或可变长度的),在没有来自该操作的进一步信息的情况下,就不可能将动态操作数归因于相应的静态可变长度操作数定义。因此,需要用SameVariadicOperandSize
或AttrSizedOperandSegments
特征来表明所有的可变长度操作数都有与之对应的动态值。
可选的操作数(Optional operands)
定义一个可选操作数,需要用Optional<...>
把TypeConstraint
包起来。
通常,Op是没有可选操作数或者只有一个可选操作数。对于后一种情况,可以通过静态可选操作数的定义很容易的推导出动态可选操作数。但是,如果一个Op有多个长度可变操作数(可选的或可变长度的),在没有来自该操作的进一步信息的情况下,就不可能将动态操作数归因于相应的静态可变长度操作数定义。因此,需要用SameVariadicOperandSize
或AttrSizedOperandSegments
特征来表明所有的可变长度操作数都有与之对应的动态值。
可选属性(Optional attributes)
定义一个可选属性,需要用OptionalAttr<...>
把AttrConstraint
包起来。
属性的默认值(Attributes with default values)
定义一个带默认值的属性,需要用DefaultValuedAttr<..., "...">
。
DefaultValuedAttr
的第二个参数应该是包含C++默认值的字符串。举个例子,一个单精度浮点默认值需要被指定为“0.5f”
,一个整型数组的默认值需要被指定为"{1, 2, 3}"
。
限制属性(Confining attributes)
Confined
作为一种通用机制被提供,以帮助对值类型带来的属性约束进行进一步建模(意思大概是,对类型进行进一步的限制)。可以通过Confined
将较为原始的约束组合成为复杂约束。举个例子,一个32bit的整型最小值为10,可以被表示为Confined<I32Attr, [IntMinValue<10>]>
。
截至目前位置,已经支持了下列原始约束:
IntMinValue<N>
:指定一个大于等于N
的整型属性IntMaxValue<N>
:指定一个小于等于N
的整型属性ArrayMinCount<N>
:指定一个至少拥有N
个元素的数组属性IntArrayNthElemEq<I, N>
:指定一个第I
个元素等于N
的整型数组属性IntArrayNthElemMinValue<I, N>
:指定一个第I
个元素大于等于N
的整型数组属性
TODO:设计和实现更多的原始约束。
Op的域(Operation regions)
Op的域在一个dag
类型的regions
变量内部指定,由region
引导:
let regions = (region
<region-constraint>:$<region-name>,
...
);
可变域(Variadic regions)
与Variadic
类层次作用于可变操作数与可变结果类似,VariadicRegion<...>
用于域。可变域目前只能指定为域列表中的最后一个域。
Op的后继符?(Operation successors)
对于结束符Op,后继符在dag
类型的successors
内被指定,由successor
引导:
let successors = (successor
<successor-constraint>:$<successor-name>,
...
);
动态后继符?(Variadic successors)
与Variadic
层次类作用于动态操作数与动态结果类似,VariadicSuccessor<...>
可用于后继符。目前仅支持动态后继符作为后继符列表的最后一个。
Op的特征和约束(Operation traits and constraints)
特征是影响语法或语义的Op属性。MLIR C++的各种特征在mlir::OpTrait
命名空间中。
Op的特征、接口或者约束涉及多个操作数/属性/结果时,要作为Op
类的第二个模板参数传入。它们都需要继承于OpTrait
类。详见Constraints章节。
构建方法(Builder methods)
每一个Op,都会基于Op的参数和Op的返回值自动生成一些构建器。举个例子,给出如下的Op定义:
def MyOp : ... {
let arguments = (ins
I32:$i32_operand,
F32:$f32_operand,
...,
I32Attr:$i32_attr,
F32Attr:$f32_attr,
...
);
let results = (outs
I32:$i32_result,
F32:$f32_result,
...
);
}
会自动生成下列构建器:
// All result-types/operands/attributes have one aggregate parameter.
// 所有 结果类型/操作数/属性都集合为一个聚合参数。
static void build(OpBuilder &odsBuilder, OperationState &odsState,
ArrayRef<Type> resultTypes,
ValueRange operands,
ArrayRef<NamedAttribute> attributes);
// Each result-type/operand/attribute has a separate parameter. The parameters
// for attributes are of mlir::Attribute types.
// 每一个 结果类型/操作数/属性 都是一个独立的参数。属性参数为 mlir::Attribute 类型
static void build(OpBuilder &odsBuilder, OperationState &odsState,
Type i32_result, Type f32_result, ...,
Value i32_operand, Value f32_operand, ...,
IntegerAttr i32_attr, FloatAttr f32_attr, ...);
// Each result-type/operand/attribute has a separate parameter. The parameters
// for attributes are raw values unwrapped with mlir::Attribute instances.
// (Note that this builder will not always be generated. See the following
// explanation for more details.)
// 每一个 结果类型/操作数/属性 都是一个独立的参数。
// 属性参数是未经 mlir::Attribute 实例包装的原始值。
// (注意,该构建器并不总是生成。详见下列解释获得更多细节。)
static void build(OpBuilder &odsBuilder, OperationState &odsState,
Type i32_result, Type f32_result, ...,
Value i32_operand, Value f32_operand, ...,
APInt i32_attr, StringRef f32_attr, ...);
// Each operand/attribute has a separate parameter but result type is aggregate.
// 每一个 操作数/属性 都是一个独立的参数。但是结果全部集合为了一个聚合类型。
static void build(OpBuilder &odsBuilder, OperationState &odsState,
ArrayRef<Type> resultTypes,
Value i32_operand, Value f32_operand, ...,
IntegerAttr i32_attr, FloatAttr f32_attr, ...);
// All operands/attributes have aggregate parameters.
// Generated if return type can be inferred.
// 每一个 操作数/属性 都是一个独立的参数。
// 这个构建器只有在返回值类型能够被推断出的情况下,才会生成。
static void build(OpBuilder &odsBuilder, OperationState &odsState,
ValueRange operands, ArrayRef<NamedAttribute> attributes);
// (And manually specified builders depending on the specific op.)
// (以及根据具体Op手动指定的构建器。)
第一种形式提供了基本的一致性,因此我们可以使用相同的形式创建Op,而不管具体Op是什么。这对于实现声明式模式重写特别有用。
第二和第三种形式很适合用于手动编写的代码(疑问:啥叫手动编写的代码?),因为它们通过签名提供了更好的保证。
当Op的属性从Attr.storageType
返回的Attr.returnType
各不相同,且我们知道如何通过一个未包装的原始值构建出属性(例如,Attr.constBuilderCall
被定义)时,第三种形式的构建器才会被生成。另外,如果一个带有默认值的属性出现在Op参数列表的末尾时,默认值将在声明中提供。现在,BoolAttr, StrAttr, EnumAttr都可以使用(作为参数列表末尾带默认值的属性),将来列表还会继续增长。所以,可能的话,带默认值的属性要放在Op参数列表的末尾来利用这个特性(这种行为本质上是由于c++函数参数默认值放置限制。)。否则,第三种形式的构建器仍然会生成,但是不在Op参数列表末尾的带默认值的属性将不提供默认值在构建器的签名中。
ODS将会生成一个不需要指定返回类型的构建器,当满足以下条件:
- Op实现了
inferTypeOpinterface
接口 - 所有返回值类型时可构建出的类型或者于给出的操作数类型相同(例如,操作数与结果之间的
AllTypeMatch
约束)。
根据具体的Op,还可能存在其他的构建器。请参考generated c++ file以获得完整的列表。
自定义构建器方法(Custom builder methods)
如果以上自动生成的构建器不能满足所有需要,那么你可以定义其他方便的构建器在builders
字段,例如:
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins "float":$val)>
];
}
builders
字段是添加到Op
类的自定义构建器列表。在这个例子中,我们提供了一个方便的构造器,它接受浮点值而不是属性。在使用TableGen dag
的ODS中,许多函数声明都使用ins
前缀。紧随其后的是用逗号分隔的列表,列表的每一项都是类型与带$
前缀的名字的组合。上述定义将会转换成如下格式的构建器:
class MyOp : /*...*/ {
/*...*/
static void build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val);
};
注意,这个构建器由两个额外的前置参数。这些参数对于构建Op很有用。注意,为了能够通过该方法构建Op,必须向state
填充该Op的属性,操作数,域和返回值类型。builder
可以用于构建属于Op的任意IR对象,例如类型或嵌套Op。当类型与名字转换为C++代码时,它们应该是有效的C++结构,一个类型(在Op的命名空间中)与一个标识符(例如,class
不是一个有效标识符)。
可以在ODS中直接提供构建器的实现,使用如下TableGen的代码块:
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins "float":$val), [{
$_state.addAttribute("attr", $_builder.getF32FloatAttr(val));
}]>
];
}
$_builder
和$_state
这两个特殊参数等效于builder
和state
。在参数列表中的变量名,可以直接使用。例如,val
。构建器的c++代码实现会通过替换ODS中的特殊变量来完成,要保证构建器ODS实现的其他部分是有效的C++结构。虽然对代码大小没有限制,但我们鼓励只在ODS中内联较短定义的构建器,而将定义较长的构建器的定义放在C++文件中。
如果有参数需要一个默认值,定义时可以通过CArg
去包装该类型与默认值,如下所示:
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins CArg<"float", "0.5f">:$val), [{
$_state.addAttribute("attr", $_builder.getF32FloatAttr(val));
}]>
];
}
转换后的C++代码中,默认参数只在声明中出现,而不会在定义中出现,这符合C++要求。
/// Header file.
class MyOp : /*...*/ {
/*...*/
static void build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val = 0.5f);
};
/// Source file.
MyOp::build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val) {
state.addAttribute("attr", builder.getF32FloatAttr(val));
}
弃用:OpBuilder类允许将自定义构建器签名指定为原始字符串,而无需将参数分隔为不同的dag
类型参数。它还支持OpBuilder &
和OperationState &
类型的前置参数,如果存在自定义构建器时手动加上了这些前置参数,将使用这些参数代替自动生成的参数。
自定义文本解析器与打印器函数(Custom parser and printer methods)
用于解析和打印Op的自定义组装格式。
自定义校检代码(Custom verifier code)
验证代码将会根据Op的各个实体(这个实体是什么,指的是Op中包含的各种对象嘛?)的约束自动生成。要执行其他验证,你可以:
let verifier = [{
...
}];
放置在verifier
中的代码将会在自动生成的校检代码之后被调用。除verifier
外的性状验证顺序是不可靠的(Op各个实体的验证代码执行顺序是随机的)。
声明组装格式(Declarative Assembly Format)
Op的自定义组装格式可以用一个配备了Op操作数、属性、等有能力表达解析或者构建该Op所需额外信息的声明式字符串来指定。
def CallOp : Std_Op<"call", ...> {
let arguments = (ins FlatSymbolRefAttr:$callee, Variadic<AnyType>:$args);
let results = (outs Variadic<AnyType>);
let assemblyFormat = [{
$callee `(` $args `)` attr-dict `:` functional-type($args, results)
}];
}
格式由三个部分组成:
指令(Directives)
指令是一种带有可选参数的内置函数。可用的指令如下:
attr-dict
- 表示Op的属性字典。
attr-dict-with-keyword
- 表示Op的属性字典,并用一个
attributes
关键字作为字典前缀。
- 表示Op的属性字典,并用一个
custom<UserDirective>(Param)
- 表示一个用户在C++中实现的自定义指令。
- 查看Custom Directives章节以获取更多信息。
functional-type(input, results)
- 将
inputs
和results
参数格式化为函数类型(Function Type,在其他章节有介绍这种类型)。 inputs
和results
的约束与type
指令的input
相同。
- 将
operands
- 表示该Op的所有操作数
ref(input)
- 表示一个变量或者指令的引用,该变量或指令必须已解析,可用作
custom
指令的参数。 - 用于将以前解析过的实体传递给自定义指令。
input
可以是除了functional-type
与custom
之外的任何指令或者变量。
- 表示一个变量或者指令的引用,该变量或指令必须已解析,可用作
regions
- 表示Op的所有域。
results
- 表示Op的所有结果。
successors
- 表示Op的所有后继者。
type(input)
- 表示给定
input
的类型 input
必须是操作数或者结果变量,或operands
指令,或者results
指令。
- 表示给定
字面值(Literals)
字面值是用``包裹起来的键值或者标点符号。下列是有效的标点符号集合:
:
,,
,=
,<
,>
,(
,)
,{
,}
,[
,]
,->
,?
,+
,*
以下是有效的空格标点:
\n
,``````
\n
标点符号有另起一行的效果,例子如下:
let assemblyFormat = [{
`{` `\n` ` ` ` ` `this_is_on_a_newline` `\n` `}` attr-dict
}];
%results = my.operation {
this_is_on_a_newline
}
内容为空的``字面量可用于删除隐式插入某些字面量元素后的空格。
例如)
或者]
等等。举个例子,]
可能出现在输出output
的末尾,但它并不是格式中的最后一个元素,在这个例子里可以使用 "]``"删除掉后续的空格。
变量(Variables)
变量是注册在Op上的实体,例如Op的参数(属性或操作数),域,结果,后继者,等等。在CallOp
中,变量代表$callee
和$args
。
属性变量将显示其各自的值类型。除非其值的类型可以构造,在这种情况下,属性变量的值类型可以省略。
自定义指令(Custom Directives)
声明式组装格式规范在在格式化一个Op的时候能够处理大部分的普通场景。对于那些需要或者想要在格式中指定Op的某一部分的Op,声明式语法是不支持的,这个时候可以尝试使用自定义指令。自定义指令本质上就是允许用户使用c++打印和解析声明式指定格式的子部分。回头看上面章节对于自定义指令的说明:
custom-directive ::= `custom` `<` UserDirective `>` `(` Params `)`
一个自定义指令主要包含两个部分:UserDirective
与Params
。在生成该格式的C++代码时,自定义指令被转换为print *
与parse *
方法的调用。UserDirective
是一个标识符用于上述两个方法的后缀,例如:custom<MyDirective>(...)
会把上述两个方法分别变为parseMyDirective
与printMyDirective
。Params
可以是任何变量(例如,属性,操作数,后继者,等等),类型指令,attr-dict
的组合。类型指令必须引用一个变量,但该变量不需要也是自定义指令的参数。
parse<UserDirective>
方法的参数首先是对OpAsmParser(OpAsmParser &)
的引用,其次是与格式中指定的参数对应的一组输出参数。从声明的参数到parse
方法的形参的映射的详细说明如下:
- 属性变量(Attribute Variables)
- Single:
<Attribute-Storage-Type>(e.g. Attribute) &
- Optional:
<Attribute-Storage-Type>(e.g. Attribute) &
- Single:
- 操作数变量(Operand Variables)
- Single:
OpAsmParser::OperandType &
- Optional:
Optional<OpAsmParser::OperandType> &
- Variadic:
SmallVectorImpl<OpAsmParser::OperandType> &
- Single:
- 引用指令(Ref Directives)
- 使用与输入操作数一样的映射将一个引用指令传递给
parse
,一个单独的域通过Region &
传递。
- 使用与输入操作数一样的映射将一个引用指令传递给
- 域变量(Region Variables)
- Single:
Block *&
- Variadic:
SmallVectorImpl<Block *> &
- Single:
- 类型指令()