已合并
feat(ascendc-port-orchestrator): PR2/5 编排引擎+子agent (RFC #364, +OKF迁移) #609
feat(ascendc-port-orchestrator): PR2/5 编排引擎+子agent (RFC #364, +OKF迁移) #609
已合并
junming222创建于 7月15日
共 644 个文件变更+156728-0
@@ -0,0 +1,43 @@
1+# MFU 求解器 (算子理论最大 MFU 模型) — v0
2+ 
3+数学口径见 [`../docs/design/OPERATOR_MFU_DEFINITION.md`](../docs/design/OPERATOR_MFU_DEFINITION.md)、硬件常数见 [`../docs/design/L0_HARDWARE_CONSTANTS.md`](../docs/design/L0_HARDWARE_CONSTANTS.md)。
4+ 
5+## 这是什么
6+给定算子(FLOPs/访存/dtype/可选通信)+ 硬件目标,算出**理论最大 MFU**(roofline 上界 ∩ 完全通算掩盖)+ 瓶颈归因(compute/mem/comm-bound)。
7+ 
8+```
9+理论最大 MFU = T_compute / max(T_compute, T_mem, T_comm) × η_util
10+ T_compute = useful_FLOPs / cube_peak(dtype)
11+ T_mem = HBM_bytes / HBM_BW
12+ T_comm = comm_bytes / interconnect_BW
13+ η_util 默认 1.0(纯理论天花板),待 msprof 真机标定下调
14+```
15+ 
16+## 文件
17+- `mfu_model.py` — L0 硬件模型(910C/950PR/950DT) + L1 原语(matmul fwd/bwd) + L3 roofline 求解器
18+- `demo.py` — matmul 在三目标上的 MFU_max 示例
19+ 
20+## 跑
21+```bash
22+cd mfu && python3 demo.py
23+```
24+ 
25+## v0 结果验证
26+- 大方阵 matmul(4096³): MFU_max=1.0 compute-bound ✓
27+- GEMV(M=1): MFU_max≈0.003 memory-bound ✓
28+- **cube peak 交叉验证**: 950PR FP8 推导 973 TFLOPS ≈ 官方 1 PFLOPS ✓("1拍 2·M·K·N FLOPs"口径成立)
29+ 
30+## 进度
31+- [x] 通算掩盖:分布式 DP 梯度 AllReduce 模型 + SuperPod384 互联(`with_data_parallel`)
32+- [x] FlashAttention(cube+vector 混合) 原语
33+- [x] L2 调度演算:多级 roofline,tiling 用 **matmul I/O 下界(Hong-Kung)** = 最优 tiling 搬运量
34+ - 这是"理论最优调度"对应的搬运量(理论最大 MFU), 而非某具体次优 tiling
35+ - 修正: 早期用 L0C-tile 计数的 2 级模型误判"910C 大矩阵 L2-bound 0.66" —— 那是次优 tiling 的人为产物;
36+ I/O 下界正确给出大矩阵 **compute-bound MFU=1.0**(理论最优), 实测与理论的差距归入 η_util(待 msprof)
37+ - 验证: 大/中矩阵 compute-bound=1.0; 瘦长(256×256×8192)/小(128³) 正确 memory-bound
38+- [x] 非 matmul 原语: softmax/rmsnorm/elementwise(向量引擎, 深度 memory-bound, MFU 0.007~0.18)
39+ - 洞察: vec_peak 越高 MFU 越低(同搬运量) → 必须算子融合减 HBM 往返; 950DT(4TB/s) 比 950PR 优 2.5×
40+- [x] 单算子 → 训练 e2e MFU 聚合(`transformer_layer_e2e`, OPERATOR_MFU_DEFINITION §4 契约)
41+ - Σuseful_FLOPs/(cube_peak×Σ各算子最优时间); 向量算子 FLOPs 小但耗时拖低 e2e; 给时间占比找瓶颈
42+- [ ] η_util msprof 真机标定 + 预测 vs 实测回归(需 NPU, 已申请)
43+- [ ] 验证闭环:FA + a5_ops 优化 agent,验证理论能否指导优化(若无用则止损)
@@ -0,0 +1,75 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""MFU 求解器 demo: matmul 在 910C/950PR/950DT 上的理论最大 MFU(前向+后向)。"""
14+from mfu_model import (HW, matmul_ops, flash_attention_ops, with_data_parallel,
15+ solve_mfu, solve_matmul_tiled, DTYPE_BYTES)
16+ 
17+ 
18+def run(M, N, K, dtype):
19+ print(f"\n==== matmul M={M} N={N} K={K} dtype={dtype} ====")
20+ fwd, bwd = matmul_ops(M, N, K, dtype)
21+ hdr = f"{'hw':7} {'stage':11} {'MFU_max':>7} {'bottleneck':>10} {'AI':>9} {'AI*':>9} {'peakTF':>7}"
22+ print(hdr)
23+ for hwname, hw in HW.items():
24+ for op in (fwd, bwd):
25+ try:
26+ r = solve_mfu(op, hw)
27+ except ValueError as e:
28+ print(f"{hwname:7} {op.name:11} skip ({e})")
29+ continue
30+ print(f"{r['hw']:7} {r['op']:11} {r['mfu_max']:>7} {r['bottleneck']:>10} "
31+ f"{r['arithmetic_intensity']:>9} {r['ridge_ai']:>9} {r['peak_TFLOPS']:>7}")
32+ 
33+ 
34+def run_fa(B, H, S, D, dtype="bf16", causal=False):
35+ print(f"\n==== FlashAttention B={B} H={H} S={S} D={D} dtype={dtype} causal={causal} ====")
36+ fwd, bwd = flash_attention_ops(B, H, S, D, dtype, causal)
37+ print(f"{'hw':18} {'stage':8} {'MFU_max':>7} {'bottleneck':>10} {'AI':>8} {'AI*':>8}")
38+ for hw in (HW["910C"], HW["950PR"], HW["950DT"]):
39+ for op in (fwd, bwd):
40+ r = solve_mfu(op, hw)
41+ print(f"{r['hw']:18} {r['op']:8} {r['mfu_max']:>7} {r['bottleneck']:>10} "
42+ f"{r['arithmetic_intensity']:>8} {r['ridge_ai']:>8}")
43+ 
44+ 
45+def run_dist_train(M, N, K, dtype, n_dev):
46+ """训练数据并行: bwd 的 dW 叠加梯度 AllReduce, 看通信是否被 compute 掩盖。"""
47+ print(f"\n==== 分布式训练 matmul M={M} N={N} K={K} dtype={dtype} DP={n_dev} (910C@SuperPod384) ====")
48+ _, bwd = matmul_ops(M, N, K, dtype)
49+ grad_bytes = K * N * DTYPE_BYTES[dtype] # dW 梯度大小
50+ bwd_dp = with_data_parallel(bwd, grad_bytes, n_dev)
51+ hw = HW["910C_superpod"]
52+ r = solve_mfu(bwd_dp, hw)
53+ print(f" MFU_max={r['mfu_max']} bottleneck={r['bottleneck']} "
54+ f"t_compute={r['t_compute_us']}us t_comm={r['t_comm_us']}us "
55+ f"-> 通信{'被掩盖' if r['bottleneck']!='comm' else '未掩盖(comm-bound)'}")
56+ 
57+ 
58+if __name__ == "__main__":
59+ # 大方阵(compute-bound 应 MFU_max≈1) vs 瘦长(memory-bound 应 <1)
60+ run(4096, 4096, 4096, "bf16")
61+ run(4096, 4096, 4096, "fp8")
62+ run(1, 4096, 4096, "bf16") # GEMV-like, memory-bound
63+ # FlashAttention (cube+vector 混合, FA tiling 高算术强度)
64+ run_fa(2, 32, 4096, 128, "bf16")
65+ run_fa(2, 32, 512, 128, "bf16") # 短序列 -> AI 降
66+ # 分布式训练通算掩盖: 大矩阵 compute 多 -> 通信易掩盖; 小矩阵 -> 易 comm-bound
67+ run_dist_train(8192, 8192, 8192, "bf16", 8)
68+ run_dist_train(512, 8192, 8192, "bf16", 64)
69+ # L2 调度演算: 多级 roofline (tiling 受 L0C 限制 -> L2 re-read)
70+ print("\n==== tiled matmul (多级 roofline: HBM+L2, L0C限tile) ====")
71+ print(" (绝对值待 msprof 校准 + 加 L1 复用级; 结构示意 L0C小->L2-bound 机理)")
72+ for M, N, K in [(4096, 4096, 4096), (256, 256, 8192)]:
73+ for hwn in ("910C", "950PR", "950DT"):
74+ r = solve_matmul_tiled(M, N, K, HW[hwn], "bf16")
75+ print(f" {hwn:6} {r['op']:22} MFU={r['mfu_max']:<6} bn={r['bottleneck']}")
@@ -0,0 +1,393 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""算子理论最大 MFU 求解器 v0 (L0 硬件模型 + L1 原语 + L3 roofline 求解).
14+ 
15+数学口径见 docs/design/OPERATOR_MFU_DEFINITION.md 与 L0_HARDWARE_CONSTANTS.md。
16+- 理论最大 MFU = T_compute_ideal / T_total_optimal
17+- T_total_optimal = max(T_compute, T_mem, T_comm) # 完全通算掩盖上界(理论天花板)
18+- η_util(tiling/尾块/流水有效利用率) 默认 1.0, 待 msprof 真机标定后下调。
19+ 
20+硬件常数来源: docs/archive/A5_HARDWARE_DETAILS.md + ASCEND_FLEET_910C_950DT_SUPERPOD384.md
21+cube peak 用"1拍 = 2·M·K·N FLOPs"口径(910C 源实测, 950PR FP8 推导≈官方1PFLOPS 交叉验证)。
22+"""
23+from dataclasses import dataclass, field
24+from typing import Optional
25+ 
26+# 每个 cube dtype 的"单拍 FLOPs" = 2·M·K·N (矩阵粒度), 来自 L0 常数表
27+CUBE_FLOPS_PER_CYCLE = {
28+ "fp32": 2 * 16 * 1 * 16, # [16x1x16]
29+ "tf32": 2 * 16 * 8 * 16, # [16x8x16]
30+ "fp16": 2 * 16 * 16 * 16, # [16x16x16] = 8192
31+ "bf16": 2 * 16 * 16 * 16, # 8192
32+ "fp8": 2 * 16 * 32 * 16, # [16x32x16] = 16384
33+ "int8": 2 * 16 * 32 * 16, # 16384
34+ "mxfp4": 2 * 16 * 64 * 16, # [16x64x16] = 32768
35+ "int4": 2 * 16 * 64 * 16, # 32768
36+}
37+DTYPE_BYTES = {"fp32": 4, "tf32": 4, "fp16": 2, "bf16": 2, "fp8": 1, "int8": 1, "mxfp4": 0.5, "int4": 0.5}
38+ 
39+ 
40+@dataclass
41+class HardwareModel:
42+ """描述设备算力、带宽和片上存储容量的简化硬件模型。"""
43+ 
44+ name: str
45+ n_aic: int # cube core 数
46+ freq_hz: float # 主频
47+ hbm_bw: float # HBM 带宽 B/s
48+ hbm_bytes: float # HBM 容量 B
49+ interconnect_bw: float = 0.0 # 卡间/die间 互联 B/s (单向有效)
50+ arch: str = "SIMD" # SIMD(A3) / SIMT(A5)
51+ l2_bw: float = 0.0 # L2 读带宽 B/s (聚合); 0=不建模L2级
52+ l2_bytes: float = 0.0 # L2 容量 B
53+ l1_bytes: float = 0.0 # L1 容量 B (matmul 复用级)
54+ l0c_bytes: float = 0.0 # L0C(输出累加器)容量 B
55+ # 直接给定的 peak(若有官方值, 覆盖推导), key=dtype -> FLOP/s
56+ peak_override: dict = field(default_factory=dict)
57+ vec_peak: dict = field(default_factory=dict) # 向量 dtype -> FLOP/s
58+ 
59+ def cube_peak(self, dtype: str) -> float:
60+ """返回指定数据类型的 Cube 峰值算力;显式配置优先。"""
61+ 
62+ if dtype in self.peak_override:
63+ return self.peak_override[dtype]
64+ fpc = CUBE_FLOPS_PER_CYCLE.get(dtype)
65+ if fpc is None:
66+ raise ValueError(f"{self.name}: cube 不支持 dtype {dtype}")
67+ return self.n_aic * fpc * self.freq_hz
68+ 
69+ def ridge_ai(self, dtype: str, engine="cube") -> float:
70+ """返回指定计算引擎的 Roofline 拐点算术强度。"""
71+ 
72+ peak = self.cube_peak(dtype) if engine == "cube" else self.vec_peak[dtype]
73+ return peak / self.hbm_bw
74+ 
75+ 
76+@dataclass
77+class OpSpec:
78+ """单算子的某个阶段(fwd 或 bwd)的 FLOPs/bytes/通信 记账。"""
79+ name: str
80+ flops: float # useful FLOPs
81+ hbm_bytes: float # 最小 HBM 访存字节(理想 tiling, 每数据读一次)
82+ dtype: str = "bf16"
83+ engine: str = "cube" # cube / vector
84+ comm_bytes: float = 0.0 # 分布式通信字节(单向有效量)
85+ 
86+ 
87+def solve_mfu(op: OpSpec, hw: HardwareModel, eta_util: float = 1.0) -> dict:
88+ """求理论最大 MFU + 瓶颈归因。eta_util=有效利用率(默认1=纯理论天花板)。"""
89+ peak = hw.cube_peak(op.dtype) if op.engine == "cube" else hw.vec_peak[op.dtype]
90+ t_compute = op.flops / peak
91+ t_mem = op.hbm_bytes / hw.hbm_bw
92+ t_comm = op.comm_bytes / hw.interconnect_bw if (op.comm_bytes and hw.interconnect_bw) else 0.0
93+ t_total = max(t_compute, t_mem, t_comm) # 完全掩盖上界
94+ mfu_max = (t_compute / t_total) * eta_util if t_total > 0 else 0.0
95+ times = {"compute": t_compute, "mem": t_mem, "comm": t_comm}
96+ bottleneck = max(times, key=times.get)
97+ ai = op.flops / op.hbm_bytes if op.hbm_bytes else float("inf")
98+ return {
99+ "op": op.name, "hw": hw.name, "dtype": op.dtype,
100+ "mfu_max": round(mfu_max, 4),
101+ "bottleneck": bottleneck,
102+ "arithmetic_intensity": round(ai, 2),
103+ "ridge_ai": round(hw.ridge_ai(op.dtype, op.engine), 2),
104+ "peak_TFLOPS": round(peak / 1e12, 1),
105+ "t_compute_us": round(t_compute * 1e6, 3),
106+ "t_mem_us": round(t_mem * 1e6, 3),
107+ "t_comm_us": round(t_comm * 1e6, 3),
108+ "eta_util": eta_util,
109+ }
110+ 
111+ 
112+# ---------- L1 原语: matmul 前向/后向 FLOPs/bytes 记账 ----------
113+def matmul_ops(M, N, K, dtype="bf16"):
114+ """Y=X·W, X[M,K] W[K,N]. 返回 (fwd, bwd) 两个 OpSpec。
115+ bytes = 理想最小 HBM 流量 = 各 tensor 读/写一次。
116+ """
117+ sz = DTYPE_BYTES[dtype]
118+ fwd = OpSpec("matmul.fwd", flops=2 * M * N * K,
119+ hbm_bytes=(M * K + K * N + M * N) * sz, dtype=dtype)
120+ # bwd: dX = dY·Wᵀ (2MNK, 读 dY[M,N]+W[K,N], 写 dX[M,K]); dW = Xᵀ·dY (2MNK, 读 X[M,K]+dY[M,N], 写 dW[K,N])
121+ bwd = OpSpec("matmul.bwd", flops=4 * M * N * K,
122+ hbm_bytes=(M * N + K * N + M * K + M * K + M * N + K * N) * sz, dtype=dtype)
123+ return fwd, bwd
124+ 
125+ 
126+# ---------- L1 原语: FlashAttention 前向/后向 (cube+vector 混合) ----------
127+def flash_attention_ops(B, H, S, D, dtype="bf16", causal=False):
128+ """Q,K,V [B,H,S,D]. FA tiling 不物化 S=[B,H,S,S], 故 HBM 仅读 QKV 写 O。
129+ cube FLOPs 主导(QKᵀ + P·V), softmax 走 vector(量级小, 此处计入但归一仍用 cube)。
130+ bwd ≈ 2.5× fwd cube FLOPs(重算 + dQ/dK/dV)。causal 减半有效 S² 工作量。
131+ """
132+ sz = DTYPE_BYTES[dtype]
133+ causal_factor = 0.5 if causal else 1.0
134+ # fwd: QKᵀ=2BHS²D, P·V=2BHS²D
135+ fwd_cube = 4 * B * H * S * S * D * causal_factor
136+ fwd = OpSpec("FA.fwd", flops=fwd_cube,
137+ hbm_bytes=(3 * B * H * S * D + B * H * S * D) * sz, # 读QKV + 写O
138+ dtype=dtype, engine="cube")
139+ bwd = OpSpec("FA.bwd", flops=2.5 * fwd_cube,
140+ hbm_bytes=(5 * B * H * S * D + 3 * B * H * S * D) * sz, # 读QKVO·dO + 写dQdKdV
141+ dtype=dtype, engine="cube")
142+ return fwd, bwd
143+ 
144+ 
145+# ---------- L2(部分): 分布式集合通信 = 通算掩盖输入 ----------
146+def allreduce_bytes(msg_bytes, n_dev, algo="ring"):
147+ """带宽最优 AllReduce 每设备通信量。ring/halving-doubling 同为 2·(N-1)/N·M。"""
148+ if n_dev <= 1:
149+ return 0.0
150+ return 2.0 * (n_dev - 1) / n_dev * msg_bytes
151+ 
152+ 
153+def with_data_parallel(op: OpSpec, grad_bytes: float, n_dev: int):
154+ """训练数据并行: 在算子(通常 bwd 的 dW)上叠加梯度 AllReduce 通信, 走通算掩盖。
155+ 返回带 comm_bytes 的新 OpSpec(浅拷贝)。
156+ """
157+ comm = allreduce_bytes(grad_bytes, n_dev)
158+ return OpSpec(op.name + f".dp{n_dev}", flops=op.flops, hbm_bytes=op.hbm_bytes,
159+ dtype=op.dtype, engine=op.engine, comm_bytes=comm)
160+ 
161+ 
162+# ---------- L4: 单算子 -> 训练 e2e MFU 聚合 (OPERATOR_MFU_DEFINITION §4 契约) ----------
163+def _matmul_time(M, N, K, hw, dtype, flop_mult=1.0):
164+ """matmul 的 (useful_flops, T_total_optimal秒)。flop_mult: fwd=1, fwd+bwd≈3。"""
165+ flops, hbm, l2 = matmul_tiled_levels(M, N, K, hw, dtype)
166+ flops *= flop_mult
167+ r = solve_mfu_multilevel(flops, hw.cube_peak(dtype),
168+ [(hw.hbm_bw, hbm * flop_mult), (hw.l2_bw, l2 * flop_mult)])
169+ return flops, max(r["t_us"].values()) / 1e6
170+ 
171+ 
172+def _vec_time(op, hw, flop_mult=1.0):
173+ r = solve_mfu(op, hw)
174+ t = max(r["t_compute_us"], r["t_mem_us"], r["t_comm_us"]) / 1e6 * flop_mult
175+ return op.flops * flop_mult, t
176+ 
177+ 
178+def transformer_layer_e2e(B, S, d_model, d_ff, n_head, hw, dtype="bf16", training=True):
179+ """一层 Transformer 的训练 e2e MFU = Σuseful_FLOPs / (cube_peak × Σ各算子最优时间)。
180+ 展示单算子 MFU 如何聚合到 e2e: 向量算子(norm/softmax)FLOPs 小但 memory-bound 耗时,
181+ 会拖低 e2e MFU —— 这正是 e2e MFU agent 要复用的契约。
182+ """
183+ M = B * S
184+ fm = 3.0 if training else 1.0 # matmul fwd+bwd≈3x flops
185+ fam = 3.5 if training else 1.0 # attention fwd+bwd≈3.5x
186+ items = [] # (name, flops, t)
187+ for name, N, K in [("qkv_proj", 3 * d_model, d_model), ("o_proj", d_model, d_model),
188+ ("ffn_up", d_ff, d_model), ("ffn_down", d_model, d_ff)]:
189+ items.append((name, *_matmul_time(M, N, K, hw, dtype, fm)))
190+ fwd, bwd = flash_attention_ops(B, n_head, S, d_model // n_head, dtype)
191+ items.append(("attention", *_vec_time(fwd, hw, fam))) # FA cube 主导, 用其 flops/time×mult
192+ for name, opf, args in [("rmsnorm_attn", rmsnorm_op, (M, d_model)),
193+ ("rmsnorm_ffn", rmsnorm_op, (M, d_model)),
194+ ("softmax_skip", elementwise_op, (M * d_ff,))]: # 占位非线性
195+ items.append((name, *_vec_time(opf(*args), hw, 2.0 if training else 1.0)))
196+ total_flops = sum(f for _, f, _ in items)
197+ total_time = sum(t for _, _, t in items)
198+ cube_peak = hw.cube_peak(dtype)
199+ mfu = total_flops / (cube_peak * total_time) if total_time else 0.0
200+ # 各算子耗时占比(找瓶颈)
201+ breakdown = sorted(((n, round(t / total_time * 100, 1)) for n, _, t in items),
202+ key=lambda x: -x[1])
203+ return {"hw": hw.name, "e2e_MFU": round(mfu, 4),
204+ "total_TFLOPs": round(total_flops / 1e12, 2),
205+ "step_us": round(total_time * 1e6, 1),
206+ "time_pct": breakdown}
207+ 
208+ 
209+# ---------- L0 硬件实例 (常数源: docs/design/L0_HARDWARE_CONSTANTS.md) ----------
210+_L2_BW = 5.28e12 # L2 读带宽聚合: 910C 48核×110GB/s≈5.28TB/s; 950PR 官方5.28TB/s (950DT 待核实,暂同)
211+HW = {
212+ # 910C 整卡(双die ~48 AIC), HBM 3.2TB/s 128GB, 互联 784GB/s 双向→单向~392
213+ "910C": HardwareModel("910C", n_aic=48, freq_hz=1.85e9,
214+ hbm_bw=3.2e12, hbm_bytes=128 * 2**30,
215+ interconnect_bw=784e9 / 2, arch="SIMD",
216+ l2_bw=_L2_BW, l2_bytes=192 * 2**20, l1_bytes=512 * 2**10, l0c_bytes=128 * 2**10),
217+ # 950PR PG(实测card2: 28 cube/56 vec, L2 112MB, soc 957b), HBM 1.6TB/s 128GB; AG变体=36 AIC
218+ "950PR": HardwareModel("950PR", n_aic=28, freq_hz=1.65e9,
219+ hbm_bw=1.6e12, hbm_bytes=128 * 2**30,
220+ interconnect_bw=2e12, arch="SIMT",
221+ l2_bw=_L2_BW, l2_bytes=112 * 2**20, l1_bytes=512 * 2**10, l0c_bytes=256 * 2**10),
222+ # 950DT: 官方 FP8 1P/MXFP4 2P -> 反推 cube; HBM 4TB/s 144GB, 互联 2TB/s
223+ "950DT": HardwareModel("950DT", n_aic=36, freq_hz=1.65e9,
224+ hbm_bw=4e12, hbm_bytes=144 * 2**30,
225+ interconnect_bw=2e12, arch="SIMT",
226+ l2_bw=_L2_BW, l2_bytes=128 * 2**20, l1_bytes=512 * 2**10, l0c_bytes=256 * 2**10,
227+ peak_override={"fp8": 1e15, "mxfp4": 2e15,
228+ "bf16": 0.5e15, "fp16": 0.5e15}),
229+}
230+# SuperPod384 = Atlas900 A3 SuperPoD: 单卡=910C, 但超节点内卡间互联 784GB/s 双向(1:1无收敛)
231+HW["910C_superpod"] = HardwareModel(
232+ "910C@SuperPod384", n_aic=48, freq_hz=1.85e9,
233+ hbm_bw=3.2e12, hbm_bytes=128 * 2**30,
234+ interconnect_bw=784e9, arch="SIMD",
235+ l2_bw=_L2_BW, l2_bytes=192 * 2**20, l1_bytes=512 * 2**10, l0c_bytes=128 * 2**10)
236+ 
237+# 910C 单 die (Ascend910_9392, torch_npu 单设备粒度 = 24 AIC/64GB; HW['910C']是整卡48AIC)
238+# 实测标定 2026-06-26 A3: bf16 matmul 实测峰值 ~321 TFLOPS (n=4096), 理论 363 -> η_util≈0.88, 天花板不破。
239+HW["910C_die"] = HardwareModel(
240+ "910C_die", n_aic=24, freq_hz=1.85e9,
241+ hbm_bw=1.6e12, hbm_bytes=64 * 2**30, # 单die 64GB; 单die HBM BW 待msprof(暂半卡)
242+ interconnect_bw=784e9 / 2, arch="SIMD",
243+ l2_bw=_L2_BW / 2, l2_bytes=96 * 2**20, l1_bytes=512 * 2**10, l0c_bytes=128 * 2**10)
244+# 实测标定的有效利用率 η_util (2026-06-26 A3 Ascend910_9392 单die):
245+# matmul 大方阵 compute-bound: 0.88 (纯 cube)
246+# FlashAttention(fusion_attn 最优核): ~0.53 vs cube peak —— cube+vector 交织代价,
247+# softmax(向量)阶段无法与 cube 完全掩盖 -> 混合算子天花板 < 纯 cube peak。
248+ETA_UTIL = {
249+ # 实测标定 2026-06-26. 关键: A5(SIMT) FA η 远高于 A3(SIMD) -> SIMT cube+vector 掩盖更好。
250+ "matmul": {"910C_die": 0.88, "950PR": 0.98},
251+ "flash_attention": {"910C_die": 0.53, "950PR": 0.87},
252+}
253+# η-provenance (main review 2026-07-01 要求): 每个 η 的来源+日期+re-validate 触发。
254+# stale-η -> 错估 headroom, 故硬件/CANN/bisheng 版本变时必须 re-validate。
255+ETA_PROVENANCE = {
256+ ("matmul", "910C_die"): {
257+ "value": 0.88,
258+ "measured": "2026-06-26",
259+ "method": "A3 Ascend910_9392 bf16 matmul n=4096 device-time vs 理论363TF",
260+ "revalidate_on": "SOC/CANN/bisheng 版本变 或 主频变",
261+ },
262+ ("matmul", "950PR"): {
263+ "value": 0.98,
264+ "measured": "2026-06-26",
265+ "method": "A5 950PR_957b bf16 matmul n=8192 372.7 vs 378.6TF",
266+ "revalidate_on": "同上",
267+ },
268+ ("flash_attention", "910C_die"): {
269+ "value": 0.53,
270+ "measured": "2026-06-26",
271+ "method": "A3 npu_fusion_attention 170TF vs cube peak 363, msprof cube_mac 0.50",
272+ "revalidate_on": "同上 + FA kernel 版本变",
273+ },
274+ ("flash_attention", "950PR"): {
275+ "value": 0.87,
276+ "measured": "2026-06-26",
277+ "method": "A5 npu_fusion_attention 330TF vs 378.6, msprof cube_mac 0.98",
278+ "revalidate_on": "同上",
279+ },
280+}
281+ 
282+ 
283+def eta_with_provenance(op_kind, hw_name):
284+ """返回 (η, provenance)。无 provenance 的 η = 未标定/stale 风险, 调用方应警惕。"""
285+ p = ETA_PROVENANCE.get((op_kind, hw_name))
286+ eta = ETA_UTIL.get(op_kind, {}).get(hw_name)
287+ return eta, p
288+ 
289+ 
290+def achievable_mfu_with_overhead(flops, peak, op_kind, hw_name, t_overhead_s=0.0, default_eta=0.85):
291+ """v2: 理论 compute 时间 / η + 固定 launch/tail overhead -> 可达 MFU。
292+ B组(2026-06-29 A3 FA)实测暴露: v1理想roofline的陡峭ridge过乐观, 小算子受
293+ kernel-launch/尾块固定开销主导。加 T_overhead 项后, A3 FA MFU-vs-S 全程拟合实测±几%。
294+ A3 FA fusion_attn 拟合: t_overhead≈0.115ms, η≈0.53。
295+ """
296+ eta = ETA_UTIL.get(op_kind, {}).get(hw_name, default_eta)
297+ t_compute_ideal = flops / peak
298+ t_total = t_compute_ideal / eta + t_overhead_s
299+ return {"achievable_mfu": round(t_compute_ideal / t_total, 4),
300+ "eta_util": eta, "t_overhead_s": t_overhead_s,
301+ "t_compute_ideal_s": t_compute_ideal}
302+ 
303+ 
304+def achievable_mfu(theoretical_mfu_max, op_kind, hw_name, default_eta=0.85):
305+ """把实测标定的 η_util 套到理论天花板上 -> 预测"可达 MFU"。
306+ 返回 (theoretical_ceiling, eta, achievable)。优化时: 实测<achievable=>还有实现空间;
307+ 实测≈achievable=>已接近该算子类的实际上限; 要再高需改算法(降 η 的机理)。
308+ """
309+ eta = ETA_UTIL.get(op_kind, {}).get(hw_name, default_eta)
310+ return {"theoretical_ceiling": theoretical_mfu_max,
311+ "eta_util": eta,
312+ "achievable_mfu": round(theoretical_mfu_max * eta, 4)}
313+ 
314+ 
315+# 向量算力 (源: 950PR FP16 54TOPS FMA / 27 单算; 910C 推导~22.7T 待msprof; 950DT 暂同950PR)
316+HW["950PR"].vec_peak = {"fp16": 54e12, "bf16": 54e12, "fp32": 27e12}
317+HW["950DT"].vec_peak = {"fp16": 54e12, "bf16": 54e12, "fp32": 27e12}
318+HW["910C"].vec_peak = {"fp16": 22.7e12, "bf16": 22.7e12, "fp32": 11.3e12}
319+HW["910C_superpod"].vec_peak = HW["910C"].vec_peak
320+ 
321+ 
322+# ---------- L1 原语: 非 matmul (向量主导, 通常 memory-bound) ----------
323+def elementwise_op(n_elem, dtype="bf16", flops_per_elem=1, n_read=1, n_write=1, name="elementwise"):
324+ """逐元素算子(add/gelu/激活...). 向量引擎. bytes=读n_read+写n_write个张量。"""
325+ sz = DTYPE_BYTES[dtype]
326+ return OpSpec(name, flops=n_elem * flops_per_elem,
327+ hbm_bytes=n_elem * (n_read + n_write) * sz, dtype=dtype, engine="vector")
328+ 
329+ 
330+def softmax_op(rows, cols, dtype="bf16"):
331+ """softmax: 读1写1(融合) + 每元素~5 FLOPs(max,exp,sum,div). 强 memory-bound。"""
332+ n = rows * cols
333+ return OpSpec("softmax", flops=5 * n, hbm_bytes=n * 2 * DTYPE_BYTES[dtype],
334+ dtype=dtype, engine="vector")
335+ 
336+ 
337+def rmsnorm_op(rows, cols, dtype="bf16"):
338+ """RMSNorm: 读x+读weight+写y, 每元素~4 FLOPs(平方/和/rsqrt/乘)。"""
339+ n = rows * cols
340+ return OpSpec("rmsnorm", flops=4 * n,
341+ hbm_bytes=(n + cols + n) * DTYPE_BYTES[dtype], dtype=dtype, engine="vector")
342+ 
343+ 
344+# ---------- L2 调度演算: 多级内存 roofline + matmul tiling ----------
345+def _matmul_io_lb(M, N, K, cap_bytes, sz):
346+ """matmul I/O 下界 (Hong-Kung): 容量 cap 的快存下, 慢存最少搬运字节。
347+ = max( 2MNK/sqrt(cap_elems) , 读A+读B+写C 各一次 )。这是**最优 tiling**的搬运量,
348+ 故对应"理论最优调度"(理论最大 MFU), 而非某个具体次优 tiling。"""
349+ import math
350+ cap_elems = max(cap_bytes / sz, 1.0)
351+ q_reuse = 2.0 * M * N * K / math.sqrt(cap_elems)
352+ q_min = M * K + K * N + M * N
353+ return max(q_reuse, q_min) * sz
354+ 
355+ 
356+def matmul_tiled_levels(M, N, K, hw: HardwareModel, dtype="bf16"):
357+ """matmul 多级访存量(理论最优 tiling, I/O 下界):
358+ - hbm_bytes: HBM↔L2 搬运, 快存=L2 容量复用
359+ - l2_bytes : L2↔L1 搬运, 快存=L1 容量复用
360+ 返回 (flops, hbm_bytes, l2_bytes)。
361+ """
362+ sz = DTYPE_BYTES[dtype]
363+ hbm_bytes = _matmul_io_lb(M, N, K, hw.l2_bytes, sz) if hw.l2_bytes else (M * K + K * N + M * N) * sz
364+ l2_bytes = _matmul_io_lb(M, N, K, hw.l1_bytes, sz) if hw.l1_bytes else 0.0
365+ return 2 * M * N * K, hbm_bytes, l2_bytes
366+ 
367+ 
368+def solve_mfu_multilevel(flops, peak, mem_levels, comm=(0.0, 0.0), eta_util=1.0):
369+ """多级 roofline: mem_levels=[(bw,bytes),...] 各级并行可掩盖, comm=(bw,bytes)。
370+ T_total = max(T_compute, 各级 T_mem, T_comm) (完全掩盖上界)。
371+ """
372+ t_compute = flops / peak
373+ t_levels = {f"mem_L{i}": (b / bw if bw else 0.0) for i, (bw, b) in enumerate(mem_levels)}
374+ t_comm = comm[1] / comm[0] if (comm[0] and comm[1]) else 0.0
375+ times = {"compute": t_compute, "comm": t_comm, **t_levels}
376+ t_total = max(times.values())
377+ mfu = (t_compute / t_total) * eta_util if t_total > 0 else 0.0
378+ bottleneck = max(times, key=times.get)
379+ return {"mfu_max": round(mfu, 4), "bottleneck": bottleneck,
380+ "peak_TFLOPS": round(peak / 1e12, 1),
381+ "t_us": {k: round(v * 1e6, 2) for k, v in times.items()}, "eta_util": eta_util}
382+ 
383+ 
384+def solve_matmul_tiled(M, N, K, hw: HardwareModel, dtype="bf16", n_dev=1, eta_util=1.0):
385+ """matmul 端到端: tiling 多级访存 + (可选)数据并行梯度通信。"""
386+ flops, hbm_b, l2_b = matmul_tiled_levels(M, N, K, hw, dtype)
387+ comm = (0.0, 0.0)
388+ if n_dev > 1:
389+ comm = (hw.interconnect_bw, allreduce_bytes(K * N * DTYPE_BYTES[dtype], n_dev))
390+ peak = hw.cube_peak(dtype)
391+ r = solve_mfu_multilevel(flops, peak, [(hw.hbm_bw, hbm_b), (hw.l2_bw, l2_b)], comm, eta_util)
392+ r.update({"op": f"matmul[{M}x{N}x{K}]", "hw": hw.name, "dtype": dtype})
393+ return r
@@ -0,0 +1,229 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""MFU 优化信号:给 ko/fo agent 用。输入算子实测(shape/dtype/device-time)+硬件目标,
14+输出 {current_mfu, achievable_mfu, gap, bottleneck, lever, done} —— 让优化器以
15+'逼近 achievable MFU' 为迭代目标/停止判据, 替代纯 ratio≥1.12x / 5-iter 的相对指标。
16+ 
17+用法(库): from optimizer_signal import mfu_signal
18+ sig = mfu_signal(flops, hbm_bytes, device_us, hw_name='910C_die', dtype='fp16',
19+ op_kind='matmul', n_aic_used=1)
20+done 判据: sig['done'] == (current_mfu >= tau*achievable_mfu) 或 杠杆耗尽。
21+"""
22+import sys
23+import json
24+from dataclasses import dataclass
25+ 
26+from mfu_model import HW, ETA_UTIL, achievable_mfu_with_overhead, DTYPE_BYTES
27+ 
28+ 
29+# op-class -> compute engine. **准度关键(main review 2026-07-01)**: peak 必须按算子类取对。
30+# matmul/FA/fused/conv -> CUBE peak; elementwise/softmax/norm/reduce/vector -> VEC peak。
31+# 用错 peak(matmul 拿 VEC) => MFU 错 => headroom 错 (roofline_eval 犯过, PR#400 修)。
32+OP_ENGINE = {
33+ "matmul": "cube", "flash_attention": "cube", "fused": "cube", "conv": "cube",
34+ "elementwise": "vector", "softmax": "vector", "rmsnorm": "vector",
35+ "layernorm": "vector", "reduce": "vector", "vector": "vector",
36+}
37+ 
38+ 
39+def _peak_for_op_class(hw, op_kind, dtype):
40+ """按算子类取正确的引擎 peak。fail-loud: vec 算子缺 dtype peak 就报错, 绝不静默回落 cube
41+ (静默回落=main 警告的 PR#400-类 bug: 错 peak 错 headroom)。"""
42+ engine = OP_ENGINE.get(op_kind)
43+ if engine is None:
44+ raise ValueError(f"unknown op_kind '{op_kind}' — 显式加进 OP_ENGINE 并确认引擎(cube/vector)")
45+ if engine == "cube":
46+ return hw.cube_peak(dtype)
47+ # vector
48+ if dtype not in hw.vec_peak:
49+ raise ValueError(f"{hw.name}: vector op '{op_kind}' 无 dtype '{dtype}' 的 vec_peak — "
50+ f"须先标定(不静默回落cube, 否则错估 headroom)")
51+ return hw.vec_peak[dtype]
52+ 
53+ 
54+@dataclass(frozen=True)
55+class _MfuMetrics:
56+ """MFU 信号计算中与输入形状无关的中间度量。"""
57+ 
58+ hw: object
59+ peak: float
60+ elapsed_s: float
61+ current_tflops: float
62+ current_mfu: float
63+ achievable: float
64+ overhead_bound: bool
65+ 
66+ 
67+def _calculate_metrics(flops, device_us, hw_name, dtype, op_kind, t_overhead_us):
68+ """计算当前 MFU、可达 MFU 与固定开销边界标记。"""
69+ hw = HW[hw_name]
70+ peak = _peak_for_op_class(hw, op_kind, dtype)
71+ elapsed_s = device_us * 1e-6
72+ current_tflops = flops / elapsed_s / 1e12
73+ current_mfu = flops / (peak * elapsed_s)
74+ t_compute_ideal = flops / peak
75+ overhead_s = t_overhead_us * 1e-6 if t_overhead_us is not None else 0.0
76+ achievable = achievable_mfu_with_overhead(
77+ flops,
78+ peak,
79+ op_kind,
80+ hw_name,
81+ t_overhead_s=overhead_s,
82+ default_eta=0.85,
83+ )["achievable_mfu"]
84+ return _MfuMetrics(
85+ hw=hw,
86+ peak=peak,
87+ elapsed_s=elapsed_s,
88+ current_tflops=current_tflops,
89+ current_mfu=current_mfu,
90+ achievable=achievable,
91+ overhead_bound=t_compute_ideal / elapsed_s < 0.1,
92+ )
93+ 
94+ 
95+def _classify_bottleneck(flops, hbm_bytes, comm_bytes, metrics):
96+ """按 roofline 和通信时间确定当前的主要瓶颈。"""
97+ arithmetic_intensity = flops / hbm_bytes if hbm_bytes else float("inf")
98+ ridge = metrics.peak / metrics.hw.hbm_bw
99+ if (
100+ comm_bytes
101+ and metrics.hw.interconnect_bw
102+ and comm_bytes / metrics.hw.interconnect_bw > metrics.elapsed_s * 0.5
103+ ):
104+ return "comm", arithmetic_intensity, ridge
105+ if arithmetic_intensity < ridge:
106+ return "memory", arithmetic_intensity, ridge
107+ return "compute", arithmetic_intensity, ridge
108+ 
109+ 
110+def _direction_advice(
111+ op_kind,
112+ n_aic_used,
113+ comm_bytes,
114+ bottleneck,
115+ arithmetic_intensity,
116+ ridge,
117+ metrics,
118+ tau,
119+):
120+ """只为已验证的 Cube 算子产生具体优化方向。"""
121+ engine = OP_ENGINE.get(op_kind)
122+ validated_direction = op_kind in ("matmul", "mm_grad", "flash_attention", "fused")
123+ if not validated_direction:
124+ note = (
125+ f"⚠方向弃权: op-class '{op_kind}' 未验证 —— roofline 对 vector/issue-bound "
126+ "会误分类(busy-ratio≠bound; 盲 issue-count+latency-hiding/double-buffer). "
127+ "只给 ceiling/stop; 定 bound + lever 需实测 msprof pipe-ratio."
128+ )
129+ return [], note, False
130+ 
131+ levers = []
132+ if engine == "cube" and n_aic_used is not None and n_aic_used < metrics.hw.n_aic:
133+ multiplier = metrics.hw.n_aic / n_aic_used
134+ levers.append(
135+ f"multi-AIC: blockDim {n_aic_used}->{metrics.hw.n_aic} "
136+ f"(~{multiplier:.0f}x, P-P74 M/N tiling)"
137+ )
138+ if bottleneck == "memory":
139+ levers.append(
140+ f"raise arithmetic intensity (AI={arithmetic_intensity:.0f}<ridge={ridge:.0f}): "
141+ "增大tile/合并/算子融合"
142+ )
143+ if bottleneck == "compute" and metrics.current_mfu < metrics.achievable * tau:
144+ levers.append("compute-bound但低于achievable: 查tiling/尾块/流水(η gap)")
145+ if comm_bytes and bottleneck == "comm":
146+ levers.append("comm-bound: 梯度累积/通信压缩/通算重叠")
147+ return levers, None, True
148+ 
149+ 
150+def _build_verdict(metrics, tau, applicable_direction, direction_note, levers):
151+ """保持 ceiling-based 停止判据和面向调用方的 verdict 文本。"""
152+ gap = metrics.current_mfu / metrics.achievable if metrics.achievable else 0.0
153+ done = metrics.current_mfu >= tau * metrics.achievable
154+ if not applicable_direction:
155+ verdict = (
156+ f"ABSTAIN(direction): at {metrics.current_mfu * 100:.1f}% MFU, "
157+ f"{gap * 100:.0f}% of achievable; ceiling given, lever withheld — {direction_note}"
158+ )
159+ elif done:
160+ verdict = "DONE (≥%.0f%% achievable)" % (tau * 100)
161+ else:
162+ verdict = "CONTINUE: at %.1f%% MFU, %.0f%% of achievable — %s" % (
163+ metrics.current_mfu * 100,
164+ gap * 100,
165+ levers[0] if levers else "?",
166+ )
167+ return gap, done, verdict
168+ 
169+ 
170+def mfu_signal(flops, hbm_bytes, device_us, hw_name="910C_die", dtype="fp16",
171+ op_kind="matmul", n_aic_used=None, comm_bytes=0.0, tau=0.8,
172+ t_overhead_us=None):
173+ """返回优化信号 dict。n_aic_used: 实际用的 cube 核数(单AIC算子=1),用于诊断 multi-AIC 杠杆。
174+ t_overhead_us: launch/tail 固定开销(若知)。**iteration-2 教训(2026-06-30)**: 小/overhead-bound
175+ 算子用 flat η 会 over-state headroom(mm_grad tiny shape: compute 0.74us 但跑 67us, vendor 也仅
176+ 1.1% MFU -> 真 achievable≈1.1% 非 88%)。传 t_overhead_us 后用 overhead-corrected achievable,
177+ 避免在 overhead-bound 算子上追幻影 headroom。
178+ """
179+ metrics = _calculate_metrics(flops, device_us, hw_name, dtype, op_kind, t_overhead_us)
180+ bottleneck, arithmetic_intensity, ridge = _classify_bottleneck(
181+ flops,
182+ hbm_bytes,
183+ comm_bytes,
184+ metrics,
185+ )
186+ levers, direction_note, applicable_direction = _direction_advice(
187+ op_kind,
188+ n_aic_used,
189+ comm_bytes,
190+ bottleneck,
191+ arithmetic_intensity,
192+ ridge,
193+ metrics,
194+ tau,
195+ )
196+ if metrics.overhead_bound and t_overhead_us is None:
197+ levers = ["⚠overhead-bound (compute<10%实测): 传 t_overhead_us 才能得真achievable; "
198+ "flat-η会over-state headroom(iter2教训)"] + levers
199+ gap, done, verdict = _build_verdict(
200+ metrics,
201+ tau,
202+ applicable_direction,
203+ direction_note,
204+ levers,
205+ )
206+ return {
207+ "current_mfu": round(metrics.current_mfu, 4),
208+ "current_TFLOPS": round(metrics.current_tflops, 1),
209+ "achievable_mfu": round(metrics.achievable, 4),
210+ "peak_TFLOPS": round(metrics.peak / 1e12, 1),
211+ "gap_to_achievable": round(gap, 3),
212+ "bottleneck": bottleneck,
213+ "overhead_bound": metrics.overhead_bound,
214+ "applicable_direction": applicable_direction,
215+ "direction_note": direction_note,
216+ "levers": levers,
217+ "done": done,
218+ "verdict": verdict,
219+ }
220+ 
221+ 
222+if __name__ == "__main__":
223+ # demo: mm_grad [512,256,512] fp16 单AIC, 78us, 2 GEMM
224+ M, K, N = 512, 256, 512
225+ flops = 2 * M * K * N * 2
226+ hbm = (M * K + K * N + M * N) * DTYPE_BYTES["fp16"] * 2
227+ sig = mfu_signal(flops, hbm, 78.0, "910C_die", "fp16", "matmul", n_aic_used=1)
228+ print("mm_grad[512,256,512] fp16 单AIC 78us:")
229+ print(json.dumps(sig, ensure_ascii=False, indent=2))
@@ -0,0 +1,268 @@
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+"""optimizer_signal / mfu_model 的测试。验证 MFU 信号对已知输入给出正确的
13+current_mfu / achievable / bottleneck / lever / done 判定。
14+ 
15+run: python3 mfu/test_optimizer_signal.py (exit 0 = all pass)
16+"""
17+import logging
18+import sys
19+import os
20+from pathlib import Path
21+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22+from mfu_model import HW, DTYPE_BYTES, flash_attention_ops, solve_mfu, matmul_ops
23+from optimizer_signal import mfu_signal
24+ 
25+FAILS = []
26+LOGGER = logging.getLogger(__name__)
27+ 
28+ 
29+def check(name, cond, detail=""):
30+ LOGGER.info(" [%s] %s%s", "PASS" if cond else "FAIL", name,
31+ f" — {detail}" if detail and not cond else "")
32+ if not cond:
33+ FAILS.append(name)
34+ 
35+ 
36+def test_mm_grad_single_aic_flags_continue():
37+ """mm_grad 单AIC shipped op: 必须判 CONTINUE + 低 MFU + multi-AIC 杠杆(owner实证)。"""
38+ M, K, N = 512, 256, 512
39+ flops = 2 * M * K * N * 2
40+ hbm = (M * K + K * N + M * N) * DTYPE_BYTES["fp16"] * 2
41+ s = mfu_signal(flops, hbm, 78.0, "910C_die", "fp16", "matmul", n_aic_used=1)
42+ check("mm_grad current_mfu<5%", s["current_mfu"] < 0.05, f"got {s['current_mfu']}")
43+ check("mm_grad NOT done", s["done"] is False)
44+ check("mm_grad lever 含 multi-AIC", any("multi-AIC" in l for l in s["levers"]))
45+ check("mm_grad gap<<1", s["gap_to_achievable"] < 0.1, f"got {s['gap_to_achievable']}")
46+ 
47+ 
48+def test_near_ceiling_matmul_is_done():
49+ """充分优化的大 matmul (用满24AIC, 实测≈理论×η): 应判 done。"""
50+ M = K = N = 4096
51+ flops = 2 * M * K * N
52+ hbm = (M * K + K * N + M * N) * DTYPE_BYTES["bf16"]
53+ peak = HW["910C_die"].cube_peak("bf16")
54+ # 实测时间 = 理论compute/0.88 (即 η=0.88, 接近 achievable) -> 满24AIC
55+ us = flops / peak / 0.88 * 1e6
56+ s = mfu_signal(flops, hbm, us, "910C_die", "bf16", "matmul", n_aic_used=24)
57+ check("near-ceiling done", s["done"] is True, f"mfu={s['current_mfu']} ach={s['achievable_mfu']}")
58+ check("near-ceiling current≈achievable", s["gap_to_achievable"] >= 0.8, f"got {s['gap_to_achievable']}")
59+ 
60+ 
61+def test_memory_bound_op_flags_AI_lever():
62+ """memory-bound 算子(瘦GEMV): 必须判 memory + AI 杠杆。"""
63+ M, K, N = 1, 4096, 4096
64+ flops = 2 * M * K * N
65+ hbm = (M * K + K * N + M * N) * DTYPE_BYTES["bf16"]
66+ us = hbm / HW["910C_die"].hbm_bw * 1e6 # 跑在 HBM 带宽上限
67+ s = mfu_signal(flops, hbm, us, "910C_die", "bf16", "matmul", n_aic_used=24)
68+ check("GEMV bottleneck=memory", s["bottleneck"] == "memory", f"got {s['bottleneck']}")
69+ check("GEMV lever 含 arithmetic intensity", any("arithmetic" in l or "AI" in l for l in s["levers"]))
70+ 
71+ 
72+def test_ceiling_never_exceeded():
73+ """天花板不被突破: 任何实测的 current_mfu 不应 > 1.0 (除非time太小=峰值口径错)。"""
74+ # 用实测 A3 matmul 321 TFLOPS (n=4096 bf16) vs 理论 363
75+ M = K = N = 4096
76+ flops = 2 * M * K * N
77+ us = flops / 321e12 * 1e6
78+ s = mfu_signal(flops, (M * K + K * N + M * N) * 2, us, "910C_die", "bf16", "matmul", n_aic_used=24)
79+ check("A3实测matmul MFU≈0.88", abs(s["current_mfu"] - 0.88) < 0.05, f"got {s['current_mfu']}")
80+ check("天花板不破 current_mfu<=1.0", s["current_mfu"] <= 1.0)
81+ 
82+ 
83+def test_model_matches_kb_fa_case():
84+ """模型对 KB 实测 FA case (B1H8S2048D128 fp16 causal): compute-bound, AI≈512, ceiling≈17us。"""
85+ fwd, _ = flash_attention_ops(1, 8, 2048, 128, "fp16", causal=True)
86+ r = solve_mfu(fwd, HW["950PR"])
87+ check("KB-FA compute-bound", r["bottleneck"] == "compute", f"got {r['bottleneck']}")
88+ check("KB-FA AI≈512", abs(r["arithmetic_intensity"] - 512) < 5, f"got {r['arithmetic_intensity']}")
89+ 
90+ 
91+def test_overhead_bound_flagged_and_corrected():
92+ """iter2教训: tiny mm_grad shape overhead-bound — 无overhead时须flag警告;
93+ 传t_overhead后achievable下修, 近vendor水平的实测应判done。
94+ """
95+ M, K, N = 512, 256, 512
96+ flops = 2 * M * K * N * 2
97+ hbm = (M * K + K * N + M * N) * DTYPE_BYTES["fp16"] * 2
98+ # 无overhead: 应 flag overhead_bound + 警告lever (compute0.74us«67us)
99+ s = mfu_signal(flops, hbm, 67.2, "910C_die", "fp16", "matmul", n_aic_used=24)
100+ check("overhead_bound flagged", s.get("overhead_bound") is True, f"got {s.get('overhead_bound')}")
101+ check("warns flat-η over-states", any("overhead-bound" in l for l in s["levers"]))
102+ # 传t_overhead=65us: achievable下修到~vendor水平, v2(1.1%)≈achievable -> done
103+ s2 = mfu_signal(flops, hbm, 67.2, "910C_die", "fp16", "matmul", n_aic_used=24, t_overhead_us=65.0)
104+ check("overhead-corrected achievable low", s2["achievable_mfu"] < 0.05, f"got {s2['achievable_mfu']}")
105+ check("near-vendor tiny op -> done", s2["done"] is True, f"mfu={s2['current_mfu']} ach={s2['achievable_mfu']}")
106+ 
107+ 
108+def test_peak_source_per_op_class():
109+ """main accuracy-critical gate: peak 必须按算子类取对。matmul->CUBE, elementwise->VEC。
110+ 用错 peak => MFU 错 => headroom 错 (PR#400-类 bug)。且 vec 缺 dtype 须 fail-loud 不静默回落 cube。
111+ """
112+ from optimizer_signal import _peak_for_op_class
113+ hw = HW["950PR"]
114+ cube = hw.cube_peak("fp16") # ~378 TF
115+ vec = hw.vec_peak["fp16"] # 54 TF
116+ check("matmul->CUBE peak", _peak_for_op_class(hw, "matmul", "fp16") == cube)
117+ check("elementwise->VEC peak", _peak_for_op_class(hw, "elementwise", "fp16") == vec)
118+ check("cube!=vec (否则测试无意义)", cube != vec)
119+ # fail-loud: vector op 缺 dtype 的 vec_peak 不静默回落 cube
120+ raised = False
121+ try:
122+ _peak_for_op_class(hw, "softmax", "fp8") # fp8 不在 vec_peak
123+ except ValueError:
124+ raised = True
125+ check("vec缺dtype fail-loud(不回落cube)", raised)
126+ # unknown op_kind fail-loud
127+ raised2 = False
128+ try:
129+ _peak_for_op_class(hw, "mystery_op", "fp16")
130+ except ValueError:
131+ raised2 = True
132+ check("unknown op_kind fail-loud", raised2)
133+ 
134+ 
135+def test_eta_provenance_present():
136+ """main纪律: 每个标定的 η 须有 provenance(来源+日期+revalidate触发), 防 stale-η 错估。"""
137+ from mfu_model import eta_with_provenance
138+ 
139+ cases = [
140+ ("matmul", "910C_die"),
141+ ("matmul", "950PR"),
142+ ("flash_attention", "910C_die"),
143+ ("flash_attention", "950PR"),
144+ ]
145+ for op, hw in cases:
146+ eta, prov = eta_with_provenance(op, hw)
147+ check(
148+ f"{op}/{hw} η有provenance",
149+ prov is not None and "measured" in prov and "revalidate_on" in prov,
150+ f"prov={prov}",
151+ )
152+ 
153+ 
154+def test_verification_hook_injects_mfu_ceiling():
155+ """机械注入: hook 读 verification.json -> 写 mfu_ceiling 块(ko 读的字段)。
156+ 验证低-MFU op 被正确标 CONTINUE + multi-AIC lever, 且 mfu_ceiling 进了 json。
157+ """
158+ import json
159+ import tempfile
160+ from verification_hook import inject_mfu_ceiling
161+ # 造一个最小 verification.json(mm_grad 单AIC 实测形态)
162+ vj = {"op": "mm_grad",
163+ "precision": {"cases": [{"MKN": [512, 256, 512], "dtype": "float16"}]},
164+ "performance": {"ours_us_two_runs": {"[512, 256, 512]": [76.6, 76.6]}}}
165+ fd, p = tempfile.mkstemp(suffix=".json")
166+ os.close(fd)
167+ Path(p).write_text(json.dumps(vj), encoding="utf-8")
168+ c = inject_mfu_ceiling(p, op_kind="mm_grad", hw_name="910C_die", n_aic_used=1)
169+ reloaded = json.loads(Path(p).read_text(encoding="utf-8"))
170+ os.unlink(p)
171+ check("mfu_ceiling written into json", "mfu_ceiling" in reloaded)
172+ sig = c["per_case"].get("[512,256,512]") or list(c["per_case"].values())[0]
173+ check("hook flags low-MFU CONTINUE", sig["done"] is False, f"got {sig}")
174+ check("hook surfaces multi-AIC lever", any("multi-AIC" in l for l in sig["levers"]))
175+ check("hook current_mfu ~1%", sig["current_mfu"] < 0.05, f"got {sig['current_mfu']}")
176+ 
177+ 
178+def test_pipeline_injection_via_phase_o5():
179+ """真链入(main gate 要求): phase_o5 perf 写完 verification.json 后, _inject_mfu_ceiling_safe
180+ 机械注入 mfu_ceiling — 非死代码. op_kind 从 op-class 取(非写死). fail-safe.
181+ """
182+ import json
183+ import tempfile
184+ import shutil
185+ import sys as _s
186+ root = Path(__file__).resolve().parents[1]
187+ _s.path.insert(0, str(root / "src/scripts/orchestrator"))
188+ try:
189+ import phase_o5
190+ except Exception as e:
191+ check("phase_o5 importable", False, f"import failed: {e!r}")
192+ return
193+ ws = Path(tempfile.mkdtemp())
194+ try:
195+ vj = {"op": "mm_grad", "target": "a3", "soc_version": "Ascend910_9392",
196+ "precision": {"cases": [{"MKN": [512, 256, 512], "dtype": "float16"}]},
197+ "performance": {"ours_us_two_runs": {"[512, 256, 512]": [76.6, 76.6]}}}
198+ vp = ws / "verification.json"
199+ vp.write_text(json.dumps(vj))
200+ (ws / "op_classification.json").write_text(json.dumps({"class": "matmul_backward"}))
201+ check("op_kind from op-class (not hardcoded)", getattr(phase_o5, "_mfu_op_kind")(ws, "mm_grad") == "mm_grad")
202+ getattr(phase_o5, "_inject_mfu_ceiling_safe")(vp, ws, "mm_grad", vj)
203+ out = json.loads(vp.read_text())
204+ check("pipeline writes mfu_ceiling (not dead code)", "mfu_ceiling" in out)
205+ pc = out.get("mfu_ceiling", {}).get("per_case", {})
206+ sig = list(pc.values())[0] if pc else {}
207+ check("pipeline per_case has done-signal", sig.get("done") is False, f"got {sig}")
208+ # fail-safe: garbage verification never raises
209+ bad = ws / "bad.json"
210+ bad.write_text("{not json")
211+ getattr(phase_o5, "_inject_mfu_ceiling_safe")(bad, ws, "mm_grad", {}) # must not raise
212+ check("fail-safe on bad input (no raise)", True)
213+ finally:
214+ shutil.rmtree(ws)
215+ 
216+ 
217+def test_applicability_gate_abstains_on_vector():
218+ """owner-approved 安全 gate: vector/未验证 op-class -> 方向弃权(不发误导 lever), 仍给 ceiling/stop.
219+ 复现 selective_scan 场景: 绝不出 raise-AI/fuse/multi-AIC(误导).
220+ """
221+ # selective_scan-like vector op (real numbers from independent review)
222+ flops = int(1.72e9)
223+ hbm = 225 * 2**20
224+ s = mfu_signal(flops, hbm, 7134.0, "950PR", "fp16", "softmax", n_aic_used=1)
225+ check("vector op -> applicable_direction=False",
226+ s["applicable_direction"] is False, f"got {s.get('applicable_direction')}")
227+ check("vector op -> NO misleading lever emitted", not any(("raise arithmetic" in l or "multi-AIC" in l)
228+ for l in s["levers"]), f"levers={s['levers']}")
229+ check("vector op -> direction_note explains abstain", s.get("direction_note") is not None)
230+ check("vector op -> verdict says ABSTAIN", "ABSTAIN" in s["verdict"])
231+ check("vector op -> ceiling still given (current_mfu present)", s["current_mfu"] > 0)
232+ # validated op (matmul) still emits levers (gate doesn't over-suppress)
233+ s2 = mfu_signal(2 * 512 * 256 * 512, (512 * 256 + 256 * 512 + 512 * 512)
234+ * 2, 76.6, "910C_die", "fp16", "matmul", n_aic_used=1)
235+ check("matmul still applicable_direction=True", s2["applicable_direction"] is True)
236+ check("matmul still emits multi-AIC lever", any("multi-AIC" in l for l in s2["levers"]))
237+ 
238+ 
239+def test_multi_aic_lever_engine_aware():
240+ """multi-AIC lever 绝不出在 vector(AIV) 算子上(selective_scan bug: cube-28x 错用在 aic=0 的 AIV 算子)."""
241+ # 即便强行传 vector op + n_aic_used, 也因弃权不发 multi-AIC
242+ s = mfu_signal(int(1.72e9), 225 * 2**20, 7134.0, "950PR", "fp16", "elementwise", n_aic_used=1)
243+ check("vector op never gets cube multi-AIC lever",
244+ not any("multi-AIC" in l for l in s["levers"]), f"levers={s['levers']}")
245+ 
246+ 
247+def test_matmul_flops_accounting():
248+ """matmul fwd/bwd FLOPs 记账正确: bwd=2×fwd。"""
249+ fwd, bwd = matmul_ops(512, 512, 512, "bf16")
250+ check("matmul fwd flops=2MNK", fwd.flops == 2 * 512**3)
251+ check("matmul bwd flops=4MNK(2x fwd)", bwd.flops == 4 * 512**3)
252+ 
253+ 
254+if __name__ == "__main__":
255+ for t in [test_mm_grad_single_aic_flags_continue, test_near_ceiling_matmul_is_done,
256+ test_memory_bound_op_flags_AI_lever, test_ceiling_never_exceeded,
257+ test_model_matches_kb_fa_case, test_overhead_bound_flagged_and_corrected,
258+ test_peak_source_per_op_class, test_eta_provenance_present,
259+ test_verification_hook_injects_mfu_ceiling,
260+ test_pipeline_injection_via_phase_o5,
261+ test_applicability_gate_abstains_on_vector,
262+ test_multi_aic_lever_engine_aware,
263+ test_matmul_flops_accounting]:
264+ LOGGER.info("%s:", t.__name__)
265+ t()
266+ LOGGER.info("%s\n%s", "=" * 50,
267+ "ALL PASS" if not FAILS else f"{len(FAILS)} FAILED: {FAILS}")
268+ sys.exit(1 if FAILS else 0)
@@ -0,0 +1,73 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""验证harness: 模型预测 MFU_max vs msprof 实测 -> η_util + 预测准确性表。
14+NPU 到位前用示例数据跑通格式; 拿到 msprof 后把 measured_us 换成实测即可。
15+ 
16+用法:
17+ python3 compare.py # 跑示例(展示对照表格式)
18+ # 真机: 解析 msprof 输出得各算子 device time, 填入 CASES 的 measured_us
19+"""
20+import sys
21+import os
22+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
23+from mfu_model import HW, flash_attention_ops, matmul_ops, solve_mfu, solve_matmul_tiled
24+ 
25+ 
26+def predict(case):
27+ """返回模型预测 (mfu_max, bottleneck, t_ideal_us)。"""
28+ hw = HW[case["hw"]]
29+ k = case["kind"]
30+ if k == "matmul":
31+ r = solve_matmul_tiled(case["M"], case["N"], case["K"], hw, case["dtype"])
32+ t_ideal = min(r["t_us"].values()) # t_compute (理想)
33+ return r["mfu_max"], r["bottleneck"], r["t_us"].get("compute", t_ideal)
34+ elif k == "fa":
35+ fwd, _ = flash_attention_ops(case["B"], case["H"], case["S"], case["D"],
36+ case["dtype"], case.get("causal", False))
37+ r = solve_mfu(fwd, hw)
38+ return r["mfu_max"], r["bottleneck"], r["t_compute_us"]
39+ raise ValueError(k)
40+ 
41+ 
42+def report(cases):
43+ print(f"{'case':32} {'hw':8} {'pred_MFU':>8} {'bottleneck':>11} "
44+ f"{'t_ideal_us':>10} {'meas_us':>9} {'meas_MFU':>8} {'eta':>6}")
45+ for c in cases:
46+ mfu_max, bn, t_ideal = predict(c)
47+ meas = c.get("measured_us")
48+ if meas:
49+ meas_mfu = round(t_ideal / meas * mfu_max, 3) # 实测MFU = 理论compute时间/实测墙钟 × 理论MFU基
50+ eta = round(meas_mfu / mfu_max, 3) if mfu_max else 0
51+ ok = "" if meas >= t_ideal - 1e-6 else " ⚠天花板被突破!"
52+ else:
53+ meas_mfu = eta = "-"
54+ ok = ""
55+ print(f"{c['name']:32} {c['hw']:8} {mfu_max:>8} {bn:>11} "
56+ f"{round(t_ideal,2):>10} {str(meas):>9} {str(meas_mfu):>8} {str(eta):>6}{ok}")
57+ print("\nη_util = 实测MFU / 理论MFU_max (有效利用率); <1 正常(tiling/尾块/流水损耗)。"
58+ "\n实测>理论(天花板被突破)=> 模型 peak/BW 口径偏低, 需修。")
59+ 
60+ 
61+# 待 msprof 填 measured_us 的 case 列表(measured_us=None 占位)
62+CASES = [
63+ {"name": "FA dense B1H8S2048D128 causal", "kind": "fa", "hw": "950PR",
64+ "B": 1, "H": 8, "S": 2048, "D": 128, "dtype": "fp16", "causal": True,
65+ "measured_us": 17.0}, # KB 实测 ceiling 0.017ms (示例: 接近理论 -> η≈1)
66+ {"name": "matmul 4096^3 bf16", "kind": "matmul", "hw": "950PR",
67+ "M": 4096, "N": 4096, "K": 4096, "dtype": "bf16", "measured_us": None},
68+ {"name": "matmul 4096^3 bf16", "kind": "matmul", "hw": "910C",
69+ "M": 4096, "N": 4096, "K": 4096, "dtype": "bf16", "measured_us": None},
70+]
71+ 
72+if __name__ == "__main__":
73+ report(CASES)
@@ -0,0 +1,111 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""把 MFU 天花板信号**机械注入** verification.json —— ko/fo 读的正是这个文件。
14+这样 MFU 不是"agent 记得跑脚本才用",而是**每次 perf 后自动进 ko 读的数据**(owner:
15+建模若 ko/fo 不用就等于没用 -> 把它变成 ko 必然读到的字段)。
16+ 
17+集成点(pipeline):
18+ perf 步骤产出 verification.json 后 -> 调 inject_mfu_ceiling() -> 写入 mfu_ceiling 块 ->
19+ ko/fo 的 done 判据读 verification.json['mfu_ceiling'](不依赖 agent 记得跑脚本)。
20+ 
21+FLOPs 是 op-语义: 优先读 verification 里 perf-case 的 'flops' 字段(perf 步骤知道 op 时填);
22+无则用 builtin 公式(matmul/mm_grad); 都没有则该 case skip 并记原因(fail-safe, 不静默造假)。
23+"""
24+import json
25+import sys
26+import statistics
27+sys.path.insert(0, __file__.rsplit("/", 1)[0])
28+from optimizer_signal import mfu_signal
29+from mfu_model import DTYPE_BYTES
30+ 
31+# builtin FLOPs 公式(op_kind -> f(M,K,N)). 扩展新 op 在此加, 或让 perf 步骤直接填 flops 字段。
32+BUILTIN_FLOPS = {
33+ "matmul": lambda M, K, N: 2 * M * K * N,
34+ "mm_grad": lambda M, K, N: 2 * M * K * N * 2, # dA + dB 两个 GEMM
35+}
36+BUILTIN_HBM = { # 最小 HBM 字节(理想), 用于 memory-bound 判据
37+ "matmul": lambda M, K, N, sz: (M * K + K * N + M * N) * sz,
38+ "mm_grad": lambda M, K, N, sz: (M * N + K * N + M * K + M * K + M * N + K * N) * sz,
39+}
40+ 
41+ 
42+def _parse_mkn(shape_key):
43+ """'[512, 256, 512]' -> (512,256,512)."""
44+ return tuple(int(x) for x in shape_key.strip("[]").split(","))
45+ 
46+ 
47+def inject_mfu_ceiling(vjson_path, op_kind="matmul", hw_name="910C_die",
48+ n_aic_used=None, t_overhead_us=None, engine_op_kind=None,
49+ write=True):
50+ """读 verification.json -> 每个 perf case 算 MFU 信号 -> 写入 mfu_ceiling 块。
51+ engine_op_kind: 传给 mfu_signal 的算子类(选 CUBE/VEC peak); 默认同 op_kind。
52+ 返回注入的 mfu_ceiling dict。
53+ """
54+ with open(vjson_path) as f:
55+ v = json.load(f)
56+ perf = v.get("performance", {})
57+ ours = perf.get("ours_us_two_runs") or perf.get("ours_us") or {}
58+ # dtype: 从 precision.cases 按 MKN 匹配, 缺省 fp16
59+ dtype_by_mkn = {}
60+ for c in v.get("precision", {}).get("cases", []):
61+ mkn = tuple(c.get("MKN", [])) if "MKN" in c else None
62+ if mkn:
63+ dtype_by_mkn[mkn] = c.get("dtype", "fp16")
64+ 
65+ eok = engine_op_kind or ("matmul" if op_kind in ("matmul", "mm_grad") else op_kind)
66+ per_case, all_done, skipped = {}, [], []
67+ for shape_key, us_val in ours.items():
68+ try:
69+ M, K, N = _parse_mkn(shape_key)
70+ except Exception:
71+ skipped.append((shape_key, "unparseable shape"))
72+ continue
73+ us = statistics.median(us_val) if isinstance(us_val, list) else float(us_val)
74+ dt = dtype_by_mkn.get((M, K, N), "fp16")
75+ dtl = {"float16": "fp16", "float32": "fp32", "bfloat16": "bf16"}.get(dt, dt)
76+ # flops: 优先 case 内 'flops', 否则 builtin
77+ ff = BUILTIN_FLOPS.get(op_kind)
78+ if ff is None:
79+ skipped.append((shape_key, f"no flops formula for op_kind '{op_kind}'"))
80+ continue
81+ flops = ff(M, K, N)
82+ hf = BUILTIN_HBM.get(op_kind, BUILTIN_HBM["matmul"])
83+ hbm = hf(M, K, N, DTYPE_BYTES.get(dtl, 2))
84+ sig = mfu_signal(flops, hbm, us, hw_name, dtl, eok,
85+ n_aic_used=n_aic_used, t_overhead_us=t_overhead_us)
86+ per_case[shape_key] = sig
87+ all_done.append(sig["done"])
88+ 
89+ ceiling = {
90+ "generated_by": "mfu/verification_hook.py (mechanical, per perf step)",
91+ "hw": hw_name, "op_kind": op_kind, "engine": eok,
92+ "per_case": per_case,
93+ "all_cases_done": bool(all_done) and all(all_done),
94+ "verdict": ("ALL cases >= 0.8*achievable -> perf DONE (ceiling-aware)"
95+ if (all_done and all(all_done))
96+ else "CONTINUE: some case below achievable ceiling (see per_case levers)"),
97+ "skipped": skipped,
98+ "note": "ko/fo 读此块作 MFU-gated done; 非仅 ratio. peak 按 op_kind 引擎取(fail-loud).",
99+ }
100+ v["mfu_ceiling"] = ceiling
101+ if write:
102+ with open(vjson_path, "w") as f:
103+ json.dump(v, f, ensure_ascii=False, indent=2)
104+ return ceiling
105+ 
106+ 
107+if __name__ == "__main__":
108+ # demo/self-test on a copy of a real verification.json
109+ path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/_mmgrad_vj_demo.json"
110+ c = inject_mfu_ceiling(path, op_kind="mm_grad", hw_name="910C_die", n_aic_used=1)
111+ print(json.dumps(c, ensure_ascii=False, indent=2))
@@ -0,0 +1,124 @@
1+[
2+ {
3+ "check": "CORE_MODE_LEAK",
4+ "file": "src/scripts/orchestrator/agent_dispatch.py",
5+ "mode": "port_a3_to_a5"
6+ },
7+ {
8+ "check": "CORE_MODE_LEAK",
9+ "file": "src/scripts/orchestrator/finalize_checks_precision.py",
10+ "mode": "port_a3_to_a5"
11+ },
12+ {
13+ "check": "CORE_MODE_LEAK",
14+ "file": "src/scripts/orchestrator/orchestrator.py",
15+ "mode": "port_a3_to_a5"
16+ },
17+ {
18+ "check": "CORE_MODE_LEAK",
19+ "file": "src/scripts/orchestrator/orchestrator_cmds.py",
20+ "mode": "port_a3_to_a5"
21+ },
22+ {
23+ "check": "CORE_MODE_LEAK",
24+ "file": "src/scripts/workflow/batch_dispatcher.py",
25+ "mode": "optimize"
26+ },
27+ {
28+ "check": "GOD_FUNCTION",
29+ "file": "src/scripts/orchestrator/agent_dispatch.py",
30+ "func": "spawn_for_state"
31+ },
32+ {
33+ "check": "GOD_FUNCTION",
34+ "file": "src/scripts/orchestrator/agent_transport.py",
35+ "func": "_run_streaming"
36+ },
37+ {
38+ "check": "GOD_FUNCTION",
39+ "file": "src/scripts/orchestrator/finalize_checks_precision.py",
40+ "func": "_check_perf_methodology"
41+ },
42+ {
43+ "check": "GOD_FUNCTION",
44+ "file": "src/scripts/orchestrator/finalize_dispatch.py",
45+ "func": "check_finalize_eligibility"
46+ },
47+ {
48+ "check": "GOD_FUNCTION",
49+ "file": "src/scripts/orchestrator/finalize_dispatch.py",
50+ "func": "finalize_op"
51+ },
52+ {
53+ "check": "GOD_FUNCTION",
54+ "file": "src/scripts/orchestrator/finalize_ge_ophost.py",
55+ "func": "assemble_ge_ophost"
56+ },
57+ {
58+ "check": "GOD_FUNCTION",
59+ "file": "src/scripts/orchestrator/kb_auto_promote.py",
60+ "func": "run_auto_promote"
61+ },
62+ {
63+ "check": "GOD_FUNCTION",
64+ "file": "src/scripts/orchestrator/orchestrator.py",
65+ "func": "run_single_op"
66+ },
67+ {
68+ "check": "GOD_FUNCTION",
69+ "file": "src/scripts/orchestrator/orchestrator_cli.py",
70+ "func": "main"
71+ },
72+ {
73+ "check": "GOD_FUNCTION",
74+ "file": "src/scripts/orchestrator/orchestrator_cmds.py",
75+ "func": "_cmd_port_a3"
76+ },
77+ {
78+ "check": "GOD_FUNCTION",
79+ "file": "src/scripts/orchestrator/orchestrator_coldstart.py",
80+ "func": "_cold_start_reset_workspace"
81+ },
82+ {
83+ "check": "GOD_FUNCTION",
84+ "file": "src/scripts/orchestrator/phase_o25_a3_ref.py",
85+ "func": "provision_a3_reference"
86+ },
87+ {
88+ "check": "GOD_FUNCTION",
89+ "file": "src/scripts/orchestrator/phase_o25_backward.py",
90+ "func": "provision_backward_reference"
91+ },
92+ {
93+ "check": "GOD_FUNCTION",
94+ "file": "src/scripts/orchestrator/phase_o5_runner.py",
95+ "func": "_resync_workspace_to_container"
96+ },
97+ {
98+ "check": "GOD_FUNCTION",
99+ "file": "src/scripts/orchestrator/phase_o5_runner.py",
100+ "func": "ssh_runner"
101+ },
102+ {
103+ "check": "GOD_FUNCTION",
104+ "file": "src/scripts/orchestrator/phase_o5_verify.py",
105+ "func": "_run_canonical_pass_a"
106+ },
107+ {
108+ "check": "GOD_FUNCTION",
109+ "file": "src/scripts/workflow/fsm_conditions.py",
110+ "func": "eval_condition"
111+ },
112+ {
113+ "check": "GOD_FUNCTION",
114+ "file": "src/scripts/workflow/workflow_critic.py",
115+ "func": "mode_pre_agent_spawn",
116+ "reason": "Existing safety-critical pre-spawn hook; preserve behavior until a dedicated refactor."
117+ },
118+ {
119+ "check": "GOD_FUNCTION",
120+ "file": "src/scripts/workflow/workflow_critic_validators.py",
121+ "func": "validate_phase_o4_state",
122+ "reason": "Existing phase-O4 FSM validator; preserve behavior until a dedicated refactor."
123+ }
124+]
@@ -0,0 +1,8 @@
1+#!/bin/bash
2+# a5_pipeline.sh — Backwards-compatibility wrapper around npu_pipeline.sh (V3.4)
3+# Pre-V3.4 callers expect this script and an A5-only execution path.
4+# We now delegate to npu_pipeline.sh with TARGET forced to a5.
5+#
6+# New code should invoke `bash src/scripts/npu_pipeline.sh <cmd>` directly so
7+# `--target=a3` / TARGET=a3 overrides work without going through this shim.
8+exec env TARGET=a5 bash "$(dirname "$0")/npu_pipeline.sh" "$@"
@@ -0,0 +1,640 @@
1+#!/usr/bin/env python3
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+"""architecture_lint.py — mechanized architecture-conformance checks.
12+ 
13+Implements the deterministic floor of docs/design/HARNESS_DESIGN_PHILOSOPHY.md §5: where a
14+design principle can be checked deterministically, it is a lint rather than left to
15+review attention. This catches *plugin-architecture rot* — the "god-function / a-bunch-of-
16+else" drift the owner flagged (2026-06-20): mode-specific branches leaking into core,
17+and oversized core functions.
18+ 
19+Checks:
20+ 
21+ 1. CORE_MODE_LEAK — a `if/elif <mode|opgen_mode|backend|target> == "<lit>"` branch
22+ inside a CORE module (orchestrator/ top-level + orchestrator/briefs/ + workflow/,
23+ not under plugins/, not a test) means mode/target-specific logic has leaked out of
24+ its plugin. Per HARNESS_DESIGN_PHILOSOPHY §2/§4(a): mode-specific logic belongs IN
25+ the plugin; the core stays mode-agnostic (mirror of OL-160, where the safety net
26+ keys off the canonical contract, not mode-specific names). 2026-06-20 widening
27+ (lint b): the original regex only matched op-gen-mode literals on `mode`; it now
28+ matches ANY string literal on `mode|opgen_mode|backend|target` so `target == "a3"`
29+ and `backend == "ascendc"` are caught too — 0-tolerance for NEW occurrences; the
30+ existing ones are grandfathered into the baseline.
31+ 
32+ 2. PLUGIN_NAME_BRANCH — (lint a) `(_active_)?plugin.name == "<lit>"` branches in core,
33+ the strongest plugin-leak smell (a core module deciding behavior by comparing the
34+ active plugin's identity string instead of calling a plugin method). 0-NEW; the one
35+ existing occurrence (phase_o5_runner.py port_a3_to_a5) is grandfathered as known
36+ debt to convert to a plugin method.
37+ 
38+ 3. GOD_FUNCTION — a function longer than --max-func-lines (default 200) in a core module
39+ is a god-function candidate; per §4(a) depth-over-breadth and general maintainability.
40+ 
41+ 4. PLUGIN_REGISTRY_INCOMPLETE — (lint c) every registered plugin must implement the
42+ BasePlugin required-method set so a NEW backend has to register + implement rather
43+ than being core-if-branched. Derived from the protocol-conformance contract
44+ (plugins/tests/test_protocol_conformance.REQUIRED_METHODS).
45+ 
46+ 5. FSM_HARDCODED_TRANSITION — (lint e) a hardcoded next-state/goto string literal in
47+ workflow/state_machine.py that bypasses the `state_transitions.jsonl`-driven
48+ `next_state()` table (e.g. `next_state = "await_worker"`). Transitions must come from
49+ the YAML/data path (`trans.get("goto")`), never a Python literal. Clean today (0
50+ findings) — a forward ratchet against re-introducing hardcoded routing.
51+ 
52+Re (d) dispatch-complexity: an if/elif-chain-length sub-check was evaluated and SKIPPED —
53+GOD_FUNCTION (function length) already covers the dispatch functions it would flag
54+(workflow_critic._dispatch's mode-elif chain is inside a function already length-bounded),
55+and the widened CORE_MODE_LEAK catches the per-branch literals directly. A separate
56+chain-length metric would double-count the same lines without catching anything new.
57+ 
58+Default is REPORT-ONLY (exit 0) so it can be adopted incrementally — like
59+kb_index_audit.py --report-only. Pass --strict to fail (for pre-commit / CI once the
60+known baseline is burned down). A --baseline file allows grandfathering known findings
61+while preventing NEW ones.
62+ 
63+Usage:
64+ python3 src/scripts/architecture_lint.py # report, exit 0
65+ python3 src/scripts/architecture_lint.py --strict # fail on any finding
66+ python3 src/scripts/architecture_lint.py --json # machine-readable
67+ python3 src/scripts/architecture_lint.py --baseline src/scripts/.arch_lint_baseline.json
68+ python3 src/scripts/architecture_lint.py --regenerate-baseline src/scripts/.arch_lint_baseline.json
69+"""
70+from __future__ import annotations
71+ 
72+import argparse
73+import ast
74+import json
75+import os
76+import re
77+import sys
78+ 
79+# Op-gen modes (the plugin names + re-entry modes). A core branch on any of these = leak.
80+# `optimize` is the --optimize re-entry mode (DEBT-161/PR#11). The staleness-guard test
81+# (test_architecture_lint) asserts this allowlist ⊇ every registered plugin's mode name,
82+# so it cannot silently lag a new backend/mode plugin (OL-160).
83+OPGEN_MODES = ("port_a3_to_a5", "backward", "optimize")
84+# DEBT-161 Batch-2 precision narrowing: a `<ident> == "<lit>"` branch in core is only an
85+# op-gen leak when <lit> is a REAL op-gen mode / backend / arch-target. The identifiers
86+# `mode`/`target` are OVERLOADED for non-op-gen meanings that are NOT leaks:
87+# mode=="foreground" → spawn execution mode (agent_transport)
88+# mode=="auto"/"pre_agent_spawn" → critic phase (workflow_critic)
89+# target=="__from_user_decision__" → FSM magic token (state_machine)
90+# Flagging those was a false positive of the lint-(b) widening. Allowlist-by-known-value
91+# fixes precision; the staleness-guard test prevents the allowlist lagging new values.
92+KNOWN_BACKENDS = ("ascendc",)
93+KNOWN_ARCH_TARGETS = ("a3", "a5")
94+_MODE_LEAK_ALLOWLIST = {
95+ "mode": OPGEN_MODES,
96+ "opgen_mode": OPGEN_MODES,
97+ "backend": KNOWN_BACKENDS,
98+ "target": KNOWN_ARCH_TARGETS,
99+}
100+ 
@@ -0,0 +1,110 @@
1+#!/bin/bash
2+# ----------------------------------------------------------------------------------------------------------
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# ----------------------------------------------------------------------------------------------------------
11+# audit_aiv_count.sh — find hardcoded AIV/core constants across archive kernels
12+#
13+# Motivation (2026-04-27 op#5 Cumsum ko-1 Iter 1):
14+# `CUMSUM_NUM_CORES = 20` was hardcoded in kernel constants. Changing it to 56
15+# (the physical AIV count on Ascend950PR) gave +169% sum-ratio lift in a single
16+# iter. This script finds other archive kernels that may have the same bug.
17+#
18+# Usage:
19+# bash src/scripts/audit_aiv_count.sh # scan default archive
20+# bash src/scripts/audit_aiv_count.sh path/to/kernels # custom root
21+#
22+# Output: groups of (file, line, line-content) for each suspicious hardcoded
23+# constant, with self-test footer confirming the script catches op#5's known
24+# bug pattern.
25+ 
26+set -e
27+ 
28+ARCHIVE_ROOT="${1:-output/a3_to_a5_port/src/kernels}"
29+ 
30+if [ ! -d "$ARCHIVE_ROOT" ]; then
31+ echo "ERROR: archive root not found: $ARCHIVE_ROOT" >&2
32+ exit 2
33+fi
34+ 
35+echo "=== Hardcoded AIV/core constants in $ARCHIVE_ROOT ==="
36+echo ""
37+ 
38+# Pattern matches identifier-like names (NUM_CORES / CORE_COUNT / NUM_AIV /
39+# coreNum / aivNum / blockDim / NPU_NUM / CORE_NUM and op-prefixed variants
40+# like CUMSUM_NUM_CORES) followed by ':=' or '=' and an integer literal.
41+# We KEEP =20 in scope because that's the exact value op#5 had.
42+# We exclude 56 (correct), 1 (single-AIV intentional), 47/48 (occasional
43+# single-AIV core reservation patterns documented in some kernels).
44+PATTERN='\b([A-Za-z_][A-Za-z0-9_]*?)(NUM_CORES|CORE_COUNT|NUM_AIV|aivNum|blockDim|NPU_NUM|CORE_NUM|coreNum|AIVNum)\b\s*[:=]\s*[0-9]+'
45+ 
46+# Collect hits — only AscendC kernel headers and sources; skip build/, probes/, .so files
47+FOUND=0
48+LAST_OP=""
49+ 
50+while IFS= read -r line; do
51+ if [ -z "$line" ]; then continue; fi
52+ # Extract <file>:<lineno>:<content>
53+ file="${line%%:*}"
54+ rest="${line#*:}"
55+ lineno="${rest%%:*}"
56+ content="${rest#*:}"
57+ 
58+ # Get op directory (3 levels up from kernel/*.h typically)
59+ op=$(echo "$file" | sed -E 's|.*/kernels/([^/]+)/.*|\1|')
60+ 
61+ if [ "$op" != "$LAST_OP" ]; then
62+ echo ""
63+ echo "── $op ──"
64+ LAST_OP="$op"
65+ fi
66+ 
67+ # Strip leading ./
68+ file_rel="${file#./}"
69+ echo " $file_rel:$lineno:$(echo "$content" | sed 's/^[[:space:]]*//')"
70+ FOUND=$((FOUND + 1))
71+done < <(
72+ # Two layout patterns coexist in the archive:
73+ # (a) flat: <op>/<op>_kernel.h, <op>/<op>_kernels.cpp (older)
74+ # (b) nested: <op>/kernel/*.h, <op>/kernel/*.cpp (newer)
75+ # We accept either, but reject build/probes/.opt*_bak intermediates.
76+ find "$ARCHIVE_ROOT" \
77+ \( -name "*.h" -o -name "*.cpp" -o -name "*.hpp" \) \
78+ -not -path "*/build/*" \
79+ -not -path "*/probes/*" \
80+ -not -name "*.opt*_bak*" \
81+ -not -name "*.bak*" \
82+ 2>/dev/null \
83+ | xargs grep -EHn "$PATTERN" 2>/dev/null \
84+ | grep -vE '=\s*56\b' \
85+ | grep -vE '=\s*1\b\s*[;,)/]' \
86+ | grep -vE '=\s*48\b' \
87+ | grep -vE '=\s*47\b' \
88+ | sort
89+)
90+ 
91+echo ""
92+echo "=== Total hits: $FOUND ==="
93+echo ""
94+ 
95+# Self-test: verify pattern catches op#5 cumsum's known bug
96+echo "=== Self-test: does the pattern match op#5 cumsum's CUMSUM_NUM_CORES = 20? ==="
97+SELF_TEST_FILE="$ARCHIVE_ROOT/5_Cumsum/kernel/cumsum_kernel.h"
98+SELF_TEST_FILE2="$ARCHIVE_ROOT/5_Cumsum/kernel/cumsum_tiling.h"
99+for tf in "$SELF_TEST_FILE" "$SELF_TEST_FILE2"; do
100+ if [ -f "$tf" ]; then
101+ if grep -EHn "$PATTERN" "$tf" 2>/dev/null | grep -E "CUMSUM_NUM_CORES|NUM_CORES" >/dev/null; then
102+ echo " ✓ pattern matches CUMSUM_NUM_CORES in $(basename $tf)"
103+ fi
104+ fi
105+done
106+ 
107+# Also verify the negative test: post-fix value (=56) should NOT trigger
108+echo ""
109+echo "=== Sanity: would the filter HIDE a corrected =56? (it should) ==="
110+echo "static constexpr int TEST_NUM_CORES = 56;" | grep -EHn "$PATTERN" 2>&1 | grep -vE '=\s*56\b' || echo " ✓ correctly suppressed (=56 is not a hit)"
@@ -0,0 +1,194 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# ----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# ----------------------------------------------------------------------------------------------------------
12+ 
13+"""Harness-backend conformance harness (HARNESS_BACKEND_ABSTRACTION_DESIGN.md §Conformance).
14+ 
15+Proves the CCBackend refactor did NOT weaken any canonical gate: freeze an artifact,
16+run the CANONICAL gate entrypoints against it, capture {gate: verdict+reason-code}, and
17+assert BIT-IDENTICAL pre-refactor vs post-refactor (later: cross-backend).
18+ 
19+**Never touches gate logic** — it REUSES `finalize_pipeline._pass_branch_gate_specs()`
20+(the canonical registry) so it stays faithful + in-sync as gates evolve. This file only
21+*invokes* gates and snapshots verdicts.
22+ 
23+Usage:
24+ capture: python3 conformance_harness.py capture <workspace> --out before.json
25+ diff: python3 conformance_harness.py diff before.json after.json # exit 1 if any divergence
26+"""
27+from __future__ import annotations
28+import argparse
29+import json
30+import sys
31+import traceback
32+from pathlib import Path
33+ 
34+_ORCH = Path(__file__).resolve().parents[1] / "orchestrator"
35+if str(_ORCH) not in sys.path:
36+ sys.path.insert(0, str(_ORCH))
37+ 
38+ 
39+def capture_verdicts(workspace: Path) -> dict:
40+ """Run every canonical gate against `workspace`, return {gate_id: verdict}.
41+ verdict = None (pass) | str (reason-code) | '__EXC__:...' (gate errored on this artifact)
42+ | '__SKIP__:...' (gate needs an input this frozen artifact lacks).
43+ """
44+ import finalize_pipeline as fp # canonical gates live here; import = reuse, not reimplement
45+ 
46+ vj = {}
47+ vjp = workspace / "verification.json"
48+ if vjp.exists():
49+ try:
50+ vj = json.loads(vjp.read_text())
51+ except Exception as e:
52+ vj = {"__vj_parse_error__": str(e)}
53+ prec = vj.get("precision", {}) if isinstance(vj, dict) else {}
54+ 
55+ verdicts: dict[str, object] = {}
56+ 
57+ def _run(name, fn):
58+ try:
59+ verdicts[name] = fn()
60+ except FileNotFoundError as e:
61+ verdicts[name] = f"__SKIP__: missing input: {e}"
62+ except Exception as e:
63+ verdicts[name] = f"__EXC__: {type(e).__name__}: {e}"
64+ 
65+ # --- inline pre-checks (not in the pass-branch registry) ---
66+ _run("STALE_ORCHESTRATOR", lambda: getattr(fp, "_check_stale_orchestrator")())
67+ _run("MODEL_PY_SHAPE", lambda: getattr(fp, "_check_model_py_shape")(workspace))
68+ _run("PASS_A_COVERAGE", lambda: getattr(fp, "_check_pass_a_coverage")(workspace, prec))
69+ 
70+ # --- canonical pass-branch gate registry (authoritative, reused) [stage: finalize] ---
71+ for gate_id, pred in getattr(fp, "_pass_branch_gate_specs")():
72+ name = getattr(gate_id, "value", str(gate_id))
73+ if name == getattr(fp, "_PLUGIN_EXTRAS_SENTINEL", "plugin_extras"):
74+ # plugin-supplied extras return a dict, not a reason string — snapshot as-is
75+ _run("finalize:PLUGIN_EXTRAS", lambda p=pred: p(workspace, vj))
76+ continue
77+ _run("finalize:" + name, lambda p=pred: p(workspace, vj))
78+ 
79+ # --- stage: O2.5 reference-provider contract (E) ---
80+ try:
81+ import phase_o25_a3_ref as o25 # canonical provider validators (main merged this round)
82+ _run("o25:model_contract", lambda: getattr(o25.validate_model_contract(workspace), "reason_code", "?"))
83+ except Exception as e:
84+ verdicts["o25:model_contract"] = f"__EXC__: import phase_o25_a3_ref: {type(e).__name__}: {e}"
85+ 
86+ # --- stage: workflow_critic CHECK-LOGIC (B) — capture rejections, NOT the hook wiring ---
87+ try:
88+ _wf = str(Path(__file__).resolve().parents[1] / "workflow")
89+ if _wf not in sys.path:
90+ sys.path.insert(0, _wf)
91+ import workflow_critic as wc
92+ for nm, fn in [("wc:target_simt_compat", wc.check_target_simt_compat),
93+ ("wc:global_invariants", wc.check_global_invariants),
94+ ("wc:taxonomy_coverage", wc.check_taxonomy_coverage)]:
95+ def _cap(f=fn):
96+ rej: list = []
97+ f(workspace, rej) # check-logic appends Rejection(s); empty = pass
98+ return None if not rej else "; ".join(sorted(getattr(r, "rule_id", str(r)) for r in rej))
99+ _run(nm, _cap)
100+ except Exception as e:
101+ verdicts["wc:__import__"] = f"__EXC__: import workflow_critic: {type(e).__name__}: {e}"
102+ 
103+ # --- stage: safety guard CHECK-LOGIC (C) — FIXED probes (per-tool-call, workspace-independent);
104+ # the key acceptance for the dispatch/hook-wiring refactor. Incl the bash-bypass airtight case. ---
105+ try:
106+ import output_read_guard as org # workflow path already on sys.path (B section)
107+ it_mode = next(iter(getattr(org, "_ITERATE_MODES", {"optimize"})), "optimize")
108+ own = "output/myproj/kernels/opA"
109+ _probes = {
110+ # fresh/cold gen: ANY output/ read denied
111+ "guard:fresh_output_read_deny": lambda: getattr(org, "_should_deny")(
112+ "output/proj/kernels/opA/verification.json", "default", ""
113+ ),
114+ # iterate on OWN archive: allowed
115+ "guard:iterate_own_archive_allow": lambda: getattr(org, "_should_deny")(own + "/kernel/x.so", it_mode, own),
116+ # iterate but CROSS archive (other op / other SoC): denied (the cheat rule)
117+ "guard:iterate_cross_archive_deny": lambda: getattr(org, "_should_deny")("output/other/kernels/opA/x.so", it_mode, own),
118+ # non-output path: not our concern
119+ "guard:non_output_ignored": lambda: getattr(org, "_should_deny")("/home/w/workspace/opA/kernel.h", "default", ""),
120+ # BASH-BYPASS airtight case (DS PoC's key finding): a bash read-cmd's extracted output frag
121+ # hits the SAME deny path (guard isn't tool-name-gated in a bypassable way for reads).
122+ "guard:bash_read_bypass_denied": lambda: (
123+ bool(getattr(org, "_output_paths")("cat output/other/kernels/opA/verification.json"))
124+ and all(
125+ getattr(org, "_should_deny")(file_path, "default", "")
126+ for file_path in getattr(org, "_output_paths")("cat output/other/kernels/opA/verification.json")
127+ )
128+ ),
129+ }
130+ for nm, fn in _probes.items():
131+ _run(nm, fn)
132+ except Exception as e:
133+ verdicts["guard:__import__"] = f"__EXC__: {type(e).__name__}: {e}"
134+ 
135+ # --- NEXT INCREMENT: ---
136+ # D precision: precision_eval_port_a3_two_tier (calls compare.py) — needs tensor inputs
137+ # (a3_capture/cpu_truth .pt); SKIP-record when a frozen archive lacks them. Low priority
138+ # (grades artifact; dispatch/wiring refactor can't touch precision judgement).
139+ # EXCLUDED from hermetic conformance (external-state-dependent, not pure check-logic):
140+ # - ship_claim_audit._verify_sha_on_main (hits git → non-hermetic)
141+ # - _maybe_port_a3_perf_remeasure (NPU device measurement, not gate-replay)
142+ 
143+ return dict(sorted(verdicts.items()))
144+ 
145+ 
146+def diff_snapshots(before: dict, after: dict) -> list[str]:
147+ """Return per-gate divergences (empty = bit-identical = conformant)."""
148+ keys = sorted(set(before) | set(after))
149+ out = []
150+ for k in keys:
151+ b, a = before.get(k, "<absent>"), after.get(k, "<absent>")
152+ if b != a:
153+ out.append(f" {k}: before={b!r} after={a!r}")
154+ return out
155+ 
156+ 
157+if __name__ == "__main__":
158+ ap = argparse.ArgumentParser()
159+ sub = ap.add_subparsers(dest="cmd", required=True)
160+ c = sub.add_parser("capture")
161+ c.add_argument("workspace")
162+ c.add_argument("--out", default=None)
163+ d = sub.add_parser("diff")
164+ d.add_argument("before")
165+ d.add_argument("after")
166+ a = ap.parse_args()
167+ 
168+ if a.cmd == "capture":
169+ snap = capture_verdicts(Path(a.workspace).resolve())
170+ payload = {"workspace": str(Path(a.workspace).resolve()), "verdicts": snap}
171+ txt = json.dumps(payload, ensure_ascii=False, indent=2)
172+ if a.out:
173+ Path(a.out).write_text(txt)
174+ print(f"captured {len(snap)} gate verdicts -> {a.out}")
175+ else:
176+ print(txt)
177+ # summary
178+ npass = sum(1 for v in snap.values() if v is None)
179+ nfail = sum(1 for v in snap.values() if isinstance(v, str) and not v.startswith("__"))
180+ nskip = sum(1 for v in snap.values() if isinstance(v, str) and v.startswith("__SKIP__"))
181+ nexc = sum(1 for v in snap.values() if isinstance(v, str) and v.startswith("__EXC__"))
182+ print(f"# pass={npass} fail/reason={nfail} skip={nskip} exc={nexc} total={len(snap)}", file=sys.stderr)
183+ sys.exit(0)
184+ 
185+ if a.cmd == "diff":
186+ before = json.loads(Path(a.before).read_text()).get("verdicts", {})
187+ after = json.loads(Path(a.after).read_text()).get("verdicts", {})
188+ div = diff_snapshots(before, after)
189+ if div:
190+ print("CONFORMANCE FAIL — canonical gate verdict divergence (refactor weakened/changed a gate):")
191+ print("\n".join(div))
192+ sys.exit(1)
193+ print(f"CONFORMANCE PASS — all {len(set(before)|set(after))} gate verdicts bit-identical")
194+ sys.exit(0)
@@ -0,0 +1,191 @@
1+#!/bin/bash
2+# build_archive.sh — per-iter capture of build stdout/stderr + kernel snapshot to A5 archive
3+#
4+# Wraps a build invocation: runs the build remotely, captures both streams + kernel snapshot
5+# to /data/build_archive/<op>/<batch>/<iter>/ on A5 host. Returns the build's exit code.
6+#
7+# Usage (called from deploy_to_npu_lane.sh after the existing build command):
8+#
9+# bash src/scripts/build_archive.sh \
10+# --ssh-cmd "$SSH_CMD" \
11+# --remote-task "$REMOTE_TASK" \
12+# --build-cmd "$BUILD_CMD" \
13+# --op-slug "$ASCENDC_WORKSPACE" \
14+# --soc "$SOC_VERSION" \
15+# --lane "${LANE:-0}" \
16+# --npu "${ASCEND_VISIBLE_DEVICES:-0}" \
17+# --workspace-marker "$_WS_DIR/.last_build_archive_path"
18+#
19+# Optional env: BUILD_ARCHIVE_ROOT (default /data/build_archive)
20+# BUILD_BATCH_ID (default orch_<UTC ts>)
21+# CANDIDATE_BRIEF_HASH (default "default")
22+#
23+# Standalone test (without integration):
24+# BUILD_ARCHIVE_ROOT=/tmp/build_archive_test bash src/scripts/build_archive.sh --dry-run
25+#
26+# Created 2026-05-02 per docs/research/GEPA_skill_design_2026_05_02.md §15.2
27+ 
28+set -uo pipefail
29+ 
30+# ─── parse args ─────────────────────────────────────────────────────────
31+SSH_CMD=""
32+REMOTE_TASK=""
33+BUILD_CMD=""
34+OP_SLUG=""
35+SOC=""
36+LANE="0"
37+NPU="0"
38+WORKSPACE_MARKER=""
39+DRY_RUN=0
40+ 
41+while [ $# -gt 0 ]; do
42+ case "$1" in
43+ --ssh-cmd) SSH_CMD="$2"; shift 2;;
44+ --remote-task) REMOTE_TASK="$2"; shift 2;;
45+ --build-cmd) BUILD_CMD="$2"; shift 2;;
46+ --op-slug) OP_SLUG="$2"; shift 2;;
47+ --soc) SOC="$2"; shift 2;;
48+ --lane) LANE="$2"; shift 2;;
49+ --npu) NPU="$2"; shift 2;;
50+ --workspace-marker) WORKSPACE_MARKER="$2"; shift 2;;
51+ --dry-run) DRY_RUN=1; shift;;
52+ *) echo "unknown arg: $1" >&2; exit 2;;
53+ esac
54+done
55+ 
56+# Strip trailing slash from OP_SLUG path; take basename
57+OP_SLUG="${OP_SLUG##*/}"
58+OP_SLUG="${OP_SLUG:-unknown_op}"
59+ 
60+ARCHIVE_ROOT="${BUILD_ARCHIVE_ROOT:-/data/build_archive}"
61+BATCH_ID="${BUILD_BATCH_ID:-orch_$(date -u +%Y%m%dT%H%M%SZ)}"
62+BRIEF_HASH="${CANDIDATE_BRIEF_HASH:-default}"
63+ 
64+if [ "$DRY_RUN" = "1" ]; then
65+ cat <<EOF
66+[dry-run] Would archive build for:
67+ archive_root: $ARCHIVE_ROOT
68+ op_slug: $OP_SLUG
69+ batch_id: $BATCH_ID
70+ brief_hash: $BRIEF_HASH
71+ lane: $LANE
72+ npu: $NPU
73+ soc: $SOC
74+ build_cmd: $BUILD_CMD
75+EOF
76+ exit 0
77+fi
78+ 
79+if [ -z "$SSH_CMD" ] || [ -z "$REMOTE_TASK" ] || [ -z "$BUILD_CMD" ]; then
80+ echo "[build_archive] ERROR: missing required args (--ssh-cmd, --remote-task, --build-cmd)" >&2
81+ exit 2
82+fi
83+ 
84+# ─── compute next iter_id atomically on remote ──────────────────────────
85+ARCHIVE_DIR_BASE="$ARCHIVE_ROOT/$OP_SLUG/$BATCH_ID"
86+ 
87+# Use shell arithmetic on remote to find the next iter number.
88+# Race-safe-ish: ls + wc on existing dirs. Two concurrent builds on same op+batch
89+# can collide, but that's a degenerate case (same op, same batch_id, parallel)
90+# and we'd just lose one iter's logs.
91+NEXT_ITER=$($SSH_CMD "
92+ mkdir -p '$ARCHIVE_DIR_BASE' 2>/dev/null
93+ n=\$(ls -d '$ARCHIVE_DIR_BASE'/iter_* 2>/dev/null | wc -l)
94+ printf 'iter_%03d' \$((n + 1))
95+" 2>/dev/null)
96+ 
97+if [ -z "$NEXT_ITER" ]; then
98+ echo "[build_archive] WARN: could not compute iter_id; falling back to ts-based" >&2
99+ NEXT_ITER="iter_$(date -u +%H%M%S)"
100+fi
101+ 
102+ARCHIVE_DIR="$ARCHIVE_DIR_BASE/$NEXT_ITER"
103+ 
104+# ─── prepare archive dir on remote ──────────────────────────────────────
105+$SSH_CMD "mkdir -p '$ARCHIVE_DIR'" 2>/dev/null
106+ 
107+# ─── run build, capture both streams ────────────────────────────────────
108+# We run the build inside the docker container (already implicit in $BUILD_CMD)
109+# and capture into the archive dir on the host filesystem (which is bind-mounted
110+# to /data inside the container — both paths see the same files).
111+START_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
112+ 
113+# Pass exit code back via $?; capture into ARCHIVE_DIR/exit_code on remote
114+$SSH_CMD "
115+ cd '$REMOTE_TASK' 2>/dev/null
116+ ($BUILD_CMD) > '$ARCHIVE_DIR/stdout.log' 2> '$ARCHIVE_DIR/stderr.log'
117+ rc=\$?
118+ echo \$rc > '$ARCHIVE_DIR/exit_code'
119+ exit \$rc
120+"
121+BUILD_RC=$?
122+ 
123+END_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
124+ 
125+# ─── snapshot kernel files ──────────────────────────────────────────────
126+# Tar the kernel/ directory at build-time to ARCHIVE_DIR/kernel_snapshot.tgz
127+$SSH_CMD "
128+ cd '$REMOTE_TASK' 2>/dev/null
129+ if [ -d kernel ]; then
130+ tar czf '$ARCHIVE_DIR/kernel_snapshot.tgz' kernel/ 2>/dev/null
131+ fi
132+" 2>/dev/null
133+ 
134+# ─── classify exit code into a one-line verdict ─────────────────────────
135+case "$BUILD_RC" in
136+ 0) VERDICT="PASS";;
137+ 1) VERDICT="COMPILE_ERROR";;
138+ 2) VERDICT="USAGE_ERROR";;
139+ 10) VERDICT="INFRA_ERROR";;
140+ *) VERDICT="UNKNOWN_$BUILD_RC";;
141+esac
142+ 
143+# ─── compress logs if large (>100 KB) ───────────────────────────────────
144+$SSH_CMD "
145+ for f in '$ARCHIVE_DIR/stdout.log' '$ARCHIVE_DIR/stderr.log'; do
146+ if [ -f \"\$f\" ] && [ \$(stat -c%s \"\$f\" 2>/dev/null || echo 0) -gt 102400 ]; then
147+ gzip \"\$f\" 2>/dev/null
148+ fi
149+ done
150+"
151+ 
152+# ─── write meta.json ────────────────────────────────────────────────────
153+META_JSON=$(cat <<EOF
154+{
155+ "op": "$OP_SLUG",
156+ "batch_id": "$BATCH_ID",
157+ "iter_id": "$NEXT_ITER",
158+ "ts_start_utc": "$START_TS",
159+ "ts_end_utc": "$END_TS",
160+ "host": "${HOST:-unknown}",
161+ "container": "${CONTAINER:-unknown}",
162+ "soc": "$SOC",
163+ "lane": "$LANE",
164+ "npu": "$NPU",
165+ "candidate_brief_hash": "$BRIEF_HASH",
166+ "build_exit_code": $BUILD_RC,
167+ "verdict": "$VERDICT"
168+}
169+EOF
170+)
171+$SSH_CMD "cat > '$ARCHIVE_DIR/meta.json' <<'METAEOF'
172+$META_JSON
173+METAEOF" 2>/dev/null
174+ 
175+# ─── workspace-side breadcrumb (so trace_serializer can find the dir) ───
176+if [ -n "$WORKSPACE_MARKER" ]; then
177+ echo "$ARCHIVE_DIR" > "$WORKSPACE_MARKER" 2>/dev/null || true
178+fi
179+ 
180+# Convenience symlinks (preserve existing /tmp/ascendc_last_build.* contract)
181+$SSH_CMD "
182+ ln -sf '$ARCHIVE_DIR/stderr.log' /tmp/ascendc_last_build.stderr 2>/dev/null || \
183+ ln -sf '$ARCHIVE_DIR/stderr.log.gz' /tmp/ascendc_last_build.stderr 2>/dev/null
184+ ln -sf '$ARCHIVE_DIR/stdout.log' /tmp/ascendc_last_build.stdout 2>/dev/null || \
185+ ln -sf '$ARCHIVE_DIR/stdout.log.gz' /tmp/ascendc_last_build.stdout 2>/dev/null
186+" 2>/dev/null
187+ 
188+# ─── stdout summary line ────────────────────────────────────────────────
189+echo "[build_archive] $OP_SLUG/$BATCH_ID/$NEXT_ITER -> $VERDICT (rc=$BUILD_RC)"
190+ 
191+exit $BUILD_RC
@@ -0,0 +1,25 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+ 
11+"""CANN-source-learn carve-out (P0x v2 — DEBT-077).
12+ 
13+Deterministic scanners + utilities for the aog-cann-learner sub-agent and
14+aog-knowledge-maintain Mode 5. Each scanner produces a numeric/boolean score
15+that the skill caller validates BEFORE merging any candidate KB entry.
16+ 
17+Modules:
18+ identifier_scanner — C34a: deterministic CANN-vs-public-API denylist scan
19+ compile_gate — C34b: bishengir-clang -fsyntax-only against public headers
20+ copy_shape — C34c: token n-gram contiguous-overlap detection
21+ kb_overlap — C35: reason-code-based overlap with existing KB
22+ 
23+See docs/design/KB_DESIGN_NOTES.md#cann-learn-agent-design-v2 for architecture + threat model.
24+"""
25+__version__ = "0.1.0-dev"
@@ -0,0 +1,426 @@
1+# ----------------------------------------------------------------------------------------------------------
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+# ----------------------------------------------------------------------------------------------------------
10+"""Production spawn adapter — wires aog-cann-learner agent into Mode 5.
11+ 
12+Provides `spawn_cann_learner_agent` matching the contract Mode 5 expects:
13+ 
14+ callable(op, workspace, module_path, sealed_dir, run_id, kb_root,
15+ api_catalog_path) → dict with keys:
16+ - sealed_files: list[Path]
17+ - summary_path: Path
18+ - candidate_paths: list[Path]
19+ - cann_files_read: list[Path]
20+ 
21+Builds the brief, invokes the configured harness backend for
22+aog-cann-learner, parses returned envelope
23++ workspace artifacts, returns the dict Mode 5 will independently re-validate.
24+ 
25+Non-trivial structural choices:
26+ 
27+1. **Brief is mostly cited path lists** (per agent's SKILL.md scope) —
28+ not free-form prompt. The agent reads `module_path`, writes sealed
29+ notes + public summary.json + appends to `patterns/unverified/candidates.md`.
30+ 
31+2. **The agent reports candidate filenames in handoff line** so we can
32+ collect them; we also do a directory diff (kb_root/patterns/unverified/
33+ listing before/after) as defense in depth.
34+ 
35+3. **cann_files_read is reconstructed** from the agent's `cann_learn_summary.json`
36+ `cann_files_read` field (per schema). If absent, falls back to
37+ listing module_path's files (over-conservative for re-scan purposes —
38+ prefer false-positive C34a/C34c hits over false-negatives).
39+"""
40+from __future__ import annotations
41+ 
42+import json
43+import sys
44+import time
45+from pathlib import Path
46+ 
47+_HERE = Path(__file__).resolve()
48+# Add src/scripts/orchestrator to path so agent_transport imports work.
49+sys.path.insert(0, str(_HERE.parents[1] / "orchestrator"))
50+ 
51+import agent_transport # noqa: E402
52+# Harness-decoupling: spawn via the Backend (CC plugin), not agent_transport directly.
53+from backends import get_backend # noqa: E402
54+ 
55+_backend = get_backend()
56+ 
57+ 
58+# Default per-spawn wall-clock cap. The agent does Read-only scanning
59+# + 4 self-checks; should comfortably finish under 30 min on any reasonable
60+# module. Bump if observed runs need more.
61+DEFAULT_TIMEOUT_SEC = 2400 # 40 min
62+ 
63+ 
64+def build_cann_learner_brief(
65+ *,
66+ op: str,
67+ workspace: Path,
68+ module_path: Path,
69+ sealed_dir: Path,
70+ run_id: str,
71+ kb_root: Path,
72+ api_catalog_path: Path,
73+ extraction_mode: str = "kernel_structural",
74+) -> str:
75+ """Construct the brief that aog-cann-learner agent receives.
76+ 
77+ Per agent's SKILL.md the brief should be terse — agent already knows
78+ its own protocol from the system prompt. The brief just provides
79+ op-specific paths + the run-id token.
80+ """
81+ slug = f"{op.lower().replace('_', '')}-cl-1"
82+ 
83+ _sealed_rel = sealed_dir.relative_to(workspace.parent.parent) if sealed_dir.is_absolute() else sealed_dir
84+ workspace_abs = workspace.resolve()
85+ module_abs = module_path.resolve()
86+ kb_root_abs = kb_root.resolve()
87+ api_catalog_abs = api_catalog_path.resolve()
88+ 
89+ cann_strategy_path = workspace_abs / "cann_strategy_inference.md"
90+ summary_path = workspace_abs / "cann_learn_summary.json"
91+ candidates_path = kb_root_abs / "patterns" / "unverified" / "candidates.md"
92+ 
93+ return f"""{slug} — cann_learn spawn
94+ 
95+OP: {op}
96+RUN_ID: {run_id}
97+WORKSPACE (absolute): {workspace_abs}
98+MODULE_PATH (CANN source root, read-only — your scope): {module_abs}
99+KB_ROOT (existing KB for cross-reference, READ): {kb_root_abs}
100+API_CATALOG (public AscendC API allowlist, READ): {api_catalog_abs}
101+ 
102+# Required reading (mandatory, before Phase A)
103+- src/skills/references/shared/ANTI_PRESSURE_PROTOCOLS.md # P1-P8 catalog
104+- {cann_strategy_path} # researcher's strategy_inference for this op
105+ (this is the "what KB already inferred" baseline — use to seed Phase A
106+ pre-scan + C35 reason-code matcher)
107+ 
108+# Output paths (write here, ONLY here)
109+- Sealed (your private working dir, agent caller will archive on exit):
110+ {sealed_dir.resolve() / 'source_notes.md'}
111+ {sealed_dir.resolve() / 'extraction_drafts.md'}
112+- Public sanitized summary (JSON-only schema, validated by skill caller):
113+ {summary_path}
114+- Candidate KB additions (append-only):
115+ {candidates_path}
116+ 
117+# Phases
118+A. KB pre-scan against {kb_root_abs} for op-class keywords from
119+ cann_strategy_inference.md. Build "what KB already knows" list.
120+ Output to sealed/source_notes.md §pre_scan.
121+B. Read 2-5 files within {module_abs} (header + impl + tiling).
122+ Capture algorithm structure + public-API equivalents + internal-only
123+ primitives (flag explicitly).
124+C. Pattern extraction: rewrite each insight in PUBLIC-API surface terms.
125+ Layered shape: title (principle) + body (abstract) + concrete-anchor
126+ (3-5 line snippet, public AscendC API only) + evidence + other-instances.
127+ Append candidates to {candidates_path} with `derived-from: cann-source`
128+ metadata. Drafts in sealed/extraction_drafts.md.
129+D. Self-review: run C34a / C34b / C34c / C35 on your own output. Drop
130+ leaking candidates BEFORE writing summary.json. Verdict + scores in
131+ summary.json `self_review_verdict` + `checks` block.
132+E. Exit handoff (last line of stdout):
133+ `→ orchestrator: cann_learn_done — kept N candidates, M metadata-fix proposals, leak_score=X`
134+ OR
135+ `→ orchestrator: cann_learn_blocked — <reason>`
136+ 
137+{_mode_specific_section(op, extraction_mode, kb_root_abs, candidates_path)}
138+ 
139+**Anti-patterns (REJECTED)**
140+- Verbatim CANN code (even renamed identifiers — C34c catches token n-gram)
141+- CANN-internal namespace / class / template / macro names in candidate body (C34a)
142+- Hand-prose inside summary.json outside schema (validator rejects)
143+- Writing to kernel/ or canonical KB files (G11 hook rejects)
144+- Patterns with NO public-API equivalent (drop, don't ship infeasible)
145+- Self-reporting verdict=PASS while leaving leaks (skill caller re-scans)
146+ 
147+# Required summary.json field — extraction_mode
148+ 
149+You MUST include `"extraction_mode": "{extraction_mode}"` as a top-level field
150+in `cann_learn_summary.json`. This tells the skill caller which scanner
151+profile + KB destination to apply. Mode 5 caller validates the value.
152+ 
153+ITER BUDGET: single shot. Self-review failure → candidate dropped, no respawn.
154+"""
155+ 
156+ 
157+def _mode_specific_section(op: str, extraction_mode: str, kb_root_abs: Path, candidates_path: Path) -> str:
158+ """Build the mode-specific Phase B/C scope + extraction-question section.
159+ 
160+ Mode 5 (kernel_structural) — DEFAULT, the historical scope. Read 2-5
161+ kernel files (header + impl + tiling). Extract algorithm structure +
162+ public-API equivalents. Candidates → patterns/unverified/candidates.md
163+ with prefix CAND-* / promotion path to P-P canonical.
164+ 
165+ Mode 6 (build_system) NEW (2026-05-21) — read CMakeLists.txt + register
166+ + op_proto + apt.cpp files. Extract build-system-level recipes
167+ (per-source-file compile flag isolation; multi-target binary registration;
168+ launch macro routing). Candidates → target/ascendc/build_system/candidates.md
169+ with prefix CAND-BSP-* / promotion path to BSP-N canonical.
170+ 
171+ Origin: FA Pattern A iter 1-5 (~$53 spend) empirically falsified all
172+ worker-scope hypotheses for V220 MIX_AIC_1_2 cube-internal sync. The
173+ remaining root-cause candidates live in build-system glue (per-source
174+ flag isolation / FFTSCNT mailbox / register attribute metadata), NOT in
175+ kernel.h. Mode 5 brief explicitly restricted scope to kernel files; the
176+ info needed wasn't reachable. Mode 6 extends scope to address this.
177+ """
178+ if extraction_mode == "build_system":
179+ bs_candidates = kb_root_abs / "target" / "ascendc" / "build_system" / "candidates.md"
180+ return f"""# Specific extraction questions for this op (Mode 6: build_system)
181+ 
182+{op} extraction in Mode 6 is BUILD-SYSTEM-FOCUSED — the kernel structural
183+patterns are out of scope (Mode 5 covers those). Focus Phase B/C on the
184+HOST-SIDE GLUE that defines how the kernel binary is built + launched:
185+ 
186+- **Per-source-file compile flag isolation**: which `target_compile_definitions`
187+ / `add_compile_definitions` apply to which source files. Especially
188+ `-DASCENDC_MATMUL_AICORE` — does it apply uniformly or only to cube cpp?
189+- **Multi-target binary registration**: how the same op registers different
190+ arch variants (V220 vs V351) via `register_*.cpp` / `op_proto*.cpp`.
191+- **Launch macro routing**: what `KERNEL_TASK_TYPE_DEFAULT` arguments appear
192+ in which `*_apt.cpp` files, gated by what `__NPU_ARCH__` macros.
193+- **CMake build dependency chains**: `add_dependencies` revealing build-time
194+ ordering constraints (e.g. tiling lib must build before kernel cpp).
195+- **Binary attribute metadata**: how `aclrtLaunchKernel` is called with
196+ arch-specific attributes — does the op's host glue inject FFTSCNT mailbox
197+ init metadata that a simpler standalone launch path doesn't replicate?
198+- **CMake-time API surface gates**: `if(ASCENDC_ENABLE_*)` blocks revealing
199+ what subsystems are conditionally compiled.
200+ 
201+## Scope reminder for Mode 6 file reads (Phase B)
202+ 
203+Within `module_path`, READ:
204+- `CMakeLists.txt` and any `*.cmake` files (top-level + per-subdir)
205+- `register_*.cpp` (host-side op registration)
206+- `op_proto*.{{cpp,h}}` (host-side op definition)
207+- `op_kernel/*_apt.cpp` (kernel adapter / launch entry — host-facing)
208+- `BUILD` / `BUILD.bazel` if present
209+- Top-level kernel.h (FOR CONTEXT ONLY — the launch macro it references;
210+ do NOT extract kernel-structural patterns in Mode 6, those are Mode 5's
211+ scope)
212+ 
213+DO NOT read internal `common/*` headers, shared utility headers, or recurse.
214+ 
215+## Output path (Mode 6 specific)
216+ 
217+Candidates → `{bs_candidates}` (NOT the patterns/unverified/candidates.md
218+that Mode 5 uses — that's reserved for kernel-structural).
219+ 
220+Use prefix `CAND-BSP-*` for candidates (CANN Build-System Pattern), promotable
221+to canonical `BSP-N` entries in `target/ascendc/build_system/PRINCIPLES.md`.
222+ 
223+## Generality requirement (Mode 6)
224+ 
225+Each BSP candidate must be applicable to AT LEAST 2 distinct op-classes
226+(e.g. fused-attention + fused-quant-matmul) — the build-system patterns
227+are higher-level than algorithm patterns and should generalize broadly.
228+ 
229+If a pattern is op-class-specific (e.g. only flash_attention_score uses it),
230+flag it as `narrow_scope: true` in the candidate metadata and drop unless
231+the body explains WHY no generalization is possible."""
232+ 
233+ # default: Mode 5 (kernel_structural) — preserves historical behavior
234+ return f"""# Specific extraction questions for this op (Mode 5: kernel_structural)
235+ 
236+{op} is a fused operator. The skill caller wants generalizable patterns
237+applicable to multiple fused ops (FA / MoE / fused-norm / KV-cache / RoPE).
238+Focus your Phase B/C on:
239+- How CANN structures multi-stage tile orchestration (Q·K^T → softmax → ·V chains)
240+- Public-API expression of online softmax (max-reduction + exp + accumulate)
241+- Multi-output API contract conventions (main return + auxiliary tensors
242+ like softmax_max / softmax_sum, tile-coord-tagged outputs)
243+- UB budget partitioning for fused ops (Q/K/V cache + scores + attn + out)
244+- dtype templating decisions (when to fp32-promote vs stay fp16)
245+ 
246+These questions seed Phase A pre-scan keywords AND Phase C `other-instances-predicted`
247+generality (so candidates aren't FA-specific).
248+ 
249+## Scope reminder for Mode 5 file reads (Phase B)
250+ 
251+Read 2-5 files (header + impl + tiling) within `module_path`. DO NOT recurse
252+beyond passed scope. Keep notes on:
253+- Algorithm structure (loops, dispatch, reduction tree, branch shape)
254+- Public-API equivalents (which AscendC API would express the same idea)
255+- Internal-only primitives — flag explicitly as non-portable
256+ 
257+## Output path (Mode 5 default)
258+ 
259+Candidates → `{candidates_path}` (patterns/unverified/candidates.md)"""
260+ 
261+ 
262+def _list_unverified_candidates(kb_root: Path) -> set[Path]:
263+ """Snapshot of candidate files in patterns/unverified/ (excluding markers)."""
264+ return _list_unverified_candidates_at(kb_root / "patterns" / "unverified")
265+ 
266+ 
267+def _list_unverified_candidates_at(dir_path: Path) -> set[Path]:
268+ """Snapshot of candidate files in `dir_path` (excluding markers).
269+ 
270+ Mode 6 (build_system) uses `target/ascendc/build_system/` as the
271+ candidates dir; Mode 5 uses `patterns/unverified/`. Shared listing
272+ logic factored here.
273+ """
274+ if not dir_path.exists():
275+ return set()
276+ candidates: set[Path] = set()
277+ for candidate in dir_path.iterdir():
278+ if (
279+ candidate.is_file()
280+ and not candidate.name.startswith(".")
281+ and candidate.suffix == ".md"
282+ and candidate.name != "candidates.md"
283+ ):
284+ candidates.add(candidate)
285+ return candidates
286+ 
287+ 
288+def _extract_cann_files_read(summary_path: Path, module_path: Path) -> list[Path]:
289+ """Read summary.json's cann_files_read field; fallback to module_path enumeration."""
290+ if summary_path.exists():
291+ try:
292+ d = json.loads(summary_path.read_text())
293+ files = d.get("cann_files_read", [])
294+ if isinstance(files, list) and files:
295+ return [Path(f) for f in files]
296+ except (json.JSONDecodeError, OSError):
297+ pass
298+ 
299+ # Fallback: enumerate module_path files (over-conservative — re-scan
300+ # may flag patterns that didn't actually read those files, but better
301+ # than missing real leaks).
302+ if module_path.is_dir():
303+ return list(module_path.rglob("*.h")) + list(module_path.rglob("*.cpp"))
304+ elif module_path.is_file():
305+ return [module_path]
306+ return []
307+ 
308+ 
309+def spawn_cann_learner_agent(
310+ *,
311+ op: str,
312+ workspace: Path,
313+ module_path: Path,
314+ sealed_dir: Path,
315+ run_id: str,
316+ kb_root: Path,
317+ api_catalog_path: Path,
318+ timeout_sec: int = DEFAULT_TIMEOUT_SEC,
319+ extraction_mode: str = "kernel_structural",
320+) -> dict:
321+ """Spawn aog-cann-learner agent and collect output artifacts.
322+ 
323+ Returns dict matching Mode 5's spawn_agent_func contract.
324+ 
325+ Raises agent_transport.AgentTransportError if claude CLI fails.
326+ 
327+ extraction_mode: "kernel_structural" (default, historical Mode 5 behavior)
328+ or "build_system" (Mode 6, 2026-05-21, extends scope to CMakeLists.txt +
329+ register + op_proto + apt.cpp for build-system pattern extraction).
330+ """
331+ brief = build_cann_learner_brief(
332+ op=op, workspace=workspace, module_path=module_path,
333+ sealed_dir=sealed_dir, run_id=run_id, kb_root=kb_root,
334+ api_catalog_path=api_catalog_path,
335+ extraction_mode=extraction_mode,
336+ )
337+ 
338+ # Snapshot candidates dir before spawn (for diff after).
339+ # Mode 6 routes candidates to target/ascendc/build_system/candidates.md
340+ # instead of patterns/unverified/candidates.md.
341+ if extraction_mode == "build_system":
342+ candidates_dir = kb_root / "target" / "ascendc" / "build_system"
343+ candidates_md = candidates_dir / "candidates.md"
344+ candidates_dir.mkdir(parents=True, exist_ok=True) # may not exist yet
345+ cands_before = _list_unverified_candidates_at(candidates_dir)
346+ else:
347+ candidates_dir = kb_root / "patterns" / "unverified"
348+ candidates_md = candidates_dir / "candidates.md"
349+ cands_before = _list_unverified_candidates(kb_root)
350+ cands_md_size_before = candidates_md.stat().st_size if candidates_md.exists() else 0
351+ 
352+ spawn_started_at = time.time()
353+ 
354+ # Spawn — STREAMING mode for diagnostic visibility (P0aau-c35.d, 2026-05-09).
355+ # First M2 attempt with --output-format json hit 40min timeout with zero
356+ # output and no diagnostic trace. Switching to stream-json + verbose so
357+ # tool_use events tee to disk and we can see what the agent did even
358+ # if it ultimately stalls. Performance penalty is acceptable (~10% per
359+ # tool call); diagnostic value is critical for first real run.
360+ extra_dirs: list[str] = []
361+ cwd = Path.cwd().resolve()
362+ for must_read in (workspace.resolve(), module_path.resolve(), kb_root.resolve()):
363+ if not str(must_read).startswith(str(cwd)):
364+ extra_dirs.extend(["--add-dir", str(must_read)])
365+ 
366+ # Tee stream events into sealed dir for forensics.
367+ tee_path = sealed_dir / "spawn_stream.jsonl"
368+ 
369+ result = _backend.dispatch(
370+ "aog-cann-learner", brief, kind="agent", mode="streaming",
371+ tee_path=tee_path, timeout=timeout_sec, cwd=cwd,
372+ extra_args=extra_dirs or None,
373+ )
374+ # (permission_mode="bypassPermissions" was explicit; it's spawn_agent_streaming's default → faithful.)
375+ 
376+ spawn_duration = time.time() - spawn_started_at
377+ 
378+ # Collect output artifacts.
379+ summary_path = workspace / "cann_learn_summary.json"
380+ 
381+ # Sealed dir contents.
382+ sealed_files: list[Path] = []
383+ if sealed_dir.exists():
384+ sealed_files = [f for f in sealed_dir.rglob("*") if f.is_file()]
385+ 
386+ # Candidate files: anything new in candidates_dir + grow in candidates.md.
387+ if extraction_mode == "build_system":
388+ cands_after = _list_unverified_candidates_at(candidates_dir)
389+ else:
390+ cands_after = _list_unverified_candidates(kb_root)
391+ new_cand_files = sorted(cands_after - cands_before)
392+ 
393+ # If candidates.md grew, treat its entire current content as a "candidate path"
394+ # for re-scan purposes — Mode 5's revalidate runs C34a / C34c against EACH
395+ # candidate path's contents.
396+ candidate_paths: list[Path] = list(new_cand_files)
397+ if candidates_md.exists() and candidates_md.stat().st_size > cands_md_size_before:
398+ candidate_paths.append(candidates_md)
399+ 
400+ cann_files_read = _extract_cann_files_read(summary_path, module_path)
401+ 
402+ # Write a small spawn audit record into sealed_dir for forensics.
403+ if sealed_dir.exists():
404+ (sealed_dir / "spawn_audit.json").write_text(json.dumps({
405+ "run_id": run_id,
406+ "op": op,
407+ "spawn_duration_sec": round(spawn_duration, 2),
408+ "agent_output_text": result.output_text,
409+ "agent_is_error": result.is_error,
410+ "agent_cost_usd": result.cost_usd,
411+ "n_sealed_files": len(sealed_files),
412+ "n_new_cand_files": len(new_cand_files),
413+ "candidates_md_size_delta": (
414+ candidates_md.stat().st_size - cands_md_size_before
415+ if candidates_md.exists() else 0
416+ ),
417+ "n_cann_files_read": len(cann_files_read),
418+ }, indent=2))
419+ 
420+ return {
421+ "sealed_files": sealed_files,
422+ "summary_path": summary_path if summary_path.exists() else None,
423+ "candidate_paths": candidate_paths,
424+ "cann_files_read": cann_files_read,
425+ "_agent_result": result, # extra; Mode 5 ignores; useful for debugging
426+ }