已合并
【feature】统一帮助信息规范 #953
kun_8创建于 8月15日
【feature】统一帮助信息规范 #953
已合并
kun_8创建于 8月15日
13 个文件变更+643-115
@@ -15,11 +15,11 @@
15# -------------------------------------------------------------------------15# -------------------------------------------------------------------------
16 16 
17import argparse17import argparse
18-import sys
19import json18import json
20import os19import os
21from msprobe.core.common.const import Const20from msprobe.core.common.const import Const
22from msprobe.core.common.file_utils import check_file_or_directory_path21from msprobe.core.common.file_utils import check_file_or_directory_path
22+from msprobe.core.common.cli_help import MindStudioArgumentParser
23 23 
24 24 
25def _detect_framework_from_api_info(api_info_path: str) -> str:25def _detect_framework_from_api_info(api_info_path: str) -> str:
@@ -62,9 +62,12 @@ def acc_check_cli(argv):
62 62 
63 # 只输 -h,没带 -api_info → 打印基础帮助63 # 只输 -h,没带 -api_info → 打印基础帮助
64 if not has_api_info:64 if not has_api_info:
65- pre_parser = argparse.ArgumentParser(add_help=True, prog="msprobe acc_check")65+ pre_parser = MindStudioArgumentParser(add_help=True, prog="msprobe acc_check")
66- pre_parser.add_argument("-api_info", required=True,66+ pre_parser.add_argument(
67- help="Path to API info JSON file. Used to determine PyTorch or MindSpore pre-check.")67+ "-api_info",
68+ required=True,
69+ help="Path to API info JSON file. Used to determine PyTorch or MindSpore pre-check.",
70+ )
68 pre_parser.print_help()71 pre_parser.print_help()
69 return72 return
70 73 
@@ -79,10 +82,10 @@ def acc_check_cli(argv):
79 # PyTorch 路径:使用原来的 PT acc_check 实现82 # PyTorch 路径:使用原来的 PT acc_check 实现
80 from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import _acc_check_parser, acc_check_command83 from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import _acc_check_parser, acc_check_command
81 84 
82- pt_parser = argparse.ArgumentParser(85+ pt_parser = MindStudioArgumentParser(
83 prog="msprobe acc_check",86 prog="msprobe acc_check",
84 formatter_class=argparse.RawDescriptionHelpFormatter,87 formatter_class=argparse.RawDescriptionHelpFormatter,
85- description="Run PyTorch acc_check with msprobe."88+ description="Run PyTorch acc_check with msprobe.",
86 )89 )
87 _acc_check_parser(pt_parser) # 这里会给 parser 加上原来的所有 PT acc_check 参数(包括 -api_info)90 _acc_check_parser(pt_parser) # 这里会给 parser 加上原来的所有 PT acc_check 参数(包括 -api_info)
88 91 
@@ -94,10 +97,10 @@ def acc_check_cli(argv):
94 from msprobe.mindspore.api_accuracy_checker.cmd_parser import add_api_accuracy_checker_argument97 from msprobe.mindspore.api_accuracy_checker.cmd_parser import add_api_accuracy_checker_argument
95 from msprobe.mindspore.api_accuracy_checker.main import api_checker_main98 from msprobe.mindspore.api_accuracy_checker.main import api_checker_main
96 99 
97- ms_parser = argparse.ArgumentParser(100+ ms_parser = MindStudioArgumentParser(
98 prog="msprobe acc_check",101 prog="msprobe acc_check",
99 formatter_class=argparse.RawDescriptionHelpFormatter,102 formatter_class=argparse.RawDescriptionHelpFormatter,
100- description="Run MindSpore Check with msprobe."103+ description="Run MindSpore Check with msprobe.",
101 )104 )
102 add_api_accuracy_checker_argument(ms_parser) # 给 acc_check 的 parser 加上原来 MS 的所有参数105 add_api_accuracy_checker_argument(ms_parser) # 给 acc_check 的 parser 加上原来 MS 的所有参数
103 106 
@@ -115,9 +118,12 @@ def multi_acc_check_cli(argv):
115 118 
116 # ====================== 情况1:只输 -h,没带 -api_info → 打印基础帮助 ======================119 # ====================== 情况1:只输 -h,没带 -api_info → 打印基础帮助 ======================
117 if not has_api_info:120 if not has_api_info:
118- pre_parser = argparse.ArgumentParser(add_help=True, prog="msprobe multi_acc_check")121+ pre_parser = MindStudioArgumentParser(add_help=True, prog="msprobe multi_acc_check")
119- pre_parser.add_argument("-api_info", required=True,122+ pre_parser.add_argument(
120- help="Path to API info JSON file. Used to determine PyTorch or MindSpore pre-check.")123+ "-api_info",
124+ required=True,
125+ help="Path to API info JSON file. Used to determine PyTorch or MindSpore pre-check.",
126+ )
121 pre_parser.print_help()127 pre_parser.print_help()
122 return128 return
123 129 
@@ -130,26 +136,26 @@ def multi_acc_check_cli(argv):
130 # 检测框架136 # 检测框架
131 framework = _detect_framework_from_api_info(pre_args.api_info)137 framework = _detect_framework_from_api_info(pre_args.api_info)
132 138 
133- 
134 if framework == Const.PT_FRAMEWORK:139 if framework == Const.PT_FRAMEWORK:
135 # PyTorch 多进程路径:沿用原来的 prepare_config + run_parallel_ut140 # PyTorch 多进程路径:沿用原来的 prepare_config + run_parallel_ut
136 from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import _acc_check_parser141 from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import _acc_check_parser
137 from msprobe.pytorch.api_accuracy_checker.acc_check.multi_acc_check import prepare_config, run_parallel_ut142 from msprobe.pytorch.api_accuracy_checker.acc_check.multi_acc_check import prepare_config, run_parallel_ut
138 143 
139- pt_parser = argparse.ArgumentParser(144+ pt_parser = MindStudioArgumentParser(
140 prog="msprobe multi_acc_check",145 prog="msprobe multi_acc_check",
141 formatter_class=argparse.RawDescriptionHelpFormatter,146 formatter_class=argparse.RawDescriptionHelpFormatter,
142- description="Run PyTorch acc_check in parallel with msprobe."147+ description="Run PyTorch acc_check in parallel with msprobe.",
143 )148 )
144 _acc_check_parser(pt_parser)149 _acc_check_parser(pt_parser)
145 pt_parser.add_argument(150 pt_parser.add_argument(
146- "-n", "--num_splits",151+ "-n",
152+ "--num_splits",
147 type=int,153 type=int,
148 choices=range(1, 65),154 choices=range(1, 65),
149 default=8,155 default=8,
150- help="Number of splits for parallel processing. Range: 1-64"156+ help="Number of splits for parallel processing. Range: 1-64",
151 )157 )
152- 158+ 
153 pt_args = pt_parser.parse_args(argv)159 pt_args = pt_parser.parse_args(argv)
154 config = prepare_config(pt_args)160 config = prepare_config(pt_args)
155 run_parallel_ut(config)161 run_parallel_ut(config)
@@ -159,13 +165,12 @@ def multi_acc_check_cli(argv):
159 from msprobe.mindspore.api_accuracy_checker.cmd_parser import multi_add_api_accuracy_checker_argument165 from msprobe.mindspore.api_accuracy_checker.cmd_parser import multi_add_api_accuracy_checker_argument
160 from msprobe.mindspore.api_accuracy_checker.main import mul_api_checker_main166 from msprobe.mindspore.api_accuracy_checker.main import mul_api_checker_main
161 167 
162- ms_parser = argparse.ArgumentParser(168+ ms_parser = MindStudioArgumentParser(
163 prog="msprobe multi_acc_check",169 prog="msprobe multi_acc_check",
164 formatter_class=argparse.RawDescriptionHelpFormatter,170 formatter_class=argparse.RawDescriptionHelpFormatter,
165- description="Run MindSpore Check in parallel with msprobe."171+ description="Run MindSpore Check in parallel with msprobe.",
166 )172 )
167 multi_add_api_accuracy_checker_argument(ms_parser)173 multi_add_api_accuracy_checker_argument(ms_parser)
168 174 
169 ms_args = ms_parser.parse_args(argv)175 ms_args = ms_parser.parse_args(argv)
170 mul_api_checker_main(ms_args)176 mul_api_checker_main(ms_args)
171- 
@@ -0,0 +1,392 @@
1+# -------------------------------------------------------------------------
2+# This file is part of the MindStudio project.
3+# Copyright (c) 2026 Huawei Technologies Co.,Ltd.
4+#
5+# MindStudio is licensed under Mulan PSL v2.
6+# You can use this software according to the terms and conditions of the Mulan PSL v2.
7+# You may obtain a copy of Mulan PSL v2 at:
8+#
9+# http://license.coscl.org.cn/MulanPSL2
10+#
11+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
12+# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
13+# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
14+# See the Mulan PSL v2 for more details.
15+# -------------------------------------------------------------------------
16+ 
17+"""MindStudio-compliant command line help rendering.
18+ 
19+The public CLI uses one renderer so that help text remains consistent when
20+commands and arguments are added in different modules. Legacy ``<Required>``,
21+``<Optional>``, and ``<Mandatory>`` help-text prefixes are removed during
22+rendering. New arguments must express requiredness through their argparse
23+configuration (for example, ``required=True``) instead of a help-text prefix.
24+"""
25+ 
26+import argparse
27+import os
28+import re
29+import shutil
30+import textwrap
31+from dataclasses import dataclass
32+from typing import Dict, Iterable, Optional, Sequence, Tuple
33+ 
34+ 
35+@dataclass(frozen=True)
36+class HelpSpec:
37+ description: str
38+ usage: Optional[str]
39+ examples: Tuple[Tuple[str, str], ...]
40+ output: Tuple[str, ...] = ()
41+ troubleshooting: Tuple[str, ...] = ()
42+ 
43+ 
44+HELP_SPECS: Dict[str, HelpSpec] = {
45+ "msprobe": HelpSpec(
46+ "Inspect, compare, and analyze model data on Ascend systems.",
47+ "msprobe <command> [options]",
48+ (("Show help for a command", "msprobe compare -h"),),
49+ ),
50+ "msprobe compare": HelpSpec(
51+ "Compare target-side dump data with golden reference data and report accuracy differences.",
52+ "msprobe compare --target_path <DIR> --golden_path <DIR> [options]",
53+ (
54+ ("Compare two dump directories", "msprobe compare --target_path ./npu_dump --golden_path ./golden_dump"),
55+ (
56+ "Write results to a custom directory",
57+ "msprobe compare --target_path ./npu_dump --golden_path ./golden_dump --output_path ./result",
58+ ),
59+ ),
60+ ("<output_path>/compare_result_{timestamp}.csv (or .xlsx)",),
61+ ),
62+ "msprobe acc_check": HelpSpec(
63+ "Run API accuracy checks using the framework recorded in the API information file.",
64+ "msprobe acc_check -api_info <FILE> [options]",
65+ (("Run an API accuracy check", "msprobe acc_check -api_info ./dump.json"),),
66+ ("<out_path>/accuracy_checking_result_{timestamp}.csv",),
67+ ),
68+ "msprobe multi_acc_check": HelpSpec(
69+ "Run API accuracy checks in parallel using the framework recorded in the API information file.",
70+ "msprobe multi_acc_check -api_info <FILE> [options]",
71+ (("Run parallel API accuracy checks", "msprobe multi_acc_check -api_info ./dump.json"),),
72+ ("<out_path>/accuracy_checking_result_{timestamp}.csv",),
73+ ),
74+ "msprobe merge_result": HelpSpec(
75+ "Merge distributed accuracy comparison results into one result set.",
76+ "msprobe merge_result --input_dir <DIR> --output_dir <DIR> --config-path <FILE> [options]",
77+ (
78+ (
79+ "Merge comparison results",
80+ "msprobe merge_result --input_dir ./results --output_dir ./merged --config-path ./merge.yaml",
81+ ),
82+ ),
83+ ("<output_dir>/",),
84+ ),
85+ "msprobe overflow_check": HelpSpec(
86+ "Analyze dumped tensor data and report overflow information.",
87+ "msprobe overflow_check --input_path <DIR> [options]",
88+ (("Analyze one dump step", "msprobe overflow_check --input_path ./dump/step_0"),),
89+ ("<output_path>/",),
90+ ),
91+ "msprobe config_check": HelpSpec(
92+ "Collect, compare, or verify training configuration data.",
93+ "msprobe config_check <operation> [values] [options]",
94+ (("Collect the current training configuration", "msprobe config_check -d ./train.sh"),),
95+ ("<output>/",),
96+ ),
97+ "msprobe api_precision_compare": HelpSpec(
98+ "Compare API accuracy result files produced on NPU and GPU devices.",
99+ "msprobe api_precision_compare --npu_csv_path <FILE> --gpu_csv_path <FILE> [options]",
100+ (
101+ (
102+ "Compare two accuracy reports",
103+ "msprobe api_precision_compare --npu_csv_path ./npu.csv --gpu_csv_path ./gpu.csv",
104+ ),
105+ ),
106+ ("<out_path>/",),
107+ ),
108+ "msprobe graph_visualize": HelpSpec(
109+ "Build graph visualization data from one or two dump directories.",
110+ "msprobe graph_visualize --target_path <DIR> --output_path <DIR> [options]",
111+ (("Build visualization data", "msprobe graph_visualize --target_path ./dump --output_path ./graph"),),
112+ ("<output_path>/",),
113+ ),
114+ "msprobe data2db": HelpSpec(
115+ "Import dump or monitor data into a SQLite database.",
116+ "msprobe data2db --db <DIR> --data <DIR> [options]",
117+ (("Import data with automatic format detection", "msprobe data2db --db ./database --data ./dump"),),
118+ ("<db>/",),
119+ ),
120+ "msprobe parse": HelpSpec(
121+ "Parse dumped tensor data into NumPy or PyTorch files.",
122+ "msprobe parse --dump_path <FILE_OR_DIR> [options]",
123+ (("Parse dump data as PyTorch files", "msprobe parse --dump_path ./dump --type pt --output_path ./output"),),
124+ ("<output_path>/",),
125+ ),
126+ "msprobe offline_dump": HelpSpec(
127+ "Run an offline model and dump intermediate model data for analysis.",
128+ "msprobe offline_dump --model_path <FILE> [options]",
129+ (("Dump an offline model", "msprobe offline_dump --model_path ./model.om -o ./output"),),
130+ ("<output_path>/",),
131+ ),
132+ "msprobe install_deps": HelpSpec(
133+ "Install optional dependencies required by a selected msprobe mode.",
134+ "msprobe install_deps --mode <MODE> [options]",
135+ (("Install offline-mode dependencies", "msprobe install_deps --mode offline"),),
136+ ),
137+ "anomaly_processor": HelpSpec(
138+ "Analyze anomaly detection results and report the earliest high-priority anomalies.",
139+ None,
140+ (("Analyze monitor anomalies", "anomaly_processor --data_path ./anomalies"),),
141+ ("<out_path>/",),
142+ ),
143+ "gen_model_config": HelpSpec(
144+ "Generate response-anomaly model and token-category configuration files.",
145+ None,
146+ (("Generate configuration for a local model", "gen_model_config --model-path ./model"),),
147+ ("../configs/mtype_config.json", "../token2category/{model-name}_{vocab-size}.json"),
148+ ),
149+ "acc_check": HelpSpec(
150+ "Run PyTorch API accuracy checks from an API information file.",
151+ "acc_check --api_info_file <FILE> [options]",
152+ (("Run an API accuracy check", "acc_check --api_info_file ./dump.json"),),
153+ ("<out_path>/accuracy_checking_result_{timestamp}.csv",),
154+ ),
155+ "multi_acc_check": HelpSpec(
156+ "Run PyTorch API accuracy checks in parallel.",
157+ "multi_acc_check --api_info_file <FILE> [options]",
158+ (("Run parallel API accuracy checks", "multi_acc_check --api_info_file ./dump.json"),),
159+ ("<out_path>/accuracy_checking_result_{timestamp}.csv",),
160+ ),
161+ "run_overflow_check": HelpSpec(
162+ "Replay API calls and report tensor overflow conditions.",
163+ "run_overflow_check --api_info_file <FILE> [options]",
164+ (("Check APIs for overflow", "run_overflow_check --api_info_file ./dump.json"),),
165+ ),
166+ "api_precision_compare": HelpSpec(
167+ "Compare API accuracy result files produced on NPU and GPU devices.",
168+ "api_precision_compare --npu_csv_path <FILE> --gpu_csv_path <FILE> [options]",
169+ (("Compare two accuracy reports", "api_precision_compare --npu_csv_path ./npu.csv --gpu_csv_path ./gpu.csv"),),
170+ ("<out_path>/",),
171+ ),
172+}
173+ 
174+ 
175+_METAVAR_BY_DEST = {
176+ "api_info": "<FILE>",
177+ "api_info_file": "<FILE>",
178+ "config_path": "<FILE>",
179+ "input_file": "<FILE>",
180+ "npu_csv_path": "<FILE>",
181+ "gpu_csv_path": "<FILE>",
182+ "mapping": "<FILE>",
183+ "cell_mapping": "<FILE>",
184+ "api_mapping": "<FILE>",
185+ "data_mapping": "<FILE>",
186+ "layer_mapping": "<FILE>",
187+ "fusion_rule_file": "<FILE>",
188+ "quant_fusion_rule_file": "<FILE>",
189+ "close_fusion_rule_file": "<FILE>",
190+ "dump_path": "<FILE_OR_DIR>",
191+ "target_path": "<DIR>",
192+ "golden_path": "<DIR>",
193+ "input_path": "<DIR>",
194+ "input_dir": "<DIR>",
195+ "output_dir": "<DIR>",
196+ "output_path": "<DIR>",
197+ "out_path": "<DIR>",
198+ "db": "<DIR>",
199+ "data": "<DIR>",
200+ "mode": "<MODE>",
201+ "format": "<FORMAT>",
202+ "rank": "<ID>",
203+ "device": "<ID>",
204+ "device_id": "<ID>",
205+ "step": "<N>",
206+ "process_num": "<N>",
207+ "num_splits": "<N>",
208+}
209+ 
210+_LABEL_RE = re.compile(r"^\s*[<\[]\s*(?:required|optional|mandatory)[^>\]]*[>\]]\s*[,.:;-]?\s*", re.IGNORECASE)
211+_PAREN_DEFAULT_RE = re.compile(r"\s*\((?:default\s*[:=]|default is)\s*[^)]*\)\s*", re.IGNORECASE)
212+_DEFAULT_MARKER_RE = re.compile(r"\s*\(default\)\s*", re.IGNORECASE)
213+_TEXT_DEFAULT_RE = re.compile(r"\s*(?:the\s+)?default(?:\s+[a-z_-]+){0,3}\s*(?:is|:|=)\s*[^.;]+[.;]?", re.IGNORECASE)
214+ 
215+ 
216+def _normalise_prog(prog: str) -> str:
217+ words = prog.replace("\\", "/").split()
218+ if not words:
219+ return prog
220+ words[0] = os.path.basename(words[0])
221+ if words[0].lower().endswith(".py"):
222+ words[0] = words[0][:-3]
223+ return " ".join(words)
224+ 
225+ 
226+def _lookup_spec(parser: argparse.ArgumentParser) -> HelpSpec:
227+ prog = _normalise_prog(parser.prog)
228+ spec = HELP_SPECS.get(prog)
229+ if spec:
230+ return spec
231+ description = (parser.description or f"Run the {prog} command.").strip()
232+ return HelpSpec(description, _build_usage(parser), ((f"Show help for {prog}", f"{prog} -h"),))
233+ 
234+ 
235+def _preferred_options(action: argparse.Action) -> Tuple[Optional[str], Optional[str]]:
236+ short = next((item for item in action.option_strings if len(item) == 2 and item.startswith("-")), None)
237+ long_name = next((item for item in action.option_strings if item.startswith("--")), None)
238+ if long_name is None:
239+ long_name = next((item for item in action.option_strings if item != short), None)
240+ return short, long_name
241+ 
242+ 
243+def _is_flag(action: argparse.Action) -> bool:
244+ return action.nargs == 0
245+ 
246+ 
247+def _metavar(action: argparse.Action) -> str:
248+ if action.choices is not None:
249+ return "{" + ",".join(str(value).lower() for value in action.choices) + "}"
250+ value = action.metavar
251+ if isinstance(value, tuple):
252+ value = value[0]
253+ if value:
254+ value = str(value).strip("<>").upper()
255+ if value not in {"STRING", "VALUE", "ARG", "PATH"}:
256+ base = f"<{value}>"
257+ else:
258+ base = _METAVAR_BY_DEST.get(action.dest, "<NAME>")
259+ elif action.dest in _METAVAR_BY_DEST:
260+ base = _METAVAR_BY_DEST[action.dest]
261+ elif action.type is int:
262+ base = "<N>"
263+ elif action.type is float:
264+ base = "<FLOAT>"
265+ elif any(token in action.dest for token in ("path", "file", "mapping", "config")):
266+ base = "<FILE>"
267+ elif any(token in action.dest for token in ("num", "count", "size", "top")):
268+ base = "<N>"
269+ else:
270+ base = "<NAME>"
271+ 
272+ if action.nargs in ("+", "*"):
273+ return f"{base} [{base} ...]"
274+ if isinstance(action.nargs, int) and action.nargs > 1:
275+ return " ".join(base for _ in range(action.nargs))
276+ return base
277+ 
278+ 
279+def _signature(action: argparse.Action) -> Tuple[str, str]:
280+ if isinstance(action, argparse._SubParsersAction):
281+ choices = "{" + ",".join(action.choices) + "}"
282+ return "", f"<command> {choices}"
283+ short, long_name = _preferred_options(action)
284+ if not action.option_strings:
285+ return "", _metavar(action)
286+ if long_name is None:
287+ long_name, short = short, None
288+ if not _is_flag(action):
289+ long_name = f"{long_name} {_metavar(action)}"
Menba
MenbaMenba8月17日

问题

  • 标签: 【可读性】
  • 级别: 一般
  • 位置: python/msprobe/core/common/cli_help.py:363
  • 现象: 仅短选项(如 -i/-n)在统一帮助中被渲染为字面量 "None "
  • 影响范围: 统一帮助是本次 PR 的唯一交付物,但多个常见命令的帮助参数表第二列会显示无意义的 "None " / "None ",用户无法获知正确参数名与占位符,帮助信息质量相比改动前(argparse 默认按 dest 显示 DUMP_FILE_PATH 等)反而倒退。

原因

触发条件:对使用 MindStudioArgumentParser 且包含纯短选项(option_strings 仅一个长度 2 的选项,如 -i、-n、-o)的命令执行 -h,例如 shape_conversion -h、inplace_layer_process -h、multi_acc_check -h。本 PR 中 shape_format_conversion.py 的 -i、inplace_layer_process.py 的 -i、multi_acc_check.py 的 -n 均会触发。

差异证据:_preferred_options 对 option_strings == ['-i'] 的 action 返回 (short='-i', long_name=None)(新增代码第311-316行,long_name 仅在存在 -- 前缀或非短选项时才有值);第363行 long_name = f"{long_name} {_metavar(action)}" 把 None 经 f-string 插值为字符串 "None",_signature 最终返回 ('-i,', 'None ')。已用复现脚本确认:-i → 'None ',-n → 'None ',渲染后帮助中实际出现 "-i, None "。

优化建议

在 _signature(或 _preferred_options)中对仅短选项做兜底:当 long_name is None 且 short 存在时,令 long_name = short 并将 short 置空避免首列重复,再拼接 metavar。例如:if long_name is None and short: long_name, short = short, None,随后 long_name = f"{long_name} {_metavar(action)}"

为什么这样优化

_preferred_options 的 long_name 对纯短选项返回 None,而 f-string 对 None 不会报错而是静默转为字符串 "None",因此缺陷不易察觉且覆盖面广;第二列设计上应始终承载“选项串 + metavar”。当前实现使占位符丢失,违背统一帮助格式的目标。

效果

  • 预期效果:统一帮助是本次 PR 的唯一交付物,但多个常见命令的帮助参数表第二列会显示无意义的 "None " / "None ",用户无法获知正确参数名与占位符,帮助信息质量相比改动前(argparse 默认按 dest 显示 DUMP_FILE_PATH 等)反而倒退。
  • 验证方式:在 test/msprobe_test/core_ut/common/test_cli_help.py 增加用例:构造仅含 -i(非 flag)的 MindStudioArgumentParser,断言 format_help() 不包含 "None" 且包含 "-i ";另对 shape_conversion、inplace_layer_process、multi_acc_check 执行 -h 做端到端冒烟,检查输出文本。
  • 回滚说明:将该行回退为使用 short 兜底的实现,或整体回退 cli_help.py(重新使用 argparse.ArgumentParser),即可恢复旧帮助文本,不影响其他功能。
likedislike
290+ return f"{short}," if short else "", long_name or ""
291+ 
292+ 
293+def _description(action: argparse.Action, required: bool) -> str:
294+ if isinstance(action, argparse._SubParsersAction):
295+ return "Command to run."
296+ help_text = "" if action.help in (None, argparse.SUPPRESS) else str(action.help)
297+ help_text = _LABEL_RE.sub("", help_text).strip()
298+ help_text = _PAREN_DEFAULT_RE.sub(" ", help_text).strip()
299+ default = action.default
300+ has_default = default not in (None, "", argparse.SUPPRESS) and not isinstance(action, argparse._HelpAction)
301+ if has_default:
302+ help_text = _DEFAULT_MARKER_RE.sub(" ", help_text)
303+ help_text = _TEXT_DEFAULT_RE.sub(" ", help_text).strip()
304+ if help_text:
305+ help_text = help_text[0].upper() + help_text[1:]
306+ else:
307+ help_text = "Show help message." if isinstance(action, argparse._HelpAction) else "Command option."
308+ if not help_text.endswith((".", "!", "?")):
309+ help_text += "."
310+ 
311+ if required or not has_default:
312+ return help_text
313+ if _is_flag(action):
314+ default_text = "on" if bool(default) else "off"
315+ elif isinstance(default, (list, tuple)):
316+ default_text = ",".join(str(item) for item in default)
317+ else:
318+ default_text = str(default)
319+ if "default:" not in help_text.lower():
320+ help_text = f"{help_text} [default: {default_text}]"
321+ return help_text
322+ 
323+ 
324+def _required(action: argparse.Action) -> bool:
325+ if isinstance(action, argparse._SubParsersAction):
326+ return True
327+ if action.option_strings:
328+ return bool(action.required)
329+ return action.nargs not in ("?", "*")
330+ 
331+ 
332+def _visible_actions(parser: argparse.ArgumentParser) -> Iterable[argparse.Action]:
333+ return (action for action in parser._actions if action.help != argparse.SUPPRESS)
334+ 
335+ 
336+def _build_usage(parser: argparse.ArgumentParser) -> str:
337+ parts = [_normalise_prog(parser.prog)]
338+ required_actions = [action for action in _visible_actions(parser) if _required(action)]
339+ for action in required_actions:
340+ _, long_name = _signature(action)
341+ parts.append(long_name)
342+ if any(not _required(action) for action in _visible_actions(parser)):
343+ parts.append("[options]")
344+ return " ".join(parts)
345+ 
346+ 
347+def _format_parameter_section(title: str, actions: Sequence[argparse.Action]) -> str:
348+ if not actions:
349+ return ""
350+ rows = [(*_signature(action), _description(action, title == "Required arguments:")) for action in actions]
351+ first_width = max(3, *(len(row[0]) for row in rows))
352+ second_width = max(len(row[1]) for row in rows)
353+ description_column = 2 + first_width + 1 + second_width + 4
354+ terminal_width = max(40, shutil.get_terminal_size(fallback=(100, 24)).columns)
355+ lines = [title]
356+ for first, second, description in rows:
357+ prefix = f" {first:<{first_width}} {second:<{second_width}} "
358+ wrapped = textwrap.wrap(description, width=max(20, terminal_width - description_column)) or [""]
359+ lines.append(prefix + wrapped[0])
360+ lines.extend(" " * description_column + line for line in wrapped[1:])
361+ return "\n".join(lines)
362+ 
363+ 
364+def format_help(parser: argparse.ArgumentParser) -> str:
W
Wwugengjun8月17日

【review】cli_help.py _description 中使用 _LABEL_RE 正则从 help 文本中剥离 / 等前缀标签。这符合统一规范的目的,但当前正则范围较宽,会删除 help 文本中任何以这些标签开头的短语,包括部分命令中原有的参数说明前缀。建议在 cli_help.py 的模块文档中明确标注此行为,告知后续开发者不要在 help 文本中使用 前缀——统一改用 required=True 来表达必选语义。

likedislike
365+ spec = _lookup_spec(parser)
366+ usage = spec.usage or _build_usage(parser)
367+ terminal_width = max(40, shutil.get_terminal_size(fallback=(100, 24)).columns)
368+ actions = list(_visible_actions(parser))
369+ required = [action for action in actions if _required(action)]
370+ optional = [action for action in actions if not _required(action)]
371+ sections = [
372+ "Description:\n" + "\n".join(f" {line}" for line in textwrap.wrap(spec.description, width=terminal_width - 4)),
373+ f"Usage:\n {usage}",
374+ _format_parameter_section("Required arguments:", required),
375+ _format_parameter_section("Optional arguments:", optional),
376+ ]
377+ example_lines = ["Examples:"]
378+ for comment, command in spec.examples:
379+ example_lines.extend((f" # {comment}", f" {command}", ""))
380+ sections.append("\n".join(example_lines).rstrip())
381+ if spec.output:
382+ sections.append("Output:\n" + "\n".join(f" {line}" for line in spec.output))
383+ if spec.troubleshooting:
384+ sections.append("Troubleshooting:\n" + "\n".join(f" - {line}" for line in spec.troubleshooting))
385+ return "\n\n".join(section for section in sections if section) + "\n"
386+ 
387+ 
388+class MindStudioArgumentParser(argparse.ArgumentParser):
389+ """Argument parser with the unified MindStudio help layout."""
390+ 
391+ def format_help(self) -> str:
392+ return format_help(self)
@@ -13,11 +13,11 @@
13# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.13# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
14# See the Mulan PSL v2 for more details.14# See the Mulan PSL v2 for more details.
15# -------------------------------------------------------------------------15# -------------------------------------------------------------------------
16+# pylint: disable=duplicate-code
16 17 
17import os18import os
18import sys19import sys
19import math20import math
20-import argparse
21import ast21import ast
22import heapq22import heapq
23from abc import ABC23from abc import ABC
@@ -26,8 +26,14 @@ from typing import List
26 26 
27from msprobe.core.common.const import MonitorConst27from msprobe.core.common.const import MonitorConst
28from msprobe.core.common.log import logger28from msprobe.core.common.log import logger
29-from msprobe.core.common.file_utils import save_json, create_directory, remove_path, \29+from msprobe.core.common.cli_help import MindStudioArgumentParser
30- check_file_or_directory_path, load_json30+from msprobe.core.common.file_utils import (
31+ save_json,
32+ create_directory,
33+ remove_path,
34+ check_file_or_directory_path,
35+ load_json,
36+)
31 37 
32 38 
33class ScanRule(ABC):39class ScanRule(ABC):
@@ -64,7 +70,6 @@ class AnomalyNan(ScanRule):
64 70 
65 71 
66class AnomalyScanner:72class AnomalyScanner:
67- 
68 @staticmethod73 @staticmethod
69 def load_rules(specs: List[dict]):74 def load_rules(specs: List[dict]):
70 """75 """
@@ -119,8 +124,7 @@ class AnomalyDataFactory(ABC):
119 self.name2callid = {}124 self.name2callid = {}
120 125 
121 def set_call_id(self, name2callid):126 def set_call_id(self, name2callid):
122- """根据当前GradContext信息更新call_id vpp_stage等信息127+ """根据当前GradContext信息更新call_id vpp_stage等信息"""
123- """
124 self.name2callid = name2callid128 self.name2callid = name2callid
125 129 
126 def create(self, tag, message, step):130 def create(self, tag, message, step):
@@ -140,15 +144,7 @@ class AnomalyDataFactory(ABC):
140 vpp_stage = 0144 vpp_stage = 0
141 145 
142 return GradAnomalyData(146 return GradAnomalyData(
143- self.rank,147+ self.rank, step, self.micro_step, self.pp_stage, vpp_stage, call_id, tag_name, message, self.group_mates
144- step,
145- self.micro_step,
146- self.pp_stage,
147- vpp_stage,
148- call_id,
149- tag_name,
150- message,
151- self.group_mates
152 )148 )
153 149 
154 150 
@@ -304,11 +300,7 @@ class AnomalyAnalyse:
304 if not step_list:300 if not step_list:
305 filtered_anomalies = anomalies301 filtered_anomalies = anomalies
306 else:302 else:
307- filtered_anomalies = [303+ filtered_anomalies = [anomaly for anomaly in anomalies if anomaly.step in step_list]
308- anomaly
309- for anomaly in anomalies
310- if anomaly.step in step_list
311- ]
312 if topk >= len(filtered_anomalies):304 if topk >= len(filtered_anomalies):
313 self.sorted_anomalies = sorted(filtered_anomalies)305 self.sorted_anomalies = sorted(filtered_anomalies)
314 else:306 else:
@@ -336,9 +328,9 @@ def _get_step_and_stop(args):
336 if not isinstance(step_list, list):328 if not isinstance(step_list, list):
337 raise ValueError(f"{args.step_list} is not a list.")329 raise ValueError(f"{args.step_list} is not a list.")
338 except (ValueError, SyntaxError, RecursionError) as e:330 except (ValueError, SyntaxError, RecursionError) as e:
339- raise Exception(f"The step list must be a resolvable list type.") from e331+ raise ValueError("The step list must be a resolvable list type.") from e
340 if args.top_k_number <= 0:332 if args.top_k_number <= 0:
341- raise Exception("The top k number must be greater than 0.")333+ raise ValueError("The top k number must be greater than 0.")
342 return step_list, args.top_k_number334 return step_list, args.top_k_number
343 335 
344 336 
@@ -348,12 +340,8 @@ def _anomaly_analyse():
348 loader = AnomalyDataLoader(args.data_path_dir)340 loader = AnomalyDataLoader(args.data_path_dir)
349 anomalies = loader.get_anomalies_from_jsons()341 anomalies = loader.get_anomalies_from_jsons()
350 analyser = AnomalyAnalyse()342 analyser = AnomalyAnalyse()
351- top_anomalies = analyser.get_range_top_k(343+ top_anomalies = analyser.get_range_top_k(top_k_number, step_list, anomalies)
352- top_k_number, step_list, anomalies344+ analyser.rewrite_sorted_anomalies(args.out_path if args.out_path else args.data_path_dir)
353- )
354- analyser.rewrite_sorted_anomalies(
355- args.out_path if args.out_path else args.data_path_dir
356- )
357 345 
358 logger.info(f"Top {top_k_number} anomalies are listed as follows:")346 logger.info(f"Top {top_k_number} anomalies are listed as follows:")
359 for index, anomaly in enumerate(top_anomalies):347 for index, anomaly in enumerate(top_anomalies):
@@ -361,23 +349,44 @@ def _anomaly_analyse():
361 349 
362 350 
363def _get_parse_args():351def _get_parse_args():
364- parser = argparse.ArgumentParser()352+ parser = MindStudioArgumentParser(prog="anomaly_processor")
365- parser.add_argument("-d", "--data_path", dest="data_path_dir", default="./", type=str,353+ parser.add_argument(
366- help="<Required> The anomaly detect result dictionary: generate from monitor tool.",354+ "-d",
367- required=True,355+ "--data_path",
368- )356+ dest="data_path_dir",
369- parser.add_argument("-o", "--out_path", dest="out_path", default="", type=str,357+ default="./",
370- help="<optional> The analyse task result out path.",358+ type=str,
371- required=False,359+ metavar="<DIR>",
372- )360+ help="<Required> The anomaly detect result dictionary: generate from monitor tool.",
373- parser.add_argument("-k", "--topk", dest="top_k_number", default=8, type=int,361+ required=True,
374- help="<optional> Top K number of earliest anomalies.",362+ )
375- required=False,363+ parser.add_argument(
376- )364+ "-o",
377- parser.add_argument("-s", "--step", dest="step_list", default="[]", type=str,365+ "--out_path",
378- help="<optional> Analyse which steps.",366+ dest="out_path",
379- required=False,367+ default="",
380- )368+ type=str,
369+ help="<optional> The analyse task result out path.",
370+ required=False,
371+ )
372+ parser.add_argument(
373+ "-k",
374+ "--topk",
375+ dest="top_k_number",
376+ default=8,
377+ type=int,
378+ help="<optional> Top K number of earliest anomalies.",
379+ required=False,
380+ )
381+ parser.add_argument(
382+ "-s",
383+ "--step",
384+ dest="step_list",
385+ default="[]",
386+ type=str,
387+ help="<optional> Analyse which steps.",
388+ required=False,
389+ )
381 return parser.parse_args(sys.argv[1:])390 return parser.parse_args(sys.argv[1:])
382 391 
383 392 
@@ -45,6 +45,7 @@ def _offline_dump_parser(parser):
45 '--model_path',45 '--model_path',
46 required=True,46 required=True,
47 dest="model_path",47 dest="model_path",
48+ metavar="<FILE>",
48 type=check_model_path_legality,49 type=check_model_path_legality,
49 help='The original model .onnx or .om file path',50 help='The original model .onnx or .om file path',
50 )51 )
@@ -32,12 +32,14 @@ from msprobe.infer.offline.compare.msquickcmp.main import _offline_dump_parser,
32from msprobe.core.install_deps.install_deps import _install_deps_parser, install_deps_cli32from msprobe.core.install_deps.install_deps import _install_deps_parser, install_deps_cli
33from msprobe.core.parse.parse_cli import _parse_parser, parse_cli33from msprobe.core.parse.parse_cli import _parse_parser, parse_cli
34from msprobe.core.common.logo import CliLogo34from msprobe.core.common.logo import CliLogo
35+from msprobe.core.common.cli_help import MindStudioArgumentParser
35 36 
36 37 
37def main():38def main():
38 CliLogo().print_logo()39 CliLogo().print_logo()
39 40 
40- parser = argparse.ArgumentParser(41+ parser = MindStudioArgumentParser(
42+ prog="msprobe",
41 formatter_class=argparse.RawDescriptionHelpFormatter,43 formatter_class=argparse.RawDescriptionHelpFormatter,
42 description="msprobe(mindstudio probe), [Powered by MindStudio].\n"44 description="msprobe(mindstudio probe), [Powered by MindStudio].\n"
43 "A full-process, all-scenario precision tool base on Ascend products.\n"45 "A full-process, all-scenario precision tool base on Ascend products.\n"
@@ -45,7 +47,7 @@ def main():
45 )47 )
46 48 
47 parser.set_defaults(print_help=parser.print_help)49 parser.set_defaults(print_help=parser.print_help)
48- subparsers = parser.add_subparsers()50+ subparsers = parser.add_subparsers(parser_class=MindStudioArgumentParser)
49 51 
50 compare_parser = subparsers.add_parser('compare')52 compare_parser = subparsers.add_parser('compare')
51 _compare_parser(compare_parser)53 _compare_parser(compare_parser)
@@ -15,6 +15,7 @@
15# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.15# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
16# See the Mulan PSL v2 for more details.16# See the Mulan PSL v2 for more details.
17# -------------------------------------------------------------------------17# -------------------------------------------------------------------------
18+# pylint: disable=duplicate-code
18 19 
19import argparse20import argparse
20import os21import os
@@ -58,6 +59,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory, get_js
58from msprobe.pytorch.common.log import logger59from msprobe.pytorch.common.log import logger
59from msprobe.pytorch.dump.pt_config import parse_json_config60from msprobe.pytorch.dump.pt_config import parse_json_config
60from msprobe.core.common.const import Const, FileCheckConst, CompareConst61from msprobe.core.common.const import Const, FileCheckConst, CompareConst
62+from msprobe.core.common.cli_help import MindStudioArgumentParser
61from msprobe.core.common.utils import safe_get_value, CompareException, is_int, check_op_str_pattern_valid63from msprobe.core.common.utils import safe_get_value, CompareException, is_int, check_op_str_pattern_valid
62from msprobe.core.common.output_postprocess.processor import postprocess_output, should_postprocess_output64from msprobe.core.common.output_postprocess.processor import postprocess_output, should_postprocess_output
63from msprobe.pytorch.common.utils import seed_all65from msprobe.pytorch.common.utils import seed_all
@@ -474,7 +476,7 @@ def preprocess_forward_content(forward_content):
474 476 
475def _acc_check(parser=None):477def _acc_check(parser=None):
476 if not parser:478 if not parser:
477- parser = argparse.ArgumentParser()479+ parser = MindStudioArgumentParser(prog="acc_check")
478 _acc_check_parser(parser)480 _acc_check_parser(parser)
479 args = parser.parse_args(sys.argv[1:])481 args = parser.parse_args(sys.argv[1:])
480 acc_check_command(args)482 acc_check_command(args)
@@ -19,7 +19,6 @@
19import subprocess # nosec19import subprocess # nosec
20import os20import os
21import sys21import sys
22-import argparse
23import time22import time
24import signal23import signal
25import threading24import threading
@@ -38,6 +37,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory, load_j
38from msprobe.core.common.file_utils import remove_path37from msprobe.core.common.file_utils import remove_path
39from msprobe.core.common.const import FileCheckConst, Const38from msprobe.core.common.const import FileCheckConst, Const
40from msprobe.core.common.utils import CompareException39from msprobe.core.common.utils import CompareException
40+from msprobe.core.common.cli_help import MindStudioArgumentParser
41 41 
42 42 
43def split_json_file(input_file, num_splits, filter_api):43def split_json_file(input_file, num_splits, filter_api):
@@ -269,7 +269,7 @@ def prepare_config(args):
269def main():269def main():
270 signal.signal(signal.SIGINT, signal_handler)270 signal.signal(signal.SIGINT, signal_handler)
271 signal.signal(signal.SIGTERM, signal_handler)271 signal.signal(signal.SIGTERM, signal_handler)
272- parser = argparse.ArgumentParser(description='Run acc_check in parallel')272+ parser = MindStudioArgumentParser(prog="multi_acc_check", description='Run acc_check in parallel')
273 _acc_check_parser(parser)273 _acc_check_parser(parser)
274 parser.add_argument(274 parser.add_argument(
275 '-n',275 '-n',
@@ -15,8 +15,9 @@
15# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.15# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
16# See the Mulan PSL v2 for more details.16# See the Mulan PSL v2 for more details.
17# -------------------------------------------------------------------------17# -------------------------------------------------------------------------
18+# pylint: disable=duplicate-code
18 19 
19-import argparse20+import math
20import os21import os
21import sys22import sys
22 23 
@@ -32,13 +33,14 @@ import torch
32from tqdm import tqdm33from tqdm import tqdm
33from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import generate_device_params, get_api_info34from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import generate_device_params, get_api_info
34from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check_utils import exec_api, is_unsupported_api, ExecParams35from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check_utils import exec_api, is_unsupported_api, ExecParams
35-from msprobe.core.common.file_utils import check_link, FileChecker36+from msprobe.core.common.file_utils import FileChecker
36from msprobe.pytorch.api_accuracy_checker.common.utils import extract_basic_api_segments37from msprobe.pytorch.api_accuracy_checker.common.utils import extract_basic_api_segments
37from msprobe.core.common.const import FileCheckConst, Const38from msprobe.core.common.const import FileCheckConst, Const
38from msprobe.core.common.utils import check_op_str_pattern_valid39from msprobe.core.common.utils import check_op_str_pattern_valid
39from msprobe.pytorch.common.log import logger40from msprobe.pytorch.common.log import logger
40from msprobe.pytorch.common.parse_json import parse_json_info_forward_backward41from msprobe.pytorch.common.parse_json import parse_json_info_forward_backward
41from msprobe.core.common.decorator import recursion_depth_decorator42from msprobe.core.common.decorator import recursion_depth_decorator
43+from msprobe.core.common.cli_help import MindStudioArgumentParser
42 44 
43 45 
44def check_tensor_overflow(x):46def check_tensor_overflow(x):
@@ -49,21 +51,10 @@ def check_tensor_overflow(x):
49 else:51 else:
50 tensor_max = torch.max(x).cpu().detach().float().numpy().tolist()52 tensor_max = torch.max(x).cpu().detach().float().numpy().tolist()
51 tensor_min = torch.min(x).cpu().detach().float().numpy().tolist()53 tensor_min = torch.min(x).cpu().detach().float().numpy().tolist()
52- # inf54+ return any(math.isinf(value) or math.isnan(value) for value in (tensor_max, tensor_min))
53- if tensor_max == float('inf') or tensor_min == float('-inf'):55+ if isinstance(x, (bool, float, int)):
54- return True56+ return math.isinf(x) or math.isnan(x)
55- # nan57+ return False
56- elif tensor_max != tensor_max or tensor_min != tensor_min:
57- return True
58- else:
59- return False
60- elif isinstance(x, bool) or isinstance(x, int) or isinstance(x, float):
61- if x == float('inf') or x == float('-inf') or x != x:
62- return True
63- else:
64- return False
65- else:
66- return False
67 58 
68 59 
69@recursion_depth_decorator("check_data_overflow")60@recursion_depth_decorator("check_data_overflow")
@@ -104,14 +95,19 @@ def run_overflow_check(forward_file):
104 except Exception as err:95 except Exception as err:
105 _, api_name, _ = api_full_name.split(Const.SEP)96 _, api_name, _ = api_full_name.split(Const.SEP)
106 if "not implemented for 'Half'" in str(err):97 if "not implemented for 'Half'" in str(err):
107- logger.warning(f"API {api_name} not support half tensor in CPU. This API does not support overflow "98+ logger.warning(
108- "check, so it will be skipped.")99+ f"API {api_name} not support half tensor in CPU. This API does not support overflow "
100+ "check, so it will be skipped."
101+ )
109 elif "expected scalar type Long" in str(err):102 elif "expected scalar type Long" in str(err):
110- logger.warning(f"API {api_name} not support int32 tensor in CPU, please add {api_name} to CONVERT_API "103+ logger.warning(
111- "'int32_to_int64' list in accuracy_tools/msprobe/core/common/const.py file.")104+ f"API {api_name} not support int32 tensor in CPU, please add {api_name} to CONVERT_API "
105+ "'int32_to_int64' list in accuracy_tools/msprobe/core/common/const.py file."
106+ )
112 elif "could not create a primitive descriptor for a matmul primitive" in str(err):107 elif "could not create a primitive descriptor for a matmul primitive" in str(err):
113- logger.warning(f"API {api_name} not support matmul primitive in CPU due to pytorch bug, "108+ logger.warning(
114- "so it will be skipped.")109+ f"API {api_name} not support matmul primitive in CPU due to pytorch bug, so it will be skipped."
110+ )
115 else:111 else:
116 logger.error(f"Run {api_full_name} acc_check Error: %s" % str(err))112 logger.error(f"Run {api_full_name} acc_check Error: %s" % str(err))
117 113 
@@ -121,8 +117,9 @@ def run_torch_api(api_full_name, api_info_dict, real_data_path):
121 api_type, api_name = extract_basic_api_segments(api_full_name)117 api_type, api_name = extract_basic_api_segments(api_full_name)
122 args, kwargs, need_grad = get_api_info(api_info_dict, api_name, real_data_path)118 args, kwargs, need_grad = get_api_info(api_info_dict, api_name, real_data_path)
123 if not need_grad:119 if not need_grad:
124- logger.warning("%s function with out=... arguments don't support automatic differentiation, skip backward." 120+ logger.warning(
125- % api_full_name)121+ "%s function with out=... arguments don't support automatic differentiation, skip backward." % api_full_name
122+ )
126 device_info_kwargs = kwargs.get(Const.DEVICE)123 device_info_kwargs = kwargs.get(Const.DEVICE)
127 if device_info_kwargs and device_info_kwargs.get(Const.VALUE):124 if device_info_kwargs and device_info_kwargs.get(Const.VALUE):
128 kwargs[Const.DEVICE] = current_device125 kwargs[Const.DEVICE] = current_device
@@ -137,8 +134,9 @@ def run_torch_api(api_full_name, api_info_dict, real_data_path):
137 logger.warning("The %s overflow is a normal overflow, out and npu_out is None." % api_full_name)134 logger.warning("The %s overflow is a normal overflow, out and npu_out is None." % api_full_name)
138 return135 return
139 if is_bool_output(out) or is_bool_output(npu_out):136 if is_bool_output(out) or is_bool_output(npu_out):
140- logger.warning("The output of %s is bool type.This dtype not support overflow, so it will be skipped."137+ logger.warning(
141- % api_full_name)138+ "The output of %s is bool type.This dtype not support overflow, so it will be skipped." % api_full_name
139+ )
142 return140 return
143 141 
144 cpu_overflow = check_data_overflow(out, Const.CPU_LOWERCASE)142 cpu_overflow = check_data_overflow(out, Const.CPU_LOWERCASE)
@@ -151,19 +149,36 @@ def run_torch_api(api_full_name, api_info_dict, real_data_path):
151 149 
152 150 
153def _run_overflow_check_parser(parser):151def _run_overflow_check_parser(parser):
154- parser.add_argument("-api_info", "--api_info_file", dest="api_info_file", default="",152+ parser.add_argument(
155- help="<Required> The api param tool result file: generate from api param tool, "153+ "-api_info",
156- "a json file.",154+ "--api_info_file",
157- required=True)155+ dest="api_info_file",
158- parser.add_argument("-j", "--jit_compile", dest="jit_compile", help="<optional> whether to turn on jit compile",156+ default="",
159- default=False, required=False)157+ help="<Required> The api param tool result file: generate from api param tool, a json file.",
160- parser.add_argument("-d", "--device", dest="device_id", type=int, help="<optional> set NPU device id to acc_check",158+ required=True,
161- default=0, required=False)159+ )
160+ parser.add_argument(
161+ "-j",
162+ "--jit_compile",
163+ dest="jit_compile",
164+ help="<optional> whether to turn on jit compile",
165+ default=False,
166+ required=False,
167+ )
168+ parser.add_argument(
169+ "-d",
170+ "--device",
171+ dest="device_id",
172+ type=int,
173+ help="<optional> set NPU device id to acc_check",
174+ default=0,
175+ required=False,
176+ )
162 177 
163 178 
164def _run_overflow_check(parser=None):179def _run_overflow_check(parser=None):
165 if not parser:180 if not parser:
166- parser = argparse.ArgumentParser()181+ parser = MindStudioArgumentParser(prog="run_overflow_check")
167 _run_overflow_check_parser(parser)182 _run_overflow_check_parser(parser)
168 args = parser.parse_args(sys.argv[1:])183 args = parser.parse_args(sys.argv[1:])
169 _run_overflow_check_command(args)184 _run_overflow_check_command(args)
@@ -173,8 +188,12 @@ def _run_overflow_check(parser=None):
173def _run_overflow_check_command(args):188def _run_overflow_check_command(args):
174 torch.npu.set_compile_mode(jit_compile=args.jit_compile)189 torch.npu.set_compile_mode(jit_compile=args.jit_compile)
175 npu_device = "npu:" + str(args.device_id)190 npu_device = "npu:" + str(args.device_id)
176- api_info_file_checker = FileChecker(file_path=args.api_info_file, path_type=FileCheckConst.FILE, 191+ api_info_file_checker = FileChecker(
177- ability=FileCheckConst.READ_ABLE, file_type=FileCheckConst.JSON_SUFFIX)192+ file_path=args.api_info_file,
193+ path_type=FileCheckConst.FILE,
194+ ability=FileCheckConst.READ_ABLE,
195+ file_type=FileCheckConst.JSON_SUFFIX,
196+ )
178 api_info = api_info_file_checker.common_check()197 api_info = api_info_file_checker.common_check()
179 try:198 try:
180 torch.npu.set_device(npu_device)199 torch.npu.set_device(npu_device)
@@ -16,7 +16,6 @@
16# See the Mulan PSL v2 for more details.16# See the Mulan PSL v2 for more details.
17# -------------------------------------------------------------------------17# -------------------------------------------------------------------------
18 18 
19-import argparse
20import os19import os
21import sys20import sys
22from collections import namedtuple21from collections import namedtuple
@@ -44,6 +43,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory
44from msprobe.pytorch.common.log import logger43from msprobe.pytorch.common.log import logger
45from msprobe.core.common.utils import CompareException, check_op_str_pattern_valid44from msprobe.core.common.utils import CompareException, check_op_str_pattern_valid
46from msprobe.core.common.const import Const, CompareConst, FileCheckConst45from msprobe.core.common.const import Const, CompareConst, FileCheckConst
46+from msprobe.core.common.cli_help import MindStudioArgumentParser
47 47 
48CompareConfig = namedtuple('CompareConfig', ['npu_csv_path', 'gpu_csv_path', 'result_csv_path', 'details_csv_path'])48CompareConfig = namedtuple('CompareConfig', ['npu_csv_path', 'gpu_csv_path', 'result_csv_path', 'details_csv_path'])
49BenchmarkInfNanConsistency = namedtuple(49BenchmarkInfNanConsistency = namedtuple(
@@ -429,7 +429,7 @@ def record_thousandth_threshold_result(input_data):
429 429 
430def _api_precision_compare(parser=None):430def _api_precision_compare(parser=None):
431 if not parser:431 if not parser:
432- parser = argparse.ArgumentParser()432+ parser = MindStudioArgumentParser(prog="api_precision_compare")
433 _api_precision_compare_parser(parser)433 _api_precision_compare_parser(parser)
434 args = parser.parse_args(sys.argv[1:])434 args = parser.parse_args(sys.argv[1:])
435 _api_precision_compare_command(args)435 _api_precision_compare_command(args)
@@ -16,7 +16,6 @@
16 16 
17from __future__ import annotations17from __future__ import annotations
18 18 
19-import argparse
20import json19import json
21import os20import os
22import re21import re
@@ -26,13 +25,15 @@ from dataclasses import dataclass
26from functools import lru_cache25from functools import lru_cache
27from transformers import AutoTokenizer26from transformers import AutoTokenizer
28from msprobe.core.common.file_utils import check_path_exists, save_json27from msprobe.core.common.file_utils import check_path_exists, save_json
28+from msprobe.core.common.cli_help import MindStudioArgumentParser
29 29 
30 30 
31def parse_args():31def parse_args():
32- parser = argparse.ArgumentParser(description="")32+ parser = MindStudioArgumentParser(prog="gen_model_config")
33 parser.add_argument(33 parser.add_argument(
34 "--model-path",34 "--model-path",
35 required=True,35 required=True,
36+ metavar="<DIR>",
36 help="Path of the model for starting the service.",37 help="Path of the model for starting the service.",
37 ) # 目标模型路径38 ) # 目标模型路径
38 parser.add_argument("--model-name", default=None, type=str) # 保存的文件名以及mtype_config.json文件里对应的key39 parser.add_argument("--model-name", default=None, type=str) # 保存的文件名以及mtype_config.json文件里对应的key
@@ -40,7 +40,7 @@ MAKESELF_DIR=${TOP_DIR}/opensource/makeself
40# footnote for creating run package40# footnote for creating run package
41CREATE_RUN_SCRIPT=${MAKESELF_DIR}/makeself.sh41CREATE_RUN_SCRIPT=${MAKESELF_DIR}/makeself.sh
42 42 
43-# footnote for controling params43+# footnote for controlling params
44CONTROL_PARAM_SCRIPT=${MAKESELF_DIR}/makeself-header.sh44CONTROL_PARAM_SCRIPT=${MAKESELF_DIR}/makeself-header.sh
45 45 
46# store run package46# store run package
@@ -226,4 +226,3 @@ function main() {
226parse_script_args "$@"226parse_script_args "$@"
227 227 
228main228main
229- 
@@ -0,0 +1,98 @@
1+# -------------------------------------------------------------------------
2+# This file is part of the MindStudio project.
3+# Copyright (c) 2026 Huawei Technologies Co.,Ltd.
4+#
5+# MindStudio is licensed under Mulan PSL v2.
6+# You can use this software according to the terms and conditions of the Mulan PSL v2.
7+# -------------------------------------------------------------------------
8+ 
9+import os
10+ 
11+from msprobe.core.common.cli_help import MindStudioArgumentParser
12+ 
13+ 
14+def test_unified_help_has_required_sections_in_order():
15+ parser = MindStudioArgumentParser(prog="msprobe parse")
16+ parser.add_argument("-d", "--dump_path", required=True, help="<Required> Dump data to parse")
17+ parser.add_argument("-t", "--type", choices=["npy", "pt"], default="pt", help="Output type")
18+ parser.add_argument(
19+ "-o", "--output_path", default="./output", help="<Optional> Output directory. Default path: ./output"
20+ )
21+ 
22+ help_text = parser.format_help()
23+ 
24+ section_offsets = [
25+ help_text.index("Description:"),
26+ help_text.index("Usage:"),
27+ help_text.index("Required arguments:"),
28+ help_text.index("Optional arguments:"),
29+ help_text.index("Examples:"),
30+ help_text.index("Output:"),
31+ ]
32+ assert section_offsets == sorted(section_offsets)
33+ assert "--dump_path <FILE_OR_DIR>" in help_text
34+ assert "--type {npy,pt}" in help_text
35+ assert "[default: pt]" in help_text
36+ assert "[default: ./output]" in help_text
37+ assert "<Required>" not in help_text
38+ assert "Default path:" not in help_text
39+ 
40+ 
41+def test_unified_help_omits_empty_output_and_required_sections():
42+ parser = MindStudioArgumentParser(prog="msprobe install_deps")
43+ parser.add_argument("--no_check", action="store_true", help="Skip certificate checks")
44+ 
45+ help_text = parser.format_help()
46+ 
47+ assert "Required arguments:" not in help_text
48+ assert "Output:" not in help_text
49+ assert "Examples:" in help_text
50+ assert "--no_check" in help_text
51+ assert "[default: off]" in help_text
52+ 
53+ 
54+def test_generated_usage_reuses_action_metavar():
55+ test_cases = (
56+ ("anomaly_processor", "-d", "--data_path", "data_path_dir"),
57+ ("gen_model_config", None, "--model-path", "model_path"),
58+ )
59+ 
60+ for prog, short_option, long_option, dest in test_cases:
61+ parser = MindStudioArgumentParser(prog=prog)
62+ options = [long_option] if short_option is None else [short_option, long_option]
63+ parser.add_argument(*options, dest=dest, required=True, metavar="<DIR>")
64+ 
65+ help_text = parser.format_help()
66+ usage_line = next(line for line in help_text.splitlines() if line.startswith(f" {prog} "))
67+ required_section = help_text.split("Required arguments:\n", 1)[1].split("\n\n", 1)[0]
68+ argument_line = next(line for line in required_section.splitlines() if long_option in line)
69+ 
70+ assert f"{long_option} <DIR>" in usage_line
71+ assert f"{long_option} <DIR>" in argument_line
72+ 
73+ 
74+def test_short_only_option_does_not_render_none():
75+ parser = MindStudioArgumentParser(prog="short_only")
76+ parser.add_argument("-i", metavar="<FILE>", help="Input file")
77+ 
78+ help_text = parser.format_help()
79+ 
80+ assert "None" not in help_text
81+ assert "-i <FILE>" in help_text
82+ 
83+ 
84+def test_parameter_descriptions_follow_terminal_width(monkeypatch):
85+ monkeypatch.setattr(
86+ "msprobe.core.common.cli_help.shutil.get_terminal_size",
87+ lambda fallback=(80, 24): os.terminal_size((60, 24)),
88+ )
89+ parser = MindStudioArgumentParser(prog="narrow_help")
90+ parser.add_argument(
91+ "--name",
92+ help="A deliberately long description that must wrap instead of overflowing a narrow terminal window.",
93+ )
94+ 
95+ help_text = parser.format_help()
96+ optional_section = help_text.split("Optional arguments:\n", 1)[1].split("\n\n", 1)[0]
97+ 
98+ assert max(len(line) for line in optional_section.splitlines()) <= 60
@@ -11,7 +11,7 @@ from msprobe.msprobe import main
11 11 
12 12 
13class TestMsprobeMain(TestCase):13class TestMsprobeMain(TestCase):
14- @patch("msprobe.msprobe.argparse.ArgumentParser")14+ @patch("msprobe.msprobe.MindStudioArgumentParser")
15 def test_main_when_no_args_then_pass(self, mock_arg_parser):15 def test_main_when_no_args_then_pass(self, mock_arg_parser):
16 parser_instance = MagicMock()16 parser_instance = MagicMock()
17 subparsers_instance = MagicMock()17 subparsers_instance = MagicMock()