MLIR内部Internals缓冲区释放
https://mlir.llvm.org/docs/BufferDeallocationInternals/
本节介绍缓冲区解除分配的内部功能 转型。转换由多个通道组成。主要passes 称为缓冲区释放可以通过MLIR上的“缓冲区释放”应用。 程序。
要求
为了在任意方言上使用BufferDeallocation缓冲区释放,几个控制流 使用自定义操作时必须实现接口。在输入程序的不同部分之间,这是了解隐式控制流依赖关系尤其重要。无需实现以下内容接口,无法正确发现控制流关系,并且生成的程序可能变为无效:
- Branch-like终止符应实现 BranchOpInterface 并操作关联的操作数。
- 涉及结构化控制流的操作必须实现RegionBranchOpInterface区域间控制流的模型。
- 终止符为其父操作生成值(特别是在基于运算的RegionBranchOpInterface嵌套区域的范围), 应该实现ReturnLike来表示逻辑“值” 返回”。
完全兼容的示例方言是“std”和“scf”方言支持所有实现的接口。
在Bufferization缓冲期间,我们将immutable 不可变值类型(张量)转换为可变值类型类型(memref)。此转换分几个步骤完成,在所有这些步骤中 IR 必须执行类似 SSA 属性的步骤。memref 的用法必须是按以下连续顺序排列:分配、写入缓冲区、读取缓冲区。在这种情况下,在初始完全缓冲区之后只允许初始化写缓冲区读取写完成。特别是,在之后不得对缓冲区进行部分写入初始写入已完成。但是,初始化中的部分写入是允许的(例如循环逐步填充缓冲区)。这意味着,所有缓冲区写入需要主导所有缓冲区读取。
打破不变量的示例:
func.func @condBranch(%arg0: i1, %arg1: memref<2xf32>) {
%0 = memref.alloc() : memref<2xf32>
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
cf.br ^bb3()
^bb2:
partial_write(%0, %0)
cf.br ^bb3()
^bb3():
test.copy(%0, %arg1) : (memref<2xf32>, memref<2xf32>) -> ()
return
}
仅在缓冲中需要维护类似 SSA 的属性过程。之后,例如在优化过程中,属性不再需要。
检测缓冲区分配
BufferDeallocation缓冲区释放转换的第一步是,确定实现SideEffects接口的可管理分配操作。 此外,这些操作需要将MemoryEffects::Allocate效果应用于不使用SideEffects::AutomaticAllocationScopeResource资源时的特定结果值(因为当前分配是保留的,类似Alloca将被父范围deallocated)。在此阶段未检测到的分配将不会在内部跟踪,因此不会自动释放。然而BufferDeallocation缓冲区释放与“混合”设置完全兼容,其中跟踪和未跟踪的分配是混合的:
func.func @mixedAllocation(%arg0: i1) {
%0 = memref.alloca() : memref<2xf32> // aliases: %2
%1 = memref.alloc() : memref<2xf32> // aliases: %2
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
use(%0)
cf.br ^bb3(%0 : memref<2xf32>)
^bb2:
use(%1)
cf.br ^bb3(%1 : memref<2xf32>)
^bb3(%2: memref<2xf32>):
...
}
将alloc 和 alloca条件分支一起使用的示例。BufferDeallocation缓冲区解除分配,可以检测和处理可能混合的不同分配类型。
注意:当前版本不支持分配操作返回多个结果缓冲区。
从 AllocOp 到 AllocaOp 的转换
如果可能,PromoteBuffersToStack-pass 会将 AllocOps 转换为 AllocaOps。在某些情况下,使用这种基于堆栈的缓冲区而不是 基于堆的缓冲区。转换仅限于几个约束,例如:
- 控制流
- 缓冲区大小
- 动态大小
如果缓冲区留下block块,我们不允许将其转换为alloca。 如果缓冲区的大小很大,我们可以转换它,但是关于堆栈溢出,限制这些缓冲区的大小并仅转换小的是有意义的。size可以通过passes选项设置。当前默认值为1KB。此外,我们不能转换具有动态大小的缓冲区,因为 维度是先验未知的。
分配的移动和放置
使用缓冲区提升通道,所有缓冲区分配都向上移动,尽可能对它们进行分组,并限制搜索空间,使即将到来的优化更容易。下图显示了这种运动。此外,如果我们将其移动到其所有用途的支配者。未来简化了进一步的优化(例如缓冲区融合)。但是,allocations变动受到外部的限制。数据依赖关系(特别是在动态形状类型分配的情况下)。此外,如果必要,为了将分配移动到与其相关的有效位置使用,我们利用活体信息。
以下代码片段显示了运行 缓冲吊装通道:
func.func @condBranch(%arg0: i1, %arg1: memref<2xf32>, %arg2: memref<2xf32>) {
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
cf.br ^bb3(%arg1 : memref<2xf32>)
^bb2:
%0 = memref.alloc() : memref<2xf32> // aliases: %1
use(%0)
cf.br ^bb3(%0 : memref<2xf32>)
^bb3(%1: memref<2xf32>): // %1 could be %0 or %arg1
test.copy(%1, %arg2) : (memref<2xf32>, memref<2xf32>) -> ()
return
}
在此程序上应用BufferHoisting缓冲区提升passes会导致以下部分代码数量:
func.func @condBranch(%arg0: i1, %arg1: memref<2xf32>, %arg2: memref<2xf32>) {
%0 = memref.alloc() : memref<2xf32> // moved to bb0
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
cf.br ^bb3(%arg1 : memref<2xf32>)
^bb2:
use(%0)
cf.br ^bb3(%0 : memref<2xf32>)
^bb3(%1: memref<2xf32>):
test.copy(%1, %arg2) : (memref<2xf32>, memref<2xf32>) -> ()
return
}
alloc从 bb2 移动到开头,并作为参数传递给 BB3.
下面的示例演示使用动态形状类型的分配。由于分配的数据依赖性为 %0,我们无法移动,在这种情况下,从 bb2 中分配:
func.func @condBranchDynamicType(
%arg0: i1,
%arg1: memref<?xf32>,
%arg2: memref<?xf32>,
%arg3: index) {
cf.cond_br %arg0, ^bb1, ^bb2(%arg3: index)
^bb1:
cf.br ^bb3(%arg1 : memref<?xf32>)
^bb2(%0: index):
%1 = memref.alloc(%0) : memref<?xf32> // cannot be moved upwards to the data
// dependency to %0
use(%1)
cf.br ^bb3(%1 : memref<?xf32>)
^bb3(%2: memref<?xf32>):
test.copy(%2, %arg2) : (memref<?xf32>, memref<?xf32>) -> ()
return
}
克隆介绍
为了保证所有分配的缓冲区都被正确释放,我们必须 注意控制流和缓冲区分配的所有潜在别名。由于并非所有分配都可以安全地释放其aliases别名(见下面的代码片段),经常需要引入 副本以消除它们。考虑以下示例,其中分配已经到位。
func.func @branch(%arg0: i1) {
%0 = memref.alloc() : memref<2xf32> // aliases: %2
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
%1 = memref.alloc() : memref<2xf32> // resides here for demonstration purposes
// aliases: %2
cf.br ^bb3(%1 : memref<2xf32>)
^bb2:
use(%0)
cf.br ^bb3(%0 : memref<2xf32>)
^bb3(%2: memref<2xf32>):
…
return
}
第一个分配可以在post-dominator block (bb3)其后支配者的实时范围之后安全地释放。bb1 中的分配在 bb3 中有一个别名 %2,也保留此缓冲区,一直到 BB3 结束。由于我们无法在运行时确定实际的分支,我们必须在 BB3 中确保正确释放所有缓冲区,无论我们将采用什么分支到达出口块。有必要为 %2 引入一个副本,这允许我们在 bb0 中释放 %alloc0,在 bb1 中释放 %alloc1。之后,我们可以继续处理 的所有别名 %2(在本例中没有),我们可以在示例末尾安全地释放 %2 程序。此示例演示并非所有分配都可以在它们相关的后支配者块中安全地释放。相反,我们必须注意他们所有的别名。
将缓冲区解除分配传递应用于上述程序,将产生以下结果:
func.func @branch(%arg0: i1) {
%0 = memref.alloc() : memref<2xf32>
cf.cond_br %arg0, ^bb1, ^bb2
^bb1:
%1 = memref.alloc() : memref<2xf32>
%3 = bufferization.clone %1 : (memref<2xf32>) -> (memref<2xf32>)
memref.dealloc %1 : memref<2xf32> // %1 can be safely freed here
cf.br ^bb3(%3 : memref<2xf32>)
^bb2:
use(%0)
%4 = bufferization.clone %0 : (memref<2xf32>) -> (memref<2xf32>)
cf.br ^bb3(%4 : memref<2xf32>)
^bb3(%2: memref<2xf32>):
…
memref.dealloc %2 : memref<2xf32> // free temp buffer %2
memref.dealloc %0 : memref<2xf32> // %0 can be safely freed here
return
}
请注意,引入了 %2 的临时缓冲区以适当地释放所有分配。进一步请注意,使用后传递转换或规范化通道之一,%3 的不必要分配很容易进行删除。
所呈现的示例也适用于动态形状的类型。
BufferDeallocation缓冲区解除分配执行定点迭代,将获取所有别名分配跟踪。使用所有跟踪的分配及其关联的别名,我们初始化一般迭代过程。一旦遇到未正确受我们的分配支配的别名,标记此别名为critical (需要由内部释放和跟踪定点迭代)。以下示例演示了critical 和non-critical别名:
func.func @condBranchDynamicTypeNested(
%arg0: i1,
%arg1: memref<?xf32>, // aliases: %3, %4
%arg2: memref<?xf32>,
%arg3: index) {
cf.cond_br %arg0, ^bb1, ^bb2(%arg3: index)
^bb1:
cf.br ^bb6(%arg1 : memref<?xf32>)
^bb2(%0: index):
%1 = memref.alloc(%0) : memref<?xf32> // cannot be moved upwards due to the data
// dependency to %0
// aliases: %2, %3, %4
use(%1)
cf.cond_br %arg0, ^bb3, ^bb4
^bb3:
cf.br ^bb5(%1 : memref<?xf32>)
^bb4:
cf.br ^bb5(%1 : memref<?xf32>)
^bb5(%2: memref<?xf32>): // non-crit. alias of %1, since %1 dominates %2
cf.br ^bb6(%2 : memref<?xf32>)
^bb6(%3: memref<?xf32>): // crit. alias of %arg1 and %2 (in other words %1)
cf.br ^bb7(%3 : memref<?xf32>)
^bb7(%4: memref<?xf32>): // non-crit. alias of %3, since %3 dominates %4
test.copy(%4, %arg2) : (memref<?xf32>, memref<?xf32>) -> ()
return
}
应用BufferDeallocation缓冲区解除分配将产生以下输出:
func.func @condBranchDynamicTypeNested(
%arg0: i1,
%arg1: memref<?xf32>,
%arg2: memref<?xf32>,
%arg3: index) {
cf.cond_br %arg0, ^bb1, ^bb2(%arg3 : index)
^bb1:
// temp buffer required due to alias %3
%5 = bufferization.clone %arg1 : (memref<?xf32>) -> (memref<?xf32>)
cf.br ^bb6(%5 : memref<?xf32>)
^bb2(%0: index):
%1 = memref.alloc(%0) : memref<?xf32>
use(%1)
cf.cond_br %arg0, ^bb3, ^bb4
^bb3:
cf.br ^bb5(%1 : memref<?xf32>)
^bb4:
cf.br ^bb5(%1 : memref<?xf32>)
^bb5(%2: memref<?xf32>):
%6 = bufferization.clone %1 : (memref<?xf32>) -> (memref<?xf32>)
memref.dealloc %1 : memref<?xf32>
cf.br ^bb6(%6 : memref<?xf32>)
^bb6(%3: memref<?xf32>):
cf.br ^bb7(%3 : memref<?xf32>)
^bb7(%4: memref<?xf32>):
test.copy(%4, %arg2) : (memref<?xf32>, memref<?xf32>) -> ()
memref.dealloc %3 : memref<?xf32> // free %3, since %4 is a non-crit. alias of %3
return
}
由于 %3 是critical别名,因此BufferDeallocation缓冲区解除分配引入了所有前置任务块中额外的临时copy。%3 有一个额外的(非关键) 别名 %4,将实时范围扩展到 BB7 结束。因此,可以在最后一次使用后释放 %3,同时考虑所有别名。请注意,%4 不需要释放,因为我们没有为它引入副本。
缓冲区副本的实际引入是在定点迭代之后完成的,并且已检测到所有关键别名。一个关键的 alias 可以是块参数,也可以是操作。块参数的副本通过分析所有前置参数来处理块。这主要是通过查询可以跳转到当前块的关联分支终止符。考虑 以下示例涉及一个简单的分支和关键块参数 %2:BranchOpInterface
custom.br ^bb1(..., %0, : ...)
...
custom.br ^bb1(..., %1, : ...)
...
^bb1(%2: memref<2xf32>):
...
这允许我们确定实际值,这是通过分析其前置块传递给块 BB1 及其参数 %2。 一旦我们解析了值 %0 和 %1(在此中与 %2 相关联) sample),可以引入一个临时缓冲区并将其内容克隆到新的缓冲区。之后,我们重新连接分支操作数以使用新分配的缓冲区操作数。但是,块可以隐式定义前置任务:实现的父操作。如果出现以下情况,则可能出现这种此块参数属于区域的入口块情况。在这种情况下,我们必须标识父操作定义的所有前置任务区域。对于每个区域,我们需要让所有终结者操作实现the trait,表明它们可以分支到我们当前的区块。最后,我们可以使用如上所述的类似功能来添加临时副本。这一次,我们无需接触高级接口,可以直接修改终结者操作数。BranchOpInterfaceRegionBranchOpInterfaceReturnLike
考虑以下使用虚数的内部区域控制流示例 “custom.region_if”操作。它要么执行“then”要么“else”区域,然后始终继续到“加入”区域。“custom.region_if_yield”操作将结果返回到父操作。此示例演示如何使用确定predecessors前辈以推断高级控制流:RegionBranchOpInterface
func.func @inner_region_control_flow(
%arg0 : index,
%arg1 : index) -> memref<?x?xf32> {
%0 = memref.alloc(%arg0, %arg0) : memref<?x?xf32>
%1 = custom.region_if %0 : memref<?x?xf32> -> (memref<?x?xf32>)
then(%arg2 : memref<?x?xf32>) { // aliases: %arg4, %1
custom.region_if_yield %arg2 : memref<?x?xf32>
} else(%arg3 : memref<?x?xf32>) { // aliases: %arg4, %1
custom.region_if_yield %arg3 : memref<?x?xf32>
} join(%arg4 : memref<?x?xf32>) { // aliases: %1
custom.region_if_yield %arg4 : memref<?x?xf32>
}
return %1 : memref<?x?xf32>
}
非块参数(其他值)在由方言特定操作。BufferDeallocation缓冲区释放通过. Consider支持此行为。考虑以下使用“scf.if”的示例,在运行时确定创建别名的 %2 值的操作:RegionBranchOpInterface
func.func @nested_region_control_flow(%arg0 : index, %arg1 : index) -> memref<?x?xf32> {
%0 = arith.cmpi "eq", %arg0, %arg1 : index
%1 = memref.alloc(%arg0, %arg0) : memref<?x?xf32>
%2 = scf.if %0 -> (memref<?x?xf32>) {
scf.yield %1 : memref<?x?xf32> // %2 will be an alias of %1
} else {
%3 = memref.alloc(%arg0, %arg1) : memref<?x?xf32> // nested allocation in a div.
// branch
use(%3)
scf.yield %1 : memref<?x?xf32> // %2 will be an alias of %1
}
return %2 : memref<?x?xf32>
}
在此示例中,插入一个 dealloc 以释放 else 中的缓冲区,因为程序的其余部分无法访问它。允许我们推断 %2 是非关键别名 %1,不需要跟踪。RegionBranchOpInterface
func.func @nested_region_control_flow(%arg0: index, %arg1: index) -> memref<?x?xf32> {
%0 = arith.cmpi "eq", %arg0, %arg1 : index
%1 = memref.alloc(%arg0, %arg0) : memref<?x?xf32>
%2 = scf.if %0 -> (memref<?x?xf32>) {
scf.yield %1 : memref<?x?xf32>
} else {
%3 = memref.alloc(%arg0, %arg1) : memref<?x?xf32>
use(%3)
memref.dealloc %3 : memref<?x?xf32> // %3 can be safely freed here
scf.yield %1 : memref<?x?xf32>
}
return %2 : memref<?x?xf32>
}
与前面的情况类似,我们必须检测 “scf.if”的所有附加区域,为其父操作提供值 (在此示例中通过 scf.yield)。查询允许我们以确定将结果“返回”到其父操作的区域。例如,在此之前,我们必须如上所述更新所有终结者。重新考虑“custom.region_if”示例的略微改编版本,来自上面使用嵌套分配:RegionBranchOpInterfaceReturnLike
func.func @inner_region_control_flow_div(
%arg0 : index,
%arg1 : index) -> memref<?x?xf32> {
%0 = memref.alloc(%arg0, %arg0) : memref<?x?xf32>
%1 = custom.region_if %0 : memref<?x?xf32> -> (memref<?x?xf32>)
then(%arg2 : memref<?x?xf32>) { // aliases: %arg4, %1
custom.region_if_yield %arg2 : memref<?x?xf32>
} else(%arg3 : memref<?x?xf32>) {
%2 = memref.alloc(%arg0, %arg1) : memref<?x?xf32> // aliases: %arg4, %1
custom.region_if_yield %2 : memref<?x?xf32>
} join(%arg4 : memref<?x?xf32>) { // aliases: %1
custom.region_if_yield %arg4 : memref<?x?xf32>
}
return %1 : memref<?x?xf32>
}
由于分配 %2 发生在发散分支中,并且在后支配者中无法安全地进行解除分配,%arg4 将被视为关键别名。此外,%arg4 返回到其父操作,并具有别名 %1。这导致缓冲区释放以引入其他副本:
func.func @inner_region_control_flow_div(
%arg0 : index,
%arg1 : index) -> memref<?x?xf32> {
%0 = memref.alloc(%arg0, %arg0) : memref<?x?xf32>
%1 = custom.region_if %0 : memref<?x?xf32> -> (memref<?x?xf32>)
then(%arg2 : memref<?x?xf32>) {
%4 = bufferization.clone %arg2 : (memref<?x?xf32>) -> (memref<?x?xf32>)
custom.region_if_yield %4 : memref<?x?xf32>
} else(%arg3 : memref<?x?xf32>) {
%2 = memref.alloc(%arg0, %arg1) : memref<?x?xf32>
%5 = bufferization.clone %2 : (memref<?x?xf32>) -> (memref<?x?xf32>)
memref.dealloc %2 : memref<?x?xf32>
custom.region_if_yield %5 : memref<?x?xf32>
} join(%arg4: memref<?x?xf32>) {
%4 = bufferization.clone %arg4 : (memref<?x?xf32>) -> (memref<?x?xf32>)
memref.dealloc %arg4 : memref<?x?xf32>
custom.region_if_yield %4 : memref<?x?xf32>
}
memref.dealloc %0 : memref<?x?xf32> // %0 can be safely freed here
return %1 : memref<?x?xf32>
}
释放放置
引入分配和副本后,必须将释放放置到免费分配内存并避免内存泄漏。需要在最后一次使用给定值之后进行释放。位置可以通过以下方式确定:使用剩余值计算所有非关键别名值的公共后支配符。一个特例是后边缘的存在:因为当新分配的缓冲区流回另一个缓冲区时,Edge 可能会导致程序的一部分内存泄漏。在这些情况下,我们需要释放关联的缓冲区,通过插入其他 Dealloc 来访问上一次迭代中的实例。
考虑以下包含嵌套结构化的“scf.for”用例控制流,如果:
func.func @loop_nested_if(
%lb: index,
%ub: index,
%step: index,
%buf: memref<2xf32>,
%res: memref<2xf32>) {
%0 = scf.for %i = %lb to %ub step %step
iter_args(%iterBuf = %buf) -> memref<2xf32> {
%1 = arith.cmpi "eq", %i, %ub : index
%2 = scf.if %1 -> (memref<2xf32>) {
%3 = memref.alloc() : memref<2xf32> // makes %2 a critical alias due to a
// divergent allocation
use(%3)
scf.yield %3 : memref<2xf32>
} else {
scf.yield %iterBuf : memref<2xf32>
}
scf.yield %2 : memref<2xf32>
}
test.copy(%0, %res) : (memref<2xf32>, memref<2xf32>) -> ()
return
}
在此示例中,嵌套的“scf.if”操作的then分支返回新分配的缓冲区。
由于此分配发生在发散分支的范围内,因此 %2 变为需要处理的关键别名。和以前一样,我们必须插入额外的副本以使用 %3 和 %iterBuf 的副本消除此别名。这保证了该 %2 将是每次迭代中返回的新分配缓冲区。 但是,将 %2 “返回”到其别名 %iterBuf 会将 %iterBuf 转换为关键别名也是如此。换句话说,我们必须创建一个 %2 的副本才能将其传递给 %iterBuf。由于此跳转表示后边缘,并且 %2 将始终是新的缓冲区,我们必须从上一次迭代中释放缓冲区以避免内存泄漏:
func.func @loop_nested_if(
%lb: index,
%ub: index,
%step: index,
%buf: memref<2xf32>,
%res: memref<2xf32>) {
%4 = bufferization.clone %buf : (memref<2xf32>) -> (memref<2xf32>)
%0 = scf.for %i = %lb to %ub step %step
iter_args(%iterBuf = %4) -> memref<2xf32> {
%1 = arith.cmpi "eq", %i, %ub : index
%2 = scf.if %1 -> (memref<2xf32>) {
%3 = memref.alloc() : memref<2xf32> // makes %2 a critical alias
use(%3)
%5 = bufferization.clone %3 : (memref<2xf32>) -> (memref<2xf32>)
memref.dealloc %3 : memref<2xf32>
scf.yield %5 : memref<2xf32>
} else {
%6 = bufferization.clone %iterBuf : (memref<2xf32>) -> (memref<2xf32>)
scf.yield %6 : memref<2xf32>
}
%7 = bufferization.clone %2 : (memref<2xf32>) -> (memref<2xf32>)
memref.dealloc %2 : memref<2xf32>
memref.dealloc %iterBuf : memref<2xf32> // free backedge iteration variable
scf.yield %7 : memref<2xf32>
}
test.copy(%0, %res) : (memref<2xf32>, memref<2xf32>) -> ()
memref.dealloc %0 : memref<2xf32> // free temp copy %0
return
}
类似循环的控制流示例。CFG 包含必须处理以避免内存泄漏。缓冲能够释放后边缘迭代变量 %iterBuf。
私有分析实施
缓冲区释放转换依赖于一个主控制流分析:缓冲区放置别名分析。此外,我们还使用优势和放置和移动节点的活跃度。活体分析决定活体 给定值的范围。在此范围内,值处于活动状态,可以或将要在程序过程中使用。在此范围之后,该值将失效,并且可以被丢弃 - 在我们的例子中,缓冲区可以被释放。为了放置分配,我们需要知道值将从哪个位置处于活动状态。分配必须是放在这个位置的前面。然而,最重要的分析是引入副本和放置所有副本所需的别名分析交易分配。
后期阶段
为了限制缓冲区释放转换的复杂性,一些 微小的代码润色/优化转换不会即时应用在安置期间。目前,规范化模式已添加到克隆中操作以减少不必要的克隆的出现。
注意:可能会将进一步的转换添加到passes后阶段。
克隆规范化
在放置克隆期间,可能会插入不必要的克隆。如果这些克隆及其相应的 dealloc 操作出现在同样的块,我们可以使用规范化器来删除这些不必要的操作。请注意,此步骤需要在插入克隆和缓冲区释放步骤中的释放。规范化包括两者,从克隆操作和源操作新创建的目标值。
克隆操作的源缓冲区的规范化
在这种情况下,可以使用克隆操作的源代替其目标。为定义的未使用的分配和取消分配操作,此克隆操作也将被删除。下面是一个由分配具有动态大小的缓冲区的缓冲区解除分配passes。更深层次对此样本的分析表明,突出显示的操作是多余的,并且可以删除。
func.func @dynamic_allocation(%arg0: index, %arg1: index) -> memref<?x?xf32> {
%1 = memref.alloc(%arg0, %arg1) : memref<?x?xf32>
%2 = bufferization.clone %1 : (memref<?x?xf32>) -> (memref<?x?xf32>)
memref.dealloc %1 : memref<?x?xf32>
return %2 : memref<?x?xf32>
}
将转换为:
func.func @dynamic_allocation(%arg0: index, %arg1: index) -> memref<?x?xf32> {
%1 = memref.alloc(%arg0, %arg1) : memref<?x?xf32>
return %1 : memref<?x?xf32>
}
在这种情况下,可以将附加副本 %2 替换为其原始源缓冲区 %1。这也适用于 %1 的关联解散操作。
克隆操作的目标缓冲区的规范化
在这种情况下,可以使用克隆操作的目标缓冲区代替它的来源。为此克隆定义的未使用的解除分配操作操作也被删除。
请考虑以下示例,其中一般测试操作写入结果到 %temp,然后将 %temp 复制到 %result。但是,这两个操作可以是合并为一个步骤。并且 %temp规范化会删除克隆操作,将 %temp 的用法替换为 %result:
func.func @reuseTarget(%arg0: memref<2xf32>, %result: memref<2xf32>){
%temp = memref.alloc() : memref<2xf32>
test.generic {
args_in = 1 : i64,
args_out = 1 : i64,
indexing_maps = [#map0, #map0],
iterator_types = ["parallel"]} %arg0, %temp {
^bb0(%gen2_arg0: f32, %gen2_arg1: f32):
%tmp2 = math.exp %gen2_arg0 : f32
test.yield %tmp2 : f32
}: memref<2xf32>, memref<2xf32>
%result = bufferization.clone %temp : (memref<2xf32>) -> (memref<2xf32>)
memref.dealloc %temp : memref<2xf32>
return
}
将转换为:
func.func @reuseTarget(%arg0: memref<2xf32>, %result: memref<2xf32>){
test.generic {
args_in = 1 : i64,
args_out = 1 : i64,
indexing_maps = [#map0, #map0],
iterator_types = ["parallel"]} %arg0, %result {
^bb0(%gen2_arg0: f32, %gen2_arg1: f32):
%tmp2 = math.exp %gen2_arg0 : f32
test.yield %tmp2 : f32
}: memref<2xf32>, memref<2xf32>
return
}
已知限制
BufferDeallocation 引入了来自“memref”方言的其他克隆 (“bufferization.clone”)。类似地,所有交易都使用“memref” 无方言操作“memref.dealloc”。 使用“test.copy”实现实际复制过程。此外,创建缓冲区在它们之后基本上是不可变的。在使用在块中的情况下,已知另一个限制无结构的控制流。
参考文献链接
https://mlir.llvm.org/docs/BufferDeallocationInternals/
标签:br,arg0,Internals,cf,MLIR,memref,func,缓冲区 From: https://www.cnblogs.com/wujianming-110117/p/16861864.html