"""
根据 CI 变更文件生成构建命令
用法:
# UT 测试模式(默认)
python3 gen_ci_cmd.py -f changes.txt # 打印命令
python3 gen_ci_cmd.py -f changes.txt --exec # 直接执行
python3 gen_ci_cmd.py -f changes.txt --experimental=TRUE # 指定 experimental 算子构建
# CI 出包模式
python3 gen_ci_cmd.py -f changes.txt --pkg=TRUE # 生成出包命令
python3 gen_ci_cmd.py -f changes.txt --pkg=TRUE --experimental=TRUE # 出包 experimental 算子
# 示例运行模式
python3 gen_ci_cmd.py -f changes.txt --run_example=TRUE # 每个算子运行示例
CI用法
# UT
## experimental
bash build.sh -f pr_filelist.txt --experimental -u --cann_3rd_lib_path=/home/jenkins/opensource -j16
## 非experimental
bash build.sh -u -f pr_filelist.txt --cann_3rd_lib_path=/home/jenkins/opensource -j16
# 编译
## experimental
bash build.sh -f pr_filelist.txt --experimental --cann_3rd_lib_path=/home/jenkins/opensource -j16
## 非experimental
bash build.sh --pkg --jit --cann_3rd_lib_path=/home/jenkins/opensource -j16
#examples
## experimental
bash build.sh -f pr_filelist.txt --experimental --run_example
## 非experimental
bash build.sh -f pr_filelist.txt --run_example
"""
import argparse
import os
import re
import subprocess
import sys
SOC_TO_ARCH = {
"ascend031": "dav_3004",
"ascend035": "dav_3505",
"ascend310b": "dav_3002",
"ascend310p": "dav_2002",
"ascend910_93": "dav_2201",
"ascend950": "dav_3510",
"ascend350": "dav_3510",
"ascend910b": "dav_2201",
"ascend910": "dav_1001",
"kirinx90": "dav_3003",
"kirin9030": "dav_3113",
"mc62": "dav_5102",
"ascend5162a": "dav_5162",
"ascend960dt": "dav_9201",
"ascend960pr": "dav_9202",
"ascend630": "dav_5102",
"ascend610lite": "dav_3102",
}
def soc_to_arch(soc):
"""按 socVersion 查询 npuArch(小写),未匹配返回空串"""
return SOC_TO_ARCH.get(soc.lower(), "")
DEFAULT_SOC = "ascend910b"
DEFAULT_EXP_OP = "acos"
DEFAULT_NORMAL_OP = "is_finite"
DEFAULT_UTS = {"ophost", "opapi", "opkernel", "opgraph"}
RULES = [
{"pattern": r"experimental/(?:math|conversion|random)/([^/]+)", "type": "exp_ops"},
{"pattern": r"^(?:math|conversion|random)/([^/]+)", "type": "ops"},
{
"pattern": r"^common/(?:src/framework|tests/ut/framework)/(?:test_)?([^/]+)_onnx_plugin\.cpp$",
"type": "ops",
},
]
DEFAULT_FILTER_SOC = "ascend910b"
def check_op_supports_soc(op_name, soc, is_experimental=False):
"""检查算子是否支持指定的SOC
检查方法:查找算子的 op_host/*def.cpp 文件,以下任一形式即视为支持:
- AddConfig("{soc}"):按 socVersion 直接注册
- AddConfigByArch(..., Ops::Base::DAV_xxx ...):按 npuArch 注册,
通过本文件 SOC_TO_ARCH 的 socVersion -> npuArch 映射换算
Args:
op_name: 算子名称
soc: SOC名称,如 'ascend910b'
is_experimental: 是否为 experimental 算子
Returns:
bool: 是否支持该SOC
"""
prefixes = ["math/", "conversion/", "random/"]
if is_experimental:
prefixes = [
"experimental/math/",
"experimental/conversion/",
"experimental/random/",
]
arch = soc_to_arch(soc)
arch_pattern = None
if arch:
arch_pattern = re.compile(
r"AddConfigByArch\s*\([^;]*\b%s\b" % re.escape(arch.upper()), re.IGNORECASE
)
for prefix in prefixes:
op_host_dir = os.path.join(prefix, op_name, "op_host")
if not os.path.isdir(op_host_dir):
continue
for filename in os.listdir(op_host_dir):
if filename.endswith("_def.cpp"):
def_file = os.path.join(op_host_dir, filename)
try:
with open(def_file, "r", encoding="utf-8") as f:
content = f.read()
if f'AddConfig("{soc}"' in content:
return True
if arch_pattern and arch_pattern.search(content):
return True
except (IOError, OSError):
continue
return False
def filter_ops_by_soc_support(ops, soc, is_experimental=False):
"""过滤出支持指定SOC的算子
Args:
ops: 算子集合
soc: 目标SOC
is_experimental: 是否为 experimental 算子
Returns:
set: 支持该SOC的算子集合
"""
supported_ops = set()
for op in ops:
if check_op_supports_soc(op, soc, is_experimental):
supported_ops.add(op)
return supported_ops
def check_op_has_op_kernel(op_name, is_experimental=False):
"""检查算子是否存在 op_kernel 目录
出包模式需要生成 kernel 二进制,没有 op_kernel 目录的算子无法出包。
Args:
op_name: 算子名称
is_experimental: 是否为 experimental 算子
Returns:
bool: 是否存在 op_kernel 目录
"""
prefixes = ["math/", "conversion/", "random/"]
if is_experimental:
prefixes = [
"experimental/math/",
"experimental/conversion/",
"experimental/random/",
]
for prefix in prefixes:
op_kernel_dir = os.path.join(prefix, op_name, "op_kernel")
if os.path.isdir(op_kernel_dir):
return True
return False
def filter_ops_by_op_kernel(ops, is_experimental=False):
"""过滤出存在 op_kernel 目录的算子
Args:
ops: 算子集合
is_experimental: 是否为 experimental 算子
Returns:
set: 存在 op_kernel 目录的算子集合
"""
return {op for op in ops if check_op_has_op_kernel(op, is_experimental)}
def read_file_lines(filepath):
"""读取文件并返回非空、非注释行列表
Args:
filepath: 文件路径
Returns:
list: 行列表,文件不存在返回空列表
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
return [
line.strip()
for line in f
if line.strip() and not line.strip().startswith("#")
]
except (FileNotFoundError, PermissionError, UnicodeDecodeError):
return []
def parse_changed_files(filepath):
"""解析变更文件,提取算子
Returns:
dict: {'exp_ops': set(), 'normal_ops': set()},文件不存在返回空集合字典
"""
exp_ops, normal_ops = set(), set()
files = read_file_lines(filepath)
for file_path in files:
if file_path.endswith(".md"):
continue
if not os.path.exists(file_path):
continue
for rule in RULES:
m = re.search(rule["pattern"], file_path)
if not m:
continue
rule_type = rule["type"]
if rule_type == "exp_ops":
exp_ops.add(m.group(1))
elif rule_type == "ops":
normal_ops.add(m.group(1))
return {"exp_ops": exp_ops, "normal_ops": normal_ops}
def get_op_ut_types(op_name, files, is_experimental):
"""获取单个算子的 UT 类型和 SOC
Returns:
tuple: (uts, socs)
- uts: UT 类型集合,如 {'ophost', 'opapi'}
- socs: 该算子检测到的 SOC 集合(通过 arch35 目录),如 {'ascend950'}
"""
uts = set()
socs = set()
prefixes = ["math/", "conversion/", "random/"]
if is_experimental:
prefixes = [
"experimental/math/",
"experimental/conversion/",
"experimental/random/",
]
for f in files:
matched = False
for prefix in prefixes:
if f.startswith(f"{prefix}{op_name}/"):
matched = True
break
if matched:
if (
"/op_host/" in f
or "/tests/ut/op_host/" in f
or "/framework/" in f
or "/tests/ut/framework/" in f
):
uts.add("ophost")
if "/op_api/" in f or "/tests/ut/op_api/" in f:
uts.add("opapi")
if "/op_kernel/" in f or "/tests/ut/op_kernel/" in f:
uts.add("opkernel")
if "/op_graph/" in f or "/tests/ut/op_graph/" in f:
uts.add("opgraph")
if "/arch35/" in f:
socs.add("ascend950")
return uts, socs
def make_command(op, uts, soc, cann_3rd_lib_path=None, is_experimental=False):
"""生成单个算子的构建命令"""
cmd_parts = ["bash", "build.sh", "-u", "-j16"]
if is_experimental:
cmd_parts.append("--experimental")
cmd_parts.append(f"--ops={op}")
for ut in sorted(uts if uts else DEFAULT_UTS):
cmd_parts.append(f"--{ut}")
cmd_parts.append(f"--soc={soc}")
if cann_3rd_lib_path:
cmd_parts.append(f"--cann_3rd_lib_path={cann_3rd_lib_path}")
return cmd_parts
def make_merged_command(
merged_ops, ut_type, soc, cann_3rd_lib_path=None, is_experimental=False
):
"""生成合并后的构建命令(多个算子合并到一条命令)
合并策略:按 (SOC, UT类型) 分组合并算子,减少命令数量
合并规则:
- opapi/opgraph: 只跑默认 SOC (ascend910b),所有算子合并为一条命令
- ophost/opkernel: 按 SOC 分组(有 arch35 变更时跑多个 SOC),同一 SOC 的算子合并
"""
cmd_parts = ["bash", "build.sh", "-u", "-j16"]
if is_experimental:
cmd_parts.append("--experimental")
cmd_parts.append(f"--ops={merged_ops}")
cmd_parts.append(f"--{ut_type}")
cmd_parts.append(f"--soc={soc}")
if cann_3rd_lib_path:
cmd_parts.append(f"--cann_3rd_lib_path={cann_3rd_lib_path}")
return cmd_parts
def make_run_example_command(op_name, mode, is_experimental=False):
"""生成算子示例运行命令
命令格式:bash build.sh [--experimental] --run_example $opname $mode cust [--vendor_name=experimental]
Args:
op_name: 算子名称
mode: 运行模式,'eager' 或 'graph'
is_experimental: 是否为 experimental 算子
"""
if is_experimental:
cmd_parts = [
"bash",
"build.sh",
"--experimental",
"--run_example",
op_name,
mode,
"cust",
"--vendor_name=experimental",
]
else:
cmd_parts = ["bash", "build.sh", "--run_example", op_name, mode, "cust"]
return cmd_parts
def build_ut_commands(filepath, experimental=False, cann_3rd_lib_path=None):
"""构建 UT 命令列表
按算子维度检测 SOC(arch35),每个算子只跑它实际支持的 SOC。
Args:
filepath: 变更文件路径
experimental: 是否为 experimental 算子
cann_3rd_lib_path: 第三方库路径
"""
parsed = parse_changed_files(filepath)
files = read_file_lines(filepath)
exp_ops = parsed["exp_ops"]
normal_ops = parsed["normal_ops"]
ops = exp_ops if experimental else normal_ops
default_op = DEFAULT_EXP_OP if experimental else DEFAULT_NORMAL_OP
if not ops:
return [
make_command(
default_op, DEFAULT_UTS, DEFAULT_SOC, cann_3rd_lib_path, experimental
)
]
groups = {}
for op in ops:
uts, op_socs = get_op_ut_types(op, files, experimental)
if not uts and any(
re.search(
rf"common/(?:src/framework|tests/ut/framework)/(?:test_)?{re.escape(op)}_onnx_plugin\.cpp$",
f,
)
for f in files
):
uts = {"ophost"}
op_socs_to_run = {DEFAULT_SOC} | op_socs
if "opapi" in uts:
key = (DEFAULT_SOC, "opapi")
groups.setdefault(key, set()).add(op)
if "opgraph" in uts:
key = (DEFAULT_SOC, "opgraph")
groups.setdefault(key, set()).add(op)
host_kernel_uts = uts - {"opapi", "opgraph"}
for ut in host_kernel_uts:
for soc in op_socs_to_run:
key = (soc, ut)
groups.setdefault(key, set()).add(op)
commands = []
for (soc, ut_type), op_names in sorted(groups.items()):
merged_ops = ",".join(sorted(op_names))
cmd = make_merged_command(
merged_ops, ut_type, soc, cann_3rd_lib_path, experimental
)
commands.append(cmd)
seen = set()
unique_commands = []
for cmd in commands:
key = tuple(cmd)
if key not in seen:
seen.add(key)
unique_commands.append(cmd)
return unique_commands
def check_op_examples(op_name, is_experimental):
"""检查算子 examples 目录下的测试文件类型
Args:
op_name: 算子名称
is_experimental: 是否为 experimental 算子
Returns:
dict: {'has_eager': bool, 'has_graph': bool}
"""
prefixes = ["math/", "conversion/", "random/"]
if is_experimental:
prefixes = [
"experimental/math/",
"experimental/conversion/",
"experimental/random/",
]
result = {"has_eager": False, "has_graph": False}
for prefix in prefixes:
examples_dir = os.path.join(prefix, op_name, "examples")
if not os.path.isdir(examples_dir):
continue
try:
for filename in os.listdir(examples_dir):
if filename.startswith("test_aclnn") and filename.endswith(".cpp"):
result["has_eager"] = True
if filename.startswith("test_geir") and filename.endswith(".cpp"):
result["has_graph"] = True
except OSError:
continue
return result
def build_example_commands(filepath, experimental=False):
"""构建 run_example 命令列表
根据算子 examples 目录下的测试文件类型生成命令:
- 存在 test_aclnn*.cpp → 生成 eager 命令
- 存在 test_geir*.cpp → 生成 graph 命令
Args:
filepath: 变更文件路径
experimental: 是否为 experimental 算子
示例:
bash build.sh --run_example add_n eager cust
bash build.sh --run_example add_n graph cust
"""
parsed = parse_changed_files(filepath)
exp_ops = parsed["exp_ops"]
normal_ops = parsed["normal_ops"]
ops = exp_ops if experimental else normal_ops
default_op = DEFAULT_EXP_OP if experimental else DEFAULT_NORMAL_OP
ops = filter_ops_by_soc_support(ops, DEFAULT_FILTER_SOC, experimental)
if not ops:
ops = {default_op}
commands = []
for op in sorted(ops):
example_check = check_op_examples(op, experimental)
if example_check["has_eager"]:
commands.append(make_run_example_command(op, "eager", experimental))
return commands
def make_package_command(merged_ops, cann_3rd_lib_path=None, is_experimental=False):
"""生成出包构建命令
命令格式:bash build.sh --pkg -j16 --ops=op1,op2 --vendor_name=experimental/custom
生成的包名:
experimental=FALSE: cann-ops-math-custom_linux-x86_64.run
experimental=TRUE: cann-ops-math-experimental_linux-x86_64.run
Args:
merged_ops: 合并后的算子列表字符串,如 "op1,op2,op3"
cann_3rd_lib_path: 可选的第三方库路径
is_experimental: 是否为 experimental 算子
"""
cmd_parts = ["bash", "build.sh", "--pkg", "-j16"]
if is_experimental:
cmd_parts.append("--experimental")
cmd_parts.append("--vendor_name=experimental")
else:
cmd_parts.append("--vendor_name=custom")
cmd_parts.append(f"--ops={merged_ops}")
if cann_3rd_lib_path:
cmd_parts.append(f"--cann_3rd_lib_path={cann_3rd_lib_path}")
return cmd_parts
def build_package_commands(filepath, experimental=False, cann_3rd_lib_path=None):
"""构建出包命令列表
出包模式特点:
- 不区分 UT 类型 和 SOC
- 所有算子合并到一条命令
- 仅保留支持默认SOC且存在 op_kernel 目录的算子(出包需要 kernel 二进制)
Args:
filepath: 变更文件路径
experimental: 是否为 experimental 算子
cann_3rd_lib_path: 第三方库路径
示例:
bash build.sh --pkg -j16 --ops=op1,op2,op3
"""
parsed = parse_changed_files(filepath)
exp_ops = parsed["exp_ops"]
normal_ops = parsed["normal_ops"]
ops = exp_ops if experimental else normal_ops
default_op = DEFAULT_EXP_OP if experimental else DEFAULT_NORMAL_OP
ops = filter_ops_by_soc_support(ops, DEFAULT_FILTER_SOC, experimental)
ops_with_kernel = filter_ops_by_op_kernel(ops, experimental)
dropped_no_kernel = ops - ops_with_kernel
if dropped_no_kernel:
print(
f"警告: 以下算子无 op_kernel 目录,出包命令中丢弃: {','.join(sorted(dropped_no_kernel))}",
flush=True,
)
ops = ops_with_kernel
if not ops:
return [make_package_command(default_op, cann_3rd_lib_path, experimental)]
merged_ops = ",".join(sorted(ops))
cmd = make_package_command(merged_ops, cann_3rd_lib_path, experimental)
return [cmd]
def print_commands(commands):
"""打印生成的命令列表"""
print(f"生成 {len(commands)} 条命令:", flush=True)
for cmd in commands:
print(f" {' '.join(cmd)}", flush=True)
print(flush=True)
def execute_commands(commands, mode="ut"):
"""执行命令列表并根据模式打印结果
Args:
commands: 命令列表
mode: 执行模式,可选 'ut', 'pkg', 'example'
Returns:
int: 0 表示全部成功,1 表示有命令失败
"""
for cmd in commands:
print(f"执行: {' '.join(cmd)}", flush=True)
result = subprocess.run(cmd)
if result.returncode != 0:
print(
f"run {mode} fail: {' '.join(cmd)} (返回码: {result.returncode})",
flush=True,
)
return 1
print(f"命令成功: {' '.join(cmd)}", flush=True)
return 0
def main():
parser = argparse.ArgumentParser(description="根据 CI 变更文件生成构建命令")
parser.add_argument("-f", "--file", required=True, help="变更文件列表 必选参数")
parser.add_argument(
"--exec", action="store_true", help="直接执行生成的命令 可选参数"
)
parser.add_argument(
"--experimental",
choices=["TRUE", "FALSE"],
default="FALSE",
help="可选参数 默认FALSE TRUE表示跑experimental目录下的用例 检测不到算子跑默认experimental目录下的acos算子 "
"FALSE表示跑基本算子的用例 检查不到算子跑math目录下的is_finite 是否指定 experimental 算子构建 (TRUE/FALSE)",
)
parser.add_argument(
"--pkg",
choices=["TRUE", "FALSE"],
default="FALSE",
help="是否生成出包命令 (TRUE/FALSE) 可选参数 默认是FALSE TRUE表示对涉及变更的算子打自定义算子包 "
"FALSE 不打包 只跑UT",
)
parser.add_argument(
"--run_example",
choices=["TRUE", "FALSE"],
default="FALSE",
help="是否生成 run_example 命令 (TRUE/FALSE) 可选参数 默认FALSE TRUE表示每个算子运行示例",
)
parser.add_argument(
"--cann_3rd_lib_path", help="可选参数 CANN third party lib path"
)
parser.add_argument(
"--list_ops",
action="store_true",
help="仅输出变更涉及的算子名列表(逗号分隔),不生成命令",
)
args = parser.parse_args()
if args.list_ops:
parsed = parse_changed_files(args.file)
ops = parsed["exp_ops"] if args.experimental == "TRUE" else parsed["normal_ops"]
if ops:
print(",".join(sorted(ops)))
return
if args.run_example == "TRUE":
commands = build_example_commands(args.file, args.experimental == "TRUE")
mode = "example"
elif args.pkg == "TRUE":
commands = build_package_commands(
args.file, args.experimental == "TRUE", args.cann_3rd_lib_path
)
mode = "pkg"
else:
commands = build_ut_commands(
args.file, args.experimental == "TRUE", args.cann_3rd_lib_path
)
mode = "ut"
print_commands(commands)
if args.exec:
sys.exit(execute_commands(commands, mode))
if __name__ == "__main__":
main()