几种依赖于内置类型(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方言描述定义的术语。
如果存在标量类型,则将其转换为LLVM对应类型。以下转换目前已实现:
i*
转换为!llvm.i*
bf16
转换为bf16
f16
转换为f16
f32
转换为f32
f64
转换为f64
f80
转换为f80
f128
转换为f128
索引类型会转换成LLVM方言中带位宽的整型,这个位宽等于最近模块(closest module)的数据布局(data layout)指针的位宽。例如,在x86-64
CPU上,索引类型会被转换为i64
。这种行为可以由类型转换器配置覆盖,它通常作为转换Pass的一个Pass选项公开。
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>>>
。
MLIR中的Memref类型具有与之相关联的静态和动态信息。通常情况下,动态信息描述了逻辑索引空间中的动态大小以及任何与Memref绑定的符号。这个动态信息必须在运行时以LLVM方言等价类型展示。
在实践中,转换支持两种约定:
类型转换器在构建的时候会决定转换方式,通常情况是作为转换Pass的一个选项(相当于由选项决定转换方式)。
不支持任意布局的Memref。但是,这些布局可以从类型中分解出来,作为Op索引计算的一部分,这样就可以通过默认布局来读取和写入Memref。
动态信息包括缓冲区指针、缓冲区大小和任何动态尺寸的步幅。Memref类型被规范化并转换为仅依赖于分级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>)>
具有static shape和default layout的Ranked Memref可以转换为指向其元素类型的LLVM方言指针。在这种情况下,只支持默认对齐方式。例如,不拥有对齐属性的alloc
Op。
来看一些例子:
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>>
无分级Memref被转为一个无分级描述符:
这个描述符主要用于与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。
函数类型转换为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)>
高阶函数类型,例如,一个将函数作为入参或者返回值的函数类型,它在向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作为函数参数时,分级与无分级的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是如何通过指针实现向更低代码的转换?