MLIR-Code Doc-Operation Definition Specification(ODS)

  除了mlir::OpC++模板特化的方式,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在运行时产生的值。而属性是静态编译时就能知道的常量值,属性有两类:

  1. Natural attributes(自然属性):这种属性会影响Op的行为(例如,填充的卷积)。
  2. 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中不太适合使用上述中用户友好的返回值类型的场景。

  所有的参数应该被命名为

  1. 提供文档
  2. 驱动自动生成getter方法
  3. 提供一个句柄用作其他地方的引用。例如,约束。

可变操作数(Variadic Operation)

  定义一个可变操作数,需要用Variadic<...>TypeConstraint包起来。
  通常,Op是没有可变操作数或者只有一个可变操作数。对于后一种情况,可以通过静态可变操作数的定义很容易的推导出动态可变操作数。但是,如果一个Op有多个可变长度操作数(可选的或可变长度的),在没有来自该操作的进一步信息的情况下,就不可能将动态操作数归因于相应的静态可变长度操作数定义。因此,需要用SameVariadicOperandSizeAttrSizedOperandSegments特征来表明所有的可变长度操作数都有与之对应的动态值。

可选的操作数(Optional operands)

  定义一个可选操作数,需要用Optional<...>TypeConstraint包起来。
  通常,Op是没有可选操作数或者只有一个可选操作数。对于后一种情况,可以通过静态可选操作数的定义很容易的推导出动态可选操作数。但是,如果一个Op有多个长度可变操作数(可选的或可变长度的),在没有来自该操作的进一步信息的情况下,就不可能将动态操作数归因于相应的静态可变长度操作数定义。因此,需要用SameVariadicOperandSizeAttrSizedOperandSegments特征来表明所有的可变长度操作数都有与之对应的动态值。

可选属性(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这两个特殊参数等效于builderstate。在参数列表中的变量名,可以直接使用。例如,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关键字作为字典前缀。
  • custom<UserDirective>(Param)
    • 表示一个用户在C++中实现的自定义指令。
    • 查看Custom Directives章节以获取更多信息。
  • functional-type(input, results)
    • inputsresults参数格式化为函数类型(Function Type,在其他章节有介绍这种类型)。
    • inputsresults的约束与type指令的input相同。
  • operands
    • 表示该Op的所有操作数
  • ref(input)
    • 表示一个变量或者指令的引用,该变量或指令必须已解析,可用作custom指令的参数。
    • 用于将以前解析过的实体传递给自定义指令。
    • input可以是除了functional-typecustom之外的任何指令或者变量。
  • 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 `)`

  一个自定义指令主要包含两个部分:UserDirectiveParams。在生成该格式的C++代码时,自定义指令被转换为print *parse *方法的调用。UserDirective是一个标识符用于上述两个方法的后缀,例如:custom<MyDirective>(...)会把上述两个方法分别变为parseMyDirectiveprintMyDirectiveParams可以是任何变量(例如,属性,操作数,后继者,等等),类型指令,attr-dict的组合。类型指令必须引用一个变量,但该变量不需要也是自定义指令的参数。

  parse<UserDirective>方法的参数首先是对OpAsmParser(OpAsmParser &)的引用,其次是与格式中指定的参数对应的一组输出参数。从声明的参数到parse方法的形参的映射的详细说明如下:

  • 属性变量(Attribute Variables)
    • Single: <Attribute-Storage-Type>(e.g. Attribute) &
    • Optional: <Attribute-Storage-Type>(e.g. Attribute) &
  • 操作数变量(Operand Variables)
    • Single: OpAsmParser::OperandType &
    • Optional: Optional<OpAsmParser::OperandType> &
    • Variadic: SmallVectorImpl<OpAsmParser::OperandType> &
  • 引用指令(Ref Directives)
    • 使用与输入操作数一样的映射将一个引用指令传递给parse,一个单独的域通过Region &传递。
  • 域变量(Region Variables)
    • Single: Block *&
    • Variadic: SmallVectorImpl<Block *> &
  • 类型指令()
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值