已合并
【msserviceprofiler】服务化调优支持--help和--version选项 #441
hewenbo创建于 8月15日
【msserviceprofiler】服务化调优支持--help和--version选项 #441
已合并
从已删除 :master合入到Ascend/msserviceprofilermaster
共 4 个文件变更+276-33
| @@ -14,34 +14,27 @@ | |||
| 14 | # See the Mulan PSL v2 for more details. | 14 | # See the Mulan PSL v2 for more details. |
| 15 | # ------------------------------------------------------------------------- | 15 | # ------------------------------------------------------------------------- |
| 16 | 16 | ||
| 17 | -import argparse | 17 | +from ms_service_profiler.cli import create_subcommand_parser, run_parser |
| 18 | -from msservice_advisor.msservice_advisor import advisor | ||
| 19 | -from ms_serviceparam_optimizer.ms_serviceparam_optimizer.optimizer import optimizer | ||
| 20 | -from ms_serviceparam_optimizer.ms_serviceparam_optimizer.train import source_to_train | ||
| 21 | -from ms_service_profiler import compare, split, analyze | ||
| 22 | 18 | ||
| 23 | 19 | ||
| 24 | def main(): | 20 | def main(): |
| 25 | - | 21 | + parser, subparsers = create_subcommand_parser() |
| 26 | - parser = argparse.ArgumentParser( | 22 | + if subparsers is None: |
| 27 | - formatter_class=argparse.ArgumentDefaultsHelpFormatter, | 23 | + return |
| 28 | - description="[MindStudio] msserviceprofiler command line tool" | 24 | + |
| 29 | - ) | 25 | + from msservice_advisor.msservice_advisor import advisor |
| 30 | - subparsers = parser.add_subparsers(help="sub-command help") | 26 | + from ms_serviceparam_optimizer.ms_serviceparam_optimizer.optimizer import optimizer |
| 27 | + from ms_serviceparam_optimizer.ms_serviceparam_optimizer.train import source_to_train | ||
| 28 | + from ms_service_profiler import compare, split, analyze, parse | ||
| 31 | 29 | ||
| 32 | source_to_train.arg_parse(subparsers) | 30 | source_to_train.arg_parse(subparsers) |
| 33 | optimizer.arg_parse(subparsers) | 31 | optimizer.arg_parse(subparsers) |
| 34 | advisor.arg_parse(subparsers) | 32 | advisor.arg_parse(subparsers) |
| 35 | analyze.arg_parse(subparsers) | 33 | analyze.arg_parse(subparsers) |
| 34 | + parse.arg_parse(subparsers) | ||
| 36 | split.arg_parse(subparsers) | 35 | split.arg_parse(subparsers) |
| 37 | compare.arg_parse(subparsers) | 36 | compare.arg_parse(subparsers) |
| 38 | - args = parser.parse_args() | 37 | + run_parser(parser) |
| 39 | - | ||
| 40 | - # run | ||
| 41 | - if hasattr(args, "func"): | ||
| 42 | - args.func(args=args) | ||
| 43 | - else: | ||
| 44 | - parser.print_help() | ||
| 45 | 38 | ||
| 46 | 39 | ||
| 47 | if __name__ == "__main__": | 40 | if __name__ == "__main__": |
| @@ -14,35 +14,37 @@ | |||
| 14 | # See the Mulan PSL v2 for more details. | 14 | # See the Mulan PSL v2 for more details. |
| 15 | # ------------------------------------------------------------------------- | 15 | # ------------------------------------------------------------------------- |
| 16 | 16 | ||
| 17 | -import argparse | ||
| 18 | from importlib.metadata import entry_points | 17 | from importlib.metadata import entry_points |
| 19 | 18 | ||
| 19 | +from ms_service_profiler.cli import create_subcommand_parser, run_parser | ||
| 20 | + | ||
| 20 | 21 | ||
| 21 | def _load_entries(): | 22 | def _load_entries(): |
| 22 | eps = entry_points() | 23 | eps = entry_points() |
| 23 | ep_group = 'ms_service_profiler_plugins' | 24 | ep_group = 'ms_service_profiler_plugins' |
| 24 | plugin_eps = eps.select(group=ep_group) | 25 | plugin_eps = eps.select(group=ep_group) |
| 25 | 26 | ||
| 26 | - yield from (ep.load() for ep in plugin_eps) | 27 | + if plugin_eps: |
| 28 | + yield from (ep.load() for ep in plugin_eps) | ||
| 29 | + return | ||
| 30 | + | ||
| 31 | + from ms_service_profiler import analyze, compare, parse, split | ||
| 32 | + | ||
| 33 | + yield analyze.arg_parse | ||
| 34 | + yield compare.arg_parse | ||
| 35 | + yield parse.arg_parse | ||
| 36 | + yield split.arg_parse | ||
| 27 | 37 | ||
| 28 | 38 | ||
| 29 | -def main(): | 39 | +def main(): |
| 30 | - parser = argparse.ArgumentParser( | 40 | + parser, subparsers = create_subcommand_parser() |
| 31 | - formatter_class=argparse.ArgumentDefaultsHelpFormatter, | 41 | + if subparsers is None: |
| 32 | - description="[MindStudio] msserviceprofiler command line tool" | 42 | + return |
| 33 | - ) | ||
| 34 | - subparsers = parser.add_subparsers(help="sub-command help") | ||
| 35 | 43 | ||
| 36 | for entry_fn in _load_entries(): | 44 | for entry_fn in _load_entries(): |
| 37 | entry_fn(subparsers) | 45 | entry_fn(subparsers) |
| 38 | 46 | ||
| 39 | - args = parser.parse_args() | 47 | + run_parser(parser) |
| 40 | - | ||
| 41 | - # run | ||
| 42 | - if hasattr(args, "func"): | ||
| 43 | - args.func(args=args) | ||
| 44 | - else: | ||
| 45 | - parser.print_help() | ||
| 46 | 48 | ||
| 47 | 49 | ||
| 48 | if __name__ == "__main__": | 50 | if __name__ == "__main__": |
| @@ -0,0 +1,196 @@ | |||
| 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 FITNESS FOR A PARTICULAR PURPOSE. | ||
| 14 | +# See the Mulan PSL v2 for more details. | ||
| 15 | +# ------------------------------------------------------------------------- | ||
| 16 | + | ||
| 17 | +import argparse | ||
| 18 | +import os | ||
| 19 | +import sys | ||
| 20 | +from importlib import metadata | ||
| 21 | +from pathlib import Path | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +TOOL_NAME = "msserviceprofiler" | ||
| 25 | +PACKAGE_NAME = "ms_service_profiler" | ||
| 26 | +REPO_URL = "https://gitcode.com/Ascend/msserviceprofiler" | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +ROOT_HELP = """Description: | ||
| 30 | + Analyze, split, compare, and parse MindStudio Service Profiler data for | ||
| 31 | + service-oriented inference performance tuning. | ||
| 32 | + | ||
| 33 | +Usage: | ||
| 34 | + msserviceprofiler <command> [options] | ||
| 35 | + msserviceprofiler --help | ||
| 36 | + msserviceprofiler --version | ||
| 37 | + | ||
| 38 | +Commands: | ||
| 39 | + analyze Analyze service profiler data and export summary results | ||
| 40 | + parse Parse profiler data into db, csv, or json outputs | ||
| 41 | + split Split prefill/decode request data for targeted analysis | ||
| 42 | + compare Compare performance profiles between two runs | ||
| 43 | + | ||
| 44 | +Optional arguments: | ||
| 45 | + -h, --help Show this help message and exit | ||
| 46 | + -V, --version Show version information and exit | ||
| 47 | + | ||
| 48 | +Examples: | ||
| 49 | + # Parse profiler data into the default ./output directory | ||
| 50 | + msserviceprofiler parse --input-path ./profiling_data | ||
| 51 | + | ||
| 52 | + # Analyze profiler data and save csv/json/db results | ||
| 53 | + msserviceprofiler analyze --input-path ./profiling_data --output-path ./output | ||
| 54 | + | ||
| 55 | + # Compare two profiling runs | ||
| 56 | + msserviceprofiler compare ./profiling_data/before ./profiling_data/after | ||
| 57 | + | ||
| 58 | +Output: | ||
| 59 | + <output-path>/profiler.db | ||
| 60 | + <output-path>/*.csv | ||
| 61 | + <output-path>/*.json | ||
| 62 | + | ||
| 63 | +Troubleshooting: | ||
| 64 | + - Use 'msserviceprofiler <command> --help' to view command-specific options. | ||
| 65 | + - If input data is not found, check that --input-path points to the directory | ||
| 66 | + containing profiler output files. | ||
| 67 | + See also: docs/zh/msserviceprofiler_serving_tuning_instruct.md | ||
| 68 | +""" | ||
| 69 | + | ||
| 70 | + | ||
| 71 | +class RootHelpParser(argparse.ArgumentParser): | ||
| 72 | + def format_help(self): | ||
| 73 | + if self.prog == TOOL_NAME: | ||
| 74 | + return ROOT_HELP | ||
| 75 | + return super().format_help() | ||
| 76 | + | ||
| 77 | + | ||
| 78 | +class VersionAction(argparse.Action): | ||
| 79 | + def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, **kwargs): | ||
| 80 | + help_text = kwargs.pop("help", None) | ||
| 81 | + super().__init__( | ||
| 82 | + option_strings=option_strings, | ||
| 83 | + dest=dest, | ||
| 84 | + nargs=0, | ||
| 85 | + default=default, | ||
| 86 | + help=help_text, | ||
| 87 | + ) | ||
| 88 | + | ||
| 89 | + def __call__(self, parser, namespace, values, option_string=None): | ||
| 90 | + parser._print_message(f"{get_version_text()}\n", sys.stdout) | ||
| 91 | + parser.exit() | ||
| 92 | + | ||
| 93 | + | ||
| 94 | +def create_parser(): | ||
| 95 | + parser = RootHelpParser( | ||
| 96 | + prog=TOOL_NAME, | ||
| 97 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
| 98 | + description="[MindStudio] msserviceprofiler command line tool", | ||
| 99 | + ) | ||
| 100 | + parser.add_argument("-V", "--version", action=VersionAction, help="Show version information and exit") | ||
| 101 | + return parser | ||
| 102 | + | ||
| 103 | + | ||
| 104 | +def is_root_cli_request(argv=None): | ||
| 105 | + argv = sys.argv[1:] if argv is None else argv | ||
| 106 | + return not argv or argv[0] in ("-h", "--help", "-V", "--version") | ||
| 107 | + | ||
| 108 | + | ||
| 109 | +def create_subcommand_parser(): | ||
| 110 | + parser = create_parser() | ||
| 111 | + if is_root_cli_request(): | ||
| 112 | + run_parser(parser) | ||
| 113 | + return parser, None | ||
| 114 | + return parser, parser.add_subparsers(help="sub-command help") | ||
| 115 | + | ||
| 116 | + | ||
| 117 | +def get_version_text(): | ||
| 118 | + dependencies = _format_dependencies() | ||
| 119 | + if dependencies: | ||
| 120 | + dependencies = "\n\nDependencies:\n" + dependencies | ||
| 121 | + | ||
| 122 | + return ( | ||
| 123 | + "=================================================================\n" | ||
| 124 | + " >>>>> MindStudio <<<<<\n" | ||
| 125 | + " THE END-TO-END TOOLCHAIN TO UNLEASH HUAWEI ASCEND COMPUTE\n" | ||
| 126 | + "=================================================================\n" | ||
| 127 | + f"{TOOL_NAME} {_get_package_version()} ({_get_git_commit()})\n" | ||
| 128 | + "Copyright (C) 2026 Huawei Technologies Co., Ltd.\n" | ||
| 129 | + "License: Mulan PSL v2.\n\n" | ||
| 130 | + "Build Info:\n" | ||
| 131 | + f" Date : {_get_build_date()}\n" | ||
| 132 | + f" Repo : {REPO_URL}" | ||
| 133 | + f"{dependencies}" | ||
| 134 | + ) | ||
| 135 | + | ||
| 136 | + | ||
| 137 | +def _get_package_version(): | ||
| 138 | + try: | ||
| 139 | + return metadata.version(PACKAGE_NAME) | ||
| 140 | + except metadata.PackageNotFoundError: | ||
| 141 | + return _read_pyproject_version() | ||
| 142 | + | ||
| 143 | + | ||
| 144 | +def _read_pyproject_version(): | ||
| 145 | + pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" | ||
| 146 | + try: | ||
| 147 | + for line in pyproject.read_text(encoding="utf-8").splitlines(): | ||
| 148 | + stripped = line.strip() | ||
| 149 | + if stripped.startswith("version"): | ||
| 150 | + return stripped.split("=", 1)[1].strip().strip("\"'") | ||
| 151 | + except OSError: | ||
| 152 | + pass | ||
| 153 | + return "unknown" | ||
| 154 | + | ||
| 155 | + | ||
| 156 | +def _get_git_commit(): | ||
| 157 | + repo_dir = Path(__file__).resolve().parents[1] | ||
| 158 | + try: | ||
| 159 | + head = (repo_dir / ".git" / "HEAD").read_text(encoding="utf-8").strip() | ||
| 160 | + if head.startswith("ref:"): | ||
| 161 | + ref_path = repo_dir / ".git" / head.split(" ", 1)[1] | ||
| 162 | + commit = ref_path.read_text(encoding="utf-8").strip() | ||
| 163 | + else: | ||
| 164 | + commit = head | ||
| 165 | + except (IndexError, OSError): | ||
| 166 | + return "unknown" | ||
| 167 | + return commit[:12] if len(commit) >= 7 else "unknown" | ||
| 168 | + | ||
| 169 | + | ||
| 170 | +def _get_build_date(): | ||
| 171 | + return os.environ.get("MS_SERVICE_PROFILER_BUILD_DATE", "unknown") | ||
| 172 | + | ||
| 173 | + | ||
| 174 | +def _format_dependencies(): | ||
| 175 | + deps = [ | ||
| 176 | + ("Pandas", "pandas"), | ||
| 177 | + ("NumPy", "numpy"), | ||
| 178 | + ("OpenTelemetry", "opentelemetry-api"), | ||
| 179 | + ("PyYAML", "PyYAML"), | ||
| 180 | + ] | ||
| 181 | + lines = [] | ||
| 182 | + for display_name, package_name in deps: | ||
| 183 | + try: | ||
| 184 | + dep_version = metadata.version(package_name) | ||
| 185 | + except metadata.PackageNotFoundError: | ||
| 186 | + continue | ||
| 187 | + lines.append(f" {display_name:<13}: {dep_version}") | ||
| 188 | + return "\n".join(lines) | ||
| 189 | + | ||
| 190 | + | ||
| 191 | +def run_parser(parser): | ||
| 192 | + args = parser.parse_args() | ||
| 193 | + if hasattr(args, "func"): | ||
| 194 | + args.func(args=args) | ||
| 195 | + return | ||
| 196 | + parser.print_help(sys.stdout) | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | +import pytest | ||
| 2 | + | ||
| 3 | +from ms_service_profiler.cli import ROOT_HELP, create_parser, get_version_text, run_parser | ||
| 4 | + | ||
| 5 | + | ||
| 6 | +def test_root_help_uses_unified_sections(capsys): | ||
| 7 | + parser = create_parser() | ||
| 8 | + | ||
| 9 | + with pytest.raises(SystemExit) as exc_info: | ||
| 10 | + parser.parse_args(["--help"]) | ||
| 11 | + | ||
| 12 | + assert exc_info.value.code == 0 | ||
| 13 | + output = capsys.readouterr().out | ||
| 14 | + assert "Description:" in output | ||
| 15 | + assert "Usage:" in output | ||
| 16 | + assert "Commands:" in output | ||
| 17 | + assert "Examples:" in output | ||
| 18 | + assert "Troubleshooting:" in output | ||
| 19 | + assert "msserviceprofiler --version" in output | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +def test_root_version_uses_mindstudio_format(): | ||
| 23 | + version_text = get_version_text() | ||
| 24 | + | ||
| 25 | + assert ">>>>> MindStudio <<<<<" in version_text | ||
| 26 | + assert "msserviceprofiler" in version_text | ||
| 27 | + assert "Copyright (C) 2026 Huawei Technologies Co., Ltd." in version_text | ||
| 28 | + assert "License: Mulan PSL v2." in version_text | ||
| 29 | + assert "Build Info:" in version_text | ||
| 30 | + assert "Repo : https://gitcode.com/Ascend/msserviceprofiler" in version_text | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +def test_root_version_action_preserves_multiline_output(capsys): | ||
| 34 | + parser = create_parser() | ||
| 35 | + | ||
| 36 | + with pytest.raises(SystemExit) as exc_info: | ||
| 37 | + parser.parse_args(["--version"]) | ||
| 38 | + | ||
| 39 | + assert exc_info.value.code == 0 | ||
| 40 | + output = capsys.readouterr().out | ||
| 41 | + assert "=================================================================\n" in output | ||
| 42 | + assert "\nBuild Info:\n" in output | ||
| 43 | + assert "\n Repo : https://gitcode.com/Ascend/msserviceprofiler" in output | ||
| 44 | + | ||
| 45 | + | ||
| 46 | +def test_run_parser_without_args_prints_root_help(monkeypatch, capsys): | ||
| 47 | + parser = create_parser() | ||
| 48 | + monkeypatch.setattr("sys.argv", ["msserviceprofiler"]) | ||
| 49 | + | ||
| 50 | + run_parser(parser) | ||
| 51 | + | ||
| 52 | + assert capsys.readouterr().out == ROOT_HELP | ||
🟡 Medium Priority
变更后的根入口
__main__.py使用共享的create_parser(),其返回的RootHelpParser在prog == "msserviceprofiler"时,format_help()直接返回静态的ROOT_HELP(cli.py 第 30-69 行,只列出了 analyze/parse/split/compare 四个 profiler 子命令)。而该根入口在
is_root_cli_request()为 False 时会注册 7 个子命令(第 34-40 行):source_to_train、optimizer、advisor、analyze、parse、split、compare。由此形成不一致:当用户执行
msserviceprofiler --help或msserviceprofiler(不带参数)时,is_root_cli_request()返回 True,直接走run_parser(parser)→print_help→ 返回静态ROOT_HELP,只显示 4 个 profiler 子命令,optimizer、advisor、source_to_train三个真实存在的子命令在帮助中完全消失。失败模式:
test/presmoke/serviceprofiler_param_optimizer.py:50明确使用msserviceprofiler optimizer命令,说明这三个子命令是真实可用的对外功能;旧实现(改动前)在无参数时调用parser.print_help()会动态列出全部子命令,因此这是本 diff 引入的用户可见帮助信息回归——用户无法从--help发现 optimizer/advisor/source_to_train。建议:根入口(组合式 CLI,注册了 source_to_train/optimizer/advisor/analyze/parse/split/compare)不应使用面向 profiler 单包的静态 ROOT_HELP。建议让 create_parser() 支持传入与入口对应的帮助文本(或标志位),使组合式 CLI 的 --help/无参数帮助包含全部子命令;或者在完成子命令注册后再动态生成帮助。至少应在 ROOT_HELP 中补全 optimizer/advisor/source_to_train 的说明。