跳转至

重放已有的 build_output

在不从 DSL 重新编译的前提下,重跑、修改并重新测量一个已编译的 build_output/<jit_dir>/。本页引用的各项诊断开关记录在 运行时 DFX 开关

需要在改完 kernel cpp 之后重新跑一遍编译产物(典型场景:手调 kernel 后用 PMU / swimlane / args-dump 验证修改是否正确),使用 debug 专用 的 pypto.runtime.debug.replay 模块。L2 构建复用与普通 compiled(...) 派发相同的路径,L3 构建则经由从目录重建的 DistributedCompiledProgram 派发。两者的 DFX 开关行为完全一致。

from pypto.runtime.debug import replay
from pypto.runtime import RunConfig

replay(
    "build_output/_jit_xxx/",
    a, b, c,
    config=RunConfig(
        platform="a2a3sim",
        enable_pmu=2,
        enable_chip_swimlane=True,
    ),
)

CLI 形式(从目录里的 golden.py 加载输入):

python -m pypto.runtime.debug.replay build_output/_jit_xxx/ \
    --pmu 2 --swimlane --log-level debug

默认 recompile=True 会强制清掉缓存的 .so / .bin,确保手改的 cpp 能被重新编译。recompile=False(或 CLI 的 --no-recompile)只关闭该 强制失效;runtime / PTO-ISA 兼容性检查仍会运行,并可能清理和重建缓存产物。 复用还要求 runtime 与 PTO-ISA 的身份都能确定。runtime 源码 checkout 必须 保持干净;安装版 runtime 也可以使用内嵌的 build commit。PTO-ISA 目前必须是 干净的 Git checkout。若任一身份无法确定,PyPTO 会按安全失败策略重新构建, 而不会信任已有二进制。 --log-level 接受和 PYPTO_RUNTIME_LOG 相同的值(debuginfotimingwarnerrornull);加上 --log-sync-pypto 可以把同一档位推到 PyPTO 的 C++ logger。

validate=True(或 --validate)会在执行结束后,用 golden.py::compute_golden 计算参考输出,并按 golden.py 里声明的 RTOL / ATOL 公差逐 output 比对;不一致会抛 AssertionError。 该开关需要目录里存在 golden.pyir.compile 默认会产出)。

.pto 而不是 cpp

replay(以及自动生成的 debug/run.py)在清理 cpp 二进制之前会先 按 mtime 扫描 ptoas/*.pto:任何比同名 ptoas/<unit>.cpp 新的 .pto 都会触发一次 ptoas 重跑,新生成的 body 会 splice 到所有命中 的 kernels/<core>/<func>.cpp —— 也就是在两条 sentinel // --- ptoas-generated code ---// --- Kernel entry point --- 之间替换。随后照常走 cpp → .so 重编译。

改了哪些文件 实际触发的路径
只改 kernels/<core>/<func>.cpp cpp → .so(保持原有行为)
只改 ptoas/<unit>.pto pto → cpp → .so(新增 —— splice + 重编译)
两者都改 .pto 决定 body 段;用户在 cpp wrapper / header 上的改动保留

需要 ptoas 可被发现(PTOAS_ROOTPATH);找不到时静默跳过。 关闭方式:--no-rebuild-from-ptoPYPTO_REBUILD_FROM_PTO=0。 若 .pto 编辑会改变 kernel 函数签名,不在本特性范围:保存的 wrapper 模板对不上,必须重新 ir.compile()

自动生成的 debug/run.py

ir.compile() 会在 <output_dir>/debug/run.py 写一个自包含的 重跑脚本,用户只需要记住一条命令:

python build_output/<jit_dir>/debug/run.py

脚本是对上面 replay 流程的封装:

  • 如果同目录有 golden.py,输入来自 golden.generate_inputs(),并用 compute_golden 做数值校验。
  • 否则(JIT 路径),输入由脚本内嵌的 shape / dtype 元数据构造, 用户可自由修改用于实验。脚本还预留了一个 _user_compare(<参数名>) 钩子,会在 replay 返回后自动调用 —— 在里面手写 assert torch.allclose(...) 即可对 kernel 输出做 自定义比对。
  • 上面 "改 .pto 而不是 cpp" 一节描述的 .pto 重建流程在生成的 脚本里同样生效:改一份 ptoas/*.pto 再跑一次,splice 自动发生。 加 --no-rebuild-from-pto 可跳过。

生成过程是 best-effort —— 没有干净 orchestration 入口的程序 会静默跳过这一步,编译流程本身不受影响。

设置环境变量 PYPTO_EMIT_DEBUG_RUNNER=0(也接受 false / no, 大小写不敏感)可全局关闭。适合大型测试套件或 benchmark 流水线 (编译量大、不需要 runner)。关闭后底层的 pypto.runtime.debug.replay 模块 / CLI 仍可直接对 output 目录使用。

对重放的单芯片构建做 benchmark

重放是目录驱动的,但 benchmark() 需要一个活的 CompiledProgram 来拿 orchestration 参数元数据——这份元数据由 IR Program 派生,而目录里没有 IR。所以 ir.compile() 会在 kernel_config.py 旁边额外 写一个 compiled_meta.json(参数元数据 + platform + backend), CompiledProgram.from_dir() 只凭它就能重建出完全可调用的程序—— 不重新编译 pypto、不重跑 pass

from_dir() 只重载元数据,不重建源码。replay 不同,它既不做 .pto → cpp 拼接,也不失效二进制缓存,所以只调它可能让改动被静默忽略: 改了 .pto 不会传到 cpp,改了 cpp 也可能仍被缓存的 .o / .so 顶替。 按 replay 内部的做法显式补上这两步:

from pypto.ir import CompiledProgram
from pypto.runtime import benchmark
from pypto.runtime.debug import invalidate_binary_cache, rebuild_kernel_cpp_from_pto

work_dir = "build_output/<jit_dir>/"
rebuild_kernel_cpp_from_pto(work_dir)  # 仅在改了 ptoas/*.pto 时需要
invalidate_binary_cache(work_dir)      # 丢弃缓存的 .o/.so,让改动真正被编译

compiled = CompiledProgram.from_dir(work_dir, platform="a2a3")
compiled(a, b, c)                                    # 正确性复检
stats = benchmark(compiled, [a, b, c], rounds=100)   # 以及计时

platform / backend_type 默认取编译时记录的值,可覆盖以在别的目标上重放 (例如 a2a3sima2a3)。运行时产物照常从 kernel_config.py 重新派生; 重载既不重写 sidecar,也不覆盖手改过的 debug/run.py。重建出的对象 programNone(IR 未持久化),validate_ir() 仍可从 passes_dump/ 工作。 multi-orch 的父目录自身没有 sidecar——每个 next_levels/<name>/ 子构建各有一份, 重载你要的那个子构建即可。分布式构建用 DistributedCompiledProgram.from_dir(见下一节)。

编译到复用的 output_dir 时,sidecar 始终描述本次编译的程序:写入是原子的; 若新程序在该目录下没有可记录的签名(multi-orch 父目录、无法提取签名的 orchestration、IR 中没有同名函数的子构建),则直接删除旧文件。 ir.compile() 本身不会清空 output_dir,正是这一步保证 from_dir() 不会 交出过期的参数 ABI。

一次构建究竟是哪种布局——顶层单个程序,还是每个 orchestration 一个子构建—— 由本次 codegen 决定(pto_backend.multi_chip_orch_names),而不是扫描目录得出。 因此,上一次 multi-orch 编译遗留的 next_levels/ 不会让随后编译到同一目录的 single-orch 构建被误判成 multi-orch:新的顶层产物照常可以通过 compiled(...)from_dir() 访问。遗留的子构建不会被单芯片 codegen 改动,各自的产物与 sidecar 仍然成对匹配(只是整体过期), CompiledProgram.from_dir(next_levels/<name>) 依旧能重放那次旧构建。

判型标记不会跨构建类型存活。 一个目录是 L2 还是 L3,由少数几个文件决定: 单芯片看顶层 kernel_config.pycompiled_meta.json,分布式看 orchestration/host_orch.pydistributed_meta.json;而 replay() 恰恰是在 「顶层没有 kernel_config.py」时才走 L3 路径。因此另一种类型遗留下来的标记不只是 「过期」,它会把整个目录重新指向那次旧构建(旧 L2 的 kernel_config.py 会让新的 L3 构建按 L2 重放;旧 L3 的 sidecar 会让 DistributedCompiledProgram.from_dir 在已是 L2 产物的目录上仍能加载)。所以每次新编译都会删除本类型不写的那些标记, 复用目录要么解析到刚编译的这次构建,要么显式报错。产物目录本身从不删除,只删这些标记。

重放 L3 / 分布式构建

分布式(L3)程序——即 @pl.jit.host orchestrator 编译出的 DistributedCompiledProgram——支持同样的「改 .pto 再重跑」循环, 但它的 build 目录形态不同:没有顶层 kernel_config.py(每个 rank 的配置在 next_levels/{rank}/ 下),host 驱动是 orchestration/host_orch.py,并且 ir.compile() 会额外写一个 distributed_meta.json

build_output/<jit_dir>/
  distributed_meta.json          # 参数元数据 + platform + DistributedConfig
  orchestration/host_orch.py     # L3 host 驱动
  next_levels/{rank}/            # 每个 rank 一个完整的单芯片子构建
      kernels/{aic,aiv}/*.cpp
      ptoas/*.pto
      kernel_config.py

replay 会自动识别这种布局(无顶层 kernel_config.py 但存在 orchestration/host_orch.py),并改用 simpler Worker(level=3) 派发, 而不是单芯片路径。同样的 CLI / debug/run.py 流程无需改动:

python -m pypto.runtime.debug.replay build_output/<jit_dir>/
# 或
python build_output/<jit_dir>/debug/run.py

.pto → cpp 拼接和 .so 失效都会递归进每个 next_levels/{rank}/, 所以改 next_levels/rank0/ptoas/<unit>.pto(或直接改 kernel cpp)会 被识别,行为与单芯片完全一致。

重建方式与上面单芯片一节相同,只凭 distributed_meta.json。 既可以一次性调用,也可以留作可复用对象:

from pypto.ir import DistributedCompiledProgram, DistributedConfig

# 一次性(对标单芯片的 CompiledProgram.from_dir):
DistributedCompiledProgram.from_dir("build_output/<jit_dir>/")(a, b, c)

# 可复用对象(需要时覆盖持久化的 platform / 设备):
prog = DistributedCompiledProgram.from_dir(
    "build_output/<jit_dir>/",
    platform="a2a3",
    distributed_config=DistributedConfig(device_ids=[0, 1]),
)
prog(a, b, c)

这里持久化的参数元数据是 HOST orchestrator 的(与 host_orch.py 匹配的 post-SSA 名字),chip callables 通过遍历 next_levels/ 重建; distributed_configplatform 一样可覆盖。

distributed_meta.json 与单芯片 sidecar 共用同一套加载逻辑,因而契约一致: 原子写入;当编译进该目录的程序没有可记录的签名时直接删除;加载时逐字段校验 (含 distributed_config 块)。所以手改或被截断的 sidecar 会以一个 ValueError 失败,并指明文件名与重新编译的修复方式。

L3 replay 会把 RunConfig 中的运行时 DFX 字段透传到每个芯片派发,产物写入 dfx_outputs/rank{r}/d{k}/;onboard 泳道使用 运行时 DFX 开关 里描述的抓图/计时两趟协议。 因此「改完再跑」既支持正确性复检,也支持 L3 运行时诊断。