已合并
【feature】统一帮助信息规范 #953
kun_8创建于 8月15日
【feature】统一帮助信息规范 #953
已合并
共 13 个文件变更+643-115
| @@ -15,11 +15,11 @@ | |||
| 15 | # ------------------------------------------------------------------------- | 15 | # ------------------------------------------------------------------------- |
| 16 | 16 | ||
| 17 | import argparse | 17 | import argparse |
| 18 | -import sys | ||
| 19 | import json | 18 | import json |
| 20 | import os | 19 | import os |
| 21 | from msprobe.core.common.const import Const | 20 | from msprobe.core.common.const import Const |
| 22 | from msprobe.core.common.file_utils import check_file_or_directory_path | 21 | from msprobe.core.common.file_utils import check_file_or_directory_path |
| 22 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | def _detect_framework_from_api_info(api_info_path: str) -> str: | 25 | def _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 | return | 72 | 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_command | 83 | 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_argument | 97 | 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_main | 98 | 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 | return | 128 | 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_ut | 140 | # PyTorch 多进程路径:沿用原来的 prepare_config + run_parallel_ut |
| 136 | from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import _acc_check_parser | 141 | 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_ut | 142 | 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_argument | 165 | 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_main | 166 | 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 | + | ||
| 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)}" | ||
| 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: | ||
| 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 | ||
| 17 | import os | 18 | import os |
| 18 | import sys | 19 | import sys |
| 19 | import math | 20 | import math |
| 20 | -import argparse | ||
| 21 | import ast | 21 | import ast |
| 22 | import heapq | 22 | import heapq |
| 23 | from abc import ABC | 23 | from abc import ABC |
| @@ -26,8 +26,14 @@ from typing import List | |||
| 26 | 26 | ||
| 27 | from msprobe.core.common.const import MonitorConst | 27 | from msprobe.core.common.const import MonitorConst |
| 28 | from msprobe.core.common.log import logger | 28 | from 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_json | 30 | +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 | ||
| 33 | class ScanRule(ABC): | 39 | class ScanRule(ABC): |
| @@ -64,7 +70,6 @@ class AnomalyNan(ScanRule): | |||
| 64 | 70 | ||
| 65 | 71 | ||
| 66 | class AnomalyScanner: | 72 | class AnomalyScanner: |
| 67 | - | ||
| 68 | 73 | ||
| 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 = name2callid | 128 | 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 = 0 | 144 | 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 = anomalies | 301 | 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 e | 331 | + 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_number | 334 | 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, anomalies | 344 | + 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 | ||
| 363 | def _get_parse_args(): | 351 | def _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, | |||
| 32 | from msprobe.core.install_deps.install_deps import _install_deps_parser, install_deps_cli | 32 | from msprobe.core.install_deps.install_deps import _install_deps_parser, install_deps_cli |
| 33 | from msprobe.core.parse.parse_cli import _parse_parser, parse_cli | 33 | from msprobe.core.parse.parse_cli import _parse_parser, parse_cli |
| 34 | from msprobe.core.common.logo import CliLogo | 34 | from msprobe.core.common.logo import CliLogo |
| 35 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 35 | 36 | ||
| 36 | 37 | ||
| 37 | def main(): | 38 | def 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 | ||
| 19 | import argparse | 20 | import argparse |
| 20 | import os | 21 | import os |
| @@ -58,6 +59,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory, get_js | |||
| 58 | from msprobe.pytorch.common.log import logger | 59 | from msprobe.pytorch.common.log import logger |
| 59 | from msprobe.pytorch.dump.pt_config import parse_json_config | 60 | from msprobe.pytorch.dump.pt_config import parse_json_config |
| 60 | from msprobe.core.common.const import Const, FileCheckConst, CompareConst | 61 | from msprobe.core.common.const import Const, FileCheckConst, CompareConst |
| 62 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 61 | from msprobe.core.common.utils import safe_get_value, CompareException, is_int, check_op_str_pattern_valid | 63 | from msprobe.core.common.utils import safe_get_value, CompareException, is_int, check_op_str_pattern_valid |
| 62 | from msprobe.core.common.output_postprocess.processor import postprocess_output, should_postprocess_output | 64 | from msprobe.core.common.output_postprocess.processor import postprocess_output, should_postprocess_output |
| 63 | from msprobe.pytorch.common.utils import seed_all | 65 | from msprobe.pytorch.common.utils import seed_all |
| @@ -474,7 +476,7 @@ def preprocess_forward_content(forward_content): | |||
| 474 | 476 | ||
| 475 | def _acc_check(parser=None): | 477 | def _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 @@ | |||
| 19 | import subprocess # nosec | 19 | import subprocess # nosec |
| 20 | import os | 20 | import os |
| 21 | import sys | 21 | import sys |
| 22 | -import argparse | ||
| 23 | import time | 22 | import time |
| 24 | import signal | 23 | import signal |
| 25 | import threading | 24 | import threading |
| @@ -38,6 +37,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory, load_j | |||
| 38 | from msprobe.core.common.file_utils import remove_path | 37 | from msprobe.core.common.file_utils import remove_path |
| 39 | from msprobe.core.common.const import FileCheckConst, Const | 38 | from msprobe.core.common.const import FileCheckConst, Const |
| 40 | from msprobe.core.common.utils import CompareException | 39 | from msprobe.core.common.utils import CompareException |
| 40 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 41 | 41 | ||
| 42 | 42 | ||
| 43 | def split_json_file(input_file, num_splits, filter_api): | 43 | def split_json_file(input_file, num_splits, filter_api): |
| @@ -269,7 +269,7 @@ def prepare_config(args): | |||
| 269 | def main(): | 269 | def 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 argparse | 20 | +import math |
| 20 | import os | 21 | import os |
| 21 | import sys | 22 | import sys |
| 22 | 23 | ||
| @@ -32,13 +33,14 @@ import torch | |||
| 32 | from tqdm import tqdm | 33 | from tqdm import tqdm |
| 33 | from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import generate_device_params, get_api_info | 34 | from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check import generate_device_params, get_api_info |
| 34 | from msprobe.pytorch.api_accuracy_checker.acc_check.acc_check_utils import exec_api, is_unsupported_api, ExecParams | 35 | from 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, FileChecker | 36 | +from msprobe.core.common.file_utils import FileChecker |
| 36 | from msprobe.pytorch.api_accuracy_checker.common.utils import extract_basic_api_segments | 37 | from msprobe.pytorch.api_accuracy_checker.common.utils import extract_basic_api_segments |
| 37 | from msprobe.core.common.const import FileCheckConst, Const | 38 | from msprobe.core.common.const import FileCheckConst, Const |
| 38 | from msprobe.core.common.utils import check_op_str_pattern_valid | 39 | from msprobe.core.common.utils import check_op_str_pattern_valid |
| 39 | from msprobe.pytorch.common.log import logger | 40 | from msprobe.pytorch.common.log import logger |
| 40 | from msprobe.pytorch.common.parse_json import parse_json_info_forward_backward | 41 | from msprobe.pytorch.common.parse_json import parse_json_info_forward_backward |
| 41 | from msprobe.core.common.decorator import recursion_depth_decorator | 42 | from msprobe.core.common.decorator import recursion_depth_decorator |
| 43 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 42 | 44 | ||
| 43 | 45 | ||
| 44 | def check_tensor_overflow(x): | 46 | def 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 | - # inf | 54 | + 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 True | 56 | + return math.isinf(x) or math.isnan(x) |
| 55 | - # nan | 57 | + 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 | 60 | ||
| @@ -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_device | 125 | 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 | return | 135 | 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 | return | 140 | 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 | ||
| 153 | def _run_overflow_check_parser(parser): | 151 | def _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 | ||
| 164 | def _run_overflow_check(parser=None): | 179 | def _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): | |||
| 173 | def _run_overflow_check_command(args): | 188 | def _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 | ||
| 20 | import os | 19 | import os |
| 21 | import sys | 20 | import sys |
| 22 | from collections import namedtuple | 21 | from collections import namedtuple |
| @@ -44,6 +43,7 @@ from msprobe.core.common.file_utils import FileChecker, create_directory | |||
| 44 | from msprobe.pytorch.common.log import logger | 43 | from msprobe.pytorch.common.log import logger |
| 45 | from msprobe.core.common.utils import CompareException, check_op_str_pattern_valid | 44 | from msprobe.core.common.utils import CompareException, check_op_str_pattern_valid |
| 46 | from msprobe.core.common.const import Const, CompareConst, FileCheckConst | 45 | from msprobe.core.common.const import Const, CompareConst, FileCheckConst |
| 46 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 47 | 47 | ||
| 48 | CompareConfig = namedtuple('CompareConfig', ['npu_csv_path', 'gpu_csv_path', 'result_csv_path', 'details_csv_path']) | 48 | CompareConfig = namedtuple('CompareConfig', ['npu_csv_path', 'gpu_csv_path', 'result_csv_path', 'details_csv_path']) |
| 49 | BenchmarkInfNanConsistency = namedtuple( | 49 | BenchmarkInfNanConsistency = namedtuple( |
| @@ -429,7 +429,7 @@ def record_thousandth_threshold_result(input_data): | |||
| 429 | 429 | ||
| 430 | def _api_precision_compare(parser=None): | 430 | def _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 | ||
| 17 | from __future__ import annotations | 17 | from __future__ import annotations |
| 18 | 18 | ||
| 19 | -import argparse | ||
| 20 | import json | 19 | import json |
| 21 | import os | 20 | import os |
| 22 | import re | 21 | import re |
| @@ -26,13 +25,15 @@ from dataclasses import dataclass | |||
| 26 | from functools import lru_cache | 25 | from functools import lru_cache |
| 27 | from transformers import AutoTokenizer | 26 | from transformers import AutoTokenizer |
| 28 | from msprobe.core.common.file_utils import check_path_exists, save_json | 27 | from msprobe.core.common.file_utils import check_path_exists, save_json |
| 28 | +from msprobe.core.common.cli_help import MindStudioArgumentParser | ||
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | def parse_args(): | 31 | def 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文件里对应的key | 39 | 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 package | 40 | # footnote for creating run package |
| 41 | CREATE_RUN_SCRIPT=${MAKESELF_DIR}/makeself.sh | 41 | CREATE_RUN_SCRIPT=${MAKESELF_DIR}/makeself.sh |
| 42 | 42 | ||
| 43 | -# footnote for controling params | 43 | +# footnote for controlling params |
| 44 | CONTROL_PARAM_SCRIPT=${MAKESELF_DIR}/makeself-header.sh | 44 | CONTROL_PARAM_SCRIPT=${MAKESELF_DIR}/makeself-header.sh |
| 45 | 45 | ||
| 46 | # store run package | 46 | # store run package |
| @@ -226,4 +226,3 @@ function main() { | |||
| 226 | parse_script_args "$@" | 226 | parse_script_args "$@" |
| 227 | 227 | ||
| 228 | main | 228 | main |
| 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 | ||
| 13 | class TestMsprobeMain(TestCase): | 13 | class 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() |


问题
python/msprobe/core/common/cli_help.py:363原因
触发条件:对使用 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行')。已用复现脚本确认:',',渲染后帮助中实际出现 "-i, None "。
long_name = f"{long_name} {_metavar(action)}"把 None 经 f-string 插值为字符串 "None",_signature 最终返回 ('-i,', 'None-i→ 'None-n→ '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”。当前实现使占位符丢失,违背统一帮助格式的目标。
效果