InlineFunctions Pass¶
通过将函数体在每个调用点展开来消除 FunctionType.Inline 函数。
概述¶
被装饰为 @pl.function(type=pl.FunctionType.Inline)(或通过 JIT 端的 @pl.jit.inline)的函数是源级实用工具:每个调用点展开为一份新的、经过 alpha 重命名的函数体副本,其中形参由实际参数表达式替换。该 pass 运行后,程序中不会再有 FunctionType.Inline 函数,也不会有指向它的 Call — 后续 pass 把已展开的代码视为如同直接写在调用点处。
作为 OptimizationStrategy.Default 中的第一个 pass 运行,以确保下游 pass(UnrollLoops、OutlineIncoreScopes 等)永远不会观察到 Inline 函数。
产生: IRProperty.InlineFunctionsEliminated。
要求: 无 — 在新解析的程序上运行。
何时使用: 始终作为默认流水线的一部分。当程序中没有 Inline 函数时,该 pass 是空操作。
API¶
| C++ | Python | 级别 |
|---|---|---|
pass::InlineFunctions() |
passes.inline_functions() |
Program 级 |
Python 用法:
from pypto.pypto_core import passes
inline_pass = passes.inline_functions()
program_inlined = inline_pass(program)
算法¶
- 收集所有
func_type == FunctionType::Inline的函数。 - 环检测 Inline → Inline 调用图;若发现环,抛出
pypto::ValueError并在消息中标明环路径。 - 迭代到不动点 — 每次迭代遍历所有函数(包括 Inline 函数本身,以便嵌套的 Inline-calls-Inline 也能传递展开):
- 对函数体中每个顶层
LHS = inline_call(args)或EvalStmt(inline_call(args)):- 构建参数替换映射(形参
Var→ 实参Expr)。 - 对内联体中每个本地绑定的
Var做 alpha 重命名(<orig>_inline<counter>,并去掉<orig>末尾的_),避免多个调用点之间冲突。 - 在调用点之前插入重命名+替换后的函数体语句。
- 按调用点形态接线被内联函数的尾部返回值:
LHS = renamed_return(单返回值赋值;当LHS与替换后的返回Var是同一个Var时省略该赋值,以避免冗余 SSA 拷贝)、逐元素替换TupleGetItemExpr而不发出MakeTuple绑定(多返回值赋值)、新的ReturnStmt(return inline_call(...)),或者当返回值被丢弃但其求值可观测时发出新的EvalStmt(EvalStmt调用点 — 参见边界情况)。
- 构建参数替换映射(形参
- 删除所有 Inline 函数。
重命名后缀使用单下划线(_inline),因为 __ 被 IR 自动命名约定保留(参见 auto_name_utils.h)。
仅靠单下划线后缀并不足够:<orig> 本身可能以 _ 结尾——_ 是 Python 的弃值名,也是 for _ in pl.split_aiv(...) 文档所载的循环变量——此时直接拼接会把两个各自合法的下划线融合成保留分隔符。因此 FreshName 通过 auto_name::JoinNameSuffix 拼接,该函数会去掉这段尾巴:_ 重命名为 _inline7,而非 __inline7。若 <orig> 本身已经含有 __,则原样透传,使作者手写的 a__b 仍然是 ValidateBaseName 报告的用户错误,而不会被悄悄规范化。
示例¶
单一调用点¶
展开前:
@pl.program
class P:
@pl.function(type=pl.FunctionType.Inline)
def helper(self, x):
y = pl.mul(x, x)
return y
@pl.function
def main(self, a):
z = self.helper(a)
return z
展开后:
@pl.program
class P:
@pl.function
def main(self, a):
y_inline0 = pl.mul(a, a)
z = y_inline0
return z
多个调用点¶
每个调用点独立 alpha 重命名,本地变量不会冲突:
展开前:
@pl.function(type=pl.FunctionType.Inline)
def square(self, x):
y = pl.mul(x, x)
return y
@pl.function
def main(self, a, b):
a2 = self.square(a)
b2 = self.square(b)
return pl.add(a2, b2)
展开后:
@pl.function
def main(self, a, b):
y_inline0 = pl.mul(a, a)
a2 = y_inline0
y_inline1 = pl.mul(b, b)
b2 = y_inline1
return pl.add(a2, b2)
内联体含 pl.at¶
scope 被原样保留,稍后由 OutlineIncoreScopes 提取为独立的 InCore 函数,与直接写在调用点处效果一致。
边界情况¶
| 情况 | 行为 |
|---|---|
| 无调用点的 Inline 函数 | 静默从程序中移除。 |
| 作为程序入口的 Inline 函数 | 此处不视为错误 — 但因为没有任何 Call 指向它,清理阶段会像任何无调用者函数那样移除。 |
| Inline 调用 Inline(传递) | 迭代到不动点。 |
| 递归 Inline(自递归或互相调用) | 在任何展开发生之前抛出 pypto::ValueError,消息中标明环路径(a -> b -> a)。 |
| 多返回值 Inline | 不发出 LHS = MakeTuple([rets...]) — 编排层 codegen 无法 lower MakeTuple。改为把克隆后的返回值记录在 LHS Var 上,并把下游 TupleGetItemExpr(LHS, i) 的使用改写为第 i 个值,使该 LHS 绑定最终无人引用(参见 SpliceInlineCallAsTupleSub)。 |
嵌套 Call 到 Inline(如 pl.add(inline_fn(x), y)) |
v1 不处理 — 保持原样。InlineFunctionsEliminated verifier 会标记任何残留的 Call。 |
EvalStmt(inline_call(...)) — 返回值被忽略 |
被丢弃的是返回值,不是它的求值。参见下方丢弃返回值。 |
丢弃返回值¶
EvalStmt 调用点(self.wrapper(x, out),没有 LHS)无处安放被调用者的尾部返回值。丢弃那个值是对的,丢弃它的求值则不对 —— 求值过程可能通过 Out / InOut 参数写入、启动任务、阻塞等待信号,或完成硬件设置。因此每个被丢弃的值都要分类:
| 被丢弃的值 | 行为 |
|---|---|
Call —— 无论被调用者是跨函数还是 builtin |
按 return 顺序重新发出为 EvalStmt。若跨函数调用的被调用者同样是 Inline,不动点循环会在下一轮展开它;否则它就保持为一次普通派发,与作者在调用点直接写出来完全一致。 |
Submit |
重新发出为 EvalStmt。任务启动本身就是有副作用的,与被调用者做什么无关。 |
其它不藏有调用的值 —— Var、常量 |
丢弃。 |
本身不是 call-like、但包裹了调用的值 —— self.bump(n) + 1 这类标量算术、MakeTuple、TupleGetItemExpr |
抛出 pypto::ValueError。它无法变成 EvalStmt,而删除它会连带删除内层的调用。请直接返回该调用,或在调用点绑定 wrapper 的结果。 |
为什么是"所有调用",而不只是"会写的调用"。 IR 里没有任何东西能回答"这次调用可以安全删除吗"。最接近的注册表数据 OpRegistryEntry::WritesAnyArg 回答的是另一个问题 —— 算子是否通过参数写入 —— 以它为删除依据在两个方向上都会出错:
- 大多数算子根本没有分类 —— 撰写时 315 个里有 263 个,其中就包括被
dce::IsSideEffectOp列为副作用算子的tile.tpush_to_aiv和system.aic_initialize_pipe。OpRegistryEntry::HasDeclaredArgEffects存在的意义正是让分析能区分"已声明不写"和"尚无人查看"。 - 正面的
no_arg_writes()判定也不等于可删除。pld.system.wait会阻塞直到信号槽满足阈值,pld.system.defer_wait注册任务完成条件,system.set_ffts把 workspace 指针交给 FFTS 单元 —— 三者都声明了no_arg_writes(),却都承担同步或硬件设置语义。
因此该 pass 保留所有调用。被丢弃的真·纯调用会留下一条无用的 EvalStmt,流水线可以无害地带着它走完。要收窄这一点,需要一个真正的"可安全删除"算子属性 —— 逐算子声明,而不是从写入行为反推。
变换前:
@pl.function(type=pl.FunctionType.Inline)
def writeout(self, t, out: pl.Out[...]):
return pl.tile.store(t, [0, 0], out) # 写入本身就是返回表达式
@pl.function(type=pl.FunctionType.InCore)
def kernel(self, a, out: pl.Out[...]):
t = pl.tile.load(a, [0, 0], [64, 64])
self.writeout(t, out) # 返回值被忽略
return out
变换后 —— store 被保留:
@pl.function(type=pl.FunctionType.InCore)
def kernel(self, a, out: pl.Out[...]):
t = pl.tile.load(a, [0, 0], [64, 64])
pl.tile.store(t, [0, 0], out)
return out
验证¶
InlineFunctionsEliminated PropertyVerifier(注册到 IRProperty.InlineFunctionsEliminated)确认:
- 不存在
func_type == FunctionType::Inline的Function。 - 不存在指向 Inline 函数的
Call。
参见¶
python/pypto/jit/decorator.py—@pl.jit.inline是用户层入口(_SubFunctionDecorator("inline", ...))。- 02-unroll_loops — 紧随其后运行。
- 09-outline_incore_scopes — 处理展开后剩余的
pl.atscope。