fix(actpred): 修复 DFC 连通性检查在 5.3 HAR 下被静默跳过的缺陷
DFC 5.3 的 HAR 不含 .edges 文件,原检查从未真正执行(仅打印 WARNING 跳过),输入断连的 HEF 仍可编译通过。改为解析主 hn 图 (layer output 边)做 BFS 可达性验证,任一输入层不可达输出层即 报错阻断;har_input_layer_names 同步改为数字后缀排序并优先取 主图 hn。新增 3 个单测(排序/通过/断连拦截),全量 76 通过。 本轮 224×224 重编译实测:优化后 HAR 5 输入均可达输出层(此前 64 版 HEF 输入不敏感的连通性隐患已消除);但 NPU 复验输出仍不 敏感,pred_token 边缘 scale=1965.92 表明抖动校准未生效,根因 待查(见后续提交)。
This commit is contained in:
1 parent
a6e1781185
commit
8c58770280
2 files changed
+105
-36
No files matched your search
+44
-36
@@ -25,6 +25,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -43,56 +44,63 @@ def hailo(*args, venv_bin: str):
|
|||||||
return subprocess.run(cmd, env=env, check=True)
|
return subprocess.run(cmd, env=env, check=True)
|
||||||
|
|
||||||
|
|
||||||
def har_input_layer_names(har_path: str) -> list:
|
def _har_hn_layers(har_path: str) -> dict:
|
||||||
"""从 HAR(tar 包)中读 hn 文件,按 input_layerN 排序返回层名"""
|
"""解包 HAR 并读取主 hn 文件的 layers(优化产物含 fp/native 变体,取主图)"""
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
with tarfile.open(har_path) as tf:
|
with tarfile.open(har_path) as tf:
|
||||||
tf.extractall(tmp)
|
tf.extractall(tmp)
|
||||||
hn_files = [f for f in os.listdir(tmp) if f.endswith(".hn")]
|
hn_files = [f for f in os.listdir(tmp) if f.endswith(".hn")]
|
||||||
with open(os.path.join(tmp, hn_files[0])) as f:
|
if not hn_files:
|
||||||
hn = json.load(f)
|
raise ValueError(f"{har_path} 中无 .hn 文件")
|
||||||
layers = [(name, l) for name, l in hn["layers"].items()
|
main = [f for f in hn_files
|
||||||
if l["type"] == "input_layer"]
|
if not f.endswith(".fp.hn") and not f.endswith(".native.hn")]
|
||||||
layers.sort(key=lambda x: x[0])
|
hn_file = main[0] if main else hn_files[0]
|
||||||
return [name for name, _ in layers]
|
with open(os.path.join(tmp, hn_file)) as f:
|
||||||
|
return json.load(f)["layers"]
|
||||||
|
|
||||||
|
|
||||||
|
def _input_layer_index(name: str) -> int:
|
||||||
|
m = re.search(r"input_layer(\d+)$", name)
|
||||||
|
return int(m.group(1)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
def har_input_layer_names(har_path: str) -> list:
|
||||||
|
"""从 HAR 主 hn 图读输入层名,按 input_layerN 数字后缀排序"""
|
||||||
|
layers = _har_hn_layers(har_path)
|
||||||
|
names = [n for n, l in layers.items() if l["type"] == "input_layer"]
|
||||||
|
names.sort(key=_input_layer_index)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
def check_input_connectivity(har_path: str, input_layers: list):
|
def check_input_connectivity(har_path: str, input_layers: list):
|
||||||
"""检查 HAR 中输入层是否都有下游边
|
"""检查 HAR 图中每个输入层是否可达任一输出层
|
||||||
|
|
||||||
输入层被优化断开时编译照常成功,但 HEF 输出恒定值且无任何报错
|
输入层被优化断开时编译照常成功,但 HEF 输出恒定值且无任何报错
|
||||||
(2026-08-13 实测踩坑),必须在编译前拦截。.edges 文件缺失或格式
|
(2026-08-13 实测踩坑),必须在编译前拦截。DFC 5.3 的 HAR 不含
|
||||||
无法识别时仅告警不阻断。
|
.edges 文件,连通性改从主 hn 图的 layer output 边 BFS 判定。
|
||||||
"""
|
"""
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
layers = _har_hn_layers(har_path)
|
||||||
with tarfile.open(har_path) as tf:
|
adj = {n: l.get("output", []) for n, l in layers.items()}
|
||||||
tf.extractall(tmp)
|
targets = {n for n, l in layers.items() if l["type"] == "output_layer"}
|
||||||
edge_files = [f for f in os.listdir(tmp) if f.endswith(".edges")]
|
if not targets:
|
||||||
if not edge_files:
|
print(f"WARNING: {har_path} 图中无输出层,跳过连通性检查")
|
||||||
print(f"WARNING: {har_path} 无 .edges 文件,跳过输入连通性检查")
|
|
||||||
return
|
|
||||||
with open(os.path.join(tmp, edge_files[0])) as f:
|
|
||||||
edges = json.load(f)
|
|
||||||
|
|
||||||
if not isinstance(edges, list):
|
|
||||||
print(f"WARNING: .edges 不是列表({type(edges).__name__}),"
|
|
||||||
f"跳过连通性检查")
|
|
||||||
return
|
return
|
||||||
adjacency = set()
|
disconnected = []
|
||||||
for e in edges:
|
for name in input_layers:
|
||||||
if isinstance(e, dict) and "from" in e:
|
seen, stack = set(), [name]
|
||||||
adjacency.add(e["from"])
|
while stack:
|
||||||
elif isinstance(e, (list, tuple)) and len(e) == 2:
|
cur = stack.pop()
|
||||||
adjacency.add(e[0])
|
if cur in seen:
|
||||||
else:
|
continue
|
||||||
print(f"WARNING: 无法识别 .edges 条目格式: {e!r},跳过连通性检查")
|
seen.add(cur)
|
||||||
return
|
stack.extend(adj.get(cur, []))
|
||||||
disconnected = [n for n in input_layers if n not in adjacency]
|
if not (seen & targets):
|
||||||
|
disconnected.append(name)
|
||||||
if disconnected:
|
if disconnected:
|
||||||
sys.exit(
|
sys.exit(
|
||||||
f"错误: {har_path} 中输入层无下游边(输入被优化断开,"
|
f"错误: {har_path} 中输入层不可达输出层(输入被优化断开,"
|
||||||
f"编译出的 HEF 将输出恒定值): {disconnected}")
|
f"编译出的 HEF 将输出恒定值): {disconnected}")
|
||||||
print(f"输入连通性检查通过: {len(input_layers)} 个输入层均有下游边")
|
print(f"输入连通性检查通过: {len(input_layers)} 个输入层均可达输出层")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -1217,5 +1217,66 @@ class TestHailoPredictor(unittest.TestCase):
|
|||||||
np.testing.assert_allclose(result, [3, 3, 3], atol=1e-6)
|
np.testing.assert_allclose(result, [3, 3, 3], atol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDfcCompile(unittest.TestCase):
|
||||||
|
"""dfc_compile.py 的 HAR 输入层连通性检查(编译前拦截输入断连)"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_har(layers):
|
||||||
|
import json
|
||||||
|
import tarfile
|
||||||
|
data = json.dumps(
|
||||||
|
{"name": "t", "net_params": {}, "layers": layers}).encode()
|
||||||
|
path = tempfile.mktemp(suffix=".har")
|
||||||
|
with tarfile.open(path, "w") as tf:
|
||||||
|
ti = tarfile.TarInfo("net.hn")
|
||||||
|
ti.size = len(data)
|
||||||
|
tf.addfile(ti, io.BytesIO(data))
|
||||||
|
return path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _layers(connect_input2=True):
|
||||||
|
return {
|
||||||
|
"n/input_layer1": {"type": "input_layer", "input": [],
|
||||||
|
"output": ["n/conv1"]},
|
||||||
|
"n/input_layer2": {"type": "input_layer", "input": [],
|
||||||
|
"output": ["n/conv2"]},
|
||||||
|
"n/conv1": {"type": "conv", "input": ["n/input_layer1"],
|
||||||
|
"output": ["n/out"]},
|
||||||
|
"n/conv2": {"type": "conv", "input": ["n/input_layer2"],
|
||||||
|
"output": ["n/out" if connect_input2 else "n/dead"]},
|
||||||
|
"n/dead": {"type": "conv", "input": ["n/conv2"], "output": []},
|
||||||
|
"n/out": {"type": "output_layer",
|
||||||
|
"input": ["n/conv1"] + (["n/conv2"] if connect_input2 else []),
|
||||||
|
"output": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_input_layer_names_sorted_by_suffix(self):
|
||||||
|
from dfc_compile import har_input_layer_names
|
||||||
|
har = self._make_har(self._layers())
|
||||||
|
try:
|
||||||
|
names = har_input_layer_names(har)
|
||||||
|
finally:
|
||||||
|
os.unlink(har)
|
||||||
|
self.assertEqual(names, ["n/input_layer1", "n/input_layer2"])
|
||||||
|
|
||||||
|
def test_connectivity_pass(self):
|
||||||
|
from dfc_compile import check_input_connectivity
|
||||||
|
har = self._make_har(self._layers())
|
||||||
|
try:
|
||||||
|
check_input_connectivity(har, ["n/input_layer1", "n/input_layer2"])
|
||||||
|
finally:
|
||||||
|
os.unlink(har)
|
||||||
|
|
||||||
|
def test_disconnected_input_blocked(self):
|
||||||
|
from dfc_compile import check_input_connectivity
|
||||||
|
har = self._make_har(self._layers(connect_input2=False))
|
||||||
|
try:
|
||||||
|
with self.assertRaises(SystemExit) as ctx:
|
||||||
|
check_input_connectivity(har, ["n/input_layer1", "n/input_layer2"])
|
||||||
|
finally:
|
||||||
|
os.unlink(har)
|
||||||
|
self.assertIn("n/input_layer2", str(ctx.exception))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
Reference in new issue
Block a user