跳转至

编译与执行入口

每个入口做什么,以及它属于哪一层。

PyPTO 的编译与执行入口比它拥有的概念要多,而且好几个名字横跨抽象层:有六个不同的 函数叫 compile,而 run 既是运行时 worker 句柄上的方法,也是 PassPipeline 上的 方法。本页是这份地图——每个入口接受什么、产出什么、什么时候该用它。

四个层次

每个入口都恰好属于四层中的一层。名字没有表达出它属于哪一层时,混乱就产生了:

  define              compile                 artifact                execute
  ────────────────────────────────────────────────────────────────────────────
  @pl.program  ──┐                        CompiledProgram      ChipWorker.run()
  @pl.function   ├─>  ir.compile()   ──>   Distributed-        DistributedWorker.run()
  @pl.jit      ──┘    kernel.compile()     CompiledProgram     compiled(*args)
                      kernel.lower()  ──>  ir.Program

产物层之下还有一个装配层(assembly layer)——把 .pto.cpp 变成可加载的 二进制。它是内部实现,没有受支持的入口。

如何选择入口

你手上有 你想要
一个 @pl.jit 内核和 torch 张量 结果 kernel(*args, config=...)
一个 @pl.jit 内核 产物,用于反复派发 kernel.compile(*args, config=...)
一个 @pl.jit 内核 IR,不做 codegen kernel.lower(*args)
一个 @pl.program 产物 ir.compile(program, ...)
一个 CompiledProgram 派发一次 compiled(*args, config=...)
一个 CompiledProgram 和常驻设备的数据 派发多次 ChipWorker.run(compiled, *args)
一个 DistributedCompiledProgram 派发多次 compiled.prepare()DistributedWorker.run(...)
磁盘上的一个构建目录 派发一次,不重新编译 CompiledProgram.from_dir(work_dir)(*args, config=...)
一个 CompiledProgram 计时派发 benchmark(compiled, args, ...)

编译

入口 位置
ir.compile 编译驱动 ir/compile.py
JITFunction.compile 特化 + 驱动 jit/decorator.py
JITFunction.lower 只做特化,止于 ir.Program 同上
device_runner._compile_and_assemble 装配 runtime/device_runner.py
device_runner._compile_single_kernel / _compile_single_orchestration 装配 同上
KernelCompiler.compile_incore 调用 ptoas runtime/kernel_compiler.py

只有 ir.compile 是受支持的入口;表中其余项列在这里,是为了让 traceback 里出现的 名字能被定位到某一层。它还遮蔽了 Python 内置的 compile,因此更推荐 from pypto import ir 后调用 ir.compile,而不是直接导入这个名字。它接受的每个 选项都是关键字参数;program 是唯一的位置参数。

它的参数在编译中有说明。

它可能返回的两种产物类型,以及选定分布式那一种的配置,都从 pypto.ir 导出:

from pypto.ir import CompiledProgram, DistributedCompiledProgram, DistributedConfig

定义它们的模块 pypto.ir.distributed_compiled_program 仍然可以导入 —— pypto.runtime 直接从那里取,以避开导入环 —— 但用户代码与测试应当从 pypto.ir 取这三个名字。

执行

入口 接受什么 位置
CompiledProgram.__call__ 产物 产物句柄 ir/compiled_program.py
Worker.run / ChipWorker.run / DistributedWorker.run 执行 产物 + worker runtime/worker.py
CompiledProgram.from_dir / DistributedCompiledProgram.from_dir 产物 产物目录 ir/compiled_program.py
runtime.execute_compiled(已弃用) 执行 产物目录 runtime/runner.py
execute_distributed_compiled(已弃用) 执行 产物目录 runtime/distributed_runner.py
device_runner._execute_on_device 装配 已装配的二进制 runtime/device_runner.py
execute_artifact_dir / execute_batch_manifest CLI 产物目录 runtime/execute_artifact.py

run 本身不说明你在哪一层,接收者才说明。ChipWorker.runDistributedWorker.run 派发产物,二者都实现 Worker.runPassPipeline.run 与此无关——它变换一个 Program——PassManager.run_passes 同理。

派发一个目录而不是一个活的句柄,正是 replay 成立的前提: PyPTO 编译被完全跳过。from_dir 就是抵达那里的方式 —— 它从持久化的 sidecar 重建 产物句柄,而调用这个句柄走的路径,与一个从未离开内存的句柄完全相同。

execute_compiledexecute_distributed_compiled 曾是它在 L2 与 L3 的写法, 现已弃用。两者仍转发到同一份实现,并各自发出 DeprecationWarning

L3 那个是纯粹改名 —— 它本来就是 from_dir 加一次调用:

# before
execute_distributed_compiled(work_dir, args, config=cfg, platform="a2a3")

# after
ir.DistributedCompiledProgram.from_dir(work_dir, platform="a2a3")(*args, config=cfg)

L2 那个不是,因为两条路径的优先级规则不同:

设置项 execute_compiled CompiledProgram.__call__
platform 显式传入的参数 传了 config 就取 config.platform,否则取产物自带的
device_id / dfx / aicpu_thread_num 显式传入的参数 一律取自 config
ring 覆写项 取自 config 取自 config

也就是说,仅为 ring 尺寸而传的 config 会顺带接管其余各项;而 RunConfig.platform 默认为 a2a3sim,直接丢掉那些显式参数会把这次运行悄悄挪到 仿真器上。正确做法是把它们并入 config:

# before —— 显式参数优先,cfg 只被用来读 ring 尺寸
execute_compiled(work_dir, args, platform="a2a3", device_id=0, config=cfg)

# after —— 由 cfg 承载全部执行期设置
cfg = dataclasses.replace(cfg, platform="a2a3", device_id=0)
ir.CompiledProgram.from_dir(work_dir)(*args, config=cfg)

dfxaicpu_thread_num 不需要翻译:各个 DFX 开关与 aicpu_thread_num 本来就是 RunConfig 的字段。而 from_dir(platform=...) 仍决定不带 config 那次调用的 platform。

用同一个 config 驱动两个阶段

RunConfig 同时承载编译期与派发期的设置。compile_kwargs() 把编译期那一半提取为 ir.compile 的关键字参数,因此一个 config 对象可以驱动两个阶段:

config = RunConfig(platform="a2a3")
compiled = ir.compile(program, **config.compile_kwargs())
compiled(*tensors, config=config)

它的字段分三类,而每一类都是一个类型:

由谁读取 类型 字段
ir.compile CompileOptions platformstrategydump_passesdump_ptoas_passesprofilingcompile_profiling)、diagnostic_phasedisabled_diagnosticsanalyze_auto_scopes_for_depsoutput_dirsave_kernels_dir)、memory_plannerdistributed_config
派发 RunOptions platformdevice_idaicpu_thread_numring_* 覆写项(Ring 尺寸),以及内嵌的 DfxOptions
一次派发采集哪些诊断 DfxOptions enable_chip_swimlaneenable_dump_argsenable_pmuenable_dep_genenable_scope_statsDFX
仅系统测试 harness —— rtolatolgolden_data_dirsave_kernelscodegen_only
无人读取 —— 派生 —— backend_type,是 platform 上的只读属性,不是字段

platform 是两个决策,所以 RunConfig 存两个字段。 这个字符串打包了架构 (a2a3 / a5)和执行模式(sim 后缀),而两者各由不同的消费者读取:编译只取架构 —— codegen 根本看不到这个字符串 —— 装配层按后缀选 .so 还是 .o,worker 则比对整个 token。 因此 RunConfig 携带 arch: BackendTypeexecution_mode: ExecutionModeplatform 降为把二者序列化出来的派生属性:

RunConfig(arch=BackendType.Ascend950, execution_mode=ExecutionMode.ONBOARD).platform  # "a5"
RunConfig(platform="a5").arch                                                         # Ascend950

platform= 仍然可以构造(238 处调用点在用),并会同时设置两个轴。其余一切不动: cfg.platform 依旧是普通 str,所以产物 sidecar、--platform、simpler 的 Worker(platform=...) 都无感。拆分只落在选择目标的地方;产物、worker 与装配层 只是携带它,继续用字符串。

随之消失两样东西:__post_init__ 不再拿四个字面量校验字符串、也不再用它蕴含的 backend 把字符串重拼一遍 —— 一个与自身架构矛盾的 platform 已经不是能被构造出来的值。而 arch 的类型就是 BackendType、不是第三个枚举,所以 backend_type 只是这个字段在编译器词汇下的 名字,而非与之竞争的输入:CompileOptions 不带它,ir.compile 保留该参数仅供完全不传 platform 的调用方使用。

RunConfig.compile_options() / run_options() / dfx_options() 是这个聚合体上的视图, 而 compile_kwargs() 就是 compile_options().as_compile_kwargs()CompileOptionsir.compile 的叫法命名字段 —— 是 output_dir 而不是 save_kernels_dir —— 因为它存在的意义就是用编译器自己的词汇说出编译侧;它也可以独立使用:

from pypto.runtime import CompileOptions

compiled = ir.compile(program, **CompileOptions(platform="a2a3").as_compile_kwargs())

三个里只导出两个,因为只有两个是调用方真能交出去的东西。CompileOptions 如上, 解包进 ir.compileDfxOptionsexecute_compiledexecute_artifact_direxecute_batch_manifestdfx= 参数。RunOptions 两者都不是:每个派发入口 —— CompiledProgram.__call__ChipWorker.run 以及分布式那一对 —— 收的都是 RunConfig, 再自己经 run_options() 取派发侧那一半。直接把它交进去会抛 AttributeError, 所以在那些签名放宽之前,它保持内部(pypto.runtime.runner.RunOptions)。放宽本身是另一件 迁移:那意味着主派发 API 的 config= 参数要变成联合类型,而只改一部分入口比不改更糟。

RunConfig 保留全部字段与全部调用方;这三个类型是它底下的词汇。有一个单测把这个划分钉成 完备的 —— 每个 RunConfig 字段要么被某个视图认领,要么属于那五个 harness 专用字段 —— 于是以后新加的字段不会悄悄两边都不属于。把那五个挪去 harness 是本次没有做的一步: 它们也出现在 pypto-lib 的构造调用里。

compile_kwargs() 是通往 ir.compile 参数的唯一映射。@pl.jit 路径过去另有一份 副本,省略 platformbackend_type 并单独转发 platform;现在它和其他调用方一样 调用 compile_kwargs(),于是给一边加的开关不会在另一边悄悄缺席。lower() 仍有自己 那份更窄的映射 —— 它止步于 codegen 之前,对准的是 pass 流水线而不是 ir.compile

层与层之间的箭头朝哪

ir 产出产物,runtime 派发它。两者之间的箭头本该单向,而且大体上是单向的 —— 但 CompiledProgram 既是编译产物又是执行句柄,于是 pypto.ir.compiled_program 通过十处函数内 import 反向伸向 pypto.runtime

这些延迟导入通常被说成是为了打破导入环。逐个实测下来,真正如此的只有一处:

延迟导入 能提到模块级吗? 真实原因
runtime.runner(6 处) 分层选择 —— 不让派发层进入 pypto.ir 的导入图
runtime.distributed_runner(2 处) 同上
runtime.debug.run_script_writer 现在能了 曾是真环:它把 ParamInfocompiled_program 读回去
runtime.device_runner 不能 它在导入期就需要可选的 simpler

那唯一的真环已经消除。参数元数据移到了 ir/param_info.py —— 一个不从 pypto.runtime 导入任何东西的叶子模块 —— 重放脚本生成器改从那里读; compiled_program 重新导出这些名字,所以别处什么都不用动。有一个单测把这个叶子性质 钉住,因为往里拉进任何一个 runtime 导入都会让环回来。

pypto.runtime 直接导入 pypto.ir.distributed_compiled_program —— 箭头唯一反向的 那一处 —— 确实是为了避开导入环,保持原样。

彻底反转这个依赖,是比这些 import 语句大得多的问题。它意味着 CompiledProgram 不再可调用、 ir.compile 改为返回一个由 runtime 包装的描述符 —— 那会改掉每个 example 和两个下游仓库 都在用的 API 返回类型。这份 import 清单是那个双重身份的症状而非成因;而把那八处能提的都提到 模块级,只会让耦合更强 —— 那等于把 pypto.runtime.runner 放上 import pypto.ir 的关键路径。

哪些是内部实现

以下没有稳定性保证。它们不在任何 __all__ 中,PyPTO 之外的代码不应导入:

  • pypto.ir.compile —— _ensure_orchestration_headers
  • pypto.ir.compiled_program —— CompiledProgram._build_orch_argsCompiledProgram._build_call_config
  • pypto.runtime.runner —— _execute_compiled_execute_golden_case_build_call_config_coerced_to_orch_argsRunOptions
  • pypto.runtime.distributed_runner —— _execute_distributed
  • pypto.runtime.device_runner —— 整个装配层:_compile_and_assemble_compile_single_kernel_compile_single_orchestration_execute_on_device
  • pypto.runtime.kernel_compiler —— KernelCompilercompile_incore
  • pypto.runtime.tensor_argpypto.runtime.elf_parserpypto.runtime._binary_cache

装配层带下划线,是为了让 traceback 一眼看出它来自边界的哪一侧:一个必须拼出 _ 的导入,是一个做过决定的导入。KernelCompiler / compile_incore 以及最后一行的三个 模块是例外 —— 它们靠模块而非名字划为内部,稳定性保证同样为零。

这两个实参构造器把用户实参编排成 simpler 的 TaskArgsCallConfigChipWorker.runChipWorker.register 是抵达这条路径的受支持方式 —— 它们会替你调用 构造器;而自行构造 simpler.worker.Worker 的调用方,仍可从公开面拿到 chip_callableruntime_nameruntime_config

命令行入口

命令 用途
pypto-ir-trace 把 pass dump 渲染成交互式 lowering trace(IR lowering trace
python -m pypto.runtime.execute_artifact 执行一个产物目录或一份 batch manifest
python -m pypto.runtime.debug.replay 手改生成代码后重跑一个构建目录(Replay
python build_output/<dir>/debug/run.py 每次构建都会生成的自包含复现脚本
python -m pypto.tools.memory_map 把片上内存渲染成 HTML(Memory Map
python -m pypto.tools.clean_sim_trace 把仿真器 dump 转成可读 trace(Trace 清洗

参见