Merged
feat: 新增 PyPTO 算子 GE 入图样例 #4797
lfz2812Add on Sep 8
feat: 新增 PyPTO 算子 GE 入图样例 #4797
Merged
lfz2812Add on Sep 8
Total: 18 files changed+1616-139
@@ -14,6 +14,7 @@
14| `data_dependent_shape_custom` | 数据依赖 shape 算子 | GE | Ascend C | CMake编译 | 不涉及 | [README](data_dependent_shape_custom/README.md) |14| `data_dependent_shape_custom` | 数据依赖 shape 算子 | GE | Ascend C | CMake编译 | 不涉及 | [README](data_dependent_shape_custom/README.md) |
15| `args_refresh_add_custom` | ArgsUpdater 地址刷新 + MallocReadOnlyDevArgs + 性能对比 | GE 在线执行 | Ascend C | RTC 运行时编译 | 在线地址刷新性能对比 | [README](./args_refresh_add_custom/cpp/README.md) |15| `args_refresh_add_custom` | ArgsUpdater 地址刷新 + MallocReadOnlyDevArgs + 性能对比 | GE 在线执行 | Ascend C | RTC 运行时编译 | 在线地址刷新性能对比 | [README](./args_refresh_add_custom/cpp/README.md) |
16| `annotated_args_refresh_add_custom` | AnnotatedArgsOp 声明式地址刷新在线场景性能对比+离线场景 | GE 在线执行 + ATC 离线编译 | Ascend C | RTC 运行时编译 | 支持在线性能对比和 OM 模型下沉 | [README](./annotated_args_refresh_add_custom/README.md) |16| `annotated_args_refresh_add_custom` | AnnotatedArgsOp 声明式地址刷新在线场景性能对比+离线场景 | GE 在线执行 + ATC 离线编译 | Ascend C | RTC 运行时编译 | 支持在线性能对比和 OM 模型下沉 | [README](./annotated_args_refresh_add_custom/README.md) |
17+| `pypto_add_custom` | PyPTO 算子通过 Python 自定义算子入图执行 | GE 在线执行(RT2 动态执行) | PyPTO | JIT 主线程预热编译 + 进程内缓存 | 不涉及 | [README](./pypto_add_custom/README.md) |
17| `tilelang_add_custom` | TileLang 算子通过 GE 入图 | GE 原生 (Session API) | TileLang | TileLang 预编译产出 `.so` | 不涉及 | [README](./tilelang_add_custom/README.md) |18| `tilelang_add_custom` | TileLang 算子通过 GE 入图 | GE 原生 (Session API) | TileLang | TileLang 预编译产出 `.so` | 不涉及 | [README](./tilelang_add_custom/README.md) |
18| `tilelang_add_custom_online` | TileLang 算子在线编译 + 在线执行 | GE 原生 (Session API) | TileLang | GE 编译阶段 `CompilableOp::Compile` subprocess 调用 Python 编译器 | 不涉及 | [README](./tilelang_add_custom_online/README.md) |19| `tilelang_add_custom_online` | TileLang 算子在线编译 + 在线执行 | GE 原生 (Session API) | TileLang | GE 编译阶段 `CompilableOp::Compile` subprocess 调用 Python 编译器 | 不涉及 | [README](./tilelang_add_custom_online/README.md) |
19| `tilelang_add_custom_offline` | TileLang 算子离线 OM 模型下沉 | GE 原生 (`aclgrphBuildModel`) | TileLang | `CompilableOp::Compile` + `PortableOp::Serialize` 序列化到 OM | 支持 OM 模型下沉 | [README](./tilelang_add_custom_offline/README.md) |20| `tilelang_add_custom_offline` | TileLang 算子离线 OM 模型下沉 | GE 原生 (`aclgrphBuildModel`) | TileLang | `CompilableOp::Compile` + `PortableOp::Serialize` 序列化到 OM | 支持 OM 模型下沉 | [README](./tilelang_add_custom_offline/README.md) |
@@ -14,6 +14,7 @@ This directory provides samples related to custom operator graph integration, co
14| `data_dependent_shape_custom` | Data dependent shape operator | GE | Ascend C | CMake compilation | Not involved | [README](data_dependent_shape_custom/README_en.md) |14| `data_dependent_shape_custom` | Data dependent shape operator | GE | Ascend C | CMake compilation | Not involved | [README](data_dependent_shape_custom/README_en.md) |
15| `args_refresh_add_custom` | ArgsUpdater address refresh + MallocReadOnlyDevArgs + performance comparison | GE online execution | Ascend C | RTC runtime compilation | Online address refresh performance comparison | [README](./args_refresh_add_custom/cpp/README_en.md) |15| `args_refresh_add_custom` | ArgsUpdater address refresh + MallocReadOnlyDevArgs + performance comparison | GE online execution | Ascend C | RTC runtime compilation | Online address refresh performance comparison | [README](./args_refresh_add_custom/cpp/README_en.md) |
16| `annotated_args_refresh_add_custom` | AnnotatedArgsOp declarative address refresh for online performance comparison and offline model | GE online execution + ATC offline compilation | Ascend C | RTC runtime compilation | Supports online performance comparison and OM model sink | [README](./annotated_args_refresh_add_custom/README_en.md) |16| `annotated_args_refresh_add_custom` | AnnotatedArgsOp declarative address refresh for online performance comparison and offline model | GE online execution + ATC offline compilation | Ascend C | RTC runtime compilation | Supports online performance comparison and OM model sink | [README](./annotated_args_refresh_add_custom/README_en.md) |
17+| `pypto_add_custom` | PyPTO operator enters graph through Python custom operator | GE online execution (RT2 dynamic execution) | PyPTO | JIT warm-up compiled on main thread + in-process cache | Not involved | [README](./pypto_add_custom/README_en.md) |
17| `tilelang_add_custom` | TileLang operator enters graph through GE | GE native (Session API) | TileLang | TileLang pre-compiled `.so` | Not involved | [README](./tilelang_add_custom/README_en.md) |18| `tilelang_add_custom` | TileLang operator enters graph through GE | GE native (Session API) | TileLang | TileLang pre-compiled `.so` | Not involved | [README](./tilelang_add_custom/README_en.md) |
18| `tilelang_add_custom_online` | TileLang operator online compilation + online execution | GE native (Session API) | TileLang | `CompilableOp::Compile` subprocess invokes Python compiler during GE compile phase | Not involved | [README](./tilelang_add_custom_online/README_en.md) |19| `tilelang_add_custom_online` | TileLang operator online compilation + online execution | GE native (Session API) | TileLang | `CompilableOp::Compile` subprocess invokes Python compiler during GE compile phase | Not involved | [README](./tilelang_add_custom_online/README_en.md) |
19| `tilelang_add_custom_offline` | TileLang operator offline OM model sinking | GE native (`aclgrphBuildModel`) | TileLang | `CompilableOp::Compile` + `PortableOp::Serialize` to OM | Supports OM model sinking | [README](./tilelang_add_custom_offline/README_en.md) |20| `tilelang_add_custom_offline` | TileLang operator offline OM model sinking | GE native (`aclgrphBuildModel`) | TileLang | `CompilableOp::Compile` + `PortableOp::Serialize` to OM | Supports OM model sinking | [README](./tilelang_add_custom_offline/README_en.md) |
@@ -26,6 +26,7 @@ Compile 回调 RTC 编译 Ascend C kernel
26## 前置依赖26## 前置依赖
27 27 
28- 已安装并配置 CANN,例如执行过 `source /usr/local/Ascend/cann/set_env.sh`(路径按实际安装位置调整)。28- 已安装并配置 CANN,例如执行过 `source /usr/local/Ascend/cann/set_env.sh`(路径按实际安装位置调整)。
29+- 参考 [安装指导](../../../../../docs/zh/quick_install.md) 完成 toolkit 和 ops 包安装。
29- CANN 头文件和库需要包含 `AnnotatedArgsOp`、`AnnotatedKernelLaunchInfo`、`AnnotatedKernelArgs` 等声明式地址刷新接口。30- CANN 头文件和库需要包含 `AnnotatedArgsOp`、`AnnotatedKernelLaunchInfo`、`AnnotatedKernelArgs` 等声明式地址刷新接口。
30- `atc` 可用。31- `atc` 可用。
31- 当前环境具备 `ACL`、`GE`、`Graph` 相关头文件与库。32- 当前环境具备 `ACL`、`GE`、`Graph` 相关头文件与库。
@@ -26,6 +26,7 @@ Key implementation points:
26## Prerequisites26## Prerequisites
27 27 
28- CANN has been installed and configured, for example by running `source /usr/local/Ascend/cann/set_env.sh` (adjust the path for your installation).28- CANN has been installed and configured, for example by running `source /usr/local/Ascend/cann/set_env.sh` (adjust the path for your installation).
29+- Refer to the [Installation Guide](../../../../../docs/en/quick_install.md) to install the toolkit and ops packages.
29- The CANN headers and libraries must provide the declarative address refresh APIs, including `AnnotatedArgsOp`, `AnnotatedKernelLaunchInfo`, and `AnnotatedKernelArgs`.30- The CANN headers and libraries must provide the declarative address refresh APIs, including `AnnotatedArgsOp`, `AnnotatedKernelLaunchInfo`, and `AnnotatedKernelArgs`.
30- `atc` is available.31- `atc` is available.
31- The environment provides ACL, GE, and Graph headers and libraries.32- The environment provides ACL, GE, and Graph headers and libraries.
@@ -1,47 +1,129 @@
1-# Python 离线自定义算子样例1+# Python 声明式参数地址刷新 Add 自定义算子离线样例
2 2 
3-本样例演示使用 Python 装饰器完成 `AnnotatedAddCustom` 的原型注册、`infer_meta`、编译和 `declare_launch_args`,再生成 AIR/OM 并通过 ACL 在 NPU 上执行。`declare_launch_args` 只在 ATC 编译期运行,离线运行期只消费已经生成的任务描述。Python 回调复用 Ascend C kernel 源码并生成 device binary。3+## 样例概述
4 4 
5-## 目录5+本离线样例展示使用 Python 装饰器完成自定义算子原型注册、`infer_meta`、`compile` 和
6+`declare_launch_args`,生成 AIR/OM 并由 ACL 在 NPU 上执行的最小链路。`declare_launch_args`
7+只在 ATC 编译期运行,离线运行期只消费已生成的任务描述,不再导入 Python 模块。
8+ 
9+核心链路:
6 10 
7```text11```text
8-offline/python12+Python 装饰器注册原型和 infer_meta
9-├── CMakeLists.txt # 构建 custom OPP 注册库和 ES wheel13+ -> build_graph.py 生成 AIR
10-├── run.sh # kernel、ES、AIR、ATC 和 ACL 验证入口14+ -> ATC 导入 Python 模块执行 compile 回调(bisheng 编译 Ascend C kernel)
11-├── proto/ # gen_esb 使用的 C++ 构图原型15+ -> DeclareLaunchArgs 声明 kernel/bin/block_dim 和地址槽位
12-└── src/16+ -> ATC 生成 TaskDef.kernel().args 和 context.args_format
13- ├── build_graph.py # Python 构图并生成 AIR17+ -> OM 加载期 GE 按 TaskDef 下发 custom kernel
14- ├── run_model.py # ACL 两轮离线 NPU 执行18+ -> 执行前刷新 INPUT/OUTPUT 地址槽位
15- └── ge/annotated_add_custom.py # Python 原型和 infer_meta
16```19```
17 20 
18-## 依赖与运行21+主要实现点:
19 22 
20-需要已安装并配置的 CANN、ATC、CMake、Python 3/pip、numpy 和可用 NPU。23+- 本样例的算子类型是 `AnnotatedAddCustom`,原型、`infer_meta`、`compile` 和
24+ `declare_launch_args` 均由 `src/ge/annotated_add_custom.py` 的 Python 装饰器提供。
25+- 离线模型路径不依赖 Python `execute` 回调;OM 运行期无需 Python 环境。
26+- `declare_launch_args` 使用 `append_input`/`append_output` 标注两个输入地址槽和一个输出地址槽,
27+ GE 根据 args 布局完成地址刷新。
28+- kernel binary 由 Python `compile` 回调调用 `bisheng` 编译 `add_custom_kernel.cpp` 生成,
29+ 并随地址布局一起写入 OM。
21 30 
22-`proto/add_custom.h` 中的 C++ `REG_OP` 仅用于 `gen_esb` 生成 `ge.es.custom.AnnotatedAddCustom` 构图接口;运行时原型、`infer_meta`、`compile` 和 `declare_launch_args` 均由 `src/ge/annotated_add_custom.py` 的 Python 装饰器提供。31+## 前置依赖
32+ 
33+- 已正确安装并配置 CANN 环境,例如执行过 `source ${ASCEND_HOME_PATH}/set_env.sh`。
34+- 当前环境具备 `ACL`、`GE`、`Graph` 相关头文件与库。
35+- 参考 [安装指导](../../../../../docs/zh/quick_install.md) 完成 toolkit 和 ops 包安装。
36+- 需要 ATC、CMake、Python 3/pip 和可用 NPU。
37+- `bisheng`、`llvm-objcopy`(CANN 工具链自带,由 Python `compile` 回调调用)。
38+ 
39+## 快速运行
40+ 
41+在 `examples/custom_op/annotated_args_refresh_add_custom/offline/python` 目录执行:
23 42 
24```bash43```bash
25-source /path/to/cann/set_env.sh
26-cd examples/custom_op/annotated_args_refresh_add_custom/offline/python
27bash run.sh44bash run.sh
28```45```
29 46 
47+脚本会完成以下步骤:
48+ 
49+1. 构建 custom OPP 注册库和 Python ES wheel 并安装。
50+2. 运行 `build_graph.py` 生成 `build/annotated_add.air`。
51+3. 调用 `atc` 导入 Python 模块,执行 `infer_meta`、`compile` 和 `declare_launch_args`,
52+ 生成 `build/annotated_add.om`。
53+4. 运行 `run_model.py` 通过 ACL 加载 OM,分两轮独立数据集执行。
54+ 
55+运行成功时,终端应打印:
56+ 
57+```text
58+ROUND_1_FIRST=3
59+ROUND_2_FIRST=9
60+NPU_TWO_ROUND_VALIDATION=PASS
61+```
62+ 
30可覆盖 `SOC_VERSION`(默认 `Ascend910B1`)和 `DEVICE_ID`(默认 `0`)。63可覆盖 `SOC_VERSION`(默认 `Ascend910B1`)和 `DEVICE_ID`(默认 `0`)。
31 64 
32-## 执行链路65+## 关键文件
33 66 
34-1. 构建 custom OPP 和 ES wheel,`build_graph.py` 生成 AIR。67+```text
35-2. ATC 导入 Python 模块执行 `infer_meta`、`compile` 和 `declare_launch_args`,将 binary 和地址布局写入68+annotated_args_refresh_add_custom
36- `build/annotated_add.om`。69+└── offline/python
37-3. `run_model.py` 使用 ACL 加载 OM,分两轮创建独立数据集并执行,验证 AnnotatedArgs 地址刷新。70+ ├── CMakeLists.txt # 构建 custom OPP 注册库和 ES wheel
71+ ├── README.md
72+ ├── README_en.md
73+ ├── run.sh # ES、AIR、ATC 和 ACL 验证入口
74+ ├── proto
75+ │ └── add_custom.h # gen_esb 使用的 C++ 构图原型
76+ └── src
77+ ├── build_graph.py # Python 构图并生成 AIR
78+ ├── run_model.py # ACL 两轮离线 NPU 执行
79+ └── ge
80+ └── annotated_add_custom.py # Python 原型、infer_meta 和编译回调
81+```
38 82 
39-运行日志中的 `NPU_TWO_ROUND_VALIDATION=PASS` 表示两轮输出均通过校验。ATC 日志应包含 Python 模块加载、83+- `proto/add_custom.h` 中的 C++ `REG_OP` 仅用于 `gen_esb` 生成 `ge.es.custom.AnnotatedAddCustom`
40-`infer_meta`、Python compile 和地址声明日志;OM 运行期不应再次导入 Python 模块。84+ 构图接口。
85+- `src/ge/annotated_add_custom.py` 提供运行时原型、`infer_meta`、`compile` 和
86+ `declare_launch_args`。
87+- `src/build_graph.py` 构建 `AnnotatedAddCustom` 图并导出 AIR。
88+- `src/run_model.py` 加载 OM 并分两轮校验 AnnotatedArgs 地址刷新。
41 89 
42-Add kernel 使用 Ascend C 编写,源码位于 `offline/cpp/ge/add_custom_kernel.cpp`。开发者可参考 CANN90+## 声明式地址刷新实现点
43-Ascend C 文档编写 kernel,Python `compile` 回调负责生成并交付 binary。
44 91 
45-## callback 约束92+```text
93+ATC 编译期:
94+ infer_meta / compile / declare_launch_args
95+ ├─ compile: bisheng 编译 add_custom_kernel.cpp 并提取 .aicore_binary
96+ ├─ KernelArgs: InputAddr{0}, InputAddr{1}, OutputAddr{0}
97+ ├─ AnnotatedKernelLaunchInfo { kernel_name, kernel_bin, block_dim }
98+ └─ binary 和地址布局写入 annotated_add.om
46 99 
47-`append_input` 和 `append_output` 使用当前节点 input/output 的平铺 index。`AnnotatedArgsContext`、Tensor、workspace 和 args builder 都是 callback 期间的 borrowed 对象,不能逃逸;args builder 在 `add_launch` 后已 consumed,不能复用。100+OM 执行期:
101+ ACL 加载 OM
102+ ├─ GE 按 TaskDef 下发 custom kernel
103+ └─ 执行前刷新 INPUT/OUTPUT 地址槽位
104+ (运行期不导入 Python 模块)
105+```
106+ 
107+callback 约束:
108+ 
109+- `append_input` 和 `append_output` 使用当前节点 input/output 的平铺 index。
110+- `AnnotatedArgsContext`、Tensor、workspace 和 args builder 都是 callback 期间的 borrowed 对象,
111+ 不能逃逸;args builder 在 `add_launch` 后已 consumed,不能复用。
112+ 
113+## 构建产物
114+ 
115+- `build/annotated_add.air`
116+ `build_graph.py` 生成的 AIR 中间图。
117+- `build/annotated_add.om`
118+ ATC 编译生成的离线模型,内嵌 kernel binary 和 AnnotatedArgs 地址布局,运行期无需 Python 环境。
119+- `build/opp/`
120+ 自定义算子 OPP 注册库(`op_proto/custom/libcust_opapi.so` 及平台拷贝),通过 `ASCEND_CUSTOM_OPP_PATH` 暴露给 GE/ATC。
121+- `build/es_output/`
122+ ES 构图接口交付件:`lib64/libes_custom.so` 和 `whl/es_custom-1.0.0-py3-none-any.whl`(已安装到 `build/whl_package/`)。
123+ 
124+## 注意事项
125+ 
126+- ATC 日志应包含 Python 模块加载、`infer_meta`、Python compile 和地址声明标记;OM 运行期
127+ 不应再次出现这些标记,`run.sh` 会自动校验。
128+- `run.sh` 在 ATC 阶段后 unset `ASCEND_CUSTOM_OPP_PATH`,验证 OM 运行不依赖 Python 插件路径。
129+- OM 内嵌 kernel binary 和地址布局,可脱离本样例目录分发执行。
@@ -1,55 +1,135 @@
1-# Python Offline Custom Operator1+# Python Declarative Address Refresh Add Custom Operator Offline Sample
2 2 
3-This sample uses Python decorators to register the `AnnotatedAddCustom` prototype, `infer_meta`, `compile`, and3+## Sample Overview
4-`declare_launch_args`. It generates AIR/OM and runs the model on an NPU through ACL. The declaration callback runs
5-only during ATC compilation; offline execution consumes the generated task description. The Python callback compiles
6-and serializes the Ascend C kernel binary.
7 4 
8-## Directory5+This offline sample demonstrates the minimal chain of registering a custom operator prototype with Python
6+decorators, running `infer_meta`, `compile`, and `declare_launch_args`, generating AIR/OM, and executing
7+the model on an NPU through ACL. `declare_launch_args` runs only during ATC compilation; offline execution
8+consumes the generated task description and never imports the Python module again.
9+ 
10+Core path:
9 11 
10```text12```text
11-offline/python13+Register prototype and infer_meta with Python decorators
12-├── CMakeLists.txt # Build the custom OPP library and ES wheel14+ -> build_graph.py generates AIR
13-├── run.sh # Kernel, ES, AIR, ATC, and ACL validation entry point15+ -> ATC imports the Python module and invokes the compile callback (bisheng compiles the Ascend C kernel)
14-├── proto/ # C++ graph prototype consumed by gen_esb16+ -> DeclareLaunchArgs declares kernel/bin/block_dim and address slots
15-└── src/17+ -> ATC generates TaskDef.kernel().args and context.args_format
16- ├── build_graph.py # Build the Python graph and save AIR18+ -> GE dispatches the custom kernel per TaskDef during OM loading
17- ├── run_model.py # Two-round ACL execution on an NPU19+ -> INPUT/OUTPUT address slots are refreshed before execution
18- └── ge/annotated_add_custom.py # Python prototype and infer_meta
19```20```
20 21 
21-## Requirements and usage22+Main implementation points:
22 23 
23-Install and configure CANN, ATC, CMake, Python 3/pip, numpy, and an available NPU.24+- The operator type is `AnnotatedAddCustom`. The prototype, `infer_meta`, `compile`, and
25+ `declare_launch_args` are all supplied by the Python decorators in `src/ge/annotated_add_custom.py`.
26+- The offline model path does not depend on the Python `execute` callback; no Python environment is
27+ required at OM runtime.
28+- `declare_launch_args` uses `append_input`/`append_output` to annotate two input address slots and one
29+ output address slot; GE refreshes addresses according to the args layout.
30+- The kernel binary is produced by the Python `compile` callback invoking `bisheng` on
31+ `add_custom_kernel.cpp` and is written into the OM together with the address layout.
24 32 
25-The C++ `REG_OP` in `proto/add_custom.h` is used only by `gen_esb` to generate the33+## Prerequisites
26-`ge.es.custom.AnnotatedAddCustom` graph-building interface. The runtime prototype, `infer_meta`, `compile`, and34+ 
27-`declare_launch_args` are supplied by the Python decorators in `src/ge/annotated_add_custom.py`.35+- The CANN environment is installed and configured, for example by running `source ${ASCEND_HOME_PATH}/set_env.sh`.
36+- The environment provides the required `ACL`, `GE`, and `Graph` headers and libraries.
37+- Refer to the [Installation Guide](../../../../../docs/en/quick_install.md) to install the toolkit and ops packages.
38+- ATC, CMake, Python 3/pip, and an available NPU are required.
39+- `bisheng` and `llvm-objcopy` (shipped with the CANN toolchain, invoked by the Python `compile` callback).
40+ 
41+## Quick Run
42+ 
43+Run the following command in the `examples/custom_op/annotated_args_refresh_add_custom/offline/python` directory:
28 44 
29```bash45```bash
30-source /path/to/cann/set_env.sh
31-cd examples/custom_op/annotated_args_refresh_add_custom/offline/python
32bash run.sh46bash run.sh
33```47```
34 48 
49+The script performs the following steps:
50+ 
51+1. Builds the custom OPP registration library and the Python ES wheel, then installs it.
52+2. Runs `build_graph.py` to generate `build/annotated_add.air`.
53+3. Invokes `atc`, which imports the Python module, executes `infer_meta`, `compile`, and
54+ `declare_launch_args`, and generates `build/annotated_add.om`.
55+4. Runs `run_model.py` to load the OM through ACL and execute two rounds with independent datasets.
56+ 
57+On success the terminal prints:
58+ 
59+```text
60+ROUND_1_FIRST=3
61+ROUND_2_FIRST=9
62+NPU_TWO_ROUND_VALIDATION=PASS
63+```
64+ 
35The script accepts `SOC_VERSION` (default `Ascend910B1`) and `DEVICE_ID` (default `0`).65The script accepts `SOC_VERSION` (default `Ascend910B1`) and `DEVICE_ID` (default `0`).
36 66 
37-## Execution flow67+## Key Files
38 68 
39-1. Build the custom OPP and ES wheel; `build_graph.py` generates AIR.69+```text
40-2. ATC imports the Python module and invokes `infer_meta`, `compile`, and `declare_launch_args` to write the Ascend C70+annotated_args_refresh_add_custom
41- binary and address layout into `build/annotated_add.om`.71+└── offline/python
42-3. `run_model.py` loads the OM with ACL, creates independent datasets for two rounds, and validates address refresh.72+ ├── CMakeLists.txt # Build the custom OPP library and ES wheel
73+ ├── README.md
74+ ├── README_en.md
75+ ├── run.sh # ES, AIR, ATC, and ACL validation entry point
76+ ├── proto
77+ │ └── add_custom.h # C++ graph prototype consumed by gen_esb
78+ └── src
79+ ├── build_graph.py # Build the Python graph and save AIR
80+ ├── run_model.py # Two-round ACL execution on an NPU
81+ └── ge
82+ └── annotated_add_custom.py # Python prototype, infer_meta, and compile callback
83+```
43 84 
44-`NPU_TWO_ROUND_VALIDATION=PASS` in the runtime log indicates that both rounds passed. The ATC log should contain85+- The C++ `REG_OP` in `proto/add_custom.h` is used only by `gen_esb` to generate the
45-the Python module-load, `infer_meta`, compile, and address-declaration markers. These86+ `ge.es.custom.AnnotatedAddCustom` graph-building interface.
46-compile-time markers must not appear during OM execution.87+- `src/ge/annotated_add_custom.py` supplies the runtime prototype, `infer_meta`, `compile`, and
88+ `declare_launch_args`.
89+- `src/build_graph.py` builds the `AnnotatedAddCustom` graph and exports AIR.
90+- `src/run_model.py` loads the OM and validates AnnotatedArgs address refresh over two rounds.
47 91 
48-The Add kernel is written with Ascend C in `offline/cpp/ge/add_custom_kernel.cpp`. Use the CANN Ascend C92+## Declarative Address Refresh Implementation
49-documentation when developing kernels and ACL RTC when delivering their binaries.
50 93 
51-## Callback constraints94+```text
95+ATC compile phase:
96+ infer_meta / compile / declare_launch_args
97+ ├─ compile: bisheng compiles add_custom_kernel.cpp and extracts .aicore_binary
98+ ├─ KernelArgs: InputAddr{0}, InputAddr{1}, OutputAddr{0}
99+ ├─ AnnotatedKernelLaunchInfo { kernel_name, kernel_bin, block_dim }
100+ └─ The binary and address layout are written into annotated_add.om
52 101 
53-Use the flattened input/output index of the current node with `append_input` and `append_output`.102+OM execution phase:
54-`AnnotatedArgsContext`, tensors, workspace, and the argument builder are borrowed objects valid only during the103+ ACL loads the OM
55-callback and must not escape. The argument builder is consumed by `add_launch` and cannot be reused.104+ ├─ GE dispatches the custom kernel per TaskDef
105+ └─ INPUT/OUTPUT address slots are refreshed before execution
106+ (The Python module is not imported at runtime)
107+```
108+ 
109+Callback constraints:
110+ 
111+- `append_input` and `append_output` use the flattened input/output index of the current node.
112+- `AnnotatedArgsContext`, tensors, workspace, and the argument builder are borrowed objects valid only
113+ during the callback and must not escape. The argument builder is consumed by `add_launch` and cannot
114+ be reused.
115+ 
116+## Build Artifacts
117+ 
118+- `build/annotated_add.air`
119+ The AIR intermediate graph generated by `build_graph.py`.
120+- `build/annotated_add.om`
121+ The offline model compiled by ATC. It embeds the kernel binary and the AnnotatedArgs address layout, so no Python environment is required at runtime.
122+- `build/opp/`
123+ The custom operator OPP registration library (`op_proto/custom/libcust_opapi.so` plus platform copies), exposed to GE/ATC through `ASCEND_CUSTOM_OPP_PATH`.
124+- `build/es_output/`
125+ ES graph-building deliverables: `lib64/libes_custom.so` and `whl/es_custom-1.0.0-py3-none-any.whl` (installed into `build/whl_package/`).
126+ 
127+## Notes
128+ 
129+- The ATC log should contain the Python module-load, `infer_meta`, compile, and address-declaration
130+ markers. These compile-time markers must not appear during OM execution; `run.sh` verifies this
131+ automatically.
132+- `run.sh` unsets `ASCEND_CUSTOM_OPP_PATH` after the ATC phase to prove that OM execution does not
133+ depend on the Python plugin path.
134+- The OM embeds the kernel binary and address layout and can be distributed for execution outside this
135+ sample directory.
@@ -1,55 +1,241 @@
1-# Python 在线自定义算子性能对比样例1+# Python 声明式参数地址刷新 Add 自定义算子在线样例
2 2 
3-本样例使用 Python 装饰器注册 `AnnotatedAddCustom` 和 `NoRefreshAddCustom` 的原型与 `infer_meta`,3+## 样例概述
4-在两张在线 GE 图中对比声明式地址刷新和不声明地址刷新的执行性能。两条链路使用同一份 Ascend C Add kernel,
5-脚本以两组独立的 device Tensor 交替运行,完成精度校验、预热和计时,不生成 AIR/OM。
6 4 
7-## 目录5+- 构图入口:`GE`(Python ES 构图接口)
6+- 算子编程语言:`Python`(原型与回调)+ `Ascend C`(kernel)
7+- 编译方式:`bisheng` 编译 Ascend C kernel,Python `compile` 回调生成并按 shape 缓存 binary
8+- 核心链路:`Python 原型注册 -> ES 构图 -> Session 在线执行 -> compile/declare_launch_args/execute 回调`
9+- 对比目标:声明式地址刷新 `AnnotatedAddCustom` 与无地址刷新 `NoRefreshAddCustom`
8 10 
9-```text11+本样例定义两个功能相同的 Add 自定义算子。输入 shape 均为 `[8192]` float32,两个算子下发同一个
10-online/python12+`add_custom` Ascend C kernel:
11-├── CMakeLists.txt # 构建 custom OPP 注册库和 ES wheel
12-├── run.sh # 构建并执行在线性能对比
13-├── proto/ # gen_esb 使用的 C++ 构图原型
14-└── src/
15- ├── run.py # 两张在线 GE 图的精度和性能对比
16- └── ge/
17- └── annotated_add_custom.py # Python 原型和 infer_meta
18-```
19 13 
20-## 依赖与运行14+- `AnnotatedAddCustom` 通过 `register_op_impl` 提供 `compile` 和 `declare_launch_args`。`compile`
21- 15+ 调用 `bisheng` 编译 `add_custom.asc` 并按输入 shape/dtype 缓存 binary;`declare_launch_args`
22-需要已安装并配置的 CANN、CMake、Python 3/pip 和可用 NPU。16+ 声明两个输入地址槽和一个输出地址槽,GE 在重复执行时按该布局刷新地址。
17+- `NoRefreshAddCustom` 通过 `register_op_impl` 提供 `execute`。它在每次图执行时申请输出 Tensor,
18+ 通过 ACL Python API 加载 binary 并重新组装 kernel 参数,用作性能对比基线。
23 19 
24`proto/add_custom.h` 中的 C++ `REG_OP` 仅用于 `gen_esb` 生成构图接口;运行时原型和 `infer_meta`20`proto/add_custom.h` 中的 C++ `REG_OP` 仅用于 `gen_esb` 生成构图接口;运行时原型和 `infer_meta`
25-由 Python 装饰器提供;`AnnotatedAddCustom` 的 `compile`、`declare_launch_args` 和 `NoRefreshAddCustom` 的21+由 Python 装饰器提供,算子执行全部由 Python 回调完成,不编译 `online/cpp/ge/custom_op.cpp`。
26-`execute` 均由 Python `register_op_impl` 提供。两个实现复用 `cpp/add_custom_kernel/add_custom.asc` 生成的
27-Ascend C binary。
28 22 
29-本目录不编译 `online/cpp/ge/custom_op.cpp`;该文件仅作为 C++ 对照样例,Python 样例的算子执行全部由 Python 回调完成。23+`run.py` 构建上述两张图。每张图分别使用两套设备内存交替执行,完成预热、100 轮性能统计和精度校验,
24+最后打印两者耗时及 speedup。
25+ 
26+## 适用场景
27+ 
28+- 了解 Python `register_op`/`register_op_impl` 如何声明 kernel task 和 args 地址布局。
29+- 对比声明式地址刷新与无地址刷新在重复在线执行中的性能差异。
30+- 想在 Python 侧完成自定义算子原型、编译回调与执行回调的完整链路验证。
31+ 
32+## 前置依赖
33+ 
34+### CANN
35+ 
36+- 已正确安装并配置 CANN 环境,例如执行过 `source ${ASCEND_HOME_PATH}/set_env.sh`。
37+- 当前环境具备 `ACL`、`GE`、`Graph` 相关头文件与库。
38+- 参考 [安装指导](../../../../../docs/zh/quick_install.md) 完成 toolkit 和 ops 包安装。
39+ 
40+### 框架与插件
41+ 
42+- Python 3 和 `pip`。
43+- ES 构图接口由本样例构建的 `es_custom` wheel 提供,无需额外安装框架插件。
44+ 
45+### 环境变量
46+ 
47+- `ASCEND_HOME_PATH`
48+- `run.sh` 会自动设置 `PYTHONPATH`、`LD_LIBRARY_PATH`、`ASCEND_CUSTOM_OPP_PATH`、
49+ `GE_PYTHON_CUSTOM_OP_SOURCE` 和 `GE_PYTHON_CUSTOM_OP_BINARY`。
50+ 
51+### 额外依赖
52+ 
53+- `cmake`
54+- `bisheng`、`llvm-objcopy`(CANN 工具链自带,用于编译和提取 kernel binary)
55+ 
56+## 快速运行
57+ 
58+在 `examples/custom_op/annotated_args_refresh_add_custom/online/python` 目录下执行:
59+ 
60+### 推荐方式
30 61 
31```bash62```bash
32-source /path/to/cann/set_env.sh63+source ${ASCEND_HOME_PATH}/set_env.sh
33-cd examples/custom_op/annotated_args_refresh_add_custom/online/python
34bash run.sh64bash run.sh
35```65```
36 66 
37-可通过 `DEVICE_ID`(默认 `0`)选择 NPU:67+`run.sh` 会自动完成 kernel 编译、OPP/ES wheel 构建、安装和在线对比:
38 68 
39-```bash69+1. `bisheng` 编译 `add_custom.asc`,`llvm-objcopy` 提取 `.aicore_binary` 生成 kernel binary。
40-DEVICE_ID=1 bash run.sh70+2. 构建 custom OPP 注册库和 Python ES wheel 并安装。
71+3. 运行两张图的在线精度与性能对比。
72+ 
73+若运行成功,终端会打印类似:
74+ 
75+```text
76+[OnlinePython] AnnotatedAddCustom precision check PASS
77+[OnlinePython] NoRefreshAddCustom precision check PASS
78+[Perf] input shape: [8192], dtype: float32
79+[Perf] iters: 100
80+[Perf] AnnotatedAddCustom: xxx us (avg xxx us/iter)
81+[Perf] NoRefreshAddCustom: xxx us (avg xxx us/iter)
82+[Perf] Annotated speedup: xxx x
83+[OnlinePython] NPU_EXECUTION=PASS
41```84```
42 85 
43-## 执行链路86+### 分步方式
44 87 
45-1. 构建 custom OPP 和 ES wheel,生成两个构图接口。88+```bash
46-2. Python `compile` 回调在 GE 编译图时编译并保存 Ascend C Add kernel binary。89+source ${ASCEND_HOME_PATH}/set_env.sh
47-3. Python `declare_launch_args` 回调声明输入、输出地址槽和 kernel launch;GE 在重复执行时刷新地址。90+mkdir -p build
48-4. Python `execute` 为 schema 输出申请 Tensor,并通过 ACL Python API 下发同一 Ascend C kernel,作为无地址声明基线。91+# 1. 编译 Ascend C kernel 并提取 binary
49-5. `run.py` 为两张图交替传入两组 device Tensor,分别完成精度校验、5 次预热和 100 次计时。92+bisheng -c ../cpp/add_custom_kernel/add_custom.asc -o build/add_custom.host.o --npu-arch=dav-2201
93+llvm-objcopy -O binary --only-section=.aicore_binary build/add_custom.host.o build/add_custom.aicore.o
94+# 2. 构建 custom OPP 和 ES wheel
95+cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
96+cmake --build build --target build_es_custom -j"$(nproc)"
97+# 3. 安装 ES wheel
98+python3 -m pip install --target build/whl_package build/es_output/whl/es_custom-1.0.0-py3-none-any.whl
99+# 4. 设置环境变量并运行对比
100+export PYTHONPATH="$(pwd)/build/whl_package:$(pwd)/src:$PYTHONPATH"
101+export LD_LIBRARY_PATH="$(pwd)/build/es_output/lib64:$LD_LIBRARY_PATH"
102+export ASCEND_CUSTOM_OPP_PATH="$(pwd)/build/opp:$(pwd)/src/ge"
103+export GE_PYTHON_CUSTOM_OP_SOURCE="$(pwd)/../cpp/add_custom_kernel/add_custom.asc"
104+export GE_PYTHON_CUSTOM_OP_BINARY="$(pwd)/build/add_custom.aicore.o"
105+DEVICE_ID=0 python3 src/run.py
106+```
50 107 
51-日志会输出 `AnnotatedAddCustom`、`NoRefreshAddCustom` 的总耗时、平均耗时和 `Annotated speedup`;108+NPU 架构默认 `dav-2201`,可通过 `ADD_CUSTOM_NPU_ARCH` 覆盖;`DEVICE_ID` 默认 `0`。
52-`NPU_EXECUTION=PASS` 表示两条链路均通过精度和执行校验。
53 109 
54-Add kernel 使用 Ascend C 编写,源码位于 `online/cpp/add_custom_kernel/add_custom.asc`。开发者可参考110+## 目录结构与关键文件
55-CANN Ascend C 文档编写并通过 ACL RTC 编译自定义 kernel。111+ 
112+```text
113+annotated_args_refresh_add_custom
114+└── online/python
115+ ├── CMakeLists.txt # 构建 custom OPP 注册库和 ES wheel
116+ ├── README.md
117+ ├── README_en.md
118+ ├── run.sh # 构建并执行在线性能对比
119+ ├── proto
120+ │ └── add_custom.h # gen_esb 使用的 C++ 构图原型
121+ └── src
122+ ├── run.py # 两张在线 GE 图的精度和性能对比
123+ └── ge
124+ └── annotated_add_custom.py # Python 原型、infer_meta 和算子实现
125+```
126+ 
127+重点文件:
128+ 
129+- `src/ge/annotated_add_custom.py`
130+ 实现 `AnnotatedAddCustom` 和 `NoRefreshAddCustom`。前者在编译回调生成 binary,并在 task 生成
131+ 阶段声明地址槽位;后者使用 eager 执行链路直接下发同一 kernel。
132+- `proto/add_custom.h`
133+ 注册 `AnnotatedAddCustom` 和 `NoRefreshAddCustom` 两个构图侧算子类型,供 `gen_esb` 生成
134+ Python ES 构图接口。
135+- `src/run.py`
136+ 构建两张图,交替两套输入/输出设备地址,分别预热 5 次、统计 100 轮并校验精度。
137+- `CMakeLists.txt`
138+ 构建 custom OPP 注册库(`libcust_opapi.so`)和 Python ES wheel。
139+ 
140+## 核心链路
141+ 
142+### 在线执行
143+ 
144+1. `run.py` 构建 `python_annotated_graph` 和 `python_no_refresh_graph`,输入均为 `[8192]` float32。
145+2. `AnnotatedAddCustom` 的 `compile` 回调读取 `add_custom.asc`,调用 `bisheng` 编译并提取
146+ `.aicore_binary`,按输入 shape/dtype 缓存 binary。
147+3. `AnnotatedAddCustom` 的 `declare_launch_args` 回调设置 kernel 名称、binary、block dim,
148+ 并声明 `InputAddr{0}`、`InputAddr{1}` 和 `OutputAddr{0}`。
149+4. `NoRefreshAddCustom` 的 `execute` 回调申请输出 Tensor,通过 ACL Python API 加载 binary、
150+ 组装 args 并下发 kernel。
151+5. 两张图分别交替使用两套设备地址执行,输出总耗时、平均耗时和 speedup。
152+ 
153+### 声明式地址刷新
154+ 
155+```text
156+编译期:
157+ compile(x, y, z)
158+ ├─ 读取 add_custom.asc
159+ ├─ bisheng 编译 + llvm-objcopy 提取 .aicore_binary -> kernel binary
160+ └─ 按输入 shape/dtype 缓存 binary
161+ 
162+Task 生成期:
163+ declare_launch_args(x, y, z)
164+ ├─ KernelArgs: InputAddr{0}, InputAddr{1}, OutputAddr{0}
165+ ├─ AnnotatedKernelLaunchInfo { kernel_name, kernel_bin, block_dim, stream_id }
166+ └─ ctx.add_launch(launch_info, args)
167+ 
168+执行期:
169+ GE 根据保存的 args 布局刷新当前输入/输出地址
170+```
171+ 
172+### 无地址刷新基线
173+ 
174+```text
175+每次图执行:
176+ NoRefreshAddCustom.execute(x, y)
177+ ├─ ctx.malloc_output_tensor()
178+ ├─ acl.rt.binary_load_from_file() / binary_get_function()
179+ ├─ kernel_args_init / append(x, y, z) / finalize
180+ └─ acl.rt.launch_kernel_with_config()
181+```
182+ 
183+该实现不声明 args 中的地址槽位。两张图的计算逻辑和 kernel 完全一致,因此性能差异用于反映
184+声明式地址刷新的效果。
185+ 
186+## 构建产物
187+ 
188+- `build/add_custom.aicore.o`
189+ Ascend C Add kernel 的设备侧 binary,由 `bisheng` 编译并提取 `.aicore_binary` 段生成。
190+- `build/opp/`
191+ 自定义算子 OPP 注册库(`op_proto/custom/libcust_opapi.so` 及平台拷贝),通过 `ASCEND_CUSTOM_OPP_PATH` 暴露给 GE。
192+- `build/es_output/`
193+ ES 构图接口交付件:`lib64/libes_custom.so` 和 `whl/es_custom-1.0.0-py3-none-any.whl`(已安装到 `build/whl_package/`)。
194+ 
195+## 结果校验
196+ 
197+成功时可观察到:
198+ 
199+- 终端输出包含 `AnnotatedAddCustom precision check PASS` 和 `NoRefreshAddCustom precision check PASS`。
200+- 终端输出包含 `AnnotatedAddCustom`、`NoRefreshAddCustom` 和 `Annotated speedup`。
201+- 终端输出包含 `NPU_EXECUTION=PASS` 和 `Online Python custom-op pipeline PASS`。
202+ 
203+若失败,优先检查:
204+ 
205+- `ASCEND_HOME_PATH` 是否已设置并正确加载 CANN 环境。
206+- `bisheng`、`llvm-objcopy`、`cmake` 是否可用。
207+- `build/es_output/whl/es_custom-1.0.0-py3-none-any.whl` 和 `build/add_custom.aicore.o` 是否生成。
208+- 当前环境是否具备可用 NPU。
209+ 
210+## 注意事项 / 限制
211+ 
212+- kernel binary 由 `compile` 回调调用 `bisheng` 编译,图编译阶段包含该开销;100 轮计时不包含这些阶段。
213+- `ge.graphRunMode` 设置为 `1`(`PRIORITY_GRAPH`),确保使用在线执行链路。
214+- 性能测试使用两套设备内存交替执行,以触发输入/输出地址变化。
215+- 加速比受 NPU 型号、系统负载等因素影响,仅供参考。
216+- NPU 架构默认 `dav-2201`(Atlas A2),其他型号需通过 `ADD_CUSTOM_NPU_ARCH` 覆盖。
217+ 
218+## 附录
219+ 
220+### 算子规格
221+ 
222+| 项目 | 内容 |
223+| --- | --- |
224+| 算子类型 | `AnnotatedAddCustom` / `NoRefreshAddCustom` |
225+| 输入 | `x`, `y` |
226+| 输出 | `z` |
227+| 输入/输出 shape | `[8192]` |
228+| 输入/输出数据类型 | `float32` |
229+| 格式 | `ND` |
230+| kernel 名称 | `add_custom`(Ascend C,`bisheng` 编译) |
231+| block 大小 | `1024` |
232+ 
233+### 关键接口
234+ 
235+| 接口 | 算子 | 用途 |
236+| --- | --- | --- |
237+| `ge.custom_op.register_op` + `infer_meta` | 两个算子 | 原型注册与输出 shape/dtype 推导 |
238+| `register_op_impl` + `compile` | `AnnotatedAddCustom` | `bisheng` 编译 kernel 并按 shape 缓存 binary |
239+| `register_op_impl` + `declare_launch_args` | `AnnotatedAddCustom` | 声明 kernel launch 和输入/输出地址槽位 |
240+| `register_op_impl` + `execute` | `NoRefreshAddCustom` | 申请输出 Tensor 并经 ACL API 下发 kernel |
241+| `acl.rt.launch_kernel_with_config` | `NoRefreshAddCustom` | ACL Python API 直接下发 kernel |
@@ -1,58 +1,254 @@
1-# Python Online Custom Operator Performance Comparison1+# Python Declarative Address Refresh Add Custom Operator Online Sample
2 2 
3-This sample uses Python decorators to register the prototypes and `infer_meta` functions of `AnnotatedAddCustom` and `NoRefreshAddCustom`. Two online GE3+## Sample Overview
4-graphs compare declarative address refresh with a path that does not declare
5-refreshable addresses. Both paths use the same Ascend C Add kernel. The script validates results and measures
6-execution without generating AIR/OM.
7 4 
8-## Directory5+- Graph building entry: `GE` (Python ES graph-building APIs)
6+- Operator language: `Python` (prototype and callbacks) + `Ascend C` (kernel)
7+- Compilation mode: `bisheng` compiles the Ascend C kernel; the Python `compile` callback generates and caches the binary by shape
8+- Core path: `Python prototype registration -> ES graph building -> Session online execution -> compile/declare_launch_args/execute callbacks`
9+- Comparison target: declarative address refresh `AnnotatedAddCustom` versus no-refresh `NoRefreshAddCustom`
9 10 
10-```text11+This sample defines two functionally identical Add custom operators. Both take `[8192]` float32 inputs and
11-online/python12+dispatch the same `add_custom` Ascend C kernel:
12-├── CMakeLists.txt # Build the custom OPP library and ES wheel
13-├── run.sh # Build and run the online comparison
14-├── proto/ # C++ graph prototypes consumed by gen_esb
15-└── src/
16- ├── run.py # Validate and benchmark the two online graphs
17- └── ge/annotated_add_custom.py # Python prototypes and infer_meta
18-```
19 13 
20-## Requirements and usage14+- `AnnotatedAddCustom` provides `compile` and `declare_launch_args` through `register_op_impl`. `compile`
15+ invokes `bisheng` to compile `add_custom.asc` and caches the binary by input shape/dtype;
16+ `declare_launch_args` declares two input address slots and one output address slot, and GE refreshes
17+ addresses according to that layout during repeated execution.
18+- `NoRefreshAddCustom` provides `execute` through `register_op_impl`. On every graph execution it allocates
19+ the output tensor, loads the binary through the ACL Python API, reassembles the kernel arguments, and
20+ serves as the performance baseline.
21 21 
22-Install and configure CANN, CMake, Python 3/pip, and an available NPU.22+The C++ `REG_OP` declarations in `proto/add_custom.h` are used only by `gen_esb` to generate the
23+graph-building APIs. The runtime prototypes and `infer_meta` functions are supplied by Python decorators,
24+and all operator execution is implemented by Python callbacks. `online/cpp/ge/custom_op.cpp` is not compiled.
23 25 
24-The C++ `REG_OP` declarations in `proto/add_custom.h` are used only by `gen_esb` to generate the graph APIs.26+`run.py` builds the two graphs above. Each graph alternates between two sets of device buffers, performs
25-The runtime prototypes and `infer_meta` functions of both operators are supplied by Python decorators. Python `register_op_impl`27+warm-up, 100 benchmark iterations, and precision checks, then prints the latency of both paths and the
26-supplies `compile`, `declare_launch_args`, and `execute` for both operators. They reuse the Ascend C binary generated28+speedup.
27-from `cpp/add_custom_kernel/add_custom.asc`.
28 29 
29-This directory does not compile `online/cpp/ge/custom_op.cpp`; that file is provided only as the C++ comparison sample.30+## Applicable Scenarios
30-All operator execution in this Python sample is implemented by Python callbacks.31+ 
32+- Understanding how Python `register_op`/`register_op_impl` declare kernel tasks and args address layouts.
33+- Comparing declarative address refresh with no-refresh paths in repeated online execution.
34+- Verifying the complete chain of Python-side prototype, compile callback, and execute callback.
35+ 
36+## Prerequisites
37+ 
38+### CANN
39+ 
40+- The CANN environment is installed and configured, for example by running `source ${ASCEND_HOME_PATH}/set_env.sh`.
41+- The environment provides the required `ACL`, `GE`, and `Graph` headers and libraries.
42+- Refer to the [Installation Guide](../../../../../docs/en/quick_install.md) to install the toolkit and ops packages.
43+ 
44+### Frameworks and Plugins
45+ 
46+- Python 3 and `pip`.
47+- The ES graph-building APIs are provided by the `es_custom` wheel built by this sample. No extra
48+ framework plugin is required.
49+ 
50+### Environment Variables
51+ 
52+- `ASCEND_HOME_PATH`
53+- `run.sh` automatically sets `PYTHONPATH`, `LD_LIBRARY_PATH`, `ASCEND_CUSTOM_OPP_PATH`,
54+ `GE_PYTHON_CUSTOM_OP_SOURCE`, and `GE_PYTHON_CUSTOM_OP_BINARY`.
55+ 
56+### Additional Dependencies
57+ 
58+- `cmake`
59+- `bisheng` and `llvm-objcopy` (shipped with the CANN toolchain, used to compile and extract the kernel binary)
60+ 
61+## Quick Run
62+ 
63+Run the following commands in the `examples/custom_op/annotated_args_refresh_add_custom/online/python` directory:
64+ 
65+### Recommended
31 66 
32```bash67```bash
33-source /path/to/cann/set_env.sh68+source ${ASCEND_HOME_PATH}/set_env.sh
34-cd examples/custom_op/annotated_args_refresh_add_custom/online/python
35bash run.sh69bash run.sh
36```70```
37 71 
38-Use `DEVICE_ID` (default `0`) to select the NPU:72+`run.sh` automatically compiles the kernel, builds and installs the OPP/ES wheel, and runs the online
73+comparison:
39 74 
40-```bash75+1. `bisheng` compiles `add_custom.asc` and `llvm-objcopy` extracts the `.aicore_binary` section into the
41-DEVICE_ID=1 bash run.sh76+ kernel binary.
77+2. Builds the custom OPP registration library and the Python ES wheel, then installs it.
78+3. Runs the online precision and performance comparison of the two graphs.
79+ 
80+On success the terminal prints something like:
81+ 
82+```text
83+[OnlinePython] AnnotatedAddCustom precision check PASS
84+[OnlinePython] NoRefreshAddCustom precision check PASS
85+[Perf] input shape: [8192], dtype: float32
86+[Perf] iters: 100
87+[Perf] AnnotatedAddCustom: xxx us (avg xxx us/iter)
88+[Perf] NoRefreshAddCustom: xxx us (avg xxx us/iter)
89+[Perf] Annotated speedup: xxx x
90+[OnlinePython] NPU_EXECUTION=PASS
42```91```
43 92 
44-## Execution flow93+### Step by Step
45 94 
46-1. The custom OPP and ES wheel are built to generate both Python graph APIs.95+```bash
47-2. The Python `compile` callback compiles and retains the Ascend C Add kernel binary while GE compiles the graph.96+source ${ASCEND_HOME_PATH}/set_env.sh
48-3. The Python `declare_launch_args` callback declares the input/output address slots and kernel launch, allowing GE to refresh addresses during97+mkdir -p build
49- repeated execution.98+# 1. Compile the Ascend C kernel and extract the binary
50-4. Python `execute` allocates the schema output and launches the same Ascend C kernel through the ACL Python API as the99+bisheng -c ../cpp/add_custom_kernel/add_custom.asc -o build/add_custom.host.o --npu-arch=dav-2201
51- no-declaration baseline.100+llvm-objcopy -O binary --only-section=.aicore_binary build/add_custom.host.o build/add_custom.aicore.o
52-5. `run.py` validates both graphs, performs five warm-up iterations, and measures 100 iterations.101+# 2. Build the custom OPP and ES wheel
102+cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
103+cmake --build build --target build_es_custom -j"$(nproc)"
104+# 3. Install the ES wheel
105+python3 -m pip install --target build/whl_package build/es_output/whl/es_custom-1.0.0-py3-none-any.whl
106+# 4. Set environment variables and run the comparison
107+export PYTHONPATH="$(pwd)/build/whl_package:$(pwd)/src:$PYTHONPATH"
108+export LD_LIBRARY_PATH="$(pwd)/build/es_output/lib64:$LD_LIBRARY_PATH"
109+export ASCEND_CUSTOM_OPP_PATH="$(pwd)/build/opp:$(pwd)/src/ge"
110+export GE_PYTHON_CUSTOM_OP_SOURCE="$(pwd)/../cpp/add_custom_kernel/add_custom.asc"
111+export GE_PYTHON_CUSTOM_OP_BINARY="$(pwd)/build/add_custom.aicore.o"
112+DEVICE_ID=0 python3 src/run.py
113+```
53 114 
54-The log reports total and average times for both operators plus `Annotated speedup`.115+The NPU architecture defaults to `dav-2201` and can be overridden with `ADD_CUSTOM_NPU_ARCH`; `DEVICE_ID`
55-`NPU_EXECUTION=PASS` indicates that both paths passed validation and execution.116+defaults to `0`.
56 117 
57-The Add kernel is written with Ascend C in `online/cpp/add_custom_kernel/add_custom.asc`. Use the CANN Ascend C118+## Directory Structure and Key Files
58-documentation when developing kernels for the target hardware.119+ 
120+```text
121+annotated_args_refresh_add_custom
122+└── online/python
123+ ├── CMakeLists.txt # Build the custom OPP library and ES wheel
124+ ├── README.md
125+ ├── README_en.md
126+ ├── run.sh # Build and run the online comparison
127+ ├── proto
128+ │ └── add_custom.h # C++ graph prototype consumed by gen_esb
129+ └── src
130+ ├── run.py # Validate and benchmark the two online graphs
131+ └── ge
132+ └── annotated_add_custom.py # Python prototypes, infer_meta, and implementations
133+```
134+ 
135+Key files:
136+ 
137+- `src/ge/annotated_add_custom.py`
138+ Implements `AnnotatedAddCustom` and `NoRefreshAddCustom`. The former generates the binary in the compile
139+ callback and declares the address slots during task generation; the latter dispatches the same kernel
140+ through the eager execution path.
141+- `proto/add_custom.h`
142+ Registers the `AnnotatedAddCustom` and `NoRefreshAddCustom` graph operator types for `gen_esb` to
143+ generate the Python ES graph-building APIs.
144+- `src/run.py`
145+ Builds the two graphs, alternates two sets of input/output device addresses, performs 5 warm-up
146+ iterations, 100 benchmark iterations, and validates precision.
147+- `CMakeLists.txt`
148+ Builds the custom OPP registration library (`libcust_opapi.so`) and the Python ES wheel.
149+ 
150+## Core Path
151+ 
152+### Online Execution
153+ 
154+1. `run.py` builds `python_annotated_graph` and `python_no_refresh_graph`, both with `[8192]` float32 inputs.
155+2. The `compile` callback of `AnnotatedAddCustom` reads `add_custom.asc`, invokes `bisheng`, extracts the
156+ `.aicore_binary` section, and caches the binary by input shape/dtype.
157+3. The `declare_launch_args` callback of `AnnotatedAddCustom` sets the kernel name, binary, and block
158+ dimension, then declares `InputAddr{0}`, `InputAddr{1}`, and `OutputAddr{0}`.
159+4. The `execute` callback of `NoRefreshAddCustom` allocates the output tensor, loads the binary through
160+ the ACL Python API, assembles the args, and dispatches the kernel.
161+5. The two graphs alternate between two sets of device addresses and report total time, average time,
162+ and speedup.
163+ 
164+### Declarative Address Refresh
165+ 
166+```text
167+Compile phase:
168+ compile(x, y, z)
169+ ├─ Read add_custom.asc
170+ ├─ bisheng + llvm-objcopy extract .aicore_binary -> kernel binary
171+ └─ Cache the binary by input shape/dtype
172+ 
173+Task generation phase:
174+ declare_launch_args(x, y, z)
175+ ├─ KernelArgs: InputAddr{0}, InputAddr{1}, OutputAddr{0}
176+ ├─ AnnotatedKernelLaunchInfo { kernel_name, kernel_bin, block_dim, stream_id }
177+ └─ ctx.add_launch(launch_info, args)
178+ 
179+Execution phase:
180+ GE refreshes the current input/output addresses according to the saved args layout
181+```
182+ 
183+### No-Refresh Baseline
184+ 
185+```text
186+Every graph execution:
187+ NoRefreshAddCustom.execute(x, y)
188+ ├─ ctx.malloc_output_tensor()
189+ ├─ acl.rt.binary_load_from_file() / binary_get_function()
190+ ├─ kernel_args_init / append(x, y, z) / finalize
191+ └─ acl.rt.launch_kernel_with_config()
192+```
193+ 
194+This implementation declares no address slots in the args. Both graphs share identical computation logic
195+and kernel, so the performance difference reflects the effect of declarative address refresh.
196+ 
197+## Build Artifacts
198+ 
199+- `build/add_custom.aicore.o`
200+ The device-side binary of the Ascend C Add kernel, compiled with `bisheng` and extracted from the `.aicore_binary` section.
201+- `build/opp/`
202+ The custom operator OPP registration library (`op_proto/custom/libcust_opapi.so` plus platform copies), exposed to GE through `ASCEND_CUSTOM_OPP_PATH`.
203+- `build/es_output/`
204+ ES graph-building deliverables: `lib64/libes_custom.so` and `whl/es_custom-1.0.0-py3-none-any.whl` (installed into `build/whl_package/`).
205+ 
206+## Result Verification
207+ 
208+On success you can observe:
209+ 
210+- The terminal output contains `AnnotatedAddCustom precision check PASS` and `NoRefreshAddCustom precision check PASS`.
211+- The terminal output contains `AnnotatedAddCustom`, `NoRefreshAddCustom`, and `Annotated speedup`.
212+- The terminal output contains `NPU_EXECUTION=PASS` and `Online Python custom-op pipeline PASS`.
213+ 
214+If it fails, check first:
215+ 
216+- Whether `ASCEND_HOME_PATH` is set and the CANN environment is loaded correctly.
217+- Whether `bisheng`, `llvm-objcopy`, and `cmake` are available.
218+- Whether `build/es_output/whl/es_custom-1.0.0-py3-none-any.whl` and `build/add_custom.aicore.o` were generated.
219+- Whether an NPU is available in the current environment.
220+ 
221+## Notes / Limitations
222+ 
223+- The kernel binary is compiled by `bisheng` inside the `compile` callback; the graph compilation phase
224+ includes this overhead, which is excluded from the 100 benchmark iterations.
225+- `ge.graphRunMode` is set to `1` (`PRIORITY_GRAPH`) to ensure the online execution path.
226+- The benchmark alternates two sets of device buffers to trigger input/output address changes.
227+- The speedup is affected by the NPU model and system load, and is for reference only.
228+- The NPU architecture defaults to `dav-2201` (Atlas A2); override it with `ADD_CUSTOM_NPU_ARCH` for
229+ other models.
230+ 
231+## Appendix
232+ 
233+### Operator Specification
234+ 
235+| Item | Content |
236+| --- | --- |
237+| Operator type | `AnnotatedAddCustom` / `NoRefreshAddCustom` |
238+| Inputs | `x`, `y` |
239+| Output | `z` |
240+| Input/output shape | `[8192]` |
241+| Input/output data type | `float32` |
242+| Format | `ND` |
243+| Kernel name | `add_custom` (Ascend C, compiled with `bisheng`) |
244+| Block size | `1024` |
245+ 
246+### Key Interfaces
247+ 
248+| Interface | Operator | Purpose |
249+| --- | --- | --- |
250+| `ge.custom_op.register_op` + `infer_meta` | Both operators | Prototype registration and output shape/dtype inference |
251+| `register_op_impl` + `compile` | `AnnotatedAddCustom` | Compile the kernel with `bisheng` and cache the binary by shape |
252+| `register_op_impl` + `declare_launch_args` | `AnnotatedAddCustom` | Declare the kernel launch and input/output address slots |
253+| `register_op_impl` + `execute` | `NoRefreshAddCustom` | Allocate the output tensor and dispatch the kernel through the ACL API |
254+| `acl.rt.launch_kernel_with_config` | `NoRefreshAddCustom` | Dispatch the kernel directly through the ACL Python API |
@@ -0,0 +1,76 @@
1+# -----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# -----------------------------------------------------------------------------------------------------------
10+ 
11+cmake_minimum_required(VERSION 3.16)
12+project(pypto_add_custom LANGUAGES CXX)
13+ 
14+set(CMAKE_CXX_STANDARD 17)
15+set(CMAKE_CXX_STANDARD_REQUIRED ON)
16+set(CMAKE_CXX_EXTENSIONS OFF)
17+ 
18+if(NOT CMAKE_BUILD_TYPE)
19+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
20+endif()
21+ 
22+set(ES_OUTPUT_PATH "${CMAKE_BINARY_DIR}/es_output")
23+set(OPP_OUTPUT_PATH "${CMAKE_BINARY_DIR}/opp")
24+set(ASCEND_HOME_PATH "$ENV{ASCEND_HOME_PATH}")
25+if(NOT ASCEND_HOME_PATH)
26+ message(FATAL_ERROR "ASCEND_HOME_PATH is empty. Please source CANN set_env.sh first.")
27+endif()
28+ 
29+list(APPEND CMAKE_MODULE_PATH "${ASCEND_HOME_PATH}/include/ge/cmake")
30+find_package(GenerateEsPackage REQUIRED)
31+add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 google=ascend_private)
32+ 
33+add_library(pypto_add_custom_op_proto SHARED proto/add_custom.cc)
34+target_compile_definitions(pypto_add_custom_op_proto PRIVATE OP_PROTO_LIB)
35+target_compile_options(pypto_add_custom_op_proto PRIVATE -fvisibility=hidden)
36+target_include_directories(pypto_add_custom_op_proto PRIVATE
37+ "${ASCEND_HOME_PATH}/include"
38+ "${ASCEND_HOME_PATH}/include/external"
39+)
40+target_link_libraries(pypto_add_custom_op_proto PRIVATE
41+ "${ASCEND_HOME_PATH}/lib64/libopp_registry.so"
42+ "${ASCEND_HOME_PATH}/lib64/libgraph.so"
43+ "${ASCEND_HOME_PATH}/lib64/libgraph_base.so"
44+)
45+ 
46+if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
47+ set(OPP_OS_TYPE "windows")
48+else()
49+ set(OPP_OS_TYPE "linux")
50+endif()
51+string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" PROCESSOR_LOWER)
52+if(PROCESSOR_LOWER MATCHES "^(aarch64|arm64)$")
53+ set(OPP_CPU_TYPE "aarch64")
54+elseif(PROCESSOR_LOWER MATCHES "^(x86_64|amd64)$")
55+ set(OPP_CPU_TYPE "x86_64")
56+else()
57+ set(OPP_CPU_TYPE "${PROCESSOR_LOWER}")
58+endif()
59+set(OPP_PROTO_OUTPUT_PATH "${OPP_OUTPUT_PATH}/op_proto/custom")
60+set(CUSTOM_OP_OUTPUT_PATH "${OPP_OUTPUT_PATH}/op_graph/lib/${OPP_OS_TYPE}/${OPP_CPU_TYPE}")
61+set_target_properties(pypto_add_custom_op_proto PROPERTIES
62+ OUTPUT_NAME cust_opapi
63+ LIBRARY_OUTPUT_DIRECTORY "${OPP_PROTO_OUTPUT_PATH}"
64+ RUNTIME_OUTPUT_DIRECTORY "${OPP_PROTO_OUTPUT_PATH}"
65+ ARCHIVE_OUTPUT_DIRECTORY "${OPP_PROTO_OUTPUT_PATH}"
66+)
67+add_custom_command(TARGET pypto_add_custom_op_proto POST_BUILD
68+ COMMAND ${CMAKE_COMMAND} -E make_directory "${CUSTOM_OP_OUTPUT_PATH}"
69+ COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:pypto_add_custom_op_proto>" "${CUSTOM_OP_OUTPUT_PATH}"
70+)
71+ 
72+add_es_library_and_whl(
73+ ES_LINKABLE_AND_ALL_TARGET es_custom
74+ OPP_PROTO_TARGET pypto_add_custom_op_proto
75+ OUTPUT_PATH ${ES_OUTPUT_PATH}
76+)
@@ -0,0 +1,199 @@
1+# PyPTO 算子通过 GE 自定义算子入图样例
2+ 
3+## 样例概述
4+ 
5+- 构图入口:`GE`(Python ES 构图接口,RT2 动态执行路径)
6+- 算子编程语言:`PyPTO`(Python Tensor 算子编程)
7+- 编译方式:`JIT`(主线程预热编译,进程内缓存复用)
8+- 模型下沉能力:`不涉及`
9+- 核心链路:`PyPTO kernel -> Python 自定义算子 execute 回调 -> torchair 零拷贝桥接 -> GE 图执行`
10+- 与其他 sample 的区别:本样例把 PyPTO JIT kernel 接入 GE 自定义算子的 Python `execute` 回调,不编译任何 `.so` 或 kernel binary,也不涉及模型下沉。
11+ 
12+本样例注册 `PyptoAddCustom` 自定义算子:`@register_op` 注册原型和 `infer_meta`,
13+`@register_op_impl` 注册 schema-bound `execute(x, y)` 实现。执行时 execute 回调通过 GE
14+`EagerOpExecutionContext` 申请输出 Tensor,把输入/输出 device 地址经 torchair `as_torch_tensors` 零拷贝包装为
15+torch Tensor,再调用 `@pypto.jit` 定义的 Add kernel。PyPTO kernel 由 `src/run.py` 在 GE
16+初始化前于主线程预热编译,execute 回调只消费进程内编译缓存。
17+ 
18+## 适用场景
19+ 
20+- 想了解 PyPTO kernel 如何通过 Python 自定义算子接入 GE 图执行。
21+- 想参考 GE Tensor 与 torch Tensor 之间的零拷贝桥接方式。
22+- 想验证 PyPTO JIT kernel 在 GE 在线执行链路下的数值正确性。
23+ 
24+## 前置依赖
25+ 
26+### CANN
27+ 
28+- 参考 [安装指导](../../../docs/zh/quick_install.md#1-环境准备) 完成 toolkit 和 ops 包安装。
29+- CANN 版本需 >= 9.2.0,`pypto` Python 包随 CANN toolkit 发布(位于 `python/site-packages/pypto`)。
30+ 
31+### 框架与插件
32+ 
33+- 已安装 `PyTorch` 和 `torch_npu`。
34+ 
35+参考:
36+ 
37+- [Ascend Extension for PyTorch](https://gitcode.com/Ascend/pytorch)
38+- [Ascend Extension for PyTorch 昇腾社区说明](https://hiascend.com/document/redirect/Pytorch-index)
39+ 
40+### 环境变量
41+ 
42+- `ASCEND_HOME_PATH`
43+- `LD_LIBRARY_PATH` 等 CANN 运行时相关变量
44+- `run.sh` 会自动设置 `PYTHONPATH`、`LD_LIBRARY_PATH` 和 `ASCEND_CUSTOM_OPP_PATH`
45+ 
46+### 额外依赖
47+ 
48+- `cmake`、Python 3/`pip`
49+ 
50+## 快速运行
51+ 
52+在 `examples/custom_op/pypto_add_custom` 目录下执行:
53+ 
54+### 推荐方式
55+ 
56+```bash
57+source ${ASCEND_HOME_PATH}/set_env.sh
58+bash run.sh
59+```
60+ 
61+`run.sh` 会自动完成构建、安装和在线执行:
62+ 
63+1. 构建 custom OPP 注册库和 ES Python 构图接口(产物全部位于 `build/` 下)。
64+2. 运行 `src/run.py`:主线程预热编译 PyPTO kernel,构建 `PyptoAddCustom` 图并在线执行。
65+ 
66+若运行成功,终端会打印类似:
67+ 
68+```text
69+[OnlinePython] PyPTO kernel warm-up PASS
70+[OnlinePython] PyptoAddCustom precision check PASS
71+[Perf] input shape: [8192], dtype: float32
72+[Perf] iters: 100
73+[Perf] PyptoAddCustom: xxx us (avg xxx us/iter)
74+[OnlinePython] NPU_EXECUTION=PASS
75+```
76+ 
77+## 目录结构与关键文件
78+ 
79+```text
80+pypto_add_custom
81+├── CMakeLists.txt # 构建 custom OPP 注册库和 ES wheel
82+├── README.md
83+├── README_en.md
84+├── run.sh # 构建并执行在线 GE 图
85+├── proto
86+│ └── add_custom.h # gen_esb 使用的 C++ 构图原型
87+└── src
88+ ├── tensor_bridge.py # GE Tensor 到 torch Tensor 的零拷贝桥接(torchair)
89+ ├── pypto_add_kernel.py # @pypto.jit Add kernel(共享)
90+ ├── run.py # 预热 + GE 图构建与在线执行
91+ └── ge
92+ └── add_custom.py # Python 原型、infer_meta 和 execute 回调
93+```
94+ 
95+重点文件:
96+ 
97+- `src/ge/add_custom.py`
98+ 实现 `PyptoAddCustom`:`@register_op` 注册原型与 `infer_meta`;`@register_op_impl` 的
99+ `execute` 申请输出 Tensor、经 torchair 桥接输入输出地址并在设备同步保护下执行 PyPTO kernel。
100+- `src/tensor_bridge.py`
101+ GE device 地址到 torch Tensor 的零拷贝桥接,复用 torchair `as_torch_tensors`
102+ (torchair Python 自定义算子回调的同款机制),设备内存始终归 GE 所有。
103+- `src/pypto_add_kernel.py`
104+ `@pypto.jit` 定义的 Add kernel,含 `pypto.set_vec_tile_shapes(1, 1024)` tile 配置,
105+ 供 execute 回调和直连测试共享。
106+- `src/run.py`
107+ 在 GE 初始化前主线程预热编译 kernel,随后构图、精度校验和 100 轮计时。
108+- `proto/add_custom.h`
109+ 注册 `PyptoAddCustom` 构图侧算子类型,供 `gen_esb` 生成 Python ES 构图接口。
110+ 
111+## 核心链路
112+ 
113+1. `run.py` 在主线程导入 torch/torch_npu/pypto 并预热编译 PyPTO kernel,填充进程内编译缓存。
114+2. `run.py` 以 `ge.exec.static_model_ops_lower_limit=-1` 初始化 GE,使图走 RT2 动态执行路径,
115+ Python execute 回调在每次图执行时被调用。
116+3. `infer_meta` 校验输入并返回输出元信息。
117+4. `execute` 通过 `ctx.malloc_output_tensor` 申请输出,把 x/y/z 的 device 地址经 torchair
118+ 零拷贝包装为 torch Tensor。
119+5. execute 在设备同步保护下调用 PyPTO kernel(torch 当前 stream),kernel 结果由 GE 图
120+ 后续读取。
121+ 
122+## 构建产物
123+ 
124+- `build/opp/`
125+ 自定义算子 OPP 注册库(`op_proto/custom/libcust_opapi.so` 及 `op_graph/lib/<os>/<arch>/` 平台拷贝),通过 `ASCEND_CUSTOM_OPP_PATH` 暴露给 GE。
126+- `build/es_custom_build/python_package/es_custom/`
127+ 构建自动生成的 ES Python 构图接口(含 `__init__.py` 与 `gen_esb` 生成的 wrapper),通过 `PYTHONPATH` 加载。
128+- `build/es_output/`
129+ ES 库交付件:`lib64/libes_custom.so`(`LD_LIBRARY_PATH` 加载)和 `whl/es_custom-1.0.0-py3-none-any.whl`。
130+ 
131+## 结果校验
132+ 
133+成功时可观察到:
134+ 
135+- 终端输出包含 `PyPTO kernel warm-up PASS`。
136+- 终端输出包含 `PyptoAddCustom precision check PASS`。
137+- 终端输出包含 `NPU_EXECUTION=PASS` 和 `Online Python PyPTO custom-op pipeline PASS`。
138+ 
139+若失败,优先检查:
140+ 
141+- `ASCEND_HOME_PATH` 是否已设置并正确加载 CANN 环境。
142+- torch/torch_npu/pypto 是否可导入(CANN >= 9.2.0)。
143+- `build/opp/`、`build/es_custom_build/python_package/es_custom/` 和 `build/es_output/lib64/` 是否生成。
144+- 当前环境是否具备可用 NPU。
145+ 
146+## PyPTO 支持现状
147+ 
148+- 当前 `pypto` Python 包仅支持 **JIT 直接执行**:首次调用 `@pypto.jit` kernel 时自动完成
149+ 解析、编译和执行,编译结果缓存在进程内部。
150+- **尚未提供独立的编译接口**:无法把 PyPTO kernel 预编译为可独立发布的 kernel binary 文件
151+ (例如 Ascend C 通过 `bisheng` 编译得到的 `.aicore.o`)。
152+- **尚未提供 binary 导出接口**:JIT 编译产物为 PyPTO 内部格式,无法转换为 GE `AnnotatedArgs`
153+ 声明式地址刷新所需的 `kernel_bin`,因此无法接入 `compile`/`declare_launch_args` 链路,
154+ 也无法通过 ATC 下沉到 om 离线模型。
155+- 本样例展示的 **Python `execute` 回调 + torchair 桥接** 是当前 PyPTO kernel 接入 GE 的可行路径。
156+ 
157+## 注意事项 / 限制
158+ 
159+- 已验证的产品:`Atlas A2 训练系列产品`。样例不包含硬编码的 NPU 架构,PyPTO JIT 在编译时自动探测实际设备架构;理论上 CANN 9.2 中 PyPTO 和 torch_npu 支持的其他平台(如 Atlas A3、Ascend 950 系列)也可运行,其他平台未实测。
160+- 样例当前使用 `float32` 的 `8192` 一维输入做结果校验,输入元素个数需能被 tile size(1024)整除。
161+- **kernel 必须在 GE 初始化前于主线程预热编译**:在 GE 回调内触发 JIT 编译会因编译器子进程
162+ 与 GE 运行时冲突而挂起。
163+- `run.py` 通过 `ge.exec.static_model_ops_lower_limit=-1` 强制走 RT2 动态执行路径,使 execute
164+ 回调在每次图执行时被调用;默认静态路径下 execute 仅在任务生成阶段调用一次,与 PyPTO 的
165+ 即时启动模型不兼容。
166+- **torchair 桥接要求 `import torchair.llm_datadist` 可用**(`src/tensor_bridge.py` 直接使用公开
167+ `create_npu_tensors` 接口,不含兼容性降级逻辑)。部分 torch/torchair 版本组合下 torchair
168+ 包初始化会失败(例如 `torch.fx` 缺少 `hint_int`),此时需将 torch_npu 与 torchair 升级为
169+ 相互匹配的版本后再运行本样例。
170+- Python 导入顺序要求 `import torch` / `import torch_npu` 在 `import pypto` 之前,
171+ 否则可能触发 `libc10.so` 静态 TLS 相关报错。
172+- execute 回调以设备同步保证数据可见性,单轮耗时(约 4 ms)高于 Ascend C + AnnotatedArgs
173+ 路径(参考 `annotated_args_refresh_add_custom`,约 0.4 ms),本样例聚焦链路打通而非性能。
174+ 
175+## 附录
176+ 
177+### 算子规格
178+ 
179+| 项目 | 内容 |
180+| --- | --- |
181+| 算子类型 | `PyptoAddCustom` |
182+| 输入 | `x`, `y` |
183+| 输出 | `z` |
184+| 输入 shape | `8192` |
185+| 输出 shape | `8192` |
186+| 数据类型 | `float32` |
187+| 格式 | `ND` |
188+| kernel 名称 | `pypto_add_kernel`(PyPTO JIT) |
189+| tile 大小 | `1024` |
190+ 
191+### 关键接口
192+ 
193+| 接口 | 用途 |
194+| --- | --- |
195+| `ge.custom_op.register_op` + `infer_meta` | 原型注册与输出 shape/dtype 推导 |
196+| `ge.custom_op.register_op_impl` + `execute` | 申请输出 Tensor 并执行 PyPTO kernel |
197+| `ge.custom_op.get_execute_ctx` | 获取 execute 回调上下文(输出申请) |
198+| `pypto.jit` + `pypto.set_vec_tile_shapes` | PyPTO kernel 定义与 tile 配置 |
199+| `torchair as_torch_tensors` | GE device 地址到 torch Tensor 的零拷贝桥接 |
@@ -0,0 +1,218 @@
1+# PyPTO Operator Enters GE Graph Through Custom Operator
2+ 
3+## Sample Overview
4+ 
5+- Graph building entry: `GE` (Python ES graph-building APIs, RT2 dynamic execution path)
6+- Operator language: `PyPTO` (Python Tensor operator programming)
7+- Compilation mode: `JIT` (warm-up compiled on the main thread, reused from the in-process cache)
8+- Model sinking capability: `N/A`
9+- Core path: `PyPTO kernel -> Python custom-op execute callback -> torchair zero-copy bridge -> GE graph execution`
10+- Difference from other samples: this sample wires a PyPTO JIT kernel into the Python `execute` callback of a GE custom operator. No `.so` or kernel binary is compiled, and model sinking is not involved.
11+ 
12+This sample registers the `PyptoAddCustom` custom operator: `@register_op` registers the prototype
13+and `infer_meta`, and `@register_op_impl` registers the schema-bound `execute(x, y)` implementation.
14+At run time the execute callback allocates the output tensor through the GE
15+`EagerOpExecutionContext`, wraps the input/output device memory as torch tensors through a
16+zero-copy torchair bridge (`as_torch_tensors`), and invokes the Add kernel defined with `@pypto.jit`. The PyPTO kernel is
17+warm-up compiled by `src/run.py` on the main thread before GE starts; the execute callback only
18+consumes the in-process compilation cache.
19+ 
20+## Applicable Scenarios
21+ 
22+- Understanding how a PyPTO kernel enters GE graph execution through a Python custom operator.
23+- Referencing the zero-copy bridge between GE tensors and torch tensors.
24+- Validating the numerics of a PyPTO JIT kernel on the GE online execution path.
25+ 
26+## Prerequisites
27+ 
28+### CANN
29+ 
30+- Follow [installation guide](../../../docs/en/quick_install.md#1-environment-preparation) to complete toolkit and ops package installation.
31+- CANN version must be >= 9.2.0. The `pypto` Python package is released with the CANN toolkit (located in `python/site-packages/pypto`).
32+ 
33+### Frameworks and Plugins
34+ 
35+- `PyTorch` and `torch_npu` have been installed.
36+ 
37+References:
38+ 
39+- [Ascend Extension for PyTorch](https://gitcode.com/Ascend/pytorch)
40+- [Ascend Extension for PyTorch on Ascend community](https://hiascend.com/document/redirect/Pytorch-index)
41+ 
42+### Environment Variables
43+ 
44+- `ASCEND_HOME_PATH`
45+- `LD_LIBRARY_PATH` and other CANN runtime related variables
46+- `run.sh` automatically sets `PYTHONPATH`, `LD_LIBRARY_PATH`, and `ASCEND_CUSTOM_OPP_PATH`
47+ 
48+### Additional Dependencies
49+ 
50+- `cmake`, Python 3/`pip`
51+ 
52+## Quick Run
53+ 
54+Run the following commands in the `examples/custom_op/pypto_add_custom` directory:
55+ 
56+### Recommended
57+ 
58+```bash
59+source ${ASCEND_HOME_PATH}/set_env.sh
60+bash run.sh
61+```
62+ 
63+`run.sh` automatically builds, installs, and runs the online graph:
64+ 
65+1. Builds the custom OPP registration library and the ES Python graph-building APIs
66+ (all artifacts are placed under `build/`).
67+2. Runs `src/run.py`: warm-up compiles the PyPTO kernel on the main thread, builds the
68+ `PyptoAddCustom` graph, and executes it online.
69+ 
70+On success the terminal prints something like:
71+ 
72+```text
73+[OnlinePython] PyPTO kernel warm-up PASS
74+[OnlinePython] PyptoAddCustom precision check PASS
75+[Perf] input shape: [8192], dtype: float32
76+[Perf] iters: 100
77+[Perf] PyptoAddCustom: xxx us (avg xxx us/iter)
78+[OnlinePython] NPU_EXECUTION=PASS
79+```
80+ 
81+## Directory Structure and Key Files
82+ 
83+```text
84+pypto_add_custom
85+├── CMakeLists.txt # Build the custom OPP library and ES wheel
86+├── README.md
87+├── README_en.md
88+├── run.sh # Build and run the online GE graph
89+├── proto
90+│ └── add_custom.h # C++ graph prototype consumed by gen_esb
91+└── src
92+ ├── tensor_bridge.py # Zero-copy bridge from GE tensors to torch tensors (torchair)
93+ ├── pypto_add_kernel.py # @pypto.jit Add kernel (shared)
94+ ├── run.py # Warm-up + GE graph building and online execution
95+ └── ge
96+ └── add_custom.py # Python prototype, infer_meta, and execute callback
97+```
98+ 
99+Key files:
100+ 
101+- `src/ge/add_custom.py`
102+ Implements `PyptoAddCustom`: `@register_op` registers the prototype and `infer_meta`; the
103+ `@register_op_impl` `execute` allocates the output tensor, bridges the input/output device memory
104+ through the torchair bridge, and runs the PyPTO kernel under device synchronization.
105+- `src/tensor_bridge.py`
106+ The zero-copy bridge from GE device memory to torch tensors. It reuses the torchair
107+ ``as_torch_tensors`` entry (the same machinery behind torchair's Python custom-op
108+ callbacks), while the device memory stays owned by GE.
109+- `src/pypto_add_kernel.py`
110+ The Add kernel defined with `@pypto.jit`, including the `pypto.set_vec_tile_shapes(1, 1024)`
111+ tile configuration; shared by the execute callback and the direct test.
112+- `src/run.py`
113+ Warm-up compiles the kernel on the main thread before GE starts, then builds the graph, validates
114+ precision, and measures 100 iterations.
115+- `proto/add_custom.h`
116+ Registers the `PyptoAddCustom` graph operator type for `gen_esb` to generate the Python ES
117+ graph-building APIs.
118+ 
119+## Core Path
120+ 
121+1. `run.py` imports torch/torch_npu/pypto on the main thread and warm-up compiles the PyPTO kernel
122+ to populate the in-process compilation cache.
123+2. `run.py` initializes GE with `ge.exec.static_model_ops_lower_limit=-1` so the graph takes the
124+ RT2 dynamic execution path, where the Python execute callback runs for every graph execution.
125+3. `infer_meta` validates the inputs and returns the output metadata.
126+4. `execute` allocates the output through `ctx.malloc_output_tensor` and wraps the x/y/z device
127+ device memory as torch tensors through the zero-copy torchair bridge.
128+5. `execute` invokes the PyPTO kernel (current torch stream) under device synchronization; the
129+ result is consumed by the subsequent tasks of the GE graph.
130+ 
131+## Build Artifacts
132+ 
133+- `build/opp/`
134+ The custom operator OPP registration library (`op_proto/custom/libcust_opapi.so` plus `op_graph/lib/<os>/<arch>/` platform copies), exposed to GE through `ASCEND_CUSTOM_OPP_PATH`.
135+- `build/es_custom_build/python_package/es_custom/`
136+ The build-generated ES Python graph-building APIs (including the auto-generated `__init__.py` and the wrappers generated by `gen_esb`), loaded through `PYTHONPATH`.
137+- `build/es_output/`
138+ ES deliverables: `lib64/libes_custom.so` (loaded through `LD_LIBRARY_PATH`) and `whl/es_custom-1.0.0-py3-none-any.whl`.
139+ 
140+## Result Verification
141+ 
142+On success you can observe:
143+ 
144+- The terminal output contains `PyPTO kernel warm-up PASS`.
145+- The terminal output contains `PyptoAddCustom precision check PASS`.
146+- The terminal output contains `NPU_EXECUTION=PASS` and `Online Python PyPTO custom-op pipeline PASS`.
147+ 
148+If it fails, check first:
149+ 
150+- Whether `ASCEND_HOME_PATH` is set and the CANN environment is loaded correctly.
151+- Whether torch/torch_npu/pypto can be imported (CANN >= 9.2.0).
152+- Whether `build/opp/`, `build/es_custom_build/python_package/es_custom/`, and `build/es_output/lib64/` were generated.
153+- Whether an NPU is available in the current environment.
154+ 
155+## PyPTO Support Status
156+ 
157+- The current `pypto` Python package only supports **JIT direct execution**: the first call of a
158+ `@pypto.jit` kernel automatically parses, compiles, and executes it, and the compiled result is
159+ cached in the process.
160+- **No standalone compilation interface yet**: a PyPTO kernel cannot be pre-compiled into an
161+ independently publishable kernel binary file (for example the `.aicore.o` produced by compiling
162+ Ascend C with `bisheng`).
163+- **No binary export interface yet**: the JIT compilation artifact is an internal PyPTO format and
164+ cannot be converted into the `kernel_bin` required by the GE `AnnotatedArgs` declarative refresh
165+ mechanism. Therefore it cannot be wired into the `compile`/`declare_launch_args` path, nor sunk
166+ into an offline om model through ATC.
167+- The **Python `execute` callback + torchair bridge** shown in this sample is the currently viable
168+ path to integrate PyPTO kernels with GE.
169+ 
170+## Notes / Limitations
171+ 
172+- Verified products: `Atlas A2 training series`. The sample contains no hardcoded NPU architecture; PyPTO JIT detects the actual device architecture at compile time. Other platforms supported by PyPTO and torch_npu in CANN 9.2 (such as Atlas A3 or the Ascend 950 series) should also work, but are untested.
173+- The sample validates results with a `float32` one-dimensional input of `8192` elements. The number
174+ of input elements must be divisible by the tile size (1024).
175+- **The kernel must be warm-up compiled on the main thread before GE starts**: triggering JIT
176+ compilation inside a GE callback hangs because the compiler subprocess conflicts with the GE
177+ runtime.
178+- `run.py` forces the RT2 dynamic execution path through `ge.exec.static_model_ops_lower_limit=-1`
179+ so the execute callback runs for every graph execution. On the default static path the callback
180+ runs only once at task generation time, which is incompatible with the immediate launch model of
181+ PyPTO.
182+- **The torchair bridge requires a working ``import torchair.llm_datadist``** (``src/tensor_bridge.py``
183+ uses the public ``create_npu_tensors`` entry directly, without compatibility fallbacks). Some
184+ torch/torchair version combinations fail during the torchair package init (for example a missing
185+ ``hint_int`` in ``torch.fx``); upgrade torch_npu and torchair to matching versions before running
186+ this sample in that case.
187+- Python import order requires `import torch` / `import torch_npu` before `import pypto`; otherwise a
188+ `libc10.so` static TLS error may occur.
189+- The execute callback guarantees data visibility with device synchronization, so the per-iteration
190+ latency (about 4 ms) is higher than the Ascend C + AnnotatedArgs path (see
191+ `annotated_args_refresh_add_custom`, about 0.4 ms). This sample focuses on wiring the path, not
192+ on performance.
193+ 
194+## Appendix
195+ 
196+### Operator Specification
197+ 
198+| Item | Content |
199+| --- | --- |
200+| Operator type | `PyptoAddCustom` |
201+| Inputs | `x`, `y` |
202+| Output | `z` |
203+| Input shape | `8192` |
204+| Output shape | `8192` |
205+| Data type | `float32` |
206+| Format | `ND` |
207+| Kernel name | `pypto_add_kernel` (PyPTO JIT) |
208+| Tile size | `1024` |
209+ 
210+### Key Interfaces
211+ 
212+| Interface | Purpose |
213+| --- | --- |
214+| `ge.custom_op.register_op` + `infer_meta` | Prototype registration and output shape/dtype inference |
215+| `ge.custom_op.register_op_impl` + `execute` | Allocate the output tensor and run the PyPTO kernel |
216+| `ge.custom_op.get_execute_ctx` | Access the execute callback context (output allocation) |
217+| `pypto.jit` + `pypto.set_vec_tile_shapes` | PyPTO kernel definition and tile configuration |
218+| `torchair as_torch_tensors` | Zero-copy bridge from GE device memory to torch tensors |
@@ -0,0 +1,15 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include "add_custom.h"
12+ 
13+// Keep this translation unit as the OPP proto library input for gen_esb.
14+// Runtime prototype and infer-meta implementations are registered by the
15+// Python decorators in src/ge/pypto_add_custom.py.
@@ -0,0 +1,25 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#ifndef EXAMPLES_CUSTOM_OP_PYPTO_ADD_CUSTOM_PROTO_ADD_CUSTOM_H_
12+#define EXAMPLES_CUSTOM_OP_PYPTO_ADD_CUSTOM_PROTO_ADD_CUSTOM_H_
13+ 
14+#include "graph/operator_reg.h"
15+ 
16+namespace ge {
17+REG_OP(PyptoAddCustom)
18+ .INPUT(x1, "T")
19+ .INPUT(x2, "T")
20+ .OUTPUT(output0, "T")
21+ .DATATYPE(T, TensorType({DT_FLOAT}))
22+ .OP_END_FACTORY_REG(PyptoAddCustom);
23+} // namespace ge
24+ 
25+#endif // EXAMPLES_CUSTOM_OP_PYPTO_ADD_CUSTOM_PROTO_ADD_CUSTOM_H_
@@ -0,0 +1,62 @@
1+#!/usr/bin/env bash
2+# -----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+ 
12+set -euo pipefail
13+ 
14+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+BUILD_DIR="${SCRIPT_DIR}/build"
16+PLUGIN_DIR="${SCRIPT_DIR}/src/ge"
17+OPP_ROOT="${BUILD_DIR}/opp"
18+ES_LIB_DIR="${BUILD_DIR}/es_output/lib64"
19+ES_PYTHON_PACKAGE_DIR="${BUILD_DIR}/es_custom_build/python_package"
20+LOG_FILE="${BUILD_DIR}/online_pypto.log"
21+ 
22+require_command() {
23+ if ! command -v "$1" >/dev/null 2>&1; then
24+ echo "[ERROR] required command not found: $1" >&2
25+ exit 1
26+ fi
27+}
28+ 
29+if [[ -z "${ASCEND_HOME_PATH:-}" || ! -d "${ASCEND_HOME_PATH}" ]]; then
30+ echo "[ERROR] ASCEND_HOME_PATH is empty. Please source CANN set_env.sh first." >&2
31+ exit 1
32+fi
33+for command_name in cmake python3; do
34+ require_command "${command_name}"
35+done
36+ 
37+mkdir -p "${BUILD_DIR}"
38+ 
39+echo "[INFO] configure and build the custom OPP package and the ES Python API"
40+cmake -S "${SCRIPT_DIR}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release
41+cmake --build "${BUILD_DIR}" --target build_es_custom -j"$(nproc 2>/dev/null || echo 8)"
42+ 
43+if ! find "${OPP_ROOT}/op_graph/lib" -name libcust_opapi.so -print -quit 2>/dev/null | grep -q .; then
44+ echo "[ERROR] OPP library was not generated under ${OPP_ROOT}/op_graph/lib" >&2
45+ exit 1
46+fi
47+if [[ ! -s "${ES_PYTHON_PACKAGE_DIR}/es_custom/__init__.py" || ! -s "${ES_LIB_DIR}/libes_custom.so" ]]; then
48+ echo "[ERROR] ES Python package or ES shared library was not generated" >&2
49+ exit 1
50+fi
51+ 
52+# The OPP root is for the C++ proto; the plugin directory is for the Python
53+# custom-op loader. Both are required by GE's two loaders.
54+export ASCEND_CUSTOM_OPP_PATH="${OPP_ROOT}:${PLUGIN_DIR}${ASCEND_CUSTOM_OPP_PATH:+:${ASCEND_CUSTOM_OPP_PATH}}"
55+export PYTHONPATH="${ES_PYTHON_PACKAGE_DIR}:${SCRIPT_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}"
56+export LD_LIBRARY_PATH="${ES_LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
57+ 
58+echo "[INFO] run the PyPTO Add custom operator on NPU through the online GE graph"
59+DEVICE_ID="${DEVICE_ID:-0}" python3 "${SCRIPT_DIR}/src/run.py" 2>&1 | tee "${LOG_FILE}"
60+grep -Fq "PyptoAddCustom precision check PASS" "${LOG_FILE}"
61+grep -Fq "NPU_EXECUTION=PASS" "${LOG_FILE}"
62+echo "[INFO] Online Python PyPTO custom-op pipeline PASS"
@@ -0,0 +1,70 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# -----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# -----------------------------------------------------------------------------------------------------------
12+ 
13+"""GE custom operator whose execute callback runs a PyPTO JIT kernel.
14+ 
15+The GE tensors are exposed to PyPTO as zero-copy torch tensors through
16+the torchair bridge in src/tensor_bridge.py. PyPTO always launches on the
17+current torch stream, so the callback synchronizes the device around the
18+launch to keep the kernel ordered with the other tasks of the same GE
19+graph.
20+ 
21+The kernel itself is compiled by the runner before GE starts (see
22+src/run.py); the execute callback only consumes the in-process PyPTO
23+compilation cache, because compiling inside the GE runtime is unsafe.
24+"""
25+ 
26+import torch_npu
27+ 
28+from tensor_bridge import to_torch
29+from pypto_add_kernel import pypto_add_kernel
30+ 
31+from ge.custom_op import get_execute_ctx, register_op, register_op_impl
32+from ge.runtime import Tensor, TensorDesc
33+ 
34+ 
35+def _launch_kernel(kernel, tensors):
36+ """Launch the compiled PyPTO kernel with device-wide ordering.
37+ 
38+ PyPTO always launches on the current torch stream, so the callback
39+ synchronizes the device before the launch (graph inputs become
40+ visible) and after the launch (the kernel finishes before the graph
41+ reads the output). The kernel itself must already be compiled; see
42+ the warm-up in src/run.py.
43+ """
44+ torch_npu.npu.synchronize()
45+ kernel(*tensors)
46+ torch_npu.npu.synchronize()
47+ 
48+ 
49+@register_op(op_type="PyptoAddCustom")
50+def pypto_add_infer_meta(x1: TensorDesc, x2: TensorDesc) -> TensorDesc:
51+ """Infer output metadata for the PyPTO Add operator."""
52+ 
53+ if x1.shape.origin_shape.dims != x2.shape.origin_shape.dims:
54+ raise ValueError("PyptoAddCustom inputs must have the same shape")
55+ if x1.data_type != x2.data_type:
56+ raise ValueError("PyptoAddCustom inputs must have the same data type")
57+ return TensorDesc(x1.shape, x1.data_type)
58+ 
59+ 
60+@register_op_impl(op_type="PyptoAddCustom")
61+class PyptoAddCustom:
62+ """Allocate the output tensor and run the PyPTO kernel for GE."""
63+ 
64+ def execute(self, x: Tensor, y: Tensor) -> None:
65+ ctx = get_execute_ctx()
66+ z = ctx.malloc_output_tensor(0, x.shape, x.format, x.data_type)
67+ x_torch = to_torch(x)
68+ y_torch = to_torch(y)
69+ z_torch = to_torch(z)
70+ _launch_kernel(pypto_add_kernel, (x_torch, y_torch, z_torch))
@@ -0,0 +1,30 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# -----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# -----------------------------------------------------------------------------------------------------------
12+ 
13+"""Shared PyPTO Add kernel used by the direct test and the GE custom op."""
14+ 
15+import pypto
16+ 
17+NUM_ELEMENTS = 8 * 1024
18+TILE_SIZE = 1024
19+ 
20+ 
21+@pypto.jit(new_ir=False)
22+def pypto_add_kernel(
23+ x: pypto.Tensor([NUM_ELEMENTS], pypto.DT_FP32),
24+ y: pypto.Tensor([NUM_ELEMENTS], pypto.DT_FP32),
25+ out: pypto.Tensor([NUM_ELEMENTS], pypto.DT_FP32),
26+):
27+ # A one-dimensional tensor needs an explicit vector tile shape whose last
28+ # dimension is 32-byte aligned.
29+ pypto.set_vec_tile_shapes(1, TILE_SIZE)
30+ out.move(pypto.add(x, y))
@@ -0,0 +1,184 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# -----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# -----------------------------------------------------------------------------------------------------------
12+ 
13+"""Run the PyPTO Add custom operator inside an online GE graph."""
14+ 
15+import os
16+import time
17+import traceback
18+ 
19+# torch must be imported before the GE libraries so that its dynamic
20+# libraries are loaded with enough static TLS slots. Importing torch_npu,
21+# pypto, and the torchair bridge in the main thread as well keeps the
22+# plugin load inside GEInitialize lightweight (sys.modules cache hits) and
23+# loads the torchair extension before GE starts, which is required because
24+# loading the shared library in the middle of GE initialization disturbs
25+# the ACL context binding of the loading thread.
26+import torch
27+import torch_npu
28+ 
29+from pypto_add_kernel import NUM_ELEMENTS, pypto_add_kernel
30+import tensor_bridge
31+ 
32+from es_custom import PyptoAddCustom
33+from ge.es.graph_builder import GraphBuilder
34+from ge.ge_global import GeApi
35+from ge.graph import Tensor
36+from ge.graph.types import DataType, Format, Placement
37+from ge.session import Session
38+ 
39+GRAPH_ID = 0
40+DEVICE_ID = int(os.environ.get("DEVICE_ID", "0"))
41+WARMUP_ITERS = 5
42+BENCHMARK_ITERS = 100
43+ 
44+ 
45+def warmup_kernel():
46+ """Compile the PyPTO kernel before GE starts.
47+ 
48+ The PyPTO compilation cache is process-wide and keyed by kernel source,
49+ so the execute callback hits this cache and never compiles inside the
50+ GE runtime, where spawning the compiler subprocess is unsafe.
51+ """
52+ if not torch_npu.npu.is_available():
53+ raise RuntimeError("NPU is not available!")
54+ # tensor_bridge is imported at module level so that the torchair
55+ # extension is loaded before GE starts.
56+ assert hasattr(tensor_bridge, "to_torch"), "tensor bridge is unavailable"
57+ x = torch.empty(NUM_ELEMENTS, dtype=torch.float32, device="npu")
58+ y = torch.empty_like(x)
59+ out = torch.empty_like(x)
60+ pypto_add_kernel(x, y, out)
61+ torch.npu.synchronize()
62+ print("[OnlinePython] PyPTO kernel warm-up PASS")
63+ 
64+ 
65+def build_graph():
66+ builder = GraphBuilder("python_pypto_add_graph")
67+ input_x = builder.create_input(
68+ index=0,
69+ name="data_x",
70+ data_type=DataType.DT_FLOAT,
71+ format=Format.FORMAT_ND,
72+ shape=[NUM_ELEMENTS],
73+ )
74+ input_y = builder.create_input(
75+ index=1,
76+ name="data_y",
77+ data_type=DataType.DT_FLOAT,
78+ format=Format.FORMAT_ND,
79+ shape=[NUM_ELEMENTS],
80+ )
81+ output0 = PyptoAddCustom(input_x, input_y)
82+ builder.set_graph_output(output0, 0)
83+ return builder.build_and_reset()
84+ 
85+ 
86+def build_input_data(start, step):
87+ return [start + float(index) * step for index in range(NUM_ELEMENTS)]
88+ 
89+ 
90+def build_device_tensor(data):
91+ return Tensor(
92+ data,
93+ None,
94+ DataType.DT_FLOAT,
95+ Format.FORMAT_ND,
96+ [NUM_ELEMENTS],
97+ Placement.PLACEMENT_DEVICE,
98+ )
99+ 
100+ 
101+def build_input_sets():
102+ values = (
103+ (build_input_data(1.0, 1.0), build_input_data(10.0, 0.5)),
104+ (build_input_data(3.0, 2.0), build_input_data(20.0, 0.25)),
105+ )
106+ return [([build_device_tensor(x), build_device_tensor(y)], x, y) for x, y in values]
107+ 
108+ 
109+def validate_graph(session, input_sets):
110+ for inputs, values_x, values_y in input_sets:
111+ outputs = session.run_graph(GRAPH_ID, inputs)
112+ if len(outputs) != 1:
113+ raise RuntimeError(
114+ "PyptoAddCustom returned {} outputs".format(len(outputs))
115+ )
116+ actual = outputs[0].data
117+ expected = [x + y for x, y in zip(values_x, values_y)]
118+ max_error = max(abs(value - golden) for value, golden in zip(actual, expected))
119+ if max_error > 1.0e-5:
120+ raise RuntimeError(
121+ "PyptoAddCustom precision check failed, max_error={}".format(max_error)
122+ )
123+ print("[OnlinePython] PyptoAddCustom precision check PASS")
124+ 
125+ 
126+def benchmark_graph(session, input_sets):
127+ for iteration in range(WARMUP_ITERS):
128+ session.run_graph(GRAPH_ID, input_sets[iteration % len(input_sets)][0])
129+ start = time.perf_counter()
130+ for iteration in range(BENCHMARK_ITERS):
131+ session.run_graph(GRAPH_ID, input_sets[iteration % len(input_sets)][0])
132+ return (time.perf_counter() - start) * 1.0e6
133+ 
134+ 
135+def run_graph():
136+ options = {
137+ "ge.exec.deviceId": str(DEVICE_ID),
138+ "ge.graphRunMode": "1",
139+ # Force the dynamic (RT2) execution path so the Python execute
140+ # callback runs for every graph execution with real input data.
141+ # The default known-shape path records the callback once at task
142+ # generation time, which is incompatible with a PyPTO launch.
143+ "ge.exec.static_model_ops_lower_limit": "-1",
144+ }
145+ ge_api = GeApi()
146+ session = None
147+ ge_initialized = False
148+ graph_ids = []
149+ 
150+ try:
151+ ge_api.ge_initialize(options)
152+ ge_initialized = True
153+ session = Session(options)
154+ session.add_graph(GRAPH_ID, build_graph())
155+ graph_ids.append(GRAPH_ID)
156+ input_sets = build_input_sets()
157+ 
158+ validate_graph(session, input_sets)
159+ total_us = benchmark_graph(session, input_sets)
160+ avg_us = total_us / BENCHMARK_ITERS
161+ print("[Perf] input shape: [{}], dtype: float32".format(NUM_ELEMENTS))
162+ print("[Perf] iters: {}".format(BENCHMARK_ITERS))
163+ print(
164+ "[Perf] PyptoAddCustom: {:.3f} us (avg {:.3f} us/iter)".format(
165+ total_us, avg_us
166+ )
167+ )
168+ print("[OnlinePython] NPU_EXECUTION=PASS")
169+ return 0
170+ except Exception as exc:
171+ print("[OnlinePython] run_graph failed: {}".format(exc))
172+ traceback.print_exc()
173+ return 1
174+ finally:
175+ if session is not None:
176+ for graph_id in graph_ids:
177+ session.remove_graph(graph_id)
178+ session = None
179+ if ge_initialized:
180+ ge_api.ge_finalize()
181+ 
182+ 
183+if __name__ == "__main__":
184+ raise SystemExit(run_graph())
@@ -0,0 +1,50 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# -----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# -----------------------------------------------------------------------------------------------------------
12+ 
13+"""Zero-copy bridge from GE device tensors to torch tensors.
14+ 
15+Reuses the torchair ``create_npu_tensors`` entry (the same machinery
16+behind torchair's Python custom-op callbacks) to wrap raw NPU addresses
17+as torch tensors. The device memory stays owned by GE.
18+ 
19+The host must import torch and torch_npu before importing this module
20+so that the npu backend is registered with enough static TLS slots.
21+"""
22+ 
23+import torch
24+ 
25+from torchair.llm_datadist import create_npu_tensors
26+ 
27+from ge.graph.types import DataType
28+from ge.runtime import Tensor
29+ 
30+# torch dtypes keyed by GE data types.
31+_GE_TO_TORCH_DTYPE = {DataType.DT_FLOAT: torch.float32}
32+ 
33+ 
34+def to_torch(tensor: Tensor):
35+ """Wrap a GE device tensor as a zero-copy torch tensor."""
36+ dtype = _GE_TO_TORCH_DTYPE.get(tensor.data_type)
37+ if dtype is None:
38+ raise ValueError(
39+ "unsupported GE data type for the tensor bridge: {}".format(
40+ tensor.data_type
41+ )
42+ )
43+ addr = tensor.addr
44+ if addr == 0:
45+ raise RuntimeError("to_torch: tensor.addr is 0 (null)")
46+ shape = list(tensor.storage_shape.dims)
47+ tensors = create_npu_tensors(shape, dtype, [int(addr)])
48+ if not tensors:
49+ raise RuntimeError("to_torch: create_npu_tensors returned empty")
50+ return tensors[0]