ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于npu-smi搭建NPU实时可视化监控:从命令解析到Dash面板落地

基于npu-smi搭建NPU实时可视化监控:从命令解析到Dash面板落地 做AI集群运维这两年我最大的感受是npu-smi这条命令本身解决不了监控问题真正让硬件状态可视化的是你在这条命令外面搭的那套东西。我刚负责训练集群那会儿排查NPU故障全靠反复敲npu-smi。卡住了敲一遍看看温度功耗过两分钟再敲一遍看看数值有没有变。碰上多卡训练一台机器八张卡挨个看输出都看得眼花。更头疼的是很多故障是间歇性的——你盯着它的时候一切正常你刚转身离开它就又开始报错。等我终于意识到必须做一套资源监控体系的时候踩过的坑比我预想的多得多。这篇文章就记录一下我是怎么基于npu-smi搭建起一套实时可视化监控的包括命令输出的解析逻辑、Python可视化方案的选型思路、以及落地过程中那些不亲自走一遍很难发现的细节。如果你也在管理带有AI加速卡的服务器节点这篇文章应该能帮你节省至少一周的摸索时间。1. 为什么不能只盯着npu-smi命令行看先说一个很反直觉的结论npu-smi本身输出的信息密度已经很高了大多数人也确实只把它当命令用但这恰恰是监控体系做不起来的核心原因——命令行给你的永远是一个瞬间快照而不是连续状态。1.1 命令行能告诉你的只有此刻npu-smi info这条命令查到的无非是当前时刻的芯片温度、算力利用率、HBM内存占用率、功耗和PCB温度。这些数值本身没有问题问题在于它们只代表执行命令的那一瞬间。我曾遇到过一台训练节点间歇性掉卡表现在日志里就是每隔几个小时出现一次device lost。我蹲在机器前反复敲npu-smi info看到的全是正常数值因为故障发生那几秒钟我恰好没在敲命令。事后回溯才明白那台机器的HBM内存温度在训练任务峰值时会瞬间飙到90度以上触发硬件保护机制后设备掉线但掉线之后温度又迅速回落到75度等你手动敲命令的时候现场早就恢复了。这种情况下命令行工具几乎是无效的。你需要的是持续采集、带时间戳的历史数据这样才能回答出事前的30秒到底发生了什么这个问题。1.2 可视化监控真正解决的问题把npu-smi的输出变成曲线图之后价值完全不一样了。第一是趋势可见。训练任务启动后算力利用率是从0%爬升到95%还是反复在80%到95%之间震荡温度曲线在高负载下是平稳收敛还是持续攀升这些只有曲线图能直观回答。第二是故障回溯。节点宕机后拉出过去一小时的监控曲线一眼就能看到是温度先异常升高、还是功耗先骤降、还是HBM利用率先打满导致OOM。有这个数据排障时间能从小时级压缩到分钟级。第三是多机横向对比。二十台训练节点哪一台的风扇老化、哪一台的硅脂干了反映在曲线上就是同样负载下温度比别人高5-8度。这种横向对比靠命令行逐台敲是比不出来的。第四是关联分析。把NPU的温度、功耗、利用率三条曲线叠加在一起看很多玄学问题都有了答案。比如为什么这个模型跑起来比其他模型慢很可能是因为某个算子触发了降频而降频的诱因是功耗撞墙或温度超阈值——这些信号全藏在曲线关联里。1.3 监控体系的核心组成我做下来的经验一套能用的NPU资源监控体系至少包含四个部分数据采集层定时执行npu-smi命令并解析输出或者直接读取相关系统文件写入时序数据库存储层保存历史数据供回溯和告警使用可视化层把数据渲染成实时刷新的图表面板告警层设置阈值规则异常时通知运维人员从零手搓一个完整的Prometheus生态太重了我的做法是用Python脚本采集数据搭配一个轻量级的可视化方案先把核心链路跑通再逐步叠加功能。下面详细拆解每一层的实现。2. npu-smi输出解析拿到原始数据的第一步不管后面接什么可视化框架第一步永远是先把npu-smi的输出转化成干净的、带时间戳的结构化数据。这一步看似简单但实际操作中有一堆容易翻车的细节。2.1 常用命令与关键参数npu-smi最常用的信息查询命令是npu-smi info输出会列出每张NPU卡的设备信息、健康状态、温度和算力利用率等核心指标适合人工快速看一眼。但如果要做自动化监控我推荐用带扩展参数的查询方式npu-smi info -t board -i 0 -c 0 # 查询板卡信息 npu-smi info -t usages -i 0 -c 0 # 查询利用率 npu-smi info -t temp -i 0 -c 0 # 查询温度 npu-smi info -t power -i 0 -c 0 # 查询功耗其中-i指定芯片编号即第几张卡-c指定芯片序号单芯卡通常为0。每种-t查询的输出格式不同解析逻辑也要分开处理。在实际大规模监控场景中我不会对每张卡单独执行一次npu-smi info去解析——那样太慢了。更高效的做法是执行一次完整输出然后整体切分因为npu-smi的完整输出会把所有卡的信息一次性打出来一次命令调用就能拿到整机的全部数据。2.2 从文本到结构化数据的解析逻辑以npu-smi info的完整输出为例它的排版是固定宽度类型的表格各列对齐且在不同机器上位置一致。解析这种输出用正则逐行匹配比按列偏移量切割更稳定——因为版本升级后列宽可能微调但单位标记和字段名基本不变。我实际用的解析思路是这样的npu-smi info输出大致形如------------------------------------------------------------------------------------------ | NPU Name | Health | Power(W) Temp(C) Hugepages-Usage(page) | | Chip | Bus-Id | AICore(%) Memory-Usage(MB) HBM-Usage(MB) | | 0 | OK | 245.2 68 0 | | 0 | 0000:C1:00:0 | 93.2 31437 / 100664 34730 / 65536 | ------------------------------------------------------------------------------------------Python里可以用一次正则匹配把关键字段全部抓出来import re import subprocess def get_npu_info(): result subprocess.run( [npu-smi, info], capture_outputTrue, textTrue, timeout10 ) return result.stdout def parse_npu_info(text): 解析npu-smi info输出返回结构化列表 devices [] pattern re.compile( r^\|\s*(\d)\s*\|\s*(\d)\s*\|\s*([0-9A-Fa-f:\.])\s*\|\s* r([\d\.])\s*\|\s*([\d\.])\s*\|\s*(\d)\s*/\s*(\d)\s*\|\s* r(\d)\s*/\s*(\d)\s*\|, re.MULTILINE ) for m in pattern.finditer(text): devices.append({ chip_id: int(m.group(1)), # NPU编号 device_id: int(m.group(2)), # 设备编号 bus_id: m.group(3), # 总线地址 power_w: float(m.group(4)), # 功耗 (W) temp_c: float(m.group(5)), # 芯片温度 (℃) aicore_used: int(m.group(6)), # AICore已用 aicore_total: int(m.group(7)), # AICore总量 memory_used_mb: int(m.group(8)), # 内存已用 memory_total_mb: int(m.group(9)), # 内存总量 }) return devices这段代码的思路是定位每一行数据记录的固定格式然后按组提取。注意最后一组HBM内存用的也是已用/总量格式单位是MB但实际显示可能是GB解析时要根据具体输出来决定是否换算。2.3 解析中容易踩的坑第一个坑是不同版本输出格式有差异。昇腾的npu-smi版本迭代时表格列顺序和单位偶尔会变特别是较新版本增加了电压、PCIe速率等字段后正则就必须同步调整。保险做法是先在目标机器上跑一次npu-smi info把原始输出存下来对着实际文本调正则不要凭记忆写。第二个坑是npu-smi执行时间不稳定。在高负载状态下npu-smi本身也会卡顿最坏情况我遇到过命令执行超过5秒才返回。采集脚本里一定要给subprocess加timeout否则监控程序自己会先被拖死。第三个坑是有些机器上npu-smi需要特定权限。普通用户执行时可能只返回部分信息或者直接报错。我的解决方式是给采集脚本配置一个专门的系统用户并做好sudoers白名单配置只允许执行npu-smi命令避免权限过大。3. Python实时可视化方案从数据到图表的完整链路数据解析搞定之后最核心的问题就是怎么把不断更新的数据变成实时刷新的可视化面板。这一步的技术选型会直接影响整个监控体系的稳定性和维护成本。3.1 技术选型为什么不用重型监控框架一开始我也考虑过直接用Prometheus加Grafana。这套组合很成熟但问题在于Prometheus的node_exporter并没有内置npu-smi指标采集器你得自己写exporterGrafana虽然支持数据源接入但整套部署链路对一个小型监控需求来说偏重。我的选择是Python脚本做采集数据写入SQLite然后用Plotly的Dash框架做可视化面板。选Dash而不是Flask配ECharts是因为Dash的图表组件是声明式的回调自动处理数据联动不需要自己写前端代码对做后端的人来说很友好。实测下来刷新频率控制在2秒到3秒完全能跟上监控需求页面平滑不闪烁。3.2 采集端实现定时器与SQLite存储采集端按固定间隔运行采集函数然后把数据插入本地SQLite库。为了方便回溯我建了两张核心表一张存实时快照一张按每小时聚合出统计值。定时任务直接用Python的threading.Timer或schedule库就够不需要引入Celery这类重框架。数据表设计大致如下CREATE TABLE npu_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, chip_id INTEGER, power_w REAL, temp_c REAL, aicore_util REAL, hbm_used_mb INTEGER, hbm_total_mb INTEGER ); CREATE TABLE npu_hourly_agg ( id INTEGER PRIMARY KEY AUTOINCREMENT, hour_start DATETIME, chip_id INTEGER, avg_temp_c REAL, max_temp_c REAL, avg_power_w REAL, max_power_w REAL, avg_aicore_util REAL, max_aicore_util REAL );实时表用于当前状态展示和最近几小时的明细回溯小时聚合表用于长周期趋势分析避免画30天曲线时数据点太多导致图表卡顿。采集脚本核心逻辑import sqlite3 import time from datetime import datetime DB_PATH /var/lib/npu-monitor/npu.db def collect_and_store(): text run_npu_smi() devices parse_npu_info(text) conn sqlite3.connect(DB_PATH) cur conn.cursor() ts datetime.now().isoformat(timespecseconds) for dev in devices: cur.execute( INSERT INTO npu_snapshot (timestamp, chip_id, power_w, temp_c, aicore_util, hbm_used_mb, hbm_total_mb) VALUES (?, ?, ?, ?, ?, ?, ?), ( ts, dev[chip_id], dev[power_w], dev[temp_c], dev[aicore_used] / dev[aicore_total] * 100, dev[memory_used_mb], dev[memory_total_mb], ) ) conn.commit() conn.close() def collector_loop(interval3): while True: try: collect_and_store() except Exception as e: log_error(e) time.sleep(interval)采集间隔我设置为3秒这个频率对温度、功耗类指标来说足够精细对SQLite的写入压力也完全可以接受。如果监控的节点多可以把采集端做成分进程模式每台机器一个独立进程再汇总到统一数据库。3.3 可视化端实现Dash面板的搭建可视化面板的搭建核心就两步第一步查询SQLite数据第二步渲染图表。Dash最方便的地方在于它的回调函数能定时拉取最新数据不用刷新整个页面。这是我选它做实时可视化的最大原因。先看一个最小能跑的面板骨架import dash from dash import dcc, html from dash.dependencies import Input, Output import plotly.graph_objects as go import sqlite3 import pandas as pd app dash.Dash(__name__) app.layout html.Div([ html.H1(NPU 硬件状态实时监控), dcc.Interval(idrefresh-timer, interval2000, n_intervals0), dcc.Graph(idtemp-chart), dcc.Graph(idpower-chart), dcc.Graph(idutil-chart), ]) def load_recent(minutes60): conn sqlite3.connect(/var/lib/npu-monitor/npu.db) df pd.read_sql_query( SELECT timestamp, chip_id, temp_c, power_w, aicore_util, hbm_used_mb, hbm_total_mb FROM npu_snapshot WHERE timestamp datetime(now, ?), conn, params(-{} minutes.format(minutes),), ) conn.close() return df app.callback( [Output(temp-chart, figure), Output(power-chart, figure), Output(util-chart, figure)], [Input(refresh-timer, n_intervals)] ) def update_charts(n): df load_recent(60) fig_temp go.Figure() for chip_id in df[chip_id].unique(): chip_df df[df[chip_id] chip_id] fig_temp.add_trace(go.Scatter( xchip_df[timestamp], ychip_df[temp_c], modelines, namefNPU {chip_id} 温度 )) fig_temp.update_layout( title芯片温度趋势, yaxis_title温度 (℃), margindict(l40, r20, t40, b30) ) # fig_power 和 fig_util 的构建逻辑同理此处省略 return fig_temp, fig_power, fig_util if __name__ __main__: app.run(host0.0.0.0, port8050, debugFalse)2秒刷新一次的温度曲线能看到温度爬坡和回落的完整过程。当训练任务启动时温度从60度升到75度那一段你会看到一条非常陡峭的上升沿这是命令行完全给不了你的体验。3.4 图表布局与多卡展示的设计经验单卡节点只需要画一条温度曲线就够但8卡节点就完全不同了。如果把8张卡的曲线都堆在同一张图里颜色一多就分不清谁是谁。我的做法是把8张卡按两行四列的小提琴图或者子图网格来展示每张卡一个独立小图同时在页面顶部加一个总览统计行显示各卡当前温度、利用率的表格作为快速总览下面再放细分图表。子图网格用Plotly的make_subplots很简单from plotly.subplots import make_subplots def make_grid_figure(df, metric, title, unit): chip_ids sorted(df[chip_id].unique()) n_chips len(chip_ids) n_cols 4 n_rows (n_chips n_cols - 1) // n_cols fig make_subplots( rowsn_rows, colsn_cols, subplot_titles[fNPU {i} for i in chip_ids] ) for idx, chip_id in enumerate(chip_ids): row idx // n_cols 1 col idx % n_cols 1 chip_df df[df[chip_id] chip_id] fig.add_trace( go.Scatter( xchip_df[timestamp], ychip_df[metric], modelines, namefNPU {chip_id}, showlegendFalse, ), rowrow, colcol ) fig.update_layout( titletitle, height200 * n_rows, margindict(l40, r20, t60, b30), ) fig.update_yaxes(title_textunit, row1, col1) return fig这种子图布局在1080p屏幕上刚好能放下4列每张子图大约400像素宽曲线走向看得比较清楚。3.5 单位换算与阈值线可视化不仅仅是画曲线还要加辅助元素。温度图上我会画一条85度的红色虚线作为告警阈值利用率图上画一条20%的绿色虚线作为低利用率提醒。Plotly的add_hline可以很方便地加水平参考线fig_temp.add_hline( y85, line_dashdash, line_colorred, annotation_text告警阈值 85℃, annotation_positiontop right )功耗曲线也建议加上额定功耗参考线比如单卡额定功耗为400W那就在400W处画条橙色虚线。一旦实测功耗在某个时间点突然顶到400W以上你就能立刻想到是不是出现了功耗墙导致的降频问题。4. 监控体系落地过程中的坑与优化前面讲的都是怎么搭起来这一节想写点搭起来之后才发现的问题。这些东西官方文档里不会写但不解决的话监控体系用半个月你就会想把它关掉。4.1 我踩过的几个真实坑第一个坑是SQLite数据库无限膨胀。刚上线时我没做数据清理跑了一周数据库涨到了1.5GB3秒一条的快照数据量太大了。后来加了定时清理任务只保留最近24小时的明细数据超过24小时的数据聚合到小时表后删除。清理脚本用SQL定时执行就行DELETE FROM npu_snapshot WHERE timestamp datetime(now, -1 day);第二个坑是npu-smi命令在采集脚本并发执行时会互相冲突。我一开始用的是多线程并行采集结果发现两个进程同时调用npu-smi会偶发返回空值或者输出截断。后来改成单线程串行采集或者给采集进程加锁这个问题就消失了。实测发现npu-smi自身实现里可能有一个全局锁并发请求会导致部分输出丢失所以采集端还是老老实实串行跑。第三个坑是时区问题。默认datetime.now()拿到的是服务器本地时间如果服务器时区设置成UTC而你的PC是北京时间可视化面板上的时间轴就和实际时间差了8个小时。排查这个问题的时候我一度以为是数据没写入后来才发现是时区显示问题。统一在采集端使用带时区的时间字符串、在展示端指定本地时区转换一处统一之后就不会乱。第四个坑是Dash页面在长时间运行后内存缓慢上涨。这通常跟回调函数里没有释放DataFrame和Figure对象有关尤其是每次都从SQLite读数据、构建新图表的场景。解决方式是在回调函数里处理完数据后显式调用del df, fig并且用gc.collect()回收内存。还有一个技巧是限制load_recent返回的数据量默认只拉最近1小时的数据而不是把整个表的数据都读进来。4.2 监控数据的存储生命周期监控数据的价值会随时间递减但不会归零。我的策略简单来说就是三级生命周期热数据近24小时保存在SQLite明细表用于故障现场回溯保留全部原始采集点温数据近90天只有小时聚合数据用于看长期趋势比如散热性能衰减冷数据超过90天导出成CSV归档到对象存储或NAS基本不再查询但保底留存这套策略实施三个月下来SQLite数据库体量一直控制在200MB以内查询速度基本没有劣化。4.3 告警规则的设定思路监控体系不能只有可视化没有告警。但告警规则设得太激进一天到晚被打扰会让整个系统形同虚设。我调了几轮之后形成了一套自己的策略温度类告警用分级阈值85度触发普通告警90度触发严重告警95度触发紧急告警。普通告警只记录不推消息严重告警才推送到工作群紧急告警直接电话通知利用率类告警用持续判定不是说利用率低于10%就立刻告警而是连续5分钟低于10%才触发避免训练加载间隙的误报功耗类告警用突变判定单卡的功耗在10秒内跳变超过30%大概率是训练任务崩溃或者硬件异常这个规则很灵敏实现告警逻辑不需要额外框架在采集循环里加几个条件判断就行。但如果节点数量多了建议把告警判断拆成一个独立进程读取同一个SQLite库避免采集卡顿影响判定速度。4.4 从单机到多机的扩展思路单机监控跑通之后自然会想到扩展到整个集群。多机扩展我用的方案是每台机器各跑一个采集脚本统一把数据上报到一台中心服务器。上报协议用HTTP POST还是用消息队列都行节点少时HTTP POST最简单直接在采集函数里加一段requests代码就搞定。中心服务器负责写SQLite库和运行Dash面板。这个架构下有个潜在瓶颈如果机器数量超过20台SQLite的写入锁竞争会开始变明显Dash页面的实时查询也会变慢。这个阶段就可以考虑迁移到InfluxDB或者TimescaleDB这类时序数据库数据模型不用大改查询语法稍有变化但核心的采集解析逻辑完全不用动。监控体系这东西永远没有彻底做完的时候。你永远会想再加一张图、再调一条告警规则、再补一个指标。但骨架的搭建必须是稳的、清晰的、可维护的。等到某天凌晨你的手机收到一条温度异常告警你打开面板看到温度曲线在任务启动后一路飙升提前预判到可能出问题并主动停了任务——你会觉得当时一周的辛苦完全值得。从npu-smi的原始输出到一张实时曲线图这套链路说难不难但每一步都有讲究。希望我踩过的这些坑能帮你少走一段弯路。
返回列表