已合并
refactor(examples): Rewrite the basic example #4922
lidongsheng创建于 7月16日
refactor(examples): Rewrite the basic example #4922
已合并
lidongsheng创建于 7月16日
11 个文件变更+184-3020
Mexamples/00_hello_world/README.md+2-4
@@ -5,7 +5,8 @@
5## 总览介绍5## 总览介绍
6 6 
7本样例通过一个简单的张量加法演示了 PyPTO 的完整开发流程:7本样例通过一个简单的张量加法演示了 PyPTO 的完整开发流程:
8-- 使用 `@pypto.frontend.jit` 定义内核函数。8+ 
9+- 使用 `@pypto.jit` 定义内核函数。
9- 通过 PyTorch 张量创建输入数据。10- 通过 PyTorch 张量创建输入数据。
10- 调用 JIT 内核执行计算并验证结果。11- 调用 JIT 内核执行计算并验证结果。
11 12 
@@ -24,9 +25,6 @@
24 25 
25# 默认路径安装,以root用户为例(非root用户,将/usr/local替换为${HOME})26# 默认路径安装,以root用户为例(非root用户,将/usr/local替换为${HOME})
26source /usr/local/Ascend/ascend-toolkit/set_env.sh27source /usr/local/Ascend/ascend-toolkit/set_env.sh
27- 
28-# 设置设备 ID
29-export TILE_FWK_DEVICE_ID=0
30```28```
31 29 
32### 执行脚本30### 执行脚本
Mexamples/00_hello_world/README_en.md+2-3
@@ -5,7 +5,8 @@ This sample demonstrates the simplest tensor addition operation in PyPTO. It is
5## Overview5## Overview
6 6 
7This sample demonstrates the complete PyPTO development process through a simple tensor addition:7This sample demonstrates the complete PyPTO development process through a simple tensor addition:
8-- Define a kernel function using `@pypto.frontend.jit`.8+ 
9+- Define a kernel function using `@pypto.jit`.
9- Create input data through PyTorch tensors.10- Create input data through PyTorch tensors.
10- Call the JIT kernel to execute the computation and verify the result.11- Call the JIT kernel to execute the computation and verify the result.
11 12 
@@ -25,8 +26,6 @@ This sample demonstrates the complete PyPTO development process through a simple
25# Default path installation, using root user as a sample (for non-root users, replace /usr/local with ${HOME})26# Default path installation, using root user as a sample (for non-root users, replace /usr/local with ${HOME})
26source /usr/local/Ascend/ascend-toolkit/set_env.sh27source /usr/local/Ascend/ascend-toolkit/set_env.sh
27 28 
28-# Set device ID
29-export TILE_FWK_DEVICE_ID=0
30```29```
31 30 
32### Execute the Script31### Execute the Script
Mexamples/00_hello_world/hello_world.py+37-160
@@ -10,192 +10,69 @@
10# -----------------------------------------------------------------------------------------------------------10# -----------------------------------------------------------------------------------------------------------
11"""11"""
12Hello World Example for PyPTO12Hello World Example for PyPTO
13- 
14-This example demonstrates the simplest tensor addition.
15"""13"""
14+ 
16import os15import os
17import sys16import sys
18import argparse17import argparse
19import pypto18import pypto
20import torch19import torch
21-import numpy as np20+ 
22-from numpy.testing import assert_allclose21+runtime_options = {}
23 22 
24 23 
25-def get_device_id():24+@pypto.jit(runtime_options=runtime_options)
26- """25+def add_kernel(x: pypto.Tensor[...], y: pypto.Tensor[...], out: pypto.Tensor[...]):
27- Get and validate TILE_FWK_DEVICE_ID from environment variable.26+ """Simple add kernel, use pypto.Tensor[...] to auto infer shape and dtype."""
28 27 
29- Returns:28+ # set vector tile shapes, it'll use by the following `vector` operations,
30- int: The device ID if valid, None otherwise.29+ # so the rank must match tensor `x` and `y`
31- """30+ pypto.set_vec_tile_shapes(32, 32)
32- if 'TILE_FWK_DEVICE_ID' not in os.environ:
33- print("If no NPU environment is available, set --run_mode sim to run in simulation mode;")
34- print("otherwise, set the environment variable TILE_FWK_DEVICE_ID.")
35- print("Please set it before running this example:")
36- print(" export TILE_FWK_DEVICE_ID=0")
37- return None
38 31 
39- try:32+ # pypto kernel does not support return value, `[:]` is just a syntax sugar to present
40- device_id = int(os.environ['TILE_FWK_DEVICE_ID'])33+ # write to output tensor, can also use `pypto.assemble(x + y, [0, 0], out)`
41- return device_id34+ out[:] = x + y
42- except ValueError:
43- print(f"ERROR: TILE_FWK_DEVICE_ID must be an integer, got: {os.environ['TILE_FWK_DEVICE_ID']}")
44- return None
45 35 
46 36 
47-def create_add_kernel(shape: tuple, run_mode: str = "npu"):37+def device_init(run_mode):
48- 38+ if run_mode == "sim":
49- if run_mode == "npu":39+ runtime_options["run_mode"] = pypto.RunMode.SIM
50- mode = pypto.RunMode.NPU40+ return "cpu"
51- elif run_mode == "sim":
52- mode = pypto.RunMode.SIM
53 else:41 else:
54- raise ValueError(f"Invalid run_mode: {run_mode}. Must be 'npu' or 'sim'")42+ try:
43+ import torch_npu
44+ except ImportError:
45+ print("torch_npu is not installed, please install it first")
46+ sys.exit(1)
55 47 
56- @pypto.frontend.jit(runtime_options={"run_mode": mode})48+ device_id = int(os.environ.get("TILE_FWK_DEVICE_ID", 0))
57- def add_kernel(49+ torch.npu.set_device(device_id)
58- x: pypto.Tensor([...], pypto.DT_FP32),
59- y: pypto.Tensor([...], pypto.DT_FP32),
60- out: pypto.Tensor([...], pypto.DT_FP32),
61- ):
62- pypto.set_vec_tile_shapes(1, 4, 1, 64)
63- out[:] = x + y
64 50 
65- return add_kernel51+ runtime_options["run_mode"] = pypto.RunMode.NPU
66- 52+ return f"npu:{device_id}"
67- 
68-def test_add_direct(device_id=None, run_mode: str = "npu") -> None:
69- device = f'npu:{device_id}' if (run_mode == "npu" and device_id is not None) else 'cpu'
70- shape = (1, 4, 1, 64)
71- #prepare data
72- input_data0 = torch.rand(shape, dtype=torch.float, device=device)
73- input_data1 = torch.rand(shape, dtype=torch.float, device=device)
74- 
75- output_data = torch.empty(shape, dtype=torch.float32, device=device)
76- create_add_kernel(shape, run_mode)(input_data0, input_data1, output_data)
77- 
78- golden = torch.add(input_data0, input_data1)
79- 
80- max_diff = np.abs(output_data.cpu().numpy() - golden.cpu().numpy()).max()
81- print(f"Input0 shape: {input_data0.shape}")
82- print(f"Input1 shape: {input_data1.shape}")
83- print(f"Output shape: {output_data.shape}")
84- 
85- if run_mode == "npu":
86- print(f"Max difference: {max_diff:.6f}")
87- assert_allclose(np.array(output_data.cpu()), np.array(golden.cpu()), rtol=3e-3, atol=3e-3)
88- print("✓ Hello world example passed")
89- print()
90 53 
91 54 
92def main():55def main():
93- """Run hello_world example.56+ parser = argparse.ArgumentParser(description="PyPTO add kernel")
94- 
95- Usage:
96- python hello_world.py # Run example
97- python hello_world.py --list # List available examples
98- """
99- parser = argparse.ArgumentParser(
100- description="PyPTO hello_world Example",
101- formatter_class=argparse.RawDescriptionHelpFormatter,
102- epilog="""
103-Examples:
104- %(prog)s hello_world::test_add_direct
105- Run the hello_world::test_add_direct example
106- %(prog)s --list List all available examples
107- """
108- )
109 parser.add_argument(57 parser.add_argument(
110- 'example_id',58+ "-m",
111- type=str,59+ "--run_mode",
112- nargs='?',
113- help='Example ID to run (1). If not specified, the example will run.'
114- )
115- parser.add_argument(
116- '--list',
117- action='store_true',
118- help='List all available examples and exit'
119- )
120- parser.add_argument(
121- '--run_mode',
122- type=str,
123- nargs='?',
124- default="npu",
125 choices=["npu", "sim"],60 choices=["npu", "sim"],
126- help='Run mode, such as npu/sim etc.'61+ default="npu",
62+ help="Execution mode (default: npu)",
127 )63 )
128- 
129 args = parser.parse_args()64 args = parser.parse_args()
130 65 
131- # Define available examples66+ shape = (64, 64)
132- examples = {67+ device = device_init(args.run_mode)
133- "hello_world::test_add_direct": {
134- 'name': 'hello_world',
135- 'description': 'add_direct implementation',
136- 'function': test_add_direct
137- }
138- }
139 68 
140- # List examples if requested69+ x = torch.randn(shape, dtype=torch.float, device=device)
141- if args.list:70+ y = torch.randn(shape, dtype=torch.float, device=device)
142- print("\n" + "=" * 60)71+ out = torch.empty(shape, dtype=torch.float, device=device)
143- print("Available Examples")
144- print("=" * 60 + "\n")
145- for ex_id, ex_info in sorted(examples.items()):
146- print(f" ID: {ex_id}")
147- print(f" name: {ex_info['name']}")
148- print(f" description: {ex_info['description']}\n")
149- return
150 72 
151- # Validate example ID if provided73+ add_kernel(x, y, out)
152- if args.example_id is not None:
153- if args.example_id not in examples:
154- print(f"ERROR: Invalid example ID: {args.example_id}")
155- print(f"Valid example IDs are: {', '.join(map(str, sorted(examples.keys())))}")
156- print("\nUse --list to see all available examples.")
157- sys.exit(1)
158 74 
159- print("\n" + "=" * 60)75+ torch.testing.assert_close(x + y, out, atol=1e-3, rtol=1e-3)
160- print("PyPTO hello_world Example")
161- print("=" * 60 + "\n")
162- 
163- # Get and validate device ID (needed for NPU examples)
164- device_id = None
165- examples_to_run = []
166- 
167- if args.example_id is not None:
168- # Run single example
169- example = examples.get(args.example_id)
170- if example is None:
171- raise ValueError(f"Invalid example ID: {args.example_id}")
172- examples_to_run = [(args.example_id, example)]
173- else:
174- # Run all examples
175- examples_to_run = list(examples.items())
176- 
177- if args.run_mode == "npu":
178- device_id = get_device_id()
179- if device_id is None:
180- return
181- import torch_npu
182- torch.npu.set_device(device_id)
183- print("Running examples that require NPU hardware...")
184- print("(Make sure CANN environment is configured and NPU is available)\n")
185- 
186- try:
187- for ex_id, ex_info in examples_to_run:
188- print(f"Running Example {ex_id}: {ex_info['name']}")
189- ex_info['function'](device_id, args.run_mode)
190- 
191- if len(examples_to_run) > 1:
192- print("=" * 60)
193- print("All hello_world tests passed!")
194- print("=" * 60)
195- 
196- except Exception as e:
197- print(f"\nError: {e}")
198- raise
199 76 
200 77 
201if __name__ == "__main__":78if __name__ == "__main__":
Mexamples/01_beginner/basic/README.md+9-54
@@ -2,37 +2,24 @@
2 2 
3本样例展示了 PyPTO 的基础算子操作和典型的编程模式,非常适合初学者快速上手。3本样例展示了 PyPTO 的基础算子操作和典型的编程模式,非常适合初学者快速上手。
4 4 
5-## 总览介绍
6- 
7-本样例涵盖了 PyPTO 编程中的核心概念,包括:
8-- 张量的创建与属性访问。
9-- 基础的逐元素算术运算(加法、标量乘法等)。
10-- 矩阵乘法(Matmul)操作。
11-- 归约运算(如 Sum)。
12-- Tiling(分块)配置(Vec / Cube Tile Shapes)。
13-- 变换操作(View + Assemble)。
14- 
15## 样例代码特性5## 样例代码特性
16 6 
17-本样例突出了 PyPTO 下特性:7+- **JIT 编译**: 使用 `@pypto.jit` 装饰器定义可在 NPU 上执行的内核函数。
18-- **JIT 编译**: 使用 `@pypto.frontend.jit` 装饰器定义可以在 NPU 上执行的内核函数。
19- **PyTorch 集成**: 能够直接接受 PyTorch NPU 张量作为输入输出,无缝衔接现有深度学习工作流。8- **PyTorch 集成**: 能够直接接受 PyTorch NPU 张量作为输入输出,无缝衔接现有深度学习工作流。
20- **显式 Tiling 控制**: 通过 `set_vec_tile_shapes``set_cube_tile_shapes` 手动优化硬件执行效率。9- **显式 Tiling 控制**: 通过 `set_vec_tile_shapes``set_cube_tile_shapes` 手动优化硬件执行效率。
21-- **符号化张量**: 即使内核函数之外,也可以定义张量的形状和类型10+- **动态Shape**: 支持运行时动态调整张量的形状,无需在编译时就确定
22 11 
23## 代码结构12## 代码结构
24 13 
25- **`basic_ops.py`**: 包含所有示例的主脚本,作为快速上手的总览入口。14- **`basic_ops.py`**: 包含所有示例的主脚本,作为快速上手的总览入口。
26- - `test_tensor_creation()`: 示例 1 - 张量创建15+ - `test_add()`: 示例 1 - 加法运算
27- - `test_elementwise_ops()`: 示例 2 - 逐元素运算(加法、标量乘法)。16+ - `test_erfc()`: 示例 2 - 逐元素运算(ERFC)。
28 - `test_matmul()`: 示例 3 - 矩阵乘法。17 - `test_matmul()`: 示例 3 - 矩阵乘法。
29- - `test_reduce_ops()`: 示例 4 - 归约运算(Sum)。18+ - `test_sum()`: 示例 4 - 归约运算(Sum)。
30- - `test_tiling_config()`: 示例 5 - Tiling 配置。19+ - `test_dynamic_add()`: 示例 5 - 动态Shape
31- - `test_transform_ops()`: 示例 6 - 变换操作(View + Assemble)。
32-- **`tensor_creation.py`**: 张量创建操作示例,包含 Arange、Full、数据类型等创建方法。
33-- **`symbolic_scalar.py`**: 符号标量(Symbolic Scalar)的使用示例。
34 20 
35更详细的各类算子用法,请参考同级目录:21更详细的各类算子用法,请参考同级目录:
22+ 
36- `../compute/`: 逐元素算子、矩阵乘法、归约算子。23- `../compute/`: 逐元素算子、矩阵乘法、归约算子。
37- `../tiling/`: Tiling 配置策略。24- `../tiling/`: Tiling 配置策略。
38- `../transform/`: 变换算子。25- `../transform/`: 变换算子。
@@ -48,9 +35,6 @@
48 35 
49# 默认路径安装,以root用户为例(非root用户,将/usr/local替换为${HOME})36# 默认路径安装,以root用户为例(非root用户,将/usr/local替换为${HOME})
50source /usr/local/Ascend/ascend-toolkit/set_env.sh37source /usr/local/Ascend/ascend-toolkit/set_env.sh
51- 
52-# 设置设备 ID
53-export TILE_FWK_DEVICE_ID=0
54```38```
55 39 
56### 执行脚本40### 执行脚本
@@ -59,40 +43,11 @@ export TILE_FWK_DEVICE_ID=0
59# 运行所有示例43# 运行所有示例
60python3 basic_ops.py44python3 basic_ops.py
61 45 
62-# 运行特定示例(例如示例 2逐元素运算)46+# 运行特定示例(例如示例 1加法运算)
63-python3 basic_ops.py elementwise_ops::test_elementwise_ops47+python3 basic_ops.py -t add
atomgit-bot
atomgit-botatomgit-bot7月16日

🟡 Medium Priority

README.md(中文)第 45-46 行,"运行特定示例(例如示例 2:逐元素运算)" 后面的命令是 python3 basic_ops.py -t add。但在 basic_ops.py 中,test_add 对应"示例 1 - 加法运算",而"示例 2:逐元素运算(ERFC)"对应的是 test_erfc,应使用 -t erfc。同样的问题存在于 README_en.md 第 45-46 行:"Run a specific sample (for example, Sample 2: Element-wise operation)" 后跟 -t add。用户按此命令操作会运行错误的示例,造成混淆。

触发条件:用户按 README 指引执行 python3 basic_ops.py -t add 期望运行"逐元素运算"示例时,实际运行的是加法示例。

建议:将中文 README 第 46 行 python3 basic_ops.py -t add 改为 python3 basic_ops.py -t erfc,同时英文 README 对应行也做相同修改;或改为更准确的描述如"(例如示例 1:加法运算)"。

改动建议
47
- python3 basic_ops.py -t add
47
+ python3 basic_ops.py -t erfc
应用建议
likedislike
64- 
65-# 查看所有可用示例列表
66-python3 basic_ops.py --list
67-```
68- 
69-## 关键代码段解析
70- 
71-### 1. JIT 函数定义与 Tiling 配置
72- 
73-```python
74-@pypto.frontend.jit()
75-def elementwise_kernel(
76- a: pypto.Tensor(shape, pypto.DT_FP16),
77- b: pypto.Tensor(shape, pypto.DT_FP16),
78- out: pypto.Tensor(shape, pypto.DT_FP16)
79-):
80- # 设置向量计算的分块形状
81- pypto.set_vec_tile_shapes(8, 8)
82- # 算子组合
83- out[:] = pypto.mul(pypto.add(a, b), 2.0)
84- 
85-```
86- 
87-### 2. 执行JIT函数
88- 
89-```python
90-# 执行 JIT 函数
91-result = elementwise_kernel(a, b)
92```48```
93 49 
94## 注意事项50## 注意事项
95 51 
96-- **数据类型**: 昇腾 NPU 对 FP16 和 BF16 有原生硬件加速,建议在算子开发中优先考虑这些类型。
97- **分块大小**: Tiling 形状的选择会显著影响算子性能,通常应根据 NPU 架构的向量/矩阵计算单元大小来设定。52- **分块大小**: Tiling 形状的选择会显著影响算子性能,通常应根据 NPU 架构的向量/矩阵计算单元大小来设定。
98- **环境**: 确保 `torch_npu` 已正确安装并能识别到昇腾显卡。53- **环境**: 确保 `torch_npu` 已正确安装并能识别到昇腾显卡。
Mexamples/01_beginner/basic/README_en.md+10-55
@@ -2,37 +2,24 @@
2 2 
3This sample demonstrates the basic operator operations and typical programming patterns of PyPTO. It is very suitable for beginners to quickly get started.3This sample demonstrates the basic operator operations and typical programming patterns of PyPTO. It is very suitable for beginners to quickly get started.
4 4 
5-## Overview
6- 
7-This sample covers the core concepts in PyPTO programming, including:
8-- Tensor creation and attribute access.
9-- Basic element-wise arithmetic operations (addition, scalar multiplication, and so on).
10-- Matrix multiplication (Matmul) operations.
11-- Reduction operations (such as Sum).
12-- Tiling configuration (Vec and Cube Tile Shapes).
13-- Transform operations (View and Assemble).
14- 
15## Sample Code Features5## Sample Code Features
16 6 
17-This sample highlights the following features of PyPTO:7+- **JIT Compilation**: Use the `@pypto.jit` decorator to define kernel functions that execute on the NPU.
18-- **JIT Compilation**: Use the `@pypto.frontend.jit` decorator to define kernel functions that execute on the NPU.
19- **PyTorch Integration**: Directly accept PyTorch NPU tensors as input and output, seamlessly integrating with existing deep learning workflows.8- **PyTorch Integration**: Directly accept PyTorch NPU tensors as input and output, seamlessly integrating with existing deep learning workflows.
20- **Explicit Tiling Control**: Manually optimize hardware execution efficiency through `set_vec_tile_shapes` and `set_cube_tile_shapes`.9- **Explicit Tiling Control**: Manually optimize hardware execution efficiency through `set_vec_tile_shapes` and `set_cube_tile_shapes`.
21-- **Symbolic Tensors**: Define tensor shapes and types even outside kernel functions.10+- **Dynamic Shape**: Supports adjusting tensor shapes dynamically at runtime, without having to determine them at compile time.
22 11 
23## Code Structure12## Code Structure
24 13 
25- **`basic_ops.py`**: Main script containing all samples, serving as the entry point for a quick overview.14- **`basic_ops.py`**: Main script containing all samples, serving as the entry point for a quick overview.
26- - `test_tensor_creation()`: Sample 1 - Tensor creation.15+ - `test_add()`: Sample 1 - Addition.
27- - `test_elementwise_ops()`: Sample 2 - Element-wise operations (addition, scalar multiplication).16+ - `test_erfc()`: Sample 2 - Element-wise operation (ERFC).
28 - `test_matmul()`: Sample 3 - Matrix multiplication.17 - `test_matmul()`: Sample 3 - Matrix multiplication.
29- - `test_reduce_ops()`: Sample 4 - Reduction operations (Sum).18+ - `test_sum()`: Sample 4 - Reduction operation (Sum).
30- - `test_tiling_config()`: Sample 5 - Tiling configuration.19+ - `test_dynamic_add()`: Sample 5 - Dynamic Shape.
31- - `test_transform_ops()`: Sample 6 - Transform operations (View and Assemble).
32-- **`tensor_creation.py`**: Tensor creation operation samples, including creation methods such as Arange, Full, and data types.
33-- **`symbolic_scalar.py`**: Sample demonstrating the use of Symbolic Scalar.
34 20 
35For more detailed usage of various operators, refer to the sibling directories:21For more detailed usage of various operators, refer to the sibling directories:
22+ 
36- `../compute/`: Element-wise operators, matrix multiplication, reduction operators.23- `../compute/`: Element-wise operators, matrix multiplication, reduction operators.
37- `../tiling/`: Tiling configuration strategies.24- `../tiling/`: Tiling configuration strategies.
38- `../transform/`: Transform operators.25- `../transform/`: Transform operators.
@@ -48,9 +35,6 @@ For more detailed usage of various operators, refer to the sibling directories:
48 35 
49# Default path installation, using root user as a sample (for non-root users, replace /usr/local with ${HOME})36# Default path installation, using root user as a sample (for non-root users, replace /usr/local with ${HOME})
50source /usr/local/Ascend/ascend-toolkit/set_env.sh37source /usr/local/Ascend/ascend-toolkit/set_env.sh
51- 
52-# Set device ID
53-export TILE_FWK_DEVICE_ID=0
54```38```
55 39 
56### Execute the Script40### Execute the Script
@@ -59,40 +43,11 @@ export TILE_FWK_DEVICE_ID=0
59# Run all samples43# Run all samples
60python3 basic_ops.py44python3 basic_ops.py
61 45 
62-# Run a specific sample (for example, Sample 2: Element-wise operations)46+# Run a specific sample (for example, Sample 1: Addition)
63-python3 basic_ops.py elementwise_ops::test_elementwise_ops47+python3 basic_ops.py -t add
atomgit-bot
atomgit-botatomgit-bot7月16日

🟡 Medium Priority

同样的问题:英文 README 第 45-46 行描述 "Run a specific sample (for example, Sample 2: Element-wise operation)" 但命令为 python3 basic_ops.py -t addadd 对应 Sample 1 (Addition),Sample 2 应是 erfc

建议:将 python3 basic_ops.py -t add 改为 python3 basic_ops.py -t erfc,或将描述改为对应 Sample 1。

改动建议
47
- python3 basic_ops.py -t add
47
+ python3 basic_ops.py -t erfc
应用建议
likedislike
64- 
65-# View the list of all available samples
66-python3 basic_ops.py --list
67-```
68- 
69-## Key Code Analysis
70- 
71-### 1. JIT Function Definition and Tiling Configuration
72- 
73-```python
74-@pypto.frontend.jit()
75-def elementwise_kernel(
76- a: pypto.Tensor(shape, pypto.DT_FP16),
77- b: pypto.Tensor(shape, pypto.DT_FP16),
78- out: pypto.Tensor(shape, pypto.DT_FP16)
79-):
80- # Set the tile shape for vector computation
81- pypto.set_vec_tile_shapes(8, 8)
82- # Operator combination
83- out[:] = pypto.mul(pypto.add(a, b), 2.0)
84- 
85-```
86- 
87-### 2. Execute the JIT Function
88- 
89-```python
90-# Execute the JIT function
91-result = elementwise_kernel(a, b)
92```48```
93 49 
94## Precautions50## Precautions
95 51 
96-- **Data Types**: The Ascend NPU provides native hardware acceleration for FP16 and BF16. Prioritize these types in operator development.52+- **Tile Size**: The choice of Tiling shapes significantly affects operator performance. Typically, you should set them according to the vector/matrix computation unit size of the NPU architecture.
97-- **Tile Size**: The choice of Tiling shapes significantly affects operator performance. Set them based on the vector or matrix computation unit size of the NPU architecture.
98- **Environment**: Ensure that `torch_npu` is correctly installed and can recognize the Ascend GPU.53- **Environment**: Ensure that `torch_npu` is correctly installed and can recognize the Ascend GPU.
Mexamples/01_beginner/basic/basic_ops.py+122-342
@@ -10,419 +10,199 @@
10# -----------------------------------------------------------------------------------------------------------10# -----------------------------------------------------------------------------------------------------------
11"""11"""
12Basic Operations Quick-Start for PyPTO12Basic Operations Quick-Start for PyPTO
13- 
14-A concise overview of core PyPTO capabilities. Each example demonstrates one
15-key category from the beginner tutorials:
16- 
17-1. Tensor creation and properties
18-2. Element-wise operations (add, mul)
19-3. Matrix multiplication (matmul)
20-4. Reduction operations (sum)
21-5. Tiling configuration (vec / cube tile shapes)
22-6. Transform operations (view + assemble)
23- 
24-For more detailed examples of each category, see the corresponding files:
25- - basic/tensor_creation.py, basic/symbolic_scalar.py
26- - compute/elementwise_ops.py, compute/matmul_ops.py, compute/reduce_ops.py
27- - tiling/tiling_config.py
28- - transform/transform_ops.py
29"""13"""
30 14 
31-import os
32import sys15import sys
33import argparse16import argparse
34import pypto17import pypto
35import torch18import torch
36-import numpy as np
37-from numpy.testing import assert_allclose
38 19 
39 20 
40-def _peek_run_mode_from_argv(default: str = "npu") -> str:21+runtime_options = {"run_mode": pypto.RunMode.NPU}
41- """Read run_mode early so module-level decorators can use it."""
42- for idx, arg in enumerate(sys.argv):
43- if arg == "--run_mode" and idx + 1 < len(sys.argv):
44- value = sys.argv[idx + 1]
45- if value in ("npu", "sim"):
46- return value
47- if arg.startswith("--run_mode="):
48- value = arg.split("=", 1)[1]
49- if value in ("npu", "sim"):
50- return value
51- return default
52 22 
53 23 
54-global_run_mode = pypto.RunMode.NPU24+@pypto.frontend.jit(runtime_options=runtime_options)
55-if _peek_run_mode_from_argv("npu") == "sim":25+def add_kernel(
56- global_run_mode = pypto.RunMode.SIM26+ a: pypto.Tensor[[...], pypto.DT_FP16],
27+ b: pypto.Tensor[[...], pypto.DT_FP16],
28+ out: pypto.Tensor[[...], pypto.DT_FP16],
29+):
30+ pypto.set_vec_tile_shapes(32, 32)
31+ out[:] = (a + b) * 2.0
57 32 
58 33 
59-def get_device_id():34+def test_add(device):
60- """35+ shape = (64, 64)
61- Get and validate TILE_FWK_DEVICE_ID from environment variable.
62- 
63- Returns:
64- int: The device ID if valid, None otherwise.
65- """
66- if 'TILE_FWK_DEVICE_ID' not in os.environ:
67- print("Please set the environment variable TILE_FWK_DEVICE_ID before running:")
68- print(" export TILE_FWK_DEVICE_ID=0")
69- return None
70- 
71- try:
72- device_id = int(os.environ['TILE_FWK_DEVICE_ID'])
73- return device_id
74- except ValueError:
75- print(f"ERROR: TILE_FWK_DEVICE_ID must be an integer, got: {os.environ['TILE_FWK_DEVICE_ID']}")
76- return None
77- 
78- 
79-# ============================================================================
80-# 1. Tensor Creation
81-# ============================================================================
82- 
83-def test_tensor_creation(device_id=None):
84- """Demonstrate tensor creation and property access."""
85- print("=" * 60)
86- print("Example 1: Tensor Creation")
87- print("=" * 60)
88- 
89- tensor = pypto.Tensor([4, 4], pypto.DT_FP16, "my_tensor")
90- print(f" name={tensor.name}, shape={tensor.shape}, dtype={tensor.dtype}, "
91- f"format={tensor.format}, dim={tensor.dim}")
92- print("✓ Tensor creation completed successfully\n")
93- 
94- 
95-# ============================================================================
96-# 2. Element-wise Operations
97-# ============================================================================
98- 
99-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
100-def elementwise_kernel(
101- a: pypto.Tensor([], pypto.DT_FP16),
102- b: pypto.Tensor([], pypto.DT_FP16),
103- out: pypto.Tensor([], pypto.DT_FP16)):
104- pypto.set_vec_tile_shapes(8, 8)
105- out.move(pypto.mul(pypto.add(a, b), 2.0))
106- 
107- 
108-def test_elementwise_ops(device_id=None):
109- print("=" * 60)
110- print("Example 2: Element-wise Operations")
111- print("=" * 60)
112- 
113- shape = (8, 8)
114- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
115 36 
116 a = torch.randn(shape, dtype=torch.float16, device=device)37 a = torch.randn(shape, dtype=torch.float16, device=device)
117 b = torch.randn(shape, dtype=torch.float16, device=device)38 b = torch.randn(shape, dtype=torch.float16, device=device)
118 out = torch.zeros(shape, dtype=torch.float16, device=device)39 out = torch.zeros(shape, dtype=torch.float16, device=device)
119- elementwise_kernel(a, b, out)
120 40 
121- if global_run_mode == pypto.RunMode.NPU:41+ add_kernel(a, b, out)
122- expected = (a + b) * 2.042+ torch.testing.assert_close(out, (a + b) * 2.0, atol=1e-3, rtol=1e-3)
123- max_diff = (out - expected).abs().max().item()
124- print(f" Max difference: {max_diff:.6f}")
125- assert max_diff < 1e-2, "Result mismatch!"
126- print("✓ Element-wise operations completed successfully\n")
127 43 
128 44 
129-@pypto.frontend.jit(runtime_options={'run_mode': global_run_mode})45+@pypto.frontend.jit(runtime_options=runtime_options)
130def erfc_kernel(46def erfc_kernel(
131- x: pypto.Tensor([], pypto.DT_FP32),47+ x: pypto.Tensor[[...], pypto.DT_FP32], out: pypto.Tensor[[...], pypto.DT_FP32]
132- out: pypto.Tensor([], pypto.DT_FP32)):48+):
133- pypto.set_vec_tile_shapes(8, 8)49+ pypto.set_vec_tile_shapes(32, 32)
134- out.move(pypto.erfc(x))50+ out[:] = pypto.erfc(x)
135 51 
136 52 
137-def test_erfc(device_id=None):53+def test_erfc(device):
138- '''Element-wise complementary error function: out = erfc(x).'''54+ shape = (64, 64)
139- print('=' * 60)
140- print('Example 3: Mathematical Function (Erfc)')
141- print('=' * 60)
142- 
143- shape = (8, 8)
144- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
145 55 
146 x = torch.randn(shape, dtype=torch.float32, device=device)56 x = torch.randn(shape, dtype=torch.float32, device=device)
147 out = torch.zeros(shape, dtype=torch.float32, device=device)57 out = torch.zeros(shape, dtype=torch.float32, device=device)
58+ 
148 erfc_kernel(x, out)59 erfc_kernel(x, out)
149- 60+ torch.testing.assert_close(out, torch.erfc(x), atol=1e-3, rtol=1e-3)
150- if global_run_mode == pypto.RunMode.NPU:
151- expected = torch.erfc(x)
152- max_diff = (out - expected).abs().max().item()
153- print(f' Max difference: {max_diff:.6f}')
154- assert max_diff < 1e-5, 'Result mismatch!'
155- print('✓ Erfc operation completed successfully\n')
156 61 
157 62 
158-# ============================================================================63+@pypto.frontend.jit(runtime_options=runtime_options)
159-# 3. Matrix Multiplication
160-# ============================================================================
161-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
162def matmul_kernel(64def matmul_kernel(
163- a: pypto.Tensor([], pypto.DT_BF16),65+ a: pypto.Tensor[[...], pypto.DT_BF16],
164- b: pypto.Tensor([], pypto.DT_BF16),66+ b: pypto.Tensor[[...], pypto.DT_BF16],
165- out: pypto.Tensor([], pypto.DT_BF16)):67+ out: pypto.Tensor[[...], pypto.DT_BF16],
68+):
166 pypto.set_cube_tile_shapes([32, 32], [64, 64], [64, 64])69 pypto.set_cube_tile_shapes([32, 32], [64, 64], [64, 64])
167 out.move(pypto.matmul(a, b, a.dtype))70 out.move(pypto.matmul(a, b, a.dtype))
168 71 
169 72 
170-def test_matmul(device_id=None):73+def test_matmul(device):
171- print("=" * 60)
172- print("Example 3: Matrix Multiplication")
173- print("=" * 60)
174- 
175 m, k, n = 64, 128, 6474 m, k, n = 64, 128, 64
176 75 
177- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
178 a = torch.randn(m, k, dtype=torch.bfloat16, device=device)76 a = torch.randn(m, k, dtype=torch.bfloat16, device=device)
179 b = torch.randn(k, n, dtype=torch.bfloat16, device=device)77 b = torch.randn(k, n, dtype=torch.bfloat16, device=device)
180 out = torch.empty((m, n), dtype=torch.bfloat16, device=device)78 out = torch.empty((m, n), dtype=torch.bfloat16, device=device)
181 matmul_kernel(a, b, out)79 matmul_kernel(a, b, out)
182 80 
183- if global_run_mode == pypto.RunMode.NPU:81+ torch.testing.assert_close(out, torch.matmul(a, b), atol=1e-3, rtol=1e-3)
184- expected = torch.matmul(a, b)
185- max_diff = (out - expected).abs().max().item()
186- print(f" Max difference: {max_diff:.6f}")
187- assert max_diff < 1e-1, "Result mismatch!"
188- print("✓ Matrix multiplication completed successfully\n")
189 82 
190 83 
191-# ============================================================================84+@pypto.frontend.jit(runtime_options=runtime_options)
192-# 4. Reduction Operations
193-# ============================================================================
194-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
195def sum_kernel(85def sum_kernel(
196- a: pypto.Tensor([], pypto.DT_FP32),86+ a: pypto.Tensor[[...], pypto.DT_FP32], out: pypto.Tensor[[...], pypto.DT_FP32]
197- out: pypto.Tensor([], pypto.DT_FP32)):87+):
198 pypto.set_vec_tile_shapes(8, 8)88 pypto.set_vec_tile_shapes(8, 8)
199- out.move(pypto.sum(a, dim=-1, keepdim=False))89+ out[:] = pypto.sum(a, dim=-1, keepdim=False)
200 90 
201 91 
202-def test_reduce_ops(device_id=None):92+def test_sum(device):
203- """Reduction: sum along last dimension."""
204- print("=" * 60)
205- print("Example 4: Reduction Operations (sum)")
206- print("=" * 60)
207 93 
208- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
209 a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32, device=device)94 a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32, device=device)
210 out = torch.empty((2), dtype=torch.float32, device=device)95 out = torch.empty((2), dtype=torch.float32, device=device)
96+ 
211 sum_kernel(a, out)97 sum_kernel(a, out)
212 98 
213- if global_run_mode == pypto.RunMode.NPU:99+ torch.testing.assert_close(out, torch.sum(a, dim=-1), atol=1e-3, rtol=1e-3)
214- expected = torch.tensor([6, 15], dtype=torch.float32, device=device)
215- assert_allclose(out.cpu().numpy(), expected.cpu().numpy(), rtol=1e-3, atol=1e-3)
216- print(f" Input: {a.tolist()}")
217- print(f" Output: {out.tolist()}")
218- print("✓ Reduction operations completed successfully\n")
219 100 
220 101 
221-# ============================================================================102+def ceildiv(x, y):
222-# 5. Tiling Configuration103+ return (x + y - 1) // y
223-# ============================================================================
224-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
225-def tiled_add_kernel(
226- a: pypto.Tensor(),
227- b: pypto.Tensor(),
228- out: pypto.Tensor()):
229- pypto.set_vec_tile_shapes(2, 8)
230- out.move(pypto.add(a, b))
231 104 
232 105 
233-def test_tiling_config(device_id=None):106+@pypto.frontend.jit(runtime_options=runtime_options)
234- """Show how to set vec and cube tile shapes."""107+def dynamic_add_kernel(
235- print("=" * 60)108+ # `pypto.DYNAMIC` marks a dynamic dimension support. Static and dynamic dimensions can be mixed.
236- print("Example 5: Tiling Configuration")109+ x: pypto.Tensor[[pypto.DYNAMIC, pypto.DYNAMIC], pypto.DT_FP16],
237- print("=" * 60)110+ output: pypto.Tensor[[pypto.DYNAMIC, pypto.DYNAMIC], pypto.DT_FP16],
111+ # Logical block processed by each loop iteration.
112+ block_m: int,
113+ block_n: int,
114+ # Hardware compute tile.
115+ tile_m: int,
116+ tile_n: int,
117+):
118+ pypto.set_vec_tile_shapes(tile_m, tile_n)
119+ 
120+ # `pypto.loop` generates loops for dynamic iteration counts.
121+ # It is also recommended for large static loops to reduce compile time.
122+ # `break` and `continue` are not supported.
123+ for m in pypto.loop(ceildiv(x.shape[0], block_m)):
124+ for n in pypto.loop(ceildiv(x.shape[1], block_n)):
125+ # Pypto computes on fixed-size blocks. `view` creates a logical BLOCK_M × BLOCK_N block,
126+ # while automatically tracking the valid region for boundary blocks.
127+ #
128+ # `shape` and `valid_shape` are symbolic compile-time values and can be inspected during compilation.
129+ # eg: `print(tile.shape)`, `print(tile.valid_shape)`
130+ tile = pypto.view(
131+ x, shape=[block_m, block_n], offsets=[m * block_m, n * block_n]
132+ )
133+ tile = tile * 2
134+ pypto.assemble(tile, [m * block_m, n * block_n], output)
238 135 
239 136 
240- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'137+def test_dynamic_add(device):
241- a = torch.ones((2, 8), dtype=torch.float32, device=device)138+ m, n = 512, 512
242- b = torch.ones((2, 8), dtype=torch.float32, device=device)139+ block_m, block_n = 128, 128
243- out = torch.empty((2, 8), dtype=torch.float32, device=device)140+ tile_m, tile_n = 32, 32
244- tiled_add_kernel(a, b, out)
245 141 
246- if global_run_mode == pypto.RunMode.NPU:142+ x = torch.randn((m, n), dtype=torch.float16, device=device)
247- expected = a + b143+ out = torch.empty((m, n), dtype=torch.float16, device=device)
248- assert_allclose(out.cpu().numpy(), expected.cpu().numpy(), rtol=1e-3, atol=1e-3)144+ 
249- print(f" vec_tile_shapes set to (2, 8)")145+ dynamic_add_kernel(x, out, block_m, block_n, tile_m, tile_n)
250- print(" Tiling configuration completed successfully\n")146+ if "npu" in device:
147+ torch.testing.assert_close(out, x * 2.0, atol=1e-2, rtol=1e-2)
atomgit-bot
atomgit-botatomgit-bot7月16日

🟡 Medium Priority

test_dynamic_add 函数在第 146-148 行仅在 device == "npu" 时执行 torch.testing.assert_close 验证结果。当 device == "cpu"(即 SIM 仿真模式)时,dynamic_add_kernel 仍然被调用执行,但结果完全不被验证,函数静默返回成功。

这导致:

  1. 用户在 SIM 模式下运行该示例时,看到所有测试通过,但实际上 dynamic_add 的结果未被验证,可能产生错误输出而用户毫不知情。
  2. 如果 SIM 模式确实不支持动态 shape(正如注释所说),那么调用 dynamic_add_kernel 本身就可能产生未定义行为或崩溃,而代码没有提前拦截。

建议:在 else 分支(SIM 模式)中至少打印一条跳过信息(如 print("Skipping verification: dynamic shape not supported in SIM mode")),而不是静默返回。更好的做法是提前检查并在 kernel 调用前就跳过执行。

likedislike
148+ else:
149+ print("Warning: dynamic_add_kernel is not supported in sim mode, skip verification")
251 150 
252 151 
253-# ============================================================================152+def device_init(run_mode):
254-# 6. Transform Operations (view + assemble)153+ if run_mode == "sim":
255-# ============================================================================154+ runtime_options["run_mode"] = pypto.RunMode.SIM
256-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})155+ return "cpu"
257-def view_assemble_kernel(156+ else:
258- x: pypto.Tensor(),157+ try:
259- output: pypto.Tensor(),158+ import torch_npu
260- tile_h: int,159+ except ImportError:
261- tile_w: int,160+ print("torch_npu is not installed, please install it first")
262- height: int,161+ sys.exit(1)
263- width: int):
264- pypto.set_vec_tile_shapes(tile_h, tile_w)
265- h_tiles = height // tile_h
266- w_tiles = width // tile_w
267- for h_idx in pypto.loop(h_tiles, name="h_loop", idx_name="h_idx"):
268- for w_idx in pypto.loop(w_tiles, name="w_loop", idx_name="w_idx"):
269- h_off = h_idx * tile_h
270- w_off = w_idx * tile_w
271- tile = pypto.view(x, [tile_h, tile_w], [h_off, w_off])
272- result = pypto.mul(tile, 2.0)
273- pypto.assemble(result, [h_off, w_off], output)
274 162 
163+ device_id = int(os.environ.get("TILE_FWK_DEVICE_ID", 0))
164+ torch.npu.set_device(device_id)
275 165 
276-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})166+ runtime_options["run_mode"] = pypto.RunMode.NPU
277-def permute_kernel(167+ return f"npu:{device_id}"
278- x: pypto.Tensor([], pypto.DT_FP32),
279- out: pypto.Tensor([], pypto.DT_FP32),
280- dims: list):
281- vec_tile_shapes = [8] * len(x.shape)
282- pypto.set_vec_tile_shapes(*vec_tile_shapes)
283- out[:] = pypto.permute(x, dims)
284 168 
285 169 
286-def test_permute(device_id=None):
287- """Permute: rearrange dimensions of a 5-D tensor."""
288- print("=" * 60)
289- print("Example 7: Permute")
290- print("=" * 60)
291- 
292- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
293- 
294- dtype = torch.float32
295- x = torch.randn(2, 3, 4, 5, 6, dtype=dtype, device=device)
296- 
297- permute_dims = (3, 1, 4, 0, 2)
298- out_shape = tuple(x.shape[d] for d in permute_dims)
299- out = torch.empty(out_shape, dtype=torch.float32, device=device)
300- permute_kernel(x, out, permute_dims)
301- 
302- if global_run_mode == pypto.RunMode.NPU:
303- golden = x.permute(*permute_dims)
304- max_diff = np.abs(out.cpu().numpy() - golden.cpu().numpy()).max()
305- print(f" Max difference: {max_diff:.6f}")
306- assert_allclose(out.cpu().numpy(), golden.cpu().numpy(), rtol=1e-3, atol=1e-3)
307- print("✓ Permute completed successfully\n")
308- 
309- 
310-def test_transform_ops(device_id=None):
311- """Loop-based tiling with view and assemble: out = input * 2."""
312- print("=" * 60)
313- print("Example 6: Transform Operations (view + assemble)")
314- print("=" * 60)
315- 
316- height, width = 64, 64
317- tile_h, tile_w = 32, 32
318- 
319- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
320- x = torch.randn((height, width), dtype=torch.float16, device=device)
321- out = torch.empty((height, width), dtype=torch.float16, device=device)
322- view_assemble_kernel(x, out, tile_h, tile_w, height, width)
323- 
324- if global_run_mode == pypto.RunMode.NPU:
325- expected = x * 2.0
326- max_diff = (out - expected).abs().max().item()
327- print(f" Max difference: {max_diff:.6f}")
328- assert max_diff < 1e-2, "Result mismatch!"
329- print("✓ Transform operations completed successfully\n")
330- 
331- 
332-# ============================================================================
333-# Main
334-# ============================================================================
335- 
336def main():170def main():
337- parser = argparse.ArgumentParser(171+ examples = {
338- description="PyPTO Basic Operations Quick-Start",172+ "add": test_add,
339- formatter_class=argparse.RawDescriptionHelpFormatter)173+ "erfc": test_erfc,
174+ "matmul": test_matmul,
175+ "sum": test_sum,
176+ "dynamic_add": test_dynamic_add,
177+ }
178+ 
179+ parser = argparse.ArgumentParser(description="PyPTO Basic Operations Quick-Start")
340 parser.add_argument(180 parser.add_argument(
341- 'example_id', type=str, nargs='?',181+ "-m",
342- help='Run a specific case. If omitted, all cases run.'182+ "--run_mode",
343- )
344- parser.add_argument('--list', action='store_true', help='List available examples')
345- parser.add_argument(
346- '--run_mode',
347- type=str,
348- nargs='?',
349- default='npu',
350 choices=["npu", "sim"],183 choices=["npu", "sim"],
351- help='Run mode, supports npu and sim.'184+ default="npu",
185+ help="Execution mode (default: npu)",
186+ )
187+ parser.add_argument(
188+ "-t",
189+ "--tests",
190+ nargs="*",
191+ choices=examples.keys(),
192+ metavar="TEST",
193+ help="Test cases to run (default: all). Choices: %(choices)s",
352 )194 )
353 args = parser.parse_args()195 args = parser.parse_args()
354 196 
355- examples = {197+ if args.tests:
356- "tensor_creation::test_tensor_creation": {198+ selected = {test: examples[test] for test in args.tests}
357- 'name': 'Tensor Creation',199+ else:
358- 'function': test_tensor_creation,200+ selected = examples
359- },
360- "elementwise_ops::test_elementwise_ops": {
361- 'name': 'Element-wise Operations',
362- 'function': test_elementwise_ops,
363- },
364- "matmul::test_matmul": {
365- 'name': 'Matrix Multiplication',
366- 'function': test_matmul,
367- },
368- "reduce_ops::test_reduce_ops": {
369- 'name': 'Reduction Operations',
370- 'function': test_reduce_ops,
371- },
372- "tiling_config::test_tiling_config": {
373- 'name': 'Tiling Configuration',
374- 'function': test_tiling_config,
375- },
376- "transform_ops::test_transform_ops": {
377- 'name': 'Transform Operations',
378- 'function': test_transform_ops,
379- },
380- "permute::test_permute": {
381- 'name': 'Permute',
382- 'function': test_permute,
383- },
384- }
385 201 
386- if args.list:202+ device = device_init(args.run_mode)
387- print("\nAvailable Examples:\n")203+ for name, test in selected.items():
388- for ex_id, ex_info in examples.items():204+ print(f"Running test_{name} ...")
389- print(f" {ex_id}: {ex_info['name']}")205+ test(device)
390- return
391- 
392- if args.example_id is not None:
393- if args.example_id not in examples:
394- print(f"ERROR: Invalid example ID: {args.example_id}")
395- print(f"Valid IDs: {', '.join(examples.keys())}")
396- sys.exit(1)
397- 
398- device_id = None
399- if args.run_mode == "npu":
400- device_id = get_device_id()
401- if device_id is None:
402- return
403- import torch_npu
404- torch.npu.set_device(device_id)
405- 
406- examples_to_run = (
407- [(args.example_id, examples[args.example_id])]
408- if args.example_id else list(examples.items())
409- )
410- 
411- print("\n" + "=" * 60)
412- print("PyPTO Basic Operations Quick-Start")
413- print("=" * 60 + "\n")
414- 
415- try:
416- for _, ex_info in examples_to_run:
417- ex_info['function'](device_id)
418- 
419- if len(examples_to_run) > 1:
420- print("=" * 60)
421- print("All examples completed successfully!")
422- print("=" * 60)
423- except Exception as e:
424- print(f"\nError: {e}")
425- raise
426 206 
427 207 
428if __name__ == "__main__":208if __name__ == "__main__":
Dexamples/01_beginner/basic/symbolic_scalar.py+0-307
@@ -1,307 +0,0 @@
1-#!/usr/bin/env python3
2-# coding: utf-8
3-# Copyright (c) 2025 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-SymbolicScalar Example for PyPTO
13- 
14-This example demonstrates how to use SymbolicScalar in PyPTO, including:
15-- SymbolicScalar as loop index inside kernel
16-- Difference between concrete and non-concrete symbolic values
17- 
18-This is a beginner-friendly example focusing on SymbolicScalar semantics
19-rather than complex numerical computation.
20-"""
21- 
22-import os
23-import sys
24-import argparse
25-import pypto
26-import torch
27-import numpy as np
28-from numpy.testing import assert_allclose
29- 
30- 
31-# ----------------------------------------------------------------------------
32-# Device Utilities
33-# ----------------------------------------------------------------------------
34- 
35- 
36-def _peek_run_mode_from_argv(default: str = "npu") -> str:
37- """Read run_mode early so module-level decorators can use it."""
38- for idx, arg in enumerate(sys.argv):
39- if arg == "--run_mode" and idx + 1 < len(sys.argv):
40- value = sys.argv[idx + 1]
41- if value in ("npu", "sim"):
42- return value
43- if arg.startswith("--run_mode="):
44- value = arg.split("=", 1)[1]
45- if value in ("npu", "sim"):
46- return value
47- return default
48- 
49- 
50-global_run_mode = pypto.RunMode.NPU
51-if _peek_run_mode_from_argv("npu") == "sim":
52- global_run_mode = pypto.RunMode.SIM
53- 
54- 
55-def get_device_id():
56- """
57- Get and validate TILE_FWK_DEVICE_ID from environment variable.
58- 
59- Returns:
60- int: The device ID if valid, None otherwise.
61- """
62- if 'TILE_FWK_DEVICE_ID' not in os.environ:
63- print("Please set the environment variable TILE_FWK_DEVICE_ID before running:")
64- print(" export TILE_FWK_DEVICE_ID=0")
65- return None
66- 
67- try:
68- device_id = int(os.environ['TILE_FWK_DEVICE_ID'])
69- return device_id
70- except ValueError:
71- print(f"ERROR: TILE_FWK_DEVICE_ID must be an integer, got: {os.environ['TILE_FWK_DEVICE_ID']}")
72- return None
73- 
74- 
75-# ----------------------------------------------------------------------------
76-# Kernel Definitions
77-# ----------------------------------------------------------------------------
78- 
79-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
80-def symbolicscalar_in_loop_kernel(
81- x: pypto.Tensor([], pypto.DT_FP32),
82- out: pypto.Tensor([], pypto.DT_FP32)):
83- pypto.set_vec_tile_shapes(2, 8)
84- for _ in pypto.loop(2, name="sym_loop", idx_name="i"):
85- # Assert is not supported yet.
86- # Assert whether not i.is_concrete()
87- # Assert whether i.is_symbol() or i.is_expression()
88- # Execute expression: let expr be the result of i + 1
89- # Assert whether not expr.is_concrete()
90- out[:] = pypto.add(x, out)
91- 
92- 
93-# ----------------------------------------------------------------------------
94-# Python Wrappers
95-# ----------------------------------------------------------------------------
96- 
97- 
98-def test_symbolicscalar_in_loop(device_id: int = None):
99- """SymbolicScalar as loop index inside kernel"""
100- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
101- x = torch.tensor(
102- [1, 2, 3],
103- dtype=torch.float32,
104- device=device
105- )
106- 
107- y = torch.zeros(x.shape, dtype=x.dtype, device=device)
108- symbolicscalar_in_loop_kernel(x, y)
109- golden = (x + x).cpu()
110- 
111- print(f"Input shape: {x.shape}")
112- print(f"Output shape: {y.shape}")
113- if global_run_mode == pypto.RunMode.NPU:
114- assert_allclose(y.cpu().numpy(), golden.numpy(), rtol=1e-3, atol=1e-3)
115- print("✓ SymbolicScalar in loop test passed")
116- print()
117- 
118- 
119-def test_init_symbolic_scalar_value_arg(device_id: int = None):
120- """SymbolicScalar Initialization"""
121- expected_value = 123
122- 
123- # Initialize from a concrete value
124- scalar = pypto.symbolic_scalar(expected_value)
125- assert scalar.is_concrete()
126- assert scalar.concrete() == expected_value
127- 
128- # Initialize from an existing SymbolicScalar
129- scalar = pypto.symbolic_scalar(scalar)
130- assert scalar.is_concrete()
131- assert scalar.concrete() == expected_value
132- 
133- # Initialize with a name and a concrete value
134- named_scalar = pypto.symbolic_scalar("scalar", expected_value)
135- assert named_scalar.is_concrete()
136- assert named_scalar.concrete() == expected_value
137- 
138- print("✓ SymbolicScalar Initialization test passed")
139- print()
140- 
141- 
142-def test_symbolic_scalar_prop(device_id: int = None):
143- """Inspect core SymbolicScalar properties"""
144- scalar = pypto.symbolic_scalar(10)
145- assert scalar.is_symbol() == False
146- assert scalar.is_expression() == False
147- assert scalar.is_immediate() == True
148- assert scalar.is_concrete() == True
149- assert scalar.concrete() == 10
150- 
151- scalar2 = pypto.symbolic_scalar("s")
152- assert scalar2.is_symbol() == True
153- assert scalar2.is_expression() == False
154- assert scalar.is_immediate() == True
155- assert scalar2.is_concrete() == False
156- 
157- scalar3 = scalar < 2
158- assert isinstance(scalar3, pypto.symbolic_scalar)
159- assert scalar3.is_symbol() == False
160- assert scalar3.is_expression() == False
161- assert scalar3.is_immediate() == True
162- assert scalar3.is_concrete() == True
163- assert scalar3.concrete() == 0
164- 
165- scalar4 = scalar2 < 2
166- assert isinstance(scalar4, pypto.symbolic_scalar)
167- assert scalar4.is_symbol() == False
168- assert scalar4.is_expression() == True
169- assert scalar4.is_immediate() == False
170- assert scalar4.is_concrete() == False
171- 
172- print("✓ SymbolicScalar properties test passed")
173- print()
174- 
175- 
176-def test_symbolic_scalar_complex_expr(device_id: int = None):
177- """SymbolicScalar expression involving multiple comparison operators"""
178- b = pypto.symbolic_scalar('b')
179- a = (b >= 2) * (b < 8)
180- assert str(a) == '((b>=2)*(b<8))'
181- 
182- print("✓ SymbolicScalar multiple test passed")
183- print()
184- 
185- 
186-def main():
187- parser = argparse.ArgumentParser(
188- description="PyPTO SymbolicScalar Example",
189- formatter_class=argparse.RawDescriptionHelpFormatter,
190- epilog="""
191-Examples:
192- %(prog)s Run all examples
193- %(prog)s --list List available examples
194- %(prog)s symbolicscalar_in_loop::test_symbolicscalar_in_loop Run SymbolicScalar loop example
195- """
196- )
197- parser.add_argument(
198- "example_id",
199- type=str,
200- nargs="?",
201- help="Run a specific case (e.g., symbolicscalar_in_loop::test_symbolicscalar_in_loop). If omitted, all cases run."
202- )
203- parser.add_argument(
204- "--list",
205- action="store_true",
206- help="List all available examples and exit"
207- )
208- parser.add_argument(
209- "--run_mode", "--run-mode",
210- nargs="?", type=str, default="npu", choices=["npu", "sim"],
211- help='Run mode, supports npu and sim.'
212- )
213- 
214- args = parser.parse_args()
215- 
216- 
217- examples = {
218- 'symbolicscalar_in_loop::test_symbolicscalar_in_loop': {
219- "name": "SymbolicScalar in Loop",
220- "description": (
221- "Demonstrate SymbolicScalar used as a loop index. "
222- "This example verifies that loop indices are symbolic rather than "
223- "concrete values, and remain symbolic when used in expressions."
224- ),
225- "function": test_symbolicscalar_in_loop
226- },
227- 'symbolic_scalar_prop::test_symbolic_scalar_prop': {
228- "name": "SymbolicScalar Properties",
229- "description": (
230- "Inspect core SymbolicScalar properties, including whether a scalar "
231- "is symbolic, concrete, immediate, or an expression."
232- ),
233- "function": test_symbolic_scalar_prop
234- },
235- 'symbolic_scalar_complex_expr::test_symbolic_scalar_complex_expr': {
236- "name": "SymbolicScalar Complex Expression (Issue #36)",
237- "description": (
238- "Demonstrate construction and string representation of a compound "
239- "SymbolicScalar expression involving multiple comparison operators."
240- ),
241- "function": test_symbolic_scalar_complex_expr
242- }
243- }
244- 
245- 
246- # List examples if requested
247- if args.list:
248- print("\n" + "=" * 60)
249- print("Available Examples")
250- print("=" * 60 + "\n")
251- for ex_id, ex_info in sorted(examples.items()):
252- print(f" {ex_id}. {ex_info['name']}")
253- print(f" {ex_info['description']}\n")
254- return
255- 
256- # Validate example ID if provided
257- if args.example_id is not None:
258- if args.example_id not in examples:
259- print(f"ERROR: Invalid example ID: {args.example_id}")
260- print(f"Valid example IDs are: {', '.join(map(str, sorted(examples.keys())))}")
261- print("\nUse --list to see all available examples.")
262- sys.exit(1)
263- 
264- print("\n" + "=" * 60)
265- print("PyPTO SymbolicScalar Example")
266- print("=" * 60 + "\n")
267- 
268- # Get and validate device ID (needed for NPU examples)
269- device_id = None
270- examples_to_run = []
271- 
272- if args.example_id is not None:
273- # Run single example
274- examples_to_run = [(args.example_id, examples[args.example_id])]
275- else:
276- # Run all examples
277- examples_to_run = list(examples.items())
278- 
279- if args.run_mode == "npu":
280- device_id = get_device_id()
281- if device_id is None:
282- return
283- # Set the device once for all examples
284- import torch_npu
285- torch.npu.set_device(device_id)
286- 
287- try:
288- for ex_id, ex_info in examples_to_run:
289- if args.run_mode == "npu" and device_id is None:
290- print(f"Skipping example {ex_id} ({ex_info['name']}): NPU device not configured")
291- continue
292- 
293- print(f"Running Example {ex_id}: {ex_info['name']}")
294- ex_info['function'](device_id)
295- 
296- if len(examples_to_run) > 1:
297- print("=" * 60)
298- print("All SymbolicScalar examples passed!")
299- print("=" * 60)
300- 
301- except Exception as e:
302- print(f"\nError: {e}")
303- raise
304- 
305- 
306-if __name__ == "__main__":
307- main()
Dexamples/01_beginner/basic/tensor_creation.py+0-399
@@ -1,399 +0,0 @@
1-#!/usr/bin/env python3
2-# coding: utf-8
3-# Copyright (c) 2025 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-Tensor Creation Operation Examples for PyPTO
13- 
14-This file contains all tensor creation examples merged into a single file.
15-You can run all examples or select specific ones using command-line arguments.
16- 
17-Usage:
18- python creation_ops.py # Run all examples
19- python creation_ops.py --list # List all available examples
20- python creation_ops.py arange::test_arange_basic # Run a specific case
21-"""
22- 
23-import argparse
24-import os
25-import sys
26-import pypto
27-import torch
28-import numpy as np
29-from numpy.testing import assert_allclose
30- 
31- 
32-def _peek_run_mode_from_argv(default: str = "npu") -> str:
33- """Read run_mode early so module-level decorators can use it."""
34- for idx, arg in enumerate(sys.argv):
35- if arg == "--run_mode" and idx + 1 < len(sys.argv):
36- value = sys.argv[idx + 1]
37- if value in ("npu", "sim"):
38- return value
39- if arg.startswith("--run_mode="):
40- value = arg.split("=", 1)[1]
41- if value in ("npu", "sim"):
42- return value
43- return default
44- 
45- 
46-global_run_mode = pypto.RunMode.NPU
47-if _peek_run_mode_from_argv("npu") == "sim":
48- global_run_mode = pypto.RunMode.SIM
49- 
50- 
51-def get_device_id():
52- """
53- Get and validate TILE_FWK_DEVICE_ID from environment variable.
54- 
55- Returns:
56- int: The device ID if valid, None otherwise.
57- """
58- if 'TILE_FWK_DEVICE_ID' not in os.environ:
59- print("Please set the environment variable TILE_FWK_DEVICE_ID before running:")
60- print(" export TILE_FWK_DEVICE_ID=0")
61- return None
62- 
63- try:
64- device_id = int(os.environ['TILE_FWK_DEVICE_ID'])
65- return device_id
66- except ValueError:
67- print(f"ERROR: TILE_FWK_DEVICE_ID must be an integer, got: {os.environ['TILE_FWK_DEVICE_ID']}")
68- return None
69- 
70- 
71-# ============================================================================
72-# ARANGE Examples
73-# ============================================================================
74- 
75-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
76-def arange_end_kernel(out: pypto.Tensor((4,), pypto.DT_INT32),
77- end,
78- ):
79- pypto.set_vec_tile_shapes(8)
80- out.move(pypto.arange(end))
81- 
82- 
83-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
84-def arange_start_end_kernel(out: pypto.Tensor((3,), pypto.DT_FP32),
85- start,
86- end):
87- pypto.set_vec_tile_shapes(8)
88- out.move(pypto.arange(start, end))
89- 
90- 
91-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
92-def arange_start_end_step_kernel(out: pypto.Tensor((6,), pypto.DT_FP32),
93- start,
94- end,
95- step):
96- pypto.set_vec_tile_shapes(8)
97- out.move(pypto.arange(start, end, step))
98- 
99- 
100-def test_arange_basic(device_id=None):
101- """Test basic usage of arange function"""
102- print("=" * 60)
103- print("Test: Basic Usage of arange Function")
104- print("=" * 60)
105- 
106- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
107- 
108- # Test 1: arange(end)
109- expected_a = torch.tensor([0, 1, 2, 3], dtype=torch.int32, device=device)
110- out_torch = torch.empty(4, dtype=torch.int32, device=device)
111- arange_end_kernel(out_torch, end=4)
112- print(f"Output a: {out_torch}")
113- print(f"Expected a: {expected_a}")
114- if global_run_mode == pypto.RunMode.NPU:
115- assert_allclose(out_torch.cpu().numpy(), expected_a.cpu().numpy(), rtol=1e-3, atol=1e-3)
116- 
117- # Test 2: arange(start, end)
118- expected_b = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32, device=device)
119- out_torch = torch.empty(3, dtype=torch.float32, device=device)
120- arange_start_end_kernel(out_torch, start=1.0, end=4.0)
121- print(f"Output b: {out_torch}")
122- print(f"Expected b: {expected_b}")
123- if global_run_mode == pypto.RunMode.NPU:
124- assert_allclose(out_torch.cpu().numpy(), expected_b.cpu().numpy(), rtol=1e-3, atol=1e-3)
125- 
126- # Test 3: arange(start, end, step)
127- expected_c = torch.tensor([1.0, 1.5, 2.0, 2.5, 3.0, 3.5], dtype=torch.float32, device=device)
128- out_torch = torch.empty(6, dtype=torch.float32, device=device)
129- arange_start_end_step_kernel(out_torch, start=1.0, end=4.0, step=0.5)
130- print(f"Output c: {out_torch}")
131- print(f"Expected c: {expected_c}")
132- if global_run_mode == pypto.RunMode.NPU:
133- assert_allclose(out_torch.cpu().numpy(), expected_c.cpu().numpy(), rtol=1e-3, atol=1e-3)
134- 
135- print("✓ Basic usage of arange function completed successfully")
136- 
137- 
138-# ============================================================================
139-# DATATYPE Examples
140-# ============================================================================
141- 
142-def test_tensor_creation_with_datatypes(device_id=None):
143- """Test tensor creation with various data types"""
144- print("=" * 60)
145- print("Test: Tensor Creation with Various Data Types")
146- print("=" * 60)
147- 
148- data_types = [
149- (pypto.DT_INT4, "DT_INT4"),
150- (pypto.DT_INT8, "DT_INT8"),
151- (pypto.DT_INT16, "DT_INT16"),
152- (pypto.DT_INT32, "DT_INT32"),
153- (pypto.DT_INT64, "DT_INT64"),
154- (pypto.DT_FP8, "DT_FP8"),
155- (pypto.DT_FP16, "DT_FP16"),
156- (pypto.DT_FP32, "DT_FP32"),
157- (pypto.DT_BF16, "DT_BF16"),
158- (pypto.DT_HF4, "DT_HF4"),
159- (pypto.DT_HF8, "DT_HF8"),
160- (pypto.DT_UINT8, "DT_UINT8"),
161- (pypto.DT_UINT16, "DT_UINT16"),
162- (pypto.DT_UINT32, "DT_UINT32"),
163- (pypto.DT_UINT64, "DT_UINT64"),
164- (pypto.DT_BOOL, "DT_BOOL")
165- ]
166- 
167- for dtype, dtype_name in data_types:
168- print(f"\nCreating tensor with data type: {dtype_name}")
169- 
170- # Create a tensor with shape [2, 3] and the specified data type
171- tensor = pypto.tensor([2, 3], dtype, f"tensor_{dtype_name}")
172- 
173- # Access tensor attributes
174- print(f"Name: {tensor.name}") # e.g., tensor_DT_INT8
175- print(f"Data Type: {tensor.dtype}") # e.g., DT_INT8
176- 
177- print("✓ Tensor creation with various data types completed successfully")
178- 
179- 
180-# ============================================================================
181-# FULL Examples
182-# ============================================================================
183- 
184-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
185-def full_float_kernel(out: pypto.Tensor((2, 2), pypto.DT_FP32),
186- fill_value):
187- pypto.set_vec_tile_shapes(2, 8)
188- out.move(pypto.full((2, 2), fill_value, pypto.DT_FP32))
189- 
190- 
191-@pypto.frontend.jit(runtime_options={"run_mode": global_run_mode})
192-def full_symbolic_scalar_kernel(out: pypto.Tensor((2, 2), pypto.DT_INT32),
193- fill_value):
194- pypto.set_vec_tile_shapes(2, 8)
195- out.move(pypto.full((2, 2), fill_value, pypto.DT_INT32))
196- 
197- 
198-def test_full_basic(device_id=None):
199- """Test basic usage of full function"""
200- print("=" * 60)
201- print("Test: Basic Usage of full Function")
202- print("=" * 60)
203- 
204- device = f'npu:{device_id}' if global_run_mode == pypto.RunMode.NPU and device_id is not None else 'cpu'
205- 
206- # Test 1: Create a 2x2 tensor filled with 1.0 (float32)
207- expected_a = torch.tensor([[1.0, 1.0], [1.0, 1.0]], dtype=torch.float32, device=device)
208- out_torch = torch.empty((2, 2), dtype=torch.float32, device=device)
209- full_float_kernel(out_torch, fill_value=1.0)
210- print(f"Output a: {out_torch}")
211- print(f"Expected a: {expected_a}")
212- if global_run_mode == pypto.RunMode.NPU:
213- assert_allclose(out_torch.cpu().numpy(), expected_a.cpu().numpy(), rtol=1e-3, atol=1e-3)
214- 
215- # Test 2: Create a 2x2 tensor filled with a symbolic scalar (int32)
216- expected_b = torch.tensor([[1, 1], [1, 1]], dtype=torch.int32, device=device)
217- out_torch = torch.empty((2, 2), dtype=torch.int32, device=device)
218- full_symbolic_scalar_kernel(out_torch, fill_value=pypto.symbolic_scalar(1))
219- print(f"Output b: {out_torch}")
220- print(f"Expected b: {expected_b}")
221- if global_run_mode == pypto.RunMode.NPU:
222- assert_allclose(out_torch.cpu().numpy(), expected_b.cpu().numpy(), rtol=1e-3, atol=1e-3)
223- 
224- print("✓ Basic usage of full function completed successfully")
225- 
226- 
227-# ============================================================================
228-# TENSOR Examples
229-# ============================================================================
230- 
231-def test_basic_tensor_creation(device_id=None):
232- """Test basic tensor creation"""
233- print("=" * 60)
234- print("Test: Basic Tensor Creation")
235- print("=" * 60)
236- 
237- # Create a tensor with shape [2, 3] and FP16 data type
238- tensor = pypto.tensor([2, 3], pypto.DT_FP16, "basic_tensor")
239- 
240- # Access tensor attributes
241- print(f"Shape: {tensor.shape}") # [2, 3]
242- print(f"Data Type: {tensor.dtype}") # DT_FP16
243- print(f"Dimensions: {tensor.dim}") # 2
244- print(f"Format: {tensor.format}") # TILEOP_ND
245- print(f"Name: {tensor.name}") # basic_tensor
246- 
247- # Rename the tensor
248- tensor.name = "new_name"
249- print(f"New Name: {tensor.name}") # new_name
250- 
251- print("✓ Basic tensor creation completed successfully")
252- 
253- 
254-def test_tensor_creation_with_format(device_id=None):
255- """Test tensor creation with specific format"""
256- print("=" * 60)
257- print("Test: Tensor Creation with Specific Format")
258- print("=" * 60)
259- 
260- # Create a tensor using the NZ format
261- tensor = pypto.tensor([512, 32], pypto.DT_FP16, "sparse_tensor", pypto.TileOpFormat.TILEOP_NZ)
262- 
263- # Access tensor attributes
264- print(f"Shape: {tensor.shape}") # [512, 32]
265- print(f"Data Type: {tensor.dtype}") # DT_FP16
266- print(f"Dimensions: {tensor.dim}") # 2
267- print(f"Format: {tensor.format}") # TILEOP_NZ
268- print(f"Name: {tensor.name}") # sparse_tensor
269- 
270- print("✓ Tensor Creation with Specific Format completed successfully")
271- 
272- 
273-# ============================================================================
274-# Main Function
275-# ============================================================================
276- 
277-def main():
278- """Run tensor creation operation examples.
279- 
280- Usage:
281- python creation_ops.py # Run all examples
282- python creation_ops.py --list # List all available examples
283- python creation_ops.py arange::test_arange_basic # Run a specific case
284- """
285- parser = argparse.ArgumentParser(
286- description="PyPTO Tensor Creation Operation Examples",
287- formatter_class=argparse.RawDescriptionHelpFormatter,
288- epilog="""
289-Examples:
290- %(prog)s Run all examples
291- %(prog)s --list List all available examples
292- %(prog)s arange::test_arange_basic Run a specific case
293- """
294- )
295- parser.add_argument(
296- 'example_id',
297- type=str,
298- nargs="?",
299- help='Run a specific case (e.g., arange::test_arange_basic). If omitted, all cases run.'
300- )
301- parser.add_argument(
302- '--list',
303- action='store_true',
304- help='List all available examples and exit'
305- )
306- parser.add_argument(
307- '--run_mode',
308- type=str,
309- nargs='?',
310- default='npu',
311- choices=["npu", "sim"],
312- help='Run mode, supports npu and sim.'
313- )
314- 
315- args = parser.parse_args()
316- 
317- # Define available examples
318- examples = {
319- 'arange::test_arange_basic': {
320- 'name': 'Test basic usage of arange function',
321- 'description': 'Basic usage of arange function example',
322- 'function': test_arange_basic,
323- },
324- 'datatype::test_tensor_creation_with_datatypes': {
325- 'name': 'Test tensor creation with various data types',
326- 'description': 'Tensor creation with various data types example',
327- 'function': test_tensor_creation_with_datatypes,
328- },
329- 'full::test_full_basic': {
330- 'name': 'Test basic usage of full function',
331- 'description': 'Basic usage of full function example',
332- 'function': test_full_basic,
333- },
334- 'tensor::test_basic_tensor_creation': {
335- 'name': 'Test basic tensor creation',
336- 'description': 'Basic tensor creation example',
337- 'function': test_basic_tensor_creation,
338- },
339- 'tensor::test_tensor_creation_with_format': {
340- 'name': 'Test tensor creation with specific format',
341- 'description': 'Tensor creation with specific format example',
342- 'function': test_tensor_creation_with_format,
343- }
344- }
345- 
346- # List examples if requested
347- if args.list:
348- print("\n" + "=" * 60)
349- print("Available Examples")
350- print("=" * 60 + "\n")
351- for ex_id, ex_info in sorted(examples.items()):
352- print(f" ID: {ex_id}")
353- print(f" name: {ex_info['name']}")
354- print(f" description: {ex_info['description']}\n")
355- return
356- 
357- # Validate case if provided
358- device_id = None
359- examples_to_run = []
360- if args.example_id:
361- if args.example_id not in examples:
362- print(f"ERROR: Invalid case: {args.example_id}")
363- print(f"Valid cases are: {', '.join(sorted(examples.keys()))}")
364- print("\nUse --list to see all available examples.")
365- sys.exit(1)
366- examples_to_run = [(args.example_id, examples[args.example_id])]
367- else:
368- examples_to_run = [(key, info) for key, info in sorted(examples.items())]
369- 
370- print("\n" + "=" * 60)
371- print("PyPTO Tensor Creation Operation Examples")
372- print("=" * 60 + "\n")
373- 
374- if args.run_mode == "npu":
375- device_id = get_device_id()
376- if device_id is None:
377- return
378- import torch_npu
379- torch.npu.set_device(device_id)
380- print("Running examples that require NPU hardware...")
381- print("(Make sure CANN environment is configured and NPU is available)\n")
382- 
383- try:
384- for ex_id, ex_info in examples_to_run:
385- print(f"Running Example {ex_id}: {ex_info['name']}")
386- ex_info['function'](device_id)
387- 
388- if len(examples_to_run) > 1:
389- print("=" * 60)
390- print("All creation tests passed!")
391- print("=" * 60)
392- 
393- except Exception as e:
394- print(f"\nError: {e}")
395- raise
396- 
397- 
398-if __name__ == "__main__":
399- main()
Mexamples/validate_examples.py+1-1651
@@ -8,1655 +8,5 @@
8# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.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.9# See LICENSE in the root of the software repository for the full text of the License.
10# -----------------------------------------------------------------------------------------------------------10# -----------------------------------------------------------------------------------------------------------
11-"""
12-Parallel Python Script Executor with Adaptive Retry Logic
13- 
14-This utility orchestrates the concurrent execution of Python scripts across multiple hardware devices
15-(NPUs), featuring intelligent script analysis, adaptive execution strategies,
16-and comprehensive reporting. Designed for validation workflows in CANN-based hardware-accelerated
17-environments, it ensures reliable script evaluation.
18- 
19-Key Capabilities:
20-- Target Flexibility: Processes single .py files or recursively scans directories, excluding itself.
21-- Intelligent Script Analysis (via AST parsing):
22- - Detects if name == 'main' guards for direct execution
23- - Skips the entire script by default when @pytest.mark.skip decorators are detected and
24- the script is run via if __name__ == '__main__':. (Can be disabled via configuration.)
25- - Identifies pytest-style tests (test* functions or Test* classes) for pytest dispatch
26- - Verifies '--run_mode' argument support for simulation mode filtering
27- - Automatically skips files with no executable content
28-- Dual Execution Modes:
29- - npu (default): Executes eligible scripts on physical hardware devices with device isolation
30- - sim: Filters and runs only scripts that explicitly support '--run_mode' argument
31- using virtual workers
32-- Script-level skip control:
33- - Automatically excludes scripts containing @pytest.mark.skip decorators (enabled by default),
34- mimicking pytest's skip behavior at the file level.
35- Can be disabled via --no-skip-pytest-mark-skip.
36- - Scripts without a '__main__' guard but with pytest-style tests are executed using pytest
37- by default. This behavior can be disabled via --no-pytest-auto-detect, which will skip
38- such scripts instead.
39-- Resource Management:
40- - Thread-safe device leasing system for hardware resource allocation
41- - Hierarchical process termination (parent + children) on timeout or failure
42- - Device-specific environment isolation (TILE_FWK_DEVICE_ID, ASCEND_VISIBLE_DEVICES,
43- TILE_FWK_STEST_DEVICE_ID)
44-- Adaptive Execution Strategies:
45- - Single-Device Mode: Serial execution with progressive retry rounds (default: 3)
46- - Multi-Device Mode:
47- * Initial parallel execution across available physical/virtual devices
48- * Configurable parallel retry rounds (default: 1)
49- * Final serial fallback for persistent failures to eliminate resource contention
50- * Optional skip of serial fallback via --no-serial-fallback flag
51-- Granular Test Selection: Passes specific test identifiers to scripts or pytest as needed
52-- Comprehensive Reporting:
53- - Real-time emoji-enhanced status indicators (✅/❌/⏭️/⚠️) with device assignment
54- - Final categorized summary with success/failure/skip counts
55- - Optional failure diagnostics showing last OUTPUT_SNIPPET_LINES lines of output
56- - Structured retry progression tracking
57-- Safety Features:
58- - Per-script timeout enforcement (default: 300s) with cleanup guarantees
59- - Dependency validation (pytest availability check)
60- - Process group isolation for reliable cleanup
61- 
62-Exit Behavior:
63-- Returns 0 only if all executed scripts succeed (skipped scripts don't affect exit code)
64-- Returns 1 if any script fails after all retry attempts
65-- Early exits with descriptive errors for invalid inputs or missing dependencies
66- 
67-Usage Examples:
68- # 1. Execute directory on single NPU device
69- python3 examples/validate_examples.py -t examples/02_intermediate -d 0
70- 
71- # 2. Multi-device parallel execution
72- python3 examples/validate_examples.py -t examples -d 0,1,2,3
73- 
74- # 3. Execute specific script on device 0
75- python3 examples/validate_examples.py -t examples/01_beginner/basic/basic_ops.py -d 0
76- 
77- # 4. Simulation mode (single virtual worker)
78- python3 examples/validate_examples.py -t examples --run_mode sim -w 1
79- 
80- # 5. Concurrent execution in simulation mode (16 virtual workers)
81- python3 examples/validate_examples.py -t examples --run_mode sim -w 16
82- 
83- # 6. Custom timeout per script
84- python3 examples/validate_examples.py -t examples/02_intermediate -d 0 --timeout 120
85- 
86- # 7. Show failure diagnostics in summary
87- python3 examples/validate_examples.py -t examples -d 0 --show-fail-details
88- 
89- # 8. Include scripts marked with @pytest.mark.skip (override default behavior)
90- python3 examples/validate_examples.py -t examples -d 0 --no-skip-pytest-mark-skip
91- 
92- # 9. Disable pytest auto-detection (skip scripts without __main__ guard)
93- python3 examples/validate_examples.py -t examples -d 0 --no-pytest-auto-detect
94- 
95- # 10. Skip serial fallback in multi-device mode (only parallel retries)
96- python3 examples/validate_examples.py -t examples -d 0,1,2,3 --no-serial-fallback
97- 
98- # 11. Full configuration
99- python3 examples/validate_examples.py -t examples -d 0,1,2,3
100- --parallel_retries 2 --serial_retries 5 --timeout 300
101- --show-fail-details
102- 
103-Note: This tool is designed specifically for CANN-based development workflows. In npu mode, device
104-parallelism is determined by provided device IDs. In sim mode, parallelism is controlled by the
105---workers parameter which creates virtual device slots.
106-"""
107-import argparse
108-import ast
109-import functools
110-import math
111-import os
112-import queue
113-import shutil
114-import signal
115-import subprocess
116-import sys
117-import threading
118-import time
119-from abc import ABC, abstractmethod
120-from concurrent.futures import ThreadPoolExecutor, as_completed
121-from dataclasses import dataclass
122-from enum import Enum
123-from pathlib import Path
124-from types import FrameType
125-from typing import Any, Callable, Dict, List, Optional, Tuple
126-import psutil
127- 
128- 
129-# =============================================================================
130-# Module-level Constants (Eliminate Magic Numbers)
131-# =============================================================================
132-SHUTDOWN_CHECK_INTERVAL: float = 0.5 # Interval to check for shutdown requests (seconds)
133-PROCESS_TERMINATE_TIMEOUT: int = 3 # Time to wait for process to terminate gracefully (seconds)
134-CHILD_PROCESS_WAIT_TIMEOUT: int = 3 # Time to wait for child processes to terminate (seconds)
135-FUTURE_RESULT_BUFFER: int = 30 # Extra buffer time for future results beyond script timeout (seconds)
136-DEFAULT_SCRIPT_TIMEOUT: int = 300 # Default per-script execution timeout (seconds)
137-DEFAULT_PARALLEL_RETRIES: int = 1 # Default number of parallel retry rounds
138-DEFAULT_SERIAL_RETRIES: int = 3 # Default number of serial retry rounds
139-DEFAULT_SIM_WORKERS: int = 16 # Maximum default number of workers in simulation mode
140-OUTPUT_SNIPPET_LINES: int = 5 # Number of lines to show in output snippets
141- 
142-# Environment variable names for device configuration (NPU mode)
143-ENV_TILE_FWK_DEVICE_ID: str = "TILE_FWK_DEVICE_ID"
144-ENV_ASCEND_VISIBLE_DEVICES: str = "ASCEND_VISIBLE_DEVICES"
145-ENV_TILE_FWK_STEST_DEVICE_ID: str = "TILE_FWK_STEST_DEVICE_ID"
146- 
147- 
148-class ExecutionStatus(Enum):
149- """Enumeration of possible script execution statuses."""
150- SUCCESS = "success"
151- FAILURE = "failure"
152- CANCELLED = "cancelled"
153- SKIPPED_NO_TESTS = "skipped_no_tests"
154- SKIPPED_SIM = "skipped_sim"
155- SKIPPED_PYTEST_MARK = "skipped_pytest_mark"
156- SKIPPED_PYTEST_DISABLED = "skipped_pytest_disabled"
157- 
158- 
159-@dataclass
160-class ExecutionResult:
161- """Unified result type for script execution.
162- 
163- This dataclass provides a consistent structure for all execution results,
164- regardless of the outcome (success, failure, skip, or cancellation).
165- """
166- rel_path: str
167- status: ExecutionStatus
168- device_id: Optional[str] = None
169- reason: Optional[str] = None
170- message: Optional[str] = None
171- output_snippet: Optional[str] = None
172- 
173- @classmethod
174- def success(cls, rel_path: str, device_id: Optional[str] = None) -> "ExecutionResult":
175- """Create a success result."""
176- return cls(rel_path=rel_path, status=ExecutionStatus.SUCCESS, device_id=device_id)
177- 
178- @classmethod
179- def failure(cls, rel_path: str, reason: str, device_id: Optional[str] = None,
180- output_snippet: Optional[str] = None) -> "ExecutionResult":
181- """Create a failure result."""
182- return cls(rel_path=rel_path, status=ExecutionStatus.FAILURE,
183- reason=reason, device_id=device_id, output_snippet=output_snippet)
184- 
185- @classmethod
186- def cancelled(cls, rel_path: str, message: str) -> "ExecutionResult":
187- """Create a cancelled result."""
188- return cls(rel_path=rel_path, status=ExecutionStatus.CANCELLED, message=message)
189- 
190- @classmethod
191- def skipped(cls, rel_path: str, status: ExecutionStatus, message: str) -> "ExecutionResult":
192- """Create a skipped result."""
193- return cls(rel_path=rel_path, status=status, message=message)
194- 
195- 
196-@dataclass
197-class ExecutionContext:
198- """Encapsulates execution parameters to reduce function argument count.
199- 
200- This context object bundles together the parameters needed for script execution,
201- providing a cleaner API for run_script and related functions.
202- """
203- args: argparse.Namespace
204- device_queue: "queue.Queue[str]"
205- timeout: int
206- process_manager: "ProcessManager"
207- safe_print: Callable[..., None]
208- print_cmd_on_serial: bool = False
209- estimated_queue_depth: int = 1
210- 
211- 
212-@dataclass
213-class SummaryData:
214- """Encapsulates data for the final execution summary.
215- 
216- This dataclass bundles together all the information needed to print
217- the final execution summary, reducing the parameter count of _print_final_summary.
218- """
219- success_list: List["ExecutionResult"]
220- failure_list: List["ExecutionResult"]
221- skipped_sim_list: List["ExecutionResult"]
222- skipped_no_tests_list: List["ExecutionResult"]
223- skipped_pytest_mark_list: List["ExecutionResult"]
224- skipped_pytest_disabled_list: List["ExecutionResult"]
225- args: argparse.Namespace
226- target: Path
227- device_ids: List[str]
228- total_time_sec: float
229- safe_print: Callable[..., None]
230- 
231- @property
232- def total_original(self) -> int:
233- """Calculate total number of scripts found."""
234- return (len(self.success_list) + len(self.failure_list) +
235- len(self.skipped_sim_list) + len(self.skipped_no_tests_list) +
236- len(self.skipped_pytest_disabled_list))
237- 
238- 
239-@dataclass
240-class CollectionParams:
241- """Encapsulates parameters for result collection to reduce function argument count.
242- 
243- This dataclass bundles together all parameters needed for collecting and formatting
244- execution results, adhering to the guideline of limiting function parameters.
245- """
246- proc: subprocess.Popen
247- stdout: Optional[str]
248- stderr: Optional[str]
249- rel_path: str
250- device_id: str
251- safe_print: Callable[..., None]
252- 
253- 
254-# =============================================================================
255-# Process Manager - Encapsulated Process Tracking and Graceful Shutdown
256-# =============================================================================
257-class ProcessManager:
258- """Manages process tracking and graceful shutdown for concurrent script execution.
259- 
260- This class encapsulates all process-related state and operations, providing:
261- - Thread-safe process registration and unregistration
262- - Cooperative shutdown via signal handling
263- - Process tree termination for reliable cleanup
264- 
265- Design principles:
266- 1. Signal handlers only set flags (async-signal-safe)
267- 2. Actual cleanup happens in regular code paths
268- 3. Atomic process registration to avoid race conditions
269- 4. Cooperative shutdown via periodic flag checking
270- 
271- Usage:
272- manager = ProcessManager()
273- manager.setup_signal_handlers()
274- # ... use manager throughout execution ...
275- manager.cleanup_all()
276- """
277- 
278- def __init__(self) -> None:
279- """Initialize the process manager with empty state."""
280- self._active_processes: List[subprocess.Popen] = []
281- self._lock = threading.Lock()
282- self._shutdown_event = threading.Event()
283- self._print_lock = threading.Lock()
284- self._safe_print: Optional[Callable[..., None]] = None
285- 
286- def setup_signal_handlers(self) -> None:
287- """Register signal handlers for graceful shutdown.
288- 
289- Should be called early, before any child processes are created.
290- """
291- if os.name != 'nt': # Unix-like systems
292- signal.signal(signal.SIGINT, self._signal_handler)
293- signal.signal(signal.SIGTERM, self._signal_handler)
294- else:
295- signal.signal(signal.SIGINT, self._signal_handler)
296- 
297- def is_shutdown_requested(self) -> bool:
298- """Check if shutdown has been requested."""
299- return self._shutdown_event.is_set()
300- 
301- def create_safe_print(self) -> Callable[..., None]:
302- """Create and return a thread-safe print function."""
303- def safe_print(*args, **kwargs):
304- with self._print_lock:
305- print(*args, **kwargs)
306- self._safe_print = safe_print
307- return safe_print
308- 
309- def create_and_register_process(
310- self, cmd: List[str], popen_kwargs: Dict[str, Any]
311- ) -> Tuple[Optional[subprocess.Popen], bool]:
312- """Atomically create a process and register it for tracking.
313- 
314- This method holds the lock during process creation, eliminating the race
315- condition window between process creation and registration.
316- 
317- Args:
318- cmd: Command to execute
319- popen_kwargs: Keyword arguments for subprocess.Popen
320- 
321- Returns:
322- Tuple of (process, success):
323- - (proc, True) if process was created and registered successfully
324- - (None, False) if shutdown was requested before creation
325- - (proc, False) if process was created but shutdown was requested during creation
326- (caller should terminate the process)
327- """
328- with self._lock:
329- if self._shutdown_event.is_set():
330- return None, False
331- proc = subprocess.Popen(cmd, **popen_kwargs)
332- # Check again after creation - if shutdown was requested during Popen,
333- # we still need to track the process but signal failure
334- if self._shutdown_event.is_set():
335- # Process created but shutdown requested - return process for cleanup
336- # but don't register (caller will terminate it)
337- return proc, False
338- self._active_processes.append(proc)
339- return proc, True
340- 
341- def unregister_process(self, proc: subprocess.Popen) -> None:
342- """Unregister a process from tracking."""
343- with self._lock:
344- if proc in self._active_processes:
345- self._active_processes.remove(proc)
346- 
347- def cleanup_all(self) -> None:
348- """Clean up all tracked active processes.
349- 
350- Should be called from regular code paths (not from signal handlers).
351- """
352- with self._lock:
353- procs = self._active_processes.copy()
354- 
355- if not procs:
356- return
357- 
358- if self._safe_print:
359- self._safe_print(f"\n⚠️ Shutdown requested. Cleaning up {len(procs)} active process(es)...")
360- 
361- for proc in procs:
362- try:
363- if proc.poll() is None:
364- terminate_process_tree(proc)
365- except (psutil.NoSuchProcess, psutil.AccessDenied, OSError) as e:
366- if self._safe_print:
367- self._safe_print(f"Warning: Error cleaning up process {proc.pid}: {e}")
368- 
369- def wait_for_process(self, proc: subprocess.Popen, timeout: int,
370- check_interval: float = SHUTDOWN_CHECK_INTERVAL
371- ) -> Tuple[Optional[str], Optional[str], bool]:
372- """Wait for process completion while periodically checking for shutdown.
373- 
374- Args:
375- proc: The subprocess to wait for
376- timeout: Maximum time to wait in seconds
377- check_interval: How often to check for shutdown (seconds)
378- 
379- Returns:
380- Tuple of (stdout, stderr, was_shutdown_requested)
381- """
382- start_time = time.perf_counter()
383- stdout_parts: List[str] = []
384- stderr_parts: List[str] = []
385- 
386- def _reader(stream, parts: List[str]) -> None:
387- try:
388- while True:
389- chunk = stream.read(4096)
390- if not chunk:
391- break
392- parts.append(chunk)
393- except (OSError, ValueError):
394- pass
395- 
396- t_out = threading.Thread(target=_reader, args=(proc.stdout, stdout_parts), daemon=True)
397- t_err = threading.Thread(target=_reader, args=(proc.stderr, stderr_parts), daemon=True)
398- t_out.start()
399- t_err.start()
400- 
401- while True:
402- if self._shutdown_event.is_set():
403- t_out.join(timeout=2)
404- t_err.join(timeout=2)
405- return "".join(stdout_parts), "".join(stderr_parts), True
406- 
407- if proc.poll() is not None:
408- t_out.join(timeout=5)
409- t_err.join(timeout=5)
410- return "".join(stdout_parts), "".join(stderr_parts), False
411- 
412- elapsed = time.perf_counter() - start_time
413- if elapsed >= timeout:
414- proc.timeout_output = "".join(stdout_parts) + "".join(stderr_parts)
415- raise subprocess.TimeoutExpired(proc.args, timeout)
416- 
417- remaining_time = min(check_interval, timeout - elapsed)
418- if remaining_time > 0:
419- time.sleep(remaining_time)
420- 
421- def _signal_handler(self, signum: int, frame: Optional[FrameType]) -> None:
422- """Signal handler that only sets the shutdown flag (async-signal-safe)."""
423- self._shutdown_event.set()
424- 
425- 
426-def terminate_process_tree(proc: subprocess.Popen) -> None:
427- """Terminate a process and all its child processes.
428- 
429- Args:
430- proc: The subprocess.Popen object to terminate
431- """
432- try:
433- parent = psutil.Process(proc.pid)
434- children = parent.children(recursive=True)
435- 
436- # Terminate child processes first
437- for child in children:
438- try:
439- child.terminate()
440- except psutil.NoSuchProcess:
441- pass
442- 
443- # Wait for child processes to terminate
444- _, still_alive = psutil.wait_procs(children, timeout=CHILD_PROCESS_WAIT_TIMEOUT)
445- 
446- # Force-kill remaining processes
447- for child in still_alive:
448- try:
449- child.kill()
450- except psutil.NoSuchProcess:
451- pass
452- 
453- # Terminate parent process
454- proc.terminate()
455- try:
456- proc.wait(timeout=PROCESS_TERMINATE_TIMEOUT)
457- except subprocess.TimeoutExpired:
458- proc.kill()
459- 
460- except (psutil.NoSuchProcess, psutil.AccessDenied, OSError) as e:
461- # Process may have already exited or we lack permissions
462- # Try direct kill as a fallback
463- try:
464- proc.kill()
465- except OSError:
466- # Process already gone or inaccessible, nothing more to do
467- pass
468- 
469- 
470-# =============================================================================
471-# AST Analysis Functions
472-# =============================================================================
473-@dataclass
474-class ScriptAnalysis:
475- """Result of analyzing a Python script's AST.
476- 
477- This dataclass consolidates all information extracted from a single AST parse,
478- eliminating the need for multiple parsing passes over the same file.
479- """
480- has_main_guard: bool = False
481- has_pytest_tests: bool = False
482- has_pytest_skip_mark: bool = False
483- supports_run_mode: bool = False
484- 
485- 
486-def _parse_ast(file_path: Path, context: str = "") -> Optional[ast.Module]:
487- """Parse a Python file and return its AST, or None if parsing fails.
488- 
489- This helper function centralizes file reading and AST parsing logic,
490- eliminating code duplication across AST analysis functions.
491- 
492- Args:
493- file_path: Path to the Python file to parse
494- context: Context string for error messages (e.g., "main guard check")
495- 
496- Returns:
497- Parsed AST module, or None if parsing fails due to:
498- - Empty file
499- - Syntax errors
500- - Encoding issues
501- - File not found
502- """
503- try:
504- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
505- content = f.read()
506- 
507- if not content.strip():
508- return None
509- 
510- return ast.parse(content, filename=str(file_path))
511- except SyntaxError as e:
512- # Log syntax errors since they likely indicate a real issue
513- context_msg = f" for {context}" if context else ""
514- print(f"Warning: Syntax error parsing {file_path}{context_msg}: {e}", file=sys.stderr)
515- return None
516- except (UnicodeDecodeError, OSError) as e:
517- # Log file access errors for debugging (user may not be aware of permission issues)
518- context_msg = f" for {context}" if context else ""
519- print(f"Warning: Cannot read {file_path}{context_msg}: {e}", file=sys.stderr)
520- return None
521- except (ValueError, TypeError) as e:
522- # Unexpected but non-fatal errors - log and return None
523- context_msg = f" for {context}" if context else ""
524- print(f"Warning: Error parsing {file_path} with AST{context_msg}: {e}", file=sys.stderr)
525- return None
526- 
527- 
528-@functools.lru_cache(maxsize=1024)
529-def _analyze_script_cached(file_path_str: str) -> ScriptAnalysis:
530- """Internal cached implementation of script analysis.
531- """
532- file_path = Path(file_path_str)
533- result = ScriptAnalysis()
534- tree = _parse_ast(file_path, "script analysis")
535- if tree is None:
536- return result
537- 
538- for node in ast.walk(tree):
539- # Check for __name__ == '__main__' guard
540- if isinstance(node, ast.If):
541- if (isinstance(node.test, ast.Compare) and
542- isinstance(node.test.left, ast.Name) and
543- node.test.left.id == '__name__' and
544- len(node.test.ops) == 1 and
545- isinstance(node.test.ops[0], ast.Eq) and
546- len(node.test.comparators) == 1 and
547- isinstance(node.test.comparators[0], ast.Constant) and
548- node.test.comparators[0].value == '__main__'):
549- result.has_main_guard = True
550- 
551- # Check for pytest-style test functions and classes
552- if isinstance(node, ast.FunctionDef):
553- if node.name.startswith('test'):
554- result.has_pytest_tests = True
555- 
556- # Check for @pytest.mark.skip decorator
557- for decorator in node.decorator_list:
558- # Check for @pytest.mark.skip(...)
559- if (isinstance(decorator, ast.Call) and
560- isinstance(decorator.func, ast.Attribute) and
561- isinstance(decorator.func.value, ast.Attribute) and
562- decorator.func.value.attr == 'mark' and
563- isinstance(decorator.func.value.value, ast.Name) and
564- decorator.func.value.value.id == 'pytest' and
565- decorator.func.attr == 'skip'):
566- result.has_pytest_skip_mark = True
567- # Check for @pytest.mark.skip (without call)
568- if (isinstance(decorator, ast.Attribute) and
569- isinstance(decorator.value, ast.Attribute) and
570- decorator.value.attr == 'mark' and
571- isinstance(decorator.value.value, ast.Name) and
572- decorator.value.value.id == 'pytest' and
573- decorator.attr == 'skip'):
574- result.has_pytest_skip_mark = True
575- 
576- if isinstance(node, ast.ClassDef) and node.name.startswith('Test'):
577- result.has_pytest_tests = True
578- 
579- # Check for --run_mode argument support
580- if (isinstance(node, ast.Call) and
581- hasattr(node.func, 'attr') and node.func.attr == 'add_argument'):
582- for arg in node.args:
583- if (isinstance(arg, ast.Constant) and
584- isinstance(arg.value, str) and
585- '--run_mode' in arg.value):
586- result.supports_run_mode = True
587- 
588- for keyword in node.keywords:
589- if keyword.arg == 'dest' and isinstance(keyword.value, ast.Constant):
590- if keyword.value.value == 'run_mode':
591- result.supports_run_mode = True
592- 
593- return result
594- 
595- 
596-def analyze_script(file_path: Path) -> ScriptAnalysis:
597- """Analyze a Python script and extract all relevant information in a single pass.
598- 
599- This function parses the AST once and extracts all needed information,
600- avoiding the overhead of multiple parse operations for the same file.
601- Results are cached using functools.lru_cache for thread-safe memoization.
602- 
603- Args:
604- file_path: Path to the Python file to analyze
605- 
606- Returns:
607- ScriptAnalysis with all extracted information
608- """
609- return _analyze_script_cached(str(file_path))
610- 
611- 
612-def get_script_analysis(file_path: Path) -> ScriptAnalysis:
613- """Get cached script analysis.
614- 
615- This function resolves the path and delegates to the lru_cache-decorated
616- analyze_script function, which handles thread-safe caching automatically.
617- 
618- Args:
619- file_path: Path to the Python file to analyze
620- 
621- Returns:
622- ScriptAnalysis with all extracted information
623- """
624- return _analyze_script_cached(str(file_path.resolve()))
625- 
626- 
627-# =============================================================================
628-# Script Execution Helper Functions (Modular Design)
629-# =============================================================================
630-def _check_skip_conditions(
631- ctx: ExecutionContext, analysis: ScriptAnalysis, rel_path: str
632-) -> Optional[ExecutionResult]:
633- """Check if script should be skipped based on analysis results.
634- 
635- Note: Skip conditions are checked in two places by design:
636- 1. In main() during initial categorization - for upfront filtering and summary stats
637- 2. Here in run_script() - as a safety check for scripts that may have been
638- added to the candidate list incorrectly or for defensive programming
639- 
640- The checks in main() prevent unnecessary device queue contention by filtering
641- early. This function provides a defensive second check and generates proper
642- skip results with logging.
643- 
644- Args:
645- ctx: Execution context containing args and safe_print
646- analysis: ScriptAnalysis result
647- rel_path: Relative path for display
648- 
649- Returns:
650- ExecutionResult if script should be skipped, None otherwise
651- """
652- if not analysis.has_main_guard and not analysis.has_pytest_tests:
653- ctx.safe_print(f"⏭️ Skipped: {rel_path} (no '__main__' guard and no pytest-style tests)")
654- return ExecutionResult.skipped(
655- rel_path, ExecutionStatus.SKIPPED_NO_TESTS,
656- "no '__main__' guard and no pytest-style tests"
657- )
658- 
659- if not analysis.has_main_guard and analysis.has_pytest_tests and not ctx.args.pytest_auto_detect:
660- ctx.safe_print(f"⏭️ Skipped: {rel_path} (pytest auto-detect disabled, no '__main__' guard)")
661- return ExecutionResult.skipped(
662- rel_path, ExecutionStatus.SKIPPED_PYTEST_DISABLED,
663- "pytest auto-detect disabled, script has no '__main__' guard"
664- )
665- 
666- if ctx.args.skip_pytest_mark_skip and analysis.has_pytest_skip_mark:
667- ctx.safe_print(f"⏭️ Skipped: {rel_path} (contains @pytest.mark.skip)")
668- return ExecutionResult.skipped(
669- rel_path, ExecutionStatus.SKIPPED_PYTEST_MARK,
670- "contains @pytest.mark.skip decorator"
671- )
672- 
673- if ctx.args.run_mode == "sim" and not analysis.supports_run_mode:
674- ctx.safe_print(f"⏭️ Skipped: {rel_path} (script does not support --run_mode)")
675- return ExecutionResult.skipped(
676- rel_path, ExecutionStatus.SKIPPED_SIM,
677- "script does not support --run_mode"
678- )
679- 
680- return None
681- 
682- 
683-def _acquire_device(
684- ctx: ExecutionContext, rel_path: str
685-) -> Tuple[Optional[str], Optional[ExecutionResult]]:
686- """Acquire a device from the queue with timeout and shutdown checks.
687- 
688- Args:
689- ctx: Execution context containing device_queue, timeout, etc.
690- rel_path: Relative path for display
691- 
692- Returns:
693- Tuple of (device_id, error_result):
694- - (device_id, None) on success
695- - (None, ExecutionResult) on failure or cancellation
696- """
697- device_acquisition_timeout = (ctx.estimated_queue_depth * ctx.timeout) + FUTURE_RESULT_BUFFER
698- device_wait_start = time.perf_counter()
699- 
700- while True:
701- if ctx.process_manager.is_shutdown_requested():
702- return None, ExecutionResult.cancelled(rel_path, "Shutdown requested during device acquisition")
703- try:
704- device_id = ctx.device_queue.get(timeout=SHUTDOWN_CHECK_INTERVAL)
705- return device_id, None
706- except queue.Empty:
707- elapsed = time.perf_counter() - device_wait_start
708- if elapsed >= device_acquisition_timeout:
709- ctx.safe_print(f"❌ Failure: {rel_path} (device acquisition timeout)")
710- return None, ExecutionResult.failure(
711- rel_path, f"Could not acquire a device within {device_acquisition_timeout}s",
712- device_id=None, output_snippet=""
713- )
714- 
715- 
716-def _build_command(
717- args, analysis: ScriptAnalysis, full_path: Path, device_id: str
718-) -> Tuple[List[str], Dict[str, str]]:
719- """Build the command and environment for script execution.
720- 
721- This function centralizes all command and environment configuration,
722- including device-specific environment variables for NPU mode.
723- 
724- Args:
725- args: Parsed command-line arguments
726- analysis: ScriptAnalysis result
727- full_path: Absolute path to the script
728- device_id: Device ID for environment setup (used in NPU mode)
729- 
730- Returns:
731- Tuple of (command_list, environment_dict)
732- """
733- env = os.environ.copy()
734- 
735- # Set device-specific environment variables in NPU mode
736- if args.run_mode != "sim":
737- env[ENV_TILE_FWK_DEVICE_ID] = device_id
738- env[ENV_ASCEND_VISIBLE_DEVICES] = device_id
739- env[ENV_TILE_FWK_STEST_DEVICE_ID] = device_id
740- 
741- if analysis.has_main_guard:
742- cmd = [sys.executable, str(full_path)]
743- if args.example_id:
744- cmd.append(args.example_id)
745- if args.run_mode == "sim":
746- cmd.extend(["--run_mode", "sim"])
747- else:
748- # Execute pytest-style tests with --forked for process isolation
749- if args.example_id:
750- cmd = ["pytest", f"{full_path}::{args.example_id}", "-v", "--capture=no", "--forked"]
751- else:
752- cmd = ["pytest", str(full_path), "-v", "--capture=no", "--forked"]
753- 
754- return cmd, env
755- 
756- 
757-def _collect_timeout_output(proc: subprocess.Popen) -> str:
758- timeout_output = getattr(proc, 'timeout_output', "")
759- if timeout_output:
760- return timeout_output
761- if not proc.stdout:
762- return ""
763- try:
764- remaining_stdout, remaining_stderr = proc.communicate(timeout=1)
765- return (remaining_stdout or "") + (remaining_stderr or "")
766- except (subprocess.TimeoutExpired, OSError):
767- return ""
768- 
769- 
770-def _execute_process(
771- cmd: List[str],
772- env: Dict[str, str],
773- device_id: str,
774- ctx: ExecutionContext,
775- rel_path: str
776-) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[str], Optional[ExecutionResult]]:
777- """Execute the process and wait for completion.
778- 
779- Args:
780- cmd: Command to execute
781- env: Environment variables (already configured with device settings)
782- device_id: Device ID (for error reporting)
783- ctx: Execution context
784- rel_path: Relative path for display
785- 
786- Returns:
787- Tuple of (proc, stdout, stderr, error_result):
788- - (proc, stdout, stderr, None) on successful execution (may have non-zero exit)
789- - (proc, None, None, ExecutionResult) on error or cancellation
790- - (None, None, None, ExecutionResult) if process couldn't be created
791- """
792- cmd_str = " ".join(str(part) for part in cmd)
793- ctx.safe_print(f"→ Executing: {cmd_str}")
794- 
795- # Final shutdown check before creating process
796- if ctx.process_manager.is_shutdown_requested():
797- return None, None, None, ExecutionResult.cancelled(
798- rel_path, "Shutdown requested before process creation"
799- )
800- 
801- # Create popen kwargs with process group settings
802- popen_kwargs: Dict[str, Any] = {
803- "stdout": subprocess.PIPE,
804- "stderr": subprocess.PIPE,
805- "env": env,
806- "text": True,
807- }
808- if os.name != 'nt':
809- if sys.version_info >= (3, 11):
810- popen_kwargs["process_group"] = 0
811- else:
812- popen_kwargs["start_new_session"] = True
813- 
814- # Atomically create and register process
815- proc, registered = ctx.process_manager.create_and_register_process(cmd, popen_kwargs)
816- if proc is None:
817- return None, None, None, ExecutionResult.cancelled(
818- rel_path, "Shutdown requested before process creation"
819- )
820- if not registered:
821- terminate_process_tree(proc)
822- return None, None, None, ExecutionResult.cancelled(
823- rel_path, "Shutdown requested during process creation"
824- )
825- 
826- try:
827- stdout, stderr, was_shutdown = ctx.process_manager.wait_for_process(
828- proc, ctx.timeout, check_interval=SHUTDOWN_CHECK_INTERVAL
829- )
830- 
831- if was_shutdown:
832- terminate_process_tree(proc)
833- ctx.safe_print(f"🛑 Cancelled: {rel_path} (shutdown requested)")
834- return proc, None, None, ExecutionResult.cancelled(
835- rel_path, "Shutdown requested during execution"
836- )
837- 
838- return proc, stdout, stderr, None
839- 
840- except subprocess.TimeoutExpired:
841- timeout_output = _collect_timeout_output(proc)
842- terminate_process_tree(proc)
843- ctx.safe_print(f"❌ Failure: {rel_path}")
844- snippet = _extract_output_snippet(timeout_output) if timeout_output else ""
845- return proc, None, None, ExecutionResult.failure(
846- rel_path, f"Timeout (exceeded {ctx.timeout}s)",
847- device_id=device_id, output_snippet=snippet
848- )
849- 
850- except OSError as e:
851- if proc:
852- terminate_process_tree(proc)
853- ctx.safe_print(f"❌ Failure: {rel_path}")
854- return proc, None, None, ExecutionResult.failure(
855- rel_path, f"Exception during execution: {e}",
856- device_id=device_id, output_snippet=""
857- )
858- 
859- 
860-def _collect_result(params: CollectionParams) -> ExecutionResult:
861- """Collect and format the execution result.
862- 
863- Args:
864- params: CollectionParams containing process, output, and context information
865- 
866- Returns:
867- ExecutionResult with success or failure status
868- """
869- output = (params.stdout or "") + (params.stderr or "")
870- snippet = _extract_output_snippet(output)
871- 
872- if params.proc.returncode == 0:
873- params.safe_print(f"✅ Success: {params.rel_path}")
874- return ExecutionResult.success(params.rel_path, params.device_id)
875- else:
876- params.safe_print(f"❌ Failure: {params.rel_path}")
877- return ExecutionResult.failure(
878- params.rel_path, f"Non-zero exit code ({params.proc.returncode})",
879- device_id=params.device_id, output_snippet=snippet
880- )
881- 
882- 
883-def run_script(ctx: ExecutionContext, full_path: Path, rel_path: str) -> ExecutionResult:
884- """Execute a single script by leasing a device from the device queue.
885- 
886- This function orchestrates the script execution pipeline:
887- 1. Analyze: Check script properties and skip conditions
888- 2. Acquire Device: Lease a device from the queue
889- 3. Execute: Run the script process
890- 4. Collect: Gather and format execution results
891- 
892- Args:
893- ctx: Execution context containing all execution parameters
894- full_path: Absolute path to the script
895- rel_path: Relative path for display purposes
896- 
897- Returns:
898- ExecutionResult with the outcome of the script execution
899- """
900- # Phase 1: Check for early shutdown
901- if ctx.process_manager.is_shutdown_requested():
902- return ExecutionResult.cancelled(rel_path, "Shutdown requested before execution")
903- 
904- # Phase 2: Analyze - Check skip conditions
905- analysis = get_script_analysis(full_path)
906- skip_result = _check_skip_conditions(ctx, analysis, rel_path)
907- if skip_result is not None:
908- return skip_result
909- 
910- # Phase 3: Acquire Device
911- if ctx.process_manager.is_shutdown_requested():
912- return ExecutionResult.cancelled(rel_path, "Shutdown requested before device acquisition")
913- 
914- device_id, error_result = _acquire_device(ctx, rel_path)
915- if error_result is not None:
916- return error_result
917- 
918- # Print start message
919- if ctx.args.run_mode == "sim":
920- ctx.safe_print(f"▶️ Starting: {rel_path}")
921- else:
922- ctx.safe_print(f"▶️ Starting: {rel_path} (device={device_id})")
923- 
924- proc = None
925- try:
926- # Phase 4: Build Command (includes environment configuration)
927- cmd, env = _build_command(ctx.args, analysis, full_path, device_id)
928- 
929- # Phase 5: Execute Process
930- proc, stdout, stderr, exec_error = _execute_process(
931- cmd, env, device_id, ctx, rel_path
932- )
933- if exec_error is not None:
934- return exec_error
935- 
936- # Phase 6: Collect Result
937- collection_params = CollectionParams(
938- proc=proc,
939- stdout=stdout,
940- stderr=stderr,
941- rel_path=rel_path,
942- device_id=device_id,
943- safe_print=ctx.safe_print
944- )
945- return _collect_result(collection_params)
946- 
947- finally:
948- # Cleanup: Unregister process and return device to queue
949- if proc:
950- ctx.process_manager.unregister_process(proc)
951- if proc.poll() is None:
952- terminate_process_tree(proc)
953- # Only return device to queue if we successfully acquired one
954- # (prevents None from polluting the device pool)
955- if device_id is not None:
956- ctx.device_queue.put(device_id)
957- ctx.safe_print("-" * 50)
958- 
959- 
960-def _extract_output_snippet(output: str) -> str:
961- """Extract the last N lines from output for error reporting."""
962- if not output.strip():
963- return ""
964- return "\n".join(output.strip().splitlines()[-OUTPUT_SNIPPET_LINES:])
965- 
966- 
967-# =============================================================================
968-# Execution Strategy Classes (Strategy Pattern with ABC)
969-# =============================================================================
970-class ExecutionStrategy(ABC):
971- """Abstract base class for execution strategies.
972- 
973- This class defines the interface for script execution strategies and provides
974- common functionality for retry logic to avoid code duplication.
975- """
976- 
977- def __init__(self, args, target_dir: Path, device_ids: List[str],
978- timeout: int, safe_print: Callable[..., None],
979- process_manager: ProcessManager) -> None:
980- self.args = args
981- self.target_dir = target_dir
982- self.device_ids = device_ids
983- self.timeout = timeout
984- self.safe_print = safe_print
985- self.process_manager = process_manager
986- 
987- @abstractmethod
988- def execute(self, scripts: List[str],
989- all_results_map: Dict[str, ExecutionResult]) -> Tuple[List[ExecutionResult], List[ExecutionResult]]:
990- """Execute scripts and return success and failure lists."""
991- pass
992- 
993- def _run_serial_retry_loop(
994- self,
995- candidates: List[str],
996- all_results_map: Dict[str, ExecutionResult],
997- max_retries: int,
998- device_ids: List[str],
999- log_prefix: str = ""
1000- ) -> List[str]:
1001- """Common serial retry loop logic used by both strategies.
1002- 
1003- Args:
1004- candidates: List of script paths to execute
1005- all_results_map: Dictionary to store results
1006- max_retries: Maximum number of retry rounds
1007- device_ids: Device IDs to use for execution
1008- log_prefix: Prefix for log messages (e.g., "Final " for multi-device)
1009- 
1010- Returns:
1011- List of remaining failed script paths
1012- """
1013- current_candidates = candidates[:]
1014- prev_failure_count = len(current_candidates)
1015- retry_round = 0
1016- 
1017- while current_candidates and retry_round <= max_retries:
1018- if self.process_manager.is_shutdown_requested():
1019- self.safe_print(f"🛑 Shutdown requested. Stopping {log_prefix.lower()}execution.\n")
1020- break
1021- 
1022- if retry_round == 0:
1023- self.safe_print(f"▶️ {log_prefix}Serial Run — {len(current_candidates)} script(s)\n")
1024- else:
1025- self.safe_print(f"🔁 {log_prefix}Serial Retry {retry_round}/{max_retries} "
1026- f"— {len(current_candidates)} script(s)\n")
1027- 
1028- results = execute_scripts(
1029- self.args, current_candidates, self.target_dir, device_ids,
1030- workers=1, timeout=self.timeout, safe_print=self.safe_print,
1031- process_manager=self.process_manager, print_cmd_on_serial=True
1032- )
1033- 
1034- for r in results:
1035- all_results_map[r.rel_path] = r
1036- 
1037- if self.process_manager.is_shutdown_requested():
1038- self.safe_print(f"🛑 Shutdown requested. Stopping {log_prefix.lower()}retries.\n")
1039- break
1040- 
1041- new_failures = [r for r in results if r.status == ExecutionStatus.FAILURE]
1042- current_candidates = [r.rel_path for r in new_failures]
1043- current_failure_count = len(current_candidates)
1044- 
1045- if current_failure_count == 0:
1046- suffix = ' after ' + log_prefix.lower() + 'retry loop' if log_prefix else ''
1047- self.safe_print(f"✅ All scripts passed{suffix}.\n")
1048- break
1049- 
1050- if current_failure_count >= prev_failure_count:
1051- self.safe_print(f"⚠️ {log_prefix}Failure count did not decrease "
1052- f"(was {prev_failure_count}, now {current_failure_count}). Stopping retries.\n")
1053- break
1054- 
1055- prev_failure_count = current_failure_count
1056- retry_round += 1
1057- 
1058- if current_candidates and retry_round > max_retries and not self.process_manager.is_shutdown_requested():
1059- self.safe_print(f"🛑 Reached maximum {log_prefix.lower()}serial retries ({max_retries}). Stopping.\n")
1060- 
1061- return current_candidates
1062- 
1063- def _collect_final_results(
1064- self,
1065- all_results_map: Dict[str, ExecutionResult]
1066- ) -> Tuple[List[ExecutionResult], List[ExecutionResult]]:
1067- """Collect success and failure lists from results map."""
1068- success_list = [r for r in all_results_map.values()
1069- if r.status == ExecutionStatus.SUCCESS]
1070- failure_list = [r for r in all_results_map.values()
1071- if r.status == ExecutionStatus.FAILURE]
1072- return success_list, failure_list
1073- 
1074- 
1075-class SingleDeviceStrategy(ExecutionStrategy):
1076- """Execution strategy for a single device (serial execution)."""
1077- 
1078- def execute(self, scripts: List[str],
1079- all_results_map: Dict[str, ExecutionResult]) -> Tuple[List[ExecutionResult], List[ExecutionResult]]:
1080- max_serial_retries = max(0, self.args.serial_retries)
1081- self._run_serial_retry_loop(
1082- scripts, all_results_map, max_serial_retries, self.device_ids
1083- )
1084- return self._collect_final_results(all_results_map)
1085- 
1086- 
1087-class MultiDeviceStrategy(ExecutionStrategy):
1088- """Execution strategy for multiple devices (parallel execution with serial fallback)."""
1089- 
1090- def execute(self, scripts: List[str],
1091- all_results_map: Dict[str, ExecutionResult]) -> Tuple[List[ExecutionResult], List[ExecutionResult]]:
1092- current_candidates = scripts[:]
1093- parallel_retries = max(0, self.args.parallel_retries)
1094- actual_workers = len(self.device_ids)
1095- 
1096- # Parallel execution rounds
1097- for round_idx in range(parallel_retries + 1):
1098- if self.process_manager.is_shutdown_requested():
1099- self.safe_print("🛑 Shutdown requested. Stopping parallel execution.\n")
1100- break
1101- 
1102- round_name = "Initial" if round_idx == 0 else f"Retry {round_idx}"
1103- self.safe_print(f"🚀 Starting Parallel Round {round_idx + 1}/{parallel_retries + 1} "
1104- f"({round_name}) — {len(current_candidates)} script(s)\n")
1105- 
1106- round_results = execute_scripts(
1107- self.args, current_candidates, self.target_dir, self.device_ids,
1108- workers=actual_workers, timeout=self.timeout, safe_print=self.safe_print,
1109- process_manager=self.process_manager, print_cmd_on_serial=False
1110- )
1111- 
1112- for r in round_results:
1113- all_results_map[r.rel_path] = r
1114- 
1115- if self.process_manager.is_shutdown_requested():
1116- self.safe_print("🛑 Shutdown requested. Stopping retries.\n")
1117- break
1118- 
1119- round_failures = [r for r in round_results if r.status == ExecutionStatus.FAILURE]
1120- if not round_failures:
1121- self.safe_print(f"✅ All scripts passed in Parallel Round {round_idx + 1}. "
1122- f"No further retries needed.")
1123- return self._collect_final_results(all_results_map)
1124- 
1125- current_candidates = [r.rel_path for r in round_failures]
1126- self.safe_print(f"🔁 {len(current_candidates)} script(s) failed and will be retried.")
1127- 
1128- # Final serial retry loop for remaining failures
1129- if current_candidates and not self.process_manager.is_shutdown_requested():
1130- if self.args.no_serial_fallback:
1131- self.safe_print(f"⏭️ Skipping serial fallback (--no-serial-fallback enabled). "
1132- f"{len(current_candidates)} script(s) remain failed.\n")
1133- else:
1134- self.safe_print(f"🔂 Starting Final Serial Retry Loop — {len(current_candidates)} "
1135- f"remaining failed script(s)\n")
1136- max_serial_retries = max(0, self.args.serial_retries)
1137- serial_device_ids = [self.device_ids[0]]
1138- self._run_serial_retry_loop(
1139- current_candidates, all_results_map, max_serial_retries,
1140- serial_device_ids, log_prefix="Final "
1141- )
1142- 
1143- if not self.process_manager.is_shutdown_requested():
1144- self.safe_print("\n🏁 All execution rounds completed.")
1145- 
1146- return self._collect_final_results(all_results_map)
1147- 
1148- 
1149-def execute_scripts(
1150- args, rel_paths: List[str], target_dir: Path, device_ids: List[str],
1151- workers: int, timeout: int, safe_print: Callable[..., None],
1152- process_manager: ProcessManager, print_cmd_on_serial: bool = False
1153-) -> List[ExecutionResult]:
1154- """Execute a list of scripts with given device pool.
1155- 
1156- This function supports graceful shutdown and properly waits for all futures.
1157- 
1158- Args:
1159- args: Parsed command-line arguments
1160- rel_paths: List of relative script paths
1161- target_dir: Base directory for scripts
1162- device_ids: List of device IDs to use
1163- workers: Number of parallel workers
1164- timeout: Per-script timeout
1165- safe_print: Thread-safe print function
1166- process_manager: ProcessManager for process tracking
1167- print_cmd_on_serial: Whether to print commands in serial mode
1168- 
1169- Returns:
1170- List of ExecutionResult objects
1171- """
1172- if not rel_paths:
1173- return []
1174- 
1175- if process_manager.is_shutdown_requested():
1176- return [ExecutionResult.cancelled(p, "Shutdown requested")
1177- for p in rel_paths]
1178- 
1179- device_queue: queue.Queue = queue.Queue()
1180- for dev in device_ids:
1181- device_queue.put(dev)
1182- 
1183- # Calculate estimated queue depth for device acquisition timeout
1184- # This represents how many scripts each device is expected to handle.
1185- # NOTE: This is an average-based estimate that may be inaccurate when script
1186- # execution times vary significantly. Fast scripts may wait longer than needed,
1187- # while slow scripts may timeout prematurely. Consider adjusting --timeout
1188- # if experiencing unexpected timeouts with mixed script durations.
1189- estimated_queue_depth = math.ceil(len(rel_paths) / len(device_ids)) if device_ids else 1
1190- 
1191- # Create execution context (shared across all script executions)
1192- ctx = ExecutionContext(
1193- args=args,
1194- device_queue=device_queue,
1195- timeout=timeout,
1196- process_manager=process_manager,
1197- safe_print=safe_print,
1198- print_cmd_on_serial=print_cmd_on_serial,
1199- estimated_queue_depth=estimated_queue_depth
1200- )
1201- 
1202- # Pre-build path order lookup for O(1) sorting later
1203- path_order = {p: i for i, p in enumerate(rel_paths)}
1204- 
1205- results: List[ExecutionResult] = []
1206- with ThreadPoolExecutor(max_workers=workers) as executor:
1207- future_to_rel = {}
1208- for rel_path in rel_paths:
1209- if process_manager.is_shutdown_requested():
1210- results.append(
1211- ExecutionResult.cancelled(rel_path, "Shutdown requested before submission")
1212- )
1213- continue
1214- 
1215- full_path = target_dir / rel_path
1216- future = executor.submit(run_script, ctx, full_path, rel_path)
1217- future_to_rel[future] = rel_path
1218- 
1219- # Collect results - wait for each future properly
1220- # Use queue depth-aware timeout for Future.result()
1221- future_timeout = (estimated_queue_depth * timeout) + FUTURE_RESULT_BUFFER
1222- for future in as_completed(future_to_rel):
1223- rel_path = future_to_rel[future]
1224- try:
1225- result = future.result(timeout=future_timeout)
1226- results.append(result)
1227- except TimeoutError:
1228- # This should rarely happen as run_script has its own timeout
1229- results.append(
1230- ExecutionResult.failure(rel_path, "Future result timeout")
1231- )
1232- except Exception as e:
1233- results.append(
1234- ExecutionResult.failure(rel_path, f"Unexpected error: {e}")
1235- )
1236- 
1237- if process_manager.is_shutdown_requested():
1238- process_manager.cleanup_all()
1239- 
1240- # Sort results using O(1) dictionary lookup instead of O(n) list.index()
1241- results.sort(key=lambda x: path_order.get(x.rel_path, float('inf')))
1242- return results
1243- 
1244- 
1245-def _print_final_summary(summary: SummaryData) -> None:
1246- """Print the final execution summary.
1247- 
1248- Args:
1249- summary: SummaryData containing all information for the summary
1250- """
1251- safe_print = summary.safe_print
1252- args = summary.args
1253- 
1254- safe_print("\n" + "=" * 60)
1255- safe_print("📊 FINAL EXECUTION SUMMARY")
1256- safe_print("=" * 60)
1257- safe_print(f"Target directory/file : {summary.target}")
1258- safe_print(f"Run mode : {args.run_mode}")
1259- if args.run_mode == "sim":
1260- safe_print(f"Workers (sim mode) : {len(summary.device_ids)}")
1261- else:
1262- safe_print(f"DEVICE_IDs (npu mode) : {', '.join(summary.device_ids)}")
1263- safe_print(f"Total scripts found : {summary.total_original}")
1264- safe_print(f"Total execution time : {summary.total_time_sec:.2f} seconds")
1265- safe_print(f"Scripts executed : {len(summary.success_list) + len(summary.failure_list)}")
1266- safe_print(f"✅ Successful : {len(summary.success_list)}")
1267- safe_print(f"❌ Failed : {len(summary.failure_list)}")
1268- if summary.skipped_sim_list:
1269- safe_print(f"⏭️ Skipped (no sim support): {len(summary.skipped_sim_list)}")
1270- if summary.skipped_no_tests_list:
1271- safe_print(f"⏭️ Skipped (no main/test): {len(summary.skipped_no_tests_list)}")
1272- if summary.skipped_pytest_mark_list:
1273- safe_print(f"⏭️ Skipped (@pytest.mark.skip): {len(summary.skipped_pytest_mark_list)}")
1274- if summary.skipped_pytest_disabled_list:
1275- safe_print(f"⏭️ Skipped (pytest disabled): {len(summary.skipped_pytest_disabled_list)}")
1276- 
1277- if summary.failure_list:
1278- safe_print("\nFailed Scripts:")
1279- for r in summary.failure_list:
1280- reason = r.reason or "Unknown"
1281- snippet = r.output_snippet or ""
1282- safe_print(f" • {r.rel_path}{reason}")
1283- if args.show_fail_details and snippet:
1284- safe_print(" Output preview:")
1285- for line in snippet.splitlines():
1286- safe_print(f" {line}")
1287- safe_print()
1288- 
1289- if summary.skipped_sim_list:
1290- safe_print("\nSkipped Due to Lack of Sim Support:")
1291- for r in summary.skipped_sim_list:
1292- safe_print(f" • {r.rel_path}")
1293- 
1294- if summary.skipped_no_tests_list:
1295- safe_print("\nSkipped Due to No Executable Content:")
1296- for r in summary.skipped_no_tests_list:
1297- safe_print(f" • {r.rel_path}")
1298- 
1299- if summary.skipped_pytest_mark_list:
1300- safe_print("\nSkipped Due to @pytest.mark.skip:")
1301- for r in summary.skipped_pytest_mark_list:
1302- safe_print(f" • {r.rel_path}")
1303- 
1304- if summary.skipped_pytest_disabled_list:
1305- safe_print("\nSkipped Due to Pytest Auto-Detect Disabled:")
1306- for r in summary.skipped_pytest_disabled_list:
1307- safe_print(f" • {r.rel_path}")
1308- 
1309- safe_print("=" * 60)
1310- 
1311- 
1312-def main() -> None:
1313- # Initialize ProcessManager early for signal handling
1314- process_manager = ProcessManager()
1315- process_manager.setup_signal_handlers()
1316- 
1317- parser = argparse.ArgumentParser(
1318- description="Execute and validate Python scripts with configurable "
1319- "parallel retries and final serial fallback."
1320- )
1321- parser.add_argument(
1322- "-t", "--target",
1323- type=str,
1324- required=True,
1325- help="Target: either a .py file path or a directory path."
1326- )
1327- parser.add_argument(
1328- "-r", "--run_mode",
1329- choices=["npu", "sim"],
1330- default="npu",
1331- help="Execution mode: 'npu' (default) or 'sim'. "
1332- "In 'sim' mode, only scripts supporting --run_mode are executed."
1333- )
1334- parser.add_argument(
1335- "-d", "--device_ids",
1336- type=str,
1337- default="0",
1338- help="Comma-separated list of DEVICE_IDs (e.g., '0,1,2,3'). Default: '0'. "
1339- "Only effective in 'npu' mode. In 'sim' mode, use --workers instead."
1340- )
1341- parser.add_argument(
1342- "-w", "--workers",
1343- type=int,
1344- default=DEFAULT_SIM_WORKERS,
1345- help=f"Number of parallel workers (only effective in 'sim' mode). "
1346- f"In 'npu' mode, parallelism is determined by device count. "
1347- f"Default: {DEFAULT_SIM_WORKERS}."
1348- )
1349- parser.add_argument(
1350- "example_id",
1351- type=str,
1352- default=None,
1353- nargs='?',
1354- help="Optional test identifier (e.g., 'test_add' or "
1355- "'test_file.py::test_add') to pass to script or pytest."
1356- )
1357- parser.add_argument(
1358- "--timeout",
1359- type=int,
1360- default=DEFAULT_SCRIPT_TIMEOUT,
1361- help=f"Per-script execution timeout in seconds (default: {DEFAULT_SCRIPT_TIMEOUT})."
1362- )
1363- parser.add_argument(
1364- "--parallel_retries",
1365- type=int,
1366- default=DEFAULT_PARALLEL_RETRIES,
1367- help=f"Number of additional parallel retry rounds after the initial run "
1368- f"(default: {DEFAULT_PARALLEL_RETRIES}). "
1369- f"Total parallel rounds = 1 (initial) + N (retries)."
1370- )
1371- parser.add_argument(
1372- "--serial_retries",
1373- type=int,
1374- default=DEFAULT_SERIAL_RETRIES,
1375- help=f"Maximum number of serial retry rounds in single-device mode "
1376- f"(default: {DEFAULT_SERIAL_RETRIES}). "
1377- f"Total serial runs = 1 (initial) + N (retries). Set to 0 to disable retries."
1378- )
1379- parser.add_argument(
1380- "--show-fail-details",
1381- action="store_true",
1382- help=f"Show last {OUTPUT_SNIPPET_LINES} lines of output for each failed script in the final summary."
1383- )
1384- parser.add_argument(
1385- "--no-pytest-auto-detect",
1386- action="store_false",
1387- dest="pytest_auto_detect",
1388- default=True,
1389- help="Disable automatic detection and execution of pytest-style tests. "
1390- "By default, scripts without a '__main__' guard but with pytest-style tests "
1391- "will be executed using pytest. With this flag, such scripts will be skipped."
1392- )
1393- parser.add_argument(
1394- "--no-skip-pytest-mark-skip",
1395- action="store_false",
1396- dest="skip_pytest_mark_skip",
1397- default=True,
1398- help="Disable the skipping of scripts based on @pytest.mark.skip decorator. "
1399- "By default, scripts with @pytest.mark.skip are skipped."
1400- )
1401- parser.add_argument(
1402- "--no-serial-fallback",
1403- action="store_true",
1404- dest="no_serial_fallback",
1405- default=False,
1406- help="Skip the final serial retry loop in multi-device mode. "
1407- "By default, when parallel execution has failures, a serial retry loop is executed. "
1408- "With this flag, serial fallback is skipped and only parallel retries are performed."
1409- )
1410- args = parser.parse_args()
1411- 
1412- # Parse device IDs
1413- device_ids = [d.strip() for d in args.device_ids.split(",") if d.strip()]
1414- if not device_ids:
1415- print("Error: --device_ids cannot be empty.", file=sys.stderr)
1416- sys.exit(1)
1417- 
1418- target = Path(args.target).resolve()
1419- self_path = Path(__file__).resolve()
1420- 
1421- if target.is_file():
1422- if target.suffix != ".py":
1423- print(f"Error: Target file '{target}' is not a .py file.", file=sys.stderr)
1424- sys.exit(1)
1425- if target.resolve() == self_path:
1426- print("Error: Cannot execute this validator script itself.", file=sys.stderr)
1427- sys.exit(1)
1428- py_files = [target]
1429- target_dir = target.parent
1430- elif target.is_dir():
1431- py_files = sorted(target.rglob("*.py"))
1432- py_files = [f for f in py_files if f.resolve() != self_path]
1433- target_dir = target
1434- else:
1435- print(f"Error: Target '{target}' does not exist.", file=sys.stderr)
1436- sys.exit(1)
1437- 
1438- if not py_files:
1439- print(f"No valid .py files found in target.")
1440- return
1441- 
1442- relative_paths = [str(f.relative_to(target_dir)) for f in py_files]
1443- relative_paths.sort()
1444- 
1445- # Pre-analyze all scripts to cache results and check pytest requirement
1446- script_analyses: Dict[Path, ScriptAnalysis] = {}
1447- for f in py_files:
1448- script_analyses[f] = get_script_analysis(f)
1449- 
1450- need_pytest = args.pytest_auto_detect and any(
1451- not analysis.has_main_guard and analysis.has_pytest_tests
1452- for analysis in script_analyses.values()
1453- )
1454- pytest_available = shutil.which("pytest") is not None
1455- 
1456- if need_pytest and not pytest_available:
1457- print("Error: Some scripts require 'pytest' but it is not installed.", file=sys.stderr)
1458- print(" Please install pytest with: pip install pytest", file=sys.stderr)
1459- sys.exit(1)
1460- 
1461- # Handle workers and device configuration based on run mode
1462- if args.run_mode == "sim":
1463- # SIM mode: workers parameter determines parallelism
1464- # Create virtual devices for SIM mode (just placeholders for the queue)
1465- virtual_devices = [str(i) for i in range(args.workers)]
1466- actual_device_ids = virtual_devices
1467- print(f"💡 Sim mode: using {args.workers} virtual workers")
1468- 
1469- # Determine if we should use single or multi-device strategy based on workers
1470- is_single_device = (args.workers == 1)
1471- else:
1472- # NPU mode: device_ids determines parallelism, workers parameter is ignored
1473- actual_device_ids = device_ids
1474- print(f"💡 NPU mode: using {len(device_ids)} physical devices")
1475- 
1476- # Determine if we should use single or multi-device strategy based on physical devices
1477- is_single_device = (len(device_ids) == 1)
1478- 
1479- print(f"Run mode : {args.run_mode}")
1480- if args.run_mode == "sim":
1481- print(f"Workers (sim mode): {args.workers}")
1482- else:
1483- print(f"DEVICE_IDs (npu mode): {', '.join(device_ids)}")
1484- 
1485- if is_single_device:
1486- print("Execution mode : Serial (single device)")
1487- else:
1488- print(f"Parallel retries : {args.parallel_retries} "
1489- f"(total parallel rounds = {args.parallel_retries + 1})")
1490- if args.no_serial_fallback:
1491- print("Serial fallback : Disabled (--no-serial-fallback)")
1492- else:
1493- print(f"Serial fallback : Enabled (max retries: {args.serial_retries})")
1494- if args.example_id:
1495- print(f"Test selector : {args.example_id}")
1496- if not args.pytest_auto_detect:
1497- print("Pytest auto-detect: Disabled (pytest-only scripts will be skipped)")
1498- else:
1499- print("Pytest auto-detect: Enabled")
1500- print(f"Target : {target}")
1501- print(f"Found {len(relative_paths)} .py file(s).")
1502- print("=" * 60)
1503- 
1504- # Create thread-safe print function via ProcessManager
1505- safe_print = process_manager.create_safe_print()
1506- 
1507- start_time = time.perf_counter()
1508- exit_code = 0
1509- cancelled_by_signal = False
1510- 
1511- try:
1512- # Initial candidate list: all scripts that are not skipped
1513- candidates_to_run = []
1514- skipped_sim_scripts = []
1515- skipped_no_tests_scripts = []
1516- skipped_pytest_mark_scripts = []
1517- skipped_pytest_disabled_scripts = []
1518- 
1519- for rel_path in relative_paths:
1520- # Check for early shutdown
1521- if process_manager.is_shutdown_requested():
1522- safe_print("\n⚠️ Shutdown requested during initialization. Aborting.")
1523- cancelled_by_signal = True
1524- break
1525- 
1526- full_path = target_dir / rel_path
1527- analysis = script_analyses.get(full_path) or get_script_analysis(full_path)
1528- 
1529- if not analysis.has_main_guard and not analysis.has_pytest_tests:
1530- skipped_no_tests_scripts.append(rel_path)
1531- continue
1532- 
1533- # Skip pytest-only scripts when pytest auto-detection is disabled
1534- if not analysis.has_main_guard and analysis.has_pytest_tests and not args.pytest_auto_detect:
1535- skipped_pytest_disabled_scripts.append(rel_path)
1536- continue
1537- 
1538- if args.run_mode == "sim" and not analysis.supports_run_mode:
1539- skipped_sim_scripts.append(rel_path)
1540- continue
1541- 
1542- if args.skip_pytest_mark_skip and analysis.has_pytest_skip_mark:
1543- skipped_pytest_mark_scripts.append(rel_path)
1544- continue
1545- 
1546- candidates_to_run.append(rel_path)
1547- 
1548- # Record skipped results for final summary
1549- skipped_sim_results = [
1550- ExecutionResult.skipped(
1551- p, ExecutionStatus.SKIPPED_SIM, "script does not support --run_mode"
1552- )
1553- for p in skipped_sim_scripts
1554- ]
1555- skipped_no_tests_results = [
1556- ExecutionResult.skipped(
1557- p, ExecutionStatus.SKIPPED_NO_TESTS,
1558- "no '__main__' guard and no pytest-style tests"
1559- )
1560- for p in skipped_no_tests_scripts
1561- ]
1562- skipped_pytest_mark_results = [
1563- ExecutionResult.skipped(
1564- p, ExecutionStatus.SKIPPED_PYTEST_MARK,
1565- "contains @pytest.mark.skip decorator"
1566- )
1567- for p in skipped_pytest_mark_scripts
1568- ]
1569- skipped_pytest_disabled_results = [
1570- ExecutionResult.skipped(
1571- p, ExecutionStatus.SKIPPED_PYTEST_DISABLED, "pytest auto-detect disabled"
1572- )
1573- for p in skipped_pytest_disabled_scripts
1574- ]
1575- 
1576- current_candidates = candidates_to_run[:]
1577- all_results_map: Dict[str, ExecutionResult] = {}
1578- 
1579- # Add skipped results to final map immediately
1580- all_skipped_results = (
1581- skipped_sim_results + skipped_no_tests_results +
1582- skipped_pytest_mark_results + skipped_pytest_disabled_results
1583- )
1584- for r in all_skipped_results:
1585- all_results_map[r.rel_path] = r
1586- 
1587- success_list: List[ExecutionResult] = []
1588- failure_list: List[ExecutionResult] = []
1589- 
1590- if cancelled_by_signal:
1591- pass # Early exit due to signal
1592- elif not current_candidates:
1593- safe_print("ℹ️ No executable scripts found. All were skipped.")
1594- else:
1595- # Create execution strategy with ProcessManager
1596- if is_single_device:
1597- strategy: ExecutionStrategy = SingleDeviceStrategy(
1598- args, target_dir, actual_device_ids, args.timeout,
1599- safe_print, process_manager
1600- )
1601- else:
1602- strategy = MultiDeviceStrategy(
1603- args, target_dir, actual_device_ids, args.timeout,
1604- safe_print, process_manager
1605- )
1606- 
1607- # Execute scripts using the selected strategy
1608- success_list, failure_list = strategy.execute(current_candidates, all_results_map)
1609- 
1610- # Check if we were cancelled during execution
1611- if process_manager.is_shutdown_requested():
1612- cancelled_by_signal = True
1613- 
1614- # Generate final summary
1615- total_time_sec = time.perf_counter() - start_time
1616- 
1617- if cancelled_by_signal:
1618- safe_print("\n" + "=" * 60)
1619- safe_print("⚠️ EXECUTION CANCELLED BY SIGNAL")
1620- safe_print("=" * 60)
1621- safe_print(f"Total execution time before cancellation: {total_time_sec:.2f} seconds")
1622- 
1623- # Count results by status
1624- cancelled_count = sum(1 for r in all_results_map.values()
1625- if r.status == ExecutionStatus.CANCELLED)
1626- completed_success = sum(1 for r in all_results_map.values()
1627- if r.status == ExecutionStatus.SUCCESS)
1628- completed_failure = sum(1 for r in all_results_map.values()
1629- if r.status == ExecutionStatus.FAILURE)
1630- 
1631- safe_print(f"Scripts completed successfully: {completed_success}")
1632- safe_print(f"Scripts failed: {completed_failure}")
1633- safe_print(f"Scripts cancelled: {cancelled_count}")
1634- safe_print("=" * 60)
1635- exit_code = 130 # Standard exit code for SIGINT
1636- else:
1637- summary = SummaryData(
1638- success_list=success_list,
1639- failure_list=failure_list,
1640- skipped_sim_list=skipped_sim_results,
1641- skipped_no_tests_list=skipped_no_tests_results,
1642- skipped_pytest_mark_list=skipped_pytest_mark_results,
1643- skipped_pytest_disabled_list=skipped_pytest_disabled_results,
1644- args=args,
1645- target=target,
1646- device_ids=actual_device_ids,
1647- total_time_sec=total_time_sec,
1648- safe_print=safe_print
1649- )
1650- _print_final_summary(summary)
1651- exit_code = 1 if len(failure_list) > 0 else 0
1652- 
1653- finally:
1654- # Final cleanup: ensure all processes are terminated
1655- if process_manager.is_shutdown_requested():
1656- process_manager.cleanup_all()
1657- 
1658- sys.exit(exit_code)
1659- 
1660- 
1661if __name__ == "__main__":11if __name__ == "__main__":
1662- main()12+ pass
Dexamples/validate_examples.sh+0-44
@@ -1,44 +0,0 @@
1-#!/bin/bash
2-# Copyright (c) 2025 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-# 1. Execute directory on single NPU device
12-python3 examples/validate_examples.py -t examples/02_intermediate -d 0
13- 
14-# 2. Multi-device parallel execution
15-python3 examples/validate_examples.py -t examples -d 0,1,2,3
16- 
17-# 3. Execute specific script on device 0
18-python3 examples/validate_examples.py -t examples/01_beginner/basic/basic_ops.py -d 0
19- 
20-# 4. Simulation mode (single virtual worker)
21-python3 examples/validate_examples.py -t examples --run_mode sim -w 1
22- 
23-# 5. Concurrent execution in simulation mode (16 virtual workers)
24-python3 examples/validate_examples.py -t examples --run_mode sim -w 16
25- 
26-# 6. Custom timeout per script
27-python3 examples/validate_examples.py -t examples/02_intermediate -d 0 --timeout 120
28- 
29-# 7. Show failure diagnostics in summary
30-python3 examples/validate_examples.py -t examples -d 0 --show-fail-details
31- 
32-# 8. Include scripts marked with @pytest.mark.skip (override default behavior)
33-python3 examples/validate_examples.py -t examples -d 0 --no-skip-pytest-mark-skip
34- 
35-# 9. Disable pytest auto-detection (skip scripts without __main__ guard)
36-python3 examples/validate_examples.py -t examples -d 0 --no-pytest-auto-detect
37- 
38-# 10. Skip serial fallback in multi-device mode (only parallel retries)
39-python3 examples/validate_examples.py -t examples -d 0,1,2,3 --no-serial-fallback
40- 
41-# 11. Full configuration
42-python3 examples/validate_examples.py -t examples -d 0,1,2,3
43- --parallel_retries 2 --serial_retries 5 --timeout 300
44- --show-fail-details
Mpython/pypto/__init__.py+1-1
@@ -43,7 +43,7 @@ from .logging import * # noqa
43# Import frontend after all other imports to avoid circular imports43# Import frontend after all other imports to avoid circular imports
44from . import frontend44from . import frontend
45 45 
46- 46+jit = frontend.jit
47tensor = Tensor47tensor = Tensor
48element = Element48element = Element
49symbolic_scalar = SymbolicScalar49symbolic_scalar = SymbolicScalar