跳转至

函数与程序结构

旧 Python 接口示例: 本页为兼容现有用法保留 platform= 调用, 这些调用会发出 DeprecationWarning;新代码应使用 backend_type 和 execution_mode。参见目标映射。

函数声明形式、参数方向、跨模块复用,以及如何把 IR 打印回 Python 语法。

JIT 实现结构

JIT 实现位于 python/pypto/jit/。公开的 jit 对象和 JITFunction 保留在 decorator.py;私有分析模块提供元数据,不持有编译对象缓存,也不负责调度 kernel。

模块 职责
decorator.py 装饰器、JITFunction、依赖发现、缓存所有权,以及编译/执行入口
_arguments.py 可选 torch 加载、张量实参元数据、注解和 signature/constexpr 校验
_ast_analysis.py 共享只读 AST 缓存、源码名称和动态维声明
_call_binding.py 调用点参数映射、constexpr 变体和跨依赖的动态维传播
_tensor_meta.py 按源码顺序推导局部张量元数据、处理分支/循环绑定及递归依赖返回值
specializer.py 专门化上下文、注解处理、程序源码组装与诊断源码位置映射
_specialize_body.py 有状态 AST 函数体改写、变量重命名与词法作用域处理
_specialize_values.py 专门化与缓存键共用的常量及 dtype/layout 源码渲染
_specialize_types.py 共享的 DynDim、ShapeDim、TensorMeta 定义;原有 specializer 导入路径保持有效
_source.py、cache.py、_persistent.py 请求命名空间快照、缓存键和持久产物解析

JITFunction 持有依赖图状态、编译对象缓存及其锁。缓存键构造与专门化继续共享 同一次请求的命名空间快照(namespace snapshot)。元数据推导通过显式回调(callback) 获取依赖,并将回调传入递归返回值分析,因此不需要在运行时导入装饰器模块。 共享 helper 通过其所属模块查找,使 AST 缓存和测试替换目标保持唯一。

JIT 常量与编译复用

@pl.jit 的函数体会被转写成 @pl.program 源码,并在只含 pl 与 pld 的命名空间中 重新解析;因此函数体从所在模块或外层函数继承的名字,必须在其使用处被替换成能求值回 同一个值的源码文本。可替换的范围包括:字面量(int、float、bool、str、None)、 pl 的 dtype 与枚举常量(pl.INT8、pl.Mem.Vec、pl.PadValue.zero、pl.NZ)、 分布式枚举常量(pld.ReduceOp.Max、pld.NotifyOp.AtomicAdd、pld.WaitCmp.Ge), 以及由它们嵌套构成的 list / tuple——例如工厂函数选定的 shape、舍入模式或集合通信归约操作。 没有源码形式的值保持原样,于是该名字得以保留,由解析器报错。

所有可替换的名字同时进入编译键(compilation key),包括传递 JIT helper 和源码注解中使用 的常量。编译键直接对每个名字实际生成的文本取哈希,且与 specializer 使用同一个函数, 因此两者不会脱节:改变生成源码的常量必然改变键,不可替换的值则不贡献任何内容。 重新绑定被引用的常量会产生新的专门化(specialization);修改无关全局变量, 或修改被正文局部变量遮蔽的同名全局变量,不会使正文依赖键失效。

BLOCK = 32

@pl.jit
def slice_kernel(x: pl.Tensor[[128, 128], pl.FP32]) -> pl.Tensor[[BLOCK, 128], pl.FP32]:
    with pl.at(level=pl.Level.CORE_GROUP):
        result = pl.slice(x, [BLOCK, 128], [0, 0])
    return result

first = slice_kernel.compile()
BLOCK = 64
second = slice_kernel.compile()  # A distinct specialization with 64 rows.

名称解析、键构造和专门化共用一次调用的命名空间快照(namespace snapshot)。 捕获后重新绑定的常量影响下一次调用,不影响正在编译的产物。 即使常量同名,各 helper 也保留各自的命名空间。重新绑定被引用的 JIT helper 还会刷新依赖图。各并发调用在键构造和专门化期间始终使用自己捕获的依赖图。 哈希还包含各 helper 的函数类型、层级和 auto_scope 设置,因此即使函数源码相同, 重新绑定到具有不同编译属性的 helper 后也不会复用旧产物。

每次请求只复制一次各模块的全局命名空间,各 helper 通过闭包覆盖层共享这份快照。 未绑定的闭包 cell 仍会遮蔽同名全局变量。验证后的依赖图保留 Python 源码哈希; 注解绑定不变时,复用已解析的布局。外部源码文件仍在每次请求时检查。 被引用的闭包常量由源码依赖哈希覆盖,无需单独的闭包键组件。

快照仅复制绑定:此常量跟踪机制不支持编译期间修改任意配置对象 内部状态,或修改编译器/源码文件。持久复用还需要完整的 产物身份与缓存策略。

动态维度元数据

解析特化程序前,JIT 会推导局部 TensorMeta,为仅有裸张量注解的 helper 补齐实参类型。 标量绑定使用 int 常量、已有张量轴的 DynDim 引用、不可变表达式元组或表示未知的 None。width = n * 8 保存为快照 (ast.Mult, (n_dim, 8));赋值别名保留同一元组, 后续算术则创建不同快照。重新绑定操作数的源变量名不会改变已有快照。

reshape 推导在记录 TensorMeta.shape 时,才为标量表达式建立结果专属的 DynDim 符号;标量快照不进入张量类型。同一结果 shape 内重复使用同一快照时共享 符号;不同结果建立独立绑定。被调函数从实际张量参数的描述符(descriptor)读取维度。 原始算术及 reshape 校验仍保留在程序中,占位上界不会替代真实运行时尺寸。

synthesized 标记自动生成的占位符号,继续沿用既有注解覆盖及 helper 返回行为。

JIT 内联 helper 的 Tile 参数

内联 helper 可以接收显式声明类型的片上 Tile:

@pl.jit.inline(auto_scope=False)
def keep_tile(tile: pl.Tile[[8, 64], pl.FP32, pl.MemorySpace.Vec]) -> pl.Tile:
    return tile

专门化(specialization)保留声明的形状、数据类型和内存空间,并折叠注解中引用的 模块与闭包常量。如果延迟求值的注解引用 helper 自身的 pl.constexpr 参数, 会先代入该调用的 constexpr 绑定,再求值注解;该绑定优先于模块或闭包中的同名常量。 不同绑定会生成各自的 helper 专门化。p.Tile 或直接导入的 Tile 等 DSL 别名 会转换为规范的 pl 路径,并保留 TileView、MemRef 元数据;用户恰好定义了 无关的同名变量 pl,也不会干扰生成结果。若工厂函数的局部常量仅用于立即求值的 Tile 注解、没有进入函数闭包,专门化可以保留原函数已经求值的注解。 即使部分形状轴依赖 helper 形参,其他独立的形状轴与 dtype 仍会保留,依赖形参的 部分则逐次代入调用参数;合并后的类型声明同样参与缓存身份计算。内存空间 constexpr 同样逐次代入,并保留独立的工厂形状。元数据替换需保持原有类别,无法确定类别时 会给出明确错误。若同一个表达式 同时引用形参与无法恢复的工厂局部值,请把该局部值作为另一个 constexpr 参数传入, 或在 helper 函数体中引用它以保留闭包。 仅被 Tile 参数类型引用的动态符号也会生成模块级 pl.dynamic() 声明,不再依赖 Tensor 参数同时引用这些符号。 生成的符号名与 Tile 或 Tensor 形参同名时也会保留声明;真实的 Scalar 维度形参仍使用局部绑定。 自由动态符号与不同的 Scalar 形参共用名字时,专门化会明确报告歧义;请为自由符号使用唯一的 pl.dynamic() 名字。

InlineFunctions 在代入实参之前检查 Tile 的数据类型、物理维数与形状,以及双方 已明确指定的内存空间是否冲突。泛型物理维度绑定到实参的物理尺寸,有效尺寸不参与 这一检查;支持 2*M 绑定到 16、得到 M=8 等整数仿射绑定。 推导前先代入已知 Scalar 实参:n=8、n+M=16 可推导出 M=8。 不同 Tile 实参可以独立绑定各自的泛型物理维度,helper 返回 Tile 形参时也遵循这一规则。 tmp = second; return tmp 等简单不可变别名也会沿用对应实参的返回类型。 与 Tensor 类型、显式 Tile 视图或 Scalar 形参共用的维度仍需满足共享约束,没有返回值的 helper 也会检查。 Scalar 实参值也约束只出现在视图中的维度:声明有效尺寸为 n 时,n=8 不能匹配实际有效尺寸 3。 n - 1 等依赖 Scalar 的复合视图表达式也会在代入实参后比较,覆盖有效尺寸、stride 和 offset。 这也包含通过 rows = 2*n 等共享维度间接依赖 Scalar 的表达式。 实参省略有效形状时使用物理形状,省略 offset 时使用零;依赖 Scalar 的 stride 约定需要实参具有显式 stride 元数据。 JIT 专门化也会保留嵌套 pl.Tuple 参数注解,包括其中完整的 Tile 类型、动态符号和逐次调用的 constexpr 绑定。 tuple 参数内的 Tile 也会递归检查相同的物理类型和视图约束。 为 Tile 注解生成的动态维度声明名必须使用 Python 规范化后可赋值的合法标识符,不能是 Python 关键字,也不能覆盖 DSL 别名 pl 或 pld。 内联(inlining)直接代入调用方的 Tile,无需复制,并保留其有效形状 (valid shape)。专门化也会保留非内联函数中可求值的 Tile 注解。 这会保留类型声明,但不会扩展设备派发接口;普通 InCore 入口仍不支持 Tile 参数。

编译选项与诊断请求

JIT 调用和 kernel.compile() 在查找进程内产物之前解析编译选项。 省略 config 时使用与 RunConfig() 相同的默认值:a2a3sim、默认优化策略, 以及关闭 pass dump。影响产物的选项进入缓存键,包括实际内存规划器、运行时 ABI, 以及由 PYPTO_EMIT_PTO_LOC 控制的源码位置输出。 device_id、codegen_only 和单独的 save_kernels 等运行控制不进入键。

以下请求每次都重新编译,不读取或添加产物缓存条目:

  • dump_passes=True 或除 PassDumpLevel.NONE 外的 dump 级别, 以及 dump_ptoas_passes=True。
  • compile_profiling=True、活跃的 CompileProfiler,或通过 PYPTO_COMPILE_PROFILING 启用的编译性能分析。
  • 显式指定 save_kernels_dir,或非空的 PYPTO_PROG_BUILD_DIR。
  • 显式诊断设置,或包含 instrument、验证/诊断设置不同于流水线默认值的活跃 PassContext。

因此已有缓存的 kernel 仍会输出请求的 dump 和报告;重复请求会重新生成输出。 诊断编译失败不会改变普通缓存条目。JIT 为每次编译自动分配唯一输出目录, 避免新的诊断请求覆盖缓存产物;显式指定的输出目录按请求使用。 诊断控制不进入编译键。 显式设置与活跃 PassContext 冲突时,缓存命中与首次编译都会报告相同错误。 仅选择规划器或运行时的普通上下文仍能复用匹配的缓存产物。

from pypto.runtime import RunConfig

cached = slice_kernel.compile()
slice_kernel.compile(config=RunConfig(dump_passes=True, save_kernels_dir="debug_kernel"))
assert slice_kernel.compile() is cached

预备二进制而不执行

kernel.warmup() 与 kernel.compile() 使用相同的特化、配置和进程内对象缓存, 并在返回前完成所有 kernel 与 orchestration 二进制的准备。它不会初始化 NPU, 也不会创建运行时 worker。构建主机仍需安装目标编译器、SDK,以及运行时的 Python 和原生依赖。

import pypto.language as pl
from pypto.runtime import RunConfig

@pl.jit
def add_three(
    x: pl.Tensor[[16, 16], pl.FP32],
    out: pl.Out[pl.Tensor[[16, 16], pl.FP32]],
):
    with pl.at(level=pl.Level.CORE_GROUP):
        tile = pl.load(x, [0, 0], [16, 16])
        pl.store(pl.add(tile, 3.0), [0, 0], out)
    return out

config = RunConfig(platform="a2a3")
prepared = add_three.warmup(config=config)  # No sample tensor allocation.
# Later, on a host with an available NPU:
# prepared(x, out, config=config)

与 compile() 一样,可以提供样本张量;预热只读取元数据,不读取张量内容。 张量注解完整时可以完全省略张量实参:标量参数是运行期值,这里无需提供其值, 动态维度沿用现有编译规则。pl.constexpr 参数是例外:它的值决定选中哪份产物, 因此必须通过关键字或签名默认值给出。 调用返回的编译对象时,需提供包含标量实参在内的完整参数列表——每个标量的值在此处读取; constexpr 参数不在该列表中,产物已经把它解析掉了。 codegen_only 等仅影响执行的设置不会禁止二进制准备。

返回值是 compile() 选中的同一个编译对象,新编译对象仍保留 IR。 预热覆盖 DistributedCompiledProgram 的所有芯片级子构建,以及多 orchestration CompiledProgram 的每个 orchestration 子构建。它不会调用分布式对象的 prepare(),后者用于创建执行所需的活动 worker。编译错误直接传递给调用方; 二进制构建失败后,可以再次调用 warmup 重试。诊断和显式输出请求仍按上文规则重新编译。

启用持久缓存 后,warmup 通过运行时协议 自动发布或复用 READY 产物。持久缓存默认关闭;只读未命中、不支持的输入和存储故障 可以生成私有结果。从缓存恢复的结果 .program is None;需要 IR 时关闭持久缓存, 或使用 specialize()/lower()。公共缓存策略、统计和仅用元数据预热的 CLI 参见 JIT 持久缓存。

函数

# Single return type
def function_name(param1: pl.INT64, param2: pl.FP32) -> pl.INT64:
    x: pl.INT64 = param1 + 1
    return x

# Multiple return types
def function_name(x: pl.INT64) -> tuple[pl.INT64, pl.INT64]:
    y: pl.INT64 = x + 1
    z: pl.INT64 = x * 2
    return y, z

# No return types
def function_name(x: pl.INT64):
    y: pl.INT64 = x + 1

# With function type
@pl.function(type=pl.FunctionType.Orchestration)
def orchestrator(n: pl.INT64) -> pl.INT64:
    return n + 1

@pl.function(type=pl.FunctionType.InCore)
def aicore_kernel(x: pl.INT64) -> pl.INT64:
    return x * 2

函数类型

类型 用途 描述
pl.FunctionType.Opaque 默认 未指定的函数类型
pl.FunctionType.Orchestration Host/AICPU 控制流和依赖分析
pl.FunctionType.InCore AICore AICore 子图执行(未特化)
pl.FunctionType.AIC Cube 核心 Cube 核心内核(特化的 InCore)
pl.FunctionType.AIV Vector 核心 Vector 核心内核(特化的 InCore)
pl.FunctionType.Group 多核 AIC + AIV 内核的协调调度组
pl.FunctionType.Graph 主机/AICPU 可录制的编排片段,由 host_build_graph runtime 回放(详见下文)

未指定类型时, 函数默认为 Opaque。

Graph 片段

pl.FunctionType.Graph 把函数标记为可录制的编排片段。在 host_build_graph runtime 下,每个调用点变成一次 task launch:runtime 在第一次调用时录制、之后回放, 于是 N 次调用只付一次建图代价,而不是 N 次:

@pl.program
class Decoder:
    @pl.function(type=pl.FunctionType.Graph)
    def layer(self, cur, normed, next_hidden, wq, layer_base: pl.Scalar[pl.INDEX]):
        ...

    @pl.function
    def decode(self, cur, normed, next_hidden, wq):
        for i in pl.range(40):
            self.layer(cur, normed, next_hidden, wq, i * 5120)

一个 Graph 函数就是一份被录制的拓扑:runtime 用生成的 C++ 函数地址来标识这份录制, 因此不存在需要命名、也不需要保证唯一的 cache key。

参数方向

参数可以使用包装类型指定 In (默认)、Out 或 InOut 方向:

@pl.function(type=pl.FunctionType.InCore)
def kernel(
    qi: pl.Tensor[[16, 128], pl.BF16],                   # In (default)
    output: pl.InOut[pl.Tensor[[16, 128], pl.FP32]],      # InOut
    result: pl.Out[pl.Tensor[[16, 128], pl.FP32]],        # Out
    scale: pl.Scalar[pl.FP32],                             # In (default)
) -> pl.Tensor[[16, 128], pl.FP32]:
    ...
方向 包装类型 描述
In 无 (默认) 只读输入参数
Out pl.Out[type] 只写输出参数
InOut pl.InOut[type] 读写输入/输出参数

约束: Scalar 参数不能使用 InOut 方向 (会抛出 ParserTypeError)。

写入 Out / InOut 参数

裸赋值不会写入参数。 它只是重新绑定 Python 名字: 参数 Var 指向一个新算出来的 张量,调用方的 buffer 完全没被碰过。程序照样能编译、能运行。调用方拿回什么取决于方向: Out buffer 是新分配且从未初始化的,读出来是垃圾值; InOut buffer 里仍是调用方传进来的 输入,结果是悄悄地陈旧。

out = pl.add(a, b)          # ❌ 什么也没写
out[:] = pl.add(a, b)       # ✅ 写入整个张量

要真正写入参数,请用下标形式:

写法 写入内容 适用场景
out[:] = <expr> 整个张量 结果就是整个输出
out[<slices>] = <expr> 该子窗口 只写输出的一部分
out = pl.assemble(out, <expr>, <offset>) <offset> 处的窗口 下标语法糖展开后的显式形式
out = <expr> 什么也不写 永远不要这么写——见下面的告警

只有第一行和第三行等价,且仅当切片覆盖全部范围、<offset> 全为 0 时才等价。

OutParamWriteDropped 告警

编译器会对丢失写入的裸赋值报告:

[warning] [OutParamWriteDropped] (pipeline_input) Assigning to Out parameter 'out'
in function 'main' rebinds the name only — the caller's buffer is never written.
Use 'out[:] = <expr>' to write the whole tensor, or 'out[<slices>] = <expr>' for
a sub-window. at repro.py:12:9

该检查基于数据流而非语法。一个值可以不提参数名就流回该参数——例如经由 loop carry—— 那是真正的回写,因此不会告警:

for col, (d,) in pl.range(0, n, chunk, init_values=(data,)):
    d = pl.store(local, [0, col], d)
    staged = pl.yield_(d)
data = pld.tensor.allreduce(staged, signal, ...)   # `staged` 就是 `data`; 不告警

该检查刻意保守: 仅仅读取参数的值 (例如 out = pl.add(out, b)) 同样会丢失写入, 但不会被报告。要区分「读取参数」和「通过参数回写」需要算子注册表并未记录的逐算子写语义, 而对正确代码误报的代价高于漏报。需要全量写入时请写成 out[:] = pl.add(out, b)。

如果该检查对你的程序没有价值,可以用 disabled_diagnostics 关闭:

disabled = passes.DiagnosticCheckSet()
disabled.insert(passes.DiagnosticCheck.OutParamWriteDropped)
ir.compile(program, disabled_diagnostics=disabled)

@pl.program 如何定位类定义

@pl.program 是从源码解析类体的,因此它必须先找到生成被装饰对象的那条 class 语句。仅凭类名无法确定这一点: 同一个函数可以在多个分支里定义同名类,它们的 __qualname__ 完全相同。

装饰器通过类体中各方法的行号来消歧,因此每个分支都按自己的源码解析:

def make(case):
    if case == "add":
        @pl.program
        class Prog:                                # 解析*这个*类体
            @pl.function
            def main(self, x: pl.Tensor[[8], pl.FP32]) -> pl.Tensor[[8], pl.FP32]:
                return pl.add(x, 1.0)
        return Prog

    @pl.program
    class Prog:                                    # 这个则解析*这个*类体
        @pl.function
        def main(self, x: pl.Tensor[[8], pl.FP32]) -> pl.Tensor[[8], pl.FP32]:
            return pl.mul(x, 3.0)
    return Prog

当多个定义确实无法区分时,装饰器会抛出 ParserSyntaxError 并列出全部候选行号, 而不是任选其一——选错就会编译出你从未写过的类体。此时请给各个类取不同的名字, 或者只定义一次、用闭包变量参数化:

def make(scale):
    @pl.program
    class Prog:                                    # 单一定义,参数化
        @pl.function
        def main(self, x: pl.Tensor[[8], pl.FP32]) -> pl.Tensor[[8], pl.FP32]:
            return pl.mul(x, scale)
    return Prog

跨模块函数复用

在 @pl.program 类之外定义的函数可通过两种机制复用。

外部 @pl.function 调用

在 @pl.program 内部可按名称调用外部定义的 @pl.function。该函数会自动加入 Program, 并生成 ir.Call(GlobalVar, args)。

@pl.function
def softmax(x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
    ...

@pl.program
class MyModel:
    @pl.function
    def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
        y: pl.Tensor[[64], pl.FP32] = softmax(x)   # ir.Call(GlobalVar("softmax"), [x])
        return y

规则:

  • 使用函数的 .name 作为 GlobalVar(别名透明)
  • 外部与内部函数名不得冲突
  • 两个不同的外部函数具有相同 .name 是错误
  • 同一外部函数从多个 method 调用时只加入一次

@pl.inline 装饰器

@pl.inline 捕获函数以便在语句级内联。不会向 Program 添加函数——每次调用点展开函数体。

@pl.inline
def normalize(x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
    result: pl.Tensor[[64], pl.FP32] = pl.mul(x, 2.0)
    return result

@pl.program
class MyModel:
    @pl.function
    def main(self, x: pl.Tensor[[64], pl.FP32]) -> pl.Tensor[[64], pl.FP32]:
        y: pl.Tensor[[64], pl.FP32] = normalize(x)  # statements inlined in-place
        return y

规则:

  • 实参个数必须与形参列表完全一致
  • 内联定义处的闭包变量可用
  • 内联函数可多次调用(每次展开相互独立)
  • 支持嵌套内联调用

完整示例

张量操作 (带 iter_args 的循环)

# pypto.program: my_program
import pypto.language as pl

def loop_sum(n: pl.INT64) -> pl.INT64:
    sum_init: pl.INT64 = 0
    for i, (sum,) in pl.range(n, init_values=(sum_init,)):
        sum = pl.yield_(sum + i)
    return sum

Tile 操作 (基于 Tile 的计算)

import pypto.language as pl

@pl.program
class BlockExample:
    @pl.function
    def tile_add(
        self,
        input_a: pl.Tensor[[64, 64], pl.FP32],
        input_b: pl.Tensor[[64, 64], pl.FP32],
        output: pl.Tensor[[64, 64], pl.FP32],
    ) -> pl.Tensor[[64, 64], pl.FP32]:
        tile_a: pl.Tile[[64, 64], pl.FP32] = pl.load(input_a, [0, 0], [64, 64])
        tile_b: pl.Tile[[64, 64], pl.FP32] = pl.load(input_b, [0, 0], [64, 64])
        tile_c: pl.Tile[[64, 64], pl.FP32] = pl.add(tile_a, tile_b)
        result: pl.Tensor[[64, 64], pl.FP32] = pl.store(tile_c, [0, 0], output)
        return result

打印 IR 节点

对任意 IR 节点调用 as_python() 获取其 Python 表示:

print(stmt.as_python())          # "x: pl.Scalar[pl.INT64] = a + b"(默认 "pl" 前缀)
print(stmt.as_python("ir"))      # "x: ir.Scalar[ir.INT64] = a + b"(自定义前缀)

通用 Function attrs 打印为函数体前导区的 pl.func_attr({...}) 指令。 解析后它们仍属于 Function 元数据,HOST SubWorker 也遵循这一规则: 捕获的 Python InlineStmt.body 排除这些指令,同时保留实现和 docstring。 引用参数的 attrs 绑定到函数自身的参数。前导区必须位于可执行语句之前 (允许开头的 docstring);指令位置错误或键重复都会报错,SubWorker 亦然。

打印器实现

私有的 IRPythonPrinter 声明位于 src/ir/transforms/python_printer.h。 各实现文件共享同一个访问器(visitor)实例,因此递归打印表达式、类型和语句时, 变量重命名、签名上下文(signature context)、缩进及作用域状态保持一致。

src/ir/transforms/ 中的源文件 职责
python_printer.cpp 公开入口与格式化回调、名称消歧、函数/程序打印及依赖排序
python_printer_expr.cpp 表达式优先级、常量、调用/提交及属性值
python_printer_stmt.cpp 控制流、作用域、注释、SSA yield 及窗口缓冲区声明
python_printer_type.cpp 类型标注、形状、内存引用(memory reference)及张量/Tile 视图

公开声明仍位于 include/pypto/ir/transforms/printer.h。 子表达式打印器继承外层的变量名称映射,保留形状表达式、内存偏移和槽位索引中的绑定。

简洁模式 (Concise Mode)

传入 concise=True 可省略中间变量的类型标注。函数签名类型(参数和返回值)始终保留:

print(func.as_python())                  # 详细模式(默认):每个赋值都包含类型
print(func.as_python(concise=True))      # 简洁模式:省略中间类型标注

详细输出:

def main(self, x: pl.Tensor[[64, 128], pl.FP32]) -> pl.Tensor[[64, 128], pl.FP16]:
    y: pl.Tensor[[64, 128], pl.FP32] = pl.some_op(x)
    result: pl.Tensor[[64, 128], pl.FP16] = pl.cast(y, pl.FP16)
    return result

简洁输出:

def main(self, x: pl.Tensor[[64, 128], pl.FP32]) -> pl.Tensor[[64, 128], pl.FP16]:
    y = pl.some_op(x)
    result = pl.cast(y, pl.FP16)
    return result

自由函数 ir.python_print(node) 同样可用,支持相同的参数。

参考资料