| @@ -0,0 +1,60 @@ | |||
| 1 | +# ------------------------------------------------------------------------- | ||
| 2 | +# This file is part of the MindStudio project. | ||
| 3 | +# Copyright (c) 2025-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 | +import logging | ||
| 18 | +from typing import Any, Dict, List, Optional | ||
| 19 | + | ||
| 20 | +from .strategy import CollectStrategy | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +logger = logging.getLogger(__name__) | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +class Collector: | ||
| 27 | + def __init__( | ||
| 28 | + self, collect_strategies: Optional[List[CollectStrategy]] = None | ||
| 29 | + ) -> None: | ||
| 30 | + self._collect_strategies: List[CollectStrategy] = [] | ||
| 31 | + if collect_strategies is not None: | ||
| 32 | + try: | ||
| 33 | + self._collect_strategies = list(collect_strategies) | ||
| 34 | + except TypeError: | ||
| 35 | + logger.error( | ||
| 36 | + "collect_strategies must be an iterable. Got %s instead", | ||
| 37 | + collect_strategies, | ||
| 38 | + ) | ||
| 39 | + raise | ||
| 40 | + | ||
| 41 | + if not all( | ||
| 42 | + isinstance(strategy, CollectStrategy) | ||
| 43 | + for strategy in self._collect_strategies | ||
| 44 | + ): | ||
| 45 | + raise TypeError( | ||
| 46 | + "All collect_strategies must be instances of CollectStrategy" | ||
| 47 | + ) | ||
| 48 | + | ||
| 49 | + def add_strategy(self, *, collect_strategy: CollectStrategy) -> None: | ||
| 50 | + if not isinstance(collect_strategy, CollectStrategy): | ||
| 51 | + raise TypeError( | ||
| 52 | + f"collect_strategy must be an instance of CollectStrategy, got {type(collect_strategy)!r} instead" | ||
| 53 | + ) | ||
| 54 | + | ||
| 55 | + self._collect_strategies.append(collect_strategy) | ||
| 56 | + | ||
| 57 | + def collect(self) -> Dict[str, Any]: | ||
【review】【设计】 这里 Collector.collect() 使用 dict comprehension 直接执行所有策略,只要任意一个 strategy.execute() 抛异常,整个采集流程就会中断;但同文件系里的 CollectStrategyGroup.execute() 已经采用了“单策略失败不影响整体”的容错语义。建议两者保持一致,否则后续调用方很难稳定依赖这套新抽象。 【修改建议】
![]() ![]() | |||
| 58 | + return { | ||
| 59 | + strategy.name: strategy.execute() for strategy in self._collect_strategies | ||
| 60 | + } | ||
| @@ -0,0 +1,149 @@ | |||
| 1 | +# ------------------------------------------------------------------------- | ||
| 2 | +# This file is part of the MindStudio project. | ||
| 3 | +# Copyright (c) 2025-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. | ||
【review】【资料】第6行注释中存在拼写错误 "terms and conditions of" 应为 "terms and conditions of the"。 代码建议:
![]() ![]() | |||
| 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 | +import itertools | ||
| 18 | +import logging | ||
| 19 | +import os | ||
| 20 | +import re | ||
| 21 | +import shutil | ||
| 22 | +import stat | ||
| 23 | +import subprocess | ||
| 24 | +from enum import Enum | ||
| 25 | +from typing import Optional | ||
| 26 | + | ||
| 27 | +try: | ||
| 28 | + from importlib.metadata import PackageNotFoundError, version | ||
| 29 | +except ImportError: | ||
| 30 | + from importlib_metadata import PackageNotFoundError, version | ||
| 31 | + | ||
| 32 | +from packaging.version import InvalidVersion, Version | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +LOG_LEVELS = { | ||
| 36 | + "debug": logging.DEBUG, | ||
| 37 | + "info": logging.INFO, | ||
| 38 | + "warning": logging.WARNING, | ||
| 39 | + "error": logging.ERROR, | ||
| 40 | + "critical": logging.CRITICAL, | ||
| 41 | +} | ||
| 42 | +LOG_FORMAT = "%(levelname)-5s %(asctime)s [%(filename)s:%(lineno)d] %(message)s" | ||
| 43 | + | ||
| 44 | + | ||
| 45 | +logger = logging.getLogger(__name__) | ||
| 46 | + | ||
| 47 | + | ||
| 48 | +class NpuType(Enum): | ||
| 49 | + d100 = "300" | ||
| 50 | + d500 = "300I_DUO" | ||
| 51 | + d801 = "800I A1" | ||
| 52 | + d802 = "800I A2" | ||
| 53 | + d803 = "800I A3" | ||
| 54 | + UNKNOWN = "unknown" | ||
| 55 | + | ||
| 56 | + | ||
| 57 | +class Framework(Enum): | ||
| 58 | + MINDIE = "mindie" | ||
| 59 | + VLLM = "vllm" | ||
| 60 | + SGLANG = "sglang" | ||
| 61 | + UNKNOWN = "unknown" | ||
| 62 | + | ||
| 63 | + | ||
| 64 | +def get_pkg_version(pkg_name: str) -> Optional[Version]: | ||
【review】【正确性】 代码建议:
-- love from glm5 ![]() ![]() | |||
| 65 | + try: | ||
| 66 | + pkg_version = version(pkg_name) | ||
| 67 | + except PackageNotFoundError: | ||
| 68 | + return None | ||
| 69 | + | ||
| 70 | + try: | ||
| 71 | + return Version(pkg_version) | ||
| 72 | + except InvalidVersion: | ||
| 73 | + logger.warning( | ||
| 74 | + "Got invalid version '%s' from package '%s'", pkg_version, pkg_name | ||
| 75 | + ) | ||
| 76 | + return None | ||
| 77 | + | ||
| 78 | + | ||
| 79 | +def get_npu_count() -> int: | ||
| 80 | + """Count Davinci NPU character devices present on the machine.""" | ||
| 81 | + template = "/dev/davinci{}" | ||
| 82 | + for device_id in itertools.count(0): | ||
| 83 | + try: | ||
| 84 | + mode = os.stat(template.format(device_id)).st_mode | ||
| 85 | + except OSError: | ||
| 86 | + # Device path doesn't exist: we've counted all devices. | ||
【review】【可维护性】第91行注释中存在拼写错误 "doesn't" 应为 "doesn't"。 代码建议:
![]() ![]() | |||
| 87 | + return device_id | ||
| 88 | + if not stat.S_ISCHR(mode): | ||
| 89 | + # Path exists but is not a character device: stop here. | ||
| 90 | + return device_id | ||
| 91 | + return 0 # unreachable; satisfies type checkers | ||
| 92 | + | ||
| 93 | + | ||
| 94 | +def get_npu_type() -> NpuType: | ||
| 95 | + """Detect NPU type via lspci accelerator entries.""" | ||
| 96 | + lspci = shutil.which("lspci") | ||
| 97 | + if not lspci: | ||
| 98 | + return NpuType.UNKNOWN | ||
| 99 | + | ||
| 100 | + try: | ||
| 101 | + output = subprocess.check_output([lspci], stderr=subprocess.DEVNULL, text=True) | ||
| 102 | + except Exception: | ||
| 103 | + logger.exception("Failed to execute lspci") | ||
| 104 | + return NpuType.UNKNOWN | ||
| 105 | + | ||
| 106 | + device_pattern = re.compile(r"device\s*(\w+)", re.IGNORECASE) | ||
| 107 | + devices = [] | ||
| 108 | + for line in output.splitlines(): | ||
| 109 | + if "accelerator" not in line.lower(): | ||
| 110 | + continue | ||
| 111 | + m = device_pattern.search(line) | ||
| 112 | + if m: | ||
| 113 | + devices.append(m.group(1)) | ||
| 114 | + | ||
| 115 | + if not devices: | ||
| 116 | + return NpuType.UNKNOWN | ||
| 117 | + | ||
| 118 | + first = devices[0] | ||
| 119 | + if first not in NpuType.__members__ or any(d != first for d in devices): | ||
| 120 | + logger.debug("Inconsistent or unrecognised device types: %s", devices) | ||
| 121 | + return NpuType.UNKNOWN | ||
| 122 | + | ||
【review】【正确性】 代码建议:
-- love from glm5 ![]() ![]() | |||
| 123 | + return NpuType[first] | ||
| 124 | + | ||
| 125 | + | ||
| 126 | +def get_npu_memory() -> Optional[int]: | ||
T [review] [规范] 第 77 行 get_npu_memory() 返回值类型为 Optional[int],但注释写的是 # MB,而 NpuInfoSection.get_info() 中直接做整除 npu_memory // 1024 时若 npu_memory 为 None 会抛出 TypeError;修改建议:添加 None 保护,参考代码如下: def get_info(self) -> str: npu_type = get_npu_type() npu_count = get_npu_count() npu_memory = get_npu_memory() memory_str = f"{npu_memory // 1024}K" if npu_memory is not None else "Unknown" return f"NPU: {npu_type.value} ({npu_count} x {memory_str})" ![]() ![]() | |||
| 127 | + """Return High-Bandwidth Memory capacity (MB) for device 0, or None if unavailable.""" | ||
| 128 | + npu_smi = shutil.which("npu-smi") | ||
| 129 | + if not npu_smi: | ||
| 130 | + return None | ||
| 131 | + | ||
| 132 | + try: | ||
| 133 | + output = subprocess.check_output( | ||
| 134 | + [npu_smi, "info", "-i", "0", "-t", "memory"], | ||
| 135 | + stderr=subprocess.DEVNULL, | ||
| 136 | + text=True, | ||
| 137 | + ) | ||
| 138 | + except Exception: | ||
| 139 | + logger.exception("Failed to execute npu-smi") | ||
| 140 | + return None | ||
| 141 | + | ||
| 142 | + for line in output.splitlines(): | ||
【Review】【正确性】 这里解析 npu-smi 输出时用了 value.isdigit(),但 HBM Capacity 的值通常会带单位,例如 65536 MB。这种情况下当前实现会一直返回 None,导致 NPU 内存信息无法正确展示。建议改成提取数值部分,这样兼容真实命令输出。 【修改建议】
![]() ![]() | |||
| 143 | + if "HB" + "M Capacity" not in line or ":" not in line: | ||
| 144 | + continue | ||
| 145 | + value = line.split(":")[-1].strip() | ||
| 146 | + if value.isdigit(): | ||
| 147 | + return int(value) | ||
| 148 | + | ||
| 149 | + return None | ||


【Review】【正确性】 banner.py (line 23) 这里从 ..core.strategy 导入了 Ascend,但当前 PR 新增的 strategy.py 中并没有这个类定义。这样会导致 msprechecker.commands.banner 在导入阶段直接失败,banner 功能无法使用。建议先补齐 Ascend 策略实现,或者继续复用现有 AscendCollector,这样可以避免引入导入时崩溃。 【修改建议】