"""output 已有 test-design+cpp 的资料一致性校验脚本(复用前强制)
功能定位:压测 aclnn 接口时,如果 output/{api}/ 下已存在 test-design 与 cpp,则对照 aclnn 资料(md 文档)
判断接口支持的范围与 test-design 里面记录的范围是否一致:
- 一致(退出码 0) → 不再生成 CPP,直接使用 output 目录下的 cpp 进行压测
- 不一致(退出码 2) → 增量更新 test-design(新增缺失的 dtype/format/attr 取值信息,不推翻已有设计)
→ 重新生成对应场景 CPP → 依次压测
- 无法判定(退出码 3,md 缺失/解析失败)→ 按不一致处理,走完整重新推导(Step 1)
校验维度:
1. dtype 支持列表:md 参数说明(含约束说明段落)中每个 tensor/scalar 的 dtype 集合
vs 03_参数定义.yaml dtype_with_ranges——md 有而 design 无 → 需新增;design 有而 md 无 → 需复核
2. format:03 yaml format 字段 vs md 提及的 format(ND/NCHW/NHWC 等)
3. attr 取值范围/枚举:md 中 attr 参数的取值列表(取值:/支持 A、B、C 等模式)
vs 03/05 yaml 是否记录(design 文本中检索枚举 token)——md 有而 design 无 → 需新增 attr 取值信息
用法:
python scripts/check_design_consistency.py --api aclnnAbs
python scripts/check_design_consistency.py --api aclnnAbs --repos ../../CANN-aclnn-api-reference ../../ops-math
python scripts/check_design_consistency.py --api aclnnAbs --json # JSON 输出(Agent 消费)
"""
import argparse
import glob
import json
import os
import re
import sys
import yaml
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
DTYPE_ALIASES = [
("bfloat16", "BFLOAT16"), ("bf16", "BFLOAT16"),
("float16", "FLOAT16"), ("fp16", "FLOAT16"), ("half", "FLOAT16"),
("float32", "FLOAT"), ("fp32", "FLOAT"), ("float", "FLOAT"),
("double", "DOUBLE"), ("fp64", "DOUBLE"), ("float64", "DOUBLE"),
("int64", "INT64"), ("int32", "INT32"), ("int16", "INT16"), ("int8", "INT8"),
("int", "INT32"),
("uint64", "UINT64"), ("uint32", "UINT32"), ("uint16", "UINT16"), ("uint8", "UINT8"),
("bool", "BOOL"), ("boolean", "BOOL"),
("complex64", "COMPLEX64"), ("complex32", "COMPLEX32"),
]
_DTYPE_RE = re.compile(
r"\b(" + "|".join(sorted((a for a, _ in DTYPE_ALIASES), key=len, reverse=True)) + r")\b",
re.IGNORECASE)
_ALIAS_MAP = {a.lower(): n for a, n in DTYPE_ALIASES}
_FORMAT_RE = re.compile(r"\b(ND|NCHW|NHWC|NCDHW|NDHWC|NC1HWC0|FZ(?:_[A-Z0-9]+)?)\b")
_ATTR_VALUE_RE = re.compile(
r"(?:取值|枚举值|可选值|value)\s*[::]\s*([^\n|]+)"
r"|支持\s*([A-Za-z][A-Za-z0-9_]*(?:\s*[、,,/|\s]\s*[A-Za-z][A-Za-z0-9_]*)+)")
_ENUM_TOKEN_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]{1,31}$")
DEFAULT_REPOS = ["CANN-aclnn-api-reference", "ops-math", "ops-nn", "ops-cv", "ops-transformer"]
def log(tag, msg):
print("[{}] {}".format(tag, msg))
def find_md(api, repos):
cands = []
for repo in repos:
root = repo if os.path.isabs(repo) else os.path.join(os.getcwd(), repo)
if not os.path.isdir(root):
continue
for p in glob.glob(os.path.join(root, "**", api + "*.md"), recursive=True):
if any(seg.lower() == "experimental" for seg in p.replace("\\", "/").split("/")):
continue
cands.append(p)
if not cands:
return None
def prio(p):
b = os.path.basename(p)[:-3]
if b == api:
return 0
if b.split("&")[0] == api:
return 1
return 2
return sorted(cands, key=prio)[0]
def parse_md_tables(md_text):
"""解析 md 全部表格 → [(行首参数名, 整行文本)];兼容 <term> 标签。"""
rows = []
for line in md_text.split("\n"):
s = line.strip()
if not s.startswith("|"):
continue
cells = [re.sub(r"<[^>]+>", "", c).strip() for c in s.strip("|").split("|")]
if not cells or not cells[0] or set(cells[0]) <= set("-: "):
continue
rows.append((cells[0], " | ".join(cells)))
return rows
def extract_dtypes(text):
"""从文本提取规范 dtype 集合。"""
found = set()
for m in _DTYPE_RE.finditer(text):
found.add(_ALIAS_MAP[m.group(1).lower()])
return found
def extract_formats(text):
return set(_FORMAT_RE.findall(text))
def extract_attr_values(text):
"""从 attr 参数行文本提取取值枚举 token 集合(启发式)。"""
tokens = set()
for m in _ATTR_VALUE_RE.finditer(text):
seg = m.group(1) or m.group(2) or ""
for tok in re.split(r"[、,,/|;;\s]+", seg):
tok = tok.strip("`*")
if _ENUM_TOKEN_RE.match(tok) and tok.upper() not in ("ND", "AND", "OR", "NONE", "TRUE", "FALSE"):
tokens.add(tok)
return tokens
def main():
parser = argparse.ArgumentParser(
description="output 已有 test-design+cpp 的资料一致性校验(md 支持范围 vs test-design 记录范围)")
parser.add_argument("--api", required=True, help="aclnn 接口名,如 aclnnAbs")
parser.add_argument("--design_dir", default=None,
help="test-design 目录(默认 output/{api}/test-design)")
parser.add_argument("--repos", nargs="*", default=None,
help="aclnn 资料(md)查找根目录列表,默认当前目录下 CANN-aclnn-api-reference 与 ops-* 仓库")
parser.add_argument("--json", action="store_true", help="以 JSON 输出结果(Agent 消费)")
args = parser.parse_args()
design_dir = args.design_dir or os.path.join("output", args.api, "test-design")
param_yaml = os.path.join(design_dir, "03_参数定义.yaml")
if not os.path.isfile(param_yaml):
log("ERROR", "缺少 {}(output 目录无 test-design,无需一致性校验,走正常生成流程)".format(param_yaml))
return 3
design = yaml.safe_load(open(param_yaml, encoding="utf-8")) or {}
params = design.get("parameters", []) or []
design_dtypes = {}
design_formats = {}
attr_params = []
for p in params:
name = str(p.get("name", "")).strip()
if not name:
continue
dtypes = {str(d.get("dtype", "")).upper() for d in (p.get("dtype_with_ranges") or []) if d.get("dtype")}
design_dtypes[name] = dtypes
if p.get("format"):
design_formats[name] = str(p["format"]).upper()
if p.get("type") in ("aclScalar", "aclBoolArray", "aclIntArray") or \
(p.get("type") in ("int8_t", "int64_t", "uint64_t", "bool", "double", "float") and p.get("io_type") == "input"):
attr_params.append(name)
design_text_all = ""
for fn in ("03_参数定义.yaml", "05_约束定义.yaml"):
fp = os.path.join(design_dir, fn)
if os.path.isfile(fp):
design_text_all += open(fp, encoding="utf-8", errors="ignore").read()
repos = args.repos if args.repos else DEFAULT_REPOS
md_path = find_md(args.api, repos)
if not md_path:
log("CHECK", "未找到 {} 的 md 资料(查找根目录: {})→ 无法判定,按不一致处理走重新推导".format(
args.api, ", ".join(repos)))
result = {"api": args.api, "verdict": "unknown", "reason": "md 资料未找到",
"action": "按不一致处理:走完整重新推导(Step 1)"}
print(json.dumps(result, ensure_ascii=False, indent=2) if args.json else "")
return 3
md_text = open(md_path, encoding="utf-8", errors="ignore").read()
md_rows = parse_md_tables(md_text)
def md_text_for(name):
"""该参数的候选文本:表格行 + 全文中含参数名的行(仅该行,避免相邻表格行误引入)。"""
texts = [row_text for pname, row_text in md_rows
if pname == name or pname.startswith(name + "Optional")]
for ln in md_text.split("\n"):
if re.search(r"\b" + re.escape(name) + r"\b", ln):
texts.append(ln)
return "\n".join(texts[:10])
missing_dtype, extra_dtype, format_mismatch, attr_missing, no_signal = [], [], [], [], []
for name in design_dtypes:
text = md_text_for(name)
if not text:
no_signal.append(name)
continue
md_dt = extract_dtypes(text)
ym_dt = design_dtypes[name] or set()
if md_dt:
miss = md_dt - ym_dt
extra = ym_dt - md_dt
if miss:
missing_dtype.append({"param": name, "md有_design缺": sorted(miss), "md集合": sorted(md_dt)})
if extra:
extra_dtype.append({"param": name, "design有_md未列": sorted(extra)})
md_fmt = extract_formats(text)
ym_fmt = design_formats.get(name, "ND")
explicit_fmt = {f for f in md_fmt if f != "ND"} - {"NC1HWC0"}
if explicit_fmt and ym_fmt not in explicit_fmt and ym_fmt != "ND":
format_mismatch.append({"param": name, "md提及": sorted(explicit_fmt), "design": ym_fmt})
if name in attr_params:
vals = extract_attr_values(text)
if vals:
miss_vals = {v for v in vals if v not in design_text_all}
if miss_vals:
attr_missing.append({"param": name, "md取值_design未记录": sorted(miss_vals)})
inconsistent = bool(missing_dtype or format_mismatch or attr_missing)
needs_review = bool(extra_dtype)
if inconsistent:
verdict, action = "inconsistent", (
"增量更新 test-design(新增缺失信息,不推翻已有设计)→ 重新生成对应场景 CPP → 依次压测")
elif needs_review:
verdict, action = "review", (
"design 记录的 dtype 多于 md 资料列举(可能为资料滞后或设计冗余),需人工复核;"
"复核确认后按需更新 test-design 并重新生成 CPP")
else:
verdict, action = "consistent", "不再生成 CPP,直接使用 output 目录下的 cpp 进行压测"
result = {
"api": args.api,
"md": os.path.relpath(md_path, os.getcwd()) if os.path.isabs(md_path) else md_path,
"design_dir": design_dir,
"verdict": verdict,
"action": action,
"missing_dtype": missing_dtype,
"extra_dtype_review": extra_dtype,
"format_mismatch": format_mismatch,
"attr_value_missing": attr_missing,
"md_no_dtype_signal_params": no_signal,
"checked_params": len(design_dtypes),
}
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
log("CHECK", "api={} | md={} | 校验参数 {} 个".format(
args.api, result["md"], result["checked_params"]))
for item in missing_dtype:
log("GAP", "参数 {} 缺 dtype: {}(md 支持)".format(item["param"], ", ".join(item["md有_design缺"])))
for item in format_mismatch:
log("GAP", "参数 {} format 不一致: md={} design={}".format(
item["param"], "/".join(item["md提及"]), item["design"]))
for item in attr_missing:
log("GAP", "attr {} 取值 design 未记录: {}".format(
item["param"], ", ".join(item["md取值_design未记录"])))
for item in extra_dtype:
log("WARN", "参数 {} design 多出 dtype(md 未列,需复核): {}".format(
item["param"], ", ".join(item["design有_md未列"])))
if no_signal:
log("NOTE", "md 未提取到 dtype 信号的参数(跳过比对): {}".format(", ".join(no_signal)))
log("RESULT", "verdict={} → {}".format(verdict, action))
return 0 if verdict == "consistent" else (2 if verdict in ("inconsistent", "review") else 3)
if __name__ == "__main__":
sys.exit(main())