ops-tune-workbench-plugin 0.0.0-stage → 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/NOTICE.md +14 -0
  2. package/README.md +39 -2
  3. package/cordis.patch.yml +20 -0
  4. package/lib/client.js +4253 -0
  5. package/lib/host.js +2518 -0
  6. package/package.json +53 -4
  7. package/skills/tool-ops-tune/INSTALL.md +52 -0
  8. package/skills/tool-ops-tune/README.md +167 -0
  9. package/skills/tool-ops-tune/SKILL.md +93 -0
  10. package/skills/tool-ops-tune/agent/AGENTS.md +195 -0
  11. package/skills/tool-ops-tune/agent/agents/perf-implementer.md +68 -0
  12. package/skills/tool-ops-tune/agent/agents/perf-reviewer.md +86 -0
  13. package/skills/tool-ops-tune/agent/workflows/task-prompts.md +362 -0
  14. package/skills/tool-ops-tune/agent/workflows/troubleshooting.md +43 -0
  15. package/skills/tool-ops-tune/docs/figures/opstune-architecture.png +0 -0
  16. package/skills/tool-ops-tune/docs/figures/opstune-architecture.svg +253 -0
  17. package/skills/tool-ops-tune/docs/ops-tune-design.md +404 -0
  18. package/skills/tool-ops-tune/docs//347/224/250/344/276/213/346/236/204/345/273/272.md +714 -0
  19. package/skills/tool-ops-tune/docs//347/237/245/350/257/206/345/272/223/350/247/204/350/214/203.md +103 -0
  20. package/skills/tool-ops-tune/evals/evals.json +242 -0
  21. package/skills/tool-ops-tune/ops-collect/SKILL.md +146 -0
  22. package/skills/tool-ops-tune/ops-collect/references/tool-routing.md +86 -0
  23. package/skills/tool-ops-tune/ops-collect/scripts/collect_board.sh +444 -0
  24. package/skills/tool-ops-tune/ops-collect/scripts/collect_cases.py +368 -0
  25. package/skills/tool-ops-tune/ops-collect/scripts/collect_sim.sh +134 -0
  26. package/skills/tool-ops-tune/ops-collect/scripts/csv_to_db.py +314 -0
  27. package/skills/tool-ops-tune/ops-collect/scripts/db_bench.py +545 -0
  28. package/skills/tool-ops-tune/ops-collect/scripts/facts_bridge.py +1063 -0
  29. package/skills/tool-ops-tune/ops-collect/scripts/isolation_test.py +454 -0
  30. package/skills/tool-ops-tune/ops-collect/scripts/jsonl_to_cases.py +163 -0
  31. package/skills/tool-ops-tune/ops-family/SKILL.md +76 -0
  32. package/skills/tool-ops-tune/ops-family/references/broadcast/advanced_tiling.md +92 -0
  33. package/skills/tool-ops-tune/ops-family/references/broadcast/broadcast_design.md +160 -0
  34. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_add_kernel.cpp +126 -0
  35. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_add_tiling.cpp +144 -0
  36. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_tiling_common.h +401 -0
  37. package/skills/tool-ops-tune/ops-family/references/broadcast/code/onedim_add_kernel.cpp +107 -0
  38. package/skills/tool-ops-tune/ops-family/references/broadcast/code/onedim_add_tiling.cpp +64 -0
  39. package/skills/tool-ops-tune/ops-family/references/broadcast/datacopypad_design.md +46 -0
  40. package/skills/tool-ops-tune/ops-family/references/broadcast/guide.md +14 -0
  41. package/skills/tool-ops-tune/ops-family/references/broadcast/nddma_design.md +53 -0
  42. package/skills/tool-ops-tune/ops-family/references/broadcast/onedim_design.md +114 -0
  43. package/skills/tool-ops-tune/ops-family/references/broadcast/ub_broadcast_design.md +67 -0
  44. package/skills/tool-ops-tune/ops-family/references/common/datacopy_optimization_design.md +351 -0
  45. package/skills/tool-ops-tune/ops-family/references/common/index.md +9 -0
  46. package/skills/tool-ops-tune/ops-family/references/common/tail_block_design.md +268 -0
  47. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/index.md +38 -0
  48. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/timing_harness_aclrt_event.cpp +199 -0
  49. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/timing_harness_torch_event.py +130 -0
  50. package/skills/tool-ops-tune/ops-family/references/common/ub_resident_design.md +342 -0
  51. package/skills/tool-ops-tune/ops-family/references/conversion/guide.md +69 -0
  52. package/skills/tool-ops-tune/ops-family/references/conversion/index.md +4 -0
  53. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_base.template +81 -0
  54. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_big_dim.template +217 -0
  55. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_cut_one_axis.template +336 -0
  56. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_cut_two_axis.template +781 -0
  57. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_n_last.template +340 -0
  58. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_small_shape.template +321 -0
  59. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_tensor_move.template +102 -0
  60. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_transdata_5hd.template +480 -0
  61. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_transdata_5hd_021axis.template +533 -0
  62. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_with_gather.template +688 -0
  63. package/skills/tool-ops-tune/ops-family/references/conversion/templates/transpose_tiling_data.template +192 -0
  64. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_big_dim.md +220 -0
  65. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_cut_one_axis.md +175 -0
  66. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_cut_two_axis.md +177 -0
  67. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_fusion_design.md +398 -0
  68. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_gather.md +191 -0
  69. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_n_last.md +276 -0
  70. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_small_shape.md +315 -0
  71. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_tensor_move.md +203 -0
  72. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_tiling_data.md +176 -0
  73. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_vconv_021.md +206 -0
  74. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_vconv_5hd.md +179 -0
  75. package/skills/tool-ops-tune/ops-family/references/elementwise/double_buffer_design.md +288 -0
  76. package/skills/tool-ops-tune/ops-family/references/elementwise/guide.md +46 -0
  77. package/skills/tool-ops-tune/ops-family/references/elementwise/index.md +10 -0
  78. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/CMakeLists.txt +131 -0
  79. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/README.md +55 -0
  80. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/add_custom_torch.cpp +80 -0
  81. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/ops.h +23 -0
  82. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/register.cpp +30 -0
  83. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_host/add_custom.asc +149 -0
  84. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_host/data_utils.h +97 -0
  85. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_kernel/add_custom_kernel.asc +136 -0
  86. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_kernel/add_custom_tiling.h +31 -0
  87. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/run.sh +86 -0
  88. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/gen_data.py +70 -0
  89. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/golden.py +34 -0
  90. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/test_torch.py +99 -0
  91. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/verify_result.py +56 -0
  92. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/usage_guide.md +61 -0
  93. package/skills/tool-ops-tune/ops-family/references/elementwise/vector_efficiency_design.md +203 -0
  94. package/skills/tool-ops-tune/ops-family/references/fa/fa_mixed_kernel_design.md +174 -0
  95. package/skills/tool-ops-tune/ops-family/references/fa/fa_roofline_design.md +141 -0
  96. package/skills/tool-ops-tune/ops-family/references/fa/guide.md +87 -0
  97. package/skills/tool-ops-tune/ops-family/references/knowledge_gaps.json +56 -0
  98. package/skills/tool-ops-tune/ops-family/references/matmul/best-practices-guide.md +14 -0
  99. package/skills/tool-ops-tune/ops-family/references/matmul/fullload_design.md +344 -0
  100. package/skills/tool-ops-tune/ops-family/references/matmul/guide.md +52 -0
  101. package/skills/tool-ops-tune/ops-family/references/matmul/mte2_preload_design.md +320 -0
  102. package/skills/tool-ops-tune/ops-family/references/matmul/pingpong_design.md +254 -0
  103. package/skills/tool-ops-tune/ops-family/references/matmul/scale_coalescing_design.md +282 -0
  104. package/skills/tool-ops-tune/ops-family/references/matmul/streamk_design.md +374 -0
  105. package/skills/tool-ops-tune/ops-family/references/matmul/swat_design.md +423 -0
  106. package/skills/tool-ops-tune/ops-family/references/matmul/templates/matmul_basic_blaze.template +165 -0
  107. package/skills/tool-ops-tune/ops-family/references/matmul/templates/usage_guide.md +29 -0
  108. package/skills/tool-ops-tune/ops-family/references/mc2/best-practices-guide.md +43 -0
  109. package/skills/tool-ops-tune/ops-family/references/mc2/guide.md +42 -0
  110. package/skills/tool-ops-tune/ops-family/references/mc2/local_matmul_design.md +130 -0
  111. package/skills/tool-ops-tune/ops-family/references/mc2/pipeline_balancing_design.md +502 -0
  112. package/skills/tool-ops-tune/ops-family/references/reduce/guide.md +178 -0
  113. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/kernel_utils.template +75 -0
  114. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_full_load.template +429 -0
  115. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_recompute.template +485 -0
  116. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_small_r.template +348 -0
  117. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_full_load.template +574 -0
  118. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_online.template +324 -0
  119. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_recompute.template +436 -0
  120. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_base.template +950 -0
  121. package/skills/tool-ops-tune/ops-family/references/reduce/templates/fused_attention_online_softmax_design.md +428 -0
  122. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_full_load.md +137 -0
  123. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_recompute.md +135 -0
  124. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_small_r.md +122 -0
  125. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_full_load.md +144 -0
  126. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_online.md +185 -0
  127. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_recompute.md +163 -0
  128. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_tiling.template +460 -0
  129. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_tiling_data.template +278 -0
  130. package/skills/tool-ops-tune/ops-family/references/reduce/templates/state_resident_design.md +337 -0
  131. package/skills/tool-ops-tune/ops-family/references/reduce/templates/usage_guide.md +253 -0
  132. package/skills/tool-ops-tune/ops-family/references/scalar/coding_principles.md +476 -0
  133. package/skills/tool-ops-tune/ops-family/references/scalar/conv_scalar.md +163 -0
  134. package/skills/tool-ops-tune/ops-family/references/scalar/guide.md +30 -0
  135. package/skills/tool-ops-tune/ops-family/references/simt/guide.md +91 -0
  136. package/skills/tool-ops-tune/ops-family/references/simt/optimization-guide.md +110 -0
  137. package/skills/tool-ops-tune/ops-family/references/sort/guide.md +83 -0
  138. package/skills/tool-ops-tune/ops-family/references/sort/radix_sort.md +354 -0
  139. package/skills/tool-ops-tune/ops-family/scripts/family_asset_audit.py +68 -0
  140. package/skills/tool-ops-tune/ops-family/scripts/tiling_candidates.py +419 -0
  141. package/skills/tool-ops-tune/ops-mechanism/SKILL.md +23 -0
  142. package/skills/tool-ops-tune/ops-mechanism/cases/ascendc/case_groupedmatmul_pipeline.md +23 -0
  143. package/skills/tool-ops-tune/ops-mechanism/cases/ascendc/case_triton_atomic_revert.md +19 -0
  144. package/skills/tool-ops-tune/ops-mechanism/cases/mc2/case_mc2_fusion.md +19 -0
  145. package/skills/tool-ops-tune/ops-mechanism/references/optimize/api-usage.md +70 -0
  146. package/skills/tool-ops-tune/ops-mechanism/references/optimize/ascendc-regbase-vf.md +45 -0
  147. package/skills/tool-ops-tune/ops-mechanism/references/optimize/ascendc-tiling.md +51 -0
  148. package/skills/tool-ops-tune/ops-mechanism/references/optimize/data-copy.md +67 -0
  149. package/skills/tool-ops-tune/ops-mechanism/references/optimize/index.md +45 -0
  150. package/skills/tool-ops-tune/ops-mechanism/references/optimize/mc2-balancing.md +77 -0
  151. package/skills/tool-ops-tune/ops-mechanism/references/optimize/mc2.md +103 -0
  152. package/skills/tool-ops-tune/ops-mechanism/references/optimize/memory-hierarchy.md +40 -0
  153. package/skills/tool-ops-tune/ops-mechanism/references/optimize/pipeline.md +95 -0
  154. package/skills/tool-ops-tune/ops-mechanism/scripts/case_backfill.py +170 -0
  155. package/skills/tool-ops-tune/ops-mechanism/scripts/log_gap.py +145 -0
  156. package/skills/tool-ops-tune/ops-mechanism/scripts/mc2_search.py +367 -0
  157. package/skills/tool-ops-tune/ops-report/SKILL.md +77 -0
  158. package/skills/tool-ops-tune/ops-verdict/evals/run_evals.py +837 -0
  159. package/skills/tool-ops-tune/ops-verdict/evals/smoke/install-hooks.sh +65 -0
  160. package/skills/tool-ops-tune/ops-verdict/evals/smoke/run_smoke.sh +405 -0
  161. package/skills/tool-ops-tune/ops-verdict/fixtures/core_tail_a5.csv +25 -0
  162. package/skills/tool-ops-tune/ops-verdict/fixtures/few_cores_a5.csv +5 -0
  163. package/skills/tool-ops-tune/ops-verdict/fixtures/mte2_bound_a2.csv +17 -0
  164. package/skills/tool-ops-tune/ops-verdict/fixtures/multi_kernel_mix_a5.csv +7 -0
  165. package/skills/tool-ops-tune/ops-verdict/fixtures/no_bound_a5.csv +9 -0
  166. package/skills/tool-ops-tune/ops-verdict/fixtures/serial_no_overlap_a2.csv +9 -0
  167. package/skills/tool-ops-tune/ops-verdict/fixtures/vec_bound_a5.csv +9 -0
  168. package/skills/tool-ops-tune/ops-verdict/references/io-contracts.md +94 -0
  169. package/skills/tool-ops-tune/ops-verdict/references/rule-schema.md +130 -0
  170. package/skills/tool-ops-tune/ops-verdict/references/scope-schema.md +61 -0
  171. package/skills/tool-ops-tune/ops-verdict/references/soc-spec.yaml +140 -0
  172. package/skills/tool-ops-tune/ops-verdict/rules/L0-instruction.yaml +67 -0
  173. package/skills/tool-ops-tune/ops-verdict/rules/L1-single-core.yaml +316 -0
  174. package/skills/tool-ops-tune/ops-verdict/rules/L2-inter-core.yaml +173 -0
  175. package/skills/tool-ops-tune/ops-verdict/rules/L3-inter-card.yaml +156 -0
  176. package/skills/tool-ops-tune/ops-verdict/scripts/batch_pipeline.py +444 -0
  177. package/skills/tool-ops-tune/ops-verdict/scripts/calibrate.py +249 -0
  178. package/skills/tool-ops-tune/ops-verdict/scripts/delta_compare.py +244 -0
  179. package/skills/tool-ops-tune/ops-verdict/scripts/render_html.py +274 -0
  180. package/skills/tool-ops-tune/ops-verdict/scripts/snapshot.py +200 -0
  181. package/skills/tool-ops-tune/ops-verdict/scripts/trace_bubble.py +331 -0
  182. package/skills/tool-ops-tune/ops-verdict/scripts/verdict.py +1072 -0
  183. package/skills/tool-ops-tune/scripts/diff_audit.py +281 -0
@@ -0,0 +1,1072 @@
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
+ """verdict.py — 算子调优判定引擎(规则-指标绑定契约 + 分层路由 + 反证 + 置信度)
13
+
14
+ 设计对齐(见 docs/算子调优AGENT设计文档.md §5 核心设计决策):
15
+ - 对标业界领先 GPU Profiling 工具的 Guided Analysis:规则声明绑定指标,未采集则规则不触发
16
+ (机制上杜绝「指标没采却报规则」);REQUIRED 缺失 → rule: SKIPPED,OPTIONAL 缺失 → 降级不阻断(optional 仅用于反证;condition 内引用必须 required——加载校验 V4 强制)。
17
+ - 判定进脚本、解释进 LLM:本引擎输出结构化 verdict.json(机器可读契约),
18
+ Agent/LLM 只消费判定书做解释与编排,不做判定。
19
+ - 分层瀑布:L3 → L2 → L1 → L0 依序评估;L3 路由规则可禁用/改道下层规则(如 MTE2 污染)。
20
+ - 每条命中规则带反证(counter_evidence)与置信度;无证据不下结论。
21
+
22
+ 用法:
23
+ # 单核判定(PipeUtilization.csv)
24
+ python3 verdict.py --rules ../rules/L1-single-core.yaml --csv PipeUtilization.csv \
25
+ --scalar mte2_active_bw_pct_of_peak=82 --soc a5 -o verdict.json
26
+
27
+ # 多 kernel CSV(msprof op_summary 全量导出):必须 --kernel-name 过滤(G15)
28
+ python3 verdict.py --rules ../rules/L1-single-core.yaml --csv op_summary.csv \
29
+ --kernel-name my_custom_kernel --soc a5 -o verdict.json
30
+
31
+ # 核间判定(CSV + OpBasicInfo 衍生量)
32
+ python3 verdict.py --rules ../rules/L2-inter-core.yaml --csv PipeUtilization.csv \
33
+ --scalar physical_cores=48,block_dim=24,task_duration_us=210 -o verdict.json
34
+
35
+ # MC² 判定(隔离测试标量输入)
36
+ python3 verdict.py --rules ../rules/L3-inter-card.yaml \
37
+ --scalar t_comm_us=420,t_compute_us=880,mte2_ratio_full=62,mte2_ratio_pure=41 -o verdict.json
38
+
39
+ 输出(verdict.json 契约,report/agent 消费同一数据源):
40
+ {
41
+ "schema": "opstune.verdict/1.0",
42
+ "generated_at": "...", "rules_file": "...", "soc": "a5",
43
+ "facts": {...指标事实(含来源 CSV/行数)...},
44
+ "findings": [ {rule_id, name, severity, confidence, evidence, counter_evidence[],
45
+ speedup_range, mechanism_refs, ...} ],
46
+ "skipped": [ {rule_id, reason, missing} ],
47
+ "verdict": "NEEDS_OPTIMIZATION | NEAR_LIMIT | NO_BOUND_FOUND | INSUFFICIENT_DATA",
48
+ "headline": "...", "recommended_next": [...]
49
+ }
50
+
51
+ 阈值纪律:所有阈值带 threshold_source 溯源(实践沉淀 / 🟡 待标定)。
52
+ 变更规则或阈值必须先过 evals/run_evals.py(fixture CSV → 断言判定输出)全绿。
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import argparse
58
+ import csv
59
+ import datetime
60
+ import json
61
+ import sys
62
+ from pathlib import Path
63
+
64
+ SCHEMA_VERDICT = (
65
+ "opstune.verdict/1.1" # 1.1:拆分 NO_BOUND_FOUND / NEAR_LIMIT(检视 F7)
66
+ )
67
+
68
+
69
+ # --------------------------------------------------------------------------
70
+ # CSV 加载与指标事实构建
71
+ # --------------------------------------------------------------------------
72
+
73
+
74
+ def load_csv_rows(csv_path: str) -> list[dict]:
75
+ """读 CSV(兼容 utf-8-sig BOM 与脏数据,容错处理 msprof 导出的真实表头)。"""
76
+ rows = []
77
+ with open(csv_path, encoding="utf-8-sig", errors="replace") as f:
78
+ for row in csv.DictReader(f):
79
+ rows.append(row)
80
+ return rows
81
+
82
+
83
+ def load_db_rows(db_path: str, table: str) -> list[dict]:
84
+ """读 npu-compute summary.db 表 → 与 CSV 行同构的 dict 行(Roadmap #4 DB 输入)。
85
+
86
+ 列名规范化差异由 FieldUnitMap 还原:DB `aiv_time` → 键 `aiv_time(us)`——
87
+ 下游 pipe 指标/kernel 分组/规则求值零改动(同一批候选列命中)。
88
+ NULL → "NA" 字符串(与 CSV 侧 NA 语义对齐,parse_float 按缺失处理)。
89
+ DB 是同一次采集 CSV 的入库副本(npu-compute 双格式),数值口径等价;
90
+ 全表 dict 化形态 DB 并不占优(行对象化开销 > csv.DictReader,实测
91
+ 见 ops-collect db_bench W2)——DB 输入的价值在 facts 聚合下推与多表查询。
92
+ """
93
+ import sqlite3
94
+
95
+ db = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
96
+ try:
97
+ tables = {
98
+ r[0]
99
+ for r in db.execute(
100
+ "SELECT name FROM sqlite_master WHERE type='table'"
101
+ ).fetchall()
102
+ }
103
+ if table not in tables:
104
+ raise SystemExit(
105
+ f"ERROR: summary.db 无表 {table}(表集合 {sorted(tables)})。"
106
+ f"检查 --db-table 或改喂 Section CSV(--csv)。"
107
+ )
108
+ units = {}
109
+ if "FieldUnitMap" in tables:
110
+ units = dict(db.execute("SELECT field_name, unit FROM FieldUnitMap"))
111
+ cur = db.execute(f'SELECT * FROM "{table}" ORDER BY rowid')
112
+ names = [d[0] for d in cur.description]
113
+ keys = [f"{n}({units[n]})" if units.get(n) else n for n in names]
114
+ return [
115
+ {k: ("NA" if v is None else v) for k, v in zip(keys, row)}
116
+ for row in cur.fetchall()
117
+ ]
118
+ finally:
119
+ db.close()
120
+
121
+
122
+ def parse_float(value) -> float:
123
+ try:
124
+ return float(str(value).strip().replace("%", ""))
125
+ except (ValueError, AttributeError, TypeError):
126
+ return None # 缺失值:None 而非 0.0 —— 0.0 会污染 mean 与条件判定
127
+
128
+
129
+ def ratio_to_pct(v: float) -> float:
130
+ """兼容 0~1 比例与 0~100 百分数两种版本字段。"""
131
+ if v is None:
132
+ return None
133
+ return v * 100 if 0 <= v <= 1.5 else v
134
+
135
+
136
+ def first_present(row: dict, columns: list) -> float:
137
+ for col in columns:
138
+ if col in row and str(row.get(col, "")).strip() not in ("", "N/A", "NA", "-"):
139
+ v = parse_float(row[col])
140
+ if v is not None:
141
+ return v
142
+ return None
143
+
144
+
145
+ def pipe_metrics_from_rows(rows: list[dict], spec: dict) -> dict:
146
+ """从 PipeUtilization 行构建 pipe 指标:per-row 取候选列最大值(aic/aiv 双列兼容),
147
+ 跨行 mean / max 双口径 —— 判定用 mean,max 仅识别长尾(单核峰值不代表全 kernel)。"""
148
+ out = {}
149
+ pipe_cols = []
150
+ for name, m in spec.items():
151
+ if m.get("source") != "pipe":
152
+ continue
153
+ pipe_cols.append((name, m))
154
+ per_row = [first_present(r, m["columns"]) for r in rows]
155
+ per_row = [
156
+ ratio_to_pct(v)
157
+ if m.get("normalize") == "ratio_to_pct" and v is not None
158
+ else v
159
+ for v in per_row
160
+ ]
161
+ valid = [v for v in per_row if v is not None]
162
+ out[f"{name}"] = {
163
+ "mean": (sum(valid) / len(valid)) if valid else None,
164
+ "max": max(valid) if valid else None,
165
+ "rows_present": len(valid),
166
+ "rows_total": len(rows),
167
+ }
168
+ # 衍生量:主导 pipe / pipe 求和(串行指纹)——仅聚合 mean 语义
169
+ means = {
170
+ n: out[n]["mean"]
171
+ for n, _ in pipe_cols
172
+ if out.get(n, {}).get("mean") is not None
173
+ }
174
+ if means:
175
+ out["__max_pipe_mean"] = {
176
+ "mean": max(means.values()),
177
+ "source": "derived:max_of_pipe_means",
178
+ }
179
+ out["__max_pipe_name"] = {
180
+ "mean_name": max(means, key=means.get),
181
+ "source": "derived",
182
+ }
183
+ out["__pipe_sum_mean"] = {
184
+ "mean": sum(means.values()),
185
+ "source": "derived:sum_of_pipe_means",
186
+ }
187
+ return out
188
+
189
+
190
+ def time_metrics_from_rows(rows: list[dict]) -> dict:
191
+ """逐核耗时(核间层):aiv_time/aic_time 双列兼容,取大者为该核主体时间。"""
192
+ times = []
193
+ for r in rows:
194
+ aiv = parse_float(r.get("aiv_time(us)"))
195
+ aic = parse_float(r.get("aic_time(us)"))
196
+ cand = [v for v in (aiv, aic) if v is not None]
197
+ if cand:
198
+ times.append(max(cand))
199
+ if not times:
200
+ return {}
201
+ mean = sum(times) / len(times)
202
+ return {
203
+ "row_count": {"value": float(len(times))},
204
+ "time_mean_us": {"value": mean},
205
+ "time_max_us": {"value": max(times)},
206
+ "time_min_us": {"value": min(times)},
207
+ "dominant_time_us": {
208
+ "value": mean
209
+ }, # 逐核均值(与 yaml 描述一致;L2_HEAD_OVERHEAD 消费)
210
+ "__tail_ratio": {"value": (max(times) / mean) if mean > 0 else None},
211
+ "__imbalance_pct": {
212
+ "value": ((max(times) - min(times)) / max(times) * 100)
213
+ if max(times) > 0
214
+ else None
215
+ },
216
+ "__two_cluster_gap_pct": {"value": _two_cluster_gap(times)},
217
+ }
218
+
219
+
220
+ def _two_cluster_gap(times: list[float]):
221
+ """两簇检测:前后半段核均值差距 %(NUMA/L2 slice 负载偏斜提示)。"""
222
+ if len(times) < 4:
223
+ return None
224
+ half = len(times) // 2
225
+ m1, m2 = sum(times[:half]) / half, sum(times[half:]) / (len(times) - half)
226
+ if m2 <= 0:
227
+ return None
228
+ return abs(m1 - m2) / m2 * 100
229
+
230
+
231
+ def split_rows_by_kernel(rows: list[dict]) -> dict[str, list[dict]]:
232
+ """按 Op Name 列分组(多 kernel 混采防护,REVIEW2 G15)。
233
+
234
+ - 无 Op Name 列(逐核明细 CSV 形态,本仓 fixtures / PipeUtilization 常态)
235
+ → 单组 "<all>",保持向后兼容;
236
+ - 有 Op Name 列(msprof op_summary 全量导出形态)→ 按 kernel 名分组,
237
+ 调用方必须指定 --kernel-name,否则全行 mean/max 是跨 kernel 平均——
238
+ 数学上无意义且静默产出错误判定。
239
+ """
240
+ if not rows or "Op Name" not in rows[0]:
241
+ return {"<all>": rows}
242
+ groups: dict[str, list[dict]] = {}
243
+ for r in rows:
244
+ groups.setdefault(str(r.get("Op Name") or "").strip() or "<empty>", []).append(
245
+ r
246
+ )
247
+ return groups
248
+
249
+
250
+ # --------------------------------------------------------------------------
251
+ # 条件求值
252
+ # --------------------------------------------------------------------------
253
+
254
+
255
+ def get_metric_value(facts: dict, metrics_spec: dict, name: str, agg: str):
256
+ """取指标值。pipe 类返回 {mean,max} 聚合桶;scalar/derived 返回 {value 或 mean}。"""
257
+ if name not in facts:
258
+ return None
259
+ entry = facts[name]
260
+ if agg == "value":
261
+ if "value" in entry:
262
+ return entry["value"]
263
+ return entry.get("mean")
264
+ if agg == "mean":
265
+ return entry.get("mean", entry.get("value"))
266
+ if agg == "max":
267
+ return entry.get("max", entry.get("value"))
268
+ if agg == "rank":
269
+ # 该 pipe 在 group=pipe 集合中是否占比最大 → 1 最大
270
+ max_name = facts.get("__max_pipe_name", {}).get("mean_name")
271
+ return 1.0 if name == max_name else (0.0 if max_name else None)
272
+ return entry.get("mean")
273
+
274
+
275
+ def eval_leaf(
276
+ cond: dict, facts: dict, metrics_spec: dict, scalars: dict
277
+ ) -> bool | None:
278
+ """叶子条件:{metric, agg?, op, value}。返回 True/False/None(证据不足 → None,不可判)。"""
279
+ name = cond["metric"]
280
+ agg = cond.get("agg", "mean")
281
+ if name in metrics_spec and metrics_spec[name].get("source") == "scalar":
282
+ value = scalars.get(name)
283
+ else:
284
+ value = get_metric_value(facts, metrics_spec, name, agg)
285
+ # 复合值:{metric: X, factor: k} → 另一指标 × 系数(如 time_max > time_mean×1.25)
286
+ target = cond["value"]
287
+ if isinstance(target, dict):
288
+ t_name, t_factor = target["metric"], target.get("factor", 1.0)
289
+ if t_name in metrics_spec and metrics_spec[t_name].get("source") == "scalar":
290
+ t_val = scalars.get(t_name)
291
+ else:
292
+ t_val = get_metric_value(facts, metrics_spec, t_name, agg)
293
+ if t_val is None or value is None:
294
+ return None
295
+ target = t_val * t_factor
296
+ if value is None or target is None:
297
+ return None
298
+ op = cond["op"]
299
+ if op == "gt":
300
+ return value > target
301
+ if op == "gte":
302
+ return value >= target
303
+ if op == "lt":
304
+ return value < target
305
+ if op == "lte":
306
+ return value <= target
307
+ if op == "eq":
308
+ return value == target
309
+ if op == "between":
310
+ lo, hi = target if isinstance(target, (list, tuple)) else (target, target)
311
+ return lo <= value <= hi
312
+ if op == "gt_ratio":
313
+ # value 与 target 本身已是比值语义(如 __tail_ratio 直接与 1.25 比较)
314
+ return value > target
315
+ if op == "lt_ratio":
316
+ return value < target
317
+ raise ValueError(f"unknown op: {op}")
318
+
319
+
320
+ def eval_cond(cond, facts: dict, metrics_spec: dict, scalars: dict):
321
+ """递归求值 all/any/叶子。三值逻辑:None=证据不足。
322
+ all: 任一 False → False;有 None 无 False → None;否则 True。
323
+ any: 任一 True → True;有 None 无 True → None;否则 False。"""
324
+ if isinstance(cond, list):
325
+ cond = {"all": cond}
326
+ if "all" in cond:
327
+ saw_none = False
328
+ for sub in cond["all"]:
329
+ r = eval_cond(sub, facts, metrics_spec, scalars)
330
+ if r is False:
331
+ return False
332
+ if r is None:
333
+ saw_none = True
334
+ return None if saw_none else True
335
+ if "any" in cond:
336
+ saw_none = False
337
+ for sub in cond["any"]:
338
+ r = eval_cond(sub, facts, metrics_spec, scalars)
339
+ if r is True:
340
+ return True
341
+ if r is None:
342
+ saw_none = True
343
+ return None if saw_none else False
344
+ return eval_leaf(cond, facts, metrics_spec, scalars)
345
+
346
+
347
+ # --------------------------------------------------------------------------
348
+ # 规则求值
349
+ # --------------------------------------------------------------------------
350
+
351
+
352
+ def required_metrics_present(rule: dict, facts: dict, scalars: dict) -> list:
353
+ """返回缺失的 REQUIRED 指标(绑定契约:缺失 → SKIPPED,不触发不误报)。"""
354
+ missing = []
355
+ spec = rule.get("_metrics_spec", {})
356
+ for m in rule.get("metrics_required", []):
357
+ src = spec.get(m, {}).get("source", "pipe")
358
+ if src == "scalar":
359
+ if scalars.get(m) is None:
360
+ missing.append(m)
361
+ elif m not in facts or facts[m].get("mean", facts[m].get("value")) is None:
362
+ missing.append(m)
363
+ return missing
364
+
365
+
366
+ def severity_of(rule: dict, facts: dict, metrics_spec: dict, scalars: dict):
367
+ sev = rule.get("severity", {})
368
+ if "default" in sev and "metric" not in sev:
369
+ return sev["default"], ""
370
+ if "metric" in sev:
371
+ v = get_metric_value(facts, metrics_spec, sev["metric"], "mean")
372
+ if v is None:
373
+ v = scalars.get(sev["metric"])
374
+ if v is not None:
375
+ for band in sev.get("bands", []):
376
+ lo, hi = band.get("min"), band.get("max")
377
+ if lo is not None and v < lo:
378
+ continue
379
+ if hi is not None and v > hi:
380
+ continue
381
+ return band.get("level", "light"), band.get("note", "")
382
+ return sev.get("default", "light"), ""
383
+ return "light", ""
384
+
385
+
386
+ def eval_rule(
387
+ rule: dict, facts: dict, metrics_spec: dict, scalars: dict, disabled: set
388
+ ):
389
+ rid = rule["id"]
390
+ if rid in disabled:
391
+ return {
392
+ "rule_id": rid,
393
+ "status": "SKIPPED",
394
+ "reason": f"disabled by routing rule ({rid} 被上层路由禁用)",
395
+ }
396
+
397
+ missing = required_metrics_present(rule, facts, scalars)
398
+ if missing:
399
+ return {
400
+ "rule_id": rid,
401
+ "status": "SKIPPED",
402
+ "reason": f"REQUIRED 指标缺失: {', '.join(missing)}(绑定契约:未采集不判定)",
403
+ "missing": missing,
404
+ }
405
+
406
+ result = eval_cond(rule["condition"], facts, metrics_spec, scalars)
407
+ if result is None:
408
+ optional_missing = [
409
+ m
410
+ for m in rule.get("metrics_optional", [])
411
+ if m not in facts and scalars.get(m) is None
412
+ ]
413
+ return {
414
+ "rule_id": rid,
415
+ "status": "INSUFFICIENT_DATA",
416
+ "reason": "条件求值证据不足(存在 None 分支)",
417
+ "missing_optional": optional_missing,
418
+ }
419
+ if not result:
420
+ finding = {"rule_id": rid, "status": "NOT_HIT"}
421
+ w = rule.get("watch")
422
+ if w:
423
+ wv = eval_cond(w["condition"], facts, metrics_spec, scalars)
424
+ if wv is True:
425
+ finding["watch"] = w["note"]
426
+ return finding
427
+
428
+ # ---- 命中:组装 finding(判定书条目契约)----
429
+ sev, sev_note = severity_of(rule, facts, metrics_spec, scalars)
430
+ finding = {
431
+ "rule_id": rid,
432
+ "status": "HIT",
433
+ "name": rule.get("name", rid),
434
+ "layer": rule.get("_layer", ""),
435
+ "severity": sev,
436
+ "confidence": rule.get("confidence", "medium"),
437
+ "threshold_source": rule.get("threshold_source", ""),
438
+ "evidence": _evidence_text(rule, facts, metrics_spec, scalars),
439
+ "counter_evidence": _counter_evidence(rule, facts, metrics_spec, scalars),
440
+ "speedup_range": rule.get("speedup_range"),
441
+ "speedup_source": rule.get("speedup_source", ""),
442
+ "mechanism_refs": rule.get("mechanism_refs", []),
443
+ }
444
+ if sev_note:
445
+ finding["severity_note"] = sev_note
446
+ # 路由副作用:命中本规则可禁用其它规则 / 改道机制路由
447
+ routing = rule.get("routing")
448
+ if routing:
449
+ finding["routing"] = routing
450
+ return finding
451
+
452
+
453
+ def _evidence_text(rule, facts, metrics_spec, scalars) -> str:
454
+ parts = []
455
+ for m in rule.get("metrics_required", []):
456
+ v = get_metric_value(facts, metrics_spec, m, "mean")
457
+ if v is None:
458
+ v = scalars.get(m)
459
+ if v is not None:
460
+ parts.append(f"{m}={v:.1f}")
461
+ return ";".join(parts) if parts else "(derived condition)"
462
+
463
+
464
+ def _counter_evidence(rule, facts, metrics_spec, scalars) -> list:
465
+ """反证条目:条件为真时以 annotate 记录(供 LLM 解释与人工复核),
466
+ 不推翻判定 —— 判定只出自规则条件,反证是质量的第二道闸。"""
467
+ out = []
468
+ for ce in rule.get("counter_evidence", []):
469
+ r = eval_cond(
470
+ {
471
+ "metric": ce["condition"]["metric"],
472
+ "agg": ce["condition"].get("agg", "value"),
473
+ "op": ce["condition"]["op"],
474
+ "value": ce["condition"]["value"],
475
+ },
476
+ facts,
477
+ metrics_spec,
478
+ scalars,
479
+ )
480
+ if r is True:
481
+ out.append({"id": ce["id"], "note": ce["note"]})
482
+ elif r is None:
483
+ out.append(
484
+ {
485
+ "id": ce["id"],
486
+ "note": ce["note"] + "(反证指标缺失,未评估)",
487
+ "unevaluated": True,
488
+ }
489
+ )
490
+ return out
491
+
492
+
493
+ # --------------------------------------------------------------------------
494
+ # 规则包加载校验(schema 纪律机制化:违规启动即报错,检视 §7a/F5/F16)
495
+ # --------------------------------------------------------------------------
496
+
497
+ # 引擎内置 derived 指标白名单(不声明在词汇表也可被规则引用)
498
+ DERIVED_WHITELIST = {
499
+ "__max_pipe_mean",
500
+ "__max_pipe_name",
501
+ "__pipe_sum_mean",
502
+ "__tail_ratio",
503
+ "__imbalance_pct",
504
+ "__two_cluster_gap_pct",
505
+ "r_value",
506
+ "__mte2_pollution_gap",
507
+ "__mte2_pure_ratio_pct",
508
+ "row_count",
509
+ "time_mean_us",
510
+ "time_max_us",
511
+ "time_min_us",
512
+ "dominant_time_us",
513
+ "__occupancy_pct",
514
+ "__head_overhead_pct",
515
+ "__host_dispatch_pct",
516
+ }
517
+
518
+
519
+ def _leaves_of(cond) -> list:
520
+ """递归展开条件树的全部叶子(含 watch / counter_evidence 由调用方分别传入)。"""
521
+ out = []
522
+ if cond is None:
523
+ return out
524
+ if isinstance(cond, list):
525
+ for sub in cond:
526
+ out.extend(_leaves_of(sub))
527
+ return out
528
+ if isinstance(cond, dict):
529
+ if "metric" in cond and "op" in cond:
530
+ out.append(cond)
531
+ for key in ("all", "any"):
532
+ if key in cond:
533
+ out.extend(_leaves_of(cond[key]))
534
+ return out
535
+
536
+
537
+ def _leaf_metrics(cond) -> set:
538
+ """条件树引用的全部指标名(含复合 value 引用的 metric)。"""
539
+ names = set()
540
+ for leaf in _leaves_of(cond):
541
+ names.add(leaf["metric"])
542
+ if isinstance(leaf.get("value"), dict) and "metric" in leaf["value"]:
543
+ names.add(leaf["value"]["metric"])
544
+ return names
545
+
546
+
547
+ def validate_rules_pack(
548
+ pack: dict, rules_path: str, all_ids: set | None = None
549
+ ) -> list[str]:
550
+ """加载期校验,返回错误清单(非空 → 调用方报错退出)。
551
+
552
+ 校验项(rule-schema.md 已承诺而未实现的三条 + optional 语义强制):
553
+ V1 引用完整性:condition / watch / counter_evidence / severity 引用的指标
554
+ 必须在词汇表(metrics)或 derived 白名单内——未声明引用 = 配置错误;
555
+ V2 路由目标存在:routing.on_hit 的 disable_rule 目标必须存在于已知规则集
556
+ (all_ids 提供时按跨包全集校验;否则按本包 id 集合校验);
557
+ V3 同指标区间无重叠:同一指标的多条规则构成档位类判定(如 L3 R 值五档)
558
+ 时,区间不得重叠(消除「同一值双命中」类矛盾,F3 的机制化护栏);
559
+ V4 optional 语义:metrics_optional 中的指标禁止出现在 condition 中
560
+ (引擎对 condition 内 None 一律 INSUFFICIENT_DATA,不存在「可选指标
561
+ 缺失时跳过叶子」的降级求值——optional 仅用于反证,rule-schema.md 明确)。
562
+ """
563
+ errs = []
564
+ metrics = pack.get("metrics", {})
565
+ rules = pack.get("rules", [])
566
+ known = set(metrics) | DERIVED_WHITELIST
567
+ ids = all_ids if all_ids is not None else {r.get("id") for r in rules}
568
+
569
+ for rule in rules:
570
+ rid = rule.get("id", "<no-id>")
571
+ # V1 引用完整性
572
+ refs = _leaf_metrics(rule.get("condition"))
573
+ refs |= _leaf_metrics(
574
+ rule.get("watch", {}).get("condition") if rule.get("watch") else None
575
+ )
576
+ for ce in rule.get("counter_evidence", []):
577
+ refs |= _leaf_metrics(ce.get("condition"))
578
+ if isinstance(rule.get("severity"), dict) and "metric" in rule["severity"]:
579
+ refs.add(rule["severity"]["metric"])
580
+ undeclared = refs - known
581
+ if undeclared:
582
+ errs.append(
583
+ f"[V1] {rid}: 引用了未声明指标 {sorted(undeclared)}"
584
+ f"(词汇表 + derived 白名单均无;rule-schema.md:未声明引用=配置错误)"
585
+ )
586
+ # V4 optional 不进 condition
587
+ opt = set(rule.get("metrics_optional", []))
588
+ cond_refs = _leaf_metrics(rule.get("condition"))
589
+ bad_opt = opt & cond_refs
590
+ if bad_opt:
591
+ errs.append(
592
+ f"[V4] {rid}: optional 指标 {sorted(bad_opt)} 出现在 condition 中"
593
+ f"(optional 仅用于反证;condition 引用必须 required,缺失即 INSUFFICIENT_DATA)"
594
+ )
595
+ # V1b REQUIRED 引用同样校验(防拼写错误)
596
+ req_undeclared = set(rule.get("metrics_required", [])) - known
597
+ if req_undeclared:
598
+ errs.append(
599
+ f"[V1] {rid}: metrics_required 含未声明指标 {sorted(req_undeclared)}"
600
+ )
601
+ # V2 路由目标存在。on_hit 支持两种形态(rule-schema.md):
602
+ # 位置式列表 [disable_rule, <rule_id>](首元素为动作名,第二元素为目标)
603
+ # 映射式 dict {disable_rule: <rule_id>} 或字符串列表(每元素一个目标)
604
+ routing = rule.get("routing") or {}
605
+ on_hit = routing.get("on_hit", [])
606
+ targets = []
607
+ if isinstance(on_hit, list) and on_hit and on_hit[0] == "disable_rule":
608
+ targets = on_hit[1:] # 位置式:跳过动作名
609
+ elif isinstance(on_hit, list):
610
+ for act in on_hit:
611
+ if isinstance(act, dict) and "disable_rule" in act:
612
+ targets.append(act["disable_rule"])
613
+ elif isinstance(act, str):
614
+ targets.append(act)
615
+ for target in targets:
616
+ if target not in ids:
617
+ errs.append(
618
+ f"[V2] {rid}: routing.on_hit 禁用了不存在的规则 {target}(悬空路由)"
619
+ )
620
+
621
+ # V3 同指标区间无重叠(档位类判定)
622
+ per_metric: dict[str, list[tuple]] = {}
623
+ for rule in rules:
624
+ cond = rule.get("condition")
625
+ leaves = _leaves_of(cond)
626
+ if not leaves:
627
+ continue
628
+ # 仅当规则的全部叶子都引用同一指标且为区间形状(带数值边界的
629
+ # gt/gte/lt/lte/between)才视为该指标的档位类规则
630
+ metrics_in_cond = {leaf["metric"] for leaf in leaves}
631
+ if len(metrics_in_cond) != 1:
632
+ continue
633
+ m = leaves[0]["metric"]
634
+ lo = hi = None
635
+ shape_ok = True
636
+ for leaf in leaves:
637
+ op, val = leaf["op"], leaf["value"]
638
+ if op == "between" and isinstance(val, list) and len(val) == 2:
639
+ lo, hi = float(val[0]), float(val[1])
640
+ elif isinstance(val, (int, float)):
641
+ if op in ("gt", "gte") and lo is None:
642
+ lo = float(val)
643
+ elif op in ("lt", "lte") and hi is None:
644
+ hi = float(val)
645
+ else:
646
+ shape_ok = False
647
+ break
648
+ else:
649
+ shape_ok = False
650
+ break
651
+ if shape_ok and (lo is not None or hi is not None):
652
+ per_metric.setdefault(m, []).append((lo, hi, rule.get("id")))
653
+ for m, bands in per_metric.items():
654
+ bands.sort(key=lambda b: b[0] if b[0] is not None else float("-inf"))
655
+ for i in range(len(bands) - 1):
656
+ lo1, hi1, id1 = bands[i]
657
+ lo2, hi2, id2 = bands[i + 1]
658
+ if hi1 is not None and lo2 is not None and hi1 > lo2:
659
+ errs.append(
660
+ f"[V3] 指标 {m} 档位区间重叠:{id1}({lo1},{hi1}) 与 "
661
+ f"{id2}({lo2},{hi2})(同一值可双命中,机制方向可能矛盾)"
662
+ )
663
+ return errs
664
+
665
+
666
+ # --------------------------------------------------------------------------
667
+ # 主流程
668
+ # --------------------------------------------------------------------------
669
+
670
+
671
+ def load_rules(path: str) -> dict:
672
+ import yaml
673
+
674
+ with open(path, encoding="utf-8") as f:
675
+ return yaml.safe_load(f)
676
+
677
+
678
+ def _rule_default_severity(rule: dict) -> str:
679
+ """取规则 default severity(severity 为 {default: X} 或分档 dict)。"""
680
+ sev = rule.get("severity", {})
681
+ if isinstance(sev, dict):
682
+ return sev.get("default", "light")
683
+ return sev or "light"
684
+
685
+
686
+ def near_limit_threshold(all_hits_seeds: list[dict]) -> float | None:
687
+ """「接近上限」触发阈值(数据驱动,检视 F7)。
688
+
689
+ threshold = min(已加载规则中 default severity=critical 且 confidence=high 的
690
+ speedup_range 上界)——不硬编码 1.15,规则集演进自动联动(当前规则集下
691
+ L3_MC2_STRONG_COMPUTE [1.0,1.2] 成为 argmin)。无可参照规则时返回 None
692
+ (该分支退化为不可达,与「无此类规则」的规则集状态一致)。
693
+ """
694
+ bounds = [
695
+ r["speedup_range"][1]
696
+ for r in all_hits_seeds
697
+ if _rule_default_severity(r) == "critical"
698
+ and r.get("confidence") == "high"
699
+ and r.get("speedup_range")
700
+ ]
701
+ return min(bounds) if bounds else None
702
+
703
+
704
+ def headline_and_verdict(
705
+ findings: list, skipped: list, rule_seeds: list | None = None
706
+ ) -> tuple[str, str, list]:
707
+ hits = [f for f in findings if f.get("status") == "HIT"]
708
+ # NOT_HIT = 数据在、条件不成立(证据完整地排除了该规则)
709
+ evaluated = [s for s in skipped if s.get("status") == "NOT_HIT"]
710
+ if not hits:
711
+ if not evaluated:
712
+ # 没有任何规则被完整评估(全部因指标缺失跳过)→ 证据不足,不判 NEAR_LIMIT
713
+ return (
714
+ "证据不足,无法判定:全部规则因 REQUIRED 指标缺失被跳过(绑定契约:未采集不判定)",
715
+ "INSUFFICIENT_DATA",
716
+ ["按 skipped[].missing 提示补采指标后重跑 verdict.py"],
717
+ )
718
+ # 「未命中任何规则」≠「接近上限」:前者是「该换更深的指标」,后者是「该收手」
719
+ # ——拆分为独立 verdict 值 NO_BOUND_FOUND(schema 1.1,检视 F7)
720
+ return (
721
+ "未命中任何瓶颈规则(各 pipe 占比均衡且低于判定线)",
722
+ "NO_BOUND_FOUND",
723
+ ["评估是否已达性能目标;如仍需提升,采集 L0 源码级指标做深度分析"],
724
+ )
725
+ order = {"critical": 0, "moderate": 1, "light": 2}
726
+ conf = {"high": 0, "medium": 1, "low": 2}
727
+ hits.sort(
728
+ key=lambda f: (
729
+ order.get(f.get("severity"), 3),
730
+ conf.get(f.get("confidence"), 3),
731
+ )
732
+ )
733
+ top = hits[0]
734
+ headline = f"{top['name']}({top['severity']})" + (
735
+ f";另有 {len(hits) - 1} 项命中" if len(hits) > 1 else ""
736
+ )
737
+ # 接近上限判定(数据驱动阈值):唯一 critical+high 命中且其 speedup 上界 ≤ 阈值
738
+ # → NEAR_LIMIT 提示(阈值 = 全部 critical+high 规则上界的 min,见 near_limit_threshold)
739
+ threshold = near_limit_threshold(rule_seeds or findings)
740
+ if (
741
+ len(hits) == 1
742
+ and top["severity"] == "critical"
743
+ and top["confidence"] == "high"
744
+ and top.get("speedup_range")
745
+ and threshold is not None
746
+ and top["speedup_range"][1] <= threshold
747
+ ):
748
+ return (
749
+ headline,
750
+ "NEAR_LIMIT",
751
+ [
752
+ f"唯一瓶颈已接近理论上限(预估收益上界 {top['speedup_range'][1]}x,"
753
+ f"阈值 {threshold}x 由规则集 critical+high 上界派生),确认标尺后再投入"
754
+ ],
755
+ )
756
+ return (
757
+ headline,
758
+ "NEEDS_OPTIMIZATION",
759
+ [f"按 speedup_range 排序实施机制:{h['rule_id']}" for h in hits[:3]],
760
+ )
761
+
762
+
763
+ def main() -> int:
764
+ ap = argparse.ArgumentParser(
765
+ description="算子调优判定引擎(分层规则 + 绑定契约 + 反证)"
766
+ )
767
+ ap.add_argument(
768
+ "--rules",
769
+ required=True,
770
+ help="规则包 YAML(可逗号分隔多个,按层排序:L3,L2,L1,L0)",
771
+ )
772
+ ap.add_argument(
773
+ "--csv", default=None, help="PipeUtilization.csv(pipe/时间类指标源)"
774
+ )
775
+ ap.add_argument(
776
+ "--db",
777
+ default=None,
778
+ help="npu-compute summary.db(DB 输入,与 --csv 二选一;"
779
+ "Roadmap #4——列名规范化由 FieldUnitMap 自动还原)",
780
+ )
781
+ ap.add_argument(
782
+ "--db-table",
783
+ default="PipeUtilization",
784
+ help="--db 输入的指标表名(缺省 PipeUtilization)",
785
+ )
786
+ ap.add_argument(
787
+ "--kernel-name",
788
+ default=None,
789
+ help="目标 kernel 名(Op Name 列存在且多 kernel 时必填——G15 混采防护;"
790
+ "缺省且检测到多 kernel 时报错退出,不静默跨 kernel 平均)",
791
+ )
792
+ ap.add_argument(
793
+ "--scalar",
794
+ default="",
795
+ help="标量指标 key=value,key=value(bw 百分比/核数/隔离测试值等)",
796
+ )
797
+ ap.add_argument(
798
+ "--soc", default=None, help="SoC 标识(a2/a3/a5),用于规则包 soc 域过滤"
799
+ )
800
+ ap.add_argument(
801
+ "--min-confidence",
802
+ default="low",
803
+ choices=["low", "medium", "high"],
804
+ help="过滤输出置信度下限(不改变触发,只影响报告呈现)",
805
+ )
806
+ ap.add_argument("-o", "--output", required=True, help="verdict.json 输出路径")
807
+ args = ap.parse_args()
808
+
809
+ scalars = {}
810
+ if args.scalar:
811
+ for kv in args.scalar.split(","):
812
+ if "=" in kv:
813
+ k, v = kv.split("=", 1)
814
+ try:
815
+ scalars[k.strip()] = float(v)
816
+ except ValueError:
817
+ # 非数值标量(脏输入如 N/A):按缺失处理(None 语义 → 相关规则
818
+ # INSUFFICIENT/SKIPPED 降级),不进 facts 参与比较(检视 F18:
819
+ # 字符串进比较会 TypeError 崩溃)
820
+ print(
821
+ f"WARN: 标量 {k.strip()}={v!r} 非数值,按缺失处理(不参与判定)",
822
+ file=sys.stderr,
823
+ )
824
+ scalars[k.strip()] = None
825
+
826
+ if args.csv and args.db:
827
+ print(
828
+ "ERROR: --csv 与 --db 互斥(同一指标源选一个输入形态)",
829
+ file=sys.stderr,
830
+ )
831
+ return 1
832
+ if args.db:
833
+ if not Path(args.db).exists():
834
+ print(f"ERROR: --db 文件不存在: {args.db}", file=sys.stderr)
835
+ return 1
836
+ rows = load_db_rows(args.db, args.db_table)
837
+ print(
838
+ f"[db] {args.db}::{args.db_table}:{len(rows)} 行(DB 通道;列名按"
839
+ f" FieldUnitMap 还原单位后缀)",
840
+ file=sys.stderr,
841
+ )
842
+ else:
843
+ rows = load_csv_rows(args.csv) if args.csv else []
844
+ # ---- G15 kernel 归属校验:多 kernel CSV 混采防护(fail loud,不静默跨 kernel 平均)----
845
+ kernel_groups = split_rows_by_kernel(rows)
846
+ if len(kernel_groups) > 1:
847
+ available = ", ".join(sorted(kernel_groups))
848
+ if not args.kernel_name:
849
+ print(
850
+ f"ERROR: CSV 含 {len(kernel_groups)} 个 kernel(Op Name 多值:{available})。"
851
+ "全行 mean/max 是跨 kernel 平均,判定无意义——请用 --kernel-name 指定目标 kernel"
852
+ "(多 kernel 程序逐 kernel 判定),或改喂单 kernel 导出的 CSV。",
853
+ file=sys.stderr,
854
+ )
855
+ return 4
856
+ if args.kernel_name not in kernel_groups:
857
+ print(
858
+ f"ERROR: --kernel-name {args.kernel_name!r} 不在 CSV Op Name 集合中"
859
+ f"(实际:{available})。检查 kernel 名拼写或去掉过滤重采。",
860
+ file=sys.stderr,
861
+ )
862
+ return 4
863
+ rows = kernel_groups[args.kernel_name]
864
+ print(
865
+ f"[kernel] 按 Op Name={args.kernel_name} 过滤:{len(rows)} 行"
866
+ f"(共 {sum(len(v) for v in kernel_groups.values())} 行)",
867
+ file=sys.stderr,
868
+ )
869
+ all_findings, all_skipped = [], []
870
+ disabled: set = set()
871
+ facts_by_pack: dict = {} # pack_stem → facts(审计分层快照,F4)
872
+ rules_files = [p.strip() for p in args.rules.split(",") if p.strip()]
873
+ soc = args.soc
874
+
875
+ # ---- 加载校验(schema 纪律机制化:V1 引用完整性 / V2 悬空路由 / V3 区间重叠 / V4 optional)----
876
+ packs = [] # (path, pack) 供下方求值循环复用
877
+ all_rule_ids: set = set()
878
+ all_rule_seeds: list = [] # 规则种子(severity/confidence/speedup_range 供阈值派生)
879
+ for rf in rules_files:
880
+ pack = load_rules(rf)
881
+ packs.append((rf, pack))
882
+ all_rule_ids |= {r.get("id") for r in pack.get("rules", [])}
883
+ all_rule_seeds.extend(pack.get("rules", []))
884
+ # routing.on_hit 的 disable_rule 语义是跨包的(rule-schema.md):目标可能在未加载的
885
+ # 规则包中。校验全集 = 本次加载的包 + 同目录其余规则包(规则宇宙),防止单包运行时
886
+ # 误报「悬空」。
887
+ rule_universe = set(all_rule_ids)
888
+ for extra in Path(rules_files[0]).parent.glob("*.yaml"):
889
+ if str(extra) not in rules_files:
890
+ try:
891
+ extra_pack = load_rules(str(extra))
892
+ rule_universe |= {r.get("id") for r in extra_pack.get("rules", [])}
893
+ except Exception:
894
+ pass # 解析失败的旁路包不阻断本次校验(它自身被加载时才校验)
895
+ load_errors = []
896
+ for rf, pack in packs:
897
+ errs = validate_rules_pack(pack, rf, all_ids=rule_universe)
898
+ load_errors.extend(f"{Path(rf).name}: {e}" for e in errs)
899
+ if load_errors:
900
+ print("ERROR: 规则包加载校验失败(配置错误,拒绝求值):", file=sys.stderr)
901
+ for e in load_errors:
902
+ print(f" {e}", file=sys.stderr)
903
+ return 3
904
+
905
+ for rf, pack in packs:
906
+ if soc and pack.get("soc") and soc not in pack["soc"]:
907
+ all_skipped.append(
908
+ {
909
+ "rule_id": f"pack:{Path(rf).stem}",
910
+ "status": "SKIPPED",
911
+ "reason": f"规则包 soc 域 {pack['soc']} 不含 {soc}",
912
+ }
913
+ )
914
+ continue
915
+ metrics_spec = pack.get("metrics", {})
916
+ facts = {}
917
+ if rows:
918
+ facts.update(pipe_metrics_from_rows(rows, metrics_spec))
919
+ facts.update(time_metrics_from_rows(rows))
920
+ # 规则包循环内按包名累积 facts(F4:全瀑布跑法下循环外只剩末包指标,
921
+ # L1 pipe 指标丢失,审计承诺失效;分层结构天然保留指标来源信息)
922
+ facts_by_pack[Path(rf).stem] = facts
923
+ # MC² 隔离实验标量 → 衍生指标(r_value 等)
924
+ if (
925
+ "t_comm_us" in scalars
926
+ and "t_compute_us" in scalars
927
+ and scalars.get("t_comm_us")
928
+ and scalars.get("t_compute_us")
929
+ ):
930
+ facts["r_value"] = {"value": scalars["t_compute_us"] / scalars["t_comm_us"]}
931
+ if "mte2_ratio_full" in scalars and "mte2_ratio_pure" in scalars:
932
+ full, pure = scalars["mte2_ratio_full"], scalars["mte2_ratio_pure"]
933
+ if full is not None and pure is not None:
934
+ facts["__mte2_pollution_gap"] = {"value": full - pure}
935
+ if full:
936
+ facts["__mte2_pure_ratio_pct"] = {"value": pure / full * 100}
937
+ # scalar 声明也并入 facts(供 get_metric_value 统一读取);非数值(None)不并入
938
+ for name, m in metrics_spec.items():
939
+ if (
940
+ m.get("source") == "scalar"
941
+ and name in scalars
942
+ and scalars[name] is not None
943
+ ):
944
+ facts[name] = {"value": scalars[name]}
945
+ # L2 derived:occupancy 与头开销(依赖 scalar 提供的 physical_cores / task_duration_us)
946
+ # — 三个 derived 此前从未计算:L2_HEAD_OVERHEAD 永 SKIPPED、L2_UNDERUTILIZATION
947
+ # 的 critical 档与头开销 critical 档是死代码(检视 F6)
948
+ if "row_count" in facts and scalars.get("physical_cores"):
949
+ facts["__occupancy_pct"] = {
950
+ "value": facts["row_count"]["value"] / scalars["physical_cores"] * 100
951
+ }
952
+ if (
953
+ scalars.get("task_duration_us")
954
+ and facts.get("time_max_us", {}).get("value") is not None
955
+ ):
956
+ # 木桶口径:头开销以最慢核为基准(Task 总时长 − 最慢核主体时间)
957
+ td, tmax = scalars["task_duration_us"], facts["time_max_us"]["value"]
958
+ if td > 0:
959
+ facts["__head_overhead_pct"] = {"value": (td - tmax) / td * 100}
960
+ # L2 derived:host 下发占比(P2-4/G17 scene=cluster 档;host_dispatch_us
961
+ # 暂无生产者——真实集群数据到位前本规则维持 SKIPPED,绑定契约兜底)
962
+ if scalars.get("host_dispatch_us") and scalars.get("task_duration_us"):
963
+ td2 = scalars["task_duration_us"]
964
+ if td2 > 0:
965
+ facts["__host_dispatch_pct"] = {
966
+ "value": scalars["host_dispatch_us"] / td2 * 100
967
+ }
968
+
969
+ rules = sorted(pack.get("rules", []), key=lambda r: r.get("priority", 999))
970
+ for rule in rules:
971
+ rule["_metrics_spec"] = metrics_spec
972
+ rule["_layer"] = pack.get("layer", "")
973
+ res = eval_rule(rule, facts, metrics_spec, scalars, disabled)
974
+ if res.get("status") == "HIT":
975
+ all_findings.append(res)
976
+ # 路由副作用生效:禁用后续规则(跨包也生效,如 L3 污染禁用 L1_MTE2_BOUND)
977
+ for act in (res.get("routing", {}) or {}).get("on_hit", []):
978
+ if isinstance(act, dict) and act.get("disable_rule"):
979
+ disabled.add(act["disable_rule"])
980
+ elif isinstance(act, str):
981
+ disabled.add(act)
982
+ elif res.get("status") in ("SKIPPED", "INSUFFICIENT_DATA"):
983
+ all_skipped.append(res)
984
+ elif res.get("status") == "NOT_HIT":
985
+ all_skipped.append(res)
986
+
987
+ headline, verdict, next_steps = headline_and_verdict(
988
+ all_findings, all_skipped, rule_seeds=all_rule_seeds
989
+ )
990
+
991
+ # 事实快照(审计:判定书可回溯到指标值与来源)——按规则包分层输出(F4):
992
+ # 全瀑布 --rules L3,L2,L1,L0 时保留每包的完整指标事实(含 L1 pipe 指标),
993
+ # 不再只剩最后一个包的 facts;scalars 字段维持现状(不依赖包)
994
+ def _snap(f: dict) -> dict:
995
+ return {
996
+ k: {
997
+ kk: (round(vv, 2) if isinstance(vv, float) else vv)
998
+ for kk, vv in v.items()
999
+ }
1000
+ for k, v in f.items()
1001
+ }
1002
+
1003
+ facts_snapshot = (
1004
+ {pack_stem: _snap(f) for pack_stem, f in facts_by_pack.items()}
1005
+ if (rows or any(v is not None and v for v in scalars.values()))
1006
+ else {}
1007
+ )
1008
+
1009
+ # watch 观察档(检视 F29:R2 风险对策的「watch 档灰度输出」要求可见,
1010
+ # 此前只在 verdict.json 内部、终端与 recommended_next 均不呈现)
1011
+ watches = [
1012
+ (s["rule_id"], s["watch"])
1013
+ for s in all_skipped
1014
+ if s.get("status") == "NOT_HIT" and s.get("watch")
1015
+ ]
1016
+ if watches:
1017
+ # watch 档进 recommended_next(判定书与终端同源)
1018
+ next_steps.append(
1019
+ f"watch 档灰度输出:{', '.join(rid for rid, _ in watches)}"
1020
+ f"(指标已具备优化价值但未达判定线,可低成本先行)"
1021
+ )
1022
+
1023
+ out = {
1024
+ "schema": SCHEMA_VERDICT,
1025
+ "generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
1026
+ "soc": soc,
1027
+ "rules_files": rules_files,
1028
+ "csv": args.csv,
1029
+ "db": args.db,
1030
+ "db_table": args.db_table if args.db else None,
1031
+ "kernel_name": args.kernel_name,
1032
+ "rows": len(rows),
1033
+ "kernels_in_csv": sorted(k for k in kernel_groups if k != "<all>")
1034
+ if len(kernel_groups) > 1
1035
+ else None,
1036
+ "scalars": {k: v for k, v in scalars.items()},
1037
+ "facts": facts_snapshot,
1038
+ "findings": all_findings,
1039
+ "skipped": all_skipped,
1040
+ "verdict": verdict,
1041
+ "headline": headline,
1042
+ "recommended_next": next_steps,
1043
+ }
1044
+ out_path = Path(args.output)
1045
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1046
+ out_path.write_text(
1047
+ json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
1048
+ )
1049
+
1050
+ # 人读摘要(Agent/终端共用)
1051
+ print(f"verdict: {verdict} | {headline}")
1052
+ for f in all_findings:
1053
+ if f.get("status") == "HIT":
1054
+ print(
1055
+ f" HIT [{f['severity']:>8}/{f['confidence']:<6}] {f['rule_id']}: {f['name']} "
1056
+ f"(est {f.get('speedup_range')}) evidence: {f['evidence']}"
1057
+ )
1058
+ for ce in f.get("counter_evidence", []):
1059
+ print(f" ⚠ 反证[{ce['id']}]: {ce['note']}")
1060
+ if watches:
1061
+ print(f" watch 档(未达判定线,观察项 {len(watches)}):")
1062
+ for rid, note in watches:
1063
+ print(f" 👁 watch [{rid}]: {note}")
1064
+ n_skip = len([s for s in all_skipped if s.get("status") == "SKIPPED"])
1065
+ n_ins = len([s for s in all_skipped if s.get("status") == "INSUFFICIENT_DATA"])
1066
+ print(f" skipped={n_skip} insufficient_data={n_ins}")
1067
+ print(f" → {out_path}")
1068
+ return 0
1069
+
1070
+
1071
+ if __name__ == "__main__":
1072
+ sys.exit(main())