memref 中 ir 的顺序很重要,移动 ir 很可能导致程序语义改变,所以 clone 行为要注意。而 tensor 中的 clone 行为一般没问题。简而言之ir-on-tensor 中的 clone 行为即使改变了 ir 的次序,一般也不会影响程序语意。而 ir-on-memref就不行了,memref 上的 ir 要避免改变 ir 次序,否则可能发生下面的情况。
若把 %load clone 到 它的 user 前(scf.forall) 内,这样程序的语意就被改变了,因为中间有对 %alloc 的 def %load = memref.load %alloc def %alloc scf.forall use %load
7. linalg dialect的设计理念听到这个问题的我,流下了平时不好好看文档的泪水 。按我的理解,linalg包含 linalg-on-tensor 和 linalg-on-memref,做了一个很强的中间胶水层,向上承接程序的计算描述,向下准备下降到 hardware dialect,更贴近目标硬件。后面看了看 linalg 的官方文档上面赫然写着: Linalg is designed to solve the High-level Hierarchical Optimization
Linalg IR 也提供了许多好用的 transfroms:
- Progressive Buffer Allocation - Parametric Tiling - Promotion to Temporary Buffer in Fast Memory(摆数行为) - Tiled Producer-Consumer Fusion with Parametric Tile-And-Fuse - Map to Parallel and Reduction Loops and Hardware(方便区分tile parallel 和 reduction)
简而言之,Linalg Dialect 是很重要的一个层级,在这之前的 dialect 更多得是对计算的描述,表达原有的 ML 程序。而从 Linalg 开始,就会经过一系列变换(tile, fuse, promotion, bufferize)贴近目标硬件。 8. mlir中一些概念Smallvector 和 std::vector 的异同SmallVector会现在栈上分配一定的预留空间,当压入的元素所占空间超过其预留空间时就会退化到和std::vector差不多的行为(在堆上分配空间)。这样避免了小规模数据空间的申请和释放开销,而且std::vector在空间增长时采用的是倍增策略。栈上的空间是由编译器自动分配释放,一般存放函数参数和局部变量啥的。程序员申请的空间一般在堆上,需要手动申请和释放。StringRef 和 std::string 的异同StringRef是个轻量化的字符串引用类,指向现有的字符串数据,而不管理这片数据的地址(没有这片数据的所有权)。想要存储一个 StringRef 往往是不安全的。(因为data的真实memory可能随时被修改)llvm/include/llvm/ADT/StringRef.h 中写到:This class does not own the string data。const char *Data = nullptr; // 不能改变指针指向区域的值,但是可以改变指针指向的区域const char * ,指针可以改,指针指向的值不能改char * const,指针不能改,指针指向的值可以改std::string 完全管理自己的内存。SmallVector可以使用什么替换(ArrayRef、SmallVectorImpl 的使用场景)ArrayRef 表示对一个array的const引用,和 StringRef 一样,也没有真实数据的所有权。当传入函数的对象(Smallvector)不需要被修改时,用ArrayRef就可以避免不必要的拷贝。const SmallVector & <--> ArrayRef (better)SmallVectorImpl 在构造时不需要“预留元素个数”这个参数,所以函数可能传入的SmallVector实例大小不一时,常用SmallVectorImpl,这样避免依赖或硬编码任何具体的容量信息,减少参数的拷贝。如果数据很多,且每次增长(push进)的量很大,或许也可以采用std::vector,这样能减少调整空间的次数。 8. 写一个图的拓扑排序代码题乱入!这道题因为当时太紧张,都忘了图节点的关系应该用二维数组或者二维链表来定义了,所以就没写出来。面试官说其实是想考如何分析def-use链。这不巧了,我刚好写过很依赖def-use分析的pass。其实greedily fuse produer and consumer 的行为也是在对op间的关系进行一个拓扑排序,最终获得的 fuse序列应该保证原来的ir执行顺序的。我们以一个简单的序列为例,a -> b 表示 a 是 b 的 producer,b 是 a 的consumer。使用一个 set 类型的数据结构作为 visited 记录,选择 vector 类型的数据结构来记录结果的拓扑序。(1)先找到当前无依赖(无前驱/无produer)的节点作为起点,这里选1,作为当前的candidantOp(2)对于candidantOp,首先看 visited 中是否已经处理过 visited.insert(candidantOp).second,如果没有处理过,如果visited中没有则进入下一步,反正当前对该(3)遍历candidantOp的producer,若存在 producer ,则递归先把 producer 当作新的 candidantOp 去处理;若没有 producer 则把该 candidantOp 压入 topological seq vector。(4)然后遍历candidantOp的consumer,若consumer存在 ,则继续把 consumer 当作新的 candidantOp 去处理。直到所有op都处理完,(2)(3)(4)步会多次处理。结合上面的例子和算法,那么我们访问的顺序是:遇见1,1没访问过,1插入visited;1无produer,1入topological seq vector。访问1的consumer,首先是3。3没访问过,3插入visited;3的producer还没处理,则当前去处理2。2没有被访问过,则开始处理2。2无producer,2入topological seq vector。访问2的consumer,2的consumer只有3,且3在visited中,2的处理结束。返回3的处理,现在3的producer已在topological seq vector中,3无其他依赖,将3加入topological seq vector。访问3的consumer,4没访问过,4插入visited。4的producer是1和3,1和3都已经被visited,所以将4加入topological seq vector。4没有consumer。继续访问1的consumer,当前是4,但是4已经被visited,所以结束。最终获得序列 1->2->3->4。