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,444 @@
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
+ """batch_pipeline.py — 全量 case 批量快照与对比(检视 F11)
13
+
14
+ 把 A1 全量 case 铁律(>20 取前 20 逐 case 覆盖)从「主 agent 手工串 20×N
15
+ 次命令再手拼表」变成一条命令:输入 cases.csv + 逐 case 性能数据 +
16
+ 基线快照,自动逐 case 建 snapshot → 跑 delta_compare → 输出
17
+ comparison_matrix.md(逐 case 表 + geomean + 最差/最好 case + 精度列)。
18
+
19
+ 输入形态(性能数据 CSV,一行一 case×variant):
20
+ case_id,variant,kernel_avg_us,precision[,kernel_p50_us,kernel_p95_us,kernel_max_us]
21
+ c01,baseline,100.0,pass
22
+ c01,round-01-ub-fusion,92.0,pass
23
+
24
+ 用法:
25
+ python3 batch_pipeline.py \
26
+ --cases cases.csv \
27
+ --perf perf_data.csv \
28
+ --baseline-variant baseline \
29
+ --after-variants round-01-ub-fusion,round-01-mte2-preload \
30
+ --scope tuning_artifacts/gelu/scope.json \
31
+ --output-dir tuning_artifacts/gelu/matrix
32
+
33
+ # 直连模式(P0-1):collect_cases.py 产物目录直接进来,无需手拼 CSV
34
+ python3 batch_pipeline.py \
35
+ --from-collect ./collect_out \
36
+ --cases cases.csv \
37
+ --baseline-variant system \
38
+ --after-variants custom \
39
+ --scope tuning_artifacts/gelu/scope.json \
40
+ --output-dir tuning_artifacts/gelu/matrix
41
+ # --from-collect 读取 <dir>/perf_data.csv(collect_cases.py 产出);
42
+ # baseline-variant=system + after-variants=custom 即「vs 标杆」读数。
43
+
44
+ 依赖 scope.json(opstune.scope/1.0)提供 12 HARD 口径字段;逐 case 的
45
+ shape/dtype 从 cases.csv 透传进 snapshot(shape 是 HARD 字段,逐 case 值不同)。
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import argparse
51
+ import csv
52
+ import json
53
+ import math
54
+ import subprocess
55
+ import sys
56
+ from pathlib import Path
57
+
58
+ HERE = Path(__file__).resolve().parent
59
+ SNAPSHOT = HERE / "snapshot.py"
60
+ DELTA_COMPARE = HERE / "delta_compare.py"
61
+
62
+ HARD_KEYS = (
63
+ "op",
64
+ "soc",
65
+ "mode",
66
+ "timing_method",
67
+ "baseline_kind",
68
+ "cann_version",
69
+ "device_id",
70
+ "shape",
71
+ "dtype",
72
+ "format",
73
+ "warmup",
74
+ "repeat",
75
+ )
76
+
77
+
78
+ def load_cases(path: str) -> list[dict]:
79
+ with open(path, encoding="utf-8-sig") as f:
80
+ rows = list(csv.DictReader(f))
81
+ for r in rows:
82
+ if not r.get("case_id") or not r.get("shape") or not r.get("dtype"):
83
+ raise SystemExit(f"ERROR: cases.csv 行缺必需列(case_id/shape/dtype): {r}")
84
+ return rows
85
+
86
+
87
+ def load_perf(path: str) -> dict[tuple[str, str], dict]:
88
+ """{(case_id, variant): {kernel_avg_us, precision, 分布字段...}}
89
+
90
+ kernel_avg_us 为空(collect 侧采集失败/无数据)的行跳过并 WARN——
91
+ 直连模式下 collect_summary 已有状态,不在此静默编造。"""
92
+ out = {}
93
+ with open(path, encoding="utf-8-sig") as f:
94
+ for row in csv.DictReader(f):
95
+ key = (row["case_id"], row["variant"])
96
+ if not str(row.get("kernel_avg_us", "")).strip():
97
+ print(f"WARN: perf 行无 kernel_avg_us,跳过: {key}", file=sys.stderr)
98
+ continue
99
+ out[key] = {
100
+ "kernel_avg_us": float(row["kernel_avg_us"]),
101
+ "precision": row.get("precision", "pass"),
102
+ "kernel_p50_us": float(row["kernel_p50_us"])
103
+ if row.get("kernel_p50_us")
104
+ else None,
105
+ "kernel_p95_us": float(row["kernel_p95_us"])
106
+ if row.get("kernel_p95_us")
107
+ else None,
108
+ "kernel_max_us": float(row["kernel_max_us"])
109
+ if row.get("kernel_max_us")
110
+ else None,
111
+ }
112
+ return out
113
+
114
+
115
+ def run_snapshot(
116
+ scope: dict, case: dict, variant: str, perf: dict, out_path: Path
117
+ ) -> None:
118
+ cmd = [
119
+ sys.executable,
120
+ str(SNAPSHOT),
121
+ "--op",
122
+ scope["op"],
123
+ "--variant",
124
+ variant,
125
+ "--mode",
126
+ scope["mode"],
127
+ "--soc",
128
+ scope["soc"],
129
+ "--precision",
130
+ perf["precision"],
131
+ "--kernel-avg-us",
132
+ str(perf["kernel_avg_us"]),
133
+ "--timing-method",
134
+ scope["timing_method"],
135
+ "--baseline-kind",
136
+ scope["baseline_kind"],
137
+ "--cann-version",
138
+ scope["cann_version"],
139
+ "--device-id",
140
+ scope["device_id"],
141
+ "--shape",
142
+ case["shape"],
143
+ "--dtype",
144
+ case["dtype"],
145
+ "--format",
146
+ scope["format"],
147
+ "--warmup",
148
+ str(scope["warmup"]),
149
+ "--repeat",
150
+ str(scope["repeat"]),
151
+ "--no-env-probe",
152
+ "-o",
153
+ str(out_path),
154
+ ]
155
+ for opt, key in (
156
+ ("--kernel-p50-us", "kernel_p50_us"),
157
+ ("--kernel-p95-us", "kernel_p95_us"),
158
+ ("--kernel-max-us", "kernel_max_us"),
159
+ ):
160
+ if perf.get(key) is not None:
161
+ cmd += [opt, str(perf[key])]
162
+ proc = subprocess.run(cmd, capture_output=True, text=True)
163
+ if proc.returncode != 0:
164
+ raise SystemExit(f"ERROR: snapshot 失败 {out_path}: {proc.stderr[:300]}")
165
+
166
+
167
+ def run_compare(base: Path, after: Path, out_md: Path) -> tuple[int, str]:
168
+ proc = subprocess.run(
169
+ [
170
+ sys.executable,
171
+ str(DELTA_COMPARE),
172
+ "--baseline",
173
+ str(base),
174
+ "--after",
175
+ str(after),
176
+ "--output",
177
+ str(out_md),
178
+ ],
179
+ capture_output=True,
180
+ text=True,
181
+ )
182
+ return proc.returncode, proc.stdout
183
+
184
+
185
+ def parse_conclusion(md_text: str) -> tuple[str, float | None]:
186
+ """从 comparison.md 提取三段结论类型与 speedup。"""
187
+ speedup = None
188
+ for line in md_text.splitlines():
189
+ # 行形态:| speedup | — | 1.200 |(第三列为值)
190
+ if line.startswith("| speedup |"):
191
+ cells = [c.strip() for c in line.split("|")]
192
+ try:
193
+ speedup = float(cells[3])
194
+ except (ValueError, IndexError):
195
+ pass
196
+ if "提升" in md_text:
197
+ return "KEEP", speedup
198
+ if "劣化" in md_text:
199
+ return "REVERT", speedup
200
+ if "噪声带内" in md_text:
201
+ return "NOISE", speedup
202
+ return "UNRESOLVED", speedup
203
+
204
+
205
+ def main() -> int:
206
+ ap = argparse.ArgumentParser(
207
+ description="全量 case 批量快照与对比(A1 铁律工具化)"
208
+ )
209
+ ap.add_argument(
210
+ "--cases", required=True, help="cases.csv(case_id/shape/dtype/attrs)"
211
+ )
212
+ ap.add_argument(
213
+ "--perf",
214
+ default=None,
215
+ help="性能数据 CSV(case_id/variant/kernel_avg_us/precision[分布字段]);"
216
+ "与 --from-collect 二选一",
217
+ )
218
+ ap.add_argument(
219
+ "--from-collect",
220
+ default=None,
221
+ help="collect_cases.py 产物根目录(P0-1 直连:读 <dir>/perf_data.csv,"
222
+ "消灭手拼 CSV;未完成的 case 行由 collect 侧标注,此处如实跳过)",
223
+ )
224
+ ap.add_argument("--baseline-variant", default="baseline")
225
+ ap.add_argument("--after-variants", required=True, help="逗号分隔的优化变体名")
226
+ ap.add_argument(
227
+ "--scope",
228
+ required=True,
229
+ help="scope.json(opstune.scope/1.0,提供 12 HARD 字段)",
230
+ )
231
+ ap.add_argument("--output-dir", required=True)
232
+ ap.add_argument(
233
+ "--max-cases",
234
+ type=int,
235
+ default=20,
236
+ help="case 截断上限(A1 铁律 >20 取前 20;0 = 不截断)",
237
+ )
238
+ args = ap.parse_args()
239
+
240
+ scope = json.loads(Path(args.scope).read_text(encoding="utf-8"))
241
+ missing = [k for k in HARD_KEYS if k not in scope]
242
+ if missing:
243
+ print(f"ERROR: scope.json 缺 HARD 字段 {missing}", file=sys.stderr)
244
+ return 1
245
+
246
+ cases = load_cases(args.cases)
247
+ truncated = False
248
+ if args.max_cases and len(cases) > args.max_cases:
249
+ cases = cases[: args.max_cases]
250
+ truncated = True
251
+ print(
252
+ f"WARN: cases {len(load_cases(args.cases))} > {args.max_cases},取前 {args.max_cases}"
253
+ f"(A1 铁律截断,报告必须告知用户)",
254
+ file=sys.stderr,
255
+ )
256
+
257
+ if not args.perf and not args.from_collect:
258
+ print("ERROR: --perf 与 --from-collect 必须提供其一", file=sys.stderr)
259
+ return 1
260
+ if args.perf and args.from_collect:
261
+ print(
262
+ "ERROR: --perf 与 --from-collect 互斥(直连模式自带 perf_data.csv)",
263
+ file=sys.stderr,
264
+ )
265
+ return 1
266
+ perf_path = args.perf
267
+ if args.from_collect:
268
+ collect_root = Path(args.from_collect)
269
+ candidate = collect_root / "perf_data.csv"
270
+ if not candidate.exists():
271
+ print(
272
+ f"ERROR: {candidate} 不存在(--from-collect 需要 collect_cases.py 产物目录)",
273
+ file=sys.stderr,
274
+ )
275
+ return 1
276
+ perf_path = str(candidate)
277
+ # 伪 Green 防护:collect 侧 failed/partial 如实透传(不静默跳过)
278
+ summary_path = collect_root / "collect_summary.json"
279
+ if summary_path.exists():
280
+ summary = json.loads(summary_path.read_text(encoding="utf-8"))
281
+ bad = summary.get("failed_or_partial", [])
282
+ if bad:
283
+ print(
284
+ f"WARN: collect 侧 {len(bad)} 项 failed/partial 将无数据(详见 "
285
+ f"{summary_path},伪 Green 防护不抹掉)",
286
+ file=sys.stderr,
287
+ )
288
+
289
+ perf = load_perf(perf_path)
290
+ variants = [v.strip() for v in args.after_variants.split(",") if v.strip()]
291
+ out_dir = Path(args.output_dir)
292
+ out_dir.mkdir(parents=True, exist_ok=True)
293
+
294
+ # ---- 逐 case × 逐变体:snapshot + delta_compare ----
295
+ matrix: dict[tuple[str, str], dict] = {}
296
+ for case in cases:
297
+ cid = case["case_id"]
298
+ base_perf = perf.get((cid, args.baseline_variant))
299
+ if base_perf is None:
300
+ print(f"WARN: case {cid} 无基线数据,跳过", file=sys.stderr)
301
+ continue
302
+ base_snap = out_dir / cid / "baseline.json"
303
+ run_snapshot(scope, case, args.baseline_variant, base_perf, base_snap)
304
+ for variant in variants:
305
+ v_perf = perf.get((cid, variant))
306
+ if v_perf is None:
307
+ print(
308
+ f"WARN: case {cid} 变体 {variant} 无数据,记 MISSING",
309
+ file=sys.stderr,
310
+ )
311
+ matrix[(cid, variant)] = {
312
+ "status": "MISSING",
313
+ "speedup": None,
314
+ "precision": "-",
315
+ "base_us": base_perf["kernel_avg_us"],
316
+ "after_us": None,
317
+ }
318
+ continue
319
+ v_snap = out_dir / cid / f"{variant}.json"
320
+ comp_md = out_dir / cid / f"{variant}_comparison.md"
321
+ run_snapshot(scope, case, variant, v_perf, v_snap)
322
+ rc, stdout = run_compare(base_snap, v_snap, comp_md)
323
+ if rc == 2:
324
+ matrix[(cid, variant)] = {
325
+ "status": "REJECTED(口径/精度)",
326
+ "speedup": None,
327
+ "precision": v_perf["precision"],
328
+ "base_us": base_perf["kernel_avg_us"],
329
+ "after_us": v_perf["kernel_avg_us"],
330
+ }
331
+ continue
332
+ status, speedup = parse_conclusion(comp_md.read_text(encoding="utf-8"))
333
+ matrix[(cid, variant)] = {
334
+ "status": status,
335
+ "speedup": speedup,
336
+ "precision": v_perf["precision"],
337
+ "base_us": base_perf["kernel_avg_us"],
338
+ "after_us": v_perf["kernel_avg_us"],
339
+ }
340
+ # 中间快照保留策略(P2-2/G20 ③,反馈修正:此前注释自相矛盾——标题「清理」
341
+ # 正文「不删」)。决策:**默认全保留**(snapshot 契约文件是审计真源,
342
+ # delta_compare 的口径指纹复验依赖两侧 json);目录膨胀用归档清单治理:
343
+ # comparison_matrix.md 末尾自动生成产物索引(哪些文件可归档/删除由人工
344
+ # 按清单决策,不自动删除——误删审计链的代价 > 磁盘成本)。
345
+
346
+ # ---- comparison_matrix.md ----
347
+ md = [
348
+ "# 全量 case 对比矩阵(batch_pipeline 自动生成)",
349
+ "",
350
+ f"- 基线变体:{args.baseline_variant};优化变体:{', '.join(variants)}",
351
+ f"- case 数:{len(cases)}{'(已按 A1 铁律截断,须告知用户)' if truncated else ''}",
352
+ f"- 口径:{scope['soc']} / CANN {scope['cann_version']} / device {scope['device_id']}"
353
+ f" / timing={scope['timing_method']} / warmup={scope['warmup']} repeat={scope['repeat']}",
354
+ "",
355
+ ]
356
+ for variant in variants:
357
+ md += [
358
+ f"## {variant}",
359
+ "",
360
+ "| Case | Shape | dtype | 基线(us) | 优化后(us) | speedup | 结论 | 精度 |",
361
+ "|------|-------|-------|---------|-----------|---------|------|------|",
362
+ ]
363
+ speeds = []
364
+ for case in cases:
365
+ m = matrix.get((case["case_id"], variant))
366
+ if m is None:
367
+ continue
368
+ sp = f"{m['speedup']:.3f}x" if m["speedup"] else "—"
369
+ md.append(
370
+ f"| {case['case_id']} | {case['shape']} | {case['dtype']} | "
371
+ f"{m['base_us']} | {m['after_us'] if m['after_us'] else '—'} | "
372
+ f"{sp} | {m['status']} | {m['precision']} |"
373
+ )
374
+ if m["speedup"]:
375
+ speeds.append(m["speedup"])
376
+ md.append("")
377
+ if speeds:
378
+ geomean = math.exp(sum(math.log(s) for s in speeds) / len(speeds))
379
+ best = max(speeds)
380
+ worst = min(speeds)
381
+ n_keep = sum(
382
+ 1
383
+ for c in cases
384
+ if matrix.get((c["case_id"], variant), {}).get("status") == "KEEP"
385
+ )
386
+ n_revert = sum(
387
+ 1
388
+ for c in cases
389
+ if matrix.get((c["case_id"], variant), {}).get("status") == "REVERT"
390
+ )
391
+ n_noise = sum(
392
+ 1
393
+ for c in cases
394
+ if matrix.get((c["case_id"], variant), {}).get("status") == "NOISE"
395
+ )
396
+ n_rej = sum(
397
+ 1
398
+ for c in cases
399
+ if str(
400
+ matrix.get((c["case_id"], variant), {}).get("status", "")
401
+ ).startswith("REJECTED")
402
+ )
403
+ md += [
404
+ "**汇总**:",
405
+ f"- geomean speedup:**{geomean:.3f}x**({len(speeds)}/{len(cases)} case 有可比数据)",
406
+ f"- 最好 case:{best:.3f}x | 最差 case:{worst:.3f}x",
407
+ f"- KEEP {n_keep} | REVERT {n_revert} | 噪声带内 {n_noise} | 拒绝比较 {n_rej} | MISSING {len(cases) - len(speeds) - n_rej}",
408
+ "",
409
+ "> 汇总口径:拒绝比较(口径漂移/精度 fail)与 MISSING 的 case 不进 geomean;"
410
+ "REVERT case 计入(负样本是瓶颈证据)。逐 case comparison.md 见各 case 子目录。",
411
+ ]
412
+ md.append("")
413
+
414
+ # ---- 产物索引(P2-2/G20 ③:目录膨胀治理——归档清单自动生成,删除由人工决策)----
415
+ n_snap = n_comp = 0
416
+ for cid_dir in out_dir.iterdir():
417
+ if not cid_dir.is_dir():
418
+ continue
419
+ n_snap += sum(1 for p in cid_dir.glob("*.json"))
420
+ n_comp += sum(1 for p in cid_dir.glob("*_comparison.md"))
421
+ md += [
422
+ "## 产物索引(tuning_artifacts 归档清单)",
423
+ "",
424
+ f"- 逐 case snapshot JSON:{n_snap} 份(口径指纹审计真源,delta_compare 复验依赖)",
425
+ f"- 逐 case comparison.md:{n_comp} 份(三段结论与处置依据)",
426
+ "- 本矩阵:comparison_matrix.md",
427
+ "",
428
+ "> 生命周期:多轮迭代结束后按本清单人工决策归档/删除( KEEP 轮的两侧 json",
429
+ "> 与 REVERT 轮的 comparison.md 建议长期保留——回滚记录是瓶颈证据;",
430
+ "> NOISE 轮中间快照可归档压缩)。本工具不自动删除任何产物。",
431
+ ]
432
+
433
+ out_md = out_dir / "comparison_matrix.md"
434
+ out_md.write_text("\n".join(md) + "\n", encoding="utf-8")
435
+ n_rows = sum(1 for k in matrix if k[1] in variants)
436
+ print(
437
+ f"matrix → {out_md}({n_rows} 行 = {len(cases)} case × {len(variants)} 机制;"
438
+ f"产物索引:{n_snap} snapshot + {n_comp} comparison)"
439
+ )
440
+ return 0
441
+
442
+
443
+ if __name__ == "__main__":
444
+ sys.exit(main())
@@ -0,0 +1,249 @@
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
+ """calibrate.py — 预估-实测校准回路(REVIEW2 G9 / 反馈 P1-2 ①,27-2 回路兑现)
13
+
14
+ 设计对齐:
15
+ - io-contracts.md 已冻结 calibration_data.json 契约(opstune.calibration/1.0,
16
+ Step 5 逐轮产出),但全仓无任何脚本消费它——「校准回路」只有入口没有回路。
17
+ - 本脚本是消费端:读多工程 calibration_data.json → 按 (mechanism, bound_rule)
18
+ 聚合 est vs measured 偏差 → 输出 speedup_range 修订建议(Markdown diff)。
19
+ - **不自动回写规则 YAML**:修订建议经人工评审后改 YAML,改完必须过
20
+ evals/run_evals.py 回归(反馈 P1-2 原文要求,阈值纪律不破坏)。
21
+
22
+ 用法:
23
+ python3 calibrate.py --inputs tuning_artifacts/gelu/calibration_data.json \
24
+ tuning_artifacts/softmax/calibration_data.json -o calibration_report.md
25
+
26
+ 偏差口径(每聚合组):
27
+ est_lo / est_hi :该组全部条目 speedup_est 区间的均值
28
+ measured_mean :实测 speedup 均值(KEEP 与 REVERT 都算——REVERT 是负样本,
29
+ 两者都是「预估 vs 实测」的校准输入;噪声带内条目不算——
30
+ 带内数字无方向信息)
31
+ verdict :est 区间对 measured 的覆盖关系(covered / est_high / est_low /
32
+ flipped:KEEP 预估被实测 REVERT 推翻)
33
+ 修订建议:
34
+ est_high(系统性高估)→ 建议区间下压到 measured_mean 的 0.9~1.1 倍带;
35
+ est_low(系统性低估)→ 建议区间上抬同带;样本 < 3 只提示不给出数值建议
36
+ (单点不改阈值——阈值纪律的统计底线)。
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import argparse
42
+ import datetime
43
+ import json
44
+ import sys
45
+ from pathlib import Path
46
+
47
+
48
+ def load_entries(paths: list[str]) -> list[dict]:
49
+ """读多份 calibration_data.json,展平为带工程来源的条目列表。"""
50
+ entries = []
51
+ for p in paths:
52
+ data = json.loads(Path(p).read_text(encoding="utf-8"))
53
+ if data.get("schema") != "opstune.calibration/1.0":
54
+ print(
55
+ f"WARN: {p} schema != opstune.calibration/1.0,仍按字段尽力解析",
56
+ file=sys.stderr,
57
+ )
58
+ for e in data.get("entries", []):
59
+ if not e.get("mechanism") or not e.get("bound_rule"):
60
+ print(
61
+ f"WARN: {p} 存在缺 mechanism/bound_rule 的条目,跳过: {e}",
62
+ file=sys.stderr,
63
+ )
64
+ continue
65
+ e["_source"] = data.get("operator", Path(p).parent.name)
66
+ e["_path"] = p
67
+ entries.append(e)
68
+ return entries
69
+
70
+
71
+ def group_key(e: dict) -> str:
72
+ return f"{e['mechanism']}@{e['bound_rule']}"
73
+
74
+
75
+ def measured_value(e: dict) -> float | None:
76
+ v = e.get("speedup_measured")
77
+ if v is None:
78
+ return None
79
+ try:
80
+ return float(v)
81
+ except (TypeError, ValueError):
82
+ return None
83
+
84
+
85
+ def est_bounds(e: dict) -> tuple[float | None, float | None]:
86
+ est = e.get("speedup_est")
87
+ if not isinstance(est, (list, tuple)) or len(est) != 2:
88
+ return None, None
89
+ try:
90
+ return float(est[0]), float(est[1])
91
+ except (TypeError, ValueError):
92
+ return None, None
93
+
94
+
95
+ def verdict_of(lo: float, hi: float, m: float, e: dict) -> str:
96
+ """est 区间对 measured 的覆盖关系。"""
97
+ if e.get("verdict") == "REVERT" and m is not None and m >= 1.0:
98
+ return "flipped" # 预估有收益、实测劣化被回滚 → 最强纠偏信号
99
+ if m is None:
100
+ return "no-data"
101
+ if lo <= m <= hi:
102
+ return "covered"
103
+ if m < lo:
104
+ return "est_high" # 实测低于预估下界 → 系统性高估
105
+ return "est_low" # 实测高于预估上界 → 系统性低估
106
+
107
+
108
+ def main() -> int:
109
+ ap = argparse.ArgumentParser(
110
+ description="预估-实测校准回路(calibration_data.json 消费端)"
111
+ )
112
+ ap.add_argument(
113
+ "--inputs",
114
+ required=True,
115
+ nargs="+",
116
+ help="一份或多份 calibration_data.json(多工程聚合)",
117
+ )
118
+ ap.add_argument("-o", "--output", required=True, help="校准报告 Markdown 输出")
119
+ args = ap.parse_args()
120
+
121
+ entries = load_entries(args.inputs)
122
+ if not entries:
123
+ print("ERROR: 无有效条目(检查 calibration_data.json)", file=sys.stderr)
124
+ return 1
125
+
126
+ # 噪声带内条目无方向信息,不进聚合(io-contracts 纪律:带内不判收益/劣化)
127
+ usable = [e for e in entries if e.get("verdict") in ("KEEP", "REVERT")]
128
+ skipped_band = len(entries) - len(usable)
129
+ if skipped_band:
130
+ print(
131
+ f"NOTE: {skipped_band} 条噪声带内/无 verdict 条目不进聚合", file=sys.stderr
132
+ )
133
+
134
+ groups: dict[str, list[dict]] = {}
135
+ for e in usable:
136
+ groups.setdefault(group_key(e), []).append(e)
137
+
138
+ md = [
139
+ "# 预估-实测校准报告(calibrate.py 自动生成)",
140
+ "",
141
+ f"- 生成时间:{datetime.datetime.now().isoformat(timespec='seconds')}",
142
+ f"- 输入:{len(args.inputs)} 份 calibration_data.json,"
143
+ f"{len(entries)} 条轮次记录(其中 {skipped_band} 条噪声带内不进聚合)",
144
+ f"- 聚合维度:(mechanism, bound_rule) 共 {len(groups)} 组",
145
+ "",
146
+ "> **处置纪律**:本报告只产出修订**建议**(Markdown diff 形态)。修订须人工评审后",
147
+ "> 手工改规则 YAML 的 speedup_range,改完必须过 `evals/run_evals.py` 回归全绿",
148
+ "> 才可合入——不自动回写(阈值纪律,反馈 P1-2 原文)。",
149
+ "",
150
+ ]
151
+
152
+ n_high = n_low = n_flip = 0
153
+ for key in sorted(groups):
154
+ g = groups[key]
155
+ est_pairs = [est_bounds(e) for e in g]
156
+ est_pairs = [(lo, hi) for lo, hi in est_pairs if lo is not None]
157
+ measured = [measured_value(e) for e in g]
158
+ measured = [m for m in measured if m is not None]
159
+ if not est_pairs:
160
+ continue
161
+ lo = sum(p[0] for p in est_pairs) / len(est_pairs)
162
+ hi = sum(p[1] for p in est_pairs) / len(est_pairs)
163
+ m_mean = sum(measured) / len(measured) if measured else None
164
+ m_min = min(measured) if measured else None
165
+ m_max = max(measured) if measured else None
166
+ verdicts = [
167
+ verdict_of(
168
+ est_bounds(e)[0] or lo, est_bounds(e)[1] or hi, measured_value(e), e
169
+ )
170
+ for e in g
171
+ ]
172
+ v_high = verdicts.count("est_high")
173
+ v_low = verdicts.count("est_low")
174
+ v_flip = verdicts.count("flipped")
175
+ n_high += v_high
176
+ n_low += v_low
177
+ n_flip += v_flip
178
+
179
+ md += [
180
+ f"## {key}({len(g)} 轮)",
181
+ "",
182
+ "| 项 | 值 |",
183
+ "|---|---|",
184
+ f"| est 区间(均值口径) | [{lo:.2f}, {hi:.2f}] |",
185
+ f"| measured | mean {f'{m_mean:.3f}' if m_mean is not None else '—'}"
186
+ f" / min {f'{m_min:.3f}' if m_min is not None else '—'}"
187
+ f" / max {f'{m_max:.3f}' if m_max is not None else '—'}({len(measured)} 条有数值) |",
188
+ f"| 覆盖判定 | est_high ×{v_high} | est_low ×{v_low} | flipped ×{v_flip} |"
189
+ f" covered ×{verdicts.count('covered')} |",
190
+ f"| 轮次来源 | {', '.join(sorted({e['_source'] for e in g}))} |",
191
+ ]
192
+
193
+ # 修订建议:样本 ≥3 且方向一致才给数值(单点不改阈值)
194
+ if len(g) >= 3 and m_mean is not None:
195
+ if v_high >= 2 and v_high > v_low:
196
+ md += [
197
+ "",
198
+ "**建议(est 系统性偏高)**:speedup_range 下压——",
199
+ "```diff",
200
+ f"- speedup_range: [{lo:.2f}, {hi:.2f}]",
201
+ f"+ speedup_range: [{m_mean * 0.9:.2f}, {m_mean * 1.1:.2f}]",
202
+ "```",
203
+ f"依据:{v_high}/{len(g)} 轮实测低于预估下界,实测均值 {m_mean:.3f}。",
204
+ ]
205
+ elif v_low >= 2 and v_low > v_high:
206
+ md += [
207
+ "",
208
+ "**建议(est 系统性偏低)**:speedup_range 上抬——",
209
+ "```diff",
210
+ f"- speedup_range: [{lo:.2f}, {hi:.2f}]",
211
+ f"+ speedup_range: [{m_mean * 0.9:.2f}, {m_mean * 1.1:.2f}]",
212
+ "```",
213
+ f"依据:{v_low}/{len(g)} 轮实测高于预估上界,实测均值 {m_mean:.3f}。",
214
+ ]
215
+ elif v_flip >= 2:
216
+ md += [
217
+ "",
218
+ f"**建议(方向翻车)**:{v_flip} 轮预估 KEEP 实测 REVERT——"
219
+ "先查机制适用前提是否写错(该 (mechanism, bound_rule) 组合的"
220
+ "机制卡「适用前提」或反证条目可能漏了排除条件),"
221
+ "比压区间更优先。",
222
+ ]
223
+ else:
224
+ md += ["", "覆盖良好(或方向不一致),无需修订。"]
225
+ else:
226
+ md += [
227
+ "",
228
+ f"样本 {len(g)} < 3:只记录不给出数值建议(单点不改阈值——统计底线)。",
229
+ ]
230
+ md.append("")
231
+
232
+ md += [
233
+ "## 汇总",
234
+ "",
235
+ f"- est_high(高估){n_high} 轮 | est_low(低估){n_low} 轮 | "
236
+ f"flipped(方向翻车){n_flip} 轮",
237
+ "- 跑得越多 🟡 越多的问题开始收敛:上述建议经评审回写后,"
238
+ "speedup_est 的可信度随调优次数复利提升。",
239
+ ]
240
+
241
+ out = Path(args.output)
242
+ out.parent.mkdir(parents=True, exist_ok=True)
243
+ out.write_text("\n".join(md) + "\n", encoding="utf-8")
244
+ print(f"calibration report → {out}({len(groups)} 组 / {len(usable)} 轮)")
245
+ return 0
246
+
247
+
248
+ if __name__ == "__main__":
249
+ sys.exit(main())