已合并
feat: 支持ATT analysis tool #1855
zhang_shengjie创建于 8月27日
feat: 支持ATT analysis tool #1855
已合并
共 48 个文件变更+5568-0
| @@ -66,6 +66,12 @@ repos: | |||
| 66 | # OAT 合规性检查 (Open Source Audit Tool) | 66 | # OAT 合规性检查 (Open Source Audit Tool) |
| 67 | - repo: local | 67 | - 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-check | 75 | - id: oat-check |
| 70 | name: OAT Compliance Check | 76 | name: OAT Compliance Check |
| 71 | entry: bash scripts/oat_check.sh | 77 | 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | + | ||
| 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 | + ) | ||