Skip to content

Commit 80ae5ad

Browse files
authored
feat: hardware monitor (#751)
1 parent 8ff44b9 commit 80ae5ad

File tree

21 files changed

+1027
-160
lines changed

21 files changed

+1027
-160
lines changed

docs/硬件信息采集.md

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,98 @@ class MonitorCron:
5959
简单来讲,所有采集器需要返回两部分内容,一部分是硬件整体信息,一部分是对应的硬件信息采集函数列表,前者在不同的采集器中不一样,但是后者需要遵循相同的规范:
6060

6161
1. 如果没什么好采集的,返回空列表
62-
2.
63-
64-
如果有需要采集的,列表内的函数签名应该一致——此函数不接受任何参数,返回一个字典,字典类型为[HardwareInfo](/swanlab/data/run/metadata/hardware/type.py)
62+
2. 如果有需要采集的,采集函数应该来自同一个基类,`__call__`魔术方法签名相同
6563

6664
> HardwareInfo字典类型遵循swanlab创建column的协议,包含列名称、图表配置、组配置等信息
6765
68-
TODO
66+
目前swanlab的硬件信息采集主要依赖于`psutil`库。
67+
68+
### CPU
69+
70+
注意:对于Apple系列的芯片的CPU信息,并不在此处采集和记录。
71+
72+
#### CPU Utilization (%)
73+
74+
代表当前cpu的平均利用率。swanlab为它打了一个 `cpu.pct` 标签。
75+
76+
#### CPU Utilization (per core) (%)
77+
78+
代表当前cpu每个核心的利用率。swanlab为它打了一个 `cpu.{cpu_index}` 标签,其中cpu_index代表cpu的核心编号。
79+
所有核心的利用率将自动在一个图表中展示。
80+
81+
#### Process CPU Threads
82+
83+
代表当前进程的CPU线程数。swanlab为它打了一个 `cpu.thds` 标签。
84+
85+
### Memory
86+
87+
注意:对于Apple系列的芯片的内存信息,并不在此处采集和记录。
88+
89+
#### System Memory Utilization (%)
90+
91+
代表当前系统的内存利用率。swanlab为它打了一个 `mem` 标签。
92+
93+
#### Process Memory In Use (non-swap) (MB)
94+
95+
代表当前进程的内存利用率。swanlab为它打了一个 `mem.proc` 标签。
96+
97+
#### Process Memory In Use (non-swap) (%)
98+
99+
代表当前进程的内存利用率。swanlab为它打了一个 `mem.proc.pct` 标签。
100+
101+
#### Process Memory Available (non-swap) (MB)
102+
103+
代表当前进程的可用内存。swanlab为它打了一个 `mem.proc.avail` 标签。
104+
105+
### Apple SoC
106+
107+
由于Apple SoC可能需要额外适配,因此这部分的cpu、内存信息以及(未来会加上的)GPU信息需要单独采集。注意,当前swanlab只针对M系列芯片做了硬件信息采集适配,早期intel芯片暂无额外调试,可能会存在问题。
108+
就目前而言,Apple的cpu信息、内存信息与上述的[CPU](#cpu)[Memory](#memory)信息相同,标签也相同(因为同出自`psutil`库)。
109+
110+
### Nvidia GPU
111+
112+
如果pynvml库可以识别到Nvidia GPU,swanlab还会采集Nvidia GPU的对应指标,他们的标签类似`gpu.{gpu_index}...`
113+
同指标不同编号的GPU将自动在一个指标图表中展示。
114+
115+
#### GPU Utilization (%)
116+
117+
表示每个GPU的利用率百分比,swanlab为它打了一个 `gpu.{gpu_index}.pct` 标签。
118+
119+
#### GPU Memory Allocated (%)
120+
121+
表示每个GPU的显存利用率百分比,swanlab为它打了一个 `gpu.{gpu_index}.mem.ptc` 标签。
122+
123+
#### GPU Temperature (℃)
124+
125+
表示每个GPU的摄氏温度,swanlab为它打了一个 `gpu.{gpu_index}.temp` 标签。
126+
127+
#### GPU Power Usage (W)
128+
129+
表示每个GPU的功耗,swanlab为它打了一个 `gpu.{gpu_index}.power` 标签。
130+
131+
### Ascend NPU
132+
133+
如果swanlab识别到Ascend NPU,swanlab会采集Ascend NPU的对应指标,他们的标签类似`npu.{npu_index}...`
134+
同指标不同编号的NPU将自动在一个指标图表中展示。
135+
根据Ascend NPU的[官方文档](https://support.huawei.com/enterprise/zh/doc/EDOC1100388864/8c5e18a7)
136+
唯一定位一块计算芯片需要同时知道NPU ID和Chip ID,因此对于Ascend NPU而言,`npu_index = f{npu_id}-{chip_id}`
137+
138+
#### NPU Utilization (%)
139+
140+
表示每个NPU的利用率百分比,swanlab为它打了一个 `npu.{npu_index}.pct` 标签。
141+
142+
#### NPU Memory Allocated (%)
143+
144+
表示每个NPU的HBM利用率百分比,swanlab为它打了一个 `npu.{npu_index}.mem.ptc` 标签。
145+
146+
#### NPU Temperature (℃)
147+
148+
表示每个NPU的摄氏温度,swanlab为它打了一个 `npu.{npu_index}.temp` 标签。
149+
150+
## TODO
151+
152+
在信息采集部分,未来还会上线:
153+
154+
1. 更详细的GPU、NPU信息(利用率、时钟信息等)
155+
2. 更多的硬件信息采集器(如硬盘、网络等)
156+
3. 更多的计算设备支持(如AMD GPU、Google TPU等)

requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
swankit==0.1.2b2
1+
swankit==0.1.2b6
22
swanboard==0.1.7b1
33
cos-python-sdk-v5
44
urllib3>=1.26.0
55
requests>=2.25.0
66
click
77
pyyaml
88
psutil>=5.0.0
9-
gputil==1.4.0
109
pynvml
1110
rich

swanlab/api/upload/model.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from enum import Enum
1212
from typing import List, Optional
1313

14-
from swankit.callback.models import ColumnClass
14+
from swankit.callback.models import ColumnClass, ColumnConfig
1515

1616
from swanlab.data.modules import MediaBuffer
1717

@@ -27,31 +27,33 @@ def __init__(
2727
name: Optional[str],
2828
cls: ColumnClass,
2929
typ: str,
30+
config: Optional[ColumnConfig],
3031
section_name: Optional[str],
3132
section_type: Optional[str],
3233
error: dict = None,
3334
):
3435
"""
35-
Args:
36-
key: 键
37-
name: 键的名称
38-
cls: 键的类别
39-
typ: 键的类型
40-
section_name: 键所在的section的名称
41-
section_type: 键所在的section的类型
42-
error: 错误信息
36+
key: 键
37+
name: 键的名称
38+
cls: 键的类别
39+
typ: 键的类型
40+
config: 键的配置
41+
section_name: 键所在的section的名称
42+
section_type: 键所在的section的类型
43+
error: 错误信息
4344
"""
4445
self.key = key
4546
self.name = name
4647
self.cls = cls
4748
self.typ = typ
49+
self.config = config
4850
self.section_name = section_name
4951
self.section_type = section_type
5052
self.error = error
5153

5254
def to_dict(self):
5355
"""
54-
序列化为Dict
56+
序列化为Dict,传递给后端
5557
"""
5658
d = {
5759
"class": self.cls,
@@ -70,6 +72,19 @@ def to_dict(self):
7072
d.pop("sectionName")
7173
if self.section_type is None:
7274
d.pop("sectionType")
75+
if self.config is None:
76+
return d
77+
# 将额外的图表配置信息加入
78+
if self.config.y_range is not None:
79+
d["yRange"] = self.config.y_range
80+
if self.config.chart_name is not None:
81+
d["chartName"] = self.config.chart_name
82+
if self.config.chart_index is not None:
83+
d["chartIndex"] = self.config.chart_index
84+
if self.config.metric_name is not None:
85+
d["metricName"] = self.config.metric_name
86+
if self.config.metric_color is not None:
87+
d["metricColors"] = self.config.metric_color
7388
return d
7489

7590

swanlab/data/callback_cloud.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def on_column_create(self, column_info: ColumnInfo):
284284
key=column_info.key,
285285
name=column_info.name,
286286
cls=column_info.cls,
287+
config=column_info.config,
287288
typ=column_info.chart_type.value.column_type,
288289
section_name=section_name,
289290
section_type=column_info.section_type,

swanlab/data/run/exp.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ def __init__(self, settings: SwanLabSharedSettings, operator: SwanLabRunOperator
3636
def __add(
3737
self,
3838
key: str,
39-
key_name: Optional[str],
40-
key_class: ColumnClass,
39+
name: Optional[str],
40+
column_class: ColumnClass,
41+
column_config: Optional[ColumnConfig],
4142
section_type: SectionType,
4243
data: DataWrapper,
4344
step: int = None,
@@ -48,19 +49,19 @@ def __add(
4849
----------
4950
key : str
5051
key的云端唯一标识
51-
data : DataWrapper
52-
包装后的数据,用于数据解析
53-
key_class : str
54-
key的类型,CUSTOM为自定义key,SYSTEM为系统key
55-
key_name : str
52+
name : str
5653
key的实际名称
54+
column_class : str
55+
列类型,CUSTOM为自定义key,SYSTEM为系统key
5756
section_type : str
5857
key的组类型
58+
data : DataWrapper
59+
包装后的数据,用于数据解析
5960
step : int, optional
6061
步数,如果不传则默认当前步数为'已添加数据数量+1'
6162
在log函数中已经做了处理,此处不需要考虑数值类型等情况
6263
"""
63-
key_index = f"{key_class}-{key}"
64+
key_index = f"{column_class}-{key}"
6465
# 判断tag是否存在,如果不存在则创建tag
6566
key_obj: SwanLabKey = self.keys.get(key_index, None)
6667

@@ -86,7 +87,15 @@ def __add(
8687
key_obj = SwanLabKey(key, self.settings)
8788
self.keys[key_index] = key_obj
8889
# 新建图表,完成数据格式校验
89-
column_info = key_obj.create_column(key, key_name, key_class, section_type, data, num)
90+
column_info = key_obj.create_column(
91+
key,
92+
name,
93+
column_class,
94+
column_config,
95+
section_type,
96+
data,
97+
num,
98+
)
9099
self.warn_type_error(key_index, key)
91100
# 创建新列,生成回调
92101
self.__operator.on_column_create(column_info)
@@ -104,32 +113,32 @@ def add(
104113
self,
105114
data: DataWrapper,
106115
key: str,
107-
key_name: str = None,
108-
key_class: ColumnClass = 'CUSTOM',
116+
name: str = None,
117+
column_class: ColumnClass = 'CUSTOM',
118+
column_config: Optional[ColumnConfig] = None,
109119
section_type: SectionType = "PUBLIC",
110120
step: int = None,
111-
column_config: Optional[ColumnConfig] = None,
112121
) -> MetricInfo:
113122
"""记录一条新的key数据
114123
Parameters
115124
----------
116125
data : DataWrapper
117126
包装后的数据,用于数据解析
118127
key : str
119-
key的云端唯一标识
120-
key_name : str
121-
key的实际名称, 默认与key相同
122-
key_class : str, optional
123-
key的类型
128+
列的云端唯一标识
129+
name : str
130+
列的实际名称, 默认与key相同
131+
column_class : str, optional
132+
列的类型
133+
column_config : Optional[ColumnConfig], optional
134+
列的额外配置信息
124135
section_type : str, optional
125136
key的组类型
126137
step : int, optional
127138
步数,如果不传则默认当前步数为'已添加数据数量+1'
128139
在log函数中已经做了处理,此处不需要考虑数值类型等情况
129-
column_config : Optional[ColumnConfig], optional
130-
列的额外配置信息
131140
"""
132-
m = self.__add(key, key_name, key_class, section_type, data, step)
141+
m = self.__add(key, name, column_class, column_config, section_type, data, step)
133142
self.__operator.on_metric_create(m)
134143
return m
135144

@@ -291,17 +300,19 @@ def add(self, data: DataWrapper) -> MetricInfo:
291300
def create_column(
292301
self,
293302
key: str,
294-
key_name: Optional[str],
295-
key_class: ColumnClass,
303+
name: Optional[str],
304+
column_class: ColumnClass,
305+
column_config: Optional[ColumnConfig],
296306
section_type: SectionType,
297307
data: DataWrapper,
298308
num: int,
299309
) -> ColumnInfo:
300310
"""
301311
创建列信息,对当前key的基本信息做一个记录
302312
:param key: str, key名称
303-
:param key_name: str, key的实际名称
304-
:param key_class: str, key的类型,CUSTOM为自定义key,SYSTEM为系统key
313+
:param name: str, key的实际名称
314+
:param column_class: str, key的类型
315+
:param column_config: ColumnConfig, key的配置
305316
:param section_type: str, key的组类型
306317
:param data: DataType, 数据
307318
:param num: 创建此列之前的列数量
@@ -320,8 +331,9 @@ def create_column(
320331
column_info = ColumnInfo(
321332
key=key,
322333
kid=str(num),
323-
name=key_name,
324-
cls=key_class,
334+
name=name,
335+
cls=column_class,
336+
config=column_config,
325337
chart_type=result.chart,
326338
section_name=result.section,
327339
section_type=section_type,

swanlab/data/run/helper.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,17 @@ class MonitorCron:
126126
用于定时采集系统信息
127127
"""
128128

129-
SLEEP_TIME = 30
130-
131129
def __init__(self, monitor_func: Callable):
130+
self.count = 0 # 计数器,执行次数
131+
132132
def _():
133133
monitor_func()
134-
self.timer = threading.Timer(self.SLEEP_TIME, _)
134+
self.count += 1
135+
self.timer = threading.Timer(self.sleep_time, _)
135136
self.timer.daemon = True
136137
self.timer.start()
137138

139+
# 立即执行
138140
self.timer = threading.Timer(0, _)
139141
self.timer.daemon = True
140142
self.timer.start()
@@ -143,6 +145,18 @@ def cancel(self):
143145
if self.timer is not None:
144146
self.timer.cancel()
145147

148+
@property
149+
def sleep_time(self):
150+
# 采集10次以下,每次间隔10秒
151+
# 采集10次到50次,每次间隔30秒
152+
# 采集50次以上,每次间隔60秒
153+
if self.count < 10:
154+
return 10
155+
elif self.count < 50:
156+
return 30
157+
else:
158+
return 60
159+
146160

147161
def check_log_level(log_level: Optional[str]) -> str:
148162
"""检查日志等级是否合法"""

0 commit comments

Comments
 (0)