已合并
refactor(examples): Rewrite the basic example #4922
lidongsheng创建于 7月16日
refactor(examples): Rewrite the basic example #4922
已合并
共 11 个文件变更+184-3020
| @@ -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}) |
| 26 | source /usr/local/Ascend/ascend-toolkit/set_env.sh | 27 | source /usr/local/Ascend/ascend-toolkit/set_env.sh |
| 27 | - | ||
| 28 | -# 设置设备 ID | ||
| 29 | -export TILE_FWK_DEVICE_ID=0 | ||
| 30 | ``` | 28 | ``` |
| 31 | 29 | ||
| 32 | ### 执行脚本 | 30 | ### 执行脚本 |
| @@ -5,7 +5,8 @@ This sample demonstrates the simplest tensor addition operation in PyPTO. It is | |||
| 5 | ## Overview | 5 | ## Overview |
| 6 | 6 | ||
| 7 | This sample demonstrates the complete PyPTO development process through a simple tensor addition: | 7 | This 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}) |
| 26 | source /usr/local/Ascend/ascend-toolkit/set_env.sh | 27 | source /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 Script | 31 | ### Execute the Script |
| @@ -10,192 +10,69 @@ | |||
| 10 | # ----------------------------------------------------------------------------------------------------------- | 10 | # ----------------------------------------------------------------------------------------------------------- |
| 11 | """ | 11 | """ |
| 12 | Hello World Example for PyPTO | 12 | Hello World Example for PyPTO |
| 13 | - | ||
| 14 | -This example demonstrates the simplest tensor addition. | ||
| 15 | """ | 13 | """ |
| 14 | + | ||
| 16 | import os | 15 | import os |
| 17 | import sys | 16 | import sys |
| 18 | import argparse | 17 | import argparse |
| 19 | import pypto | 18 | import pypto |
| 20 | import torch | 19 | import torch |
| 21 | -import numpy as np | 20 | + |
| 22 | -from numpy.testing import assert_allclose | 21 | +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_id | 34 | + 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.NPU | 40 | + 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_kernel | 51 | + 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 | ||
| 92 | def main(): | 55 | def 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 examples | 66 | + 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 requested | 69 | + 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 provided | 73 | + 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 | ||
| 201 | if __name__ == "__main__": | 78 | if __name__ == "__main__": |
| @@ -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}) |
| 50 | source /usr/local/Ascend/ascend-toolkit/set_env.sh | 37 | source /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 | # 运行所有示例 |
| 60 | python3 basic_ops.py | 44 | python3 basic_ops.py |
| 61 | 45 | ||
| 62 | -# 运行特定示例(例如示例 2:逐元素运算) | 46 | +# 运行特定示例(例如示例 1:加法运算) |
| 63 | -python3 basic_ops.py elementwise_ops::test_elementwise_ops | 47 | +python3 basic_ops.py -t add |
| 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` 已正确安装并能识别到昇腾显卡。 |
| @@ -2,37 +2,24 @@ | |||||||
| 2 | 2 | ||||||
| 3 | This sample demonstrates the basic operator operations and typical programming patterns of PyPTO. It is very suitable for beginners to quickly get started. | 3 | This 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 Features | 5 | ## 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 Structure | 12 | ## 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 | ||||||
| 35 | For more detailed usage of various operators, refer to the sibling directories: | 21 | For 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}) | ||||
| 50 | source /usr/local/Ascend/ascend-toolkit/set_env.sh | 37 | source /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 Script | 40 | ### Execute the Script | ||||
| @@ -59,40 +43,11 @@ export TILE_FWK_DEVICE_ID=0 | |||||||
| 59 | # Run all samples | 43 | # Run all samples | ||||
| 60 | python3 basic_ops.py | 44 | python3 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_ops | 47 | +python3 basic_ops.py -t add | ||||
🟡 Medium Priority 同样的问题:英文 README 第 45-46 行描述 "Run a specific sample (for example, Sample 2: Element-wise operation)" 但命令为 建议:将 改动建议
![]() ![]() | |||||||
| 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 | ## Precautions | 50 | ## 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. | ||||
| @@ -10,419 +10,199 @@ | |||
| 10 | # ----------------------------------------------------------------------------------------------------------- | 10 | # ----------------------------------------------------------------------------------------------------------- |
| 11 | """ | 11 | """ |
| 12 | Basic Operations Quick-Start for PyPTO | 12 | Basic 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 | ||
| 32 | import sys | 15 | import sys |
| 33 | import argparse | 16 | import argparse |
| 34 | import pypto | 17 | import pypto |
| 35 | import torch | 18 | import 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.NPU | 24 | +@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.SIM | 26 | + 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 | - | ||
| 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.0 | 42 | + 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) |
| 130 | def erfc_kernel( | 46 | def 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 | - | ||
| 162 | def matmul_kernel( | 64 | def 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, 64 | 74 | 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 | - | ||
| 195 | def sum_kernel( | 85 | def 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 Configuration | 103 | + return (x + y - 1) // y |
| 223 | -# ============================================================================ | ||
| 224 | - | ||
| 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 + b | 143 | + 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) | ||
🟡 Medium Priority
这导致:
建议:在 ![]() ![]() | |||
| 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 | - | ||
| 336 | def main(): | 170 | def 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 | ||
| 428 | if __name__ == "__main__": | 208 | if __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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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() | ||
| @@ -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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 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 | - | ||
| 1661 | if __name__ == "__main__": | 11 | if __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 | ||
| @@ -43,7 +43,7 @@ from .logging import * # noqa | |||
| 43 | # Import frontend after all other imports to avoid circular imports | 43 | # Import frontend after all other imports to avoid circular imports |
| 44 | from . import frontend | 44 | from . import frontend |
| 45 | 45 | ||
| 46 | - | 46 | +jit = frontend.jit |
| 47 | tensor = Tensor | 47 | tensor = Tensor |
| 48 | element = Element | 48 | element = Element |
| 49 | symbolic_scalar = SymbolicScalar | 49 | symbolic_scalar = SymbolicScalar |


🟡 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:加法运算)"。add