已合并
feat: 支持ATT analysis tool #1855
feat: 支持ATT analysis tool #1855
已合并
zhang_shengjie创建于 8月27日
共 48 个文件变更+5568-0
@@ -66,6 +66,12 @@ repos:
66 # OAT 合规性检查 (Open Source Audit Tool)66 # OAT 合规性检查 (Open Source Audit Tool)
67 - repo: local67 - repo: local
68 hooks:68 hooks:
69+ - id: reject-docs-superpowers
70+ name: Reject docs/superpowers changes
71+ entry: bash scripts/reject_forbidden_paths.sh
72+ language: system
73+ pass_filenames: false
74+ stages: [pre-commit]
69 - id: oat-check75 - id: oat-check
70 name: OAT Compliance Check76 name: OAT Compliance Check
71 entry: bash scripts/oat_check.sh77 entry: bash scripts/oat_check.sh
@@ -0,0 +1,122 @@
1+<!--
2+Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+Licensed under the CANN Open Software License Agreement Version 2.0.
4+-->
5+ 
6+# Autofuse 工具说明
7+ 
8+这里的工具用于开发调试和问题定位,不属于 Autofuse 运行时接口。下面按“职责—输入—命令—效果”说明当前工具;参数变化以命令的 `--help` 为准。
9+ 
10+## ATT-ANALYZE:模板、tiling 和 profiling 分析
11+ 
12+工具目录:[`att_analyze/`](att_analyze/),统一入口:
13+ 
14+```bash
15+python3 autofuse/tools/att_analyze/src/att.py --help
16+```
17+ 
18+### `summary`:汇总 ATT 日志
19+ 
20+- 职责:从 `[PROF]` 日志提取算子、graph/result/group/case、tiling 参数、objective 和 result performance。
21+- 输入:一个日志文件或日志目录。
22+- 使用:
23+ 
24+ ```bash
25+ python3 autofuse/tools/att_analyze/src/att.py summary run.log -f csv -o summary.csv
26+ ```
27+ 
28+- 效果:生成包含 `Operator`、`Case`、`Objective Value` 等列的 CSV;日志字段缺失时保留空值并标记 `parse_status`,不会用 0 或 objective 冒充实测值。
29+ 
30+### `compare`:比较两次选择结果
31+ 
32+- 职责:比较默认配置与 PGO、强制模板或其他候选配置的 CSV,识别字段和性能差异。
33+- 输入:两个 `summary` 生成的 CSV;同一 graph/result/group 即使 case 不同也会匹配。
34+- 使用:
35+ 
36+ ```bash
37+ python3 autofuse/tools/att_analyze/src/att.py compare default.csv candidate.csv -f text -o compare.txt
38+ ```
39+ 
40+- 效果:输出共同算子、只存在于一侧的算子、case/tiling 差异和性能变化;负的周期差通常表示候选更快。
41+ 
42+### `evidence`:导出机器可读证据
43+ 
44+- 职责:把原始 ATT 日志转换为带来源行号和解析状态的 JSONL,供 skill 或其他程序继续分析。
45+- 输入:日志文件或目录。
46+- 使用:
47+ 
48+ ```bash
49+ python3 autofuse/tools/att_analyze/src/att.py evidence run.log -o evidence/
50+ ```
51+ 
52+- 效果:生成 `att-evidence.jsonl` 和工具清单;每条记录包含算子、case、tiling、objective、`source_path`、`source_line` 和 `parse_status`。
53+ 
54+### `split-slog`:拆分 DFX/PROF 日志
55+ 
56+- 职责:按算子和 graph/result/group/case 拆分编译期 `[DFX]` model-info 片段及运行期 `[PROF]` 片段。
57+- 输入:slog、stdout 或日志目录;不要求固定文件名。
58+- 使用:
59+ 
60+ ```bash
61+ python3 autofuse/tools/att_analyze/src/att.py split-slog slog/ --op FlashAttentionScore --case r=1,g=0,c=2 -o split/
62+ ```
63+ 
64+- 效果:在 `split/<operator>/compiler/...` 和 `split/<operator>/runtime/...` 下生成真实编号的 `case*.log`,便于逐 case 查看 DFX 边界。
65+ 
66+### `perf-formula`:分析 `[PERF]` pipe 公式
67+ 
68+- 职责:解析 tiling 函数输出的 Load/Store/Vector pipe 公式,标出瓶颈并比较多个 case 的敏感参数。
69+- 输入:包含 tiling 源文件的目录和 `[PERF]` 日志。
70+- 使用:
71+ 
72+ ```bash
73+ python3 autofuse/tools/att_analyze/src/att.py perf-formula generated/ run.log --case r=0,g=0,c=1 -o perf/
74+ ```
75+ 
76+- 效果:生成 `perf_formula.svg`;图中红色节点是当前最大 pipe,跨 case 区域显示方差最大的参数。没有 `[PERF]` 时命令返回非零并提示证据不足。
77+ 
78+### `verify-tiling`:验证 TensorFlow/Inductor tiling ABI
79+ 
80+- 职责:编译用户提供的 tiling 函数并按明确 ABI 调用 `AutofuseTiling`,检查 block_dim 和 workspace 返回值。
81+- 输入:TensorFlow tiling C++ 文件目录,或包含 `output_code.py` 的 Inductor 目录;需要 Ascend 编译环境。
82+- 使用:
83+ 
84+ ```bash
85+ python3 autofuse/tools/att_analyze/src/att.py verify-tiling generated/ \
86+ --scene inductor --input-json input.json --keep-build -o verify/
87+ 
88+ # TensorFlow 动态 ABI;执行前会打印实际使用的 aiv_num,--aiv-num 可覆盖 preset
89+ python3 autofuse/tools/att_analyze/src/att.py verify-tiling generated/ \
90+ --scene tf --preset B --aiv-num 56 -o verify/
91+ ```
92+ 
93+- 效果:控制台显示编译和运行结果,`verify/result.json` 保存状态、返回值和失败原因;编译或运行失败会返回非零退出码。`output_code.py` 只读取字面量 artifact,不执行其中代码。
94+- 执行前会打印 `aiv_num`、`ub_size`、动态维度和配置来源,方便用户核对硬件;`preset_B` 默认 `aiv_num=56`,不应视为所有芯片的固定规格。`aiv_num` 仅用于 TensorFlow tiling ABI,Inductor ABI 不使用它。
95+ 
96+## NWA `fusion_precision_analyzer`:融合精度定位
97+ 
98+工具目录:[`nwa_tool/`](nwa_tool/)。当关闭自动融合精度正常、开启自动融合精度下降时,比较两侧 dump 图和 NPY 数据,定位造成误差的融合算子。
99+ 
100+- 模式 1(默认):按 dump 图映射批量比较融合算子输入/输出。
101+- 模式 2:直接比较两个 NPY 文件。
102+ 
103+示例:
104+ 
105+```bash
106+python3 autofuse/tools/nwa_tool/fusion_precision_analyzer.py \
107+ --af-open-graph open/Build.json --af-close-graph close/Build.json \
108+ --af-open-data open/npy --af-close-data close/npy --compare-input
109+ 
110+python3 autofuse/tools/nwa_tool/fusion_precision_analyzer.py --mode 2 \
111+ --npy-a open.npy --npy-b close.npy
112+```
113+ 
114+效果:输出余弦相似度、最大绝对误差、最大相对误差及状态(如 `OK`、`FILE_NOT_FOUND`、`SHAPE_MISMATCH`);完整参数和格式转换规则见 [`nwa_tool/README.md`](nwa_tool/README.md)。
115+ 
116+## 共同注意事项
117+ 
118+- ATT `[PROF]`/`[DFX]` 采集通常需要在用户执行命令中设置 `ASCEND_SLOG_PRINT_TO_STDOUT=1`、`ASCEND_GLOBAL_LOG_LEVEL=1`;`att_profiling` 只表示 tiling 函数耗时统计。
119+- skill 默认只读。重新执行、编译、profiling 或 PGO 必须由用户提供命令并明确授权。
120+- 原始证据和分析结论应分开归档,已有归档不会覆盖。
121+- 安装依赖:ATT Excel 输出需要 `openpyxl`,NWA 工具需要 `numpy`。
122+- `autofuse/tools/att_analyze/tests/data/` 是固定回归样例。真实日志格式变化时,应脱敏新增 fixture 并同步 `summary`/`evidence` 期望结果,不要覆盖旧样例。
@@ -0,0 +1,49 @@
1+<!--
2+Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+Licensed under the CANN Open Software License Agreement Version 2.0.
4+-->
5+ 
6+# Autofuse Tools
7+ 
8+These tools support Autofuse development, debugging, and issue analysis. They are not runtime APIs. Use each command's `--help` output as the authoritative reference for options.
9+ 
10+## ATT-ANALYZE: template, tiling, and profiling analysis
11+ 
12+Tool directory: [`att_analyze/`](att_analyze/). The common entry point is:
13+ 
14+```bash
15+python3 autofuse/tools/att_analyze/src/att.py --help
16+```
17+ 
18+- `summary`: parses a log file or directory and exports operator, case, tiling, objective, and measured performance fields to CSV or text. Missing fields remain empty and are marked by `parse_status`.
19+- `compare`: compares two `summary` CSV files, such as default Autofuse versus PGO or a forced template. It reports matched operators, case/tiling differences, and performance changes.
20+- `evidence`: converts logs to JSONL evidence with source paths, line numbers, and parse status for subsequent automated analysis.
21+- `split-slog`: separates compiler DFX and runtime PROF fragments by operator and graph/result/group/case. It accepts slog, stdout, or a log directory without requiring fixed filenames.
22+- `perf-formula`: parses `[PERF]` pipe formulas from tiling output, identifies the bottleneck, and writes a `perf_formula.svg` comparison. It returns a non-zero status when the required evidence is absent.
23+- `verify-tiling`: compiles user-provided TensorFlow or Inductor tiling code and checks the `AutofuseTiling` ABI, including block dimensions and workspace results. It prints the selected `aiv_num` and saves `result.json`; `preset_B` defaults to 56 but must be checked against the target hardware.
24+ 
25+Examples:
26+ 
27+```bash
28+python3 autofuse/tools/att_analyze/src/att.py summary run.log -f csv -o summary.csv
29+python3 autofuse/tools/att_analyze/src/att.py compare default.csv candidate.csv -f text -o compare.txt
30+python3 autofuse/tools/att_analyze/src/att.py evidence run.log -o evidence/
31+python3 autofuse/tools/att_analyze/src/att.py split-slog slog/ --op FlashAttentionScore -o split/
32+python3 autofuse/tools/att_analyze/src/att.py perf-formula generated/ run.log -o perf/
33+python3 autofuse/tools/att_analyze/src/att.py verify-tiling generated/ --scene tf --preset B --aiv-num 56 -o verify/
34+```
35+ 
36+## NWA `fusion_precision_analyzer`: fusion precision diagnosis
37+ 
38+Tool directory: [`nwa_tool/`](nwa_tool/). Compare graphs and NPY data from Autofuse enabled and disabled runs to locate the fusion operator responsible for a precision regression.
39+ 
40+```bash
41+python3 autofuse/tools/nwa_tool/fusion_precision_analyzer.py \
42+ --af-open-graph open/Build.json --af-close-graph close/Build.json \
43+ --af-open-data open/npy --af-close-data close/npy --compare-input
44+ 
45+python3 autofuse/tools/nwa_tool/fusion_precision_analyzer.py --mode 2 \
46+ --npy-a open.npy --npy-b close.npy
47+```
48+ 
49+The tool reports cosine similarity, maximum absolute and relative errors, and statuses such as `OK`, `FILE_NOT_FOUND`, and `SHAPE_MISMATCH`. See [`nwa_tool/README.md`](nwa_tool/README.md) for the complete option and format reference.
@@ -0,0 +1,33 @@
1+# 输出文件
2+output/
3+*.csv
4+*.xlsx
5+*.txt
6+ 
7+# Python缓存
8+__pycache__/
9+*.py[cod]
10+*$py.class
11+*.so
12+ 
13+# 测试输出
14+.pytest_cache/
15+.coverage
16+htmlcov/
17+ 
18+# IDE
19+.vscode/
20+.idea/
21+*.swp
22+*.swo
23+*~
24+ 
25+# 本地代码索引元数据
26+.serena/
27+ 
28+# 临时文件
29+comparison_output.txt
30+run_comparison.py
31+test_compare.py
32+test_csv_comparison.py
33+test_csv_tool.py
@@ -0,0 +1,68 @@
1+# ATT Analyze
2+ 
3+仓内 ATT 日志分析工具,入口为:
4+ 
5+```bash
6+python3 autofuse/tools/att_analyze/src/att.py --help
7+```
8+ 
9+支持 `summary`、`compare`、`split-slog`、`perf-formula`、`verify-tiling` 和 `evidence` 六个命令。`summary` 默认只读解析日志;`verify-tiling` 会编译并执行代码,使用前请确认输入目录和授权。
10+ 
11+`LogParser` 的 `OperatorSummary.parse_status` 用于标明证据是否完整:`ok` 表示日志包含完整选择信息,`inferred_graph_result` 表示仅从模板行推断 graph/result,`missing_group_case`、`missing_result_performance` 和 `missing_graph_result` 表示相应日志缺失。CSV 列保持历史含义不变。
12+ 
13+```bash
14+python3 autofuse/tools/att_analyze/src/att.py summary path/to/att.log -f csv -o /tmp/summary.csv
15+python3 autofuse/tools/att_analyze/src/att.py compare baseline.csv candidate.csv
16+python3 autofuse/tools/att_analyze/src/att.py evidence path/to/att.log -o /tmp/evidence
17+python3 autofuse/tools/att_analyze/src/att.py verify-tiling generated/ --scene tf --preset B --aiv-num 56
18+```
19+ 
20+工具不会把缺失值当作有效的 0;请在后续分析中根据 `parse_status` 决定是否需要补充日志。
21+ 
22+## 与 ATT 模板/tiling 分析 Skill 配合
23+ 
24+`att_analyze` 由本仓维护,Skill 调用的固定入口是
25+`autofuse/tools/att_analyze/src/att.py`,不需要安装或访问其他仓库。对已经采集的
26+数据做离线分析时,在仓库根目录执行:
27+ 
28+```bash
29+python3 .claude/skills/att-template-tiling-analysis/scripts/att_analysis.py \
30+ analyze --run-root <run-root> --output <report-dir>
31+```
32+ 
33+`run-root/default` 和 `run-root/pgo`(也支持 `base`)放入用户已执行得到的日志;
34+目录名称不固定时可显式传 `--default-root` 和 `--candidate-root`,脚本也会递归发现
35+`logs/`、`profiling/`、`kernel_meta/` 和 `dump/`。脚本只读取这些数据,不会自行
36+选择 case、编造执行命令或重新运行任务。Python 3.9+ 可运行基础分析,安装
37+`openpyxl` 后会额外生成 `summary.xlsx`。
38+ 
39+如果需要现场执行,用户需先提供完整 case 范围和命令。本地执行需要可用的
40+`python3`;远端执行使用标准 `ssh`,并要求远端 checkout 能访问相同的工具脚本。
41+`devssh` 只能作为用户明确提供的 wrapper。编译、profiling、PGO 和
42+`verify-tiling` 均需用户单独确认。
43+ 
44+原始证据与分析结论分开保存,建议布局如下:
45+ 
46+```text
47+run-root/ # 原始运行目录
48+ default/ pgo/
49+ att.log profile/ kernel_meta/ dump/
50+evidence-archive/<run-name>/ # 原始文件归档
51+report-archive/<run-name>/ # report.md、summary.csv、root-cause.jsonl 等
52+```
53+ 
54+归档脚本会为同名运行自动创建递增目录并写入 `archive-manifest.json`,不会覆盖
55+已有归档。详细交互契约和归档规则见 Skill 的
56+`references/execution-contract.md` 与 `references/archive-layout.md`。
57+ 
58+## preset 和真实日志维护
59+ 
60+`preset_B.json` 是 TensorFlow 动态 ABI 的示例输入,默认 `aiv_num=56`、
61+`ub_size=262144`,不代表所有芯片的硬件规格。执行 `verify-tiling` 时会打印
62+实际传入的 `aiv_num`、参数来源和动态维度;请根据目标设备核对,必要时使用
63+`--aiv-num` 或 `--input-json` 修改。`aiv_num` 是传给 TensorFlow tiling 的配置值,
64+Inductor ABI 不使用该字段。
65+ 
66+`tests/data/` 中的日志是固定回归样例,不会自动同步现场日志。遇到新的 CANN、
67+TensorFlow 或 Inductor 日志格式时,请对真实日志脱敏后新增样例,并同步增加
68+`summary`/`evidence` 的期望结果;保留旧样例以防止已有格式回归。
@@ -0,0 +1,81 @@
1+# ATT Analyze
2+ 
3+In-repository ATT log analysis utilities. Run:
4+ 
5+```bash
6+python3 autofuse/tools/att_analyze/src/att.py --help
7+```
8+ 
9+The CLI provides `summary`, `compare`, `split-slog`, `perf-formula`, `verify-tiling`, and `evidence`. `summary` is read-only. `verify-tiling` compiles and executes code, so review its inputs and authorization first.
10+ 
11+`LogParser` exposes `OperatorSummary.parse_status` to make incomplete evidence explicit. `ok` means complete selection records; `inferred_graph_result` means graph/result came from template lines; `missing_group_case`, `missing_result_performance`, and `missing_graph_result` identify missing records. Existing CSV column meanings are unchanged.
12+ 
13+## verify-tiling ABI input
14+ 
15+Custom input JSON must include an explicit ABI contract. `tf_static` uses no shape dimensions; `tf_dynamic` and `inductor` require one or more shape dimensions. `block_dim_width` is either 32 or 64.
16+ 
17+```json
18+{"dynamic_dims": [], "aiv_num": 48, "ub_size": 196608,
19+ "abi": {"kind": "tf_static", "shape_dims": 0, "block_dim_width": 32}}
20+```
21+ 
22+```json
23+{"dynamic_dims": [1024, 512], "aiv_num": 56, "ub_size": 262144,
24+ "abi": {"kind": "tf_dynamic", "shape_dims": 2, "block_dim_width": 32}}
25+```
26+ 
27+Unknown or missing ABI contracts are rejected before native code is called.
28+ 
29+## Using the ATT template/tiling analysis skill
30+ 
31+`att_analyze` is maintained in this repository. The skill always invokes
32+`autofuse/tools/att_analyze/src/att.py`; no checkout of another repository is
33+required. To analyze an existing run offline from the repository root:
34+ 
35+```bash
36+python3 .claude/skills/att-template-tiling-analysis/scripts/att_analysis.py \
37+ analyze --run-root <run-root> --output <report-dir>
38+```
39+ 
40+Place user-collected logs in `run-root/default` and `run-root/pgo` (or `base`),
41+or pass arbitrary variant directories with `--default-root` and `--candidate-root`.
42+The command recursively discovers logs, profiling, `kernel_meta/`, and `dump/`;
43+it does not require a producer-specific directory layout. The command
44+is read-only: it does not invent case scope or commands and does not rerun a
45+workload. Python 3.9+ is sufficient for the base analysis; installing
46+`openpyxl` additionally enables `summary.xlsx`.
47+ 
48+For live execution, provide the exact cases and command first. Local execution
49+requires `python3`; remote execution uses standard `ssh` and a checkout on the
50+remote host. A site-specific `devssh` wrapper is accepted only when explicitly
51+provided by the user. Build, profiling, PGO, and `verify-tiling` actions require
52+separate confirmation.
53+ 
54+Keep raw evidence and conclusions in separate archives:
55+ 
56+```text
57+run-root/ # raw run data
58+ default/ pgo/
59+ att.log profile/ kernel_meta/ dump/
60+evidence-archive/<run-name>/ # raw files
61+report-archive/<run-name>/ # report.md, summary.csv, root-cause.jsonl, ...
62+```
63+ 
64+The archive helper creates an incremented directory for duplicate run names and
65+writes `archive-manifest.json`; existing archives are never overwritten. See
66+the skill references for the execution contract and archive rules.
67+ 
68+## Presets and real-log maintenance
69+ 
70+`preset_B.json` is an example TensorFlow dynamic-ABI input. Its default
71+`aiv_num=56` and `ub_size=262144` are not guaranteed hardware specifications
72+for every chip. `verify-tiling` prints the effective `aiv_num`, its source, and
73+dynamic dimensions before compilation. Check the value against the target
74+device and override it with `--aiv-num` or `--input-json` when needed.
75+`aiv_num` is passed to the TensorFlow tiling ABI; the Inductor ABI does not use
76+this field.
77+ 
78+Logs under `tests/data/` are fixed regression fixtures and are not synchronized
79+with live runs. When a CANN, TensorFlow, or Inductor log format changes, add a
80+sanitized fixture from a real run and update the expected `summary`/`evidence`
81+results while keeping older fixtures for compatibility coverage.
@@ -0,0 +1,179 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+"""
13+使用示例:演示如何使用日志解析工具
14+"""
15+ 
16+from summary_templates import LogParser, print_summary_table
17+ 
18+ 
19+def example_1_cli_usage():
20+ """示例1:命令行调用(只展示命令,不会执行)"""
21+ print("示例1:命令行调用(只展示命令,不会执行)")
22+ print("=" * 80)
23+ print("请在仓库根目录执行以下命令,并将路径替换为自己的日志或产物目录:")
24+ commands = (
25+ "python3 autofuse/tools/att_analyze/src/att.py summary run.log -f csv -o summary.csv",
26+ "python3 autofuse/tools/att_analyze/src/att.py compare default.csv candidate.csv -f text -o compare.txt",
27+ "python3 autofuse/tools/att_analyze/src/att.py evidence run.log -o evidence/",
28+ "python3 autofuse/tools/att_analyze/src/att.py split-slog slog/ --op FlashAttentionScore --case r=1,g=0,c=2 -o split/",
29+ "python3 autofuse/tools/att_analyze/src/att.py perf-formula generated/ run.log --case r=0,g=0,c=1 -o perf/",
30+ "python3 autofuse/tools/att_analyze/src/att.py verify-tiling generated/ --scene tf --preset B --aiv-num 56 -o verify/",
31+ )
32+ for command in commands:
33+ print(f"$ {command}")
34+ print()
35+ 
36+ 
37+def example_2_basic_usage():
38+ """示例2:基本使用"""
39+ print("示例2:基本使用")
40+ print("=" * 80)
41+ 
42+ parser = LogParser()
43+ summaries = parser.parse_log_file("test_concat.log")
44+ print_summary_table(summaries)
45+ print()
46+ 
47+ 
48+def example_3_multiple_operators():
49+ """示例3:多个算子"""
50+ print("示例3:多个算子")
51+ print("=" * 80)
52+ 
53+ parser = LogParser()
54+ summaries = parser.parse_log_file("test_complete.log")
55+ print_summary_table(summaries)
56+ print()
57+ 
58+ 
59+def example_4_programmatic_access():
60+ """示例4:编程方式访问数据"""
61+ print("示例4:编程方式访问数据")
62+ print("=" * 80)
63+ 
64+ parser = LogParser()
65+ summaries = parser.parse_log_file("test_concat.log")
66+ 
67+ for summary in summaries:
68+ print(f"算子名称: {summary.operator_name}")
69+ print(
70+ f"选择的配置: graph{summary.graph}_result{summary.result}_g{summary.group}_case{summary.case}"
71+ )
72+ print("性能指标:")
73+ print(f" - AIV_MTE2: {summary.aiv_mte2}")
74+ print(f" - AIV_MTE3: {summary.aiv_mte3}")
75+ print(f" - Objective Value: {summary.objective_value}")
76+ print(f" - Result Performance: {summary.result_performance}")
77+ print("切分参数:")
78+ for key, value in summary.tiling_values.items():
79+ print(f" - {key}: {value}")
80+ print()
81+ 
82+ 
83+def example_5_custom_processing():
84+ """示例5:自定义处理"""
85+ print("示例5:自定义处理")
86+ print("=" * 80)
87+ 
88+ parser = LogParser()
89+ summaries = parser.parse_log_file("test_complete.log")
90+ 
91+ # 找出性能最好的算子
92+ best_operator = None
93+ best_performance = float("inf")
94+ 
95+ for summary in summaries:
96+ if summary.objective_value and summary.objective_value < best_performance:
97+ best_performance = summary.objective_value
98+ best_operator = summary
99+ 
100+ if best_operator:
101+ print(f"性能最好的算子: {best_operator.operator_name}")
102+ print(f"Objective Value: {best_operator.objective_value}")
103+ print(
104+ f"选择的配置: graph{best_operator.graph}_result{best_operator.result}_g{best_operator.group}_case{best_operator.case}"
105+ )
106+ print()
107+ 
108+ 
109+def example_6_export_to_csv():
110+ """示例6:导出到CSV"""
111+ print("示例6:导出到CSV")
112+ print("=" * 80)
113+ 
114+ parser = LogParser()
115+ summaries = parser.parse_log_file("test_complete.log")
116+ 
117+ import csv
118+ 
119+ # 收集所有切分参数
120+ all_tiling_keys = set()
121+ for summary in summaries:
122+ all_tiling_keys.update(summary.tiling_values.keys())
123+ 
124+ fixed_tiling_keys = ["ub_size", "block_dim"]
125+ dynamic_tiling_keys = sorted(all_tiling_keys - set(fixed_tiling_keys))
126+ 
127+ # 写入CSV文件
128+ with open("output.csv", "w", newline="", encoding="utf-8") as csvfile:
129+ fieldnames = [
130+ "Operator",
131+ "Graph",
132+ "Result",
133+ "Group",
134+ "Case",
135+ "AIV_MTE2",
136+ "AIV_MTE3",
137+ "Objective Value",
138+ "Result Perf",
139+ ]
140+ fieldnames.extend(dynamic_tiling_keys)
141+ fieldnames.extend(fixed_tiling_keys)
142+ 
143+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
144+ writer.writeheader()
145+ 
146+ for summary in summaries:
147+ row = {
148+ "Operator": summary.operator_name,
149+ "Graph": summary.graph,
150+ "Result": summary.result,
151+ "Group": summary.group,
152+ "Case": summary.case,
153+ "AIV_MTE2": summary.aiv_mte2,
154+ "AIV_MTE3": summary.aiv_mte3,
155+ "Objective Value": summary.objective_value,
156+ "Result Perf": summary.result_performance,
157+ }
158+ 
159+ for key in dynamic_tiling_keys:
160+ row[key] = summary.tiling_values.get(key, "")
161+ 
162+ for key in fixed_tiling_keys:
163+ row[key] = summary.tiling_values.get(key, "")
164+ 
165+ writer.writerow(row)
166+ 
167+ print("数据已导出到 output.csv")
168+ print()
169+ 
170+ 
171+if __name__ == "__main__":
172+ example_1_cli_usage()
173+ example_2_basic_usage()
174+ example_3_multiple_operators()
175+ example_4_programmatic_access()
176+ example_5_custom_processing()
177+ example_6_export_to_csv()
178+ 
179+ print("所有示例运行完成!")
@@ -0,0 +1,129 @@
1+#!/usr/bin/env python3
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+ 
12+"""att.py - ATT-Analyze 统一 CLI 入口"""
13+ 
14+import sys
15+import os
16+import argparse
17+ 
18+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19+ 
20+ 
21+def build_parser() -> argparse.ArgumentParser:
22+ parser = argparse.ArgumentParser(prog="att", description="ATT-Analyze 工具集")
23+ subparsers = parser.add_subparsers(dest="command", required=True)
24+ 
25+ # summary 子命令
26+ p_summary = subparsers.add_parser("summary", help="解析 ATT 日志,输出算子汇总")
27+ p_summary.add_argument("log_path", help="日志文件或目录")
28+ p_summary.add_argument(
29+ "-a",
30+ "--all",
31+ action="store_true",
32+ help="汇总所有 result 下所有 group 的最佳 case",
33+ )
34+ p_summary.add_argument(
35+ "-f", "--format", choices=["console", "csv", "excel"], default="console"
36+ )
37+ p_summary.add_argument("-o", "--output", default=None)
38+ 
39+ # compare 子命令
40+ p_compare = subparsers.add_parser("compare", help="对比两个 CSV 文件")
41+ p_compare.add_argument("csv1", help="基准 CSV")
42+ p_compare.add_argument("csv2", help="对比 CSV")
43+ p_compare.add_argument(
44+ "-f", "--format", choices=["console", "text", "excel"], default="console"
45+ )
46+ p_compare.add_argument("-o", "--output", default=None)
47+ 
48+ # verify-tiling 子命令
49+ p_vt = subparsers.add_parser("verify-tiling", help="TilingFunc 编译+执行验证")
50+ p_vt.add_argument("source_dir", help="源文件目录(tf 或 inductor)")
51+ p_vt.add_argument(
52+ "--scene", choices=["tf", "inductor"], default=None, help="不填则自动检测"
53+ )
54+ p_vt.add_argument("--preset", choices=["A", "B"], default="A")
55+ p_vt.add_argument(
56+ "--input-json", default=None, help="输入参数 JSON,与 --preset 互斥"
57+ )
58+ p_vt.add_argument(
59+ "--aiv-num",
60+ type=int,
61+ default=None,
62+ help="覆盖 preset 或 input-json 中的 AI Vector 核数配置",
63+ )
64+ p_vt.add_argument(
65+ "--log", default=None, help="ATT 日志,用于提取输入参数和 --case 默认值"
66+ )
67+ p_vt.add_argument("--case", default=None)
68+ p_vt.add_argument("--compile-config", default=None)
69+ p_vt.add_argument("--keep-build", action="store_true")
70+ p_vt.add_argument("-o", "--output", default="output/verify/")
71+ 
72+ # split-slog 子命令
73+ p_ss = subparsers.add_parser("split-slog", help="slog 日志按算子/模板拆分")
74+ p_ss.add_argument("log_path", help="日志文件或目录")
75+ p_ss.add_argument("--op", default=None, help="只处理指定算子")
76+ p_ss.add_argument("--case", default=None)
77+ p_ss.add_argument("-o", "--output", default="output/split/")
78+ 
79+ # perf-formula 子命令
80+ p_pf = subparsers.add_parser("perf-formula", help="性能公式分析+SVG 可视化")
81+ p_pf.add_argument("source_dir", help="tiling_func 源文件目录")
82+ p_pf.add_argument("log_path", help="ATT 结果日志")
83+ p_pf.add_argument("--case", default=None)
84+ p_pf.add_argument("-o", "--output", default="output/perf/")
85+ 
86+ # evidence 子命令
87+ p_ev = subparsers.add_parser("evidence", help="导出机器可读 ATT evidence JSONL")
88+ p_ev.add_argument("log_path", help="日志文件或目录")
89+ p_ev.add_argument("-o", "--output", required=True, help="输出目录")
90+ 
91+ return parser
92+ 
93+ 
94+def main():
95+ parser = build_parser()
96+ args = parser.parse_args()
97+ 
98+ exit_code = 0
99+ if args.command == "summary":
100+ from commands.summary import run
101+ 
102+ result = run(args)
103+ elif args.command == "compare":
104+ from commands.compare import run
105+ 
106+ result = run(args)
107+ elif args.command == "verify-tiling":
108+ from commands.verify_tiling import run
109+ 
110+ result = run(args)
111+ elif args.command == "split-slog":
112+ from commands.split_slog import run
113+ 
114+ result = run(args)
115+ elif args.command == "perf-formula":
116+ from commands.perf_formula import run
117+ 
118+ result = run(args)
119+ elif args.command == "evidence":
120+ from commands.evidence import run
121+ 
122+ result = run(args)
123+ if isinstance(result, int):
124+ exit_code = result
125+ return exit_code
126+ 
127+ 
128+if __name__ == "__main__":
129+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1+# -*- coding: utf-8 -*-
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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+"""Command-line handlers for ATT analysis."""
13+ 
14+__all__ = []
@@ -0,0 +1,65 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+from dataclasses import dataclass
13+from typing import List, Optional
14+ 
15+_DIM_ALIASES = {
16+ "r": "results",
17+ "result": "results",
18+ "g": "groups",
19+ "group": "groups",
20+ "c": "cases",
21+ "case": "cases",
22+}
23+ 
24+ 
25+@dataclass
26+class CaseFilter:
27+ results: Optional[List[int]] = None
28+ groups: Optional[List[int]] = None
29+ cases: Optional[List[int]] = None
30+ 
31+ def match(self, result_id: int, group_id: int, case_id: int) -> bool:
32+ if self.results is not None and result_id not in self.results:
33+ return False
34+ if self.groups is not None and group_id not in self.groups:
35+ return False
36+ if self.cases is not None and case_id not in self.cases:
37+ return False
38+ return True
39+ 
40+ 
41+def parse_case_arg(value: Optional[str]) -> Optional[CaseFilter]:
42+ """将 --case 字符串解析为 CaseFilter;value 为 None 时返回 None"""
43+ if value is None:
44+ return None
45+ dims = {"results": None, "groups": None, "cases": None}
46+ current_dim = None
47+ for token in value.split(","):
48+ token = token.strip()
49+ if "=" in token:
50+ raw_dim, id_str = token.split("=", 1)
51+ raw_dim = raw_dim.strip()
52+ dim = _DIM_ALIASES.get(raw_dim)
53+ if dim is None:
54+ raise ValueError(f"未知维度: {raw_dim!r},支持 r/result/g/group/c/case")
55+ current_dim = dim
56+ if dims[current_dim] is None:
57+ dims[current_dim] = []
58+ dims[current_dim].append(int(id_str.strip()))
59+ else:
60+ if current_dim is None:
61+ raise ValueError(
62+ f"--case 解析错误:{token!r} 前缺少维度标识(r=/g=/c=)"
63+ )
64+ dims[current_dim].append(int(token))
65+ return CaseFilter(**dims)
@@ -0,0 +1,28 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+"""compare 子命令:调度 compare_csv 逻辑"""
13+ 
14+import sys
15+ 
16+ 
17+def run(args):
18+ import compare_csv
19+ 
20+ argv = [args.csv1, args.csv2, "-f", args.format]
21+ if args.output:
22+ argv += ["-o", args.output]
23+ old_argv = sys.argv[:]
24+ sys.argv = ["compare_csv"] + argv
25+ try:
26+ compare_csv.main()
27+ finally:
28+ sys.argv = old_argv
@@ -0,0 +1,287 @@
1+#!/usr/bin/env python3
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+ 
12+"""Export ATT log observations as canonical JSONL evidence."""
13+ 
14+from __future__ import annotations
15+ 
16+import hashlib
17+import json
18+import os
19+import re
20+import sys
21+from datetime import datetime, timezone
22+from typing import Any, Dict, Iterable, List, Optional, Tuple
23+ 
24+from core.evidence_schema import make_record
25+from core.log_parser import LogParser
26+ 
27+ 
28+_NUMBER = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)"
29+# ATT output is sometimes prefixed with an additional severity tag, for
30+# example ``[INFO] [Add] [PROF] ...``. Consume all bracketed tags and retain
31+# the tag immediately before ``[PROF]`` as the operator name. This keeps the
32+# parser from incorrectly reporting ``INFO`` as the operator.
33+_PROF_PREFIX = r"(?:\[[^\]]+\]\s*)*\[([^\]]+)\]\s*\[PROF\]"
34+_MESSAGE_PREFIX = (
35+ r"(?:\[(?!PROF\])[^\]]+\]\s*)*"
36+ r"\[(?!PROF\])([^\]]+)\]\s*(?:\[PROF\])?"
37+)
38+ 
39+# Tiling case labels are emitted both as ``_0`` and as ``_R0`` in profiling
40+# logs. The latter is a sub-case notation used by the PERF records; both map
41+# to the same integer case id in the evidence schema.
42+_KEY = r"graph(\d+)_result(\d+)_g(-?\d+)_(?:R)?(\d+)"
43+_VALUE_RE = re.compile(
44+ rf"{_PROF_PREFIX}The value of\s+(\w+)\s+is\s+({_NUMBER})\s+in\s+{_KEY}"
45+)
46+_OBJECTIVE_RE = re.compile(
47+ rf"{_PROF_PREFIX}The objective value of the tiling data is\s+({_NUMBER})\s+in\s+{_KEY}"
48+)
49+_TEMPLATE_RE = re.compile(
50+ rf"{_PROF_PREFIX}Among the templates,\s*tiling case\s+(\d+)\s+of\s+graph(\d+)_result(\d+)_g(-?\d+)(?:_(?:R)?\d+)?\s+is the best choice"
51+)
52+_GRAPH_SELECTION_RE = re.compile(
53+ rf"{_PROF_PREFIX}Among all schedule results,\s*graph(\d+)_result(\d+)\s+is the best choice"
54+)
55+_GRAPH_RE = re.compile(
56+ rf"{_MESSAGE_PREFIX}The value of\s+graph(\d+)_result(\d+)\s+is\s+({_NUMBER})"
57+)
58+ 
59+ 
60+def _log_files(path: str) -> List[str]:
61+ if os.path.isfile(path):
62+ return [path]
63+ if not os.path.isdir(path):
64+ return []
65+ return sorted(
66+ os.path.join(root, name)
67+ for root, dirs, files in os.walk(path)
68+ for name in sorted(files)
69+ if name.endswith(".log")
70+ )
71+ 
72+ 
73+def _key(
74+ operator: str, graph: int, result: int, group: int, case: int
75+) -> Tuple[str, int, int, int, int]:
76+ return operator, graph, result, group, case
77+ 
78+ 
79+def _scan_log(path: str) -> List[Dict[str, Any]]:
80+ """Scan all candidate records while retaining first source locations."""
81+ 
82+ records: Dict[Tuple[str, int, int, int, int], Dict[str, Any]] = {}
83+ graph_perf: Dict[Tuple[str, int, int], float] = {}
84+ selected: Dict[Tuple[str, int, int, int], int] = {}
85+ explicit_graph_results = set()
86+ with open(path, "r", encoding="utf-8") as stream:
87+ for line_no, line in enumerate(stream, 1):
88+ match = _VALUE_RE.search(line)
89+ if match:
90+ operator, name, value, graph, result, group, case = match.groups()
91+ key = _key(operator, int(graph), int(result), int(group), int(case))
92+ item = records.setdefault(
93+ key, {"tiling_values": {}, "source_lines": {}}
94+ )
95+ item["tiling_values"][name] = float(value)
96+ item["source_lines"].setdefault(name, line_no)
97+ item.setdefault("source_line", line_no)
98+ continue
99+ 
100+ match = _OBJECTIVE_RE.search(line)
101+ if match:
102+ operator, objective, graph, result, group, case = match.groups()
103+ key = _key(operator, int(graph), int(result), int(group), int(case))
104+ item = records.setdefault(
105+ key, {"tiling_values": {}, "source_lines": {}}
106+ )
107+ item["objective"] = float(objective)
108+ item["source_lines"].setdefault("objective", line_no)
109+ item.setdefault("source_line", line_no)
110+ continue
111+ 
112+ match = _TEMPLATE_RE.search(line)
113+ if match:
114+ operator, case, graph, result, group = match.groups()
115+ graph_i, result_i, group_i, case_i = map(
116+ int, (graph, result, group, case)
117+ )
118+ selected[(operator, graph_i, result_i, group_i)] = case_i
119+ key = _key(operator, graph_i, result_i, group_i, case_i)
120+ item = records.setdefault(
121+ key, {"tiling_values": {}, "source_lines": {}}
122+ )
123+ item["source_lines"].setdefault("template_selection", line_no)
124+ item.setdefault("source_line", line_no)
125+ continue
126+ 
127+ match = _GRAPH_SELECTION_RE.search(line)
128+ if match:
129+ operator, graph, result = match.groups()
130+ explicit_graph_results.add((operator, int(graph), int(result)))
131+ continue
132+ 
133+ match = _GRAPH_RE.search(line)
134+ if match:
135+ operator, graph, result, perf = match.groups()
136+ graph_perf[(operator, int(graph), int(result))] = float(perf)
137+ 
138+ result: List[Dict[str, Any]] = []
139+ for (operator, graph, schedule_result, group, case), item in records.items():
140+ selection_key = (operator, graph, schedule_result, group)
141+ is_selected = selected.get(selection_key) == case
142+ if is_selected:
143+ status = (
144+ "ok"
145+ if (operator, graph, schedule_result) in explicit_graph_results
146+ else "inferred_graph_result"
147+ )
148+ elif selection_key in selected:
149+ # This group has a recorded winner, but the current line describes
150+ # another candidate case.
151+ status = "candidate"
152+ else:
153+ # Tiling/objective lines without a template choice are incomplete
154+ # evidence; do not silently mark them as complete.
155+ status = "missing_group_case"
156+ result.append(
157+ make_record(
158+ operator=operator,
159+ graph_id=graph,
160+ result_id=schedule_result,
161+ group_id=group,
162+ case_id=case,
163+ tiling_values=item.get("tiling_values"),
164+ objective=item.get("objective"),
165+ result_performance=graph_perf.get((operator, graph, schedule_result)),
166+ source_path=os.path.abspath(path),
167+ source_line=item.get("source_line"),
168+ source_lines=item.get("source_lines"),
169+ parse_status=status,
170+ )
171+ )
172+ 
173+ # Logs containing only selection lines still need one record per selected
174+ # group/case. The scanner above already creates those records; parser
175+ # output is used solely to expose explicit missing-data statuses.
176+ parser = LogParser()
177+ summaries = parser.parse_log_file(path, summary_mode="all_results_all_groups")
178+ by_key = {
179+ (s.operator_name, s.graph, s.result, s.group, s.case): s for s in summaries
180+ }
181+ for item in result:
182+ summary = by_key.get(
183+ (
184+ item["operator"],
185+ item["graph_id"],
186+ item["result_id"],
187+ item["group_id"],
188+ item["case_id"],
189+ )
190+ )
191+ if summary and summary.parse_status != "ok":
192+ current_status = item["parse_status"]
193+ if current_status == "missing_group_case":
194+ # Keep the specific missing template diagnosis even when a
195+ # secondary metric (for example result performance) is absent.
196+ continue
197+ if (
198+ summary.parse_status == "missing_result_performance"
199+ and item.get("result_performance") is not None
200+ ):
201+ # The evidence scanner understands prefixed PROF performance
202+ # lines that the legacy parser does not. Do not downgrade a
203+ # complete record merely because the compatibility parser
204+ # could not recognize the same metric.
205+ continue
206+ if current_status != summary.parse_status:
207+ item["parse_status"] = f"{current_status};{summary.parse_status}"
208+ return sorted(
209+ result,
210+ key=lambda x: (
211+ x["source_path"],
212+ x["operator"],
213+ x["graph_id"],
214+ x["result_id"],
215+ x["group_id"],
216+ x["case_id"],
217+ ),
218+ )
219+ 
220+ 
221+def _sha256(path: str) -> str:
222+ digest = hashlib.sha256()
223+ with open(path, "rb") as stream:
224+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
225+ digest.update(chunk)
226+ return digest.hexdigest()
227+ 
228+ 
229+def _artifacts(output_dir: str) -> List[Dict[str, str]]:
230+ artifacts = []
231+ for root, dirs, files in os.walk(output_dir):
232+ dirs.sort()
233+ for name in sorted(files):
234+ if name == "tool-manifest.json":
235+ continue
236+ path = os.path.join(root, name)
237+ artifacts.append(
238+ {"path": os.path.relpath(path, output_dir), "sha256": _sha256(path)}
239+ )
240+ return artifacts
241+ 
242+ 
243+def export(
244+ log_path: str, output_dir: str, command: Optional[Iterable[str]] = None
245+) -> Dict[str, Any]:
246+ files = _log_files(log_path)
247+ if not files:
248+ raise FileNotFoundError(f"No log files found at '{log_path}'")
249+ os.makedirs(output_dir, exist_ok=True)
250+ evidence_path = os.path.join(output_dir, "att-evidence.jsonl")
251+ count = 0
252+ with open(evidence_path, "w", encoding="utf-8") as stream:
253+ for path in files:
254+ for record in _scan_log(path):
255+ stream.write(
256+ json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n"
257+ )
258+ count += 1
259+ 
260+ manifest = {
261+ "manifest_version": "att-tool/v1",
262+ "schema_version": "att-evidence/v1",
263+ "tool": "att_analyze",
264+ "generated_at_utc": datetime.now(timezone.utc).isoformat(),
265+ "command": list(command or []),
266+ "inputs": [
267+ {"path": os.path.abspath(path), "sha256": _sha256(path)} for path in files
268+ ],
269+ "record_count": count,
270+ "artifacts": _artifacts(output_dir),
271+ }
272+ manifest_path = os.path.join(output_dir, "tool-manifest.json")
273+ with open(manifest_path, "w", encoding="utf-8") as stream:
274+ json.dump(manifest, stream, ensure_ascii=False, indent=2, sort_keys=True)
275+ stream.write("\n")
276+ return manifest
277+ 
278+ 
279+def run(args: Any) -> None:
280+ try:
281+ manifest = export(
282+ args.log_path, args.output, command=getattr(args, "_argv", None) or sys.argv
283+ )
284+ except (OSError, UnicodeError) as exc:
285+ print(f"Error: {exc}", file=sys.stderr)
286+ return
287+ print(f"Exported {manifest['record_count']} evidence record(s) to '{args.output}'")
@@ -0,0 +1,270 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+import os
13+import statistics
14+from html import escape
15+from typing import List, Optional, Tuple
16+from core.tiling_func_reader import CasePerfInfo, NodePerfInfo, parse_perf_lines
17+from core.log_parser import LogParser
18+from core.file_utils import find_log_files, ensure_output_dir
19+from commands.case_filter import parse_case_arg
20+ 
21+# ──── SVG 布局常数 ────────────────────────────────────────────
22+_CANVAS_W = 900
23+_FONT_SIZE = 12
24+_LINE_H = 20
25+_BAR_H = 14
26+_OP_GAP = 40
27+_GROUP_GAP = 16
28+_OP_TITLE_H = 40
29+_CASE_TAB_H = 28
30+_MARGIN = 20
31+ 
32+ 
33+def _bar(x: int, y: int, ratio: float, fill: str, max_w: int) -> str:
34+ bar_w = max(1, int(ratio * max_w))
35+ return f'<rect x="{x}" y="{y}" width="{bar_w}" height="{_BAR_H}" fill="{fill}" opacity="0.7"/>'
36+ 
37+ 
38+def _text(
39+ x: int,
40+ y: int,
41+ content: str,
42+ fill: str = "#222",
43+ anchor: str = "start",
44+ size: Optional[int] = None,
45+) -> str:
46+ sz = size or _FONT_SIZE
47+ return f'<text x="{x}" y="{y}" font-size="{sz}" fill="{escape(fill, quote=True)}" text-anchor="{anchor}" font-family="monospace">{escape(str(content))}</text>'
48+ 
49+ 
50+def _render_node(node: NodePerfInfo, x: int, y: int, col_w: int) -> Tuple[str, int]:
51+ """渲染单个节点,返回 (fragment, height_used)"""
52+ parts = []
53+ pipe_color = "red" if node.is_bottleneck else "#333"
54+ pipe_label = f"{node.pipe_type} = {node.total:.2f}"
55+ if node.is_bottleneck:
56+ pipe_label += " \U0001f534"
57+ parts.append(_text(x, y + _FONT_SIZE, pipe_label, fill=pipe_color))
58+ h = _LINE_H
59+ 
60+ bar_x = x + 130
61+ bar_max_w = max(col_w - 140, 10)
62+ for item in node.sub_items:
63+ if item.value == 0:
64+ continue
65+ label = f"{item.name:<16} {item.contribution * 100:.1f}%"
66+ parts.append(_text(x, y + h + _FONT_SIZE, label))
67+ parts.append(_bar(bar_x, y + h, item.contribution, "#4a90d9", bar_max_w))
68+ h += _LINE_H
69+ 
70+ return "\n".join(parts), h + 4
71+ 
72+ 
73+def _render_group(
74+ cases: List[CasePerfInfo],
75+ group_id: int,
76+ x: int,
77+ y: int,
78+ col_w: int,
79+ selected_case_id: Optional[int],
80+) -> Tuple[str, int]:
81+ """渲染单个 group 列,返回 (fragment, height)"""
82+ parts = [_text(x, y + _FONT_SIZE, f"group{group_id}", size=_FONT_SIZE + 1)]
83+ h = _LINE_H + 4
84+ 
85+ # case 标签行
86+ tab_y = y + h
87+ tab_x = x
88+ for c in cases:
89+ is_sel = c.case_id == selected_case_id
90+ label = f"[case{c.case_id}{'★' if is_sel else ''}]"
91+ fill = "#1a6fb5" if is_sel else "#888"
92+ parts.append(_text(tab_x, tab_y + _FONT_SIZE, label, fill=fill))
93+ tab_x += len(label) * 7 + 4
94+ h += _CASE_TAB_H
95+ 
96+ # 显示选中 case 的节点
97+ display_case = next(
98+ (c for c in cases if c.case_id == selected_case_id), cases[0] if cases else None
99+ )
100+ if display_case:
101+ for node in display_case.nodes:
102+ frag, node_h = _render_node(node, x + 4, y + h, col_w - 8)
103+ parts.append(frag)
104+ h += node_h + 4
105+ 
106+ # 跨 case 方差对比
107+ if len(cases) > 1:
108+ item_vals: dict = {}
109+ for c in cases:
110+ for node in c.nodes:
111+ for si in node.sub_items:
112+ item_vals.setdefault(si.name, []).append(si.value)
113+ if item_vals:
114+ max_var_name = max(
115+ item_vals,
116+ key=lambda k: statistics.variance(item_vals[k])
117+ if len(item_vals[k]) > 1
118+ else 0,
119+ )
120+ vals = item_vals[max_var_name]
121+ parts.append(
122+ _text(
123+ x,
124+ y + h + _FONT_SIZE,
125+ f"敏感参数: {max_var_name}(跨 case 方差最大)",
126+ fill="#b85c00",
127+ )
128+ )
129+ h += _LINE_H
130+ max_v = max(vals) if vals else 1
131+ for c, v in zip(cases, vals):
132+ bar_w = int((v / max_v) * (col_w - 80))
133+ bar_y = y + h
134+ parts.append(_text(x, bar_y + _FONT_SIZE, f"case{c.case_id}"))
135+ parts.append(
136+ f'<rect x="{x + 50}" y="{bar_y}" width="{bar_w}" height="{_BAR_H}" fill="#e07b39" opacity="0.7"/>'
137+ )
138+ parts.append(_text(x + 55 + bar_w, bar_y + _FONT_SIZE, f"{v:.2f}"))
139+ h += _LINE_H
140+ 
141+ return "\n".join(parts), h
142+ 
143+ 
144+def render_svg(
145+ op_name: str,
146+ cases: List[CasePerfInfo],
147+ selected_case: Optional[Tuple[int, int, int]],
148+) -> Tuple[str, int]:
149+ """
150+ 生成单算子的 SVG <g> 片段,返回 (fragment, height)。
151+ 由 build_full_svg 负责包裹为完整 <svg> 根元素。
152+ """
153+ groups: dict = {}
154+ for c in cases:
155+ groups.setdefault(c.group_id, []).append(c)
156+ 
157+ group_ids = sorted(groups.keys())
158+ n_groups = len(group_ids)
159+ cols_per_row = min(n_groups, 2) if n_groups > 3 else max(n_groups, 1)
160+ col_w = (_CANVAS_W - _MARGIN * 2 - _GROUP_GAP * (cols_per_row - 1)) // cols_per_row
161+ 
162+ selected_str = ""
163+ if selected_case:
164+ r, g, c = selected_case
165+ selected_str = f"Selected: graph0_result{r}_g{g}_case{c}"
166+ 
167+ inner_parts = [
168+ f'<rect width="{_CANVAS_W}" height="{_OP_TITLE_H}" fill="#f0f4f8" rx="4"/>',
169+ _text(_MARGIN, _FONT_SIZE + 6, op_name, size=_FONT_SIZE + 4, fill="#1a1a1a"),
170+ ]
171+ if selected_str:
172+ inner_parts.append(
173+ _text(_MARGIN, _FONT_SIZE * 2 + 14, selected_str, fill="#555")
174+ )
175+ inner_parts.append(
176+ f'<line x1="{_MARGIN}" y1="{_OP_TITLE_H - 4}" x2="{_CANVAS_W - _MARGIN}" y2="{_OP_TITLE_H - 4}" stroke="#ccc"/>'
177+ )
178+ 
179+ body_y = _OP_TITLE_H
180+ max_row_h = 0
181+ for col_idx, gid in enumerate(group_ids):
182+ col = col_idx % cols_per_row
183+ x = _MARGIN + col * (col_w + _GROUP_GAP)
184+ y = body_y
185+ 
186+ sel_case_id = (
187+ selected_case[2] if selected_case and selected_case[1] == gid else None
188+ )
189+ frag, gh = _render_group(groups[gid], gid, x, y, col_w, sel_case_id)
190+ inner_parts.append(frag)
191+ max_row_h = max(max_row_h, gh)
192+ 
193+ total_rows = (n_groups + cols_per_row - 1) // cols_per_row
194+ total_h = _OP_TITLE_H + max_row_h * total_rows + _OP_GAP
195+ 
196+ fragment = "<g>\n" + "\n".join(inner_parts) + "\n</g>"
197+ return fragment, total_h
198+ 
199+ 
200+def build_full_svg(sections: List[Tuple[str, int]]) -> str:
201+ """将多个算子 section 垂直堆叠,包裹为合法单 <svg> 根元素"""
202+ total_h = sum(h for _, h in sections)
203+ svg_parts = [
204+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{_CANVAS_W}" height="{total_h}">'
205+ ]
206+ y_offset = 0
207+ for frag, h in sections:
208+ svg_parts.append(f'<g transform="translate(0,{y_offset})">')
209+ svg_parts.append(frag)
210+ svg_parts.append("</g>")
211+ y_offset += h
212+ svg_parts.append("</svg>")
213+ return "\n".join(svg_parts)
214+ 
215+ 
216+def run(args):
217+ if not os.path.isdir(args.source_dir):
218+ print(f"[perf-formula] 源目录不存在: {args.source_dir}")
219+ return 2
220+ log_files = find_log_files(args.log_path)
221+ full_content = ""
222+ for path in log_files:
223+ with open(path, encoding="utf-8", errors="replace") as f:
224+ full_content += f.read()
225+ 
226+ log_parser = LogParser()
227+ op_names = log_parser.extract_operator_names(full_content)
228+ case_filter = parse_case_arg(args.case)
229+ out_dir = ensure_output_dir(args.output)
230+ sections: List[Tuple[str, int]] = []
231+ 
232+ for op in op_names:
233+ graph_id, result_id = log_parser.extract_graph_result(full_content, op)
234+ group_cases = log_parser.extract_all_group_cases(
235+ full_content, op, graph_id, result_id
236+ )
237+ perf_cases = parse_perf_lines(full_content, op)
238+ 
239+ if case_filter:
240+ perf_cases = [
241+ c
242+ for c in perf_cases
243+ if case_filter.match(c.result_id, c.group_id, c.case_id)
244+ ]
245+ elif group_cases:
246+ selected_keys = {(result_id, gid, cid) for gid, cid in group_cases.items()}
247+ filtered = [
248+ c
249+ for c in perf_cases
250+ if (c.result_id, c.group_id, c.case_id) in selected_keys
251+ ]
252+ perf_cases = filtered or perf_cases
253+ 
254+ if not perf_cases:
255+ print(f"[perf-formula] {op}: 未找到 [PERF] 日志行,跳过")
256+ continue
257+ 
258+ selected_case = None
259+ if group_cases:
260+ first_gid = min(group_cases.keys())
261+ selected_case = (result_id, first_gid, group_cases[first_gid])
262+ 
263+ frag, h = render_svg(op, perf_cases, selected_case)
264+ sections.append((frag, h))
265+ 
266+ out_path = os.path.join(out_dir, "perf_formula.svg")
267+ with open(out_path, "w", encoding="utf-8") as f:
268+ f.write(build_full_svg(sections))
269+ print(f"[perf-formula] 输出: {out_path}")
270+ return 0 if sections else 1
@@ -0,0 +1,6 @@
1+{
2+ "dynamic_dims": [],
3+ "aiv_num": 48,
4+ "ub_size": 196608,
5+ "abi": {"kind": "tf_static", "shape_dims": 0, "block_dim_width": 32}
6+}
@@ -0,0 +1,6 @@
1+{
2+ "dynamic_dims": [1024, 512],
3+ "aiv_num": 56,
4+ "ub_size": 262144,
5+ "abi": {"kind": "tf_dynamic", "shape_dims": 2, "block_dim_width": 32}
6+}
@@ -0,0 +1,181 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+import os
13+import re
14+from typing import Dict, List, Tuple
15+from core.file_utils import ensure_output_dir, find_log_files
16+from commands.case_filter import parse_case_arg
17+ 
18+ 
19+# ──── 正则模式 ───────────────────────────────────────────────
20+ 
21+_OUTER_BEGIN = re.compile(
22+ r"\[DFX\]Begin to gen model info for asc graph (\d+), schedule result (\d+), schedule group (\d+)"
23+)
24+_OUTER_END = re.compile(
25+ r"\[DFX\]End to gen model info for graph(\d+)_result(\d+)_g(\d+)"
26+)
27+_INNER_BEGIN = re.compile(
28+ r"\[DFX\]Begin to generate model info for graph \S+ of tiling case id (\d+)"
29+)
30+_INNER_END = re.compile(
31+ r"\[DFX\]End to generate model info for graph \S+ of tiling case id (\d+)"
32+)
33+_PROF_CASE = re.compile(r"graph(\d+)_result(\d+)_g(\d+)_(R?\d+)")
34+ 
35+ 
36+class SlogSplitter:
37+ @staticmethod
38+ def _case_id(content: str, fallback: int) -> int:
39+ match = re.search(r"tiling case id\s+(?:R)?(\d+)", content, re.I)
40+ if not match:
41+ match = _PROF_CASE.search(content)
42+ if match:
43+ return int(re.sub(r"^R", "", match.group(4)))
44+ return fallback
45+ 
46+ def parse_compiler_model_info(self, log_content: str) -> Dict[Tuple, List[str]]:
47+ """返回 {(graph, result, group): [case1_content, case2_content, ...]}"""
48+ result: Dict[Tuple, List[str]] = {}
49+ current_group = None
50+ in_inner = False
51+ current_case_lines: List[str] = []
52+ case_list: List[str] = []
53+ 
54+ for line in log_content.splitlines(keepends=True):
55+ m = _OUTER_BEGIN.search(line)
56+ if m:
57+ current_group = (int(m.group(1)), int(m.group(2)), int(m.group(3)))
58+ case_list = []
59+ continue
60+ 
61+ m = _OUTER_END.search(line)
62+ if m and current_group:
63+ result[current_group] = case_list
64+ current_group = None
65+ continue
66+ 
67+ m = _INNER_BEGIN.search(line)
68+ if m and current_group:
69+ in_inner = True
70+ current_case_lines = []
71+ continue
72+ 
73+ m = _INNER_END.search(line)
74+ if m and in_inner:
75+ in_inner = False
76+ case_list.append("".join(current_case_lines))
77+ current_case_lines = []
78+ continue
79+ 
80+ if in_inner:
81+ current_case_lines.append(line)
82+ 
83+ return result
84+ 
85+ def parse_runtime_cases(
86+ self, log_content: str, operator_name: str
87+ ) -> Dict[Tuple, List[str]]:
88+ """返回 {(graph, result, group): [case1_content, case2_content, ...]}"""
89+ pending: Dict[Tuple, List[str]] = {}
90+ for line in log_content.splitlines(keepends=True):
91+ if f"[{operator_name}]" not in line:
92+ continue
93+ m = _PROF_CASE.search(line)
94+ if m:
95+ key = (
96+ int(m.group(1)),
97+ int(m.group(2)),
98+ int(m.group(3)),
99+ int(re.sub(r"^R", "", m.group(4))),
100+ )
101+ pending.setdefault(key, []).append(line)
102+ 
103+ # 按 (graph, result, group) 分组,按 case 顺序排列
104+ result: Dict[Tuple, List[str]] = {}
105+ for (g, r, grp, c), lines in sorted(pending.items()):
106+ group_key = (g, r, grp)
107+ result.setdefault(group_key, []).append("".join(lines))
108+ return result
109+ 
110+ def write_compiler_output(
111+ self, groups: Dict[Tuple, List[str]], op: str, out_base: str
112+ ):
113+ for (g, r, grp), cases in groups.items():
114+ dir_path = os.path.join(
115+ out_base, op, "compiler", f"graph{g}_result{r}", f"g{grp}"
116+ )
117+ ensure_output_dir(dir_path)
118+ for i, content in enumerate(cases):
119+ case_id = self._case_id(content, i)
120+ with open(os.path.join(dir_path, f"case{case_id}.log"), "w") as f:
121+ f.write(content)
122+ 
123+ def write_runtime_output(
124+ self, groups: Dict[Tuple, List[str]], op: str, out_base: str
125+ ):
126+ for (g, r, grp), cases in groups.items():
127+ dir_path = os.path.join(
128+ out_base, op, "runtime", f"graph{g}_result{r}", f"g{grp}"
129+ )
130+ ensure_output_dir(dir_path)
131+ for i, content in enumerate(cases):
132+ case_id = self._case_id(content, i)
133+ with open(os.path.join(dir_path, f"case{case_id}.log"), "w") as f:
134+ f.write(content)
135+ 
136+ 
137+def run(args):
138+ log_files = find_log_files(args.log_path)
139+ full_content = ""
140+ for path in log_files:
141+ with open(path, encoding="utf-8", errors="replace") as f:
142+ full_content += f.read()
143+ 
144+ from core.log_parser import LogParser
145+ 
146+ parser = LogParser()
147+ op_names = [args.op] if args.op else parser.extract_operator_names(full_content)
148+ 
149+ splitter = SlogSplitter()
150+ out_base = args.output
151+ 
152+ for op in op_names:
153+ compiler_groups = splitter.parse_compiler_model_info(full_content)
154+ runtime_groups = splitter.parse_runtime_cases(full_content, op)
155+ case_filter = parse_case_arg(args.case)
156+ if case_filter:
157+ compiler_groups = {
158+ key: [
159+ content
160+ for index, content in enumerate(values)
161+ if case_filter.match(
162+ key[1], key[2], splitter._case_id(content, index)
163+ )
164+ ]
165+ for key, values in compiler_groups.items()
166+ }
167+ runtime_groups = {
168+ key: [
169+ content
170+ for index, content in enumerate(values)
171+ if case_filter.match(
172+ key[1], key[2], splitter._case_id(content, index)
173+ )
174+ ]
175+ for key, values in runtime_groups.items()
176+ }
177+ splitter.write_compiler_output(compiler_groups, op, out_base)
178+ splitter.write_runtime_output(runtime_groups, op, out_base)
179+ print(
180+ f"[split-slog] {op}: compiler={len(compiler_groups)} groups, runtime={len(runtime_groups)} groups"
181+ )
@@ -0,0 +1,30 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+"""summary 子命令:调度 summary_templates 逻辑"""
13+ 
14+import sys
15+ 
16+ 
17+def run(args):
18+ import summary_templates
19+ 
20+ argv = [args.log_path, "-f", args.format]
21+ if getattr(args, "all", False):
22+ argv.append("--all")
23+ if args.output:
24+ argv += ["-o", args.output]
25+ old_argv = sys.argv[:]
26+ sys.argv = ["summary_templates"] + argv
27+ try:
28+ summary_templates.main()
29+ finally:
30+ sys.argv = old_argv
@@ -0,0 +1,391 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+import os
13+import re
14+import json
15+import subprocess
16+import ctypes
17+import tempfile
18+import shutil
19+import ast
20+from ctypes import byref, c_uint32, c_uint64, c_size_t, c_void_p, c_int64
21+from typing import Dict, Tuple
22+from core.file_utils import ensure_output_dir
23+ 
24+ 
25+# ──── 场景检测 ────────────────────────────────────────────────
26+ 
27+ 
28+def detect_scene(source_dir: str) -> str:
29+ """返回 'tf' 或 'inductor'"""
30+ files = os.listdir(source_dir)
31+ if any(f == "output_code.py" for f in files):
32+ return "inductor"
33+ if any("tiling_func" in f and f.endswith(".cpp") for f in files):
34+ return "tf"
35+ raise ValueError(
36+ f"无法自动检测场景:{source_dir} 中未找到 *tiling_func*.cpp 或 output_code.py"
37+ )
38+ 
39+ 
40+# ──── 输入参数加载 ────────────────────────────────────────────
41+ 
42+ 
43+def load_input_params(args) -> Dict:
44+ """加载输入参数,优先级:--input-json > --preset"""
45+ if getattr(args, "input_json", None):
46+ with open(args.input_json) as f:
47+ params = json.load(f)
48+ else:
49+ preset_dir = os.path.join(os.path.dirname(__file__), "presets")
50+ preset_file = os.path.join(preset_dir, f"preset_{args.preset}.json")
51+ with open(preset_file) as f:
52+ params = json.load(f)
53+ override = getattr(args, "aiv_num", None)
54+ if isinstance(override, int):
55+ params["aiv_num"] = override
56+ return params
57+ 
58+ 
59+def print_input_config(input_params: Dict, source: str) -> None:
60+ """Print the effective tiling inputs so users can check hardware assumptions."""
61+ print(
62+ "[verify-tiling] input-config "
63+ f"source={source} "
64+ f"aiv_num={input_params.get('aiv_num', 48)} "
65+ f"ub_size={input_params.get('ub_size', 196608)} "
66+ f"dynamic_dims={input_params.get('dynamic_dims', [])}"
67+ )
68+ print(
69+ "[verify-tiling] aiv_num 是传入 tiling 的配置值,请根据实际设备核数检查;"
70+ "如不一致可使用 --aiv-num 或 --input-json 修改"
71+ )
72+ 
73+ 
74+# ──── 编译配置加载 ────────────────────────────────────────────
75+ 
76+_DEFAULT_FLAGS = (
77+ "-O0 -g -fno-common -Werror -Wextra -Wfloat-equal -fvisibility=default -DLOG_CPP"
78+)
79+ 
80+ 
81+def validate_input_params(input_params: Dict) -> None:
82+ """Validate values passed to the native tiling entrypoint."""
83+ dynamic_dims = input_params.get("dynamic_dims", [])
84+ if not isinstance(dynamic_dims, list) or len(dynamic_dims) > 32:
85+ raise ValueError("dynamic_dims must be a list with at most 32 dimensions")
86+ if any(
87+ not isinstance(dim, int) or not 0 < dim <= 0x7FFFFFFF for dim in dynamic_dims
88+ ):
89+ raise ValueError("dynamic_dims values must be in [1, 2147483647]")
90+ aiv_num = input_params.get("aiv_num", 48)
91+ ub_size = input_params.get("ub_size", 196608)
92+ if not isinstance(aiv_num, int) or not 0 < aiv_num <= 0xFFFF:
93+ raise ValueError("aiv_num must be in [1, 65535]")
94+ if not isinstance(ub_size, int) or not 0 < ub_size <= 0x7FFFFFFF:
95+ raise ValueError("ub_size must be in [1, 2147483647]")
96+ abi = input_params.get("abi")
97+ if not isinstance(abi, dict) or abi.get("kind") not in (
98+ "tf_static",
99+ "tf_dynamic",
100+ "inductor",
101+ ):
102+ raise ValueError("abi.kind must be tf_static, tf_dynamic, or inductor")
103+ if abi.get("block_dim_width") not in (32, 64):
104+ raise ValueError("abi.block_dim_width must be 32 or 64")
105+ if not isinstance(abi.get("shape_dims"), int) or abi["shape_dims"] < 0:
106+ raise ValueError("abi.shape_dims must be a non-negative integer")
107+ if abi["kind"] == "tf_static" and abi["shape_dims"] != 0:
108+ raise ValueError("tf_static requires shape_dims=0")
109+ if abi["kind"] in ("tf_dynamic", "inductor") and abi["shape_dims"] == 0:
110+ raise ValueError(f"{abi['kind']} requires shape_dims>0")
111+ 
112+ 
113+def load_compile_config(args) -> Dict:
114+ """加载编译配置,返回 {'flags': str, 'extra_includes': [], 'extra_links': []}"""
115+ config_path = getattr(args, "compile_config", None)
116+ if config_path is None:
117+ config_path = os.path.expanduser("~/.att_analyze/compile.toml")
118+ if config_path and os.path.exists(config_path):
119+ try:
120+ import tomllib
121+ except ImportError:
122+ try:
123+ import tomli as tomllib
124+ except ImportError:
125+ tomllib = None
126+ if tomllib:
127+ with open(config_path, "rb") as f:
128+ data = tomllib.load(f)
129+ return data.get("compile", {})
130+ return {"flags": _DEFAULT_FLAGS, "extra_includes": [], "extra_links": []}
131+ 
132+ 
133+# ──── inductor 场景:提取 artifacts ──────────────────────────
134+ 
135+ 
136+def extract_inductor_artifacts(output_code_py: str) -> Tuple[str, str]:
137+ """从 output_code.py 提取 tiling_def 和 host_impl 字符串"""
138+ with open(output_code_py) as f:
139+ src = f.read()
140+ artifacts_match = re.search(r"(\w+_artifacts)\s*=\s*\{", src)
141+ if not artifacts_match:
142+ raise ValueError("output_code.py 中未找到 *_artifacts 字典")
143+ var_name = artifacts_match.group(1)
144+ tree = ast.parse(src, filename=output_code_py)
145+ artifacts = None
146+ for node in tree.body:
147+ if isinstance(node, ast.Assign) and any(
148+ isinstance(target, ast.Name) and target.id == var_name
149+ for target in node.targets
150+ ):
151+ try:
152+ artifacts = ast.literal_eval(node.value)
153+ except (ValueError, TypeError, SyntaxError) as exc:
154+ raise ValueError(f"{var_name} 必须是可解析的字面量字典") from exc
155+ break
156+ if not isinstance(artifacts, dict):
157+ raise ValueError(f"{var_name} 不是字典")
158+ return artifacts["tiling_def"], artifacts["host_impl"]
159+ 
160+ 
161+# ──── 准备编译目录 ────────────────────────────────────────────
162+ 
163+_CMAKELISTS_TEMPLATE = """\
164+cmake_minimum_required(VERSION 3.10)
165+project({kernel_name})
166+set(CMAKE_CXX_STANDARD 17)
167+set(ASCEND_PATH $ENV{{ASCEND_HOME_PATH}})
168+file(GLOB ALL_CPP_SRCS *tiling_func*.cpp *infershape*.cpp)
169+add_library({kernel_name} SHARED ${{ALL_CPP_SRCS}})
170+target_compile_options({kernel_name} PRIVATE {flags})
171+target_include_directories({kernel_name} PRIVATE
172+ ${{ASCEND_PATH}}/include
173+ ${{ASCEND_PATH}}/aarch64-linux/include
174+ ${{ASCEND_PATH}}/x86_64-linux/include
175+ {extra_includes}
176+)
177+target_link_libraries({kernel_name}
178+ c_sec ascendalog platform error_manager tiling_api graph_base register
179+ {extra_links}
180+)
181+"""
182+ 
183+ 
184+def prepare_build_dir(
185+ source_dir: str, scene: str, tmp_dir: str, compile_cfg: Dict
186+) -> Tuple[str, str]:
187+ """返回 (build_dir, kernel_name)"""
188+ flags = compile_cfg.get("flags", _DEFAULT_FLAGS)
189+ extra_includes = "\n ".join(compile_cfg.get("extra_includes", []))
190+ extra_links = "\n ".join(compile_cfg.get("extra_links", []))
191+ 
192+ if scene == "inductor":
193+ output_code = os.path.join(source_dir, "output_code.py")
194+ tiling_def, host_impl = extract_inductor_artifacts(output_code)
195+ build_dir = tmp_dir
196+ with open(os.path.join(build_dir, "autofuse_tiling_data.h"), "w") as f:
197+ f.write(tiling_def)
198+ with open(os.path.join(build_dir, "tiling_func.cpp"), "w") as f:
199+ f.write(host_impl)
200+ else:
201+ build_dir = tmp_dir
202+ for filename in os.listdir(source_dir):
203+ if (
204+ filename.endswith(".cpp")
205+ and ("tiling_func" in filename or "infershape" in filename)
206+ ) or filename.endswith((".h", ".hpp", ".inc")):
207+ shutil.copy2(
208+ os.path.join(source_dir, filename),
209+ os.path.join(build_dir, filename),
210+ )
211+ 
212+ cpp_files = [
213+ f for f in os.listdir(build_dir) if "tiling_func" in f and f.endswith(".cpp")
214+ ]
215+ kernel_name = (
216+ re.sub(r"_tiling_func.*", "", cpp_files[0]) if cpp_files else "KernelUnknown"
217+ )
218+ 
219+ cmake_path = os.path.join(build_dir, "CMakeLists.txt")
220+ if not os.path.exists(cmake_path):
221+ with open(cmake_path, "w") as f:
222+ f.write(
223+ _CMAKELISTS_TEMPLATE.format(
224+ kernel_name=kernel_name,
225+ flags=flags,
226+ extra_includes=extra_includes,
227+ extra_links=extra_links,
228+ )
229+ )
230+ return build_dir, kernel_name
231+ 
232+ 
233+# ──── 编译 ───────────────────────────────────────────────────
234+ 
235+ 
236+def compile_tiling(build_dir: str, kernel_name: str) -> Tuple[bool, str]:
237+ """执行 cmake + make,返回 (success, so_path_or_error_msg)"""
238+ cmake_build = os.path.join(build_dir, "build")
239+ os.makedirs(cmake_build, exist_ok=True)
240+ 
241+ cmake_cmd = [
242+ "cmake",
243+ "-S",
244+ build_dir,
245+ "-B",
246+ cmake_build,
247+ "-DCMAKE_C_COMPILER=gcc",
248+ "-DCMAKE_CXX_COMPILER=g++",
249+ ]
250+ r = subprocess.run(cmake_cmd, capture_output=True, text=True)
251+ if r.returncode != 0:
252+ return False, f"cmake 失败:\n{r.stderr}"
253+ 
254+ make_cmd = ["make", "-C", cmake_build, "-j8"]
255+ r = subprocess.run(make_cmd, capture_output=True, text=True)
256+ if r.returncode != 0:
257+ return False, f"make 失败:\n{r.stderr}"
258+ 
259+ so_path = os.path.join(cmake_build, f"lib{kernel_name}.so")
260+ if not os.path.exists(so_path):
261+ return False, f"编译成功但未找到 {so_path}"
262+ return True, so_path
263+ 
264+ 
265+# ──── ctypes 执行 ────────────────────────────────────────────
266+ 
267+ 
268+def execute_tiling(so_path: str, input_params: Dict, scene: str) -> Dict:
269+ """按场景调用 AutofuseTiling,返回 {block_dim, workspace_size}."""
270+ validate_input_params(input_params)
271+ lib = ctypes.CDLL(so_path)
272+ lib.GetTilingDataSize.restype = c_size_t
273+ tiling_size = lib.GetTilingDataSize()
274+ if not isinstance(tiling_size, int) or not 0 < tiling_size <= 1 << 30:
275+ raise ValueError("GetTilingDataSize returned an invalid size")
276+ 
277+ aiv_num = input_params.get("aiv_num", 48)
278+ ub_size = input_params.get("ub_size", 196608)
279+ dynamic_dims = [c_uint32(dim) for dim in input_params.get("dynamic_dims", [])]
280+ 
281+ tiling_buf = ctypes.create_string_buffer(tiling_size)
282+ ws = c_uint32(0)
283+ abi = input_params["abi"]
284+ bd_type = c_uint64 if abi["block_dim_width"] == 64 else c_uint32
285+ bd = bd_type(0)
286+ 
287+ dims = input_params.get("dynamic_dims", [])
288+ if len(dims) != abi["shape_dims"]:
289+ raise ValueError("dynamic_dims count does not match abi.shape_dims")
290+ common_args = [c_void_p, ctypes.POINTER(c_uint32), ctypes.POINTER(bd_type)]
291+ if scene == "inductor" and abi["kind"] == "inductor":
292+ lib.AutofuseTiling.argtypes = (
293+ [c_uint32] * len(dynamic_dims) + common_args + [c_void_p]
294+ )
295+ call_args = dynamic_dims + [tiling_buf, byref(ws), byref(bd), None]
296+ elif scene == "tf" and abi["kind"] in ("tf_static", "tf_dynamic"):
297+ lib.AutofuseTiling.argtypes = (
298+ [c_uint32] * len(dynamic_dims) + common_args + [c_uint32, c_uint32]
299+ )
300+ call_args = dynamic_dims + [
301+ tiling_buf,
302+ byref(ws),
303+ byref(bd),
304+ c_uint32(aiv_num),
305+ c_uint32(ub_size),
306+ ]
307+ else:
308+ raise ValueError("scene does not match abi.kind")
309+ lib.AutofuseTiling.restype = c_int64
310+ status = lib.AutofuseTiling(*call_args)
311+ if status != 0:
312+ raise RuntimeError(f"AutofuseTiling failed with status {status}")
313+ 
314+ block_dim_val = bd.value
315+ result = {"block_dim": block_dim_val, "workspace_size": ws.value}
316+ if any(not 0 <= value <= 0xFFFFFFFF for value in result.values()):
317+ raise ValueError("AutofuseTiling returned an invalid value")
318+ return result
319+ 
320+ 
321+# ──── 主入口 ─────────────────────────────────────────────────
322+ 
323+ 
324+def run(args):
325+ source_dir = os.path.abspath(args.source_dir)
326+ if not os.path.isdir(source_dir):
327+ print(f"✗ 源目录不存在: {source_dir}")
328+ return 2
329+ try:
330+ scene = args.scene or detect_scene(source_dir)
331+ except ValueError as exc:
332+ print(f"✗ {exc}")
333+ return 2
334+ input_params = load_input_params(args)
335+ compile_cfg = load_compile_config(args)
336+ if args.log and not os.path.exists(args.log):
337+ print(f"✗ 日志不存在: {args.log}")
338+ return 2
339+ 
340+ print(f"[verify-tiling] scene={scene}")
341+ source = (
342+ f"input-json:{args.input_json}" if args.input_json else f"preset_{args.preset}"
343+ )
344+ if isinstance(getattr(args, "aiv_num", None), int):
345+ source += "+--aiv-num"
346+ print_input_config(input_params, source)
347+ holder = (
348+ tempfile.TemporaryDirectory(prefix="att-verify-")
349+ if not args.keep_build
350+ else None
351+ )
352+ tmp_dir = holder.name if holder else tempfile.mkdtemp(prefix="att-verify-")
353+ record = {
354+ "scene": scene,
355+ "source_dir": source_dir,
356+ "case": args.case,
357+ "log": args.log,
358+ }
359+ try:
360+ build_dir, kernel_name = prepare_build_dir(
361+ source_dir, scene, tmp_dir, compile_cfg
362+ )
363+ print("\n=== Compile Check ===")
364+ ok, result = compile_tiling(build_dir, kernel_name)
365+ if not ok:
366+ print(f"✗ 编译失败:\n{result}")
367+ record.update(status="COMPILE_FAILED", error=result)
368+ return_code = 1
369+ else:
370+ print(f"✓ cmake OK\n✓ make OK → {result}")
371+ record["so_path"] = result
372+ print("\n=== Tiling Execution ===")
373+ try:
374+ fields = execute_tiling(result, input_params, scene)
375+ for k, v in fields.items():
376+ print(f" {k:<20} = {v}")
377+ record.update(status="SUCCEEDED", fields=fields)
378+ return_code = 0
379+ except Exception as exc:
380+ print(f"✗ Runtime Error: {exc}")
381+ record.update(status="RUNTIME_FAILED", error=str(exc))
382+ return_code = 1
383+ ensure_output_dir(args.output)
384+ with open(
385+ os.path.join(args.output, "result.json"), "w", encoding="utf-8"
386+ ) as stream:
387+ json.dump(record, stream, ensure_ascii=False, indent=2)
388+ return return_code
389+ finally:
390+ if holder:
391+ holder.cleanup()
@@ -0,0 +1,639 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+import csv
13+import os
14+import argparse
15+from typing import Dict, List, Tuple, Optional
16+from dataclasses import dataclass
17+ 
18+try:
19+ import openpyxl
20+ from openpyxl.styles import Font, Alignment, PatternFill
21+ 
22+ HAS_OPENPYXL = True
23+except ImportError:
24+ HAS_OPENPYXL = False
25+ 
26+ 
27+@dataclass
28+class ComparisonResult:
29+ file1_path: str
30+ file2_path: str
31+ file1_rows: int
32+ file2_rows: int
33+ file1_cols: List[str]
34+ file2_cols: List[str]
35+ common_operators: List[str]
36+ only_in_file1: List[str]
37+ only_in_file2: List[str]
38+ operator_differences: Dict[str, List["FieldDifference"]]
39+ structure_differences: List[str]
40+ performance_differences: List["PerformanceDifference"]
41+ 
42+ 
43+@dataclass
44+class FieldDifference:
45+ field_name: str
46+ value1: str
47+ value2: str
48+ diff_type: str
49+ numeric_diff: Optional[float] = None
50+ percentage_diff: Optional[float] = None
51+ 
52+ 
53+@dataclass
54+class PerformanceDifference:
55+ operator: str
56+ metric: str
57+ value1: float
58+ value2: float
59+ absolute_diff: float
60+ percentage_diff: float
61+ improvement: bool
62+ 
63+ 
64+class CSVComparator:
65+ @staticmethod
66+ def _row_key(row: Dict) -> str:
67+ """Identify the same graph/result/group across runs.
68+ 
69+ ``Case`` is deliberately excluded: selecting a different tiling case is
70+ the difference this comparator is intended to show.
71+ """
72+ return "|".join(
73+ str(row.get(field, ""))
74+ for field in ("Operator", "Graph", "Result", "Group")
75+ )
76+ 
77+ def __init__(self):
78+ self.performance_fields = [
79+ "AIV_MTE2",
80+ "AIV_MTE3",
81+ "Objective Value",
82+ "Result Perf",
83+ ]
84+ self.numeric_fields = [
85+ "Graph",
86+ "Result",
87+ "Group",
88+ "Case",
89+ "AIV_MTE2",
90+ "AIV_MTE3",
91+ "Objective Value",
92+ "Result Perf",
93+ ]
94+ 
95+ def read_csv_file(self, file_path: str) -> Tuple[List[Dict[str, str]], List[str]]:
96+ if not os.path.exists(file_path):
97+ raise FileNotFoundError(f"CSV file '{file_path}' not found.")
98+ 
99+ rows = []
100+ with open(file_path, "r", encoding="utf-8-sig") as f:
101+ reader = csv.DictReader(f)
102+ fieldnames = reader.fieldnames
103+ for row in reader:
104+ rows.append(row)
105+ 
106+ return rows, fieldnames
107+ 
108+ def compare_structure(
109+ self, file1_cols: List[str], file2_cols: List[str]
110+ ) -> List[str]:
111+ differences = []
112+ 
113+ if file1_cols != file2_cols:
114+ only_in_file1 = set(file1_cols) - set(file2_cols)
115+ only_in_file2 = set(file2_cols) - set(file1_cols)
116+ 
117+ if only_in_file1:
118+ differences.append(f"列仅在文件1中: {', '.join(only_in_file1)}")
119+ if only_in_file2:
120+ differences.append(f"列仅在文件2中: {', '.join(only_in_file2)}")
121+ 
122+ return differences
123+ 
124+ def compare_operators(
125+ self, file1_rows: List[Dict], file2_rows: List[Dict]
126+ ) -> Tuple[List[str], List[str], List[str]]:
127+ file1_operators = {self._row_key(row): row for row in file1_rows}
128+ file2_operators = {self._row_key(row): row for row in file2_rows}
129+ 
130+ common_operators = set(file1_operators.keys()) & set(file2_operators.keys())
131+ only_in_file1 = set(file1_operators.keys()) - set(file2_operators.keys())
132+ only_in_file2 = set(file2_operators.keys()) - set(file1_operators.keys())
133+ 
134+ return sorted(common_operators), sorted(only_in_file1), sorted(only_in_file2)
135+ 
136+ def parse_numeric(self, value: str) -> Optional[float]:
137+ if value == "" or value == "N/A":
138+ return None
139+ try:
140+ return float(value)
141+ except (ValueError, TypeError):
142+ return None
143+ 
144+ def compare_field_values(
145+ self, value1: str, value2: str, field_name: str
146+ ) -> Optional[FieldDifference]:
147+ if value1 == value2:
148+ return None
149+ 
150+ diff_type = "字符串差异"
151+ numeric_diff = None
152+ percentage_diff = None
153+ 
154+ num1 = self.parse_numeric(value1)
155+ num2 = self.parse_numeric(value2)
156+ 
157+ if num1 is not None and num2 is not None:
158+ diff_type = "数值差异"
159+ numeric_diff = num2 - num1
160+ if num1 != 0:
161+ percentage_diff = (numeric_diff / num1) * 100
162+ else:
163+ percentage_diff = 0.0 if num2 == 0 else float("inf")
164+ 
165+ return FieldDifference(
166+ field_name=field_name,
167+ value1=value1,
168+ value2=value2,
169+ diff_type=diff_type,
170+ numeric_diff=numeric_diff,
171+ percentage_diff=percentage_diff,
172+ )
173+ 
174+ def compare_operator_rows(
175+ self, row1: Dict, row2: Dict, operator: str
176+ ) -> List[FieldDifference]:
177+ differences = []
178+ 
179+ all_fields = set(row1.keys()) | set(row2.keys())
180+ 
181+ for field in all_fields:
182+ value1 = row1.get(field, "N/A")
183+ value2 = row2.get(field, "N/A")
184+ 
185+ diff = self.compare_field_values(value1, value2, field)
186+ if diff:
187+ differences.append(diff)
188+ 
189+ return differences
190+ 
191+ def compare_performance_metrics(
192+ self,
193+ common_operators: List[str],
194+ file1_rows: List[Dict],
195+ file2_rows: List[Dict],
196+ ) -> List[PerformanceDifference]:
197+ file1_operators = {self._row_key(row): row for row in file1_rows}
198+ file2_operators = {self._row_key(row): row for row in file2_rows}
199+ 
200+ performance_diffs = []
201+ 
202+ for operator in common_operators:
203+ row1 = file1_operators[operator]
204+ row2 = file2_operators[operator]
205+ 
206+ for metric in self.performance_fields:
207+ value1 = self.parse_numeric(row1.get(metric, ""))
208+ value2 = self.parse_numeric(row2.get(metric, ""))
209+ 
210+ if value1 is not None and value2 is not None and value1 != value2:
211+ absolute_diff = value2 - value1
212+ percentage_diff = (
213+ (absolute_diff / value1) * 100 if value1 != 0 else 0.0
214+ )
215+ improvement = absolute_diff < 0
216+ 
217+ performance_diffs.append(
218+ PerformanceDifference(
219+ operator=operator,
220+ metric=metric,
221+ value1=value1,
222+ value2=value2,
223+ absolute_diff=absolute_diff,
224+ percentage_diff=percentage_diff,
225+ improvement=improvement,
226+ )
227+ )
228+ 
229+ return performance_diffs
230+ 
231+ def compare_csv_files(self, file1_path: str, file2_path: str) -> ComparisonResult:
232+ file1_rows, file1_cols = self.read_csv_file(file1_path)
233+ file2_rows, file2_cols = self.read_csv_file(file2_path)
234+ 
235+ structure_differences = self.compare_structure(file1_cols, file2_cols)
236+ 
237+ common_operators, only_in_file1, only_in_file2 = self.compare_operators(
238+ file1_rows, file2_rows
239+ )
240+ 
241+ file1_operators = {self._row_key(row): row for row in file1_rows}
242+ file2_operators = {self._row_key(row): row for row in file2_rows}
243+ 
244+ operator_differences = {}
245+ for operator in common_operators:
246+ row1 = file1_operators[operator]
247+ row2 = file2_operators[operator]
248+ diffs = self.compare_operator_rows(row1, row2, operator)
249+ if diffs:
250+ operator_differences[operator] = diffs
251+ 
252+ performance_differences = self.compare_performance_metrics(
253+ common_operators, file1_rows, file2_rows
254+ )
255+ 
256+ return ComparisonResult(
257+ file1_path=file1_path,
258+ file2_path=file2_path,
259+ file1_rows=len(file1_rows),
260+ file2_rows=len(file2_rows),
261+ file1_cols=file1_cols,
262+ file2_cols=file2_cols,
263+ common_operators=common_operators,
264+ only_in_file1=only_in_file1,
265+ only_in_file2=only_in_file2,
266+ operator_differences=operator_differences,
267+ structure_differences=structure_differences,
268+ performance_differences=performance_differences,
269+ )
270+ 
271+ 
272+class ComparisonReporter:
273+ def __init__(self, result: ComparisonResult):
274+ self.result = result
275+ 
276+ def print_console_report(self):
277+ self._print_header()
278+ self._print_file_info()
279+ self._print_structure_comparison()
280+ self._print_operator_comparison()
281+ self._print_detailed_differences()
282+ self._print_performance_comparison()
283+ self._print_summary()
284+ 
285+ def _print_header(self):
286+ print("=" * 80)
287+ print("CSV文件对比分析报告")
288+ print("=" * 80)
289+ print()
290+ 
291+ def _print_file_info(self):
292+ print("文件信息:")
293+ print(f" 文件1: {self.result.file1_path} ({self.result.file1_rows} 个算子)")
294+ print(f" 文件2: {self.result.file2_path} ({self.result.file2_rows} 个算子)")
295+ print()
296+ 
297+ def _print_structure_comparison(self):
298+ print("结构对比:")
299+ print(
300+ f" 列数: {len(self.result.file1_cols)} vs {len(self.result.file2_cols)}",
301+ end="",
302+ )
303+ col_diff = len(self.result.file2_cols) - len(self.result.file1_cols)
304+ if col_diff != 0:
305+ print(f" (差异: {col_diff:+d})")
306+ else:
307+ print(" (相同)")
308+ 
309+ print(f" 行数: {self.result.file1_rows} vs {self.result.file2_rows}", end="")
310+ row_diff = self.result.file2_rows - self.result.file1_rows
311+ if row_diff != 0:
312+ print(f" (差异: {row_diff:+d})")
313+ else:
314+ print(" (相同)")
315+ 
316+ if self.result.structure_differences:
317+ print("\n 结构差异:")
318+ for diff in self.result.structure_differences:
319+ print(f" - {diff}")
320+ 
321+ print()
322+ 
323+ def _print_operator_comparison(self):
324+ print("算子对比:")
325+ print(f" 共同算子: {len(self.result.common_operators)} 个")
326+ if self.result.common_operators:
327+ print(f" {', '.join(self.result.common_operators)}")
328+ 
329+ if self.result.only_in_file1:
330+ print(f" 仅在文件1中: {len(self.result.only_in_file1)} 个")
331+ print(f" {', '.join(self.result.only_in_file1)}")
332+ 
333+ if self.result.only_in_file2:
334+ print(f" 仅在文件2中: {len(self.result.only_in_file2)} 个")
335+ print(f" {', '.join(self.result.only_in_file2)}")
336+ 
337+ print()
338+ 
339+ def _print_detailed_differences(self):
340+ if not self.result.operator_differences:
341+ print("详细差异分析:")
342+ print(" 无差异")
343+ print()
344+ return
345+ 
346+ print("详细差异分析:")
347+ for operator, diffs in self.result.operator_differences.items():
348+ print(f" [{operator}]")
349+ for diff in diffs:
350+ if diff.numeric_diff is not None:
351+ print(
352+ f" - {diff.field_name}: {diff.value1} → {diff.value2}",
353+ end="",
354+ )
355+ if diff.percentage_diff is not None and abs(
356+ diff.percentage_diff
357+ ) != float("inf"):
358+ print(
359+ f" (差异: {diff.numeric_diff:+.6f}, {diff.percentage_diff:+.2f}%)"
360+ )
361+ else:
362+ print(f" (差异: {diff.numeric_diff:+.6f})")
363+ else:
364+ print(f" - {diff.field_name}: {diff.value1} → {diff.value2}")
365+ print()
366+ 
367+ def _print_performance_comparison(self):
368+ if not self.result.performance_differences:
369+ return
370+ 
371+ print("性能指标对比:")
372+ for perf_diff in self.result.performance_differences:
373+ status = "✓ 改善" if perf_diff.improvement else "✗ 下降"
374+ print(f" [{perf_diff.operator}] {perf_diff.metric}")
375+ print(f" {perf_diff.value1:.6f} → {perf_diff.value2:.6f}", end="")
376+ print(
377+ f" ({perf_diff.absolute_diff:+.6f}, {perf_diff.percentage_diff:+.2f}%) {status}"
378+ )
379+ print()
380+ 
381+ def _print_summary(self):
382+ print("=" * 80)
383+ print("对比总结:")
384+ total_differences = sum(
385+ len(diffs) for diffs in self.result.operator_differences.values()
386+ )
387+ print(f" - 总计 {total_differences} 个字段差异")
388+ print(f" - {len(self.result.performance_differences)} 个性能指标变化")
389+ 
390+ if self.result.only_in_file1 or self.result.only_in_file2:
391+ print(
392+ f" - {len(self.result.only_in_file1) + len(self.result.only_in_file2)} 个算子差异"
393+ )
394+ 
395+ has_differences = (
396+ self.result.structure_differences
397+ or self.result.operator_differences
398+ or self.result.only_in_file1
399+ or self.result.only_in_file2
400+ )
401+ 
402+ if not has_differences:
403+ print(" ✓ 两个文件完全相同")
404+ else:
405+ print(" ✓ 两个文件存在差异")
406+ 
407+ print("=" * 80)
408+ 
409+ def save_text_report(self, output_file: str):
410+ with open(output_file, "w", encoding="utf-8") as f:
411+ import sys
412+ 
413+ original_stdout = sys.stdout
414+ sys.stdout = f
415+ 
416+ self.print_console_report()
417+ 
418+ sys.stdout = original_stdout
419+ 
420+ print(f"对比报告已保存到: {output_file}")
421+ 
422+ def save_excel_report(self, output_file: str):
423+ if not HAS_OPENPYXL:
424+ print("Warning: openpyxl not installed. Falling back to text format.")
425+ text_file = output_file.replace(".xlsx", ".txt").replace(".xls", ".txt")
426+ self.save_text_report(text_file)
427+ return
428+ 
429+ wb = openpyxl.Workbook()
430+ 
431+ ws_summary = wb.active
432+ ws_summary.title = "摘要"
433+ self._write_summary_sheet(ws_summary)
434+ 
435+ if self.result.operator_differences:
436+ ws_details = wb.create_sheet("详细差异")
437+ self._write_details_sheet(ws_details)
438+ 
439+ if self.result.performance_differences:
440+ ws_performance = wb.create_sheet("性能对比")
441+ self._write_performance_sheet(ws_performance)
442+ 
443+ wb.save(output_file)
444+ print(f"对比报告已保存到: {output_file}")
445+ 
446+ def _write_summary_sheet(self, ws):
447+ headers = ["项目", "文件1", "文件2", "差异"]
448+ 
449+ for col_num, header in enumerate(headers, 1):
450+ cell = ws.cell(row=1, column=col_num, value=header)
451+ cell.font = Font(bold=True)
452+ cell.fill = PatternFill(
453+ start_color="4472C4", end_color="4472C4", fill_type="solid"
454+ )
455+ cell.alignment = Alignment(horizontal="center", vertical="center")
456+ 
457+ row = 2
458+ ws.cell(row=row, column=1, value="文件路径")
459+ ws.cell(row=row, column=2, value=self.result.file1_path)
460+ ws.cell(row=row, column=3, value=self.result.file2_path)
461+ ws.cell(row=row, column=4, value="")
462+ 
463+ row += 1
464+ ws.cell(row=row, column=1, value="算子数量")
465+ ws.cell(row=row, column=2, value=self.result.file1_rows)
466+ ws.cell(row=row, column=3, value=self.result.file2_rows)
467+ ws.cell(
468+ row=row,
469+ column=4,
470+ value=f"{self.result.file2_rows - self.result.file1_rows:+d}",
471+ )
472+ 
473+ row += 1
474+ ws.cell(row=row, column=1, value="列数")
475+ ws.cell(row=row, column=2, value=len(self.result.file1_cols))
476+ ws.cell(row=row, column=3, value=len(self.result.file2_cols))
477+ ws.cell(
478+ row=row,
479+ column=4,
480+ value=f"{len(self.result.file2_cols) - len(self.result.file1_cols):+d}",
481+ )
482+ 
483+ row += 1
484+ ws.cell(row=row, column=1, value="共同算子")
485+ ws.cell(row=row, column=2, value=len(self.result.common_operators))
486+ ws.cell(row=row, column=3, value=len(self.result.common_operators))
487+ ws.cell(row=row, column=4, value="相同")
488+ 
489+ row += 1
490+ ws.cell(row=row, column=1, value="仅在文件1中")
491+ ws.cell(row=row, column=2, value=len(self.result.only_in_file1))
492+ ws.cell(row=row, column=3, value=0)
493+ ws.cell(row=row, column=4, value="")
494+ 
495+ row += 1
496+ ws.cell(row=row, column=1, value="仅在文件2中")
497+ ws.cell(row=row, column=2, value=0)
498+ ws.cell(row=row, column=3, value=len(self.result.only_in_file2))
499+ ws.cell(row=row, column=4, value="")
500+ 
501+ row += 1
502+ total_differences = sum(
503+ len(diffs) for diffs in self.result.operator_differences.values()
504+ )
505+ ws.cell(row=row, column=1, value="字段差异总数")
506+ ws.cell(row=row, column=2, value="")
507+ ws.cell(row=row, column=3, value="")
508+ ws.cell(row=row, column=4, value=total_differences)
509+ 
510+ row += 1
511+ ws.cell(row=row, column=1, value="性能指标变化数")
512+ ws.cell(row=row, column=2, value="")
513+ ws.cell(row=row, column=3, value="")
514+ ws.cell(row=row, column=4, value=len(self.result.performance_differences))
515+ 
516+ for col_num in range(1, 5):
517+ ws.column_dimensions[openpyxl.utils.get_column_letter(col_num)].width = 20
518+ 
519+ def _write_details_sheet(self, ws):
520+ headers = [
521+ "算子",
522+ "字段名",
523+ "文件1值",
524+ "文件2值",
525+ "差异类型",
526+ "数值差异",
527+ "百分比差异",
528+ ]
529+ 
530+ for col_num, header in enumerate(headers, 1):
531+ cell = ws.cell(row=1, column=col_num, value=header)
532+ cell.font = Font(bold=True)
533+ cell.fill = PatternFill(
534+ start_color="4472C4", end_color="4472C4", fill_type="solid"
535+ )
536+ cell.alignment = Alignment(horizontal="center", vertical="center")
537+ 
538+ row = 2
539+ for operator, diffs in self.result.operator_differences.items():
540+ for diff in diffs:
541+ ws.cell(row=row, column=1, value=operator)
542+ ws.cell(row=row, column=2, value=diff.field_name)
543+ ws.cell(row=row, column=3, value=diff.value1)
544+ ws.cell(row=row, column=4, value=diff.value2)
545+ ws.cell(row=row, column=5, value=diff.diff_type)
546+ 
547+ if diff.numeric_diff is not None:
548+ ws.cell(row=row, column=6, value=diff.numeric_diff)
549+ if diff.percentage_diff is not None and abs(
550+ diff.percentage_diff
551+ ) != float("inf"):
552+ ws.cell(row=row, column=7, value=diff.percentage_diff)
553+ 
554+ row += 1
555+ 
556+ for col_num in range(1, 8):
557+ ws.column_dimensions[openpyxl.utils.get_column_letter(col_num)].width = 15
558+ 
559+ def _write_performance_sheet(self, ws):
560+ headers = [
561+ "算子",
562+ "性能指标",
563+ "文件1值",
564+ "文件2值",
565+ "绝对差异",
566+ "百分比差异",
567+ "状态",
568+ ]
569+ 
570+ for col_num, header in enumerate(headers, 1):
571+ cell = ws.cell(row=1, column=col_num, value=header)
572+ cell.font = Font(bold=True)
573+ cell.fill = PatternFill(
574+ start_color="4472C4", end_color="4472C4", fill_type="solid"
575+ )
576+ cell.alignment = Alignment(horizontal="center", vertical="center")
577+ 
578+ row = 2
579+ for perf_diff in self.result.performance_differences:
580+ ws.cell(row=row, column=1, value=perf_diff.operator)
581+ ws.cell(row=row, column=2, value=perf_diff.metric)
582+ ws.cell(row=row, column=3, value=perf_diff.value1)
583+ ws.cell(row=row, column=4, value=perf_diff.value2)
584+ ws.cell(row=row, column=5, value=perf_diff.absolute_diff)
585+ ws.cell(row=row, column=6, value=perf_diff.percentage_diff)
586+ ws.cell(
587+ row=row, column=7, value="改善" if perf_diff.improvement else "下降"
588+ )
589+ row += 1
590+ 
591+ for col_num in range(1, 8):
592+ ws.column_dimensions[openpyxl.utils.get_column_letter(col_num)].width = 15
593+ 
594+ 
595+def main():
596+ parser = argparse.ArgumentParser(
597+ description="Compare two CSV files generated by summary_templates.py"
598+ )
599+ parser.add_argument("file1", help="Path to first CSV file")
600+ parser.add_argument("file2", help="Path to second CSV file")
601+ parser.add_argument(
602+ "-o", "--output", help="Output file path (optional, default: print to console)"
603+ )
604+ parser.add_argument(
605+ "-f",
606+ "--format",
607+ choices=["console", "text", "excel"],
608+ default="console",
609+ help="Output format: console (default), text, or excel",
610+ )
611+ 
612+ args = parser.parse_args()
613+ 
614+ try:
615+ comparator = CSVComparator()
616+ result = comparator.compare_csv_files(args.file1, args.file2)
617+ 
618+ reporter = ComparisonReporter(result)
619+ 
620+ if args.format == "console":
621+ if args.output:
622+ reporter.save_text_report(args.output)
623+ else:
624+ reporter.print_console_report()
625+ elif args.format == "text":
626+ output_file = args.output if args.output else "comparison_report.txt"
627+ reporter.save_text_report(output_file)
628+ elif args.format == "excel":
629+ output_file = args.output if args.output else "comparison_report.xlsx"
630+ reporter.save_excel_report(output_file)
631+ 
632+ except FileNotFoundError as e:
633+ print(f"错误: {e}")
634+ except Exception as e:
635+ print(f"错误: {e}")
636+ 
637+ 
638+if __name__ == "__main__":
639+ main()
@@ -0,0 +1,14 @@
1+# -*- coding: utf-8 -*-
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 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+"""Core data models and analysis helpers for ATT."""
13+ 
14+__all__ = []
@@ -0,0 +1,119 @@
1+#!/usr/bin/env python3
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+# ----------------------------------------------------------------------------------------------------------
12+# Copyright (c) 2025 Huawei Technologies Co., Ltd.
13+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
14+# CANN Open Software License Agreement Version 2.0 (the "License").
15+# Please refer to the License for details. You may not use this file except in compliance with the License.
16+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
17+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
18+# See LICENSE in the root of the software repository for the full text of the License.
19+# ----------------------------------------------------------------------------------------------------------
20+ 
21+"""Canonical, JSON-serialisable schema used by the ``evidence`` command.
22+ 
23+The legacy ATT parser returns a dataclass intended for human-oriented CSV
24+output. This module is deliberately small and has no dependency on the CLI
25+or parser internals so that the skill can consume the generated JSONL later.
26+"""
27+ 
28+from __future__ import annotations
29+ 
30+from dataclasses import asdict, is_dataclass
31+from typing import Any, Dict, Mapping, Optional
32+ 
33+ 
34+SCHEMA_VERSION = "att-evidence/v1"
35+ 
36+ 
37+def _json_value(value: Any) -> Any:
38+ """Convert parser values to values accepted by :mod:`json`.
39+ 
40+ In particular, dictionaries may contain values represented as numpy-like
41+ scalar objects in downstream integrations. ``item`` is used when
42+ available while preserving ordinary Python values unchanged.
43+ """
44+ 
45+ if is_dataclass(value):
46+ return {key: _json_value(item) for key, item in asdict(value).items()}
47+ if isinstance(value, Mapping):
48+ return {str(key): _json_value(item) for key, item in value.items()}
49+ if isinstance(value, (list, tuple)):
50+ return [_json_value(item) for item in value]
51+ item = getattr(value, "item", None)
52+ if callable(item):
53+ try:
54+ return item()
55+ except (TypeError, ValueError):
56+ pass
57+ return value
58+ 
59+ 
60+def make_record(
61+ *,
62+ operator: str,
63+ graph_id: Optional[int],
64+ result_id: Optional[int],
65+ group_id: Optional[int],
66+ case_id: Optional[int],
67+ tiling_values: Optional[Mapping[str, Any]] = None,
68+ objective: Any = None,
69+ source_path: str,
70+ source_line: Optional[int],
71+ parse_status: str = "ok",
72+ result_performance: Any = None,
73+ source_lines: Optional[Mapping[str, int]] = None,
74+) -> Dict[str, Any]:
75+ """Build one canonical ATT evidence record.
76+ 
77+ The required fields are intentionally explicit rather than copying an
78+ ``OperatorSummary`` wholesale. Additional fields are additive and keep
79+ provenance useful to consumers without changing the required schema.
80+ """
81+ 
82+ record: Dict[str, Any] = {
83+ "schema_version": SCHEMA_VERSION,
84+ "operator": operator,
85+ "graph_id": graph_id,
86+ "result_id": result_id,
87+ "group_id": group_id,
88+ "case_id": case_id,
89+ "tiling_values": dict(tiling_values or {}),
90+ "objective": objective,
91+ "source_path": source_path,
92+ "source_line": source_line,
93+ "parse_status": parse_status,
94+ }
95+ if result_performance is not None:
96+ record["result_performance"] = result_performance
97+ if source_lines:
98+ record["source_lines"] = dict(source_lines)
99+ return _json_value(record)
100+ 
101+ 
102+def record_from_summary(
103+ summary: Any, source_path: str, source_line: Optional[int]
104+) -> Dict[str, Any]:
105+ """Adapt a legacy ``OperatorSummary`` to the canonical record shape."""
106+ 
107+ return make_record(
108+ operator=summary.operator_name,
109+ graph_id=summary.graph,
110+ result_id=summary.result,
111+ group_id=summary.group,
112+ case_id=summary.case,
113+ tiling_values=summary.tiling_values,
114+ objective=summary.objective_value,
115+ result_performance=summary.result_performance,
116+ source_path=source_path,
117+ source_line=source_line,
118+ parse_status=summary.parse_status,
119+ )
@@ -0,0 +1,41 @@
1+#!/usr/bin/env python3
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+import os
12+from typing import List
13+ 
14+ 
15+def find_log_files(path: str) -> List[str]:
16+ """递归查找 .log 文件;path 可以是文件或目录"""
17+ if os.path.isfile(path):
18+ return [path]
19+ if os.path.isdir(path):
20+ result = []
21+ for root, _, files in os.walk(path):
22+ for fname in files:
23+ if fname.endswith(".log"):
24+ result.append(os.path.join(root, fname))
25+ return sorted(result)
26+ return []
27+ 
28+ 
29+def ensure_output_dir(path: str) -> str:
30+ """确保目录存在,返回 path"""
31+ os.makedirs(path, exist_ok=True)
32+ return path
33+ 
34+ 
35+def build_case_output_path(
36+ base: str, op: str, result_id: int, group_id: int, case_num: int
37+) -> str:
38+ """构造拆分输出路径:base/op/graph0_result{r}/g{g}/case{n}.log"""
39+ return os.path.join(
40+ base, op, f"graph0_result{result_id}", f"g{group_id}", f"case{case_num}.log"
41+ )