凌晨两点多,值班手机震了一下,某台八卡机器的显存水位告警。我迷迷糊糊登上跳板机,第一件事不是翻日志,也不是去看训练脚本,而是敲下nvidia-smi——看哪张卡顶着、哪个进程在吃显存、温度和功耗正不正常。这条命令大概是所有跟 GPU 打交道的人敲得最多的一条,从刚跑通第一个训练脚本的学生,到维护几百张卡集群的平台工程师,日常都绕不开它。但真要把每一列讲清楚、把参数用在合适的场景、在输出对不上号的时候知道往哪查,很多人其实只有一个模糊印象。
nvidia-smi是随 GPU 驱动一起装进系统的管理工具,全称 System Management Interface。它能干的事远比"看一眼显卡"多:查看每张卡的型号、温度、功耗、风扇转速、显存占用、计算利用率、性能状态、ECC 错误计数、PCIe 链路状态、多卡之间的互联拓扑;能把指定字段按 CSV 吐出来直接喂给监控系统;还能改持久模式、设功耗上限、重置卡。它解决的核心问题是——当你怀疑"是不是卡出问题了"的时候,给你一个不依赖任何框架、不依赖 Python 环境、直接在系统层面取证的手段。这篇内容适合三类人:刚接触 GPU、想搞懂输出里那些数字含义的新手;需要在服务器上排查训练卡顿、显存泄漏、掉卡问题的算法和运维同学;以及想把 GPU 指标接进自有监控体系的平台开发。
1. 先搞清楚这条命令在跟谁对话
1.1 它读的不是显卡本体,是驱动暴露的接口
不少人以为nvidia-smi是直接去问显卡"你几度",其实不是。它是驱动安装包里的一个用户态可执行文件,通过设备节点和内核态驱动模块通信,把驱动维护的统计数据读出来格式化打印。这条链路决定了好几个很实际的结论,理解它们能省下大量瞎折腾的时间。
第一,命令存在不代表驱动正常。系统里留着旧版二进制、但内核模块没加载时,执行会直接报错,提示无法与驱动通信。这句话跟显卡坏没坏关系不大,多数情况是模块没加载、被卸载了,或者内核升级后模块没跟着重新编译。先跑lsmod | grep nvidia看模块在不在,比反复重装驱动有效得多。
第二,首行那个 CUDA Version 最容易误导人。它表示当前驱动能够支持的最高 CUDA 运行时版本,不是机器上实际安装的工具包版本。经常有人看到这里写着 12.x 就以为环境配好了,结果编译时报找不到nvcc。想看实际安装的版本,用nvcc --version,或者去看/usr/local/cuda这个软链接指向哪里。
第三,容器里能不能用,取决于驱动有没有被映射进去。容器共享宿主机内核,所以不需要在容器内再装一套驱动,但运行时必须把设备节点和驱动库挂进来。容器里执行报 "No devices were found",先别怀疑镜像,回宿主机确认卡在不在,再看容器启动参数有没有正确声明 GPU 资源。
1.2 一份默认输出里的六个信息区块
不带任何参数执行,输出是固定版式的表格。很多人盯着它看半天,其实从上到下就是六块内容,拆开来看比一行行猜要快得多。
| 区块位置 | 内容 | 用途 |
|---|---|---|
| 表头第一行 | 工具版本、驱动版本、支持的最高 CUDA 版本 | 判断环境基线 |
| 每卡第 1 行 | 卡序号、型号、持久模式、PCIe 总线地址、是否用于显示、ECC 状态 | 定位物理卡 |
| 每卡第 2 行 | 风扇转速、温度、性能状态、功耗/功耗上限、显存已用/总量、GPU 利用率、计算模式 | 看运行状态 |
| 每卡第 3 行 | MIG 模式 | 判断是否切分了实例 |
| 进程区表头 | GPU、GI ID、CI ID、PID、类型、进程名、显存占用 | 找占用来源 |
| 进程区内容 | 每个使用 GPU 的进程一行 | 定位到具体进程 |
有个细节值得记一下:卡序号(0、1、2……)是驱动枚举出来的逻辑序号,不一定等于机箱里的物理位置,也不一定等于 CUDA 里device 0对应的那张卡。真正稳定的是 UUID 和 PCIe 总线地址。多卡机器加卡、换槽之后,序号可能整体错位,写脚本做映射时用 UUID 才靠谱。我自己就遇到过一次,脚本里硬编码了-i 0,换卡之后每次采到的都是另一张卡的指标,排查了小半天才发现是序号漂移。
1.3 为什么别人的输出和你的长得不一样
新手最容易困惑的一点:网上搜到的输出里有 ECC 那一列、有 NVLink 那一栏,自己机器上却是 N/A 或者压根没有。这不是版本旧,是硬件能力不同。消费级显卡通常不支持 ECC 显存、不支持 MIG 切分、也没有 NVLink 桥,这些列自然显示 N/A 或者干脆不出现。数据中心卡才有完整的一套。
驱动版本高低也会影响字段多少,新驱动会加一些监控项,老驱动没有。开了 MIG 之后,一张卡被切成若干实例,顶部的卡级信息还在,但进程区会多出 GI/CI 两列来标识实例归属。虚拟机里做直通的场景,某些字段同样可能拿不到。所以看到 N/A 先别慌,对照型号确认这个能力本身存不存在,比盲目升级驱动有效。
2. 输出字段逐项拆解
2.1 显存三个数字,以及"为什么释放不掉"
显存那一栏格式是"已用 / 总量",单位 MiB。这是驱动统计的、被各个进程上下文占用的显存总和。围绕这三个数字,实际工作中被问得最多的一句话是:"训练脚本都退出了,为什么显存还没释放?"
先分清两种占用。一种是框架的缓存分配器,比如深度学习框架里的 caching allocator,它会向驱动一次性申请大块显存,然后自己在内部切分复用。进程退出前,这些块即使空闲也不会还给驱动,所以nvidia-smi看到的往往接近峰值而不是实时需求。这种情况下显存"看起来"很高,但程序内部其实还有余量。另一种是真正的进程残留:进程死了但父进程没回收,或者卡在内核态等待,驱动还没来得及清理上下文。
判断办法很朴素:进程区里还有没有对应条目。如果进程列表是空的、显存却仍然被占,就往上查父进程和容器,用fuser -v /dev/nvidia*看谁还开着设备节点,或者lsof过滤一下。还有一种容易忽略的情况——每个 CUDA 进程都要建上下文,这部分开销按驱动版本不同在几百 MB 量级,八卡机器上如果每张卡都挂着几个进程,光是上下文就能吃掉不少显存,进程显存之和对不上总量也就不奇怪了。
提示:显存总量本身也不等于"可用量"。开启 ECC 会保留一部分,MIG 切分后各实例的可用量是重新划分的,看
memory.total时要把这些因素算进去。
2.2 GPU-Util 的真实含义
利用率那一列几乎是误解最多的地方。它的定义是:在过去一个采样周期内,至少有一个 kernel 在 GPU 上执行的时间占比。注意,是"时间占比",不是"算力占用百分比"。所以 100% 只能说明每个采样窗口里都有任务在跑,完全不代表计算单元被打满了;反过来 0% 也不代表这张卡没被占着资源。
实际场景里这两种情况都很常见。一是利用率 100% 但训练速度上不去,这时候瓶颈可能在显存带宽或者核函数本身写得低效,光看这一个数字得不出结论。二是利用率长期在 0% 附近徘徊、显存却占得很高,八成是进程卡在数据加载、进程间通信或者锁等待上,GPU 根本没活干。我见过一次典型的:训练脚本在读一个网络存储上的小文件,DataLoader 的 worker 数量又设得很少,结果每张卡的利用率在 0 和 30 之间来回跳,看着像"卡在摸鱼",实际是 CPU 侧喂不饱。
和它配合看的是utilization.memory这个字段,含义是显存控制器读写时间占比,对访存密集型的任务更有参考价值。默认输出里没有它,需要用--query-gpu单独取。排查性能问题时,把这两个指标加上功耗、SM 时钟放在一起看,比单看一个利用率靠谱得多。
2.3 温度、功耗、时钟与性能状态的联动
温度、功耗、性能状态这三列在同一条线上,不是随便排的,它们之间存在明确的因果链。性能状态(P-State)从 P0 到 P12,P0 是最高性能档,空闲时通常掉到 P8 左右,跑任务时回到 P0。功耗那一栏是"当前功耗 / 功耗上限",当当前功耗贴着上限跑时,卡会主动降频来维持在预算内,这就是所谓的撞功耗墙。
撞墙的证据在哪?不在默认输出里,得用详细查询看降频原因字段。那里会列出空闲、应用设置时钟、软件功耗限制、硬件降速、硬件温度降速、软件温度降速等若干项,哪一项是 Active,就说明卡正被哪个因素压着。温度墙一般出现在散热条件差的机器上,比如风道被线缆挡住、风扇策略保守、机房进风温度偏高。判断标准可以看详细温度信息里的降速阈值和关机阈值,GPU 温度逼近降速阈值时,时钟就会开始往下掉。
功耗墙则更常见于高密度机型。同样一颗芯片,功耗上限设成 300W 和 450W,满载时的持续频率差别不小,而很多机器的默认上限是按整机供电能力配的,不一定适合你的负载。查看详细功耗信息能看到默认上限、当前生效上限和可调范围,需要时可以在有权限的前提下调整。这个操作有风险,改之前务必确认供电和散热余量,别为了跑分把机器搞不稳定。
2.4 进程区里的类型、GI/CI 与对不上的显存
进程区的类型列有三个取值:C 表示计算类(CUDA、OpenCL 之类),G 表示图形渲染,C+G 表示两者兼有。服务器上基本清一色是 C,工作站上跑图形任务时会看到 G。MIG 开启后,GI ID 和 CI ID 用来标识这个进程落在哪个 GPU 实例、哪个计算实例上,做多租户隔离时这两列很关键。
另一个高频疑问是:进程显存加起来为什么和总量对不上?除了前面说的上下文开销,还有几个来源:驱动自身保留、ECC 占用、MIG 实例的固定开销、以及采样瞬间进程正在申请或释放导致的时间差。所以做容量规划时不要拿"进程显存之和"当精确值,留出 10% 到 20% 的余量更稳妥。
3. 高频参数与组合用法
3.1 持续观察:循环刷新与采样间隔
单次输出只是一个瞬间。要看趋势,用循环刷新参数,后面跟秒数,比如每 2 秒刷一次。它会在原地不断重绘,适合盯着看负载爬升的过程。如果要做毫秒级的短时观察,还有毫秒版的循环参数,用来抓那种一闪而过的尖峰。
不过要注意,循环刷新是给人看的,不适合重定向到文件做长期记录——它带大量控制字符,落盘之后基本没法解析。长时间记录应该用查询模式配合自己的循环,这一点在第 4 节会给出完整脚本。另外采样间隔别设得太小,驱动查询本身有开销,几十毫秒一次的高频轮询在多卡机器上会明显增加系统负载。
3.2 结构化查询:把输出变成可解析的数据
这是我认为最值得花时间掌握的一组参数。查询模式允许你只取需要的字段,输出格式支持 CSV,还能去掉表头和单位,直接对接脚本和监控系统。
# 只取关键指标,带表头 nvidia-smi --query-gpu=index,uuid,name,utilization.gpu,memory.used,memory.total,power.draw,temperature.gpu \ --format=csv # 去掉表头和单位,方便程序解析 nvidia-smi --query-gpu=index,uuid,utilization.gpu,memory.used \ --format=csv,noheader,nounits # 查询进程维度的信息 nvidia-smi --query-compute-apps=pid,process_name,used_memory \ --format=csv,noheader,nounits可用字段很多,常用的包括序号、UUID、型号、温度、风扇、利用率、显存、功耗、SM 时钟、显存时钟、性能状态、ECC 错误计数、PCIe 链路代次和宽度、计算模式、MIG 模式、持久模式等。字段名都是点分结构,写脚本时建议先跑一次--help-query-gpu把当前驱动支持的字段列出来,不同版本之间存在差异。
注意:型号字段里偶尔可能包含逗号,解析时别用简单的字符串切分,走标准 CSV 解析器更稳。
3.3 多卡定位与互联拓扑
多卡机器上排查问题,绕不开"这两张卡之间是怎么连的"。拓扑查询会打印一个矩阵,行和列都是卡,交叉点是连接方式。常见取值里,带 NV 前缀表示通过高速互联桥直连,后面的数字是链路数;SYS 表示要跨处理器间互联;PHB 和 PXB 表示经过 PCIe 主机桥或交换芯片;PIX 表示在同一颗交换芯片下;NODE 表示同 NUMA 节点。这个矩阵对分布式训练的性能分析非常有用——如果两张卡本该走高速互联却显示成了跨节点的 PCIe 路径,通信瓶颈几乎必然出现。
配套的还有链路状态查询,能看到每条互联链路的速率和状态。部分机型还支持链路计数器。拓扑信息建议在机器交付验收时就存一份档,后面出现性能异常可以拿来对比,比凭记忆判断可靠。
3.4 平时少用、关键时刻救命的参数
| 参数 | 作用 | 典型场景 | 风险提示 |
|---|---|---|---|
-i | 指定卡号或 UUID | 只看某张卡,脚本里避免序号漂移 | 无 |
-q | 输出详细全量信息 | 排查 ECC、功耗、时钟细节 | 输出很长,建议配合-d限定域 |
-q -d MEMORY | 只看显存域详情 | 分析显存构成 | 无 |
-q -d PERFORMANCE | 看降频原因 | 性能不达预期 | 无 |
-q -d ECC | 看 ECC 错误计数 | 排查静默数据错误 | 无 |
-dmon | 设备级持续采样 | 终端里长时间看趋势 | 输出量大 |
topo -m | 打印互联拓扑矩阵 | 多卡通信调优 | 无 |
-pm 1 | 开启持久模式 | 减少驱动初始化延迟 | 待机功耗略增 |
-r | 重置指定 GPU | 掉卡、异常状态恢复 | 会中断所有在用进程 |
-pl | 设置功耗上限 | 控制整机功耗与散热 | 需权限,改错影响稳定性 |
重置这个操作要单独说一句。它能救回处于异常状态的卡,但会直接干掉卡上所有进程,生产环境里必须确认没有任务在跑,而且要在维护窗口内做。重启之后如果同样的错误反复出现,那基本可以判定是硬件或者散热问题,别再指望软件层解决。
4. 实操:搭一套轻量 GPU 指标采集
4.1 第一版:十行 shell 采样
需求很简单——每 10 秒采一次所有卡的关键指标,追加到 CSV,跑在后台。这个脚本我用了很久,足够应付大多数临时排查。
#!/usr/bin/env bash set -euo pipefail INTERVAL="${1:-10}" OUT="${2:-gpu_metrics.csv}" FIELDS="index,uuid,utilization.gpu,memory.used,memory.total,power.draw,temperature.gpu" [ -f "$OUT" ] || echo "ts,$FIELDS" > "$OUT" while true; do ts=$(date +%s) nvidia-smi --query-gpu="$FIELDS" --format=csv,noheader,nounits \ | sed "s/^/${ts},/" >> "$OUT" sleep "$INTERVAL" done几个设计取舍说明一下。用date +%s而不是格式化时间,是为了后续做时间序列分析时省掉解析成本。用sed给每行加时间戳而不是逐行循环处理,少一次进程创建开销,多卡机器上差异明显。noheader和nounits必须加,否则数值里带着 "MiB" 之类的后缀,入库时还要再做一次清洗。
4.2 第二版:Python 解析并做阈值判断
shell 版本适合快速上手,但要做阈值告警和字段扩展,Python 更顺手。下面这段是核心逻辑,省略了告警发送部分。
import csv import subprocess import time from datetime import datetime FIELDS = [ "index", "uuid", "name", "utilization.gpu", "utilization.memory", "memory.used", "memory.total", "power.draw", "power.limit", "temperature.gpu", "clocks.current.sm", "pstate", ] def collect(): cmd = [ "nvidia-smi", f"--query-gpu={','.join(FIELDS)}", "--format=csv,noheader,nounits", ] raw = subprocess.check_output(cmd, text=True, timeout=5) rows = [] for rec in csv.reader(raw.strip().splitlines()): item = dict(zip(FIELDS, [v.strip() for v in rec])) rows.append(item) return rows def check(rows, mem_threshold=0.9, temp_threshold=80): alerts = [] for r in rows: try: used = float(r["memory.used"]) total = float(r["memory.total"]) temp = float(r["temperature.gpu"]) except ValueError: continue if total and used / total >= mem_threshold: alerts.append(f"GPU{r['index']} 显存水位 {used/total:.0%}") if temp >= temp_threshold: alerts.append(f"GPU{r['index']} 温度 {temp}C") return alerts if __name__ == "__main__": while True: rows = collect() ts = datetime.now().isoformat(timespec="seconds") for r in rows: print(ts, r["index"], r["utilization.gpu"], r["memory.used"]) for msg in check(rows): print("[ALERT]", msg) time.sleep(10)有个坑必须提醒:字段值可能是[N/A]这种带方括号的形式,float()会直接抛异常,所以要么用nounits让驱动输出纯数值,要么在转换前做清洗。我第一次写的时候没注意,某台机器没有风扇传感器,跑到那台就整段崩掉,日志里全是堆栈。
4.3 容器与多用户环境下的注意点
容器化部署现在很普遍,但这里有几个反直觉的地方。第一,环境变量里指定可见设备,只影响 CUDA 应用枚举到哪些卡,不影响nvidia-smi的输出。也就是说容器里限制只给两张卡,敲nvidia-smi仍然会看到全部卡,很多人因此误判"隔离没生效"。第二,进程列表里显示的 PID 是当前命名空间视角的,容器内看到的 PID 和宿主机上不一致,排查占用时最好回宿主机执行一次,对照着找。
还有多用户共用一台机器的情况。不同用户跑的任务混在一张卡上,显存互相挤占,靠人工协调不现实,但至少要做到出问题时能定位。建议在采集脚本里同时记录进程维度的数据,把 PID、进程名、显存占用一起落盘,事后回溯时有据可查。
5. 常见问题与排查实录
5.1 显存占着,进程却找不到
这是被问得最多的一个问题,处理顺序建议固定下来。先看进程区是不是真的空,如果空但显存高,检查是否有容器在跑——容器里的进程在宿主机上是可见的,但如果你在容器内看,可能因为命名空间差异看不到别的容器。再用fuser -v /dev/nvidia*确认设备节点被哪些进程打开,这个手段比nvidia-smi更底层,能抓到一些没被驱动正确登记的残留。
还有一种情况是进程已经变成僵尸,父进程没回收,显存自然释放不了。找到父进程处理掉即可。如果确认没有进程、显存依然不降,那就要考虑驱动状态异常,这时候重置卡是最后手段,重置前把dmesg里最近的驱动相关日志抓下来,那是判断硬件问题的关键证据。
5.2 命令卡住、执行超时、掉卡
nvidia-smi突然不返回、卡在那儿几十秒,通常说明驱动和硬件之间的通信出了问题。这时候别反复敲,先看内核日志里有没有驱动报错记录,重点找带 Xid 字样的行,那个编号是判断故障类型的核心线索。同时确认卡是否还在 PCIe 总线上,用系统自带的总线枚举工具看一眼。
如果是偶发的一两次,重置后恢复正常,可以先观察。如果同一张卡反复出现,基本可以排除软件因素,往供电、散热、插槽接触、卡本身硬件方向查。我处理过一次,某张卡每隔几天就"消失"一次,重置能恢复,最后发现是散热风道被一组电源线挡住,局部温度过高导致降频保护触发。
注意:掉卡之后不要急着重启整机,先抓日志。重启会把现场清掉,等于放弃了唯一的诊断线索。
5.3 利用率与预期对不上
训练慢但利用率高,优先查两处:数据加载是否成为瓶颈,以及核函数效率。判断数据瓶颈的办法是看利用率的时间序列,如果是规律的锯齿状,基本可以确定是在等数据。训练慢且利用率低,先看是不是撞了功耗墙或温度墙,再看进程间通信是否在等。
还有一个隐蔽的场景:多进程共享一张卡,每个进程 batch 很小,kernel 启动开销占比过高,表现出来就是利用率看起来不低但吞吐很差。这时候把 batch 调大或者减少并发进程,效果比优化模型明显。
5.4 一份速查表
| 现象 | 优先检查 | 常见原因 | 处理方向 |
|---|---|---|---|
| 显存高但无进程 | fuser、容器列表 | 残留进程、父进程未回收 | 清理进程或重置卡 |
| 利用率 0 显存高 | 进程状态、通信日志 | 数据等待、死锁 | 查 CPU 侧和网络 |
| 利用率高但速度慢 | 显存带宽、batch 大小 | 访存瓶颈、kernel 开销 | 调 batch、优化算子 |
| 温度高、频率下降 | 降频原因字段 | 散热不足、风道受阻 | 清理风道、调风扇策略 |
| 功耗贴上限 | 功耗详情、降频原因 | 功耗墙 | 评估供电后调整上限 |
| 命令无响应 | 内核日志、总线枚举 | 驱动异常、掉卡 | 抓日志后重置 |
| 容器内看不到卡 | 宿主机设备、启动参数 | 驱动未映射 | 检查容器运行时配置 |
6. 把它当基线,别当全部
用了这么多年,我对这条命令的定位一直很明确:它是排查的起点,不是终点。它的采样粒度比较粗,默认刷新间隔在一秒量级,抓不住毫秒级的波动;它给的是卡级和进程级的聚合数据,看不到 SM 占用、显存带宽利用率、拷贝引擎负载这些更细的指标;在 MIG 或者多进程共享的场景下,进程归属也不总是精确。
所以真正做集群监控时,我会把它当成一条保底链路——任何机器上都能跑,不依赖额外组件,出问题时第一时间有数据可看。要做长期趋势和精细分析,再叠加专门的指标采集组件或者更丰富的终端工具。两层配合,既保证覆盖面,又保证深度。
最后分享一个我养成的小习惯:每次新机器上架,先跑一遍详细查询,把型号、UUID、拓扑、功耗上限、温度阈值这些基线信息存成一份文件,跟机器资产绑定。后面任何一次异常,都可以拿当前状态和基线对比,很多"感觉不对劲"的模糊判断,一对比就变成了明确的结论。这个动作花不了五分钟,但省下来的排查时间,往往是以小时计的。