当前位置: 首页 > 工具软件 > llvm-mctoll > 使用案例 >

MLIR-Documentation-Conversion to the LLVM Dialect

华萧迟
2023-12-01

  几种依赖于内置类型(built-in types)的方言转换到LLVM方言(LLVM Dialect)需要借助方言转换(Dialect Conversion)的基础设施。
  本文档描述了类型和整个模块结构的类型转换。单个转换Pass(individual conversion passes)为不同方言的Op提供了一组转换模式。例如标准方言(Standard dialect)中的-convert-std-to-llvm和向量方言(Vector dialect)中的-convert-vector-to-llvm
  我们在整个文档中使用LLVM方言描述定义的术语。

类型转换(Type Conversion)

标量类型(Scalar Type)

  如果存在标量类型,则将其转换为LLVM对应类型。以下转换目前已实现:

  • i*转换为!llvm.i*
  • bf16转换为bf16
  • f16转换为f16
  • f32转换为f32
  • f64转换为f64
  • f80转换为f80
  • f128转换为f128

索引类型(Index Type)

  索引类型会转换成LLVM方言中带位宽的整型,这个位宽等于最近模块(closest module)的数据布局(data layout)指针的位宽。例如,在x86-64CPU上,索引类型会被转换为i64。这种行为可以由类型转换器配置覆盖,它通常作为转换Pass的一个Pass选项公开。

向量类型(Vector Types)

  LLVM IR仅支持一维向量,不像MLIR一样允许向量是多维的。向量类型不能嵌套在任何一个IR中。在一维向量的情况下,MLIR向量能够转换为相同大小(这里指向量长度一致)的LLVM IR向量,向量的元素将使用它们自己的转换规则进行转换。在n维向量的情况下,MLIR向量转换为元素类型为一维向量的(n-1)维数组。
  例如,vector<4xf32>转换为vector<4xf32>vector<4 x 8 x 16 xf32>转换为!llvm.array<4 x array<8 x vec<16 x f32>>>

分级内存引用类型?(Ranked Memref Types)

  MLIR中的Memref类型具有与之相关联的静态和动态信息。通常情况下,动态信息描述了逻辑索引空间中的动态大小以及任何与Memref绑定的符号。这个动态信息必须在运行时以LLVM方言等价类型展示。
  在实践中,转换支持两种约定:

  • Memref的默认转换在strided form中
  • 通过默认布局(default layout)将静态形状(statically-shaped)的Memref转换为一个"裸指针(bare pointer)"

  类型转换器在构建的时候会决定转换方式,通常情况是作为转换Pass的一个选项(相当于由选项决定转换方式)。
  不支持任意布局的Memref。但是,这些布局可以从类型中分解出来,作为Op索引计算的一部分,这样就可以通过默认布局来读取和写入Memref。

默认转换(Default Convention)

  动态信息包括缓冲区指针、缓冲区大小和任何动态尺寸的步幅。Memref类型被规范化并转换为仅依赖于分级Memref的描述符。描述符按照顺序包含以下字段:

  1. 指向已分配的数据缓冲区的指针,称为“已分配指针”。仅用于释放Memref。
  2. 指向正确对齐的数据指针的指针,这是Memref的索引,称为“对其指针”。
  3. 一个经过降低转换的索引(index)类型的整数,包含了缓冲区开始位置(对齐的)到通过Memref访问的第一个元素之间的元素数间距,称为“偏移”。
  4. 一个包含了转换后的索引(Index)类型的整型的数组,该数组的元素数量与分级Memref的分级层数一致:数组中的元素分别记录了Memref对应维度上的长度。对于Memref每个维度长度都为常量的情况,运行时值必须与静态值匹配(这里的静态指的是静态编译时期,要保证静态编译时期与运行时,Memref每个维度的长度前后不变)。
  5. 第二个数组元素也是转换后的索引(Index)类型的整型,该数组的元素数量与分级Memref分级层数一致:第二个数组代表了“步幅”(在张量抽象意义上)。举个例子,为了到达下一个逻辑索引元素,需要跳过底层缓冲区的连续元素的数量。

  对于Memref每个维度长度都为常量的情况,运行时值必须与静态值匹配(这里的静态指的是静态编译时期,要保证静态编译时期与运行时,Memref每个维度的长度前后不变)。这种规范化充当了Memref类型与外部链接函数互操作的ABI(这里确定不是API的意思?)。在Memref0级别的特殊情况下,描述维度与描述步幅的数组可以被省略,只剩下两个指针和一个偏移。
  来看一些例子:

memref<f32> -> !llvm.struct<(ptr<f32> , ptr<f32>, i64)>
// 这里有些疑问,为什么两个数组的元素类型不一致,一个是不带i的,一个是带i的?这是 LLVM 方言的两种标量类型嘛?
memref<1 x f32> -> !llvm.struct<(ptr<f32>, ptr<f32>, i64,
                                 array<1 x 64>, array<1 x i64>)>
memref<? x f32> -> !llvm.struct<(ptr<f32>, ptr<f32>, i64
                                 array<1 x 64>, array<1 x i64>)>
memref<10x42x42x43x123 x f32> -> !llvm.struct<(ptr<f32>, ptr<f32>, i64
                                               array<5 x 64>, array<5 x i64>)>
memref<10x?x42x?x123 x f32> -> !llvm.struct<(ptr<f32>, ptr<f32>, i64
                                             array<5 x 64>, array<5 x i64>)>

// Memref types can have vectors as element types
memref<1x? x vector<4xf32>> -> !llvm.struct<(ptr<vec<4 x f32>>,
                                             ptr<vec<4 x float>>, i64,
                                             array<1 x i64>, array<1 x i64>)>

裸指针转换(Bare Pointer Convention)

  具有static shape和default layout的Ranked Memref可以转换为指向其元素类型的LLVM方言指针。在这种情况下,只支持默认对齐方式。例如,不拥有对齐属性的allocOp。
  来看一些例子:

memref<f32> -> !llvm.ptr<f32>
// 这玩意儿,转为llvm之后我怎么解引用去使用?
memref<10x42 x f32> -> !llvm.ptr<f32>

// Memrefs with vector types are also supported.
memref<10x42 x vector<4xf32>> -> !llvm.ptr<vec<4 x f32>>

无分级内存引用类型(Unranked Memref types)

  无分级Memref被转为一个无分级描述符:

  1. 一个转换为索引类型的整数用于表示该Memref的动态分级(这个动态级别就很好的解释了什么是无分级,就是谁也不知道这个指针分了多少级)。
  2. 一个指向具有上面列出内容的分级Memref描述符的类型擦除指针(!llvm.ptr<i8>)

  这个描述符主要用于与rank-polymorphic动态库函数的接口。当动态库的函数使用这个指向分级Memref的指针时,这个指针会指向函数在堆上分配得到的内存。(这里有点难以理解,我用自己的理解来描述。)(原文:The pointer to the ranked memref descriptor points to memory allocated on stack of the function in which it is used.)

  请注意,堆栈分配可能在无分级Memref第一次出现的位置发出,例如,强制转换操作,并在函数的整个生命周期中保持活动状态;如果在循环中使用,这可能会导致堆栈耗尽。
  来看一些例子:

// Unranked descriptor.
memref<*xf32> -> !llvm.struct<(i64, ptr<i8>)>

  裸指针转换不支持无分级Memref。

函数类型(Function Types)

  函数类型转换为LLVM方言的函数类型。函数的参数根据它们自己的规则进行单独转换,函数参数和高阶函数中的Memref除外,我们在下面解释。函数的返回值类型需要适配,LLVM的函数总是有返回类型,虽然有可能是!llvm.void类型。转换后的函数总是只有一个返回类型。如果原函数(转换之前的函数)没有返回值,那么转换后的函数将会拥有一个!llvm.void返回值类型。如果原函数有一个返回值,转换后的函数也将会拥有一个转换后的返回值类型,返回类型的转换方式取决于返回值类型本身。除此之外,如果返回值是一个LLVM方言结构体类型,该结构体的元素与原函数返回值一一对应,它们之间的转换规则取决于它们自身类型。
  来看一些例子:

// Zero-ary function type with no results:
() -> ()
// is converted to a zero-ary function with `void` result.
!llvm.func<void ()>

// Unary function with one result:
(i32) -> (i64)
// has its argument and result type converted, before creating the LLVM dialect
// function type.
!llvm.func<i64 (i32)>

// Binary function with one result:
(i32, f32) -> (i64)
// has its arguments handled separately
!llvm.func<i64 (i32, f32)>

// Binary function with two results:
(i32, f32) -> (i64, f64)
// has its result aggregated into a structure type.
!llvm.func<struct<(i64, f64)> (i32, f32)>

函数作为函数的入参或者返回值(Functions as Function Arguments or Results)

  高阶函数类型,例如,一个将函数作为入参或者返回值的函数类型,它在向LLVM方言转换的时候必须要做出改变以适应LLVM IR不允许函数类型值的事实。但是,函数可以作为指针传入其他函数或者作为返回值。因此,函数类型的函数参数和返回值会被转换为指向函数的指针类型。该指针数据的转换规则如下:

// Function-typed arguments or results in higher-order functions:
(() -> ()) -> (() -> ())
// are converted into pointers to functions.
!llvm.func<ptr<func<void ()>> (ptr<func<void ()>>)>

// These rules apply recursively: a function type taking a function that takes
// another function
( ( (i32) -> (i64) ) -> () ) -> ()
// is converted into a function type taking a pointer-to-function that takes
// another point-to-function.
!llvm.func<void (ptr<func<void (ptr<func<i64 (i32)>>)>>)>

内存引用作为函数参数(Memref as Function Arguments)

  Memref作为函数参数时,分级与无分级的Memref都会被转换成参数列表,这些参数表示它们描述符的每个组件标量。这么做是为了与C ABI具有一定的兼容性,在这种情况下,如果使用结构类型则需要通过指针传递,从而导致需要分配以及其他相关问题,以及别名注释(目前在函数参数中附加到指针)。使用标量组件意味着每个尺寸和步幅作为单独的值传递。
  当Memref作为函数返回值使用时,Memref的转换使用Memref本身规则,例如,Memref转换为一个描述符结构体(默认转换),或者一个指针(裸指针转换)。
  来看一些例子:

// A memref descriptor appearing as function argument:
(memref<f32>) -> ()
// gets converted into a list of individual scalar components of a descriptor.
!llvm.func<void (ptr<f32>, ptr<f32>, i64)>

// The list of arguments is linearized and one can freely mix memref and other
// types in this list:
(memref<f32>, f32) -> ()
// which gets converted into a flat list.
!llvm.func<void (ptr<f32>, ptr<f32>, i64, f32)>

// For nD ranked memref descriptors:
(memref<?x?xf32>) -> ()
// the converted signature will contain 2n+1 `index`-typed integer arguments,
// offset, n sizes and n strides, per memref argument type.
!llvm.func<void (ptr<f32>, ptr<f32>, i64, i64, i64, i64, i64)>

// Same rules apply to unranked descriptors:
(memref<*xf32>) -> ()
// which get converted into their components.
!llvm.func<void (i64, ptr<i8>)>

// However, returning a memref from a function is not affected:
() -> (memref<?xf32>)
// gets converted to a function returning a descriptor structure.
!llvm.func<struct<(ptr<f32>, ptr<f32>, i64, array<1xi64>, array<1xi64>)> ()>

// If multiple memref-typed results are returned:
() -> (memref<f32>, memref<f64>)
// their descriptor structures are additionally packed into another structure,
// potentially with other non-memref typed results.
!llvm.func<struct<(struct<(ptr<f32>, ptr<f32>, i64)>,
                   struct<(ptr<double>, ptr<double>, i64)>)> ()>

随笔

  这一章,重点的Memref转换,疑点很多,比方说无分级Memref与分级Memref的区别,很模糊,无分级Memref到底时指一个不知道有多少层级的指针,还是指一个只有一层分级的指针?
  对应函数类型,将函数视为一个!llvm.ptr<>类型去传递。这个!llvm.ptr<>与传统意义中仅记录地址信息的指针应该也是不一样的,否则如何记录不同形参类型、返回值类型的函数?还是说,其实根本不需要记录,在所有用到这个函数的地方,都遵循同样的访问原则?对于这一点,是不是应该不看那么深,在MLIR->LLVM IR和LLVM IR -> MLIR,两个方向上,我遵循相同的访问原则是不是就可以了。实际上在这里还不需要知道LLVM IR是如何通过指针实现向更低代码的转换?

 类似资料:

相关阅读

相关文章

相关问答